diff --git a/.env.example b/.env.example index 976335e..69e6e7f 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,17 @@ DEFAULT_LLM_PROVIDER="openai" # 其他可选模型密钥 ANTHROPIC_API_KEY="" GOOGLE_API_KEY="" +# google-genai 官方别名;同时设置时 GOOGLE_API_KEY 优先 +# GEMINI_API_KEY="" +# Vertex AI ADC(不使用 GOOGLE_API_KEY 时启用) +# GOOGLE_GENAI_USE_VERTEXAI="true" +# GOOGLE_CLOUD_PROJECT="your-project-id" +# GOOGLE_CLOUD_LOCATION="us-central1" +# GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/service-account.json" SILICONFLOW_API_KEY="" QWEN_API_KEY="" +# Ark API key(与 AccessKey/SecretKey 二选一) +ARK_API_KEY="" VOLC_ACCESS_KEY="" VOLC_SECRET_KEY="" JINA_API_KEY="" @@ -21,8 +30,3 @@ GROK_API_KEY="" # 默认嵌入模型已在 config.toml 中设置为 local-hash。 # 如需切换,请在这里覆盖 DEFAULT_EMBEDDING_PROVIDER 或在 config.toml 中修改。 # DEFAULT_EMBEDDING_PROVIDER="local-hash" - -# iflow 示例 -# OPENAI_API_KEY="sk-your-iflow-key" -# OPENAI_API_BASE="https://apis.iflow.cn/v1" -# DEFAULT_LLM_PROVIDER="iflow-qwen3-max" diff --git a/.gitignore b/.gitignore index d101b87..f56791b 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,8 @@ htmlcov/ *wheels/ *.egg *instance/ +# PyInstaller 构建产物(scripts/build_binary_release.py 生成后即删除,不应入库) +*.spec # 虚拟环境 /.env* !/.env.example diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index ed27fed..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "dify"] - path = dify - url = https://github.com/langgenius/dify.git diff --git a/AGENTS.md b/AGENTS.md index fc3942d..5db3a3c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,32 +1,66 @@ -# 仓库指南 (Repository Guidelines) +# Repository Guidelines -## 项目结构与模块组织 (Project Structure & Module Organization) +本指南适用于仓库根目录的 PyRAG-Kit。项目要求 Python 3.11 或更高版本,使用 `uv` 管理依赖。 -本仓库是一个 Python 3.11+ 的 RAG 工具集。核心代码位于 `src/` 目录下,按职责拆分为以下主要模块: -`src/chat/`、`src/providers/`、`src/retrieval/`、`src/etl/`、`src/ui/` 和 `src/utils/`。入口程序和脚本位于 -`main.py` 和 `scripts/`。测试代码位于 `tests/`,其目录结构与源码对应。运行产物(如 `data/` 目录和生成的日志)不应提交至版本控制。 +## Project Structure & Module Organization -## 构建、测试与开发命令 (Build, Test, and Development Commands) +- `main.py` 是交互式入口;`src/` 按职责划分为 `chat/`、`etl/`、`models/`、`providers/`、`retrieval/`、`retrieval_test/`、`runtime/`、`services/`、`ui/` 和 `utils/`。 +- `scripts/` 放置知识库快照构建、发布打包和发布说明提取脚本;`tests/` 按源码模块组织 pytest 测试。 +- `knowledge_base/` 保存本地 Markdown 原文;`data/kb/` 和 `data/logs/` 保存运行产物,均不应提交。仓库不再包含 Dify 上游子模块,Dify 相关逻辑以 `src/` 中的移植代码维护。 +- 核心流程为 ETL -> embedding -> FAISS 快照 -> 混合检索 -> provider-backed chat。 -- `uv sync`:根据 `pyproject.toml` 安装依赖并创建本地环境。 -- `uv run main.py`:启动应用程序。 -- `uv run python -m scripts.embed_knowledge_base`:重建知识库快照。 -- `uv run pytest`:运行完整测试套件。 -- `uv run pytest tests/test_config.py`:在迭代特定更改时运行单个测试文件。 +## Build, Test, and Development Commands -## 编码风格与命名规范 (Coding Style & Naming Conventions) +```bash +uv sync +uv sync --group dev +uv run main.py --smoke-test +uv run main.py +uv run python -m scripts.embed_knowledge_base --mode standard +uv run pytest +uv run pytest tests/providers/test_factory.py +uv run python scripts/build_binary_release.py --target macos-arm64 --validate +``` -遵循现有的 Python 风格:使用 4 空格缩进,公开函数必须包含明确的类型标注,命名应具有描述性。 -函数、变量和模块使用 `snake_case`;类使用 `PascalCase`;常量使用 `UPPER_CASE`。保持文档字符串(docstrings)和注释简短且客观。修改代码时应贴合周围代码风格,避免引入新的格式化样式。 +`standard` 也可替换为 `hierarchical`。发布目标支持 `windows-x64`、`macos-x64`、`macos-arm64`、`linux-x64` 和 `linux-arm64`;标签 `v*` 会触发 GitHub Release 工作流。 +发布脚本不做跨平台交叉编译,`--target` 必须匹配当前主机或 CI runner 的系统与 CPU 架构;目标不匹配时会在清理构建产物前显式失败。 -## 测试指南 (Testing Guidelines) +## Quality Checks -所有自动化测试统一使用 `pytest`。测试文件命名为 `test_*.py`,测试函数命名为 `test_*`。建议将针对特定行为的测试放在该行为相关的代码附近,特别是对于配置解析、提供商工厂、ETL 和检索逻辑。项目没有强制的覆盖率阈值;针对每次行为变更都应补充测试,并优先运行最小相关测试集。 +开发依赖包含 Ruff、Bandit 和 MyPy。提交前可运行: -## 提交与拉取请求指引 (Commit & Pull Request Guidelines) +```bash +uv run ruff check main.py src scripts tests +uv run bandit -r main.py src scripts -ll +uv run mypy --cache-dir /tmp/pyrag-kit-mypy main.py src +uv run python -m compileall -q main.py src scripts tests +uv lock --check +git diff --check +``` -Git 提交历史应使用约定式前缀,如 `feat:`、`refactor:`、`test:` 和 `chore:`,后接简短摘要。提交信息应具体且使用祈使句。拉取请求(PR)需说明改动内容,列出验证命令,并注明是否对配置或数据文件产生影响。仅在 UI 输出发生变化时附带截图。 +MyPy 使用任务专用缓存目录,避免多个进程共享 `.mypy_cache` 造成锁等待;优先修复本次改动引入的问题,并记录未覆盖的历史基线告警。 -## 安全与配置提示 (Security & Configuration Tips) +## Coding Style & Naming Conventions -严禁提交密钥或本地覆盖配置。请复制 `config.toml.example` 为本地 `config.toml`,并使用 `.env` 文件处理私密数值。`config.toml` 为本地文件,默认不纳入版本控制。将 `data/` 目录和生成的日志视为本地产物。若在 `dify/` 子目录下工作,请先阅读该目录下的 `AGENTS.md`,因为子目录指令优先于此根目录指南。 +使用 4 个空格缩进、明确的公开函数类型标注和简短客观的 docstring。函数、变量和模块使用 `snake_case`,类使用 `PascalCase`,常量使用 `UPPER_CASE`。项目使用 Ruff 做 lint 检查,未配置独立 formatter;修改时遵循相邻代码风格并运行 `git diff --check`。保持服务、provider、检索和快照边界,不用静默回退或占位结果掩盖失败。 + +## Testing Guidelines + +测试框架为 pytest,配置见 `pytest.ini`;该配置禁用未使用的 `langsmith` 插件。测试文件命名为 `test_*.py`,测试函数命名为 `test_*`。新增行为应在对应 `tests/` 子目录补充回归测试;优先运行相关文件,再运行完整 `uv run pytest`。仓库未设置强制覆盖率阈值。 + +## Commit & Pull Request Guidelines + +提交历史使用 `feat:`、`fix:`、`docs:`、`build:`、`ci:`、`chore:` 等约定式前缀,并以简短祈使句说明变更。PR 应说明目的、影响范围和验证命令,注明配置或生成数据影响;只有 UI 输出改变时才附截图。发布相关变更同步更新 `CHANGELOG.md`。 + +## Security & Configuration Tips + +用 `cp config.toml.example config.toml` 和 `cp .env.example .env` 创建本地配置。密钥只放环境变量或本地 `.env`,不得写入源码、示例、日志或提交。根项目框架采用 MIT;`src/etl/`、`src/retrieval/` 等 Dify 衍生部分仍须遵守 `DIFY_LICENSE`,并保留上游版权声明。移除上游子模块不改变这些许可义务。 +知识快照保留 Dify 兼容的 pickle 文件格式;只从本项目生成的本地快照或明确受信的 legacy 文件加载,不导入不可信来源的 `.pkl` 文件。 + +## API Channel Integration + +Provider 必须在 `src/providers/factory.py` 声明能力并通过对应抽象接口校验。OpenAI 兼容渠道复用 `openai` SDK;Google、Anthropic、Volcengine 使用各自 SDK;Jina 和 SiliconFlow Rerank 使用 HTTP JSON。新增或调整渠道时同步更新 `config.toml.example`、用户文档和 provider 契约测试。Google 的 Auth Token、Anthropic Beta 资源和 Ark Classification 资源都需通过显式 Facade/入口调用,不能把缺少资源挂载静默当成成功。 + +`local-hash` 是无需远端凭证的离线 Embedding provider,默认配置使用它构建本地知识快照。 + +LLM 配置可通过 `protocol = "responses"` 选择 OpenAI/Ark Responses;省略时保持 `chat_completions` 兼容行为。Responses 仅在目标 Base URL 确实提供 `/responses` 时可用,`protocol_status()` 的 `adapter_supported` 不等于真实服务端验收;兼容渠道和 Ark 都必须在 `options.server_verified_protocols` 中显式登记已验证协议。Ark Responses 可使用 SDK 支持的 `input_audio`、`input_video` 和 `image_pixel_limit`,OpenAI 兼容适配器不得把这些 Ark 专属块静默发送。Embedding 和 Rerank 配置不得填写 `protocol`。 diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a0ce8e..a880a1a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,49 @@ 所有此项目的显著更改都将记录在此文件中。 +## [1.4.0] - 2026-09-20 + +### Provider 与协议 + +- 建立统一 Provider 工厂与抽象请求模型,补齐 LLM、Embedding、Rerank 的能力声明、配置校验、缓存与同步/异步生命周期管理。 +- 新增 `complete()`/`acomplete()` 高级调用入口,统一支持多轮消息、工具调用、结构化输出、多模态内容、usage 与结束原因;`invoke()`/`ainvoke()` 保留文本流兼容行为。 +- 修复多轮工具调用的历史归一化:共享的 `normalize_messages` 不再强制要求工具调用带 `id`,因为 Gemini 的 `FunctionCall.id` 是可选字段且 SDK 默认 `None`,否则把上一轮工具调用回填给 Google 会在请求发出前被本地拒绝;需要 `id` 的 Chat Completions 与 Responses 改为显式校验。同一处还修复了 `input` 参数别名被静默清空,以及 `function_call`/`custom_tool_call` 类型未映射为 `function` 的问题。 +- 支持 Chat Completions、Responses、Google Generate Content 和 Anthropic Messages 四类线协议;OpenAI 兼容渠道与 Ark 可通过 `protocol = "responses"` 选择 Responses,Ark 的 `ark` 取值是 Chat Completions 的别名。 +- Responses 对非官方端点要求 `options.server_verified_protocols = ["responses"]` 显式登记;未登记时工厂、Provider 和资源 Facade 均在请求前拒绝,避免把本地 SDK 资源误发到未实现 `/responses` 的服务端。 +- Ark Responses 在构造阶段拒绝 `instructions` 与 `caching={"type": "enabled"}` 同时出现:官方规定配置 `instructions` 后本轮请求无法写入或使用缓存,`caching` 为 `enabled` 时服务端直接报错,而 SDK 不做本地校验。错误信息会指出 `instructions` 来自 `system_prompt` 的兼容默认值,并给出改走 `messages` 的修复方式。 +- 新增原生资源 Facade,显式暴露文件、批处理、缓存、向量库、token 计数、调优等 SDK 能力,不再把资源生命周期混入普通聊天请求。 +- 深度适配 Google GenAI、Anthropic、Volcengine Ark、Jina 与 SiliconFlow Rerank;OpenAI 兼容渠道复用 `openai` SDK,Jina 与 SiliconFlow Rerank 使用 HTTP JSON。 +- Embedding 区分文档与查询任务类型:Google 使用 `RETRIEVAL_DOCUMENT` 与 `RETRIEVAL_QUERY`。 + +### 安全 + +- 新增配置、请求与日志边界的凭证校验:模型级 `options` 与请求级扩展不得携带密钥、请求头、query 或 Base URL 覆盖,资源 Facade 的关键字与位置参数同样受检。 +- 凭证键识别补齐火山引擎的 `ak`/`sk` 与 Azure 存储的 `account_key`;此前这些键可绕过请求覆盖校验,而 `api_key` 会被拒绝。 +- 日志与错误信息统一脱敏常见凭证表示(Bearer、API key、URL userinfo 与 query);补齐 `ak=`/`sk=`/`account_key=` 形式,以及服务端错误里「首尾可见、中间掩码」的凭证形态(如 `sk-abc***...***xyz`,此前会原样落进日志)。 +- 修复脱敏日志 formatter 的缓存泄漏:`logging.Formatter` 会把格式化后的 traceback 缓存在 `record.exc_text` 上供后续 handler 复用,先格式化后脱敏会让同一 logger 上的非脱敏 handler 输出原始凭证;现在脱敏结果会写回缓存。 +- 兼容渠道不再被当作官方 OpenAI 端点放行 SDK 专属资源;仅精确匹配 `https://api.openai.com/v1` 时按 SDK 契约放行。 +- 收紧失败语义:凭证、SDK 或远端调用失败时显式报错,不再以空 embedding、零分 rerank 或占位回答掩盖失败。 + +### 移除与调整 + +- 移除 Dify 上游子模块;`src/etl/`、`src/retrieval/` 等移植代码继续遵守 `DIFY_LICENSE` 并保留上游版权声明。 +- 清理死代码:移除 `has_system_message`、`_configured_extra_body_keys`、`SnapshotRepository.write_stats`/`load_stats`/`load_active_manifest`,以及 `recursive_text_splitter` 中未被引用的 `DEFAULT_CHILD_CHUNK_*` 常量。这些逻辑均已在调用点内联或由框架通过装饰器调用,保留会造成同一行为的两处实现。 +- 移除 iFlow 渠道及其配置示例。 +- 移除 qwen rerank 配置示例;该渠道不提供 rerank 能力,调用时会显式报错。 +- 发布脚本新增目标环境校验,`--target` 与主机系统/架构不匹配时在清理构建产物前显式失败,不再生成错误架构的发布包。 +- Qwen Base URL 改为 OpenAI 兼容接口 `https://dashscope.aliyuncs.com/compatible-mode/v1`;Volcengine Base URL 改为 Ark API `https://ark.cn-beijing.volces.com/api/v3`。配置仍为旧值时会在加载时给出显式升级警告:DashScope 原生 `api/v1` 不提供本仓库请求的 `{base_url}/chat/completions` 路径,火山旧域名 `maas-api.ml-platform-cn-beijing.volces.com` 则已停止服务。百炼的 `/responses` 只挂在业务空间专属域名下,默认共享域名不可用,文档已补充说明。 +- Jina Rerank 与 SiliconFlow Rerank 一致,`return_documents` 固定为 `True` 且不允许被 `options` 覆盖。 + +### 测试与文档 + +- 新增 Provider 协议适配、SDK 能力、凭证边界、Rerank 契约与失败语义回归测试;测试总数增至 823。 +- 为活动快照的 embedding 兼容性检测补充回归测试:快照记录的 embedding provider 或模型名与当前运行配置不一致时必须显式失败,避免用错向量空间后静默产出错误检索结果。 +- 引入 Ruff、Bandit 与 MyPy 到开发依赖,并补齐对应配置。 +- 更新 `README.md`、`AGENTS.md` 与 `docs/`,同步协议选择、`options` 边界、原生资源入口和 Vertex ADC 配置口径。 +- 核对各渠道官方现状并刷新配置示例:Google Embedding 改用 `gemini-embedding-2`(`text-embedding-004` 已于 2026-01-14 退役),OpenAI 改用 `gpt-5.6-*`(`gpt-3.5-turbo` 于 2026-10-23 退役),Anthropic 改用 `claude-sonnet-4-6`(`claude-3-5-sonnet-20240620` 已于 2025-10-28 退役),DeepSeek 改用 `deepseek-v4-pro`/`deepseek-v4-flash`(`deepseek-chat` 已于 2026-07-24 退役),火山改用 `doubao-seed-2-0-lite-260428` 与 `doubao-embedding-text-240715`(`doubao-pro-32k`、`bge-large-zh` 已不在方舟模型列表),Qwen 改用 `qwen3.8-max`/`qwen3.7-plus`,SiliconFlow 改用 `Qwen/Qwen3.5-27B`/`deepseek-ai/DeepSeek-V3.2`(复核发现 `Qwen/Qwen3-8B` 已不在该平台在售列表,其 Qwen 对话模型现从 Qwen3.5-27B 起步),Grok 改用 `grok-4.6`(`llama3-70b-8192` 实为 Groq 的 ID,不属于 xAI),Ollama 改用 `llama3.1`/`gemma3`。示例配置的键名同步改为与模型一致,模型内置默认值一并更新;内置 embedding 与 rerank 兜底值此前含无效 ID(Google 的裸名 `embedding-001`、SiliconFlow 不存在的 `alibaba/` 命名空间与已下线的 `bge-reranker-large`、Ollama 的 `llama3`),现分别改为 `gemini-embedding-2`、`BAAI/bge-large-zh-v1.5`、`BAAI/bge-reranker-v2-m3` 与 `llama3.1`。 +- 需要重建知识库快照:`gemini-embedding-2` 与 `text-embedding-004` 的向量空间不兼容,旧 FAISS 索引不能复用。火山的 `doubao-embedding-text-240715` 已于 2025-12-26 停止新购(EOM),且已不在方舟「向量化能力」模型列表中;官方下线公告给出的迁移目标是 `doubao-embedding-vision-251215`,该模型同时接受纯文本输入,但它按 `/api/v3/embeddings/multimodal` 提供,与本项目使用的文本 `embeddings.create` 路径不同,迁移前需实测。embedding 模型只有 EOM 阶段、不涉及 EOS,存量接入点不受影响,因此示例配置暂未改动该值。 +- 修复 Anthropic 采样字段弃用识别:家族名不再限定 `opus`/`sonnet`/`haiku`,覆盖 5 代新增的 `claude-fable-5`、`claude-mythos-5` 等命名;此前这些模型会被透传 `temperature`/`top_p`/`top_k`,而 Python SDK v1.0+ 已移除这些参数,请求会直接失败。 + ## [1.3.0] - 2026-03-20 ### 运行与配置 diff --git a/DIFY_LICENSE b/DIFY_LICENSE index a209de7..fb74e97 100644 --- a/DIFY_LICENSE +++ b/DIFY_LICENSE @@ -15,7 +15,11 @@ b. LOGO and copyright information: In the process of using Dify's frontend, you a. The producer can adjust the open-source agreement to be more strict or relaxed as deemed necessary. b. Your contributed code may be used for commercial purposes, including but not limited to its cloud business operations. -Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. Detailed information about the Apache License 2.0 can be found at http://www.apache.org/licenses/LICENSE-2.0. +Apart from the specific conditions mentioned above, all other rights and restrictions follow the Apache License 2.0. + +A copy of the Apache License 2.0 is included in this repository at `licenses/APACHE-2.0.txt` +and is redistributed with every release bundle, as required by section 4(a) of that license. +The canonical text is also available at http://www.apache.org/licenses/LICENSE-2.0. The interactive design of this product is protected by appearance patent. diff --git a/README.md b/README.md index 7352db2..544c07c 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,9 @@ uv sync --group dev uv run python scripts/build_binary_release.py --target macos-x64 --validate ``` -发布工作流定义在 [`.github/workflows/release.yml`](/Volumes/Work/code/PyRAG-kit/.github/workflows/release.yml),发布说明会从 [CHANGELOG.md](/Volumes/Work/code/PyRAG-kit/CHANGELOG.md) 的对应版本节自动提取。 +打包脚本不执行跨平台交叉编译,`--target` 必须与当前主机或 CI runner 的系统和 CPU 架构一致;例如 Intel macOS 使用 `macos-x64`,Apple Silicon macOS 使用 `macos-arm64`。不匹配时脚本会在清理产物前显式失败,避免生成错误架构的发布包。 + +发布工作流定义在 [`.github/workflows/release.yml`](./.github/workflows/release.yml),发布说明会从 [CHANGELOG.md](./CHANGELOG.md) 的对应版本节自动提取。 二进制包内默认只包含空的 `knowledge_base/` 占位目录,使用时请自行放入 Markdown 源文档后再构建知识快照。 --- diff --git a/config.toml.example b/config.toml.example index 67cecc8..b590f89 100644 --- a/config.toml.example +++ b/config.toml.example @@ -12,16 +12,16 @@ openai_api_base = "https://api.openai.com/v1" # SiliconFlow 的 Base URL siliconflow_base_url = "https://api.siliconflow.cn/v1" -# Qwen 官方接口的 Base URL -qwen_base_url = "https://dashscope.aliyuncs.com/api/v1" +# Qwen OpenAI 兼容接口的 Base URL +qwen_base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1" # DeepSeek 的 Base URL deepseek_base_url = "https://api.deepseek.com" # Ollama 的 Base URL ollama_base_url = "http://localhost:11434/v1" # LM Studio 的 Base URL lm_studio_base_url = "http://localhost:1234/v1" -# 火山引擎的 Base URL -volc_base_url = "https://maas-api.ml-platform-cn-beijing.volces.com" +# 火山引擎 Ark API 的 Base URL +volc_base_url = "https://ark.cn-beijing.volces.com/api/v3" # Grok 的 Base URL grok_base_url = "https://api.x.ai/v1" @@ -102,8 +102,10 @@ model_name = "local-hash-256" [embedding_configurations.google] # 提供商实现标识 provider = "google" -# API 实际模型名 -model_name = "models/text-embedding-004" +# API 实际模型名(text-embedding-004 已于 2026-01-14 退役) +model_name = "gemini-embedding-2" +# 可选 SDK 参数:查询会自动使用 RETRIEVAL_QUERY,文档使用 RETRIEVAL_DOCUMENT +# options = { output_dimensionality = 768 } # openai 向量化模型 [embedding_configurations.openai] @@ -123,8 +125,14 @@ model_name = "text-embedding-v4" [embedding_configurations.volcengine] # 提供商实现标识 provider = "volcengine" -# API 实际模型名 -model_name = "bge-large-zh" +# API 实际模型名。bge-large-zh 已不在方舟模型列表中。 +# doubao-embedding-text-240715 已于 2025-12-26 停止新购(EOM),且不在方舟当前 +# 模型列表的「向量化能力」章节中;官方下线公告给出的迁移目标只有 +# doubao-embedding-vision-251215(多模态向量化模型,也接受纯文本输入)。 +# 迁移前请在方舟控制台「开通管理」确认账号可开通的向量化模型。 +# 注意:本项目的火山 embedding 走文本 embeddings.create(/api/v3/embeddings), +# 而 vision 系列官方示例走 /api/v3/embeddings/multimodal;启用前需实测确认。 +model_name = "doubao-embedding-text-240715" # ollama 向量化模型 [embedding_configurations.ollama] @@ -155,13 +163,6 @@ provider = "jina" # API 实际模型名 model_name = "jina-reranker-v2-base-multilingual" -# qwen rerank 模型 -[rerank_configurations.qwen] -# 提供商实现标识 -provider = "qwen" -# API 实际模型名 -model_name = "Qwen/Qwen3-Reranker-4B" - # --- LLM 配置 --- # google 聊天模型 [llm_configurations.google] @@ -177,75 +178,87 @@ provider = "google" # API 实际模型名 model_name = "gemini-2.5-pro" -# openai-gpt4o 聊天模型 -[llm_configurations.openai-gpt4o] +# openai 聊天模型;与 .env.example 的 DEFAULT_LLM_PROVIDER=openai 对应 +[llm_configurations.openai] # 提供商实现标识 provider = "openai" -# API 实际模型名 -model_name = "gpt-4o" +# API 实际模型名(gpt-4o 各快照自 2026-10-23 起退役) +model_name = "gpt-5.6-sol" -# iflow-qwen3-max 聊天模型 -[llm_configurations.iflow-qwen3-max] -# 提供商实现标识 +# openai-responses Responses 协议示例 +# 仅当 Base URL 对应服务端实现了 /responses 时使用;省略 protocol 默认使用 Chat Completions。 +[llm_configurations.openai-responses] provider = "openai" -# API 实际模型名 -model_name = "qwen3-max" - -# openai-gpt3.5 聊天模型 -[llm_configurations."openai-gpt3.5"] +model_name = "gpt-5.6-sol" +protocol = "responses" +# 仅放非敏感 SDK 参数;API key、Base URL 必须放 .env/环境变量 +# 如果 Base URL 是代理、中转或自建网关,完成服务端验证后显式登记: +# options = { server_verified_protocols = ["responses"], reasoning = { effort = "medium" }, store = true } + +# openai-fast 聊天模型(gpt-3.5-turbo 于 2026-10-23 退役,示例改用现行模型) +[llm_configurations.openai-fast] # 提供商实现标识 provider = "openai" # API 实际模型名 -model_name = "gpt-3.5-turbo" +model_name = "gpt-5.6-terra" -# qwen-max 聊天模型 -[llm_configurations.qwen-max] +# qwen3.8-max 聊天模型(qwen-max 为旧版裸名,快照系列已陆续下线) +[llm_configurations.qwen3-8-max] # 提供商实现标识 provider = "qwen" # API 实际模型名 -model_name = "qwen-max" +model_name = "qwen3.8-max" -# qwen-plus 聊天模型 -[llm_configurations.qwen-plus] +# qwen3.7-plus 聊天模型 +[llm_configurations.qwen3-7-plus] # 提供商实现标识 provider = "qwen" # API 实际模型名 -model_name = "qwen-plus" +model_name = "qwen3.7-plus" -# volc-doubao-pro-32k 聊天模型 -[llm_configurations.volc-doubao-pro-32k] +# volc-doubao-seed 聊天模型(doubao-pro-32k 全部快照已退役) +[llm_configurations.volc-doubao-seed] # 提供商实现标识 provider = "volcengine" # API 实际模型名 -model_name = "doubao-pro-32k" - -# sf-qwen2-7b 聊天模型 -[llm_configurations.sf-qwen2-7b] +model_name = "doubao-seed-2-0-lite-260428" + +# Ark Responses 示例;只有目标 Ark 服务端支持 /responses 时启用 +# [llm_configurations.volc-responses] +# provider = "volcengine" +# model_name = "doubao-seed-1-6-251015" +# protocol = "responses" +# 仅在目标 Ark Base URL 已实际验证 /responses 后保留此登记 +# options = { server_verified_protocols = ["responses"] } + +# sf-qwen3-27b 聊天模型(Qwen2-7B-Instruct、Qwen/Qwen3-8B 均已不在 SiliconFlow +# 模型广场;该平台现列的 Qwen 对话模型从 Qwen3.5-27B 起步) +[llm_configurations.sf-qwen3-27b] # 提供商实现标识 provider = "siliconflow" # API 实际模型名 -model_name = "Qwen/Qwen2-7B-Instruct" +model_name = "Qwen/Qwen3.5-27B" -# sf-llama3-8b 聊天模型 -[llm_configurations.sf-llama3-8b] +# sf-deepseek-v3 聊天模型(meta-llama/* 已从 SiliconFlow 模型广场下线) +[llm_configurations.sf-deepseek-v3] # 提供商实现标识 provider = "siliconflow" # API 实际模型名 -model_name = "meta-llama/Meta-Llama-3-8B-Instruct" +model_name = "deepseek-ai/DeepSeek-V3.2" -# ollama-llama3 聊天模型 -[llm_configurations.ollama-llama3] +# ollama-llama3.1 聊天模型 +[llm_configurations.ollama-llama3-1] # 提供商实现标识 provider = "ollama" # API 实际模型名 -model_name = "llama3" +model_name = "llama3.1" -# ollama-gemma 聊天模型 -[llm_configurations.ollama-gemma] +# ollama-gemma3 聊天模型 +[llm_configurations.ollama-gemma3] # 提供商实现标识 provider = "ollama" # API 实际模型名 -model_name = "gemma" +model_name = "gemma3" # lm-studio 聊天模型 [llm_configurations.lm-studio] @@ -254,30 +267,30 @@ provider = "lm-studio" # API 实际模型名 model_name = "local-model/gguf-model-name" -# deepseek-chat 聊天模型 -[llm_configurations.deepseek-chat] +# deepseek-v4-pro 聊天模型(deepseek-chat 已于 2026-07-24 完全退役) +[llm_configurations.deepseek-v4-pro] # 提供商实现标识 provider = "deepseek" # API 实际模型名 -model_name = "deepseek-chat" +model_name = "deepseek-v4-pro" -# deepseek-v2 聊天模型 -[llm_configurations.deepseek-v2] +# deepseek-v4-flash 聊天模型(deepseek-v2 从未作为官方 API ID 存在) +[llm_configurations.deepseek-v4-flash] # 提供商实现标识 provider = "deepseek" # API 实际模型名 -model_name = "deepseek-v2" +model_name = "deepseek-v4-flash" -# grok-llama3-70b 聊天模型 -[llm_configurations.grok-llama3-70b] +# grok-4.6 聊天模型(llama3-70b-8192 是 Groq 的 ID,xAI 只有 grok-* 系列) +[llm_configurations.grok-4-6] # 提供商实现标识 provider = "grok" # API 实际模型名 -model_name = "llama3-70b-8192" +model_name = "grok-4.6" -# anthropic-sonnet-3.5 聊天模型 -[llm_configurations."anthropic-sonnet-3.5"] +# anthropic-sonnet-4-6 聊天模型(claude-3-5-sonnet-20240620 已于 2025-10-28 退役) +[llm_configurations.anthropic-sonnet-4-6] # 提供商实现标识 provider = "anthropic" # API 实际模型名 -model_name = "claude-3-5-sonnet-20240620" +model_name = "claude-sonnet-4-6" diff --git a/dify b/dify deleted file mode 160000 index aa71784..0000000 --- a/dify +++ /dev/null @@ -1 +0,0 @@ -Subproject commit aa71784627fafe252e8bb246e7abed38171e48b3 diff --git a/docs/developer_docs/developer-guide.md b/docs/developer_docs/developer-guide.md index 48b98a0..c443143 100644 --- a/docs/developer_docs/developer-guide.md +++ b/docs/developer_docs/developer-guide.md @@ -49,17 +49,23 @@ PyRAG-Kit 采用了清晰、模块化的项目结构,旨在实现高内聚、 这是扩展新模型的关键目录。 -* **`__base__/model_provider.py`**: 定义了所有模型提供商必须遵循的抽象基类 `LargeLanguageModel` 和 `RerankModel`。它们规定了 `invoke` 和 `rerank` 等核心接口。 +* **`__base__/model_provider.py`**: 定义 `CompletionRequest`、`CompletionResult`、`LargeLanguageModel`、`TextEmbeddingModel` 和 `RerankModel`。`invoke`/`ainvoke` 保留文本流兼容接口;需要多轮、多模态、工具调用、结构化输出和 usage 时使用 `complete`/`acomplete`。 * **`factory.py`**: 实现了一个工厂模式,用于根据配置动态创建和获取指定的模型提供商实例。 * **`google.py`, `openai.py`, etc.**: 每个文件都是一个具体模型提供商的实现,负责处理与该平台 API 的所有交互(如认证、请求构建、响应解析)。 **如何添加一个新的模型提供商?** 1. 在 `src/providers/` 目录下创建一个新的 `my_new_provider.py` 文件。 -2. 在该文件中创建一个类,继承自 `LargeLanguageModel`。 -3. 实现基类中定义的抽象方法,主要是 `invoke()`。 -4. 在 `src/providers/factory.py` 中导入您的新类,并在 `ModelProviderFactory` 的 `get_llm_provider` 方法中添加一个 `elif` 分支来注册您的新提供商。 -5. 在 `config.toml` 的 `llm_configurations`、`embedding_configurations` 或 `rerank_configurations` 中添加使用您的新提供商的配置。 +2. 在该文件中实现所需能力接口:`LargeLanguageModel`、`TextEmbeddingModel` 或 `RerankModel`。 +3. 实现对应抽象方法,并在 `src/providers/factory.py` 的 `_provider_map` 中注册模块和类;不要在 `get_*_provider` 方法中添加分支。 +4. 确保配置角色与实现能力匹配。工厂会在实例化时校验接口,并对需要不同协议实现的渠道使用角色别名。 +5. 在 `config.toml` 的 `llm_configurations`、`embedding_configurations` 或 `rerank_configurations` 中添加配置,并为工厂和渠道请求补充测试。 + +Provider 的协议边界保持清晰:OpenAI、Qwen、SiliconFlow、DeepSeek、Grok、Ollama 和 LM Studio 复用 `openai` SDK 的 Chat/Responses 兼容接口;这些渠道的 LLM 配置默认使用 Chat Completions,也可显式选择 `responses`,但兼容服务端是否实现该路径必须单独验证。官方 OpenAI Base URL 可按 SDK 契约使用 Responses;代理、中转或自建 Base URL 必须在完成实际验证后通过 `options.server_verified_protocols = ["responses"]` 登记,否则 Provider 会在请求前显式拒绝。只有官方 OpenAI Provider 声明 Files、Batches、Vector Stores、Skills、Realtime、Webhooks、Admin 和 Content Provenance Checks 等 OpenAI 专属资源,并通过 `.native` 保留当前 SDK 的完整资源树。Google、Anthropic 和 Volcengine 使用各自 SDK,Volcengine 同时支持 Ark Chat、Responses 和 Classification;Ark Classification 通过 SDK 官方资源类显式挂载,SDK 不提供该资源时直接报错。Jina 与 SiliconFlow Rerank 使用 HTTP JSON 接口。新增渠道时先确认协议、服务端证据和能力,再选择复用适配器或实现独立 provider。 + +`ModelDetail.options` 是非敏感 SDK 扩展参数的唯一配置入口。工厂会将其传入 Provider,并拒绝凭证、请求头和 Base URL 字段;Provider 还会把客户端构造参数与请求参数分流,不支持的能力应在 Provider 边界显式报错。SDK 专属文件、批处理、缓存、token 计数等资源通过 Provider 的显式方法调用,不得塞进普通 `invoke()`。同步和异步 Facade 应保持 SDK 的真实调用约定:例如 Google `aio.models.generate_content_stream()` 需要等待一次后再异步迭代,而 `aio.chats.create()` 本身同步返回 `AsyncChat`。 + +Responses 适配位于 `src/providers/openai_compatible.py`,共享消息归一化位于 `src/providers/__base__/model_provider.py`。请求构造、Chat Completions 工具转换、`assistant.tool_calls`/`role=tool` 历史转换、`text`/`image_url`/`file` 多模态块转换、同步/异步文本提取和 SSE 事件解析集中在这些适配器中;Ark Responses 还要按目标 SDK 变体转换 `input_audio`、`input_video` 和 `image_pixel_limit`,当前 OpenAI SDK 不支持的音视频输入块必须在请求前显式拒绝。`response.failed`、`response.incomplete`、取消状态和错误事件必须显式转换为异常,不能静默结束流。扩展事件类型时应先补契约测试,再修改统一文本流接口。 **当前默认嵌入提供商** diff --git a/docs/user_guide/configuration.md b/docs/user_guide/configuration.md index 82f1849..bd8b69b 100644 --- a/docs/user_guide/configuration.md +++ b/docs/user_guide/configuration.md @@ -44,20 +44,51 @@ cp .env.example .env ``` ```dotenv -OPENAI_API_KEY="sk-xxxxxxxxxxxxxxxxxxxx" +OPENAI_API_KEY="your-openai-api-key" GOOGLE_API_KEY="AIza..." +# 也可使用 google-genai 官方别名;同时设置时 GOOGLE_API_KEY 优先 +# GEMINI_API_KEY="AIza..." ``` `.env` 中的同名键会覆盖 `config.toml`。 +Google Vertex AI 使用 Application Default Credentials(ADC)时,不要把凭证对象或密钥内容写入 `config.toml`。 +请在运行环境中完成 `gcloud auth application-default login`,或设置服务账号文件路径,并显式选择 Vertex 模式: + +```dotenv +GOOGLE_GENAI_USE_VERTEXAI="true" +GOOGLE_CLOUD_PROJECT="your-project-id" +GOOGLE_CLOUD_LOCATION="us-central1" +# 可选:改用服务账号文件;文件本身不要提交到仓库 +GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/service-account.json" +``` + +没有 `GOOGLE_API_KEY` 时,`GOOGLE_GENAI_USE_VERTEXAI=true` 会让 Provider 使用 Vertex ADC;`ModelDetail.options` 只接受非敏感参数,不能用来保存 `credentials`。 + 如果您使用 OpenAI 兼容渠道,可以这样配置: ```dotenv OPENAI_API_KEY="sk-..." -OPENAI_API_BASE="https://apis.iflow.cn/v1" -DEFAULT_LLM_PROVIDER="iflow-qwen3-max" +OPENAI_API_BASE="https://api.openai.com/v1" +DEFAULT_LLM_PROVIDER="openai" ``` +`.env` 中的 `OPENAI_API_BASE` 会覆盖 `config.toml` 的 `openai_api_base`,适合把流量指向代理、中转或自建网关。 +注意 `provider = "openai"` 的条目都会走这个 Base URL,因此指向网关时,`model_name` 必须是该网关实际提供的模型 ID。 + +### 旧端点升级提示 + +配置加载时会对已知失效的旧端点发出显式警告,而不是让请求在运行期失败: + +- `qwen_base_url` 为 `https://dashscope.aliyuncs.com/api/v1` 时会提示改为 `compatible-mode/v1`。 + DashScope 原生 `/api/v1` 本身仍在服务,但它的路径是 + `/services/aigc/text-generation/generation`,不提供本项目按 OpenAI 兼容协议请求的 + `{base_url}/chat/completions`。 +- `volc_base_url` 为 `https://maas-api.ml-platform-cn-beijing.volces.com` 时会提示改为 + `https://ark.cn-beijing.volces.com/api/v3`;旧域名已停止服务。 + +自建或代理端点不受影响,警告只针对上述精确匹配的旧值。 + ## 主要字段 ### Base URL @@ -102,7 +133,7 @@ DEFAULT_LLM_PROVIDER="iflow-qwen3-max" ```toml [llm_configurations.demo] provider = "openai" -model_name = "gpt-4o" +model_name = "gpt-5.6-sol" ``` - `llm_configurations`: 聊天模型 @@ -120,3 +151,17 @@ model_name = "local-hash-256" ``` 它用于本地可复现验证,不依赖外部 Embedding API。 + +### 切换 Embedding 需重建快照 + +活动快照的 `manifest.toml` 会记录构建时的 `embedding_provider` 与 `embedding_model`。 +加载快照时框架会核对两者是否与当前运行配置一致,不一致会显式报错: + +``` +活动知识快照的 embedding 配置与当前运行配置不一致: +快照=/,当前=/。请重建知识快照或切换回原 embedding 配置。 +``` + +这是刻意的硬性检查:不同模型的向量空间不兼容,静默复用旧索引会产出错误的检索结果。 +切换 Embedding 模型(包括同一 provider 内换模型,例如 `text-embedding-004` → `gemini-embedding-2`) +后必须重建知识快照。 diff --git a/docs/user_guide/getting-started.md b/docs/user_guide/getting-started.md index adeb37b..926a55c 100644 --- a/docs/user_guide/getting-started.md +++ b/docs/user_guide/getting-started.md @@ -62,8 +62,8 @@ uv sync 4. 打开 `.env` 文件,填入您要使用的聊天模型 API Key。例如,如果您使用 OpenAI 兼容渠道: ```dotenv OPENAI_API_KEY="sk-..." - OPENAI_API_BASE="https://apis.iflow.cn/v1" - DEFAULT_LLM_PROVIDER="iflow-qwen3-max" + OPENAI_API_BASE="https://api.openai.com/v1" + DEFAULT_LLM_PROVIDER="openai" ``` 5. 如需调整默认路径、base url 或检索参数,请编辑 `config.toml`。 diff --git a/docs/user_guide/llm-providers.md b/docs/user_guide/llm-providers.md index eb8cdad..8dad468 100644 --- a/docs/user_guide/llm-providers.md +++ b/docs/user_guide/llm-providers.md @@ -15,8 +15,8 @@ PyRAG-Kit 的一个核心优势是其高度的灵活性和可扩展性,尤其 ### 云服务模型 * **Google**: 支持 Gemini 系列模型。 -* **OpenAI**: 支持 GPT 系列模型,如 GPT-4o, GPT-3.5-Turbo。 -* **Anthropic**: 支持 Claude 系列模型,如 Claude 3.5 Sonnet。 +* **OpenAI**: 支持 GPT 系列模型,如 GPT-5.6 系列。 +* **Anthropic**: 支持 Claude 系列模型,如 Claude Sonnet 4.6 与 Claude 5 系列。 * **阿里云 (Qwen)**: 支持通义千问系列模型。 * **火山引擎 (VolcEngine)**: 支持豆包 (Doubao) 系列模型。 * **深度求索 (DeepSeek)**: 支持 DeepSeek 系列模型。 @@ -24,9 +24,22 @@ PyRAG-Kit 的一个核心优势是其高度的灵活性和可扩展性,尤其 * **SiliconFlow**: 一个集成了多种开源模型的平台,可通过其统一 API 调用。 * **Jina AI**: 主要用于提供高质量的 Rerank 模型。 +> **火山引擎向量化的已知问题**:示例配置中的 `doubao-embedding-text-240715` 已于 +> 2025-12-26 停止新购(EOM),且不在方舟「向量化能力」模型列表中。方舟官方给出的迁移目标是 +> `doubao-embedding-vision-251215`,但该项目走 `/api/v3/embeddings/multimodal`,与本项目火山 +> embedding 使用的文本 `embeddings.create` 不是同一条路径,**迁移前需要实测确认**。 +> embedding 模型只有 EOM 阶段、不涉及 EOS,已有接入点不受影响,因此示例配置暂未改动该值。 +> 默认的知识库链路使用离线 `local-hash`,不受此问题影响。 + +> **百炼的 Responses 端点挂在专属域名下**:`/responses` 只存在于业务空间专属域名 +> `https://{WorkspaceId}.{region}.maas.aliyuncs.com/compatible-mode/v1`,默认共享域名 +> `dashscope.aliyuncs.com/compatible-mode/v1` 并未提供该路径;旧版 +> `/api/v2/apps/protocols/compatible-mode/v1/responses` 已停止维护。在默认域名上启用 +> `protocol = "responses"` 会收到 HTTP 404。 + ### 本地化模型 -* **Ollama**: 支持通过 Ollama 在本地运行的各种开源模型,如 Llama3, Gemma 等。 +* **Ollama**: 支持通过 Ollama 在本地运行的各种开源模型,如 Llama 3.1、Gemma 3 等。 * **LM Studio**: 支持通过 LM Studio 在本地运行的 `gguf` 格式模型。 ## 配置方法 @@ -37,7 +50,85 @@ PyRAG-Kit 的一个核心优势是其高度的灵活性和可扩展性,尤其 如果您使用代理、第三方中转服务或本地模型,请在 `config.toml` 中配置对应的 API 访问地址。例如,Ollama 的默认地址是 `http://localhost:11434/v1`。 -### 2. 定义模型实例 +Google Vertex AI 的 Application Default Credentials(ADC)通过环境变量配置,不要把凭证对象或密钥内容写入模型条目的 `options`。先完成 ADC 登录,再设置: + +```dotenv +GOOGLE_GENAI_USE_VERTEXAI="true" +GOOGLE_CLOUD_PROJECT="your-project-id" +GOOGLE_CLOUD_LOCATION="us-central1" +# 可选:服务账号文件路径(不要提交该文件) +GOOGLE_APPLICATION_CREDENTIALS="/absolute/path/service-account.json" +``` + +`google-genai` 也支持 `GEMINI_API_KEY` 作为 API key 别名;同时设置时优先使用 `GOOGLE_API_KEY`。未设置 API key 时,显式的 Vertex 开关允许 SDK 使用 ADC;程序化集成也可向 `GoogleProvider` 传入 `credentials`,但该对象不能放进 `config.toml`。 + +### 2. 协议与 SDK + +不同渠道不强制使用各供应商的官方 SDK,项目按协议复用适配器: + +| 渠道 | 能力 | 实现方式 | +| --- | --- | --- | +| Google | LLM、Embedding | `google-genai` SDK | +| OpenAI | LLM、Embedding | `openai` SDK | +| Qwen、SiliconFlow、Ollama、LM Studio | LLM、Embedding | `openai` SDK 的 Chat/Responses 兼容接口;资源能力按目标服务端另行确认 | +| DeepSeek、Grok | LLM | `openai` SDK 的 Chat/Responses 兼容接口;资源能力按目标服务端另行确认 | +| Anthropic | LLM | `anthropic` SDK | +| Volcengine | LLM、Embedding、Ark Responses | Volcengine Ark SDK | +| Jina、SiliconFlow | Rerank | HTTP JSON 接口(`httpx`) | + +Qwen 的 OpenAI 兼容地址默认使用 `https://dashscope.aliyuncs.com/compatible-mode/v1`;SiliconFlow 的聊天/Embedding 和 Rerank 是两种不同接口,Rerank 会由工厂映射到专用适配器。 + +OpenAI 兼容的 LLM 配置默认调用 Chat Completions。需要调用 Responses 时,在单个 `llm_configurations` 条目中设置 `protocol = "responses"`。Ark 也可使用同样的协议字段: + +```toml +[llm_configurations.openai-responses] +provider = "openai" +model_name = "gpt-5.6-sol" +protocol = "responses" +``` + +如果 `provider = "openai"` 使用的是代理、中转或自建 Base URL,即使它实现了 Responses,也必须先完成该渠道的实际验证,再在条目的 `options` 中登记: + +```toml +[llm_configurations.openai-proxy-responses] +provider = "openai" +model_name = "your-model" +protocol = "responses" +options = { server_verified_protocols = ["responses"] } +``` + +官方 `https://api.openai.com/v1` 会按 OpenAI SDK 契约使用 Responses;自定义 Base URL 不会因 provider 名称为 `openai` 而自动视为官方服务。未完成服务端验证时,系统会在请求前显式报错,避免把本地 SDK 资源误发到不支持的渠道。 + +Ark Responses 也遵循同一门禁。使用 Volcengine 的 Responses 前,必须确认目标 Ark Base URL 提供 `/responses`,并在配置中登记 `options = { server_verified_protocols = ["responses"] }`;未登记时,工厂、Provider 和资源 Facade 都会在请求前显式拒绝。 + +Responses 请求会使用 `input`、`instructions` 和 `tools` 字段,并把文本增量统一转换为现有聊天流。工具定义会从 Chat Completions 的 `function` 结构转换为 Responses Function Tool;多轮历史中的 `assistant.tool_calls` 与 `role=tool` 会分别转换为 `function_call` 和 `function_call_output`,消息中的 `text`、`image_url` 和 `file` 内容块也会转换为 Responses 的 `input_text`、`input_image` 和 `input_file`。Ark Responses 另外支持 `input_audio`、`input_video` 与 `image_pixel_limit`;OpenAI Responses 对这些 Ark 专属块会在请求前显式拒绝。缺少关联 ID、无效 JSON 或不受当前 SDK 支持的内容块也会在请求前显式报错。失败、取消或不完整响应也会显式报错。Embedding 仍使用 `embeddings.create`,不会因为 LLM 协议设置而改变。 + +统一的高级调用入口是 Provider 的 `complete()`/`acomplete()`,支持 `messages`(多轮和多模态内容)、`tools`、`tool_choice`、`response_format`、`max_tokens`、`top_p`、`stop`、`seed` 和 `extra_body`。结果包含 `text`、`tool_calls`、`usage`、`finish_reason` 和原始 SDK 响应。旧版 `invoke()`/`ainvoke()` 仍只输出文本流,以保持聊天界面兼容。需要 reasoning、thinking、拒答或流式 usage 时使用 `stream_events()`/`astream_events()`。 + +多轮工具调用的历史可以原样回填:请求边界会把扁平的 `{"id","type","name","arguments"}` 归一化为各协议需要的形状,并读取 Responses `custom_tool_call` 与 Anthropic `tool_use` 使用的 `input` 别名。各协议对关联 ID 的要求不同——Chat Completions 与 Responses 要求 `assistant.tool_calls` 带 `id`(缺少时在请求前显式报错),Google Gemini 不需要,其 `FunctionCall.id` 为可选且默认缺省,因此无 id 的 Google 工具调用可以正常回填。Provider 自己产出的 `tool_calls` 都带有协议所需的标识。 + +非敏感供应商参数放在模型条目的 `options` 表中,工厂会拒绝 `api_key`、`access_key`、`secret_key`、`token`、`headers` 和 `base_url` 等字段: + +```toml +[llm_configurations.claude-thinking] +provider = "anthropic" +model_name = "claude-sonnet-4-6" +options = { max_tokens = 8192, thinking = { type = "adaptive" } } +``` + +Claude Sonnet 4.6 及更新的支持自适应 thinking;Sonnet 4.5 等仅支持手动模式时,应改用 `thinking = { type = "enabled", budget_tokens = 4096 }`。 + +Claude 4.5 及更新模型默认按官方契约移除 `temperature`、`top_p` 和 `top_k`。如果目标是明确支持这些字段的 Anthropic-compatible 代理,可在该模型的 `options` 中显式设置 `allow_deprecated_sampling = true`;官方端点不要开启此选项。 + +SDK 专属资源使用显式方法,不混入普通聊天请求:官方 OpenAI Provider 提供 `files`、`batches`、`vector_stores`、`audio`、`images`、`videos`、`uploads`、`conversations`、`containers`、`fine_tuning`、`evals`、`skills`、`realtime`、`webhooks`、`admin` 和 `content_provenance_checks` 等资源;这些资源也可通过 `provider.resources.native` / `provider.async_resources.async_native` 访问当前 SDK 的完整原生树。Google 提供 `files`、`caches`、`batches`、`file_search`、图像/视频、模型、调优、`auth_tokens` 和 NextGen 资源;Anthropic 提供 token count、Message Batches、Beta Files、Models,以及 `resources.beta_agents`、`resources.beta_sessions` 等 Beta 资源树;Ark 提供 Responses、Files、Batch、Tokenization、Classification、多模态 Embedding、Context、Content Generation 和 Images。兼容渠道只保证通用 Chat/Responses/Embedding 适配,默认只通过 `.native` 暴露底层客户端,不会把官方 OpenAI 的资源接口伪装成所有服务端都支持。这些方法只证明本地 SDK 调用链已适配,未替代真实账号、模型和渠道验收。 + +Embedding 文档和查询会区分任务类型:Google 使用 `RETRIEVAL_DOCUMENT` 与 `RETRIEVAL_QUERY`,OpenAI 兼容/Ark 可在 `options` 中设置 `dimensions`、`encoding_format` 等参数。批处理、文件、缓存和图像等资源必须显式调用,不会被普通 RAG 流程隐式触发。 + +这里的支持表示本地适配器能够发出 Responses 请求,不代表每个 Qwen、SiliconFlow、Ollama、LM Studio、DeepSeek 或 Grok 的兼容服务端都已实现 `/responses`。工厂的 `protocol_status()` 会分别返回 `adapter_supported` 与 `server_verified`;当前仓库未对这些外部 Base URL 做真实渠道验收,`server_verified` 默认是 `false`。启用前请确认目标 Base URL 的服务端文档和实际能力;不支持时应删除 `protocol` 或改回 `chat_completions`。 + +注意「供应商支持 Responses」不等于「你配置的那个 Base URL 支持 Responses」。百炼(DashScope)的 `/responses` 挂在业务空间专属域名 `https://{WorkspaceId}.{region}.maas.aliyuncs.com/compatible-mode/v1` 下,默认的共享域名 `dashscope.aliyuncs.com/compatible-mode/v1` 并未提供该路径;百炼旧版 Responses 路径 `/api/v2/apps/protocols/compatible-mode/v1/responses` 也已停止维护。若在默认域名上启用 `protocol = "responses"`,请求会收到 HTTP 404,此时应改用专属域名或回到 `chat_completions`。 + +### 3. 定义模型实例 所有可供程序使用的模型都在 `config.toml` 中以 TOML 表定义,分为三类: @@ -50,12 +141,13 @@ PyRAG-Kit 的一个核心优势是其高度的灵活性和可扩展性,尤其 ```toml [llm_configurations.google-pro] provider = "google" -model_name = "gemini-1.5-pro-latest" +model_name = "gemini-2.5-pro" ``` * **`your-custom-name`**: 您为这个模型配置起的名字。这个名字会显示在 `/config` 菜单中供您选择。例如,`google-pro`。 * **`provider`**: 指定使用哪个模型提供商的实现。这个值必须与 `src/providers/` 目录下的某个文件名(或工厂类中的标识符)相对应。例如,`google`。 -* **`model_name`**: 要调用的实际模型名称/ID。这个值会直接传递给对应服务商的 API。例如,`gemini-1.5-pro-latest`。 +* **`model_name`**: 要调用的实际模型名称/ID。这个值会直接传递给对应服务商的 API。例如,`gemini-2.5-pro`。 +* **`protocol`**(仅 LLM 可选): 指定渠道协议。OpenAI 兼容渠道和 Volcengine 可使用 `chat_completions`/`responses`(Volcengine 省略时为 Ark Chat);Google、Anthropic 使用各自固定协议。Embedding 与 Rerank 配置不应填写此字段。 ## 当前默认推荐组合 @@ -75,31 +167,68 @@ model_name = "local-hash-256" ```dotenv OPENAI_API_KEY="sk-..." -OPENAI_API_BASE="https://apis.iflow.cn/v1" -DEFAULT_LLM_PROVIDER="iflow-qwen3-max" +OPENAI_API_BASE="https://api.openai.com/v1" +DEFAULT_LLM_PROVIDER="openai" ``` 并在 `config.toml` 中保留对应模型定义: ```toml -[llm_configurations.iflow-qwen3-max] +[llm_configurations.openai] provider = "openai" -model_name = "qwen3-max" +model_name = "gpt-5.6-sol" ``` ## 如何添加新模型 -假设您想添加一个通过 SiliconFlow 平台提供的 `Qwen/Qwen2-57B-A14B-Instruct` 模型,可以按以下步骤操作: +假设您想添加一个通过 SiliconFlow 平台提供的 `zai-org/GLM-5.3` 模型,可以按以下步骤操作: 1. **确保密钥已配置**: 在 `.env` 中填入 `SILICONFLOW_API_KEY`。 2. **编辑 `llm_configurations`**: 在 `config.toml` 中添加一个新的表: ```toml - [llm_configurations.sf-qwen2-57b] + [llm_configurations.sf-glm-5-3] provider = "siliconflow" - model_name = "Qwen/Qwen2-57B-A14B-Instruct" + model_name = "zai-org/GLM-5.3" ``` 3. **重启程序**: 保存 `config.toml` 文件并重新启动 `uv run main.py`。 -现在,您就可以在 `/config` 菜单的“切换模型” -> “语言模型 (LLM)” 选项中看到并选择 `sf-qwen2-57b` 了。 +现在,您就可以在 `/config` 菜单的“切换模型” -> “语言模型 (LLM)” 选项中看到并选择 `sf-glm-5-3` 了。 + +### 模型 ID 会退役,示例值不等于长期有效 + +各厂商会定期下线旧模型 ID,届时调用会直接失败(通常表现为 404 或 503 `model_not_found`), +错误来自服务端而非本项目。示例配置中的模型 ID 是写入时仍可用的值,长期使用前请在对应平台的 +模型列表中核对。 + +已知的退役规律与替代项见 `CHANGELOG.md` 的 `[1.4.0]` 节;概览: + +| 渠道 | 已退役 | 现行替代 | +| --- | --- | --- | +| Google | `text-embedding-004` | `gemini-embedding-2`(向量空间不兼容,需重建快照) | +| OpenAI | `gpt-4o`、`gpt-3.5-turbo` | `gpt-5.6-sol` / `gpt-5.6-terra` | +| Anthropic | `claude-3-5-sonnet-20240620` | `claude-sonnet-4-6` | +| DeepSeek | `deepseek-chat` | `deepseek-v4-pro` / `deepseek-v4-flash` | +| 火山方舟 | `doubao-pro-32k`、`bge-large-zh` | `doubao-seed-2-0-lite-260428`;向量化见下方说明 | +| SiliconFlow | `Qwen/Qwen2-7B-Instruct`、`Qwen/Qwen3-8B` | `Qwen/Qwen3.5-27B` | + +### 部分模型有专属参数限制 + +个别模型对请求字段有额外约束,本项目会在构造请求前显式报错,而不是把非法请求发到服务端: + +- **`kimi-k3`** 只接受 `temperature = 1`。项目默认 `chat_temperature = 0.7` 会触发 HTTP 400, + 需在该模型条目中覆盖: + + ```toml + [llm_configurations.kimi-k3] + provider = "openai" + model_name = "kimi-k3" + options = { temperature = 1 } + ``` + +- **Ark Responses 的 `instructions` 与 `caching` 互斥**:官方规定配置 `instructions` 后本轮请求 + 无法写入或使用缓存,`caching = {"type": "enabled"}` 时服务端直接报错。注意 + `CompletionRequest.system_prompt` 带兼容默认值,未显式传入时也会设置 `instructions`; + 如需两者并存,请显式传 `system_prompt=None` 并把系统提示放进 `messages`。 + diff --git a/docs/user_guide/user-guide.md b/docs/user_guide/user-guide.md index 8d5a4d5..c08294f 100644 --- a/docs/user_guide/user-guide.md +++ b/docs/user_guide/user-guide.md @@ -59,7 +59,7 @@ uv run python -m scripts.embed_knowledge_base --mode standard * **Rerank重排**: 启用或禁用 Rerank 模型进行二次精排。 * **混合搜索权重**: 当使用“混合检索”时,调整向量和关键词搜索结果的权重。 * **切换模型**: - * **语言模型 (LLM)**: 动态切换当前对话使用的聊天模型(例如,从 `google` 切换到 `openai-gpt4o`)。切换后,程序会自动重载模型。 + * **语言模型 (LLM)**: 动态切换当前对话使用的聊天模型(例如,从 `google` 切换到 `openai`)。切换后,程序会自动重载模型。 * **重排模型 (Rerank)**: 切换用于精排的 Rerank 模型。 * **查看当前完整配置**: 显示所有当前生效的聊天和模型参数。 diff --git a/licenses/APACHE-2.0.txt b/licenses/APACHE-2.0.txt new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/licenses/APACHE-2.0.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/logo_ASCII.py b/logo_ASCII.py index 688dfe6..fe82e7b 100644 --- a/logo_ASCII.py +++ b/logo_ASCII.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- +import pyfiglet from rich.console import Console from rich.text import Text -import pyfiglet -from typing import Tuple console = Console() -def create_gradient(text: str, start_color: Tuple[int, int, int], end_color: Tuple[int, int, int]) -> Text: + +def create_gradient( + text: str, start_color: tuple[int, int, int], end_color: tuple[int, int, int] +) -> Text: """为文本创建从左到右的水平颜色渐变效果。""" text_obj = Text() total_length = len(text) @@ -31,4 +32,4 @@ def display_banner(): console.print(gradient_banner) if __name__ == "__main__": - display_banner() \ No newline at end of file + display_banner() diff --git a/main.py b/main.py index 53434ea..efcb21a 100644 --- a/main.py +++ b/main.py @@ -1,35 +1,38 @@ -# -*- coding: utf-8 -*- -import sys import os -from io import StringIO +import sys from contextlib import redirect_stderr, redirect_stdout -from rich.console import Console -from rich.panel import Panel -from rich.text import Text +from io import StringIO + import pyfiglet -from typing import Tuple from prompt_toolkit import prompt from prompt_toolkit.formatted_text import HTML +from rich.console import Console +from rich.panel import Panel +from rich.text import Text os.environ.setdefault("PROMPT_TOOLKIT_NO_CPR", "1") # --- 绝对导入 --- # 导入 cleanup 模块以注册 atexit 钩子 import src.utils.cleanup -# 从新的配置模块导入 settings 实例 -from src.utils.config import get_settings, resolve_app_root + # 导入日志管理器,以便调用其清理函数 import src.utils.log_manager + # 导入UI工具 from src.ui.display_utils import CONSOLE_WIDTH +# 从新的配置模块导入 settings 实例 +from src.utils.config import get_settings, resolve_app_root +from src.utils.security import safe_exception_text + console = Console() -VERSION = "1.3.0" # 程序版本 +VERSION = "1.4.0" # 程序版本 # ================================================================= # 应用程序界面 (APP UI) # ================================================================= -def create_gradient(text: str, start_color: Tuple[int, int, int], end_color: Tuple[int, int, int]) -> Text: +def create_gradient(text: str, start_color: tuple[int, int, int], end_color: tuple[int, int, int]) -> Text: """为文本创建从左到右的水平颜色渐变效果。""" text_obj = Text() total_length = len(text) @@ -69,8 +72,7 @@ def display_banner(): # 计算填充,确保署名在右下角对齐 padding_size = banner_width - len(line_content) - len(attribution_text) - if padding_size < 1: - padding_size = 1 + padding_size = max(padding_size, 1) padding = Text(" " * padding_size) @@ -132,7 +134,7 @@ def initialize_dependencies(): import jieba.posseg as pseg jieba.setLogLevel(jieba.logging.ERROR) - setattr(jieba.dt, 'tmp_dir', str(cache_dir)) + jieba.dt.tmp_dir = str(cache_dir) list(pseg.cut("")) console.print("[dim]依赖项初始化完成。[/dim]") @@ -190,8 +192,8 @@ def main(): except (KeyboardInterrupt, EOFError): console.print("\n[bold yellow]检测到中断信号,正在退出程序... 再见![/bold yellow]") sys.exit(0) - except Exception as e: - console.print(f"\n[bold red]程序运行期间发生错误:[/bold red] {e}") + except Exception as e: # noqa: BLE001 - top-level CLI boundary reports failures + console.print(f"\n[bold red]程序运行期间发生错误:[/bold red] {safe_exception_text(e)}") console.print("[bold red]请检查错误信息并重试。[/bold red]") if __name__ == "__main__": diff --git a/pyproject.toml b/pyproject.toml index 0cae10a..3f45b8e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,39 +1,55 @@ [project] name = "pyrag-kit" -version = "1.3.0" +version = "1.4.0" description = "A lightweight Python implementation of Dify core RAG logic" readme = "README.md" authors = [{ name = "Mison", email = "1360962086@qq.com" }] license = { text = "MIT" } requires-python = ">=3.11" dependencies = [ - "anthropic>=0.86.0", - "faiss-cpu>=1.13.2", - "google-genai>=1.68.0", + "anthropic>=1.4.0", + "faiss-cpu>=1.15.0", + "google-genai>=2.22.0", "httpx>=0.28.1", "jieba>=0.42.1", - "langchain>=1.2.13", - "langchain-community>=0.4.1", - "langchain-core>=1.2.20", - "langchain-text-splitters>=1.1.1", - "numpy>=2.4.3", - "openai>=2.29.0", + "langchain>=1.4.0", + "langchain-community>=0.4.2", + "langchain-core>=1.6.2", + "langchain-text-splitters>=1.1.2", + "numpy>=2.4.6", + "openai>=3.10.0", "openpyxl>=3.1.5", - "prompt-toolkit>=3.0.52", - "pydantic>=2.12.5", - "pydantic-settings>=2.13.1", + "prompt-toolkit>=3.0.53", + "pydantic>=2.13.5", + "pydantic-settings>=2.15.0", "pyfiglet>=1.0.4", "questionary>=2.1.1", "rank-bm25>=0.2.2", - "requests>=2.32.5", - "rich>=14.3.3", + "requests>=2.34.2", + "rich>=15.0.0", "tenacity>=9.1.4", - "tiktoken>=0.12.0", - "volcengine-python-sdk[ark]>=5.0.18", + "tiktoken>=0.14.0", + "volcengine-python-sdk[ark]>=5.0.48", ] [dependency-groups] dev = [ - "pyinstaller>=6.16.0", - "pytest>=9.0.2", + "bandit>=1.9.4", + "mypy>=2.3.1", + "pyinstaller>=6.22.2", + "pytest>=9.1.1", + "ruff>=0.16.6", ] + +[tool.ruff.lint] +# Provider and configuration boundaries intentionally expose ValueError for +# malformed values as part of the existing public contract. +ignore = ["TRY004"] + +[tool.mypy] +# jieba does not publish typing metadata; the application boundary keeps its +# use isolated in main.py, so missing third-party stubs should not hide errors +# in the repository's own modules. +[[tool.mypy.overrides]] +module = ["jieba", "jieba.posseg"] +ignore_missing_imports = true diff --git a/pytest.ini b/pytest.ini index 109f387..c501f07 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,6 +1,9 @@ [pytest] pythonpath = . testpaths = tests +# langsmith is a transitive dependency of langchain and its pytest plugin +# imports the full tracing SDK during collection; PyRAG-Kit does not use it. +addopts = -p no:langsmith # 忽略第三方库的 DeprecationWarning 和 UserWarning filterwarnings = ignore::DeprecationWarning diff --git a/requirements.txt b/requirements.txt index f7b92f2..361ccb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,361 +1,528 @@ # This file was autogenerated by uv via the following command: -# uv export --format requirements-txt --output-file requirements.txt -aiohappyeyeballs==2.6.1 \ - --hash=sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558 \ - --hash=sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8 +# uv export --format requirements.txt --output-file requirements.txt --locked +aiohappyeyeballs==2.7.1 \ + --hash=sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d \ + --hash=sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472 # via aiohttp -aiohttp==3.13.3 \ - --hash=sha256:01ad2529d4b5035578f5081606a465f3b814c542882804e2e8cda61adf5c71bf \ - --hash=sha256:042e9e0bcb5fba81886c8b4fbb9a09d6b8a00245fd8d88e4d989c1f96c74164c \ - --hash=sha256:05861afbbec40650d8a07ea324367cb93e9e8cc7762e04dd4405df99fa65159c \ - --hash=sha256:084911a532763e9d3dd95adf78a78f4096cd5f58cdc18e6fdbc1b58417a45423 \ - --hash=sha256:0add0900ff220d1d5c5ebbf99ed88b0c1bbf87aa7e4262300ed1376a6b13414f \ - --hash=sha256:10b47b7ba335d2e9b1239fa571131a87e2d8ec96b333e68b2a305e7a98b0bae2 \ - --hash=sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf \ - --hash=sha256:1cb93e166e6c28716c8c6aeb5f99dfb6d5ccf482d29fe9bf9a794110e6d0ab64 \ - --hash=sha256:2712039939ec963c237286113c68dbad80a82a4281543f3abf766d9d73228998 \ - --hash=sha256:27234ef6d85c914f9efeb77ff616dbf4ad2380be0cda40b4db086ffc7ddd1b7d \ - --hash=sha256:28e027cf2f6b641693a09f631759b4d9ce9165099d2b5d92af9bd4e197690eea \ - --hash=sha256:2b8d8ddba8f95ba17582226f80e2de99c7a7948e66490ef8d947e272a93e9463 \ - --hash=sha256:2be0e9ccf23e8a94f6f0650ce06042cefc6ac703d0d7ab6c7a917289f2539ad4 \ - --hash=sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767 \ - --hash=sha256:2eb752b102b12a76ca02dff751a801f028b4ffbbc478840b473597fc91a9ed43 \ - --hash=sha256:2fc82186fadc4a8316768d61f3722c230e2c1dcab4200d52d2ebdf2482e47592 \ - --hash=sha256:2fff83cfc93f18f215896e3a190e8e5cb413ce01553901aca925176e7568963a \ - --hash=sha256:34749271508078b261c4abb1767d42b8d0c0cc9449c73a4df494777dc55f0687 \ - --hash=sha256:34bac00a67a812570d4a460447e1e9e06fae622946955f939051e7cc895cfab8 \ - --hash=sha256:37239e9f9a7ea9ac5bf6b92b0260b01f8a22281996da609206a84df860bc1261 \ - --hash=sha256:3b61b7169ababd7802f9568ed96142616a9118dd2be0d1866e920e77ec8fa92a \ - --hash=sha256:3d9908a48eb7416dc1f4524e69f1d32e5d90e3981e4e37eb0aa1cd18f9cfa2a4 \ - --hash=sha256:3dd4dce1c718e38081c8f35f323209d4c1df7d4db4bab1b5c88a6b4d12b74587 \ - --hash=sha256:4021b51936308aeea0367b8f006dc999ca02bc118a0cc78c303f50a2ff6afb91 \ - --hash=sha256:425c126c0dc43861e22cb1c14ba4c8e45d09516d0a3ae0a3f7494b79f5f233a3 \ - --hash=sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344 \ - --hash=sha256:48e377758516d262bde50c2584fc6c578af272559c409eecbdd2bae1601184d6 \ - --hash=sha256:49a03727c1bba9a97d3e93c9f93ca03a57300f484b6e935463099841261195d3 \ - --hash=sha256:568f416a4072fbfae453dcf9a99194bbb8bdeab718e08ee13dfa2ba0e4bebf29 \ - --hash=sha256:5b179331a481cb5529fca8b432d8d3c7001cb217513c94cd72d668d1248688a3 \ - --hash=sha256:5b6073099fb654e0a068ae678b10feff95c5cae95bbfcbfa7af669d361a8aa6b \ - --hash=sha256:5d2d94f1f5fcbe40838ac51a6ab5704a6f9ea42e72ceda48de5e6b898521da51 \ - --hash=sha256:5dff64413671b0d3e7d5918ea490bdccb97a4ad29b3f311ed423200b2203e01c \ - --hash=sha256:5e1d8c8b8f1d91cd08d8f4a3c2b067bfca6ec043d3ff36de0f3a715feeedf926 \ - --hash=sha256:5f8ca7f2bb6ba8348a3614c7918cc4bb73268c5ac2a207576b7afea19d3d9f64 \ - --hash=sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f \ - --hash=sha256:65d2ccb7eabee90ce0503c17716fc77226be026dcc3e65cce859a30db715025b \ - --hash=sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e \ - --hash=sha256:694976222c711d1d00ba131904beb60534f93966562f64440d0c9d41b8cdb440 \ - --hash=sha256:697753042d57f4bf7122cab985bf15d0cef23c770864580f5af4f52023a56bd6 \ - --hash=sha256:6de499a1a44e7de70735d0b39f67c8f25eb3d91eb3103be99ca0fa882cdd987d \ - --hash=sha256:6fc0e2337d1a4c3e6acafda6a78a39d4c14caea625124817420abceed36e2415 \ - --hash=sha256:7a4a94eb787e606d0a09404b9c38c113d3b099d508021faa615d70a0131907ce \ - --hash=sha256:7b5e8fe4de30df199155baaf64f2fcd604f4c678ed20910db8e2c66dc4b11603 \ - --hash=sha256:7bfdc049127717581866fa4708791220970ce291c23e28ccf3922c700740fdc0 \ - --hash=sha256:7f9120f7093c2a32d9647abcaf21e6ad275b4fbec5b55969f978b1a97c7c86bf \ - --hash=sha256:8057c98e0c8472d8846b9c79f56766bcc57e3e8ac7bfd510482332366c56c591 \ - --hash=sha256:80dd4c21b0f6237676449c6baaa1039abae86b91636b6c91a7f8e61c87f89540 \ - --hash=sha256:82611aeec80eb144416956ec85b6ca45a64d76429c1ed46ae1b5f86c6e0c9a26 \ - --hash=sha256:8542f41a62bcc58fc7f11cf7c90e0ec324ce44950003feb70640fc2a9092c32a \ - --hash=sha256:87797e645d9d8e222e04160ee32aa06bc5c163e8499f24db719e7852ec23093a \ - --hash=sha256:87b9aab6d6ed88235aa2970294f496ff1a1f9adcd724d800e9b952395a80ffd9 \ - --hash=sha256:90455115e5da1c3c51ab619ac57f877da8fd6d73c05aacd125c5ae9819582aba \ - --hash=sha256:90751b8eed69435bac9ff4e3d2f6b3af1f57e37ecb0fbeee59c0174c9e2d41df \ - --hash=sha256:96d604498a7c782cb15a51c406acaea70d8c027ee6b90c569baa6e7b93073679 \ - --hash=sha256:9ae8dd55c8e6c4257eae3a20fd2c8f41edaea5992ed67156642493b8daf3cecc \ - --hash=sha256:9af5e68ee47d6534d36791bbe9b646d2a7c7deb6fc24d7943628edfbb3581f29 \ - --hash=sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984 \ - --hash=sha256:9d4c940f02f49483b18b079d1c27ab948721852b281f8b015c058100e9421dd1 \ - --hash=sha256:a19884d2ee70b06d9204b2727a7b9f983d0c684c650254679e716b0b77920632 \ - --hash=sha256:a1e53262fd202e4b40b70c3aff944a8155059beedc8a89bba9dc1f9ef06a1b56 \ - --hash=sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239 \ - --hash=sha256:a45530014d7a1e09f4a55f4f43097ba0fd155089372e105e4bff4ca76cb1b168 \ - --hash=sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88 \ - --hash=sha256:add1da70de90a2569c5e15249ff76a631ccacfe198375eead4aadf3b8dc849dc \ - --hash=sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046 \ - --hash=sha256:b0d95340658b9d2f11d9697f59b3814a9d3bb4b7a7c20b131df4bcef464037c0 \ - --hash=sha256:b1a6102b4d3ebc07dad44fbf07b45bb600300f15b552ddf1851b5390202ea2e3 \ - --hash=sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1 \ - --hash=sha256:b903a4dfee7d347e2d87697d0713be59e0b87925be030c9178c5faa58ea58d5c \ - --hash=sha256:b928f30fe49574253644b1ca44b1b8adbd903aa0da4b9054a6c20fc7f4092a25 \ - --hash=sha256:bb4f7475e359992b580559e008c598091c45b5088f28614e855e42d39c2f1033 \ - --hash=sha256:bbe7d4cecacb439e2e2a8a1a7b935c25b812af7a5fd26503a66dadf428e79ec1 \ - --hash=sha256:c014c7ea7fb775dd015b2d3137378b7be0249a448a1612268b5a90c2d81de04d \ - --hash=sha256:c19b90316ad3b24c69cd78d5c9b4f3aa4497643685901185b65166293d36a00f \ - --hash=sha256:c685f2d80bb67ca8c3837823ad76196b3694b0159d232206d1e461d3d434666f \ - --hash=sha256:d32764c6c9aafb7fb55366a224756387cd50bfa720f32b88e0e6fa45b27dcf29 \ - --hash=sha256:d60ac9663f44168038586cab2157e122e46bdef09e9368b37f2d82d354c23f72 \ - --hash=sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57 \ - --hash=sha256:e636b3c5f61da31a92bf0d91da83e58fdfa96f178ba682f11d24f31944cdd28c \ - --hash=sha256:ea37047c6b367fd4bd632bff8077449b8fa034b69e812a18e0132a00fae6e808 \ - --hash=sha256:f33ed1a2bf1997a36661874b017f5c4b760f41266341af36febaf271d179f6d7 \ - --hash=sha256:f76c1e3fe7d7c8afad7ed193f89a292e1999608170dcc9751a7462a87dfd5bc0 \ - --hash=sha256:f9444f105664c4ce47a2a7171a2418bce5b7bae45fb610f4e2c36045d85911d3 \ - --hash=sha256:fc290605db2a917f6e81b0e1e0796469871f5af381ce15c604a3c5c7e51cb730 \ - --hash=sha256:fc353029f176fd2b3ec6cfc71be166aba1936fe5d73dd1992ce289ca6647a9aa +aiohttp==3.14.3 \ + --hash=sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39 \ + --hash=sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043 \ + --hash=sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b \ + --hash=sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf \ + --hash=sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7 \ + --hash=sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc \ + --hash=sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559 \ + --hash=sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f \ + --hash=sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929 \ + --hash=sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147 \ + --hash=sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf \ + --hash=sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7 \ + --hash=sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8 \ + --hash=sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85 \ + --hash=sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30 \ + --hash=sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553 \ + --hash=sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7 \ + --hash=sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86 \ + --hash=sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e \ + --hash=sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a \ + --hash=sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c \ + --hash=sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da \ + --hash=sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5 \ + --hash=sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d \ + --hash=sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100 \ + --hash=sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71 \ + --hash=sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22 \ + --hash=sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1 \ + --hash=sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb \ + --hash=sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062 \ + --hash=sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661 \ + --hash=sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427 \ + --hash=sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a \ + --hash=sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db \ + --hash=sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42 \ + --hash=sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a \ + --hash=sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd \ + --hash=sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06 \ + --hash=sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8 \ + --hash=sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228 \ + --hash=sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0 \ + --hash=sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919 \ + --hash=sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee \ + --hash=sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f \ + --hash=sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098 \ + --hash=sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c \ + --hash=sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371 \ + --hash=sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b \ + --hash=sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0 \ + --hash=sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f \ + --hash=sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100 \ + --hash=sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529 \ + --hash=sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc \ + --hash=sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c \ + --hash=sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41 \ + --hash=sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716 \ + --hash=sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33 \ + --hash=sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa \ + --hash=sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b \ + --hash=sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80 \ + --hash=sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646 \ + --hash=sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e \ + --hash=sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b \ + --hash=sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c \ + --hash=sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963 \ + --hash=sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae \ + --hash=sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25 \ + --hash=sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f \ + --hash=sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807 \ + --hash=sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f \ + --hash=sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d \ + --hash=sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82 \ + --hash=sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15 \ + --hash=sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0 \ + --hash=sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9 \ + --hash=sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19 \ + --hash=sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239 \ + --hash=sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0 \ + --hash=sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c \ + --hash=sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5 \ + --hash=sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b \ + --hash=sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4 \ + --hash=sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2 \ + --hash=sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9 \ + --hash=sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0 \ + --hash=sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883 \ + --hash=sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d \ + --hash=sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d \ + --hash=sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6 \ + --hash=sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3 \ + --hash=sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924 \ + --hash=sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde \ + --hash=sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787 \ + --hash=sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0 \ + --hash=sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910 \ + --hash=sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9 \ + --hash=sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627 \ + --hash=sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce \ + --hash=sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a \ + --hash=sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0 \ + --hash=sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24 # via langchain-community aiosignal==1.4.0 \ --hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \ --hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7 # via aiohttp -annotated-types==0.7.0 \ - --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ - --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 +altgraph==0.17.5 \ + --hash=sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7 \ + --hash=sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597 + # via + # macholib + # pyinstaller +annotated-types==0.8.0 \ + --hash=sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7 \ + --hash=sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0 # via pydantic -anthropic==0.86.0 \ - --hash=sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5 \ - --hash=sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57 +anthropic==1.4.0 \ + --hash=sha256:590e85bff75b713a123b03f586d68f02266b5fdc49f70dd75f721ced93a4716c \ + --hash=sha256:f0d017e901e48b343520b5d458f8240c283c8d850bf6d119834c622207e0a74c # via pyrag-kit -anyio==4.12.1 \ - --hash=sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703 \ - --hash=sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c +anyio==4.15.1 \ + --hash=sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101 \ + --hash=sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94 # via # anthropic + # google-genai # httpx + # httpx2 + # langsmith # openai + # volcengine-python-sdk +ast-serialize==0.9.0 \ + --hash=sha256:1514f4a39704e2e815f9fc675fc13f19f694f212086b520110840782cf3c5295 \ + --hash=sha256:161914666a21d48b681982146ac0fa4086ef099d91c637cf595387f5f06aa099 \ + --hash=sha256:1675dc46578298ae00936164a160997801a6ca2385913150d8d16df634296cf3 \ + --hash=sha256:19cde5c2110f7b90ab1210a599178524f6c9f34862b20ba2b9aa7832c67bb35d \ + --hash=sha256:1b779fdaee34d19900a5ba5fd6bd4cefe225650081f9de28de256eacee5113c2 \ + --hash=sha256:1de0933a4c1d104d77d6e75f053f5e628b54cf8f9fea809b8250cb04cda07bd3 \ + --hash=sha256:1e05701fde79affa1cc53e391867f9da3eb03fa8501f87354292796b0f8398fd \ + --hash=sha256:1ed5824b4b2fa37ad93fa2db23d8243a3d315b3e2d7ca70f99b2727d8788af05 \ + --hash=sha256:207ac73afa1f4654840593853c130eac2591dd942434176eea33f730afb3359b \ + --hash=sha256:20fce3885eeff05a3d6afefa845c8168016e3ea1f6fc9cdc84c8db28b863a550 \ + --hash=sha256:2223ead73b5a5399d39610cf9c4164ad0b2bf2025226626b87ae15226d93d3f7 \ + --hash=sha256:24de2bc930b7e1ca86641136b9875a1e5f80f52b484deddc67893eeaf9077bd9 \ + --hash=sha256:25c8d517c45cf2b1820fc2af6ac593783654818f79d05646d25d624360678a4e \ + --hash=sha256:30651ccdec6d23c49ee4711b1a1096d8dbd3be38eecf2f09fdd98a608ce7ac24 \ + --hash=sha256:322282ac5337e5e776bc416f2b5201e680fa4dacf92ea739bd35e46a28a66c41 \ + --hash=sha256:383f56e3ae925f154632458f01b4bfcde3dd382f3ec04f5c7f6d72f76524ff48 \ + --hash=sha256:3ee40752ddb4fb5c6a67161d13f3ce3df7987dcb9272260542a86b0be1519ae2 \ + --hash=sha256:41da4332492222d56345d5e436eed4fbec76caadee959f6ffa3cd2fc1bd51895 \ + --hash=sha256:4411d1cba9eeecb301365343a7e96813b4a44fcdb20181557867ff7e751804cf \ + --hash=sha256:4c522377f383670abfe21c94edc3032cb3bd34d8fcacd280fa9556907d4edd4b \ + --hash=sha256:4cd886f6e900f5e13f758cb8ef359652e690e4f3f9c257a7269e095940534167 \ + --hash=sha256:4db7f524eaa857fbe650cac33b9cedb5ccda14393d40f640b75dfb06aa13c98c \ + --hash=sha256:54f0babed5e2a4eb86a0716ac612aff33f933e7572e5bc067adcdbe672a26321 \ + --hash=sha256:5a4dd005d4095a13eb312dc712c943c7730f262b000a87df963925328a38ffdb \ + --hash=sha256:5fc57f17fb4ce49b4eeccfcd2670e4a55659bd740bb8e8aedbe511ccab8b5f03 \ + --hash=sha256:62a96e327e2a178d6c295b10422e95c992a9286f0ec1b2bc7cd5b4873252a38c \ + --hash=sha256:6b7c5f5838408fb000d76abd14e886836412b7ec7eccd028dbb5ed5819780008 \ + --hash=sha256:6c428444656cffbd32c1e76626c6eec5237b58c8fcb0b5d3df75941cd50c4f3c \ + --hash=sha256:74473258a5c55855d5306c864a5c799fbff03a0f0ea1197346b2b5cc5b4ea48a \ + --hash=sha256:79fe8be1c934aa572940d1811d8dbe4d1b6f22291e3f16755c9b062e9ac92fb7 \ + --hash=sha256:7ac7b4cdf89ca8318aae157d824017596784982851c8a89a621973f261574696 \ + --hash=sha256:7e9f2540741ad10657a209209f7e5cc6b530eb3ed145fd77258ab43542d96ad7 \ + --hash=sha256:871fb7c5b049897ee137b67efad7fe4545ad270f7eccd970da877833f8e63aa7 \ + --hash=sha256:9482672ca8ec09f85cd050a053fb88c30c882c8e20ce7a140d8defe19c0ef2eb \ + --hash=sha256:95dcb93f30258dcf09d9b6a302dac9323b70e9df8c18ee0bf4ea1fa7cc5f1875 \ + --hash=sha256:9ae01129e2cc5d57a3c434d8a990019de039350310a2e1dd3c9f61311964cf25 \ + --hash=sha256:a6a568d1d489f0669a31aed90ca4845aa7f08e1b8cd5d05e1905dbdc3ae9b2b0 \ + --hash=sha256:a95485d5e8704af2ecc7f723757b88f992ae8122028d687ccf877cad2b4c3da4 \ + --hash=sha256:ae1c46eb97865823f9843c4b80145e011874923e1a4a44b45738a5309d83e9f5 \ + --hash=sha256:aed6e413c6c22a23c33c47a01dd2adce01d7a7ed408748e896903f47d0a1aa47 \ + --hash=sha256:af082cb7e6c4fa3a428aa616c13d709a944076eba84a73184de15621cc1a915d \ + --hash=sha256:b373beff65b01fcffca5aaad3269ae629f3a998b09efdd3635e48039008a5dec \ + --hash=sha256:b559cffac5a71a698d9194e4295765ff2132a10fd1284860f02b30f12c1f729e \ + --hash=sha256:b5c3724faf780e25def89c369eb6340a15dff06ac348160c61781e2373a4cd10 \ + --hash=sha256:b9ef3d4173907bd19aa8f1683be9f06e7862e6cdf2ca6bca3633305c0df32063 \ + --hash=sha256:bb378efb5537b43f38660e2e6d6e138a40885cf191d43443bb3ff7ff47e9cd9b \ + --hash=sha256:c55010ea0fafc6bc8231809d328bda781bfe41a01c43522be5eb3713fd855cda \ + --hash=sha256:d013c36eb2f2ac0cb7d4d0e79918a92ab00fbce8f1542fe47f34a46e06168f82 \ + --hash=sha256:d2a37795a90809da6094825e7063118b4cf723b8701b134973b4566ed8b9ea09 \ + --hash=sha256:d9a46caf5e3f2cd266e8638b2f4ea8bf54cf376f015f8418397b4633fdb38e9b \ + --hash=sha256:d9c635eacfc02b91da6796d3b5ff9086e511b8a29b19f9b3f4f978b8d170f838 \ + --hash=sha256:dac690f99538d9df0d23ce0299e946add2744b007a36b480a292fe361c82553d \ + --hash=sha256:dad8a3f7106efcf252fc289c092ee0cee5c3512c0088bcf3fffa01458323092f \ + --hash=sha256:ded5ed06ec469407d6cd571ace7a7a25809cc388e4bac1dd35c7747469fc7fdf \ + --hash=sha256:e3c69f2ce565c786bd3853ce42495e8a63610516f79651c7cb4f2cd0ddfaee52 \ + --hash=sha256:f3f0f3359cf0f22bf096b07021ffe6bf0ec88ac8a2cf7ce5f4701af973112faa \ + --hash=sha256:f8c824d822a2ac54ec4228b88c0d170ab0024cf285eeee57b9d4f994003fb553 + # via mypy attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via aiohttp -certifi==2026.2.25 \ - --hash=sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa \ - --hash=sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7 +bandit==1.9.4 \ + --hash=sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628 \ + --hash=sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e +certifi==2026.7.22 \ + --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \ + --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55 # via # httpcore # httpx # requests -cffi==2.0.0 ; platform_python_implementation != 'PyPy' \ - --hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \ - --hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \ - --hash=sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f \ - --hash=sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9 \ - --hash=sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c \ - --hash=sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75 \ - --hash=sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e \ - --hash=sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e \ - --hash=sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25 \ - --hash=sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe \ - --hash=sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b \ - --hash=sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91 \ - --hash=sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592 \ - --hash=sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187 \ - --hash=sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c \ - --hash=sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1 \ - --hash=sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94 \ - --hash=sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba \ - --hash=sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529 \ - --hash=sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca \ - --hash=sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6 \ - --hash=sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743 \ - --hash=sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5 \ - --hash=sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5 \ - --hash=sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4 \ - --hash=sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d \ - --hash=sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b \ - --hash=sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93 \ - --hash=sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205 \ - --hash=sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27 \ - --hash=sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512 \ - --hash=sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d \ - --hash=sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c \ - --hash=sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037 \ - --hash=sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26 \ - --hash=sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c \ - --hash=sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8 \ - --hash=sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414 \ - --hash=sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9 \ - --hash=sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664 \ - --hash=sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9 \ - --hash=sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775 \ - --hash=sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc \ - --hash=sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062 \ - --hash=sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe \ - --hash=sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92 \ - --hash=sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5 \ - --hash=sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13 \ - --hash=sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d \ - --hash=sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26 \ - --hash=sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b \ - --hash=sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6 \ - --hash=sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c \ - --hash=sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef \ - --hash=sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5 \ - --hash=sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18 \ - --hash=sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad \ - --hash=sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3 \ - --hash=sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2 \ - --hash=sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5 + # volcengine-python-sdk +cffi==2.1.1 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e \ + --hash=sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66 \ + --hash=sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2 \ + --hash=sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0 \ + --hash=sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971 \ + --hash=sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c \ + --hash=sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d \ + --hash=sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517 \ + --hash=sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735 \ + --hash=sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80 \ + --hash=sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f \ + --hash=sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1 \ + --hash=sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29 \ + --hash=sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8 \ + --hash=sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c \ + --hash=sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e \ + --hash=sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48 \ + --hash=sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813 \ + --hash=sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac \ + --hash=sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632 \ + --hash=sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6 \ + --hash=sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1 \ + --hash=sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688 \ + --hash=sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0 \ + --hash=sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062 \ + --hash=sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779 \ + --hash=sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94 \ + --hash=sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50 \ + --hash=sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab \ + --hash=sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac \ + --hash=sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6 \ + --hash=sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676 \ + --hash=sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1 \ + --hash=sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13 \ + --hash=sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e \ + --hash=sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e \ + --hash=sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973 \ + --hash=sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527 \ + --hash=sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72 \ + --hash=sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890 \ + --hash=sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c \ + --hash=sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990 \ + --hash=sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd \ + --hash=sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94 \ + --hash=sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3 \ + --hash=sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80 \ + --hash=sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5 \ + --hash=sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c \ + --hash=sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a \ + --hash=sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4 \ + --hash=sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e \ + --hash=sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6 \ + --hash=sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b \ + --hash=sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03 \ + --hash=sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af \ + --hash=sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231 \ + --hash=sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2 \ + --hash=sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3 \ + --hash=sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836 \ + --hash=sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5 \ + --hash=sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399 \ + --hash=sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96 \ + --hash=sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e \ + --hash=sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf \ + --hash=sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc \ + --hash=sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455 \ + --hash=sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0 \ + --hash=sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12 \ + --hash=sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7 \ + --hash=sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692 \ + --hash=sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54 \ + --hash=sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3 \ + --hash=sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b \ + --hash=sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be \ + --hash=sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d \ + --hash=sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358 \ + --hash=sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a \ + --hash=sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7 \ + --hash=sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc \ + --hash=sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960 \ + --hash=sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125 \ + --hash=sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb \ + --hash=sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a \ + --hash=sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa \ + --hash=sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf \ + --hash=sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3 \ + --hash=sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4 \ + --hash=sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264 # via cryptography -charset-normalizer==3.4.6 \ - --hash=sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e \ - --hash=sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c \ - --hash=sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5 \ - --hash=sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815 \ - --hash=sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f \ - --hash=sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484 \ - --hash=sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407 \ - --hash=sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6 \ - --hash=sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815 \ - --hash=sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2 \ - --hash=sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579 \ - --hash=sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f \ - --hash=sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab \ - --hash=sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a \ - --hash=sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84 \ - --hash=sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f \ - --hash=sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1 \ - --hash=sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843 \ - --hash=sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c \ - --hash=sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7 \ - --hash=sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f \ - --hash=sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0 \ - --hash=sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9 \ - --hash=sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389 \ - --hash=sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0 \ - --hash=sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30 \ - --hash=sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd \ - --hash=sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e \ - --hash=sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9 \ - --hash=sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc \ - --hash=sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d \ - --hash=sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2 \ - --hash=sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f \ - --hash=sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e \ - --hash=sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff \ - --hash=sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398 \ - --hash=sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db \ - --hash=sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a \ - --hash=sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43 \ - --hash=sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c \ - --hash=sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e \ - --hash=sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2 \ - --hash=sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e \ - --hash=sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4 \ - --hash=sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6 \ - --hash=sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5 \ - --hash=sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194 \ - --hash=sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69 \ - --hash=sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f \ - --hash=sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316 \ - --hash=sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e \ - --hash=sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73 \ - --hash=sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8 \ - --hash=sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923 \ - --hash=sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88 \ - --hash=sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f \ - --hash=sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21 \ - --hash=sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4 \ - --hash=sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2 \ - --hash=sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866 \ - --hash=sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021 \ - --hash=sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2 \ - --hash=sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d \ - --hash=sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8 \ - --hash=sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de \ - --hash=sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4 \ - --hash=sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb \ - --hash=sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc \ - --hash=sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602 \ - --hash=sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4 \ - --hash=sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8 \ - --hash=sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf \ - --hash=sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d \ - --hash=sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077 \ - --hash=sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef \ - --hash=sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e \ - --hash=sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421 \ - --hash=sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff \ - --hash=sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc \ - --hash=sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d \ - --hash=sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed \ - --hash=sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659 +charset-normalizer==3.5.1 \ + --hash=sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5 \ + --hash=sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b \ + --hash=sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f \ + --hash=sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5 \ + --hash=sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22 \ + --hash=sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5 \ + --hash=sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac \ + --hash=sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90 \ + --hash=sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626 \ + --hash=sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4 \ + --hash=sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369 \ + --hash=sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b \ + --hash=sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e \ + --hash=sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee \ + --hash=sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1 \ + --hash=sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102 \ + --hash=sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8 \ + --hash=sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9 \ + --hash=sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0 \ + --hash=sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031 \ + --hash=sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e \ + --hash=sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235 \ + --hash=sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072 \ + --hash=sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb \ + --hash=sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c \ + --hash=sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950 \ + --hash=sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2 \ + --hash=sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e \ + --hash=sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6 \ + --hash=sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2 \ + --hash=sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa \ + --hash=sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2 \ + --hash=sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032 \ + --hash=sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71 \ + --hash=sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96 \ + --hash=sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687 \ + --hash=sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8 \ + --hash=sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3 \ + --hash=sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61 \ + --hash=sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9 \ + --hash=sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1 \ + --hash=sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4 \ + --hash=sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65 \ + --hash=sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663 \ + --hash=sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f \ + --hash=sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591 \ + --hash=sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a \ + --hash=sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e \ + --hash=sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7 \ + --hash=sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3 \ + --hash=sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c \ + --hash=sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3 \ + --hash=sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7 \ + --hash=sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96 \ + --hash=sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486 \ + --hash=sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3 \ + --hash=sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6 \ + --hash=sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b \ + --hash=sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731 \ + --hash=sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959 \ + --hash=sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9 \ + --hash=sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf \ + --hash=sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8 \ + --hash=sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e \ + --hash=sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885 \ + --hash=sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0 \ + --hash=sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506 \ + --hash=sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2 \ + --hash=sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0 \ + --hash=sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e \ + --hash=sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e \ + --hash=sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491 \ + --hash=sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a \ + --hash=sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20 \ + --hash=sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449 \ + --hash=sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af \ + --hash=sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c \ + --hash=sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712 \ + --hash=sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7 \ + --hash=sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a \ + --hash=sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9 \ + --hash=sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e \ + --hash=sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5 \ + --hash=sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36 \ + --hash=sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263 \ + --hash=sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4 \ + --hash=sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11 \ + --hash=sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a \ + --hash=sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3 \ + --hash=sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375 \ + --hash=sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d \ + --hash=sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99 \ + --hash=sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c \ + --hash=sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488 \ + --hash=sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6 \ + --hash=sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc \ + --hash=sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b \ + --hash=sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f \ + --hash=sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00 \ + --hash=sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10 \ + --hash=sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598 \ + --hash=sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6 \ + --hash=sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962 \ + --hash=sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c \ + --hash=sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08 \ + --hash=sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573 \ + --hash=sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90 \ + --hash=sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5 \ + --hash=sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18 \ + --hash=sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af \ + --hash=sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c \ + --hash=sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b \ + --hash=sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6 \ + --hash=sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8 \ + --hash=sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774 \ + --hash=sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004 \ + --hash=sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a \ + --hash=sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2 \ + --hash=sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2 \ + --hash=sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe \ + --hash=sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3 \ + --hash=sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc \ + --hash=sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d \ + --hash=sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc \ + --hash=sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893 \ + --hash=sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef \ + --hash=sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d \ + --hash=sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30 \ + --hash=sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7 \ + --hash=sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5 \ + --hash=sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f \ + --hash=sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9 \ + --hash=sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada \ + --hash=sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876 \ + --hash=sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a \ + --hash=sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348 \ + --hash=sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3 \ + --hash=sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f \ + --hash=sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0 \ + --hash=sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f # via requests colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via tqdm -cryptography==46.0.5 \ - --hash=sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72 \ - --hash=sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235 \ - --hash=sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9 \ - --hash=sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356 \ - --hash=sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257 \ - --hash=sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad \ - --hash=sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4 \ - --hash=sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c \ - --hash=sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614 \ - --hash=sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed \ - --hash=sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31 \ - --hash=sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229 \ - --hash=sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0 \ - --hash=sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731 \ - --hash=sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b \ - --hash=sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4 \ - --hash=sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4 \ - --hash=sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263 \ - --hash=sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595 \ - --hash=sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1 \ - --hash=sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678 \ - --hash=sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48 \ - --hash=sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76 \ - --hash=sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0 \ - --hash=sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18 \ - --hash=sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d \ - --hash=sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d \ - --hash=sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1 \ - --hash=sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981 \ - --hash=sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7 \ - --hash=sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82 \ - --hash=sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2 \ - --hash=sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4 \ - --hash=sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663 \ - --hash=sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c \ - --hash=sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d \ - --hash=sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a \ - --hash=sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a \ - --hash=sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d \ - --hash=sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b \ - --hash=sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a \ - --hash=sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826 \ - --hash=sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee \ - --hash=sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9 \ - --hash=sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648 \ - --hash=sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da \ - --hash=sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2 \ - --hash=sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2 \ - --hash=sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87 - # via google-auth -dataclasses-json==0.6.7 \ - --hash=sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a \ - --hash=sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0 - # via langchain-community + # via + # bandit + # pytest +cryptography==50.0.1 \ + --hash=sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71 \ + --hash=sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23 \ + --hash=sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6 \ + --hash=sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e \ + --hash=sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361 \ + --hash=sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054 \ + --hash=sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f \ + --hash=sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6 \ + --hash=sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49 \ + --hash=sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5 \ + --hash=sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149 \ + --hash=sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88 \ + --hash=sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad \ + --hash=sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a \ + --hash=sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f \ + --hash=sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2 \ + --hash=sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20 \ + --hash=sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45 \ + --hash=sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f \ + --hash=sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b \ + --hash=sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527 \ + --hash=sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3 \ + --hash=sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6 \ + --hash=sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367 \ + --hash=sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0 \ + --hash=sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94 \ + --hash=sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239 \ + --hash=sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b \ + --hash=sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a \ + --hash=sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9 \ + --hash=sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5 \ + --hash=sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc \ + --hash=sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648 \ + --hash=sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986 \ + --hash=sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959 \ + --hash=sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0 \ + --hash=sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17 \ + --hash=sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e \ + --hash=sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733 \ + --hash=sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f \ + --hash=sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8 \ + --hash=sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf \ + --hash=sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671 \ + --hash=sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80 \ + --hash=sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558 \ + --hash=sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef + # via + # google-auth + # volcengine-python-sdk distro==1.9.0 \ --hash=sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed \ --hash=sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2 # via - # anthropic - # openai -docstring-parser==0.17.0 \ - --hash=sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912 \ - --hash=sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708 + # google-genai + # langsmith +docstring-parser==0.18.0 \ + --hash=sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015 \ + --hash=sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b # via anthropic et-xmlfile==2.0.0 \ --hash=sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa \ --hash=sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54 # via openpyxl -faiss-cpu==1.13.2 \ - --hash=sha256:0ee330a284042c2480f2e90450a10378fd95655d62220159b1408f59ee83ebf1 \ - --hash=sha256:1243967eeb2298791ff7f3683a4abd2100d7e6ec7542ca05c3b75d47a7f621e5 \ - --hash=sha256:19508a1badfb36e456c1c8664eeb948349f604db5c7545f277a0126b4a84b080 \ - --hash=sha256:2c4f696ae76e7c97cbc12311db83aaf1e7f4f7be06a3ffea7e5b0e8ec1fd805b \ - --hash=sha256:7c5944d7807d58fe7244b6aba06be710ee7ed99343365ed92699349efe979f51 \ - --hash=sha256:8113a2a80b59fe5653cf66f5c0f18be0a691825601a52a614c30beb1fca9bc7c \ - --hash=sha256:85511129b34f890d19c98b82a0cd5ffb27d89d1cec2ee41d2621ee9f9ef8cf3f \ - --hash=sha256:8b32eb4065bac352b52a9f5ae07223567fab0a976c7d05017c01c45a1c24264f \ - --hash=sha256:a9064eb34f8f64438dd5b95c8f03a780b1a3f0b99c46eeacb1f0b5d15fc02dc1 \ - --hash=sha256:ab88ee287c25a119213153d033f7dd64c3ccec466ace267395872f554b648cd7 \ - --hash=sha256:b82c01d30430dd7b1fa442001b9099735d1a82f6bb72033acdc9206d5ac66a64 \ - --hash=sha256:c8b645e7d56591aa35dc75415bb53a62e4a494dba010e16f4b67daeffd830bd7 \ - --hash=sha256:c8d097884521e1ecaea6467aeebbf1aa56ee4a36350b48b2ca6b39366565c317 \ - --hash=sha256:cb4b5ee184816a4b099162ac93c0d7f0033d81a88e7c1291d0a9cc41ec348984 +faiss-cpu==1.15.0 \ + --hash=sha256:22dddb013e764aad66dac6cd15b49c7598d60339e0591b73b5e081629419c21b \ + --hash=sha256:30da3029952f0de69f16ce31946fd63fc3e292c867749bbcd2c0a0f09fd06f65 \ + --hash=sha256:37170d5e9ead4b6bfd9c314afc39e17e92064068a0c5a4063dd3f39568c2667e \ + --hash=sha256:50ea471ef1f4f3580eda8ab0ec9727d4bf65fd71c444bf306ce7cdbba8a42b21 \ + --hash=sha256:5b940897b317febaa761088513a3db164fad3ac71a5e1ed7be9a052c9bf1a447 \ + --hash=sha256:5d0a2d5d33fe023e263d0d355a837f20db67578e3be27fc5f4012a273274abf6 \ + --hash=sha256:88fbe1acac6978869063cb2f9477f85718da596a6e0a17751618f9c756bce255 \ + --hash=sha256:90169515a95ea58a9a95d419e518907927a8ef54c46788396365ec5902c9c8df \ + --hash=sha256:dd383bb1ce06fabcff5785f998f253aa88f88dcbe1fe36c922417cd6666dd896 \ + --hash=sha256:ec9b29aae29e428c085c2d49dbb02e4673cdea75db418d420f9e60e0b4184498 # via pyrag-kit frozenlist==1.8.0 \ --hash=sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686 \ @@ -459,280 +626,232 @@ frozenlist==1.8.0 \ # via # aiohttp # aiosignal -google-ai-generativelanguage==0.6.15 \ - --hash=sha256:5a03ef86377aa184ffef3662ca28f19eeee158733e45d7947982eb953c6ebb6c \ - --hash=sha256:8f6d9dc4c12b065fe2d0289026171acea5183ebf2d0b11cefe12f3821e159ec3 - # via google-generativeai -google-api-core==2.25.2 \ - --hash=sha256:1c63aa6af0d0d5e37966f157a77f9396d820fba59f9e43e9415bc3dc5baff300 \ - --hash=sha256:e9a8f62d363dc8424a8497f4c2a47d6bcda6c16514c935629c257ab5d10210e7 - # via - # google-ai-generativelanguage - # google-api-python-client - # google-generativeai -google-api-python-client==2.193.0 \ - --hash=sha256:8f88d16e89d11341e0a8b199cafde0fb7e6b44260dffb88d451577cbd1bb5d33 \ - --hash=sha256:c42aa324b822109901cfecab5dc4fc3915d35a7b376835233c916c70610322db - # via google-generativeai -google-auth==2.49.1 \ - --hash=sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64 \ - --hash=sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7 - # via - # google-ai-generativelanguage - # google-api-core - # google-api-python-client - # google-auth-httplib2 - # google-generativeai -google-auth-httplib2==0.3.0 \ - --hash=sha256:177898a0175252480d5ed916aeea183c2df87c1f9c26705d74ae6b951c268b0b \ - --hash=sha256:426167e5df066e3f5a0fc7ea18768c08e7296046594ce4c8c409c2457dd1f776 - # via google-api-python-client -google-generativeai==0.8.6 \ - --hash=sha256:37a0eaaa95e5bbf888828e20a4a1b2c196cc9527d194706e58a68ff388aeb0fa +google-auth==2.57.1 \ + --hash=sha256:ab439dee60a6856412bc058f13a68eb6a59c5b81526bb89cfdcf89ce9c0a48c9 \ + --hash=sha256:eb47b230fc6707eed4aee1c9cef55ec05bc1785eecba74ff8b572d531e921b1e + # via google-genai +google-genai==2.22.0 \ + --hash=sha256:9fa3b5d9ddb635005d8ab2d6206fb2b3d7204b66965bbce7de13ecd1a866ebcd \ + --hash=sha256:c514001c45470cc0a942440ae1b8215445d12bfb6c373aac94637127e1f74ec6 # via pyrag-kit -googleapis-common-protos==1.73.0 \ - --hash=sha256:778d07cd4fbeff84c6f7c72102f0daf98fa2bfd3fa8bea426edc545588da0b5a \ - --hash=sha256:dfdaaa2e860f242046be561e6d6cb5c5f1541ae02cfbcb034371aadb2942b4e8 - # via - # google-api-core - # grpcio-status -greenlet==3.3.2 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' \ - --hash=sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd \ - --hash=sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082 \ - --hash=sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b \ - --hash=sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5 \ - --hash=sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f \ - --hash=sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727 \ - --hash=sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e \ - --hash=sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2 \ - --hash=sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f \ - --hash=sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327 \ - --hash=sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd \ - --hash=sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070 \ - --hash=sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99 \ - --hash=sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be \ - --hash=sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79 \ - --hash=sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506 \ - --hash=sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a \ - --hash=sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395 \ - --hash=sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4 \ - --hash=sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492 \ - --hash=sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab \ - --hash=sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358 \ - --hash=sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce \ - --hash=sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5 \ - --hash=sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac \ - --hash=sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55 \ - --hash=sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124 \ - --hash=sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4 \ - --hash=sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986 \ - --hash=sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd \ - --hash=sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4 \ - --hash=sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab \ - --hash=sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff \ - --hash=sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a \ - --hash=sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9 \ - --hash=sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86 \ - --hash=sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71 \ - --hash=sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92 \ - --hash=sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643 \ - --hash=sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54 +greenlet==3.5.5 ; platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64' \ + --hash=sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537 \ + --hash=sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39 \ + --hash=sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277 \ + --hash=sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2 \ + --hash=sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d \ + --hash=sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e \ + --hash=sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5 \ + --hash=sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc \ + --hash=sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b \ + --hash=sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5 \ + --hash=sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769 \ + --hash=sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0 \ + --hash=sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da \ + --hash=sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3 \ + --hash=sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e \ + --hash=sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476 \ + --hash=sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e \ + --hash=sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380 \ + --hash=sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef \ + --hash=sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18 \ + --hash=sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272 \ + --hash=sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053 \ + --hash=sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07 \ + --hash=sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52 \ + --hash=sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed \ + --hash=sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95 \ + --hash=sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c \ + --hash=sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad \ + --hash=sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db \ + --hash=sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328 \ + --hash=sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8 \ + --hash=sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71 \ + --hash=sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864 \ + --hash=sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0 \ + --hash=sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1 \ + --hash=sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b \ + --hash=sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c \ + --hash=sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926 \ + --hash=sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc \ + --hash=sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd \ + --hash=sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007 \ + --hash=sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6 \ + --hash=sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0 \ + --hash=sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53 \ + --hash=sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c \ + --hash=sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa \ + --hash=sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61 \ + --hash=sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206 \ + --hash=sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9 \ + --hash=sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874 \ + --hash=sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d \ + --hash=sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae \ + --hash=sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0 \ + --hash=sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773 \ + --hash=sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552 \ + --hash=sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b # via sqlalchemy -grpcio==1.78.0 \ - --hash=sha256:082653eecbdf290e6e3e2c276ab2c54b9e7c299e07f4221872380312d8cf395e \ - --hash=sha256:12a771591ae40bc65ba67048fa52ef4f0e6db8279e595fd349f9dfddeef571f9 \ - --hash=sha256:185dea0d5260cbb2d224c507bf2a5444d5abbb1fa3594c1ed7e4c709d5eb8383 \ - --hash=sha256:1afa62af6e23f88629f2b29ec9e52ec7c65a7176c1e0a83292b93c76ca882558 \ - --hash=sha256:2045397e63a7a0ee7957c25f7dbb36ddc110e0cfb418403d110c0a7a68a844e9 \ - --hash=sha256:207db540302c884b8848036b80db352a832b99dfdf41db1eb554c2c2c7800f65 \ - --hash=sha256:271c73e6e5676afe4fc52907686670c7cea22ab2310b76a59b678403ed40d670 \ - --hash=sha256:2777b783f6c13b92bd7b716667452c329eefd646bfb3f2e9dabea2e05dbd34f6 \ - --hash=sha256:2bf5e2e163b356978b23652c4818ce4759d40f4712ee9ec5a83c4be6f8c23a3a \ - --hash=sha256:35eb275bf1751d2ffbd8f57cdbc46058e857cf3971041521b78b7db94bdaf127 \ - --hash=sha256:3c586ac70e855c721bda8f548d38c3ca66ac791dc49b66a8281a1f99db85e452 \ - --hash=sha256:3f8904a8165ab21e07e58bf3e30a73f4dffc7a1e0dbc32d51c61b5360d26f43e \ - --hash=sha256:459ab414b35f4496138d0ecd735fed26f1318af5e52cb1efbc82a09f0d5aa911 \ - --hash=sha256:51b13f9aed9d59ee389ad666b8c2214cc87b5de258fa712f9ab05f922e3896c6 \ - --hash=sha256:5397fff416b79e4b284959642a4e95ac4b0f1ece82c9993658e0e477d40551ec \ - --hash=sha256:57bab6deef2f4f1ca76cc04565df38dc5713ae6c17de690721bdf30cb1e0545c \ - --hash=sha256:6092beabe1966a3229f599d7088b38dfc8ffa1608b5b5cdda31e591e6500f856 \ - --hash=sha256:735e38e176a88ce41840c21bb49098ab66177c64c82426e24e0082500cc68af5 \ - --hash=sha256:7382b95189546f375c174f53a5fa873cef91c4b8005faa05cc5b3beea9c4f1c5 \ - --hash=sha256:748b6138585379c737adc08aeffd21222abbda1a86a0dca2a39682feb9196c20 \ - --hash=sha256:74be1268d1439eaaf552c698cdb11cd594f0c49295ae6bb72c34ee31abbe611b \ - --hash=sha256:859b13906ce098c0b493af92142ad051bf64c7870fa58a123911c88606714996 \ - --hash=sha256:85f93781028ec63f383f6bc90db785a016319c561cc11151fbb7b34e0d012303 \ - --hash=sha256:8dfffba826efcf366b1e3ccc37e67afe676f290e13a3b48d31a46739f80a8724 \ - --hash=sha256:8f2ac84905d12918e4e55a16da17939eb63e433dc11b677267c35568aa63fc84 \ - --hash=sha256:94309f498bcc07e5a7d16089ab984d42ad96af1d94b5a4eb966a266d9fcabf68 \ - --hash=sha256:9566fe4ababbb2610c39190791e5b829869351d14369603702e890ef3ad2d06e \ - --hash=sha256:9dca934f24c732750389ce49d638069c3892ad065df86cb465b3fa3012b70c9e \ - --hash=sha256:a9f136fbafe7ccf4ac7e8e0c28b31066e810be52d6e344ef954a3a70234e1702 \ - --hash=sha256:b2342d87af32790f934a79c3112641e7b27d63c261b8b4395350dad43eff1dc7 \ - --hash=sha256:b58f37edab4a3881bc6c9bca52670610e0c9ca14e2ea3cf9debf185b870457fb \ - --hash=sha256:be63c88b32e6c0f1429f1398ca5c09bc64b0d80950c8bb7807d7d7fb36fb84c7 \ - --hash=sha256:c41bc64626db62e72afec66b0c8a0da76491510015417c127bfc53b2fe6d7f7f \ - --hash=sha256:ce3a90455492bf8bfa38e56fbbe1dbd4f872a3d8eeaf7337dc3b1c8aa28c271b \ - --hash=sha256:dce09d6116df20a96acfdbf85e4866258c3758180e8c49845d6ba8248b6d0bbb \ - --hash=sha256:e87cbc002b6f440482b3519e36e1313eb5443e9e9e73d6a52d43bd2004fcfd8e \ - --hash=sha256:f12857d24d98441af6a1d5c87442d624411db486f7ba12550b07788f74b67b04 \ - --hash=sha256:f2d4e43ee362adfc05994ed479334d5a451ab7bc3f3fee1b796b8ca66895acb4 \ - --hash=sha256:f9ab915a267fc47c7e88c387a3a28325b58c898e23d4995f765728f4e3dedb97 \ - --hash=sha256:fbe6e89c7ffb48518384068321621b2a69cab509f58e40e4399fdd378fa6d074 \ - --hash=sha256:fd5f135b1bd58ab088930b3c613455796dfa0393626a6972663ccdda5b4ac6ce - # via - # google-api-core - # grpcio-status -grpcio-status==1.71.2 \ - --hash=sha256:803c98cb6a8b7dc6dbb785b1111aed739f241ab5e9da0bba96888aa74704cfd3 \ - --hash=sha256:c7a97e176df71cdc2c179cd1847d7fc86cca5832ad12e9798d7fed6b7a1aab50 - # via google-api-core h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 - # via httpcore + # via + # httpcore + # httpcore2 httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via httpx -httplib2==0.31.2 \ - --hash=sha256:385e0869d7397484f4eab426197a4c020b606edd43372492337c0b4010ae5d24 \ - --hash=sha256:dbf0c2fa3862acf3c55c078ea9c0bc4481d7dc5117cae71be9514912cf9f8349 - # via - # google-api-python-client - # google-auth-httplib2 +httpcore2==2.12.0 ; sys_platform != 'emscripten' \ + --hash=sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb \ + --hash=sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648 + # via httpx2 httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad # via - # anthropic + # google-genai + # langchain-core # langgraph-sdk - # langsmith - # openai + # pyrag-kit + # volcengine-python-sdk httpx-sse==0.4.3 \ --hash=sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc \ --hash=sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d # via langchain-community -idna==3.11 \ - --hash=sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea \ - --hash=sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902 +httpx2==2.12.0 \ + --hash=sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf \ + --hash=sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36 + # via + # anthropic + # langsmith + # openai +httpx2-jsfetch==1.0 ; python_full_version >= '3.12' and sys_platform == 'emscripten' \ + --hash=sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60 \ + --hash=sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32 + # via httpx2 +idna==3.19 \ + --hash=sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15 \ + --hash=sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4 # via # anyio # httpx + # httpx2 # requests # yarl +iniconfig==2.3.0 \ + --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ + --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 + # via pytest jieba==0.42.1 \ --hash=sha256:055ca12f62674fafed09427f176506079bc135638a14e23e25be909131928db2 # via pyrag-kit -jiter==0.13.0 \ - --hash=sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726 \ - --hash=sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654 \ - --hash=sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d \ - --hash=sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663 \ - --hash=sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8 \ - --hash=sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5 \ - --hash=sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394 \ - --hash=sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad \ - --hash=sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202 \ - --hash=sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1 \ - --hash=sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59 \ - --hash=sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d \ - --hash=sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92 \ - --hash=sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228 \ - --hash=sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf \ - --hash=sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018 \ - --hash=sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6 \ - --hash=sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d \ - --hash=sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024 \ - --hash=sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820 \ - --hash=sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2 \ - --hash=sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72 \ - --hash=sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089 \ - --hash=sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a \ - --hash=sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9 \ - --hash=sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434 \ - --hash=sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4 \ - --hash=sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa \ - --hash=sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0 \ - --hash=sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d \ - --hash=sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0 \ - --hash=sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5 \ - --hash=sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6 \ - --hash=sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911 \ - --hash=sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607 \ - --hash=sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9 \ - --hash=sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d \ - --hash=sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d \ - --hash=sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95 \ - --hash=sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08 \ - --hash=sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19 \ - --hash=sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe \ - --hash=sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09 \ - --hash=sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2 \ - --hash=sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc \ - --hash=sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0 \ - --hash=sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91 \ - --hash=sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663 \ - --hash=sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6 \ - --hash=sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f \ - --hash=sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411 \ - --hash=sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66 \ - --hash=sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59 \ - --hash=sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef \ - --hash=sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68 \ - --hash=sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c \ - --hash=sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b \ - --hash=sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93 \ - --hash=sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df \ - --hash=sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152 \ - --hash=sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701 \ - --hash=sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0 \ - --hash=sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3 \ - --hash=sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2 \ - --hash=sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40 \ - --hash=sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2 \ - --hash=sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939 \ - --hash=sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096 \ - --hash=sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c \ - --hash=sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159 \ - --hash=sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165 \ - --hash=sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f \ - --hash=sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4 \ - --hash=sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a \ - --hash=sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb \ - --hash=sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505 \ - --hash=sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10 \ - --hash=sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f +jiter==0.16.0 \ + --hash=sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536 \ + --hash=sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f \ + --hash=sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3 \ + --hash=sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce \ + --hash=sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e \ + --hash=sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85 \ + --hash=sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7 \ + --hash=sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207 \ + --hash=sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5 \ + --hash=sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056 \ + --hash=sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331 \ + --hash=sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03 \ + --hash=sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93 \ + --hash=sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290 \ + --hash=sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a \ + --hash=sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284 \ + --hash=sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195 \ + --hash=sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730 \ + --hash=sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48 \ + --hash=sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69 \ + --hash=sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274 \ + --hash=sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7 \ + --hash=sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5 \ + --hash=sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106 \ + --hash=sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00 \ + --hash=sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9 \ + --hash=sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b \ + --hash=sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818 \ + --hash=sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c \ + --hash=sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2 \ + --hash=sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077 \ + --hash=sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037 \ + --hash=sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2 \ + --hash=sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84 \ + --hash=sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a \ + --hash=sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a \ + --hash=sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3 \ + --hash=sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb \ + --hash=sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe \ + --hash=sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c \ + --hash=sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad \ + --hash=sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee \ + --hash=sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053 \ + --hash=sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929 \ + --hash=sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450 \ + --hash=sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3 \ + --hash=sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9 \ + --hash=sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734 \ + --hash=sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585 \ + --hash=sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29 \ + --hash=sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91 \ + --hash=sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee \ + --hash=sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e \ + --hash=sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5 \ + --hash=sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db \ + --hash=sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd \ + --hash=sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620 \ + --hash=sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567 \ + --hash=sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898 \ + --hash=sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01 \ + --hash=sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea \ + --hash=sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026 \ + --hash=sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e \ + --hash=sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e \ + --hash=sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8 \ + --hash=sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883 \ + --hash=sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702 \ + --hash=sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09 \ + --hash=sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72 \ + --hash=sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8 \ + --hash=sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af \ + --hash=sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3 \ + --hash=sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf \ + --hash=sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f \ + --hash=sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1 \ + --hash=sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e \ + --hash=sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e \ + --hash=sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127 \ + --hash=sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e # via # anthropic # openai -joblib==1.5.3 \ - --hash=sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713 \ - --hash=sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3 - # via scikit-learn jsonpatch==1.33 \ --hash=sha256:0ae28c0cd062bbd8b8ecc26d7d164fbbea9652a1a3693f3b956c1eae5145dade \ --hash=sha256:9fcd4009c41e6d12348b4a0ff2563ba56a2923a7dfee731d004e212e1ee5030c # via langchain-core -jsonpointer==3.0.0 \ - --hash=sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942 \ - --hash=sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef +jsonpointer==3.1.1 \ + --hash=sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900 \ + --hash=sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca # via jsonpatch -langchain==1.2.13 \ - --hash=sha256:37d4526ac4b0cdd3d7706a6366124c30dc0771bf5340865b37cdc99d5e5ad9b1 \ - --hash=sha256:d566ef67c8287e7f2e2df3c99bf3953a6beefd2a75a97fe56ecce905e21f3ef4 +langchain==1.4.0 \ + --hash=sha256:08da122a439f738f4c09a5158cfa3d2c1d8bea2d0bde7fbbf4aeb4d7f7fd9d80 \ + --hash=sha256:af1dc0161d30944a52ec7844d9bf890d0497b12ec0703069f3503b4003cbbac3 # via pyrag-kit -langchain-classic==1.0.3 \ - --hash=sha256:168ef1dfbfb18cae5a9ff0accecc9413a5b5aa3464b53fa841561a3384b6324a \ - --hash=sha256:26df1ec9806b1fbff19d9085a747ea7d8d82d7e3fb1d25132859979de627ef79 +langchain-classic==1.0.8 \ + --hash=sha256:1a11ea7fbe630c4f2af2f3873d27718ceac9488cf32d0821030be7cf039a6213 \ + --hash=sha256:ada0cc341a8a5b80fb24d73bdfaaeb849056ee2d8a41cc468355163fd3667484 # via langchain-community -langchain-community==0.4.1 \ - --hash=sha256:2135abb2c7748a35c84613108f7ebf30f8505b18c3c18305ffaecfc7651f6c6a \ - --hash=sha256:f3b211832728ee89f169ddce8579b80a085222ddb4f4ed445a46e977d17b1e85 +langchain-community==0.4.2 \ + --hash=sha256:84dd8c5122532394d5b6849a5fc9995ef28e4f77227daeb09f24b3d942e9e466 \ + --hash=sha256:a99308160d53d7e9b5965ee665e5173709914338210089fd5788ad724432c21e # via pyrag-kit -langchain-core==1.2.20 \ - --hash=sha256:b65ff678f3c3dc1f1b4d03a3af5ee3b8d51f9be5181d74eb53c6c11cd9dd5e68 \ - --hash=sha256:c7ac8b976039b5832abb989fef058b88c270594ba331efc79e835df046e7dc44 +langchain-core==1.6.2 \ + --hash=sha256:1ec6d3a98f7c8cdbb5bb0deff86e7ca37e16bf4f8f49f022d10723656c62352d \ + --hash=sha256:21e6c7cf097c68b777fd69ea00864f09bd9ca76ac73e3e3fa14e1ee366eb4711 # via # langchain # langchain-classic @@ -741,46 +860,164 @@ langchain-core==1.2.20 \ # langgraph # langgraph-checkpoint # langgraph-prebuilt + # langgraph-sdk # pyrag-kit -langchain-text-splitters==1.1.1 \ - --hash=sha256:34861abe7c07d9e49d4dc852d0129e26b32738b60a74486853ec9b6d6a8e01d2 \ - --hash=sha256:5ed0d7bf314ba925041e7d7d17cd8b10f688300d5415fb26c29442f061e329dc +langchain-protocol==0.0.19 \ + --hash=sha256:4cdf879a492a35980fd859ae792d3c65458ccaae504e183c9a10d7eac1f0720f \ + --hash=sha256:79d90a1425122ac87e8052e2ec054fbd09c3edbf341bdfb6397112a495c7bf8c + # via + # langchain-core + # langgraph-sdk +langchain-text-splitters==1.1.2 \ + --hash=sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627 \ + --hash=sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10 # via # langchain-classic # pyrag-kit -langgraph==1.1.3 \ - --hash=sha256:57cd6964ebab41cbd211f222293a2352404e55f8b2312cecde05e8753739b546 \ - --hash=sha256:ee496c297a9c93b38d8560be15cbb918110f49077d83abd14976cb13ac3b3370 +langgraph==1.2.11 \ + --hash=sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b \ + --hash=sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363 # via langchain -langgraph-checkpoint==4.0.1 \ - --hash=sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9 \ - --hash=sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034 +langgraph-checkpoint==4.2.0 \ + --hash=sha256:0547fd228935a0b758865de3a3d6d7a2537c308895d0f9ab092ce9151b5da942 \ + --hash=sha256:51a593b6bee684b0818e5d6e58e28ab340c6db7794575056ce7bd1b746a84ed7 # via # langgraph # langgraph-prebuilt -langgraph-prebuilt==1.0.8 \ - --hash=sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69 \ - --hash=sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0 +langgraph-prebuilt==1.1.0 \ + --hash=sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528 \ + --hash=sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9 # via langgraph -langgraph-sdk==0.3.12 \ - --hash=sha256:44323804965d6ec2a07127b3cf08a0428ea6deaeb172c2d478d5cd25540e3327 \ - --hash=sha256:c9c9ec22b3c0fcd352e2b8f32a815164f69446b8648ca22606329f4ff4c59a71 +langgraph-sdk==0.4.4 \ + --hash=sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64 \ + --hash=sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b # via langgraph -langsmith==0.7.22 \ - --hash=sha256:35bfe795d648b069958280760564632fd28ebc9921c04f3e209c0db6a6c7dc04 \ - --hash=sha256:6e9d5148314d74e86748cb9d3898632cad0320c9323d95f70f969e5bc078eee4 +langsmith==0.12.2 \ + --hash=sha256:ca2b6386cf452cf8edebf27c9c476503c18bad895e7ad566578393fa0ba7fea2 \ + --hash=sha256:ff369ba4390e0969dbb109e838281404ae637e76ef73b347336bae05b4f87331 # via # langchain-classic # langchain-community # langchain-core -markdown-it-py==4.0.0 \ - --hash=sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147 \ - --hash=sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3 +librt==0.15.0 ; platform_python_implementation != 'PyPy' \ + --hash=sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302 \ + --hash=sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad \ + --hash=sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08 \ + --hash=sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785 \ + --hash=sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9 \ + --hash=sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890 \ + --hash=sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2 \ + --hash=sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab \ + --hash=sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13 \ + --hash=sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2 \ + --hash=sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56 \ + --hash=sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b \ + --hash=sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6 \ + --hash=sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d \ + --hash=sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d \ + --hash=sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95 \ + --hash=sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714 \ + --hash=sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416 \ + --hash=sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54 \ + --hash=sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae \ + --hash=sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28 \ + --hash=sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d \ + --hash=sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40 \ + --hash=sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47 \ + --hash=sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19 \ + --hash=sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162 \ + --hash=sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c \ + --hash=sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa \ + --hash=sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc \ + --hash=sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988 \ + --hash=sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f \ + --hash=sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81 \ + --hash=sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d \ + --hash=sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95 \ + --hash=sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605 \ + --hash=sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108 \ + --hash=sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf \ + --hash=sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8 \ + --hash=sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a \ + --hash=sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965 \ + --hash=sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b \ + --hash=sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218 \ + --hash=sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd \ + --hash=sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b \ + --hash=sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d \ + --hash=sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622 \ + --hash=sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022 \ + --hash=sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60 \ + --hash=sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef \ + --hash=sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22 \ + --hash=sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8 \ + --hash=sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb \ + --hash=sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca \ + --hash=sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc \ + --hash=sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239 \ + --hash=sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0 \ + --hash=sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0 \ + --hash=sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa \ + --hash=sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3 \ + --hash=sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0 \ + --hash=sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915 \ + --hash=sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953 \ + --hash=sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65 \ + --hash=sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0 \ + --hash=sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd \ + --hash=sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1 \ + --hash=sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570 \ + --hash=sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c \ + --hash=sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101 \ + --hash=sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801 \ + --hash=sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2 \ + --hash=sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab \ + --hash=sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8 \ + --hash=sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18 \ + --hash=sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162 \ + --hash=sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81 \ + --hash=sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a \ + --hash=sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63 \ + --hash=sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc \ + --hash=sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1 \ + --hash=sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656 \ + --hash=sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d \ + --hash=sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9 \ + --hash=sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad \ + --hash=sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879 \ + --hash=sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d \ + --hash=sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3 \ + --hash=sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328 \ + --hash=sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659 \ + --hash=sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285 \ + --hash=sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26 \ + --hash=sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9 \ + --hash=sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1 \ + --hash=sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6 \ + --hash=sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c \ + --hash=sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38 \ + --hash=sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b \ + --hash=sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e \ + --hash=sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21 \ + --hash=sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374 \ + --hash=sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa \ + --hash=sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8 \ + --hash=sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab \ + --hash=sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15 \ + --hash=sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091 \ + --hash=sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993 \ + --hash=sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db \ + --hash=sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a \ + --hash=sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9 + # via mypy +macholib==1.16.4 ; sys_platform == 'darwin' \ + --hash=sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea \ + --hash=sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362 + # via pyinstaller +markdown-it-py==4.2.0 \ + --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ + --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich -marshmallow==3.26.2 \ - --hash=sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73 \ - --hash=sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57 - # via dataclasses-json mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba @@ -899,161 +1136,272 @@ multidict==6.7.1 \ # via # aiohttp # yarl +mypy==2.3.1 \ + --hash=sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d \ + --hash=sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0 \ + --hash=sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d \ + --hash=sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226 \ + --hash=sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133 \ + --hash=sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc \ + --hash=sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63 \ + --hash=sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3 \ + --hash=sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9 \ + --hash=sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4 \ + --hash=sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561 \ + --hash=sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419 \ + --hash=sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d \ + --hash=sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb \ + --hash=sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b \ + --hash=sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1 \ + --hash=sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523 \ + --hash=sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc \ + --hash=sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4 \ + --hash=sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb \ + --hash=sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57 \ + --hash=sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82 \ + --hash=sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac \ + --hash=sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74 \ + --hash=sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355 \ + --hash=sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080 \ + --hash=sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29 \ + --hash=sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f \ + --hash=sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531 \ + --hash=sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6 \ + --hash=sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff \ + --hash=sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8 \ + --hash=sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045 \ + --hash=sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9 \ + --hash=sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b \ + --hash=sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3 \ + --hash=sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb \ + --hash=sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595 \ + --hash=sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01 \ + --hash=sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021 \ + --hash=sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2 \ + --hash=sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6 \ + --hash=sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e \ + --hash=sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306 \ + --hash=sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8 \ + --hash=sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 - # via typing-inspect -numpy==2.4.3 \ - --hash=sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5 \ - --hash=sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc \ - --hash=sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657 \ - --hash=sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd \ - --hash=sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02 \ - --hash=sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a \ - --hash=sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26 \ - --hash=sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92 \ - --hash=sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22 \ - --hash=sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9 \ - --hash=sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9 \ - --hash=sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9 \ - --hash=sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152 \ - --hash=sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168 \ - --hash=sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e \ - --hash=sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd \ - --hash=sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15 \ - --hash=sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093 \ - --hash=sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5 \ - --hash=sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb \ - --hash=sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b \ - --hash=sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd \ - --hash=sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18 \ - --hash=sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5 \ - --hash=sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950 \ - --hash=sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e \ - --hash=sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a \ - --hash=sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd \ - --hash=sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4 \ - --hash=sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0 \ - --hash=sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef \ - --hash=sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5 \ - --hash=sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24 \ - --hash=sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476 \ - --hash=sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71 \ - --hash=sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc \ - --hash=sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e \ - --hash=sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8 \ - --hash=sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c \ - --hash=sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec \ - --hash=sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875 \ - --hash=sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a \ - --hash=sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79 \ - --hash=sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f \ - --hash=sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147 \ - --hash=sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857 \ - --hash=sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368 \ - --hash=sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720 \ - --hash=sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470 \ - --hash=sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b \ - --hash=sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920 \ - --hash=sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52 \ - --hash=sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73 \ - --hash=sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349 \ - --hash=sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4 \ - --hash=sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3 \ - --hash=sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7 \ - --hash=sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee \ - --hash=sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395 \ - --hash=sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c \ - --hash=sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611 \ - --hash=sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028 \ - --hash=sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d \ - --hash=sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7 \ - --hash=sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070 \ - --hash=sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0 \ - --hash=sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc \ - --hash=sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687 \ - --hash=sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0 \ - --hash=sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f \ - --hash=sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97 \ - --hash=sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67 + # via mypy +numpy==2.4.6 ; python_full_version < '3.12' \ + --hash=sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1 \ + --hash=sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4 \ + --hash=sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f \ + --hash=sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079 \ + --hash=sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096 \ + --hash=sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47 \ + --hash=sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66 \ + --hash=sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d \ + --hash=sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1 \ + --hash=sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e \ + --hash=sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147 \ + --hash=sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd \ + --hash=sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75 \ + --hash=sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063 \ + --hash=sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73 \ + --hash=sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab \ + --hash=sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4 \ + --hash=sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41 \ + --hash=sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402 \ + --hash=sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698 \ + --hash=sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7 \ + --hash=sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8 \ + --hash=sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b \ + --hash=sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8 \ + --hash=sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0 \ + --hash=sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662 \ + --hash=sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91 \ + --hash=sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0 \ + --hash=sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f \ + --hash=sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3 \ + --hash=sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f \ + --hash=sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67 \ + --hash=sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6 \ + --hash=sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997 \ + --hash=sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b \ + --hash=sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e \ + --hash=sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538 \ + --hash=sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627 \ + --hash=sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93 \ + --hash=sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02 \ + --hash=sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853 \ + --hash=sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c \ + --hash=sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43 \ + --hash=sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd \ + --hash=sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8 \ + --hash=sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089 \ + --hash=sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778 \ + --hash=sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1 \ + --hash=sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb \ + --hash=sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261 \ + --hash=sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb \ + --hash=sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a \ + --hash=sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8 \ + --hash=sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359 \ + --hash=sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5 \ + --hash=sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7 \ + --hash=sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751 \ + --hash=sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8 \ + --hash=sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605 \ + --hash=sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e \ + --hash=sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45 \ + --hash=sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2 \ + --hash=sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895 \ + --hash=sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe \ + --hash=sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb \ + --hash=sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a \ + --hash=sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577 \ + --hash=sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d \ + --hash=sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a \ + --hash=sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda \ + --hash=sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6 \ + --hash=sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20 # via # faiss-cpu # langchain-community - # pandas # pyrag-kit # rank-bm25 - # scikit-learn - # scipy -openai==2.29.0 \ - --hash=sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec \ - --hash=sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a +numpy==2.5.2 ; python_full_version >= '3.12' \ + --hash=sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a \ + --hash=sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f \ + --hash=sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7 \ + --hash=sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0 \ + --hash=sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3 \ + --hash=sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c \ + --hash=sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce \ + --hash=sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8 \ + --hash=sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1 \ + --hash=sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4 \ + --hash=sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee \ + --hash=sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740 \ + --hash=sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98 \ + --hash=sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710 \ + --hash=sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee \ + --hash=sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68 \ + --hash=sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf \ + --hash=sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8 \ + --hash=sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf \ + --hash=sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b \ + --hash=sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884 \ + --hash=sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03 \ + --hash=sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69 \ + --hash=sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4 \ + --hash=sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842 \ + --hash=sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65 \ + --hash=sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080 \ + --hash=sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e \ + --hash=sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e \ + --hash=sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414 \ + --hash=sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59 \ + --hash=sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8 \ + --hash=sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617 \ + --hash=sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4 \ + --hash=sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb \ + --hash=sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251 \ + --hash=sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d \ + --hash=sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2 \ + --hash=sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab \ + --hash=sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657 \ + --hash=sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15 \ + --hash=sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9 \ + --hash=sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8 \ + --hash=sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323 \ + --hash=sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788 \ + --hash=sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc \ + --hash=sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56 \ + --hash=sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1 \ + --hash=sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d \ + --hash=sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec \ + --hash=sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2 \ + --hash=sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e \ + --hash=sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7 \ + --hash=sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26 \ + --hash=sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514 \ + --hash=sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860 \ + --hash=sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a \ + --hash=sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1 \ + --hash=sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab \ + --hash=sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba \ + --hash=sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12 \ + --hash=sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6 \ + --hash=sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e \ + --hash=sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac \ + --hash=sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb \ + --hash=sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f + # via + # faiss-cpu + # langchain-community + # pyrag-kit + # rank-bm25 +openai==3.10.0 \ + --hash=sha256:c2dc841b944c4720a222f9a92f66a9a122e46968d87ed7857104655054c687ba \ + --hash=sha256:d1f307d866c7d12872809aeb68b7d1a085b49c2656787eae7c39ba381a41ad0d # via pyrag-kit openpyxl==3.1.5 \ --hash=sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2 \ --hash=sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050 # via pyrag-kit -orjson==3.11.7 \ - --hash=sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e \ - --hash=sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f \ - --hash=sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8 \ - --hash=sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733 \ - --hash=sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223 \ - --hash=sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d \ - --hash=sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650 \ - --hash=sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5 \ - --hash=sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1 \ - --hash=sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8 \ - --hash=sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3 \ - --hash=sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6 \ - --hash=sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910 \ - --hash=sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2 \ - --hash=sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d \ - --hash=sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a \ - --hash=sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5 \ - --hash=sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e \ - --hash=sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471 \ - --hash=sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892 \ - --hash=sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3 \ - --hash=sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b \ - --hash=sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504 \ - --hash=sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539 \ - --hash=sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785 \ - --hash=sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1 \ - --hash=sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab \ - --hash=sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576 \ - --hash=sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b \ - --hash=sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141 \ - --hash=sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62 \ - --hash=sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c \ - --hash=sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2 \ - --hash=sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b \ - --hash=sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49 \ - --hash=sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960 \ - --hash=sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705 \ - --hash=sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace \ - --hash=sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b \ - --hash=sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1 \ - --hash=sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561 \ - --hash=sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157 \ - --hash=sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10 \ - --hash=sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5 \ - --hash=sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757 \ - --hash=sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d \ - --hash=sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f \ - --hash=sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf \ - --hash=sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183 \ - --hash=sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74 \ - --hash=sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0 \ - --hash=sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e \ - --hash=sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d \ - --hash=sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539 \ - --hash=sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4 \ - --hash=sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0 \ - --hash=sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad \ - --hash=sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa \ - --hash=sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f \ - --hash=sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1 \ - --hash=sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867 +orjson==3.12.0 \ + --hash=sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a \ + --hash=sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e \ + --hash=sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55 \ + --hash=sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c \ + --hash=sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed \ + --hash=sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b \ + --hash=sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54 \ + --hash=sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df \ + --hash=sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578 \ + --hash=sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c \ + --hash=sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83 \ + --hash=sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94 \ + --hash=sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710 \ + --hash=sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d \ + --hash=sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38 \ + --hash=sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e \ + --hash=sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7 \ + --hash=sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873 \ + --hash=sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f \ + --hash=sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328 \ + --hash=sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8 \ + --hash=sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868 \ + --hash=sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222 \ + --hash=sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc \ + --hash=sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e \ + --hash=sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806 \ + --hash=sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978 \ + --hash=sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc \ + --hash=sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647 \ + --hash=sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3 \ + --hash=sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13 \ + --hash=sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7 \ + --hash=sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900 \ + --hash=sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5 \ + --hash=sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d \ + --hash=sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a \ + --hash=sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10 \ + --hash=sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5 \ + --hash=sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e \ + --hash=sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92 \ + --hash=sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0 \ + --hash=sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03 \ + --hash=sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d \ + --hash=sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2 \ + --hash=sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998 \ + --hash=sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e \ + --hash=sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a \ + --hash=sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5 \ + --hash=sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92 \ + --hash=sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1 \ + --hash=sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e \ + --hash=sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8 \ + --hash=sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517 \ + --hash=sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1 \ + --hash=sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f \ + --hash=sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252 # via # langgraph-sdk # langsmith @@ -1100,190 +1448,145 @@ ormsgpack==1.12.2 \ --hash=sha256:f3601f19afdbea273ed70b06495e5794606a8b690a568d6c996a90d7255e51c1 \ --hash=sha256:fcd55e5f6ba0dbce624942adf9f152062135f991a0126064889f68eb850de0dd # via langgraph-checkpoint -packaging==26.0 \ - --hash=sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4 \ - --hash=sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529 +packaging==26.3 \ + --hash=sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79 \ + --hash=sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c # via # faiss-cpu # langchain-core # langsmith - # marshmallow -pandas==3.0.1 \ - --hash=sha256:06aff2ad6f0b94a17822cf8b83bbb563b090ed82ff4fe7712db2ce57cd50d9b8 \ - --hash=sha256:0ab749dfba921edf641d4036c4c21c0b3ea70fea478165cb98a998fb2a261955 \ - --hash=sha256:0f463ebfd8de7f326d38037c7363c6dacb857c5881ab8961fb387804d6daf2f7 \ - --hash=sha256:108dd1790337a494aa80e38def654ca3f0968cf4f362c85f44c15e471667102d \ - --hash=sha256:15860b1fdb1973fffade772fdb931ccf9b2f400a3f5665aef94a00445d7d8dd5 \ - --hash=sha256:1849f0bba9c8a2fb0f691d492b834cc8dadf617e29015c66e989448d58d011ee \ - --hash=sha256:1ff8cf1d2896e34343197685f432450ec99a85ba8d90cce2030c5eee2ef98791 \ - --hash=sha256:24ba315ba3d6e5806063ac6eb717504e499ce30bd8c236d8693a5fd3f084c796 \ - --hash=sha256:331ca75a2f8672c365ae25c0b29e46f5ac0c6551fdace8eec4cd65e4fac271ff \ - --hash=sha256:356e5c055ed9b0da1580d465657bc7d00635af4fd47f30afb23025352ba764d1 \ - --hash=sha256:3b66857e983208654294bb6477b8a63dee26b37bdd0eb34d010556e91261784f \ - --hash=sha256:406ce835c55bac912f2a0dcfaf27c06d73c6b04a5dde45f1fd3169ce31337389 \ - --hash=sha256:4186a699674af418f655dbd420ed87f50d56b4cd6603784279d9eef6627823c8 \ - --hash=sha256:44f1364411d5670efa692b146c748f4ed013df91ee91e9bec5677fb1fd58b937 \ - --hash=sha256:476f84f8c20c9f5bc47252b66b4bb25e1a9fc2fa98cead96744d8116cb85771d \ - --hash=sha256:4a68773d5a778afb31d12e34f7dd4612ab90de8c6fb1d8ffe5d4a03b955082a1 \ - --hash=sha256:4e1b677accee34a09e0dc2ce5624e4a58a1870ffe56fc021e9caf7f23cd7668f \ - --hash=sha256:5272627187b5d9c20e55d27caf5f2cd23e286aba25cadf73c8590e432e2b7262 \ - --hash=sha256:532527a701281b9dd371e2f582ed9094f4c12dd9ffb82c0c54ee28d8ac9520c4 \ - --hash=sha256:536232a5fe26dd989bd633e7a0c450705fdc86a207fec7254a55e9a22950fe43 \ - --hash=sha256:56cf59638bf24dc9bdf2154c81e248b3289f9a09a6d04e63608c159022352749 \ - --hash=sha256:58eeb1b2e0fb322befcf2bbc9ba0af41e616abadb3d3414a6bc7167f6cbfce32 \ - --hash=sha256:5ae2ab1f166668b41e770650101e7090824fd34d17915dd9cd479f5c5e0065e9 \ - --hash=sha256:661e0f665932af88c7877f31da0dc743fe9c8f2524bdffe23d24fdcb67ef9d56 \ - --hash=sha256:6bf0603c2e30e2cafac32807b06435f28741135cb8697eae8b28c7d492fc7d76 \ - --hash=sha256:6c426422973973cae1f4a23e51d4ae85974f44871b24844e4f7de752dd877098 \ - --hash=sha256:75e6e292ff898679e47a2199172593d9f6107fd2dd3617c22c2946e97d5df46e \ - --hash=sha256:830994d7e1f31dd7e790045235605ab61cff6c94defc774547e8b7fdfbff3dc7 \ - --hash=sha256:84f0904a69e7365f79a0c77d3cdfccbfb05bf87847e3a51a41e1426b0edb9c79 \ - --hash=sha256:85fe4c4df62e1e20f9db6ebfb88c844b092c22cd5324bdcf94bfa2fc1b391221 \ - --hash=sha256:93325b0fe372d192965f4cca88d97667f49557398bbf94abdda3bf1b591dbe66 \ - --hash=sha256:94f87a04984d6b63788327cd9f79dda62b7f9043909d2440ceccf709249ca988 \ - --hash=sha256:97ca08674e3287c7148f4858b01136f8bdfe7202ad25ad04fec602dd1d29d132 \ - --hash=sha256:9832c2c69da24b602c32e0c7b1b508a03949c18ba08d4d9f1c1033426685b447 \ - --hash=sha256:99d0f92ed92d3083d140bf6b97774f9f13863924cf3f52a70711f4e7588f9d0a \ - --hash=sha256:9d810036895f9ad6345b8f2a338dd6998a74e8483847403582cab67745bff821 \ - --hash=sha256:9fea306c783e28884c29057a1d9baa11a349bbf99538ec1da44c8476563d1b25 \ - --hash=sha256:a64ce8b0f2de1d2efd2ae40b0abe7f8ae6b29fbfb3812098ed5a6f8e235ad9bf \ - --hash=sha256:a8d37a43c52917427e897cb2e429f67a449327394396a81034a4449b99afda59 \ - --hash=sha256:a9cabbdcd03f1b6cd254d6dda8ae09b0252524be1592594c00b7895916cb1324 \ - --hash=sha256:b03f91ae8c10a85c1613102c7bef5229b5379f343030a3ccefeca8a33414cf35 \ - --hash=sha256:b8e36891080b87823aff3640c78649b91b8ff6eea3c0d70aeabd72ea43ab069b \ - --hash=sha256:c1a9f55e0f46951874b863d1f3906dcb57df2d9be5c5847ba4dfb55b2c815249 \ - --hash=sha256:c3d288439e11b5325b02ae6e9cc83e6805a62c40c5a6220bea9beb899c073b1c \ - --hash=sha256:cd9af1276b5ca9e298bd79a26bda32fa9cc87ed095b2a9a60978d2ca058eaf87 \ - --hash=sha256:d54855f04f8246ed7b6fc96b05d4871591143c46c0b6f4af874764ed0d2d6f06 \ - --hash=sha256:de09668c1bf3b925c07e5762291602f0d789eca1b3a781f99c1c78f6cac0e7ea \ - --hash=sha256:eca8b4510f6763f3d37359c2105df03a7a221a508f30e396a51d0713d462e68a - # via pyrag-kit -prompt-toolkit==3.0.52 \ - --hash=sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855 \ - --hash=sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955 + # pyinstaller + # pyinstaller-hooks-contrib + # pytest +pathspec==1.1.1 \ + --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ + --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 + # via mypy +pefile==2024.8.26 ; sys_platform == 'win32' \ + --hash=sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632 \ + --hash=sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f + # via pyinstaller +pluggy==1.6.0 \ + --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ + --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 + # via pytest +prompt-toolkit==3.0.53 \ + --hash=sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2 \ + --hash=sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6 # via # pyrag-kit # questionary -propcache==0.4.1 \ - --hash=sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4 \ - --hash=sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be \ - --hash=sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3 \ - --hash=sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85 \ - --hash=sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b \ - --hash=sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367 \ - --hash=sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf \ - --hash=sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393 \ - --hash=sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1 \ - --hash=sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717 \ - --hash=sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc \ - --hash=sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe \ - --hash=sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75 \ - --hash=sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6 \ - --hash=sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e \ - --hash=sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566 \ - --hash=sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12 \ - --hash=sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367 \ - --hash=sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874 \ - --hash=sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf \ - --hash=sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566 \ - --hash=sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a \ - --hash=sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a \ - --hash=sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1 \ - --hash=sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6 \ - --hash=sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61 \ - --hash=sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726 \ - --hash=sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49 \ - --hash=sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44 \ - --hash=sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af \ - --hash=sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa \ - --hash=sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153 \ - --hash=sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc \ - --hash=sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf \ - --hash=sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8 \ - --hash=sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c \ - --hash=sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85 \ - --hash=sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e \ - --hash=sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0 \ - --hash=sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1 \ - --hash=sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992 \ - --hash=sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f \ - --hash=sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d \ - --hash=sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1 \ - --hash=sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e \ - --hash=sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89 \ - --hash=sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a \ - --hash=sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b \ - --hash=sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f \ - --hash=sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1 \ - --hash=sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66 \ - --hash=sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded \ - --hash=sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0 \ - --hash=sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165 \ - --hash=sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778 \ - --hash=sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455 \ - --hash=sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f \ - --hash=sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b \ - --hash=sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237 \ - --hash=sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81 \ - --hash=sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859 \ - --hash=sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c \ - --hash=sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835 \ - --hash=sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393 \ - --hash=sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5 \ - --hash=sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641 \ - --hash=sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144 \ - --hash=sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74 \ - --hash=sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db \ - --hash=sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403 \ - --hash=sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9 \ - --hash=sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f \ - --hash=sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311 \ - --hash=sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36 \ - --hash=sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f \ - --hash=sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2 \ - --hash=sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7 \ - --hash=sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239 \ - --hash=sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757 \ - --hash=sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72 \ - --hash=sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9 \ - --hash=sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4 \ - --hash=sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24 \ - --hash=sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207 \ - --hash=sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e \ - --hash=sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1 \ - --hash=sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d \ - --hash=sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37 \ - --hash=sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e \ - --hash=sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570 \ - --hash=sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af \ - --hash=sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48 +propcache==0.5.2 \ + --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ + --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ + --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ + --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ + --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ + --hash=sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660 \ + --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ + --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ + --hash=sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42 \ + --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ + --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ + --hash=sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33 \ + --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ + --hash=sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511 \ + --hash=sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0 \ + --hash=sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84 \ + --hash=sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66 \ + --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ + --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ + --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ + --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ + --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ + --hash=sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6 \ + --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ + --hash=sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82 \ + --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ + --hash=sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64 \ + --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ + --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ + --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ + --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ + --hash=sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab \ + --hash=sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f \ + --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ + --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ + --hash=sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba \ + --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ + --hash=sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b \ + --hash=sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144 \ + --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ + --hash=sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67 \ + --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ + --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ + --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ + --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ + --hash=sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476 \ + --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ + --hash=sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78 \ + --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ + --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ + --hash=sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a \ + --hash=sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba \ + --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ + --hash=sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c \ + --hash=sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9 \ + --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ + --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ + --hash=sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031 \ + --hash=sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913 \ + --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ + --hash=sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f \ + --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ + --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ + --hash=sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959 \ + --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ + --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ + --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ + --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ + --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ + --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ + --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ + --hash=sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1 \ + --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ + --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ + --hash=sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7 \ + --hash=sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a \ + --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ + --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ + --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ + --hash=sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0 \ + --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ + --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ + --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ + --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ + --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ + --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ + --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ + --hash=sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42 \ + --hash=sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a \ + --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ + --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ + --hash=sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27 \ + --hash=sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf \ + --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ + --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ + --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ + --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ + --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ + --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ + --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ + --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ + --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ + --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ + --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via # aiohttp # yarl -proto-plus==1.27.1 \ - --hash=sha256:912a7460446625b792f6448bade9e55cd4e41e6ac10e27009ef71a7f317fa147 \ - --hash=sha256:e4643061f3a4d0de092d62aa4ad09fa4756b2cbb89d4627f3985018216f9fefc - # via - # google-ai-generativelanguage - # google-api-core -protobuf==5.29.6 \ - --hash=sha256:62e8a3114992c7c647bce37dcc93647575fc52d50e48de30c6fcb28a6a291eb1 \ - --hash=sha256:6b9edb641441b2da9fa8f428760fc136a49cf97a52076010cf22a2ff73438a86 \ - --hash=sha256:7e6ad413275be172f67fdee0f43484b6de5a904cc1c3ea9804cb6fe2ff366eda \ - --hash=sha256:a8866b2cff111f0f863c1b3b9e7572dc7eaea23a7fae27f6fc613304046483e6 \ - --hash=sha256:b5a169e664b4057183a34bdc424540e86eea47560f3c123a0d64de4e137f9269 \ - --hash=sha256:da9ee6a5424b6b30fd5e45c5ea663aef540ca95f9ad99d1e887e819cdf9b8723 \ - --hash=sha256:e3387f44798ac1106af0233c04fb8abf543772ff241169946f698b3a9a3d3ab9 - # via - # google-ai-generativelanguage - # google-api-core - # google-generativeai - # googleapis-common-protos - # grpcio-status - # proto-plus -pyasn1==0.6.3 \ - --hash=sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf \ - --hash=sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde +pyasn1==0.6.4 \ + --hash=sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81 \ + --hash=sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b # via pyasn1-modules pyasn1-modules==0.4.2 \ --hash=sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a \ @@ -1293,12 +1596,12 @@ pycparser==3.0 ; implementation_name != 'PyPy' and platform_python_implementatio --hash=sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29 \ --hash=sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992 # via cffi -pydantic==2.12.5 \ - --hash=sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49 \ - --hash=sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d +pydantic==2.13.5 \ + --hash=sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73 \ + --hash=sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08 # via # anthropic - # google-generativeai + # google-genai # langchain # langchain-classic # langchain-core @@ -1307,98 +1610,104 @@ pydantic==2.12.5 \ # openai # pydantic-settings # pyrag-kit -pydantic-core==2.41.5 \ - --hash=sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90 \ - --hash=sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740 \ - --hash=sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84 \ - --hash=sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33 \ - --hash=sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0 \ - --hash=sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e \ - --hash=sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0 \ - --hash=sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34 \ - --hash=sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3 \ - --hash=sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815 \ - --hash=sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14 \ - --hash=sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375 \ - --hash=sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf \ - --hash=sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1 \ - --hash=sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808 \ - --hash=sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553 \ - --hash=sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1 \ - --hash=sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470 \ - --hash=sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2 \ - --hash=sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b \ - --hash=sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660 \ - --hash=sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c \ - --hash=sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594 \ - --hash=sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008 \ - --hash=sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a \ - --hash=sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a \ - --hash=sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd \ - --hash=sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284 \ - --hash=sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586 \ - --hash=sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869 \ - --hash=sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294 \ - --hash=sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f \ - --hash=sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66 \ - --hash=sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51 \ - --hash=sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc \ - --hash=sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d \ - --hash=sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c \ - --hash=sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07 \ - --hash=sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36 \ - --hash=sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e \ - --hash=sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05 \ - --hash=sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e \ - --hash=sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612 \ - --hash=sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b \ - --hash=sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe \ - --hash=sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11 \ - --hash=sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd \ - --hash=sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b \ - --hash=sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c \ - --hash=sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a \ - --hash=sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1 \ - --hash=sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf \ - --hash=sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858 \ - --hash=sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2 \ - --hash=sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9 \ - --hash=sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2 \ - --hash=sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3 \ - --hash=sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6 \ - --hash=sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770 \ - --hash=sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc \ - --hash=sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23 \ - --hash=sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26 \ - --hash=sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa \ - --hash=sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d \ - --hash=sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3 \ - --hash=sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d \ - --hash=sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034 \ - --hash=sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9 \ - --hash=sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1 \ - --hash=sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56 \ - --hash=sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b \ - --hash=sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c \ - --hash=sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e \ - --hash=sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9 \ - --hash=sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5 \ - --hash=sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e \ - --hash=sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc \ - --hash=sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb \ - --hash=sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0 \ - --hash=sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8 \ - --hash=sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69 \ - --hash=sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c \ - --hash=sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75 \ - --hash=sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f \ - --hash=sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad \ - --hash=sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b \ - --hash=sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7 + # volcengine-python-sdk +pydantic-core==2.46.5 \ + --hash=sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942 \ + --hash=sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821 \ + --hash=sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5 \ + --hash=sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c \ + --hash=sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184 \ + --hash=sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2 \ + --hash=sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc \ + --hash=sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5 \ + --hash=sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0 \ + --hash=sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931 \ + --hash=sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e \ + --hash=sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25 \ + --hash=sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47 \ + --hash=sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038 \ + --hash=sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8 \ + --hash=sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b \ + --hash=sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a \ + --hash=sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e \ + --hash=sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074 \ + --hash=sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0 \ + --hash=sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433 \ + --hash=sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034 \ + --hash=sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21 \ + --hash=sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c \ + --hash=sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4 \ + --hash=sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c \ + --hash=sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c \ + --hash=sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061 \ + --hash=sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29 \ + --hash=sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3 \ + --hash=sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa \ + --hash=sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e \ + --hash=sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38 \ + --hash=sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f \ + --hash=sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b \ + --hash=sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8 \ + --hash=sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e \ + --hash=sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c \ + --hash=sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be \ + --hash=sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1 \ + --hash=sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b \ + --hash=sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f \ + --hash=sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761 \ + --hash=sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869 \ + --hash=sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111 \ + --hash=sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5 \ + --hash=sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7 \ + --hash=sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129 \ + --hash=sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c \ + --hash=sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec \ + --hash=sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689 \ + --hash=sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f \ + --hash=sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea \ + --hash=sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9 \ + --hash=sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519 \ + --hash=sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f \ + --hash=sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee \ + --hash=sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a \ + --hash=sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e \ + --hash=sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6 \ + --hash=sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2 \ + --hash=sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f \ + --hash=sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a \ + --hash=sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f \ + --hash=sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a \ + --hash=sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47 \ + --hash=sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda \ + --hash=sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0 \ + --hash=sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4 \ + --hash=sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d \ + --hash=sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3 \ + --hash=sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2 \ + --hash=sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355 \ + --hash=sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f \ + --hash=sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5 \ + --hash=sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2 \ + --hash=sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829 \ + --hash=sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266 \ + --hash=sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575 \ + --hash=sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290 \ + --hash=sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f \ + --hash=sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62 \ + --hash=sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f \ + --hash=sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a \ + --hash=sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7 \ + --hash=sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed \ + --hash=sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f \ + --hash=sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3 \ + --hash=sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff \ + --hash=sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084 \ + --hash=sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0 \ + --hash=sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9 # via pydantic -pydantic-settings==2.13.1 \ - --hash=sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025 \ - --hash=sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237 +pydantic-settings==2.15.0 \ + --hash=sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42 \ + --hash=sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117 # via # langchain-community # pyrag-kit @@ -1406,22 +1715,44 @@ pyfiglet==1.0.4 \ --hash=sha256:65b57b7a8e1dff8a67dc8e940a117238661d5e14c3e49121032bd404d9b2b39f \ --hash=sha256:db9c9940ed1bf3048deff534ed52ff2dafbbc2cd7610b17bb5eca1df6d4278ef # via pyrag-kit -pygments==2.19.2 \ - --hash=sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887 \ - --hash=sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b - # via rich -pyparsing==3.3.2 \ - --hash=sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d \ - --hash=sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc - # via httplib2 +pygments==2.21.0 \ + --hash=sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9 \ + --hash=sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c + # via + # pytest + # rich +pyinstaller==6.22.2 \ + --hash=sha256:0260eaad6be3f6fbc1affffe6dc7b8e5b636dbca51b463224daba971610b6fc1 \ + --hash=sha256:06d7b3827a8049db4a2d47e3ec4ae2f69a1041577d8833b8f169745cde573ab4 \ + --hash=sha256:7bee432404eca5dc3ef37c36811c266561b03988f6d3e70ab0fa5352dd5de9e4 \ + --hash=sha256:89b65a3ad07d9dd5832253e37bc45f31872d10d7f9d5c9fd0fdd6088a83829dd \ + --hash=sha256:8c2c4b14caad38c1f3df8e9bf5276fc265e27fe8b4180cab4a37864288427bc0 \ + --hash=sha256:9622686ecc5d5fa492fe6cde29d47df9dd41138cff8177be9f901ca3260f2096 \ + --hash=sha256:9a078877caa3920558a3242d0a7019fe5b825cff4b65ed380c7d1e1bd200ddd9 \ + --hash=sha256:9b990fa6bbe143572f06644a984ad0d7aa2e2ccc6929d4916031343a5888e9a7 \ + --hash=sha256:afb6f9a95d19b6dcd3a7decc40d9adb6ba9c4f8802ddd6c972dfb552953f384e \ + --hash=sha256:becb47ad78272bede87acf2ed830d7545a8f65ab11d06a68fe2b99ba1afaac6d \ + --hash=sha256:ebd1b1ca932d7cf25d7366ce691aaf79a5ff9425811ed7328b5116e4471b6d6d \ + --hash=sha256:f5ccb847451df4207bce18bf53a57b124c9bb4e7e4bad08c5ecc627bcf00b28c +pyinstaller-hooks-contrib==2026.7 \ + --hash=sha256:24257a04c7a5a7a034cf28e39dcee20fbeeb9f043076729480f2e1b69904408a \ + --hash=sha256:5fbcaacb22c4f4aac869a127dce283f67a4b4cfcc37d496f2446603e6d68aefa + # via pyinstaller +pytest==9.1.1 \ + --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ + --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c python-dateutil==2.9.0.post0 \ --hash=sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3 \ --hash=sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427 - # via pandas -python-dotenv==1.2.2 \ - --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ - --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 + # via volcengine-python-sdk +python-dotenv==1.2.3 \ + --hash=sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9 \ + --hash=sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35 # via pydantic-settings +pywin32-ctypes==0.2.3 ; sys_platform == 'win32' \ + --hash=sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8 \ + --hash=sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755 + # via pyinstaller pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ @@ -1472,6 +1803,7 @@ pyyaml==6.0.3 \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 \ --hash=sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0 # via + # bandit # langchain-classic # langchain-community # langchain-core @@ -1483,110 +1815,127 @@ rank-bm25==0.2.2 \ --hash=sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d \ --hash=sha256:7bd4a95571adadfc271746fa146a4bcfd89c0cf731e49c3d1ad863290adbe8ae # via pyrag-kit -regex==2026.2.28 \ - --hash=sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1 \ - --hash=sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a \ - --hash=sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4 \ - --hash=sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d \ - --hash=sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a \ - --hash=sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911 \ - --hash=sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952 \ - --hash=sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b \ - --hash=sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97 \ - --hash=sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25 \ - --hash=sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8 \ - --hash=sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff \ - --hash=sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a \ - --hash=sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7 \ - --hash=sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0 \ - --hash=sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215 \ - --hash=sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43 \ - --hash=sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b \ - --hash=sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e \ - --hash=sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae \ - --hash=sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8 \ - --hash=sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11 \ - --hash=sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae \ - --hash=sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5 \ - --hash=sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64 \ - --hash=sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472 \ - --hash=sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18 \ - --hash=sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a \ - --hash=sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd \ - --hash=sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d \ - --hash=sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d \ - --hash=sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9 \ - --hash=sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96 \ - --hash=sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784 \ - --hash=sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b \ - --hash=sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff \ - --hash=sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc \ - --hash=sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf \ - --hash=sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098 \ - --hash=sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2 \ - --hash=sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05 \ - --hash=sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf \ - --hash=sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6 \ - --hash=sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768 \ - --hash=sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15 \ - --hash=sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881 \ - --hash=sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f \ - --hash=sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8 \ - --hash=sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c \ - --hash=sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d \ - --hash=sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341 \ - --hash=sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e \ - --hash=sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2 \ - --hash=sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550 \ - --hash=sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e \ - --hash=sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8 \ - --hash=sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59 \ - --hash=sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3 \ - --hash=sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117 \ - --hash=sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc \ - --hash=sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea \ - --hash=sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b \ - --hash=sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e \ - --hash=sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703 \ - --hash=sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2 \ - --hash=sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952 \ - --hash=sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944 \ - --hash=sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7 \ - --hash=sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b \ - --hash=sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033 \ - --hash=sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4 \ - --hash=sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8 \ - --hash=sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f \ - --hash=sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d \ - --hash=sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d \ - --hash=sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec \ - --hash=sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b \ - --hash=sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a \ - --hash=sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92 \ - --hash=sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9 \ - --hash=sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc \ - --hash=sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022 \ - --hash=sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6 \ - --hash=sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c \ - --hash=sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27 \ - --hash=sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b \ - --hash=sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc \ - --hash=sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1 \ - --hash=sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07 \ - --hash=sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c \ - --hash=sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a \ - --hash=sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33 \ - --hash=sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95 \ - --hash=sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081 \ - --hash=sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7 \ - --hash=sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb \ - --hash=sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61 +regex==2026.9.3 \ + --hash=sha256:01bdce6a372efd5ae3d8560cedbc691be259a50206564bbaf04008b2937721ab \ + --hash=sha256:0540de6e7917f89acaf9771bdcd6fa7e505c67416d3b283e52ad4ab25399c6d6 \ + --hash=sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089 \ + --hash=sha256:0edd12c8201222f58817689dc61fe44893f3f2f2aee530b211dfa92af84df9cc \ + --hash=sha256:0ee4721472e00e96b3cceec545c9867f91815f628f6ea304ae6cea93a7e4e7ae \ + --hash=sha256:14bf4a88833c12a990dbf5cca77eb293401d60878be7d566a2d5096fb0216c27 \ + --hash=sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd \ + --hash=sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36 \ + --hash=sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280 \ + --hash=sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e \ + --hash=sha256:1f975a75dae06e88665e4a709873d936a7e8f9445e3d354b75de0e735d28cf71 \ + --hash=sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687 \ + --hash=sha256:24b4bbb65ff2c4e8c552c93c342bf5ffa0ad162d963d96bac7c1883c20e1b34e \ + --hash=sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a \ + --hash=sha256:294ef8fb58a45f912513692380f366ca191940de5b3b3de5308ce2e8500d8ea4 \ + --hash=sha256:2a1269856278ae8bc78342bf20191c1f10638d2c22df72364d2fa02d70d49f35 \ + --hash=sha256:2c71da070224baf426850d9eab23ac797d9859b1841dbda43012c01b69697803 \ + --hash=sha256:2cbc83154c8b0201ada07bc5d6e37106df6325f2fda60d73228e2e8da7433cae \ + --hash=sha256:2d25e41851e41539898116ac760fcc856e2c1047a843a336b19e2c8e4a43ad16 \ + --hash=sha256:2ef9a284ec658d48ec57edfba0944d1582532601472f695b799ba08d37fd6544 \ + --hash=sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211 \ + --hash=sha256:307dbd844f678de48ea74cdc909b007729c50e6be7f182bdf6a1df6732374c69 \ + --hash=sha256:3367d5eefae493ac2a1586ec11cc8213c3305528ed3f8e19d5c3871cc01da6ce \ + --hash=sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b \ + --hash=sha256:33d3a772ff62c882a5d1402045e17c0199f33b9b173139071c4203e7e0292420 \ + --hash=sha256:356fc21b4c313decb3214a4306bb5bd0623dce4a8d03d0d5ada6fb0b6a5b93b5 \ + --hash=sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596 \ + --hash=sha256:3cc1a82779315f7b2a4642d5028b39057838cd4c0415744d4e53c8b512352141 \ + --hash=sha256:3d75065d9f6ed1afb2a41588def408cf442cee65b3651cbd7e86650146127bb4 \ + --hash=sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14 \ + --hash=sha256:445623b1337e971ccc571d3642aeb3f2fec77e60b6ee193dd7688168471d1846 \ + --hash=sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08 \ + --hash=sha256:490a81770599b17b8594227674872dfb215af05f2be0065a32a7c70175fd9e23 \ + --hash=sha256:4a9cd8485a6729387c889c88c24d5eace39dc0c0c9ddb9003f9c81751f654b69 \ + --hash=sha256:4ac52b95a938789fcfcfbf6faf647b45d7be940f0c51f06991c1353db54c67aa \ + --hash=sha256:4f39485bb02dae23e14cdbad086ab0e468756775bc5c64bb69e7cb756ebc1dcd \ + --hash=sha256:5133884ec10c9d6bcf7fed4ceb98fb3a6acbb6c2ba1bab6c4b6700d6c39c7595 \ + --hash=sha256:55493b5b6cb6c4ec9a3c310d4ef947dbd34326ee4eecd4249e18e00d84f14be5 \ + --hash=sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b \ + --hash=sha256:57f405fcb82e2b78df88f04f8aa49ce513ce23bfea073b581597bd52545e9b3f \ + --hash=sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77 \ + --hash=sha256:5dd356a646fe549d42b766cb9075b54eccd3f20604d87a3eff25f1430bba5b2e \ + --hash=sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151 \ + --hash=sha256:5fecba510f6b8f9cf1dfd103d785a61da47221cf09d4cec10946d1950daf1a17 \ + --hash=sha256:610371fe95c7e8e824ad8762248836cabbb5a4ab8befb982cb7dc8df773075d3 \ + --hash=sha256:61a9da95a836e7d300945891265bbeb6510a860863f6b01ed6397e6239a31253 \ + --hash=sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c \ + --hash=sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20 \ + --hash=sha256:647983d2609be6155c748249e770ab7e75e15e386cbf15469569f3eaf165bbb7 \ + --hash=sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce \ + --hash=sha256:690ab9d06cd689b79aab84afa0984a1bc85ea4b93b0a42b015f3ff5e2f5b08be \ + --hash=sha256:6b5dc780377e35be6b0cf6fec7fb4a45cadb1e834bc0ef2ce596cc015290ef69 \ + --hash=sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90 \ + --hash=sha256:6c997a1703401089bc02d731e360127428fb4ecdf6524268e8975882ff02528b \ + --hash=sha256:6f198b622a3ccf02eeab00de71f6b2f45b1b50b1979aa56b25178c963e475950 \ + --hash=sha256:6f64c66b3b13758b4f8f56f17972cd0ce5d0033d19d7332ed32e2dbdbce94dec \ + --hash=sha256:6fe39780de6916ecb1c664eda81802e6310fd9d4a07dccb13a86b63918e00e65 \ + --hash=sha256:70082b2a8f099b8bf660b553b22a4b8ffd34fcc09ef154fc6b9c7108518fc124 \ + --hash=sha256:72df83ee0eb89b070e28d1260786d13485b98a8b80228c7439d303dd9aac9970 \ + --hash=sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c \ + --hash=sha256:7bb75921a4885d30d9e881d7a595ce50407a8a82933e15451ad7e0d89d1a5944 \ + --hash=sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753 \ + --hash=sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3 \ + --hash=sha256:862c29f7e7927e71391df6700079b1dd7c2aeb75115dc46e37004a092f8f16b7 \ + --hash=sha256:8c8a63d55cdf3c716225a2f8741a1df7a52b5fa98ac7530165c9cc9b32feabcc \ + --hash=sha256:90aa2a7f9cc1cd2e8082db61618a2ed0f4197eef52266a6420e88277f184e328 \ + --hash=sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275 \ + --hash=sha256:9887e9455398a1517294dec14e23ed9a178c8dd909f2788e971b66623d3f7c16 \ + --hash=sha256:99034ec353c973e2c89555866083491b9a2dbe81f2fbe15fb0f2b68506232f01 \ + --hash=sha256:994fa00a9b0d14c6e6926ff5ade98d4d83676a5eeab6f87718170e481396d380 \ + --hash=sha256:99896cc18fb421be93e337d6bf2c1686ba330bc2d5c0ef581c842f0639a5e886 \ + --hash=sha256:9ce34fc5a6f6c9b2ba4a8060009d989e4f55630e3f9c3bf5962f42dcc31355ef \ + --hash=sha256:9e1c602c55dc8ec05cc7e0a8e31f3ad3d4644dbcb7ac39114105c9bd0384371f \ + --hash=sha256:9e61478a8a06e6456ff2e66b9ac18f7f28d76a75fa5fda0c5198a4de07b8dcb8 \ + --hash=sha256:a87ab35e92d40b53c5373af36625794e422f83ec56122f0a63871892856d721c \ + --hash=sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db \ + --hash=sha256:ad2027883344e70ddddff02259411f80faf47d5e779eb0e39cb95ee546fa628c \ + --hash=sha256:ae9f7055e7357b2873866a3d77d0136a251ca2ae2dde3d8cdb819425d717c177 \ + --hash=sha256:af06c9099df15ee44fda3fdfa002bfe37de02901c2b3a5ef350853861ef3b4b5 \ + --hash=sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9 \ + --hash=sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b \ + --hash=sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e \ + --hash=sha256:bb2d4ad7f9bac398a7a19f07bd09fd5b2c1e4eeab316065aa84a305f62fc5361 \ + --hash=sha256:bda6af6fb4d5fe9532620e4f72d3c7efcdf7472ead9037b184c0a060f0e7c71f \ + --hash=sha256:c07eaf30bc072b179acc8ac50519a2a81f03f88a220750c6d83dadbdc33fb1de \ + --hash=sha256:c0ea77435b1d5a27cccf27f8762f50e73fd2d94e8e412a8e5cdedb650b36d5fc \ + --hash=sha256:c1fa3f84cee5211a3e574ba76ac9596df8c8d16a855f31a25681d23eadcc6c16 \ + --hash=sha256:c90f34f1b1905d7d6c42b25b6ffb4e5066e6c95c7715169e3332b1760614d092 \ + --hash=sha256:cb6374a84f11a6b25e63aa69ee2d015286048c1efee93c4a3b5b8df62da79ff8 \ + --hash=sha256:cc5d0f82cf05beb6c0d463398173a09357ed4f4a631f7641cef6f6480a05bc57 \ + --hash=sha256:cce8e5243d95068f595155663e3e18b1938b16cf716dce6cf2491ebc08b98d96 \ + --hash=sha256:cda393bb35828e3993fcaf39c8ede78b16614954eb15fe77336d5d141be40f76 \ + --hash=sha256:ce6505846d29f860966e0e9cfadaad1041a7d8ce7dc4af39940fcb1877dec29a \ + --hash=sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec \ + --hash=sha256:d1d52739de118acf82bfbaf7046955ead4fb613d24ba4187be565cd91ff9a64e \ + --hash=sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28 \ + --hash=sha256:d539a51be176e874ed66029b2df8cb8e1123a3e6420d52a690de6effded4f20d \ + --hash=sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136 \ + --hash=sha256:d963442186918577ad83e3a8c5564eeeba90e2da233ce59f6eeccbb7b5cf771e \ + --hash=sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f \ + --hash=sha256:e27003c0a93a5aa541c260bd8ba8b917a2af4c378eb684daa9f1565f5e363181 \ + --hash=sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32 \ + --hash=sha256:e337dceb936f333775cf51d49f6badb8cd3d2a6b27e8cb443a6c5861fbf3c1f9 \ + --hash=sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9 \ + --hash=sha256:e7663a6803a47255c32cc7e17ae3ccfe02dba5b0849f87729651c0263a129d20 \ + --hash=sha256:ecc27adda0d1e1bc39793b41fdd562d2f4bc4dcee6ee0e3c733d519c332183b2 \ + --hash=sha256:edfd2b0cad175780f8668fd6f66486354b8770e4057e44038daaa4a066f8ddff \ + --hash=sha256:f02091b425bbcc2d8481913855c744baa4dd73e334814337b201d837e9040ef7 \ + --hash=sha256:f568bdc17b7ebb3a323ee8920468d2ed74af84da911a00d9585ac887bac73b88 \ + --hash=sha256:f584dd93ef6ddb10ba028e247fe1fc0ba52ed70ca4d596bb8d52bf4304c147ec \ + --hash=sha256:f5e8a0ce681ddabf6a35d7b817d74a94ab237ae7233a5b97118db0b4e524473f \ + --hash=sha256:f93c60d8c522b4ecea35dd6c58cc42f251ecb12882a5d67d4bd8d12137fbd05b \ + --hash=sha256:fbaf76379bf2a72e534bbb1276d45e63a80d8cade17953ed79a350d6426262fb # via tiktoken -requests==2.32.5 \ - --hash=sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6 \ - --hash=sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf +requests==2.34.2 \ + --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ + --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via - # google-api-core + # google-auth + # google-genai # langchain-classic # langchain-community # langsmith @@ -1597,508 +1946,631 @@ requests-toolbelt==1.0.0 \ --hash=sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6 \ --hash=sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06 # via langsmith -rich==14.3.3 \ - --hash=sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d \ - --hash=sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b - # via pyrag-kit -scikit-learn==1.8.0 \ - --hash=sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2 \ - --hash=sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a \ - --hash=sha256:146b4d36f800c013d267b29168813f7a03a43ecd2895d04861f1240b564421da \ - --hash=sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9 \ - --hash=sha256:2838551e011a64e3053ad7618dda9310175f7515f1742fa2d756f7c874c05961 \ - --hash=sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6 \ - --hash=sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271 \ - --hash=sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809 \ - --hash=sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242 \ - --hash=sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4 \ - --hash=sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7 \ - --hash=sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76 \ - --hash=sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6 \ - --hash=sha256:5e30adb87f0cc81c7690a84f7932dd66be5bac57cfe16b91cb9151683a4a2d3b \ - --hash=sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e \ - --hash=sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7 \ - --hash=sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e \ - --hash=sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57 \ - --hash=sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735 \ - --hash=sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb \ - --hash=sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb \ - --hash=sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e \ - --hash=sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd \ - --hash=sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a \ - --hash=sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9 \ - --hash=sha256:ada8121bcb4dac28d930febc791a69f7cb1673c8495e5eee274190b73a4559c1 \ - --hash=sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde \ - --hash=sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3 \ - --hash=sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f \ - --hash=sha256:c57b1b610bd1f40ba43970e11ce62821c2e6569e4d74023db19c6b26f246cb3b \ - --hash=sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3 \ - --hash=sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e \ - --hash=sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702 \ - --hash=sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c \ - --hash=sha256:f984ca4b14914e6b4094c5d52a32ea16b49832c03bd17a110f004db3c223e8e1 \ - --hash=sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4 \ - --hash=sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd - # via pyrag-kit -scipy==1.17.1 \ - --hash=sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0 \ - --hash=sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458 \ - --hash=sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118 \ - --hash=sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39 \ - --hash=sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e \ - --hash=sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6 \ - --hash=sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec \ - --hash=sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21 \ - --hash=sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1 \ - --hash=sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6 \ - --hash=sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce \ - --hash=sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8 \ - --hash=sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448 \ - --hash=sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19 \ - --hash=sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b \ - --hash=sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87 \ - --hash=sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4 \ - --hash=sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9 \ - --hash=sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b \ - --hash=sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082 \ - --hash=sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464 \ - --hash=sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87 \ - --hash=sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c \ - --hash=sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369 \ - --hash=sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad \ - --hash=sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f \ - --hash=sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c \ - --hash=sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475 \ - --hash=sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd \ - --hash=sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866 \ - --hash=sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d \ - --hash=sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6 \ - --hash=sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb \ - --hash=sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca \ - --hash=sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0 \ - --hash=sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca \ - --hash=sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d \ - --hash=sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee \ - --hash=sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4 \ - --hash=sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717 \ - --hash=sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49 \ - --hash=sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2 \ - --hash=sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a \ - --hash=sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350 \ - --hash=sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950 \ - --hash=sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b \ - --hash=sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086 \ - --hash=sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444 \ - --hash=sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068 \ - --hash=sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff \ - --hash=sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a \ - --hash=sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50 \ - --hash=sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696 \ - --hash=sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21 \ - --hash=sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c \ - --hash=sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484 \ - --hash=sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118 \ - --hash=sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3 \ - --hash=sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea \ - --hash=sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293 \ - --hash=sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76 - # via scikit-learn +rich==15.0.0 \ + --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ + --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 + # via + # bandit + # pyrag-kit +ruff==0.16.6 \ + --hash=sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70 \ + --hash=sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25 \ + --hash=sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39 \ + --hash=sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3 \ + --hash=sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88 \ + --hash=sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d \ + --hash=sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953 \ + --hash=sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8 \ + --hash=sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e \ + --hash=sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5 \ + --hash=sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876 \ + --hash=sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c \ + --hash=sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718 \ + --hash=sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4 \ + --hash=sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757 \ + --hash=sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050 \ + --hash=sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1 \ + --hash=sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32 +setuptools==84.0.0 \ + --hash=sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670 \ + --hash=sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73 + # via pyinstaller six==1.17.0 \ --hash=sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274 \ --hash=sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81 - # via python-dateutil + # via + # python-dateutil + # volcengine-python-sdk sniffio==1.3.1 \ --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc # via # anthropic + # google-genai + # langsmith # openai -sqlalchemy==2.0.48 \ - --hash=sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e \ - --hash=sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6 \ - --hash=sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0 \ - --hash=sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc \ - --hash=sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b \ - --hash=sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f \ - --hash=sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0 \ - --hash=sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894 \ - --hash=sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b \ - --hash=sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8 \ - --hash=sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131 \ - --hash=sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b \ - --hash=sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f \ - --hash=sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb \ - --hash=sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9 \ - --hash=sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c \ - --hash=sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241 \ - --hash=sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658 \ - --hash=sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7 \ - --hash=sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a \ - --hash=sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae \ - --hash=sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7 \ - --hash=sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3 \ - --hash=sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0 \ - --hash=sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2 \ - --hash=sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d \ - --hash=sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99 \ - --hash=sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485 \ - --hash=sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617 \ - --hash=sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096 \ - --hash=sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed \ - --hash=sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f \ - --hash=sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2 \ - --hash=sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933 \ - --hash=sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b \ - --hash=sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd \ - --hash=sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79 \ - --hash=sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4 \ - --hash=sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571 \ - --hash=sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c \ - --hash=sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121 \ - --hash=sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb +sqlalchemy==2.0.52 \ + --hash=sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee \ + --hash=sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca \ + --hash=sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43 \ + --hash=sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee \ + --hash=sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89 \ + --hash=sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9 \ + --hash=sha256:4699dbb8d396d199e7e78fd4d525e3ad3d6008a9c8c0160b87e74c606c2c3736 \ + --hash=sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd \ + --hash=sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8 \ + --hash=sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97 \ + --hash=sha256:5f8438a98d49424acf69d0d53c0a522951dfe49a6f2d86417fbb37ad3066ab43 \ + --hash=sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c \ + --hash=sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608 \ + --hash=sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437 \ + --hash=sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b \ + --hash=sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1 \ + --hash=sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1 \ + --hash=sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751 \ + --hash=sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d \ + --hash=sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057 \ + --hash=sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2 \ + --hash=sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e \ + --hash=sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15 \ + --hash=sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727 \ + --hash=sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582 \ + --hash=sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d \ + --hash=sha256:cef328349452ae152637df4d11ce5a0919ecdf0a363e16c830c3518ee33bde72 \ + --hash=sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e \ + --hash=sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c \ + --hash=sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf \ + --hash=sha256:f1c850792a3b25a3ad74dade3f05e4f402cdebfea27438bcadafaa1617f77bcc # via # langchain-classic # langchain-community +stevedore==5.9.1 \ + --hash=sha256:5c8ff3a9f336cc1a06ac0f597bc79d11a2f950bfd32e290ca56b5a301fafafbf \ + --hash=sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf + # via bandit tenacity==9.1.4 \ --hash=sha256:6095a360c919085f28c6527de529e76a06ad89b23659fa881ae0649b867a9d55 \ --hash=sha256:adb31d4c263f2bd041081ab33b498309a57c77f9acf2db65aadf0898179cf93a # via + # google-genai # langchain-community # langchain-core -threadpoolctl==3.6.0 \ - --hash=sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb \ - --hash=sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e - # via scikit-learn -tiktoken==0.12.0 \ - --hash=sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa \ - --hash=sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e \ - --hash=sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb \ - --hash=sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25 \ - --hash=sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff \ - --hash=sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b \ - --hash=sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5 \ - --hash=sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3 \ - --hash=sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def \ - --hash=sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded \ - --hash=sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be \ - --hash=sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd \ - --hash=sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a \ - --hash=sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0 \ - --hash=sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0 \ - --hash=sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b \ - --hash=sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37 \ - --hash=sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb \ - --hash=sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3 \ - --hash=sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3 \ - --hash=sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b \ - --hash=sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a \ - --hash=sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3 \ - --hash=sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160 \ - --hash=sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967 \ - --hash=sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646 \ - --hash=sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931 \ - --hash=sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a \ - --hash=sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697 \ - --hash=sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8 \ - --hash=sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa \ - --hash=sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365 \ - --hash=sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e \ - --hash=sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830 \ - --hash=sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16 \ - --hash=sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88 \ - --hash=sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f \ - --hash=sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63 \ - --hash=sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad \ - --hash=sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc \ - --hash=sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71 \ - --hash=sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27 \ - --hash=sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd + # pyrag-kit +tiktoken==0.14.0 \ + --hash=sha256:087538c080e5ff421abd3a0785ed63c5111d06af98e6cd0d374dbe5969147ca3 \ + --hash=sha256:11d8211b290855d2721334ff17dd9b3a17bfb26872be01f25d73612ef7ece890 \ + --hash=sha256:144a3fc369f92b7d548995217c5d6e84038d3572157a0f6f34080d65291d0f78 \ + --hash=sha256:149d97453c4c98c04b081d64a85e635921269b532710d6faf81e9e82b790e7d3 \ + --hash=sha256:14b47e3674f2624803a8acc8fb367b7e24fc53055f9df3296482fe9a3a34a232 \ + --hash=sha256:151d37a150c8f3dfc5f4345597b10e101876bd1bd13494e0185af6b508758d2e \ + --hash=sha256:19d643d701fdaa70e5b9c7f8f96abcaffe77ca5e482a3a1a7dde46feb4284695 \ + --hash=sha256:1b6e4adcfd285c44502aed51df98aaaca4f0fea028165dbf8a9e857b9f98d8ea \ + --hash=sha256:1f83081065ee5833d35b49e9180f3d8d15622a603dd1c435da0da6cc12b3662f \ + --hash=sha256:2157f52e4b4d7ac5ecc7457b3716834706e7ef9a46f5144029bfeb7cf71f4e06 \ + --hash=sha256:231dec90efcdccf1b565a1416107736f1e09b1a08fe736ef9d6363e626d03874 \ + --hash=sha256:26cc4b4840fa0e9f4b72ed489883e12f57e00d1021ca794720e3c29a12f0edef \ + --hash=sha256:26e60f6a956ee171ab728b37b8439905d7ea1db435c30f9822f291e9861c861d \ + --hash=sha256:2cc19ac87b41c9493c9778ff5847f0c8bbcf5bd0ec6b87ce06c1c802adc8a771 \ + --hash=sha256:2ea70afba6b9eddbf22c165142e5f0a2ad7aa36a452873c48b57bb2aeb8492ae \ + --hash=sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a \ + --hash=sha256:380873f330b741c4435574f37edb20813d04603ace2d53e0a63560e1fec83010 \ + --hash=sha256:3c5349c9f916283bba32bec8af69b763e4faa304dc004d0eaaea66a3cf004c1f \ + --hash=sha256:3de75343041a1c57333b1e707ac8a9769738241d7d6a55d39e12cf84548337c6 \ + --hash=sha256:3fd7c14b1cb45b486c39fc9b3443bb341f3e2fc7e6f31247f3435a5836651632 \ + --hash=sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9 \ + --hash=sha256:60c47ca69ddda0dea8256fffd12e1b86f4b59734a20e4a70c61f63cc5f021df4 \ + --hash=sha256:6eb94895c45f26bb8f5546e5fd8a069efcf6e3f108ea9d5cbe3bf6f7f3983438 \ + --hash=sha256:728303a072163130c5b477b1f20d6211895569c1d5302c24ffc93a3009160871 \ + --hash=sha256:78571efc311c30b73f31eb949a921d6dac39a5d9dc42d1cfa8f8db157b3447b1 \ + --hash=sha256:7896eea257fe497a2b7134474d909156c6744ce8da35bce88011a960e008aa0d \ + --hash=sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0 \ + --hash=sha256:7db45b98e94adf4173a5cd7422b150999a7ee11ff847783a14f6e1b80cc38cb6 \ + --hash=sha256:86951a971c53979ec857bd8c4a32dc227ab0fd33f6c12a3bd62d3fbf5f0bfcaa \ + --hash=sha256:86f66c85e796f5d05d5c4a60ec1d40cbfebc47a32464053528c797163fa9ab89 \ + --hash=sha256:8e947aefe98ef74cce94923f90e48c98fe34eb1ec0a6bfdfadfc5a96359bfc36 \ + --hash=sha256:90a762670c7f968184723769a06ed51f5cf5ce5dcd1e30164f25c72d85c2d1f1 \ + --hash=sha256:979c1524f753b662b0f3cd261b135afe6659cce33caaa7a5ea00dd1756b3055c \ + --hash=sha256:b950248272f1b303dc32986396e2dccfa10cf6d1e83ec8f0bba1776660305482 \ + --hash=sha256:c2edf09b381fafbc014ae8e018ed25087abb9a3dafa8465a0ea63c6558c47a79 \ + --hash=sha256:c77d4a3e1deb2707819df92046b89aad1ac81d27e07616b797cbff3f62c037da \ + --hash=sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58 \ + --hash=sha256:cbe2cc3bba939bcdaf103e03df9d5039d33887080b315624be28ec69059e5f94 \ + --hash=sha256:cd8ca1305c1c902fe42c486165f2e4808d9997625c98ffb05b9e0366d99d3948 \ + --hash=sha256:d0781223705199b289faa59601bb9c2441712d4c600dd13c43d8fd6a33d22cd5 \ + --hash=sha256:d6cebe67765569df3dafac8474e4eccf5c19d24140492567a5e58a11445732a4 \ + --hash=sha256:e067f4cbcc5d036e8aff7fe7a6b530a8f4de2e4616ad9005a24a1879e24e6450 \ + --hash=sha256:e2eca764c53490f8930dbce329e0769f11108d87d908282a80c5c130e26e7037 \ + --hash=sha256:e3442bbb2f0c588cec876061e37ae67b455b9df9978b003c8fe30e45f2ef5b42 \ + --hash=sha256:e4ddf863b59347deaa92302dcd90e5eb003cdc9be06ec2b692c38d1bdd9efd49 \ + --hash=sha256:e9c5fe393aab56469f04e432ff851216d3def3436cf5f07e442a240164bf500f \ + --hash=sha256:eceeff0c62419bc78d4b6e70a4762a4d25df3ae8f2d5946e3853ce93e7a57098 \ + --hash=sha256:f2af4a336ea56d6c14f27741a0e1d8294a35dd0b038bcf990d232ebb54eb994b \ + --hash=sha256:f5e7665f6624e052e5e7f6a36919ab69279decdc976d7b16b4fa15e1897d0513 \ + --hash=sha256:f702e0aeeb6506e57687e881c59e844ebe8f0a6a097ddafe20e3ab25f387be4e # via pyrag-kit -tqdm==4.67.3 \ - --hash=sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb \ - --hash=sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf +truststore==0.10.4 ; sys_platform != 'emscripten' \ + --hash=sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301 \ + --hash=sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981 # via - # google-generativeai - # openai -typing-extensions==4.15.0 \ - --hash=sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466 \ - --hash=sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548 + # httpcore2 + # httpx2 +typing-extensions==4.16.0 \ + --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ + --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via + # aiohttp # aiosignal # anthropic # anyio - # google-generativeai - # grpcio + # google-genai + # httpx2 # langchain-core + # langchain-protocol + # langsmith + # mypy # openai # pydantic # pydantic-core # sqlalchemy - # typing-inspect # typing-inspection -typing-inspect==0.9.0 \ - --hash=sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f \ - --hash=sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78 - # via dataclasses-json -typing-inspection==0.4.2 \ - --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ - --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 +typing-inspection==0.4.4 \ + --hash=sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47 \ + --hash=sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147 # via # pydantic # pydantic-settings -tzdata==2025.3 ; sys_platform == 'emscripten' or sys_platform == 'win32' \ - --hash=sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1 \ - --hash=sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7 - # via pandas -uritemplate==4.2.0 \ - --hash=sha256:480c2ed180878955863323eea31b0ede668795de182617fef9c6ca09e6ec9d0e \ - --hash=sha256:962201ba1c4edcab02e60f9a0d3821e82dfc5d2d6662a21abd533879bdb8a686 - # via google-api-python-client -urllib3==2.6.3 \ - --hash=sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed \ - --hash=sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4 - # via requests -uuid-utils==0.14.1 \ - --hash=sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b \ - --hash=sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3 \ - --hash=sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf \ - --hash=sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c \ - --hash=sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531 \ - --hash=sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2 \ - --hash=sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62 \ - --hash=sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae \ - --hash=sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0 \ - --hash=sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69 \ - --hash=sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a \ - --hash=sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8 \ - --hash=sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523 \ - --hash=sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1 \ - --hash=sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96 \ - --hash=sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43 \ - --hash=sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01 \ - --hash=sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297 \ - --hash=sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741 \ - --hash=sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862 \ - --hash=sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533 \ - --hash=sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba +urllib3==2.7.0 \ + --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ + --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 + # via + # requests + # volcengine-python-sdk +uuid-utils==0.17.0 \ + --hash=sha256:03815cea572c8a693cab5475b9d750cc161470961c7defa27e9286cad62f38f5 \ + --hash=sha256:04452640d8b6920c480c16e5afe91ff896d236e0c972830f9247e0898d38c803 \ + --hash=sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869 \ + --hash=sha256:0ab4a66e7a035ad6625cfc1fbdb34f5c2d25a80ae1ef4bfee458ea2036333c6d \ + --hash=sha256:0bc4c431ccd59c764080ceb43b126043325fe17861b87759d026a0cdd8423bb2 \ + --hash=sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6 \ + --hash=sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968 \ + --hash=sha256:1019476b6bdc047216ef7414be5babe0fa5ccfde977c0cac4fd6c75ddec66ff7 \ + --hash=sha256:14dc2f46abb1091260c0d203fcbdf4e045042cc07e49183fd3b255904b95eb70 \ + --hash=sha256:1776a80d16369999b21627028cc5dbce819be83e1e079fdd7a51b587d2916db9 \ + --hash=sha256:1fd6f0e8a162dc0e9255b6aebe3cd175e76c33202f1bf39da9e6294b93db0099 \ + --hash=sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3 \ + --hash=sha256:220b52746d99e11964badac3c0869016e0c24bafb70a7dd5c2c072a6be3da9cc \ + --hash=sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc \ + --hash=sha256:239d8a281fe10bae33205b5d43185834d556b18434e0a113b5dc1dfb2fd97e91 \ + --hash=sha256:29179ffb7b317239b6d6afb100d14c439c728770460718280b9c0a42d2561ec2 \ + --hash=sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310 \ + --hash=sha256:2dd4a21baaac9a88486f0dd166c5793feb101a0bb9f006f2c401657fff5a1343 \ + --hash=sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb \ + --hash=sha256:3150d836290c88f1d26eb59c4db280d87417dd3bfaadd2889c77416c8f0ff6fa \ + --hash=sha256:32abaafc8e91928b3d9f4d82e42d2094041e38ad6bb964066faadff28e4162f1 \ + --hash=sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9 \ + --hash=sha256:351462debd866f1f25e4d4f5c7fac89525b52151f0102a1bdfe94a999b046f5f \ + --hash=sha256:375cde148430d60a4a07c03abaa0774c4fddfdd90de99b4ba02f24088bc9d750 \ + --hash=sha256:387cf7437c94ddec08651a0f1081381299c7075bc48a6251d8922bf39973378a \ + --hash=sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988 \ + --hash=sha256:405233a5f625b3d995648f4647fa6befa4567cf3f74e1f6b9837e16f7310f0e0 \ + --hash=sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64 \ + --hash=sha256:42275ebd0e8e74e32cdbfb8bd88fc99576567d51d54a508020611fd8f4f463a0 \ + --hash=sha256:4441600447d340ae103a353f01dbcd22ff680e5ee1a22988efe8d7b791d8fdb3 \ + --hash=sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d \ + --hash=sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c \ + --hash=sha256:5641071337eb11d61a001ea08793bf72216f3241f0a433ed2764804b2a3e3cc7 \ + --hash=sha256:5670c52a438e21483ce715776144914a4e2a2a5c62d9dee15f8a3e90cf128ae6 \ + --hash=sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131 \ + --hash=sha256:570db214f6d8507587a8faa968a3fe65e957daeb7bc48b27dc7f69bc3ecdd6f1 \ + --hash=sha256:58838921e377791ef22c64cc92141bfae030f43651ff9272f0f28a208a9e6a5a \ + --hash=sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098 \ + --hash=sha256:622cdde768300591ac79bfcd7bb3468e4b191b1105d5dbfe8d87c39d8f63dd46 \ + --hash=sha256:673d89cc434cc9b97a0b4cf61272f6fca70a81f64eb0afbface2a0d9f77f06cd \ + --hash=sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e \ + --hash=sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a \ + --hash=sha256:6f29689a76fe7a49cbd629a794d0ec1eab48814e323a00a146a741b0195bde68 \ + --hash=sha256:75d7411e8eb9259764dd60310738540649057cda4509b4af14b36b7f663bfeb0 \ + --hash=sha256:793229621e1ad6cac55f015cfa9f4eff102accbc3da25d607b91c6b0bec167fb \ + --hash=sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73 \ + --hash=sha256:7b9044ce4acbf392d4b3a503fe377641f4deff82e6c341c36ef27af0dea76cdf \ + --hash=sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89 \ + --hash=sha256:89a0980d49683c00539c59cd9f46b1908c538e6b5b0a48ad12187bb856d0f391 \ + --hash=sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a \ + --hash=sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc \ + --hash=sha256:9082e709014946b1f6e96ae6ecd93652efca2d2a6a3ab67dbe151c8b4bf193a4 \ + --hash=sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287 \ + --hash=sha256:9472a8de37faf8bd216c628e0e68c8f6bef730d3ba0a5060f3b0fa460c992ac2 \ + --hash=sha256:967955620df45e6cffe2e9950cb9903cb455649396f896b26b04363a91a5054b \ + --hash=sha256:975c17da26c5b9d46c336b03c52a057ac28378d6f9d98b58d32a038589bb3912 \ + --hash=sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1 \ + --hash=sha256:9a91c4814c7150a4d798da691b7804eacd78c4b84fb392a60fa0de21341861eb \ + --hash=sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff \ + --hash=sha256:a46bedc273b6f58f11dee816ff74999625ef8d007890f411b7a4975bf1c89330 \ + --hash=sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2 \ + --hash=sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c \ + --hash=sha256:b5d11cccba076a32321ef1380dea956821f0b51794ef59df64e58fb1cd543aae \ + --hash=sha256:b6c5d2d71e1f17329150ad9427d27f4a3f29a01792e7ecdc64a98ac5368fc4d5 \ + --hash=sha256:c00d182e31034250690f417b9068b78eab423c10d76766664e82d9860c340479 \ + --hash=sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f \ + --hash=sha256:c4f845166b09acc65c5213a35551a7f81c17fa010ab467229b5813f79d17fe13 \ + --hash=sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63 \ + --hash=sha256:d27c531edb8d1f38ca2eddaa1fa24913a460aeb721f2efd4ef42a124ce94e354 \ + --hash=sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652 \ + --hash=sha256:d63010803d7c368963bbe6f7ec379593e76dd581d7db0f29118d88713c9e0354 \ + --hash=sha256:dd741c73440b328f937dc53b344ecadc46bc4f0cec0333a8f42b55f3468ce7ec \ + --hash=sha256:de1064663aa7c839286488a319d2b3b478ca5ab5b2091ade888ed0eeca11a98a \ + --hash=sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206 \ + --hash=sha256:e288a06cbbbcd01b44386e767985c9e21d2ad9bf59829aa7058d9a2a494804ab \ + --hash=sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637 \ + --hash=sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94 \ + --hash=sha256:e7b04935a79c03c41ad08d0a5f390aac968bfb561f1268897bc5b0f077971efd \ + --hash=sha256:f7e9b8728ba07a3cb2f29d5aa1a266c2664eb8ef0fd43afa34627c92f7fac8f0 \ + --hash=sha256:f9b093cb3b6c9d6233ef45a05cab064d2aa0a8cb3c5777084c9e20fcb77c2371 \ + --hash=sha256:fae8b282f0cb22a5de222999f7723f4e5ec04f6fcdf4aaef879b5b36625ae2b0 # via # langchain-core # langsmith -wcwidth==0.6.0 \ - --hash=sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad \ - --hash=sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159 +volcengine-python-sdk==5.0.48 \ + --hash=sha256:05c15f9138e016e479cdf29971d1e5fedbb9e8f4378930c68e2a9622d087995c \ + --hash=sha256:bb78995e35c34ad7a24e535826a1cb197e1209783c8a7dc11b001a09cb4bc48c + # via pyrag-kit +wcwidth==0.8.3 \ + --hash=sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb \ + --hash=sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4 # via prompt-toolkit -xxhash==3.6.0 \ - --hash=sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad \ - --hash=sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c \ - --hash=sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3 \ - --hash=sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b \ - --hash=sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f \ - --hash=sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c \ - --hash=sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1 \ - --hash=sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0 \ - --hash=sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec \ - --hash=sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d \ - --hash=sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67 \ - --hash=sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799 \ - --hash=sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679 \ - --hash=sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef \ - --hash=sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8 \ - --hash=sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa \ - --hash=sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa \ - --hash=sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad \ - --hash=sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7 \ - --hash=sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5 \ - --hash=sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11 \ - --hash=sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae \ - --hash=sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d \ - --hash=sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6 \ - --hash=sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2 \ - --hash=sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89 \ - --hash=sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e \ - --hash=sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6 \ - --hash=sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb \ - --hash=sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3 \ - --hash=sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b \ - --hash=sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db \ - --hash=sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119 \ - --hash=sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec \ - --hash=sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518 \ - --hash=sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296 \ - --hash=sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033 \ - --hash=sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729 \ - --hash=sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca \ - --hash=sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063 \ - --hash=sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5 \ - --hash=sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f \ - --hash=sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42 \ - --hash=sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e \ - --hash=sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392 \ - --hash=sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f \ - --hash=sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd \ - --hash=sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77 \ - --hash=sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d \ - --hash=sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1 \ - --hash=sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374 \ - --hash=sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263 \ - --hash=sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13 \ - --hash=sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62 \ - --hash=sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11 \ - --hash=sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0 \ - --hash=sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b \ - --hash=sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2 \ - --hash=sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6 \ - --hash=sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e \ - --hash=sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702 \ - --hash=sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405 \ - --hash=sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f \ - --hash=sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3 \ - --hash=sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a \ - --hash=sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8 \ - --hash=sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db \ - --hash=sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99 \ - --hash=sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a \ - --hash=sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204 \ - --hash=sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b \ - --hash=sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546 \ - --hash=sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95 \ - --hash=sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9 \ - --hash=sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54 \ - --hash=sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c \ - --hash=sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152 \ - --hash=sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4 \ - --hash=sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93 \ - --hash=sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd \ - --hash=sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd \ - --hash=sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248 \ - --hash=sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd \ - --hash=sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6 \ - --hash=sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf \ - --hash=sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7 \ - --hash=sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490 \ - --hash=sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0 \ - --hash=sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829 \ - --hash=sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746 \ - --hash=sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292 \ - --hash=sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6 \ - --hash=sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd \ - --hash=sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7 \ - --hash=sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0 \ - --hash=sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee +websockets==16.1.1 \ + --hash=sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175 \ + --hash=sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a \ + --hash=sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d \ + --hash=sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985 \ + --hash=sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1 \ + --hash=sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573 \ + --hash=sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9 \ + --hash=sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22 \ + --hash=sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328 \ + --hash=sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747 \ + --hash=sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab \ + --hash=sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499 \ + --hash=sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d \ + --hash=sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62 \ + --hash=sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512 \ + --hash=sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7 \ + --hash=sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf \ + --hash=sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa \ + --hash=sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57 \ + --hash=sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e \ + --hash=sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3 \ + --hash=sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0 \ + --hash=sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43 \ + --hash=sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe \ + --hash=sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31 \ + --hash=sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b \ + --hash=sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383 \ + --hash=sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217 \ + --hash=sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499 \ + --hash=sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8 \ + --hash=sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51 \ + --hash=sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509 \ + --hash=sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428 \ + --hash=sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead \ + --hash=sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc \ + --hash=sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1 \ + --hash=sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3 \ + --hash=sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0 \ + --hash=sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f \ + --hash=sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5 \ + --hash=sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be \ + --hash=sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df \ + --hash=sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb \ + --hash=sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293 \ + --hash=sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e \ + --hash=sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7 \ + --hash=sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3 \ + --hash=sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3 \ + --hash=sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a \ + --hash=sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56 \ + --hash=sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562 \ + --hash=sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0 \ + --hash=sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15 \ + --hash=sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869 \ + --hash=sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7 \ + --hash=sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999 \ + --hash=sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01 \ + --hash=sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57 \ + --hash=sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1 \ + --hash=sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458 \ + --hash=sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3 \ + --hash=sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392 \ + --hash=sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a \ + --hash=sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9 \ + --hash=sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785 \ + --hash=sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648 \ + --hash=sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49 \ + --hash=sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1 \ + --hash=sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7 \ + --hash=sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d \ + --hash=sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a \ + --hash=sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6 \ + --hash=sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231 \ + --hash=sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00 \ + --hash=sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac \ + --hash=sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea \ + --hash=sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c \ + --hash=sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81 \ + --hash=sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751 \ + --hash=sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68 \ + --hash=sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2 \ + --hash=sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c \ + --hash=sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57 \ + --hash=sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737 \ + --hash=sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b \ + --hash=sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854 \ + --hash=sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87 \ + --hash=sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2 \ + --hash=sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847 \ + --hash=sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8 \ + --hash=sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29 \ + --hash=sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051 + # via + # google-genai + # langgraph-sdk + # langsmith +xxhash==4.0.1 \ + --hash=sha256:0163b5d259de23ae9e07b7eabf435ce4704f6f205589a2b154e6af4be985ce1b \ + --hash=sha256:04f9a24de11a6647666d5302fd73d6a5224ce50ddc965fb0bb44cee736e6bd7c \ + --hash=sha256:06713a5aaf1d0905c5579416c020c02e42b3ceb931e86c7d3b7fb85403dee3f3 \ + --hash=sha256:06d7fbd609503c3be5e65cdb6bb2f040d6a98574404e2e1d5c60815c97fff4aa \ + --hash=sha256:08ed8da18cd4fd0a6a5d6a444852d8fbd0e565388a74a4937085451b5f1a312a \ + --hash=sha256:0ab851b45c70d4992be7cdeeee16f97a0b677408c758c4b1efb1cfe8030bfd37 \ + --hash=sha256:0b20a06454b34f1531fc677c54efe2ecdec691ef9224f7fa919bf2c1363f7ff1 \ + --hash=sha256:0b42a5a26607e4b2409fea174773a66f2dff9dfdbf2c1a851bb7b804e2c97535 \ + --hash=sha256:101aa300de6ceef3d9c77569706330d8921fc45dd82bceed2084f1e9f2557a24 \ + --hash=sha256:1216f7ba5683f17a89eb7dcb4bc50a0b743dfe1902278d7b3d0786f538118433 \ + --hash=sha256:1642907941ee4b75aacc3db688af52ea02ca2305ab22af7ee686ed726b332684 \ + --hash=sha256:1749f0688020209fe0d357ce1e1cd9ec9c6161ed0405ea949d24581c4c43fa91 \ + --hash=sha256:1b3cccf75eeb5b01639b2feadb042a8e07889293b7ca72fa2985e7dcb64763cf \ + --hash=sha256:1b50223d92df94d54e1a31469335a2c74b16692e6c1cb726f1e6949514458706 \ + --hash=sha256:1bc591533fc975614f7e13594daee76af96b8e1fbcf8de76c8773858fa9e7cea \ + --hash=sha256:1c7c642a0f79c3e3cf2965475507574d3d1a50ec71060039d60cb87358667cb2 \ + --hash=sha256:1ee523f51718e41753f04f7102bb4dc55a18d2ea5cbaceef8ec7ca08571bd428 \ + --hash=sha256:1f3346c5c287ac3c7f38b20380f55e8768230e7252af59fabcf3b87ab21e4256 \ + --hash=sha256:237b8f63a2a0fcfb1ffc06e21dad23add44e6d354b2b014364a1d41e419a4dee \ + --hash=sha256:23a4376b4a3183cb50d4d2a3179f887a7773cc695eb2c908e551bec3221b8c60 \ + --hash=sha256:247ece770647c0aef080561fa996f9774b4dadce2d0c42eeb98229db7dcf820d \ + --hash=sha256:2d52dc7c33c1b83082b707f6b7814dc76d2faaa2ea62bd9c5fab4b36f83c087f \ + --hash=sha256:2df3ca8757dc381e75e90a4d7995a6324f58a923c7145220a7b2c0231f66fddc \ + --hash=sha256:3088dadbffa33c29e0518578430a7dff2e901a212e487aefa5faaa0dc06dad34 \ + --hash=sha256:31d86f9e81f3e84e00131ac7c54caf5119ae4ddd82c09c31cff597c813ce1ee2 \ + --hash=sha256:348c8f288dc961d6bbd1985c8152a3ed7a85c95df00e82320f0c5215d922a399 \ + --hash=sha256:349775ac30372b344d2338b2a168c0a1312a644194da25b8bec476d55761a128 \ + --hash=sha256:34ed93e20bfd98d722b902121643791eeb4b1641871e2dc63d0d4c2d93f187df \ + --hash=sha256:37f667dee0f867c42894b34e2a6fe26bf195c0ea4683d9d2b713db023f242c3a \ + --hash=sha256:38c3d22129a6958846a3098d68bc8e661704461c0be4793ae28836e4690c8478 \ + --hash=sha256:3c2445edafc300cc40feb6a25a8356a971c30cd0bf47b5349c2ad74c508343b1 \ + --hash=sha256:41e579025a6e13a99e6d71e39c9cfc621a0dcdbbf19106325e145fa858f2d794 \ + --hash=sha256:421b94f3ba7067958d02e38960d987756347aa150df06df11aa68ae1af78c619 \ + --hash=sha256:427b62d62d4f967fbb10b82a3813e4875c2a6e7e7634739f17265b650c7f65a6 \ + --hash=sha256:43e5f9169e73d0f0db33b5f6b8554bcce69ac278c966daf83d5eb4eb2f13829f \ + --hash=sha256:440c401e146ce64bdb3beb8ff0c84677b6f21307c28a34779071cecee5d4d70c \ + --hash=sha256:44ab12e8cd17d4f001769f00ad465208b4bcb897ed29e65f058f74466b57a98f \ + --hash=sha256:4528cf80ebbbf57d40edfb31521ae265daa6dd636d615b1cf0ac86209579e59d \ + --hash=sha256:45e88111ebe331de478ef8d4293efbe88f3cf8b863386c9a2357136b838e1af0 \ + --hash=sha256:4741d42d59e4e5fa1a86c17ab9c27dc8ea459c700d91b6742fdb9138d9a516cb \ + --hash=sha256:4751f1d7eecae6b2d2a773630f1a7248f125c9a92a456694d03c15bceffc9d68 \ + --hash=sha256:488ca5c5e28ef56ec4bbb12f835b3f1cbecc5f3510062e70117bc6594851932a \ + --hash=sha256:4a252fb862b0ae2590587e625f47a0e03da05cf0205e8830b67b6596c06038b1 \ + --hash=sha256:4a76345f5aceb4ec404918edf9c7f2b5507db864dc0d7455982009ac0890b57b \ + --hash=sha256:4af350bc3f329970c0e3a59af84a8a30998bf8a9167eb50cd48e59baaa1d7bec \ + --hash=sha256:4e5141543c7f7fe3087500bbb4ac2845cb528a980aa91f8f1e661e2292ff4a5d \ + --hash=sha256:4f5e5c6df4b703afcbe9352d238a51efd97c3b91fdc3a2052e40fdacb1e7505f \ + --hash=sha256:515a822c73abbf6a0b7c70976d9662be342835c9d78b8dc7c023411f39c35dbc \ + --hash=sha256:554f87034635bcec47c5d72447bf3db7e02da1bf493a0ada010db28a76f891c6 \ + --hash=sha256:567cbc630302a46a8ecfd943b309ccf5372bb3718f1f3762d452df30f033bcf0 \ + --hash=sha256:57d7fa8f23908d173001c21a9e82bfc6ad997d1b6c270fb121812b7ed158891c \ + --hash=sha256:5adf927dca8c47fde7e683fe69efdd81bc865c4db1fb6bb00b391e2b6185207b \ + --hash=sha256:5b7875ac1a2edcb691f27642b8b94b904baa6bcecb7d79c72df2228ba8cb5c51 \ + --hash=sha256:5b7979f71d06ae45a769de0699900a246d8cb632db1e8bfdc79ec019063a503c \ + --hash=sha256:5c2d525a3afabcd8e3549d85fc7e111fde6bc302d06a1893fe73adb79823415e \ + --hash=sha256:5f1ea31d61bcd2cd2f3ec4ca80a64187bbd7948f490b63cf0dcbc6e717b4c1e9 \ + --hash=sha256:62198213fc3e0c56e567894b318ba45834e007d065f84ba6dc9165d21546fc56 \ + --hash=sha256:63aa52659bc32bb9bd7cb5caf523b4d14429a477762cfac886132d687c1f80fc \ + --hash=sha256:67e57b834e07ed973cee7b6da1548ff28a56458d77696fd2a5f397f340694848 \ + --hash=sha256:6a8c5ce76b94ba49f3be8a8f2611abc6564210702c72ac9e237ca2bebfd17794 \ + --hash=sha256:6a9f98af872355e0c02439e48583958eee00e60b928bb20476460d9d40cb7b4e \ + --hash=sha256:6c45258a37fc22721395c09927cb982d3e7a83607cab15be7e2416501bd3a330 \ + --hash=sha256:6cbf4e21ef0890804b5bb9ad25c48f9c127758d7f6c66bef374efcacc63c738a \ + --hash=sha256:6cf633df84d80a1668fcf61e330791dae46825e395549e7d34f376411e75088a \ + --hash=sha256:6efb8f21cc136c79b3e5bb747c8682d37916fb202cdbbc32182de5c4e47f821f \ + --hash=sha256:704381264b36a18b9c62ecbabe2e71d0fc58c77c129c15355c989b10bf05b6b0 \ + --hash=sha256:72f34834518157a75e7090f328ee7a16c70c804cfc7c694fa069cc888e9fc03e \ + --hash=sha256:74379a577a9f3b6afbdedf1b90e5c7764467051977f18a326d7d607336d743bd \ + --hash=sha256:74a164e8b63f1e9cf35c9a7809d082b033d1a00e7375d5d814415436e7867e57 \ + --hash=sha256:760de77279e9cf9c81d012ce0705cba13afccee9b09c480f17d778c8c5cefae8 \ + --hash=sha256:79a3203aadf39637869dfea1185227d8452844d78b837e54fb1117b4d34ba5c3 \ + --hash=sha256:7c343ee174d417a44d0c3355602c0cbbfa52a04d1bbbf1723378c7d2c8f60626 \ + --hash=sha256:81507a68ba84c55241fb61cce1469f473a5da4205fc8ef6f698e5948eea8dd88 \ + --hash=sha256:81664268dba92e037b740ecf37fa02f1cab4a391f93f28e35792b3341c60648f \ + --hash=sha256:839f58c5bd9989875be0fd28446dbf32cace2c2cd8bf2f6762acdc38a95cd1aa \ + --hash=sha256:83b8c2013edb5dc1f9e7268b6496130705bc48d79c86bb8817b3d210b81a5513 \ + --hash=sha256:8580aab306888224074c7edeec734de0c3c5ccde65b2da4e6c9a5e28f7c0a1bd \ + --hash=sha256:85e402dab0f9acd3604539747c6fcc57dc188a18af6ab07eb8189351cd32466c \ + --hash=sha256:863f3d3b44110f7243e86cf994aa5c5d88f2348b6e84ab4402fadadfbf9f7da7 \ + --hash=sha256:86b2b12bec60c678ed8f5cca0258ad93a8928ebddb6ca7732f0875afe1451d1a \ + --hash=sha256:87aa309a93bd5ec13f14309a305ff4e9bf74c5363fc46c264c0a22edfd5b0670 \ + --hash=sha256:87cbdec1a7dd930079671a60b249f3ca4e773e6fbd0676e21e36fdc9dd0f3b00 \ + --hash=sha256:8b4477edc03091f51f5309406d230851c23cf4822029e3bf40b8df53093fff1c \ + --hash=sha256:8b99ebaf9e816ac5069423b1367ee7e8078fbcebcf62545506bb0608d2f4f468 \ + --hash=sha256:8ba782ca3bf1e81492611152b9a0d5264971339e95e34d69de0ac2c926be496d \ + --hash=sha256:8bcba9456242ebf180a04d9443812fd85ffe6bd12bda464dd116fcece8886ff3 \ + --hash=sha256:8c9fe122444e129881afd1d4d1c7ac0d3ce2d91b68c2b40173b6025ff1c31f9a \ + --hash=sha256:90cb2a1c9cc503a054a19612b48ff6e8e47805f618bdb3224a07568aad03a37e \ + --hash=sha256:9283d9dd6b44acad35118e2976fc763a065509e4118debdb61916ec322ed17b9 \ + --hash=sha256:94ac8a6b8c47951173f0b67bf862bcb971bf24e493b9fbbdb0e010cbbc7d9f54 \ + --hash=sha256:96d8de55029d42251945531f6aa7590c32b48163c66a43bf29d8657d7446a377 \ + --hash=sha256:96dedccfb09a73a25751053a183159b88f4ee75f388df8166040c152ac0531c6 \ + --hash=sha256:9761ff4a0ffa583fe850731ad24fe82c88cccb7a2294727db0955f3279a4cb3f \ + --hash=sha256:97b94fb29abf21f5f0bde15f7dbdd3a4aa2dc59f37026adc7b4bee8563b84375 \ + --hash=sha256:99054b838b74d8d3995ea0d410976ae967c46207ae22d6ddfc535e809197dab9 \ + --hash=sha256:99166cc98637e8bf550cda2aab07f4f1d5f899c45fbd721801aeabcc9d404824 \ + --hash=sha256:9a51b061d54cda8b83e62c44458bfbf0dabbef9b975dd9649952ba5076b9f349 \ + --hash=sha256:9c3c4b9aa9a27196b921197f7daf9e6c1412739df06a99cfa6e923879362eff6 \ + --hash=sha256:a14578102a6081465aec9cf73c76c3cd3f79f0709bdb3b8ae7ab0b54c9d8b089 \ + --hash=sha256:a16a3fa6936e36bb1414d16a6bd012c9033e5161b68b426805b61d895392437d \ + --hash=sha256:a33de7633c948ab2dc144af370a66e7e7af29b425dcd0f7e4f59689fb9391b53 \ + --hash=sha256:a43418e1a90b4809a9caf64aeb8b0696e3e1f300a323acc1e6ee2f93ae319fcf \ + --hash=sha256:a4553d36cc0b7fce1f35ba8a94dfd775aa3ed12f5eab2dc3b46ac75a0706b0bb \ + --hash=sha256:a5b21b42a01a343096a1c018d35e9b7aec9c7065dda53ae8da071e37478b2cea \ + --hash=sha256:a65785e653573fcd1e33062760ab4c3c3440e8e910765018e4b6ed4ad07b54a0 \ + --hash=sha256:a6e3653df1a70b8ac4191216324242e4be2bca18c9a7c10934e1bd56dc7ca15e \ + --hash=sha256:a865d2d470220e659220fdb59d5b6c4422802d8d6098e1324bc4d12444798914 \ + --hash=sha256:a949b072ea59c6eca0811ccd9e95133cc50d2afda8d464b5b077c78f78efa269 \ + --hash=sha256:aa6ccc7f31018484d652cf52db020003433f3c9fa83189c028bd807d2adde503 \ + --hash=sha256:ac0f291ab6485bd71f33941f9b92771318332a05d505460b41e893a549caadc0 \ + --hash=sha256:acb31ecdd1a97fab5cd39a84ee9f515e727d319f796fec48703b8339b9998360 \ + --hash=sha256:acf52474b2494ef66dc7e0fb6d5e2b50c18313039ad4d275fbf9f9907c804bc5 \ + --hash=sha256:adbd48b30e3f82c89fb2b3e6a87cdd28d113b190a5ed0ee2dee286323ee9a621 \ + --hash=sha256:af05a3f650220a6c59fa0ad2410249f2d2470a05225807c378fb67458693f8df \ + --hash=sha256:b3662719007e059abde7eddacf8517142ba076ddc7b30c807260e57d28c3c191 \ + --hash=sha256:b3bece52127ac20044311ee73567f9f0893b5de64f9028aecc90cc740cfd525a \ + --hash=sha256:b4c8842fb19d78b5e8c2a52baf4c8357658cc56c62bc822b86ce0f942f28e286 \ + --hash=sha256:b659fad79c99b0238c7ad7e9d7dbf4eebfea9097c2dba65fa0a4d18a25b29a2f \ + --hash=sha256:b6c1f9c59bbe593f88a0aad30be4150f15bd57bd64efb95feeabcb8e563f1ecd \ + --hash=sha256:bdd16718b63aa3ebd68aabb79021a40e47c81374852d41a306b9453141bbcbee \ + --hash=sha256:bf430c587f447a554c53768ad76b9846fe7c5632180ef6f69c4fce8b0552fbd0 \ + --hash=sha256:bfed61996d618eb90d6eaae0178002e3466a28b06bfc557a7a3a7266378d8c5a \ + --hash=sha256:c0e6ccc2b19ec8a726b2e26062ac71ea63e15500d6bf85910e42481844fdffc1 \ + --hash=sha256:c101180495cb4ba3617b279a944345c53a5e73b0c150053d1fa8d8af32de9579 \ + --hash=sha256:c10b9206753b64aa791b35b201485477525b26fdec5bf86e8364c388a03e2592 \ + --hash=sha256:c3074db513c81f764053e3da079312ecf85a50d8350c71f4cc0105d9662a9e6c \ + --hash=sha256:c30dd1af66a820820398b26e0d74e7a9aa43cae705924f23ed828cd8e5c26c3d \ + --hash=sha256:c57963970d359a72262f7fe6be88f945e2334d4bc41462b7f08c37b0abf35ca6 \ + --hash=sha256:c6301d92545c591ad31c3e050aa40a5f8a4c16413f1f9e6f9322c6f0f9d2b736 \ + --hash=sha256:c6370189e8e66b7e608f533b939a9de092ddca6cce084ca0d3d414d2ed5b5d59 \ + --hash=sha256:c6fc415b5568bd9accc7187f1729a99707330c0a67a8b9f93c1149ed573ed75d \ + --hash=sha256:cba763d84b06bda2c38d5185dee76f1b9dfdc0789e96e476d9e10005526d0788 \ + --hash=sha256:cd878d32f5c6cbce9783f8d6897561fb772211edba9dde49d85672b88ed45276 \ + --hash=sha256:d0d24a4f3fb63852cd09af46ae4b7a4d00cc8b8615a046dca543786e728d1056 \ + --hash=sha256:d54b8ae068af532c8cdf56abb9e09a60fbe7b10792444c9c27987bb6d3b450fa \ + --hash=sha256:d55bf4ef10eb09b8b6866790e083d26d087d84caa3cc0946ba87c3ca7ecaf7b7 \ + --hash=sha256:d9f3848ffaf010bdbabdbf4c25641fa258b6227ff27bc74a4d06edef521a4873 \ + --hash=sha256:da0264844a09b538c894e5eff25313d941deb4dedec2131b98418a71a3c9944e \ + --hash=sha256:da544672efd9ad76077928a3e6c5d894e52ce82d3bf14002db4a1bf17d1a36a2 \ + --hash=sha256:daade8936c4deaaf7b01561324ce438ba4f885d717e9adc62b4d67212ad7d7bd \ + --hash=sha256:deca2a30d983d240b8375ec2ee0a4288e72042827fc61df2f7671f8467e4cb2f \ + --hash=sha256:e259bb7e1e2d8de6b35f430f5c7220b1c0ebf3962d1ba7ec7545980d5931edb8 \ + --hash=sha256:e3996ff9b6f99180357024336bf5749a8ad6476a9a2523e535c5212b995b12a2 \ + --hash=sha256:e4296fcc790876a8b0f297edc83d3b088457b774d8f67b4636807f8a2ec69a79 \ + --hash=sha256:e681a6fc7e4f715252b9b5acfb30536ec7dd1f75033a32dc617e6fa95af1a3fd \ + --hash=sha256:e71b34978e77868cbf2d18c5206a4603f9c644dd7181bec5643bd40141d3b8c5 \ + --hash=sha256:e8cda075b10bb3917b002c74a04f9e02b7d13b5bf732571404d51c52b11c7329 \ + --hash=sha256:e90b4bcf1d9eb1010fdaee7c9209fb667e74c0684f3ba17f9032bd7319da90c9 \ + --hash=sha256:e998cb3685b92101ec5de0fb4d9485cf01e50bc418211955c55d98064664cf4c \ + --hash=sha256:ec1a470c6db94ac4589c203921e89ac1bc13e796a8b1784d8135e1893559cd3b \ + --hash=sha256:edccc2ec58435a580f96a48a3ccae8cd0a480824119165dd90108718ad81ae6e \ + --hash=sha256:f00330ac7e24769e2032203f2b01794d670916b0c1799fd261340f1af9499875 \ + --hash=sha256:f09ee747e2a5f876cc5ad56947734811828335e13b403dd8ea1e06d77a9dd48d \ + --hash=sha256:f18732adcc271741bd651c3e56fa519d8a237d2cccda01fe3afb226bf87f783b \ + --hash=sha256:f33cf0baa91eccd2cb7b62bf00f10c2264ef578b71dd33a12962e71a36eb4d32 \ + --hash=sha256:f3e1a44af01b6692de0ec6caba5f0bf93ceb36896e02b7fc00952c6ea7ef39e1 \ + --hash=sha256:f484ed57bb3e4142f9d6439568658c38be5f94b702ba00a1ff32c69783b6c66d \ + --hash=sha256:f5d031f35962e5483a613214e61f09fe24ab523062c3646d592dc16c4a217451 \ + --hash=sha256:f6247f5e23ee94f2557ac9dab738a336f607c6ff476fcf66ca70c3aef5eee15a \ + --hash=sha256:f7db035447a0ac8959aa230c5d36545ecf9f547413eb1711c0ca6f0ba1418925 \ + --hash=sha256:fac4832b638000106207bc44e44b9616a6a416aaee56c62b01d61f3705e49f58 \ + --hash=sha256:fb59a0dd61fb2ad481c03fda399d78ce57dab6bb62c2c8fdb446a7ba4754b89a \ + --hash=sha256:ff48915bf1871a1f19f74c11834c6329443d306cedc0c05fe7fe617810422a80 \ + --hash=sha256:ffa44b4c7c5d0ffa31356b4428659516c0e47647825c74079a296b3857b6d99d # via # langgraph # langsmith -yarl==1.23.0 \ - --hash=sha256:03214408cfa590df47728b84c679ae4ef00be2428e11630277be0727eba2d7cc \ - --hash=sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4 \ - --hash=sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85 \ - --hash=sha256:0e40111274f340d32ebcc0a5668d54d2b552a6cca84c9475859d364b380e3222 \ - --hash=sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de \ - --hash=sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25 \ - --hash=sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e \ - --hash=sha256:170e26584b060879e29fac213e4228ef063f39128723807a312e5c7fec28eff2 \ - --hash=sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e \ - --hash=sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860 \ - --hash=sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957 \ - --hash=sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760 \ - --hash=sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52 \ - --hash=sha256:1dc702e42d0684f42d6519c8d581e49c96cefaaab16691f03566d30658ee8788 \ - --hash=sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912 \ - --hash=sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719 \ - --hash=sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035 \ - --hash=sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412 \ - --hash=sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41 \ - --hash=sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4 \ - --hash=sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd \ - --hash=sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a \ - --hash=sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4 \ - --hash=sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34 \ - --hash=sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069 \ - --hash=sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25 \ - --hash=sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2 \ - --hash=sha256:4764a6a7588561a9aef92f65bda2c4fb58fe7c675c0883862e6df97559de0bfb \ - --hash=sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f \ - --hash=sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5 \ - --hash=sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8 \ - --hash=sha256:4c41e021bc6d7affb3364dc1e1e5fa9582b470f283748784bd6ea0558f87f42c \ - --hash=sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512 \ - --hash=sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6 \ - --hash=sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5 \ - --hash=sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9 \ - --hash=sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072 \ - --hash=sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5 \ - --hash=sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277 \ - --hash=sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a \ - --hash=sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae \ - --hash=sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2 \ - --hash=sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4 \ - --hash=sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70 \ - --hash=sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723 \ - --hash=sha256:6b41389c19b07c760c7e427a3462e8ab83c4bb087d127f0e854c706ce1b9215c \ - --hash=sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9 \ - --hash=sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5 \ - --hash=sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e \ - --hash=sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c \ - --hash=sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4 \ - --hash=sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2 \ - --hash=sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b \ - --hash=sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7 \ - --hash=sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2 \ - --hash=sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474 \ - --hash=sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7 \ - --hash=sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123 \ - --hash=sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595 \ - --hash=sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe \ - --hash=sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea \ - --hash=sha256:99c8a9ed30f4164bc4c14b37a90208836cbf50d4ce2a57c71d0f52c7fb4f7598 \ - --hash=sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679 \ - --hash=sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8 \ - --hash=sha256:9ee33b875f0b390564c1fb7bc528abf18c8ee6073b201c6ae8524aca778e2d83 \ - --hash=sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6 \ - --hash=sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f \ - --hash=sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94 \ - --hash=sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51 \ - --hash=sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120 \ - --hash=sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039 \ - --hash=sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1 \ - --hash=sha256:aafe5dcfda86c8af00386d7781d4c2181b5011b7be3f2add5e99899ea925df05 \ - --hash=sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144 \ - --hash=sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa \ - --hash=sha256:b2c6b50c7b0464165472b56b42d4c76a7b864597007d9c085e8b63e185cf4a7a \ - --hash=sha256:b35d13d549077713e4414f927cdc388d62e543987c572baee613bf82f11a4b99 \ - --hash=sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928 \ - --hash=sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d \ - --hash=sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3 \ - --hash=sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434 \ - --hash=sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86 \ - --hash=sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46 \ - --hash=sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319 \ - --hash=sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67 \ - --hash=sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c \ - --hash=sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169 \ - --hash=sha256:cbb0fef01f0c6b38cb0f39b1f78fc90b807e0e3c86a7ff3ce74ad77ce5c7880c \ - --hash=sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59 \ - --hash=sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4 \ - --hash=sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a \ - --hash=sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb \ - --hash=sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f \ - --hash=sha256:dc52310451fc7c629e13c4e061cbe2dd01684d91f2f8ee2821b083c58bd72432 \ - --hash=sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090 \ - --hash=sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4 \ - --hash=sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b \ - --hash=sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543 \ - --hash=sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24 \ - --hash=sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5 \ - --hash=sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b \ - --hash=sha256:f2af5c81a1f124609d5f33507082fc3f739959d4719b56877ab1ee7e7b3d602b \ - --hash=sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6 \ - --hash=sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735 \ - --hash=sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e \ - --hash=sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28 \ - --hash=sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3 \ - --hash=sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401 \ - --hash=sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6 \ - --hash=sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d +yarl==1.24.5 \ + --hash=sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36 \ + --hash=sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331 \ + --hash=sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498 \ + --hash=sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780 \ + --hash=sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027 \ + --hash=sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144 \ + --hash=sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76 \ + --hash=sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3 \ + --hash=sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb \ + --hash=sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740 \ + --hash=sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec \ + --hash=sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e \ + --hash=sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a \ + --hash=sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24 \ + --hash=sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95 \ + --hash=sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb \ + --hash=sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928 \ + --hash=sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c \ + --hash=sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41 \ + --hash=sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550 \ + --hash=sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f \ + --hash=sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b \ + --hash=sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e \ + --hash=sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede \ + --hash=sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad \ + --hash=sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104 \ + --hash=sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2 \ + --hash=sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba \ + --hash=sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9 \ + --hash=sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12 \ + --hash=sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1 \ + --hash=sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027 \ + --hash=sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385 \ + --hash=sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840 \ + --hash=sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2 \ + --hash=sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4 \ + --hash=sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc \ + --hash=sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c \ + --hash=sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213 \ + --hash=sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448 \ + --hash=sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0 \ + --hash=sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6 \ + --hash=sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966 \ + --hash=sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58 \ + --hash=sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54 \ + --hash=sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075 \ + --hash=sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4 \ + --hash=sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f \ + --hash=sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4 \ + --hash=sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c \ + --hash=sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f \ + --hash=sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7 \ + --hash=sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9 \ + --hash=sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723 \ + --hash=sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047 \ + --hash=sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da \ + --hash=sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293 \ + --hash=sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b \ + --hash=sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61 \ + --hash=sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca \ + --hash=sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a \ + --hash=sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a \ + --hash=sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688 \ + --hash=sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16 \ + --hash=sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d \ + --hash=sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077 \ + --hash=sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce \ + --hash=sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd \ + --hash=sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d \ + --hash=sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88 \ + --hash=sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5 \ + --hash=sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75 \ + --hash=sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1 \ + --hash=sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f \ + --hash=sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b \ + --hash=sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff \ + --hash=sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9 \ + --hash=sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f \ + --hash=sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0 \ + --hash=sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5 \ + --hash=sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25 \ + --hash=sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6 \ + --hash=sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373 \ + --hash=sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd \ + --hash=sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d \ + --hash=sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba \ + --hash=sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104 # via aiohttp zstandard==0.25.0 \ --hash=sha256:011d388c76b11a0c165374ce660ce2c8efa8e5d87f34996aa80f9c0816698b64 \ diff --git a/scripts/build_binary_release.py b/scripts/build_binary_release.py old mode 100644 new mode 100755 index 93d80c5..e07246d --- a/scripts/build_binary_release.py +++ b/scripts/build_binary_release.py @@ -1,16 +1,17 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- import argparse import os +import platform import shutil -import subprocess + +# The release command is assembled from fixed local build arguments. +import subprocess # nosec B404 import sys import tarfile import tomllib import zipfile from pathlib import Path - PROJECT_ROOT = Path(__file__).resolve().parent.parent BUILD_ROOT = PROJECT_ROOT / "build" DIST_ROOT = PROJECT_ROOT / "dist" @@ -21,6 +22,9 @@ "README.md", "LICENSE", "DIFY_LICENSE", + # Dify 衍生代码遵循修改后的 Apache 2.0,该许可证第 4(a) 条要求随分发 + # 提供许可证副本;DIFY_LICENSE 只是引用它的摘要,不能替代全文。 + "licenses/APACHE-2.0.txt", "config.toml.example", ".env.example", ] @@ -47,6 +51,54 @@ "_pytest", ] +_TARGET_ENVIRONMENTS = { + "windows-x64": ("windows", "x86_64"), + "macos-x64": ("darwin", "x86_64"), + "macos-arm64": ("darwin", "arm64"), + "linux-x64": ("linux", "x86_64"), + "linux-arm64": ("linux", "arm64"), +} + + +def _normalize_machine(machine: str) -> str: + aliases = { + "amd64": "x86_64", + "x64": "x86_64", + "x86-64": "x86_64", + "aarch64": "arm64", + } + normalized = machine.strip().lower().replace("-", "_") + return aliases.get(normalized, normalized) + + +def validate_target_environment( + target: str, + *, + system: str | None = None, + machine: str | None = None, +) -> None: + """Reject targets that the current PyInstaller host cannot produce. + + PyInstaller builds for the interpreter's host platform and architecture; + the release script does not perform cross-compilation. Failing before the + output cleanup prevents an incorrectly named artifact from replacing a + previously valid one. + """ + expected = _TARGET_ENVIRONMENTS.get(target) + if expected is None: + raise ValueError(f"不支持的发布目标: {target}") + + actual_system = (system or platform.system()).strip().lower() + actual_machine = _normalize_machine(machine or platform.machine()) + expected_system, expected_machine = expected + if actual_system != expected_system or actual_machine != expected_machine: + detected = f"{actual_system}-{actual_machine}" + required = f"{expected_system}-{expected_machine}" + raise RuntimeError( + f"发布目标 {target} 需要 {required} 构建环境,当前检测到 {detected}。" + " PyInstaller 不执行跨平台交叉编译,请在匹配的 runner 或主机上构建。" + ) + def load_version() -> str: with (PROJECT_ROOT / "pyproject.toml").open("rb") as handle: @@ -103,7 +155,8 @@ def run_pyinstaller() -> None: for excluded_module in EXCLUDED_MODULES: command.extend(["--exclude-module", excluded_module]) command.append("main.py") - subprocess.run(command, cwd=PROJECT_ROOT, check=True) + # shell=False and fixed command arguments keep this invocation bounded. + subprocess.run(command, cwd=PROJECT_ROOT, check=True) # nosec B603 def stage_bundle(target: str, version: str) -> Path: @@ -114,7 +167,11 @@ def stage_bundle(target: str, version: str) -> Path: shutil.copytree(app_source, app_target) for relative_file in PACKAGE_FILES: - shutil.copy2(PROJECT_ROOT / relative_file, bundle_root / relative_file) + target = bundle_root / relative_file + # PACKAGE_FILES 含子目录路径(如 licenses/APACHE-2.0.txt), + # copy2 不会自动创建父目录。 + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(PROJECT_ROOT / relative_file, target) prepare_runtime_layout(bundle_root) return bundle_root @@ -158,7 +215,8 @@ def executable_path(bundle_root: Path) -> Path: def validate_bundle(bundle_root: Path) -> None: executable = executable_path(bundle_root) - result = subprocess.run( + # The executable is the bundle produced immediately before validation. + result = subprocess.run( # nosec B603 [str(executable), "--smoke-test"], cwd=bundle_root, text=True, @@ -174,6 +232,7 @@ def main() -> None: args = parse_args() version = load_version() + validate_target_environment(args.target) clean_output_dirs() run_pyinstaller() bundle_root = stage_bundle(args.target, version) diff --git a/scripts/embed_knowledge_base.py b/scripts/embed_knowledge_base.py index fcde6bc..fcc20c2 100644 --- a/scripts/embed_knowledge_base.py +++ b/scripts/embed_knowledge_base.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- import argparse import asyncio import sys from pathlib import Path -from typing import Optional from rich import box from rich.console import Console @@ -30,12 +28,10 @@ def display_config_and_confirm(splitter_structure_mode: str): settings = get_settings() run_config = build_run_config(settings) - def mask_api_key(key: Optional[str]) -> str: + def mask_api_key(key: str | None) -> str: if not key or key == "no-key-required": return "[dim]未设置或无需设置[/dim]" - if len(key) > 12: - return f"[white]{key[:6]}...{key[-4:]}[/white]" - return "[white]已设置[/white]" + return "[dim]已设置(值已隐藏)[/dim]" table = Table( box=box.ROUNDED, diff --git a/scripts/extract_release_notes.py b/scripts/extract_release_notes.py old mode 100644 new mode 100755 index 27ca185..f682f5f --- a/scripts/extract_release_notes.py +++ b/scripts/extract_release_notes.py @@ -1,9 +1,7 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- import argparse from pathlib import Path - PROJECT_ROOT = Path(__file__).resolve().parent.parent CHANGELOG_PATH = PROJECT_ROOT / "CHANGELOG.md" diff --git a/src/chat/core.py b/src/chat/core.py index 215a80a..cf77b25 100644 --- a/src/chat/core.py +++ b/src/chat/core.py @@ -1,7 +1,9 @@ -# -*- coding: utf-8 -*- import asyncio +import inspect import os -from typing import Any, AsyncGenerator, Dict, List, Optional +from collections.abc import AsyncGenerator +from copy import deepcopy +from typing import Any from prompt_toolkit import PromptSession from prompt_toolkit.formatted_text import HTML @@ -13,10 +15,16 @@ os.environ.setdefault("PROMPT_TOOLKIT_NO_CPR", "1") +from ..providers.__base__.model_provider import LargeLanguageModel, close_resource_sync from ..providers.factory import ModelProviderFactory from ..retrieval.vdb.base import VectorStoreBase from ..retrieval.vdb.factory import VectorStoreFactory -from ..runtime.contracts import RunConfig, SessionConfig, build_run_config, build_session_config +from ..runtime.contracts import ( + RunConfig, + SessionConfig, + build_run_config, + build_session_config, +) from ..services.chat_service import ChatService from ..services.embedding_service import EmbeddingService from ..services.retrieval_service import RetrievalService @@ -24,6 +32,7 @@ from ..ui.display_utils import display_chat_config, get_relative_path from ..utils.config import get_settings from ..utils.log_manager import get_chat_logger +from ..utils.security import safe_exception_text class Chatbot: @@ -31,10 +40,10 @@ def __init__(self, console: Console): self.console = console self.run_config: RunConfig = build_run_config(get_settings()) self.session_config: SessionConfig = build_session_config(get_settings()) - self.vector_store: Optional[VectorStoreBase] = None - self.retrieval_service: Optional[RetrievalService] = None - self.llm_model = None - self.chat_service: Optional[ChatService] = None + self.vector_store: VectorStoreBase | None = None + self.retrieval_service: RetrievalService | None = None + self.llm_model: LargeLanguageModel | None = None + self.chat_service: ChatService | None = None self.logger = get_chat_logger() self._initialize_vector_store() @@ -45,7 +54,7 @@ def chat_config(self) -> SessionConfig: return self.session_config @chat_config.setter - def chat_config(self, value: SessionConfig | Dict[str, Any]): + def chat_config(self, value: SessionConfig | dict[str, Any]): if isinstance(value, SessionConfig): self.session_config = value return @@ -63,49 +72,177 @@ def _initialize_vector_store(self): self.console.print("[green]知识快照加载成功。[/green]") def reload_llm(self) -> bool: + previous_model = self.llm_model + new_model: LargeLanguageModel | None = None try: llm_key = self.session_config.active_llm_configuration - llm_model = ModelProviderFactory.get_llm_provider(llm_key) + llm_configurations = getattr(self.session_config, "llm_configurations", None) + if llm_configurations is None: + new_model = ModelProviderFactory.get_llm_provider(llm_key) + else: + new_model = ModelProviderFactory.get_llm_provider(llm_key, llm_configurations) if self.retrieval_service is None: raise RuntimeError("检索服务尚未初始化。") - chat_service = ChatService(llm_model, self.retrieval_service) - self.llm_model = llm_model + chat_service = ChatService(new_model, self.retrieval_service) + self.llm_model = new_model self.chat_service = chat_service + if previous_model is not None and previous_model is not new_model: + self._dispose_model(previous_model) return True - except Exception as exc: - self.console.print(f"[bold red]重载 LLM 出错: {exc}[/bold red]") + except Exception as exc: # noqa: BLE001 - reload boundary reports provider failures + if new_model is not None and new_model is not self.llm_model: + self._dispose_model(new_model) + self.console.print(f"[bold red]重载 LLM 出错: {safe_exception_text(exc)}[/bold red]") return False - def apply_config_update(self, updated_config: SessionConfig | Dict[str, Any], llm_needs_reload: bool) -> None: - previous_llm = self.session_config.active_llm_configuration + def _dispose_model(self, model: LargeLanguageModel) -> None: + """关闭已替换或初始化失败的 Provider,避免连接池泄漏。""" + try: + close_resource_sync(model, "LLM Provider") + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "关闭 LLM Provider 失败: %s", + safe_exception_text(exc), + ) + + async def _adispose_model(self, model: LargeLanguageModel) -> None: + """在异步会话中释放同步、异步 SDK 客户端。""" + close = getattr(model, "aclose", None) + if callable(close): + try: + result = close() + if inspect.isawaitable(result): + await result + return + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "异步关闭 LLM Provider 失败: %s", + safe_exception_text(exc), + ) + return + await asyncio.to_thread(self._dispose_model, model) + + def close(self) -> None: + model = self.llm_model + self.llm_model = None + self.chat_service = None + if model is not None: + self._dispose_model(model) + retrieval_service = self.retrieval_service + self.retrieval_service = None + close_retrieval = getattr(retrieval_service, "close", None) + if callable(close_retrieval): + try: + close_retrieval() + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "关闭检索服务失败: %s", + safe_exception_text(exc), + ) + + async def aclose(self) -> None: + model = self.llm_model + self.llm_model = None + self.chat_service = None + if model is not None: + await self._adispose_model(model) + retrieval_service = self.retrieval_service + self.retrieval_service = None + close_retrieval = getattr(retrieval_service, "aclose", None) + if callable(close_retrieval): + try: + result = close_retrieval() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "异步关闭检索服务失败: %s", + safe_exception_text(exc), + ) + else: + close_retrieval = getattr(retrieval_service, "close", None) + if callable(close_retrieval): + try: + result = close_retrieval() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - teardown continues after SDK failures + self.logger.warning( + "关闭检索服务失败: %s", + safe_exception_text(exc), + ) + + def apply_config_update(self, updated_config: SessionConfig | dict[str, Any], llm_needs_reload: bool) -> None: + previous_config = deepcopy(self.session_config) if llm_needs_reload else None self.chat_config = updated_config if not llm_needs_reload: return if self.reload_llm(): return - self.session_config.active_llm_configuration = previous_llm + if previous_config is not None: + self.session_config = previous_config self.console.print("[bold yellow]LLM 切换失败,已保留当前模型配置。[/bold yellow]") + async def apply_config_update_async( + self, + updated_config: SessionConfig | dict[str, Any], + llm_needs_reload: bool, + ) -> None: + """异步会话中的配置更新,避免同步 close 遗漏异步连接池。""" + previous_config = deepcopy(self.session_config) if llm_needs_reload else None + self.chat_config = updated_config + if not llm_needs_reload: + return + + previous_model = self.llm_model + new_model: LargeLanguageModel | None = None + try: + llm_key = self.session_config.active_llm_configuration + configurations = getattr(self.session_config, "llm_configurations", None) + new_model = ModelProviderFactory.get_llm_provider( + llm_key, configurations + ) if configurations is not None else ModelProviderFactory.get_llm_provider(llm_key) + if self.retrieval_service is None: + raise RuntimeError("检索服务尚未初始化。") + chat_service = ChatService(new_model, self.retrieval_service) + self.llm_model = new_model + self.chat_service = chat_service + if previous_model is not None and previous_model is not new_model: + await self._adispose_model(previous_model) + return + except Exception as exc: # noqa: BLE001 - reload boundary reports provider failures + if new_model is not None and new_model is not self.llm_model: + await self._adispose_model(new_model) + if previous_config is not None: + self.session_config = previous_config + self.console.print(f"[bold red]重载 LLM 出错: {safe_exception_text(exc)}[/bold red]") + self.console.print("[bold yellow]LLM 切换失败,已保留当前模型配置。[/bold yellow]") + async def _identify_intent_async(self, user_query: str) -> str: - if getattr(self, "chat_service", None) is None: + chat_service = getattr(self, "chat_service", None) + if chat_service is None: return user_query - return await self.chat_service.identify_intent(user_query) + return await chat_service.identify_intent(user_query) - async def _retrieve_knowledge_async(self, retrieval_query: str) -> List[Dict[str, Any]]: - if getattr(self, "retrieval_service", None) is None: + async def _retrieve_knowledge_async(self, retrieval_query: str) -> list[dict[str, Any]]: + retrieval_service = getattr(self, "retrieval_service", None) + if retrieval_service is None: return [] - return await self.retrieval_service.retrieve(retrieval_query, self.session_config, console=self.console) + return await retrieval_service.retrieve(retrieval_query, self.session_config, console=self.console) async def chat_async(self, user_input: str) -> AsyncGenerator[str, None]: - intent = await self._identify_intent_async(user_input) - retrieved_docs = await self._retrieve_knowledge_async(intent) - self.console.print(f" [bold]意图:[/bold] [yellow]{intent}[/yellow]") - self._display_retrieved_docs(retrieved_docs) - + intent = user_input + retrieved_docs: list[dict[str, Any]] = [] full_response = "" try: - if getattr(self, "chat_service", None) is not None: - async for chunk in self.chat_service.generate_reply( + intent = await self._identify_intent_async(user_input) + retrieved_docs = await self._retrieve_knowledge_async(intent) + self.console.print(f" [bold]意图:[/bold] [yellow]{intent}[/yellow]") + self._display_retrieved_docs(retrieved_docs) + + chat_service = getattr(self, "chat_service", None) + if chat_service is not None: + async for chunk in chat_service.generate_reply( user_input=user_input, intent=intent, documents=retrieved_docs, @@ -113,19 +250,21 @@ async def chat_async(self, user_input: str) -> AsyncGenerator[str, None]: ): full_response += chunk yield chunk - else: + elif (llm_model := getattr(self, "llm_model", None)) is not None: prompt = ( f"你是一个智能客服。用户问题: {user_input}\n" f"意图: {intent}\n" f"知识: {retrieved_docs}\n回答要简洁。" ) - async for chunk in self.llm_model.ainvoke( - prompt=prompt, - stream=True, - temperature=self.session_config["chat_temperature"], - ): + invoke_kwargs: dict[str, Any] = {"prompt": prompt, "stream": True} + supports_sampling = getattr(llm_model, "supports_sampling_option", None) + if callable(supports_sampling) and supports_sampling("temperature"): + invoke_kwargs["temperature"] = self.session_config["chat_temperature"] + async for chunk in llm_model.ainvoke(**invoke_kwargs): full_response += chunk yield chunk + else: + raise RuntimeError("聊天服务尚未初始化。") self.logger.info( "Query: %s | Intent: %s | Docs: %s\nResponse: %s", user_input, @@ -134,10 +273,18 @@ async def chat_async(self, user_input: str) -> AsyncGenerator[str, None]: full_response, ) except Exception as exc: - self.console.print(f"[red]LLM 异步生成出错: {exc}[/red]") - yield "抱歉,生成回复时遇到错误。" + error_text = safe_exception_text(exc) + self.logger.exception( + "聊天请求处理失败: %s", + error_text, + ) + self.console.print(f"[red]LLM 异步生成出错: {safe_exception_text(exc)}[/red]") + # 已经输出部分内容时不要追加道歉文本,避免用户收到混合响应; + # 具体异常已通过日志和控制台显式记录。 + if not full_response: + yield "抱歉,处理请求时遇到错误。" - def _display_retrieved_docs(self, docs: List[Dict[str, Any]]): + def _display_retrieved_docs(self, docs: list[dict[str, Any]]): if not docs: self.console.print("[yellow]无相关文档。[/yellow]") return @@ -156,46 +303,62 @@ def _display_retrieved_docs(self, docs: List[Dict[str, Any]]): async def start_chat_session_async(): console = Console() bot = Chatbot(console) - session = PromptSession() + try: + session = PromptSession() - if bot.llm_model: - display_chat_config(console, bot.chat_config) - console.print(f"客服已就绪 ([bold green]{bot.chat_config['active_llm_configuration']}[/bold green])") + if bot.llm_model: + display_chat_config(console, bot.chat_config) + console.print(f"客服已就绪 ([bold green]{bot.chat_config['active_llm_configuration']}[/bold green])") - while True: - try: - user_query = await session.prompt_async(HTML('你: ')) - if not user_query.strip(): - continue - if user_query.lower() == "/quit": - break + while True: + try: + user_query = await session.prompt_async(HTML('你: ')) + if not user_query.strip(): + continue + if user_query.lower() == "/quit": + break - if user_query.lower() == "/config": - editable_config = ( - bot.chat_config.to_dict() - if isinstance(bot.chat_config, SessionConfig) - else dict(bot.chat_config) - ) - llm_needs_reload, updated_config = await asyncio.to_thread( - launch_config_editor, - editable_config, - ) - bot.apply_config_update(updated_config, llm_needs_reload) - display_chat_config(console, bot.chat_config) - continue - - response_panel = Panel("", title="客服", border_style="green") - full_response = "" - with Live(response_panel, console=console, refresh_per_second=10) as live: - async for chunk in bot.chat_async(user_query): - full_response += chunk - live.update(Panel(Text(full_response), title="客服", border_style="green")) - - except (KeyboardInterrupt, EOFError): - break - console.print("[yellow]感谢使用,再见![/yellow]") - else: - console.print("[red]模型初始化失败。[/red]") + if user_query.lower() == "/config": + editable_config = ( + bot.chat_config.to_dict() + if isinstance(bot.chat_config, SessionConfig) + else dict(bot.chat_config) + ) + llm_needs_reload, updated_config = await asyncio.to_thread( + launch_config_editor, + editable_config, + ) + # 真实 Chatbot 使用异步重载以正确释放 SDK 连接池;保留 + # 同步兼容入口,便于嵌入方或旧版替身实现配置编辑。 + apply_async = getattr(bot, "apply_config_update_async", None) + if callable(apply_async): + result = apply_async(updated_config, llm_needs_reload) + if inspect.isawaitable(result): + await result + else: + apply_sync = getattr(bot, "apply_config_update", None) + if not callable(apply_sync): + raise AttributeError("Chatbot 缺少配置更新方法。") + apply_sync(updated_config, llm_needs_reload) + display_chat_config(console, bot.chat_config) + continue + + response_panel = Panel("", title="客服", border_style="green") + full_response = "" + with Live(response_panel, console=console, refresh_per_second=10) as live: + async for chunk in bot.chat_async(user_query): + full_response += chunk + live.update(Panel(Text(full_response), title="客服", border_style="green")) + + except (KeyboardInterrupt, EOFError): + break + console.print("[yellow]感谢使用,再见![/yellow]") + else: + console.print("[red]模型初始化失败。[/red]") + finally: + close = getattr(bot, "aclose", None) + if close is not None: + await close() def start_chat_session(): diff --git a/src/etl/cleaners/base.py b/src/etl/cleaners/base.py index 38e56b2..6737178 100644 --- a/src/etl/cleaners/base.py +++ b/src/etl/cleaners/base.py @@ -3,9 +3,10 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 from abc import ABC, abstractmethod -from typing import List + from src.models.document import Document + class BaseCleaner(ABC): """ 抽象基类:文档内容清洗器。 @@ -13,7 +14,7 @@ class BaseCleaner(ABC): """ @abstractmethod - def clean(self, documents: List[Document], **kwargs) -> List[Document]: + def clean(self, documents: list[Document], **kwargs) -> list[Document]: """ 对文档列表中的文本内容进行清洗。 @@ -24,4 +25,3 @@ def clean(self, documents: List[Document], **kwargs) -> List[Document]: Returns: List[Document]: 清洗后的文档对象列表。 """ - pass diff --git a/src/etl/cleaners/basic_cleaner.py b/src/etl/cleaners/basic_cleaner.py index e9ac3f3..cd168fc 100644 --- a/src/etl/cleaners/basic_cleaner.py +++ b/src/etl/cleaners/basic_cleaner.py @@ -3,17 +3,19 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 import re -from typing import List -from .base import BaseCleaner + from src.models.document import Document +from .base import BaseCleaner + + class BasicCleaner(BaseCleaner): """ 基础文本内容清洗器。 执行常见的文本清洗操作,如去除多余空格、换行符等。 """ - def clean(self, documents: List[Document], **kwargs) -> List[Document]: + def clean(self, documents: list[Document], **kwargs) -> list[Document]: """ 对文档列表中的文本内容进行基础清洗。 diff --git a/src/etl/extractors/base.py b/src/etl/extractors/base.py index 2c134f1..47d4ca7 100644 --- a/src/etl/extractors/base.py +++ b/src/etl/extractors/base.py @@ -3,9 +3,10 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 from abc import ABC, abstractmethod -from typing import List + from src.models.document import Document + class BaseExtractor(ABC): """ 抽象基类:文档内容抽取器。 @@ -13,7 +14,7 @@ class BaseExtractor(ABC): """ @abstractmethod - def extract(self, document: Document, **kwargs) -> List[Document]: + def extract(self, document: Document, **kwargs) -> list[Document]: """ 从给定的 Document 对象中抽取内容。 对于某些类型(如纯文本),可能只是原样返回。 @@ -26,4 +27,3 @@ def extract(self, document: Document, **kwargs) -> List[Document]: Returns: List[Document]: 抽取出的一个或多个文档对象列表。 """ - pass diff --git a/src/etl/extractors/markdown_extractor.py b/src/etl/extractors/markdown_extractor.py index d329f52..f41f9e3 100644 --- a/src/etl/extractors/markdown_extractor.py +++ b/src/etl/extractors/markdown_extractor.py @@ -2,10 +2,12 @@ # 原始来源: https://github.com/langgenius/dify # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 -from typing import List -from .base import BaseExtractor + from src.models.document import Document +from .base import BaseExtractor + + class MarkdownExtractor(BaseExtractor): """ Markdown 文档内容抽取器。 @@ -13,7 +15,7 @@ class MarkdownExtractor(BaseExtractor): 因为内容已在创建Document对象时从文件中读取。 """ - def extract(self, document: Document, **kwargs) -> List[Document]: + def extract(self, document: Document, **kwargs) -> list[Document]: """ 从代表源文件的 Document 对象中“抽取”内容。 对于Markdown,主要是将单个文档对象放入列表中,以符合流水线格式。 diff --git a/src/etl/pipeline.py b/src/etl/pipeline.py index 602bf78..a880d25 100644 --- a/src/etl/pipeline.py +++ b/src/etl/pipeline.py @@ -3,16 +3,15 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 import time -from typing import List, Type from pathlib import Path -from src.etl.extractors.base import BaseExtractor + from src.etl.cleaners.base import BaseCleaner -from src.etl.splitters.base import BaseSplitter -from src.etl.extractors.markdown_extractor import MarkdownExtractor from src.etl.cleaners.basic_cleaner import BasicCleaner +from src.etl.extractors.base import BaseExtractor +from src.etl.extractors.markdown_extractor import MarkdownExtractor +from src.etl.splitters.base import BaseSplitter from src.etl.splitters.recursive_text_splitter import RecursiveTextSplitter from src.models.document import Document -from src.utils.config import get_settings from src.utils.log_manager import get_module_logger logger = get_module_logger(__name__) @@ -56,7 +55,7 @@ def from_file_path(cls, file_path: Path, splitter_structure_mode: str = "standar splitter=splitter_instance ) - def process(self, document: Document) -> List[Document]: + def process(self, document: Document) -> list[Document]: """ 处理单个文档,并监控每一步的耗时 (CSE Sensor)。 """ diff --git a/src/etl/splitters/base.py b/src/etl/splitters/base.py index 4f1994f..7cf247c 100644 --- a/src/etl/splitters/base.py +++ b/src/etl/splitters/base.py @@ -3,9 +3,10 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 from abc import ABC, abstractmethod -from typing import List + from src.models.document import Document + class BaseSplitter(ABC): """ 抽象基类:文档内容分割器。 @@ -13,7 +14,7 @@ class BaseSplitter(ABC): """ @abstractmethod - def split(self, documents: List[Document], **kwargs) -> List[Document]: + def split(self, documents: list[Document], **kwargs) -> list[Document]: """ 将文档列表中的文本内容分割成更小的块。 @@ -24,4 +25,3 @@ def split(self, documents: List[Document], **kwargs) -> List[Document]: Returns: List[Document]: 分割后的文档块列表。 """ - pass diff --git a/src/etl/splitters/recursive_text_splitter.py b/src/etl/splitters/recursive_text_splitter.py index bcd88ec..1828d98 100644 --- a/src/etl/splitters/recursive_text_splitter.py +++ b/src/etl/splitters/recursive_text_splitter.py @@ -1,20 +1,17 @@ -# -*- coding: utf-8 -*- import re import uuid -from typing import Callable, List, Optional +from collections.abc import Callable import tiktoken +from langchain_text_splitters import RecursiveCharacterTextSplitter -from .base import BaseSplitter from src.models.document import Document -from langchain_text_splitters import RecursiveCharacterTextSplitter from src.utils.config import get_settings from src.utils.log_manager import get_module_logger -logger = get_module_logger(__name__) +from .base import BaseSplitter -DEFAULT_CHILD_CHUNK_SIZE = 300 -DEFAULT_CHILD_CHUNK_OVERLAP = 30 +logger = get_module_logger(__name__) class RecursiveTextSplitter(BaseSplitter): """ @@ -28,10 +25,10 @@ def __init__( mode: str = "token", encoding_name: str = "cl100k_base", structure_mode: str = "standard", - parent_chunk_size: Optional[int] = None, - parent_chunk_overlap: Optional[int] = None, - child_chunk_size: Optional[int] = None, - child_chunk_overlap: Optional[int] = None, + parent_chunk_size: int | None = None, + parent_chunk_overlap: int | None = None, + child_chunk_size: int | None = None, + child_chunk_overlap: int | None = None, ): """ 初始化 RecursiveTextSplitter。 @@ -101,9 +98,9 @@ def _strip_leading_punctuation(text: str) -> str: """去掉分片开头的句号类标点。""" return re.sub(r"^[\s.。]+", "", text).strip() - def _split_standard_documents(self, documents: List[Document]) -> List[Document]: + def _split_standard_documents(self, documents: list[Document]) -> list[Document]: """标准单层分片。""" - all_chunks: List[Document] = [] + all_chunks: list[Document] = [] for doc in documents: logger.debug(f"正在分割文档: {doc.metadata.get('source', '未知来源')}") @@ -128,9 +125,9 @@ def _split_standard_documents(self, documents: List[Document]) -> List[Document] ) return all_chunks - def split_hierarchical(self, documents: List[Document]) -> List[Document]: + def split_hierarchical(self, documents: list[Document]) -> list[Document]: """层级分片:先切父块,再切子块。""" - all_chunks: List[Document] = [] + all_chunks: list[Document] = [] child_splitter = self._build_child_splitter() self.parent_documents = {} @@ -178,7 +175,7 @@ def split_hierarchical(self, documents: List[Document]) -> List[Document]: return all_chunks - def split(self, documents: List[Document], **kwargs) -> List[Document]: + def split(self, documents: list[Document], **kwargs) -> list[Document]: """ 将文档列表中的文本内容分割成更小的块。 """ diff --git a/src/models/document.py b/src/models/document.py index a851cb7..8ff1c1f 100644 --- a/src/models/document.py +++ b/src/models/document.py @@ -2,8 +2,10 @@ # 原始来源: https://github.com/langgenius/dify # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 +from typing import Any + from pydantic import BaseModel, Field -from typing import Dict, Any + class Document(BaseModel): """ @@ -11,7 +13,7 @@ class Document(BaseModel): 用于在ETL流水线和检索过程中传递文本内容及其相关元数据。 """ content: str - metadata: Dict[str, Any] = Field(default_factory=dict) + metadata: dict[str, Any] = Field(default_factory=dict) def __str__(self): """返回文档内容的字符串表示,方便调试。""" diff --git a/src/providers/__base__/model_provider.py b/src/providers/__base__/model_provider.py index 58246ac..da631a6 100644 --- a/src/providers/__base__/model_provider.py +++ b/src/providers/__base__/model_provider.py @@ -1,56 +1,1864 @@ -# -*- coding: utf-8 -*- +import asyncio +import base64 +import binascii +import inspect +import json +import math +import struct from abc import ABC, abstractmethod -from typing import Any, AsyncGenerator, Dict, Generator, List, Tuple +from collections.abc import ( + AsyncGenerator, + AsyncIterable, + AsyncIterator, + Awaitable, + Callable, + Generator, + Iterable, + Mapping, + Sequence, +) +from contextlib import asynccontextmanager, contextmanager +from dataclasses import dataclass, replace +from dataclasses import field as dataclass_field +from functools import wraps +from typing import Any, TypeVar + +from tenacity import ( + AsyncRetrying, + Retrying, + retry_if_exception, + stop_after_attempt, + wait_exponential, +) + +from src.utils.security import ( + validate_secret_free_options as _validate_secret_free_options, +) +from src.utils.security import ( + validate_secret_free_payload as _validate_secret_free_payload, +) +from src.utils.security import ( + validate_secret_free_request_overrides as _validate_secret_free_request_overrides, +) +from src.utils.security import ( + validate_secret_free_resource_args as _validate_secret_free_resource_args, +) +from src.utils.security import ( + validate_secret_free_resource_kwargs as _validate_secret_free_resource_kwargs, +) + +_UNSET_TEMPERATURE = object() + + +@dataclass +class CompletionRequest: + """统一聊天请求。 + + `prompt` 和 `system_prompt` 保留旧版便捷调用;需要多轮或多模态输入时 + 使用 `messages`。供应商专属参数放在 `extra_body` 或 Provider 配置的 + `options` 中,适配器会在请求边界进行校验和转换。未显式传入 + `system_prompt` 时沿用兼容默认值 `You are a helpful assistant.`; + 如需完全不带系统提示,请显式传入 `None`。 + """ + + prompt: str | None = None + system_prompt: str | None = "You are a helpful assistant." + messages: Sequence[Mapping[str, Any]] | None = None + tools: list[dict[str, Any]] | None = None + stream: bool = True + # Keep the historical public default while distinguishing an omitted + # value from an explicit ``temperature=0.7`` for model-level defaults. + temperature: float | None = _UNSET_TEMPERATURE # type: ignore[assignment] + _temperature_explicit: bool = dataclass_field( + init=False, default=False, repr=False, compare=False + ) + max_tokens: int | None = None + top_p: float | None = None + top_k: int | None = None + frequency_penalty: float | None = None + presence_penalty: float | None = None + repetition_penalty: float | None = None + n: int | None = None + logit_bias: dict[str, int] | None = None + logprobs: bool | None = None + top_logprobs: int | None = None + modalities: list[str] | None = None + audio: dict[str, Any] | None = None + prediction: dict[str, Any] | None = None + web_search_options: dict[str, Any] | None = None + seed: int | None = None + stop: str | Sequence[str] | None = None + response_format: dict[str, Any] | None = None + tool_choice: Any | None = None + extra_body: dict[str, Any] | None = None + extra_headers: dict[str, str] | None = None + extra_query: dict[str, Any] | None = None + metadata: dict[str, Any] | None = None + user: str | None = None + timeout: float | None = None + # OpenAI Responses/Ark Responses 的会话与推理参数。其它 Provider 会在 + # 请求边界忽略不适用字段,但不会把它们误传给各自的 SDK。 + previous_response_id: str | None = None + # OpenAI Responses input token count 的模型风格预设。当前 SDK 仅在 + # `responses.input_tokens.count` 支持该字段,普通 Responses 创建会显式拒绝。 + personality: str | None = None + include: Sequence[str] | None = None + reasoning: dict[str, Any] | None = None + thinking: dict[str, Any] | None = None + store: bool | None = None + background: bool | None = None + parallel_tool_calls: bool | None = None + max_tool_calls: int | None = None + conversation: str | dict[str, Any] | None = None + session: dict[str, Any] | None = None + context_management: dict[str, Any] | None = None + caching: dict[str, Any] | None = None + expire_at: int | None = None + prompt_cache_options: dict[str, Any] | None = None + safety_identifier: str | None = None + moderation: dict[str, Any] | None = None + verbosity: str | None = None + prompt_cache_key: str | None = None + prompt_cache_retention: str | None = None + service_tier: str | None = None + truncation: str | None = None + stream_options: dict[str, Any] | None = None + # Anthropic Messages/Beta Messages 的资源级请求字段。它们保留在统一 + # 请求对象中,只有对应 Provider 会转发到 SDK。 + cache_control: dict[str, Any] | None = None + container: str | dict[str, Any] | None = None + inference_geo: str | None = None + mcp_servers: Sequence[Mapping[str, Any]] | None = None + output_config: dict[str, Any] | None = None + output_format: dict[str, Any] | None = None + speed: str | None = None + betas: Sequence[str] | None = None + diagnostics: dict[str, Any] | None = None + fallback_credit_token: str | None = None + fallbacks: Sequence[Mapping[str, Any]] | None = None + + def __post_init__(self) -> None: + """在统一请求边界校验并复制 HTTP 扩展,避免凭证绕过 Provider。""" + + self._temperature_explicit = self.temperature is not _UNSET_TEMPERATURE + if not self._temperature_explicit: + self.temperature = 0.7 + + headers, query = validate_secret_free_request_overrides( + self.extra_headers, + self.extra_query, + "CompletionRequest", + ) + self.extra_headers = headers # type: ignore[assignment] + self.extra_query = query + if self.extra_body is not None: + self.extra_body = _validate_secret_free_payload( + self.extra_body, + "CompletionRequest", + "extra_body", + ) + + @property + def temperature_is_explicit(self) -> bool: + """返回调用方是否显式提供了温度参数。""" + return self._temperature_explicit + + def copy_with(self, **changes: Any) -> "CompletionRequest": + """复制请求并保留隐式温度的显式性标记。 + + ``dataclasses.replace`` 会把已经规范化的 ``0.7`` 再当成显式值, + 从而覆盖模型级 temperature。Provider 在切换 stream 或调整其它 + 请求字段时应使用此方法,避免复制过程改变调用方语义。 + """ + if not self._temperature_explicit and "temperature" not in changes: + changes["temperature"] = _UNSET_TEMPERATURE + return replace(self, **changes) + + def to_invoke_kwargs(self) -> dict[str, Any]: + """转换为 Provider `invoke` 的关键字参数。""" + values = { + "prompt": self.prompt, + "system_prompt": self.system_prompt, + "messages": self.messages, + "tools": self.tools, + "stream": self.stream, + "temperature": self.temperature, + "max_tokens": self.max_tokens, + "top_p": self.top_p, + "top_k": self.top_k, + "frequency_penalty": self.frequency_penalty, + "presence_penalty": self.presence_penalty, + "repetition_penalty": self.repetition_penalty, + "n": self.n, + "logit_bias": self.logit_bias, + "logprobs": self.logprobs, + "top_logprobs": self.top_logprobs, + "modalities": self.modalities, + "audio": self.audio, + "prediction": self.prediction, + "web_search_options": self.web_search_options, + "seed": self.seed, + "stop": self.stop, + "response_format": self.response_format, + "tool_choice": self.tool_choice, + "extra_body": self.extra_body, + "extra_headers": self.extra_headers, + "extra_query": self.extra_query, + "metadata": self.metadata, + "user": self.user, + "timeout": self.timeout, + "previous_response_id": self.previous_response_id, + "personality": self.personality, + "include": self.include, + "reasoning": self.reasoning, + "thinking": self.thinking, + "store": self.store, + "background": self.background, + "parallel_tool_calls": self.parallel_tool_calls, + "max_tool_calls": self.max_tool_calls, + "conversation": self.conversation, + "session": self.session, + "context_management": self.context_management, + "caching": self.caching, + "expire_at": self.expire_at, + "prompt_cache_options": self.prompt_cache_options, + "safety_identifier": self.safety_identifier, + "moderation": self.moderation, + "verbosity": self.verbosity, + "prompt_cache_key": self.prompt_cache_key, + "prompt_cache_retention": self.prompt_cache_retention, + "service_tier": self.service_tier, + "truncation": self.truncation, + "stream_options": self.stream_options, + "cache_control": self.cache_control, + "container": self.container, + "inference_geo": self.inference_geo, + "mcp_servers": self.mcp_servers, + "output_config": self.output_config, + "output_format": self.output_format, + "speed": self.speed, + "betas": self.betas, + "diagnostics": self.diagnostics, + "fallback_credit_token": self.fallback_credit_token, + "fallbacks": self.fallbacks, + } + if not self._temperature_explicit: + values.pop("temperature", None) + return {key: value for key, value in values.items() if value is not None} + + +@dataclass +class CompletionResult: + """非流式或聚合后的统一聊天结果。""" + + text: str = "" + tool_calls: list[dict[str, Any]] = dataclass_field(default_factory=list) + usage: dict[str, Any] = dataclass_field(default_factory=dict) + finish_reason: str | None = None + response_id: str | None = None + refusal: str | None = None + reasoning: str = "" + raw: Any = None + + +@dataclass +class StreamEvent: + """统一流式事件。 + + `invoke`/`ainvoke` 仍只返回文本,以兼容现有聊天界面;需要工具调用、 + usage、reasoning 或完成原因时使用 `stream_events`/`astream_events`。 + """ + + type: str + text: str = "" + reasoning: str = "" + tool_call: dict[str, Any] | None = None + usage: dict[str, Any] = dataclass_field(default_factory=dict) + finish_reason: str | None = None + response_id: str | None = None + refusal: str | None = None + raw: Any = None + + +def merge_tool_call_fragment( + accumulator: dict[Any, dict[str, Any]], + tool_call: Mapping[str, Any], + fallback_index: Any = None, +) -> tuple[Any, dict[str, Any]]: + """累积 OpenAI/Anthropic 风格的流式工具调用片段。 + + 供应商通常只在第一个片段发送 id/name,后续片段只发送 index 和 + partial arguments。统一在适配层保留这些字段,避免调用方自行拼接。 + """ + index = tool_call.get("index", fallback_index) + call_id = tool_call.get("id") or tool_call.get("call_id") + if index is not None: + key: Any = ("index", index) + elif call_id: + key = ("id", call_id) + else: + key = ("position", len(accumulator)) + + merged = accumulator.setdefault( + key, + { + "index": index, + "type": tool_call.get("type", "function") or "function", + "arguments": "", + }, + ) + if index is not None: + merged["index"] = index + for name in ("id", "call_id", "type", "name"): + value = tool_call.get(name) + if value is not None and value != "": + merged[name] = value + + fragment = tool_call.get("arguments") + if isinstance(fragment, str): + if fragment: + current = merged.get("arguments", "") + merged["arguments"] = (current if isinstance(current, str) else "") + fragment + elif fragment is not None and not merged.get("arguments"): + merged["arguments"] = fragment + return key, merged + + +_CHAT_TOOL_CALL_TYPES = frozenset({"function"}) + + +def _chat_tool_call_item( + tool_call: Mapping[str, Any], + location: str, + *, + require_call_id: bool = False, + keep_responses_type: bool = False, +) -> dict[str, Any]: + """将工具调用归一化为 OpenAI Chat Completions 的嵌套结构。 + + ``CompletionResult.tool_calls`` 使用扁平的 ``{"id", "type", "name", + "arguments"}`` 形状,而 Chat Completions 要求 assistant 历史的 + ``tool_calls`` 使用 ``{"id", "type", "function": {"name", "arguments"}}``。 + 两种输入都在这里统一,调用方可以把上一轮的 ``tool_calls`` 原样回填。 + + 历史工具调用并不总是带 ``id``:Gemini 的 ``FunctionCall.id`` 是可选字段, + SDK 默认 ``None``,其 ``_contents`` 也允许无 id 的调用。因此默认只在调用方 + 确实提供 id 时保留该字段,不用本地错误打断本可发送的请求;只有确实要求 + ``id`` 的协议(Chat Completions)才传入 ``require_call_id=True`` 显式校验。 + """ + if not isinstance(tool_call, Mapping): + raise ValueError(f"{location} 必须是对象。") + normalized = dict(tool_call) + + function = normalized.get("function") + if function is not None and not isinstance(function, Mapping): + raise ValueError(f"{location}.function 必须是对象。") + function_values = function if isinstance(function, Mapping) else normalized + + name = function_values.get("name") or normalized.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"{location} 缺少函数名。") + arguments = function_values.get("arguments") + if arguments is None: + arguments = normalized.get("arguments") + if arguments is None: + # ``input`` 是 Responses ``custom_tool_call`` 与 Anthropic ``tool_use`` + # 的参数别名,回退顺序与 Responses 适配保持一致。 + arguments = normalized.get("input") + # 复用统一助手:非字符串参数按 JSON 文本发送,None 保持空串, + # 与 Responses 适配和流式工具调用使用同一套规则。 + arguments = normalize_tool_arguments(arguments) + + call_id = normalized.get("id") or normalized.get("call_id") + if not isinstance(call_id, str) or not call_id.strip(): + if require_call_id: + raise ValueError(f"{location} 缺少 id/call_id。") + call_id = None + + # Chat Completions 的 ``tool_calls[].type`` 只接受 ``function``;Responses 的 + # ``function_call``/``custom_tool_call`` 与流式片段都要映射回该取值。 + source_type = normalized.get("type") + tool_type = source_type if source_type in _CHAT_TOOL_CALL_TYPES else "function" + + item: dict[str, Any] = { + "type": tool_type, + "function": {"name": name, "arguments": arguments}, + } + if call_id is not None: + item["id"] = call_id + # Responses 需要区分 ``custom_tool_call``(其 ``input`` 是任意文本,不做 + # JSON 规范化),但 Chat Completions 只有 ``function`` 一种取值。调用方若 + # 来自 Responses 路径,就在类型被改写时记录原值,避免多轮 custom tool + # 回填被误当 function_call。默认不记录,防止该内部字段进入 Chat 请求体。 + if keep_responses_type and source_type != tool_type: + item["_responses_tool_call_type"] = source_type + # Responses 用 ``call_id`` 关联 ``function_call`` 与 ``function_call_output``, + # 而流式事件会同时给出 ``id``(输出项 ID)与 ``call_id``(调用 ID)两个不同 + # 的值。Chat 归一化只保留 ``id``,会把 ``call_id`` 丢掉,导致回填 Responses + # 时输出项配不上调用。这里在 Responses 路径上一并保留。 + if keep_responses_type: + source_call_id = normalized.get("call_id") + if isinstance(source_call_id, str) and source_call_id.strip(): + item["call_id"] = source_call_id + return item + + +def normalize_chat_messages( + messages: Sequence[Mapping[str, Any]], + location: str = "messages", + *, + require_tool_call_ids: bool = False, + keep_responses_type: bool = False, +) -> list[dict[str, Any]]: + """归一化 Chat Completions 消息,转换工具调用并保留其它字段。 + + ``keep_responses_type`` 由 Responses 路径开启,用于保留 + ``custom_tool_call`` 这类 Chat Completions 不支持的原始调用类型; + Chat 路径保持默认关闭,避免内部字段进入请求体。 + """ + normalized: list[dict[str, Any]] = [] + for index, message in enumerate(messages): + if not isinstance(message, Mapping): + raise ValueError(f"{location}[{index}] 必须是对象。") + item = dict(message) + tool_calls = item.get("tool_calls") + if tool_calls is not None: + if isinstance(tool_calls, (str, bytes, bytearray)) or not isinstance( + tool_calls, Sequence + ): + raise ValueError(f"{location}[{index}].tool_calls 必须是序列。") + item["tool_calls"] = [ + _chat_tool_call_item( + tool_call, + f"{location}[{index}].tool_calls[{call_index}]", + require_call_id=require_tool_call_ids, + keep_responses_type=keep_responses_type, + ) + for call_index, tool_call in enumerate(tool_calls) + ] + normalized.append(item) + return normalized + + +def normalize_messages( + prompt: str | None, + system_prompt: str | None, + messages: Sequence[Mapping[str, Any]] | None, + *, + require_tool_call_ids: bool = False, + keep_responses_type: bool = False, +) -> list[dict[str, Any]]: + """生成不修改调用方对象的标准消息列表。 + + ``require_tool_call_ids`` 只应由确实要求 assistant 历史工具调用带 ``id`` + 的协议(Chat Completions)开启;其它协议继续接受 Gemini 这类无 id 的历史。 + """ + if messages is not None: + if isinstance(messages, (str, bytes, bytearray)) or not isinstance(messages, Sequence): + raise ValueError("messages 必须是消息对象序列。") + normalized = normalize_chat_messages( + messages, + require_tool_call_ids=require_tool_call_ids, + keep_responses_type=keep_responses_type, + ) + if system_prompt and not any(message.get("role") == "system" for message in normalized): + normalized.insert(0, {"role": "system", "content": system_prompt}) + return normalized + + if prompt is None or not str(prompt).strip(): + raise ValueError("聊天请求必须提供 prompt 或 messages。") + result: list[dict[str, Any]] = [{"role": "user", "content": prompt}] + if system_prompt: + result.insert(0, {"role": "system", "content": system_prompt}) + return result + + +def _responses_json_text(value: Any, location: str, *, allow_empty: bool = False) -> str: + """将工具参数或输出规范为 Responses 接受的 JSON 文本。""" + if value is None: + if allow_empty: + return "" + return "{}" + if isinstance(value, str): + if not value and allow_empty: + return "" + try: + parsed = json.loads(value) + except (TypeError, ValueError) as exc: + raise ValueError(f"{location} 必须是有效 JSON。") from exc + else: + parsed = value + try: + return json.dumps(parsed, ensure_ascii=False, separators=(",", ":")) + except (TypeError, ValueError) as exc: + raise ValueError(f"{location} 必须可序列化为 JSON。") from exc + + +def _responses_has_content(value: Any) -> bool: + if value is None: + return False + if isinstance(value, str): + return bool(value) + if isinstance(value, Sequence) and not isinstance(value, (bytes, bytearray)): + return bool(value) + return True + + +def _responses_provider_variant(provider: str) -> str: + """规范化 Responses 目标 SDK 变体。""" + normalized = str(provider).strip().lower().replace("-", "_") + return "ark" if normalized in {"ark", "volcengine", "volc_engine"} else "openai" + + +# Ark 专属的 Responses 内容块类型;OpenAI 兼容端点不接受这些块。 +_ARK_ONLY_RESPONSES_ITEM_TYPES = frozenset( + {"input_audio", "audio_url", "input_video", "video_url"} +) + + +def _reject_unsupported_responses_item( + item: Mapping[str, Any], + location: str, + provider: str, +) -> None: + """拒绝经顶层 item 形态绕过 variant 守卫的供应商专属内容块。 + + ``normalize_responses_input`` 对带 ``content`` 的消息走 + ``_responses_message_content``,那里会按 provider 变体拒绝 Ark 专属块。 + 但原生 Responses item(``{"type": "input_audio", ...}``)没有 ``content``, + 会直通到请求体,因此需要在这里做等价检查,避免同一种块因书写位置不同 + 而一个被拒、一个静默发给不支持它的端点。 + """ + if _responses_provider_variant(provider) == "ark": + return + item_type = item.get("type") + if item_type in _ARK_ONLY_RESPONSES_ITEM_TYPES: + raise ValueError( + f"{location} 的 {item_type} 内容块当前不受 Responses SDK 支持。" + ) + if "image_pixel_limit" in item: + raise ValueError( + f"{location}.image_pixel_limit 不受 OpenAI Responses SDK 支持。" + ) + + +def _responses_content_part( + part: Any, + location: str, + *, + provider: str = "openai", +) -> dict[str, Any]: + """将 Chat 风格多模态内容块转换为 Responses 内容块。 + + OpenAI/Ark Responses 不接受 Chat Completions 的 ``text``、 + ``image_url`` 和 ``file`` 类型;其输入内容分别使用 + ``input_text``、``input_image`` 和 ``input_file``。Ark 还支持 + ``input_audio``、``input_video`` 与 ``image_pixel_limit``,由 + ``provider`` 参数选择对应变体。不在目标 SDK 输入联合类型中的内容块 + 统一显式失败,不能把无效请求静默发送到服务端。 + """ + if isinstance(part, str): + return {"type": "input_text", "text": part} + if not isinstance(part, Mapping): + raise ValueError(f"{location} 必须是字符串或对象。") + + source = dict(part) + part_type = source.get("type") + variant = _responses_provider_variant(provider) + if part_type in {"text", "input_text", "output_text"}: + text = source.get("text") + if not isinstance(text, str): + raise ValueError(f"{location}.text 必须是字符串。") + converted = {"type": "input_text", "text": text} + if "prompt_cache_breakpoint" in source and variant == "openai": + converted["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted + + if part_type in {"audio_url", "input_audio"}: + if variant != "ark": + raise ValueError( + f"{location} 的 {part_type} 内容块当前不受 Responses SDK 支持。" + ) + audio_value = source.get("audio_url") + if isinstance(audio_value, Mapping): + audio_url = audio_value.get("url") or audio_value.get("audio_url") + audio_file_id = source.get("file_id") or audio_value.get("file_id") + chunking_strategy = source.get( + "chunking_strategy", audio_value.get("chunking_strategy") + ) + else: + audio_url = audio_value + audio_file_id = source.get("file_id") + chunking_strategy = source.get("chunking_strategy") + if not isinstance(audio_url, str) or not audio_url.strip(): + raise ValueError(f"{location} 缺少有效 audio_url。") + if audio_file_id is not None and not isinstance(audio_file_id, str): + raise ValueError(f"{location}.file_id 必须是字符串。") + if chunking_strategy is not None and not isinstance(chunking_strategy, Mapping): + raise ValueError(f"{location}.chunking_strategy 必须是对象。") + converted_audio: dict[str, Any] = {"type": "input_audio", "audio_url": audio_url} + if audio_file_id: + converted_audio["file_id"] = audio_file_id + if chunking_strategy is not None: + converted_audio["chunking_strategy"] = dict(chunking_strategy) + return converted_audio + + if part_type in {"video_url", "input_video"}: + if variant != "ark": + raise ValueError( + f"{location} 的 {part_type} 内容块当前不受 Responses SDK 支持。" + ) + video_value = source.get("video_url") + if isinstance(video_value, Mapping): + video_url = video_value.get("url") or video_value.get("video_url") + video_file_id = source.get("file_id") or video_value.get("file_id") + fps = source.get("fps", video_value.get("fps")) + else: + video_url = video_value + video_file_id = source.get("file_id") + fps = source.get("fps") + if video_url is not None and not isinstance(video_url, str): + raise ValueError(f"{location}.video_url.url 必须是字符串。") + if video_file_id is not None and not isinstance(video_file_id, str): + raise ValueError(f"{location}.file_id 必须是字符串。") + if not video_url and not video_file_id: + raise ValueError(f"{location} 缺少 video_url 或 file_id。") + if fps is not None and ( + isinstance(fps, bool) + or not isinstance(fps, (int, float)) + or not math.isfinite(float(fps)) + ): + raise ValueError(f"{location}.fps 必须是有限数字。") + converted_video: dict[str, Any] = {"type": "input_video"} + if video_url: + converted_video["video_url"] = video_url + if video_file_id: + converted_video["file_id"] = video_file_id + if fps is not None: + converted_video["fps"] = fps + return converted_video + + if part_type == "image_url": + image_value = source.pop("image_url", None) + image_url: Any = None + image_file_id: Any = source.get("file_id") + detail: Any = source.get("detail") + image_pixel_limit: Any = source.get("image_pixel_limit") + if isinstance(image_value, Mapping): + image_url = image_value.get("url") or image_value.get("image_url") + image_file_id = image_file_id or image_value.get("file_id") + detail = detail or image_value.get("detail") + image_pixel_limit = image_pixel_limit or image_value.get("image_pixel_limit") + elif isinstance(image_value, str): + image_url = image_value + elif image_value is not None: + raise ValueError(f"{location}.image_url 必须是字符串或对象。") + fallback_url = source.pop("url", None) + image_url = image_url or fallback_url + if image_url is not None and not isinstance(image_url, str): + raise ValueError(f"{location}.image_url.url 必须是字符串。") + if image_file_id is not None and not isinstance(image_file_id, str): + raise ValueError(f"{location}.file_id 必须是字符串。") + if not image_url and not image_file_id: + raise ValueError(f"{location} 缺少 image_url 或 file_id。") + if detail is None: + detail = "auto" + allowed_details = {"auto", "low", "high"} + if variant == "openai": + allowed_details.add("original") + if not isinstance(detail, str) or detail not in allowed_details: + allowed = "、".join(sorted(allowed_details)) + raise ValueError(f"{location}.detail 必须是 {allowed}。") + converted_image: dict[str, Any] = {"type": "input_image", "detail": detail} + if image_url: + converted_image["image_url"] = image_url + if image_file_id: + converted_image["file_id"] = image_file_id + if image_pixel_limit is not None: + if variant != "ark": + raise ValueError(f"{location}.image_pixel_limit 不受 OpenAI Responses SDK 支持。") + if not isinstance(image_pixel_limit, Mapping): + raise ValueError(f"{location}.image_pixel_limit 必须是对象。") + converted_image["image_pixel_limit"] = dict(image_pixel_limit) + if "prompt_cache_breakpoint" in source and variant == "openai": + converted_image["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted_image + + if part_type == "input_image": + image_value = source.get("image_url") + if isinstance(image_value, Mapping): + image_url = image_value.get("url") or image_value.get("image_url") + file_id = source.get("file_id") or image_value.get("file_id") + if source.get("detail") is None and image_value.get("detail") is not None: + source["detail"] = image_value["detail"] + if ( + source.get("image_pixel_limit") is None + and image_value.get("image_pixel_limit") is not None + ): + source["image_pixel_limit"] = image_value["image_pixel_limit"] + if image_url is not None: + source["image_url"] = image_url + else: + source.pop("image_url", None) + if file_id is not None: + source["file_id"] = file_id + if source.get("image_url") is not None and not isinstance(source["image_url"], str): + raise ValueError(f"{location}.image_url 必须是字符串。") + if source.get("file_id") is not None and not isinstance(source["file_id"], str): + raise ValueError(f"{location}.file_id 必须是字符串。") + if not source.get("image_url") and not source.get("file_id"): + raise ValueError(f"{location} 缺少 image_url 或 file_id。") + if source.get("detail") is None: + source["detail"] = "auto" + allowed_details = {"auto", "low", "high"} + if variant == "openai": + allowed_details.add("original") + if source["detail"] not in allowed_details: + allowed = "、".join(sorted(allowed_details)) + raise ValueError(f"{location}.detail 必须是 {allowed}。") + converted_input_image: dict[str, Any] = {"type": "input_image", "detail": source["detail"]} + if source.get("image_pixel_limit") is not None: + if variant != "ark": + raise ValueError(f"{location}.image_pixel_limit 不受 OpenAI Responses SDK 支持。") + if not isinstance(source["image_pixel_limit"], Mapping): + raise ValueError(f"{location}.image_pixel_limit 必须是对象。") + converted_input_image["image_pixel_limit"] = dict(source["image_pixel_limit"]) + for key in ("image_url", "file_id"): + if source.get(key) is not None: + converted_input_image[key] = source[key] + if "prompt_cache_breakpoint" in source and variant == "openai": + converted_input_image["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted_input_image + + if part_type == "file": + nested_file = source.pop("file", None) + nested: dict[str, Any] = {} + if isinstance(nested_file, Mapping): + nested.update(nested_file) + elif isinstance(nested_file, str): + if nested_file.startswith(("http://", "https://")): + nested["file_url"] = nested_file + else: + nested["file_id"] = nested_file + elif nested_file is not None: + raise ValueError(f"{location}.file 必须是字符串或对象。") + for key in ("file_id", "file_data", "file_url", "filename", "detail"): + if key not in source and key in nested: + source[key] = nested[key] + if "file_url" not in source and "url" in nested: + source["file_url"] = nested["url"] + if "file_url" not in source and isinstance(source.get("url"), str): + source["file_url"] = source["url"] + if not any(source.get(key) for key in ("file_id", "file_data", "file_url")): + raise ValueError( + f"{location} 缺少 file_id、file_data 或 file_url。" + ) + for key in ("file_id", "file_data", "file_url", "filename"): + if source.get(key) is not None and not isinstance(source[key], str): + raise ValueError(f"{location}.{key} 必须是字符串。") + converted = {"type": "input_file"} + if source.get("detail") is not None: + if variant != "openai" or source["detail"] not in {"auto", "low", "high"}: + raise ValueError(f"{location}.detail 不受 {variant.title()} Responses SDK 支持。") + converted["detail"] = source["detail"] + for key in ("file_id", "file_data", "file_url", "filename"): + if source.get(key) is not None: + converted[key] = source[key] + if "prompt_cache_breakpoint" in source and variant == "openai": + converted["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted + + if part_type == "input_file": + for key in ("file_id", "file_data", "file_url", "filename"): + if source.get(key) is not None and not isinstance(source[key], str): + raise ValueError(f"{location}.{key} 必须是字符串。") + if not any(source.get(key) for key in ("file_id", "file_data", "file_url")): + raise ValueError( + f"{location} 缺少 file_id、file_data 或 file_url。" + ) + converted = {"type": "input_file"} + if source.get("detail") is not None: + if variant != "openai" or source["detail"] not in {"auto", "low", "high"}: + raise ValueError(f"{location}.detail 不受 {variant.title()} Responses SDK 支持。") + converted["detail"] = source["detail"] + for key in ("file_id", "file_data", "file_url", "filename"): + if source.get(key) is not None: + converted[key] = source[key] + if "prompt_cache_breakpoint" in source and variant == "openai": + converted["prompt_cache_breakpoint"] = source["prompt_cache_breakpoint"] + elif "prompt_cache_breakpoint" in source: + raise ValueError(f"{location}.prompt_cache_breakpoint 不受 Ark Responses SDK 支持。") + return converted + + if not isinstance(part_type, str) or not part_type.strip(): + raise ValueError(f"{location}.type 必须是非空字符串。") + raise ValueError( + f"{location} 的内容类型 {part_type} 不受 Responses SDK 支持。" + ) + + +def _responses_message_content( + value: Any, + location: str, + *, + provider: str = "openai", +) -> Any: + """复制并归一化 Responses 消息的 content 字段。""" + if value is None or isinstance(value, str): + return value + if isinstance(value, (bytes, bytearray)) or not isinstance(value, Sequence): + raise ValueError(f"{location}.content 必须是字符串或内容块序列。") + return [ + _responses_content_part( + part, + f"{location}.content[{index}]", + provider=provider, + ) + for index, part in enumerate(value) + ] + + +def _responses_function_call_item( + tool_call: Mapping[str, Any], + location: str, + *, + require_call_id: bool = False, +) -> dict[str, Any]: + """将 Chat Completions 工具调用转换为 Responses function_call item。""" + explicit_call_id = tool_call.get("call_id") + call_id = explicit_call_id + if call_id is None and not require_call_id: + call_id = tool_call.get("id") + if not isinstance(call_id, str) or not call_id.strip(): + raise ValueError(f"{location} 缺少 call_id/id。") + + function = tool_call.get("function") + if function is not None and not isinstance(function, Mapping): + raise ValueError(f"{location}.function 必须是对象。") + function_values = function if isinstance(function, Mapping) else tool_call + name = function_values.get("name") or tool_call.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"{location} 缺少函数名。") + + raw_arguments = function_values.get( + "arguments", + tool_call.get("arguments", tool_call.get("input")), + ) + # Responses 的 custom tool 按 SDK 契约接受任意文本(``input: str``,无 JSON + # 约束),Provider 自己产出的 ``custom_tool_call`` 也带任意文本。若把它当 + # function_call 强制 json.loads,多轮 custom tool 回填会在请求边界直接失败。 + # 因此按类型分流:custom 保留原始文本,function 才做 JSON 规范化。 + # + # 优先取 _chat_tool_call_item 保留的原始类型:``type`` 已被归一化为 Chat + # Completions 唯一允许的 ``function``,直接读它会丢掉 custom 语义。 + source_type = tool_call.get("_responses_tool_call_type") or tool_call.get("type") + if source_type == "custom_tool_call": + custom_input = "" if raw_arguments is None else str(raw_arguments) + item: dict[str, Any] = { + "type": "custom_tool_call", + "call_id": call_id, + "name": name, + "input": custom_input, + } + else: + item = { + "type": "function_call", + "call_id": call_id, + "name": name, + "arguments": _responses_json_text(raw_arguments, f"{location}.arguments"), + } + # A Chat Completions ``id`` is the call identifier, not the Responses + # output-item identifier. Do not duplicate it as ``id`` unless the caller + # supplied a distinct Responses-style ``call_id`` explicitly. + if explicit_call_id is not None and tool_call.get("id") is not None: + item["id"] = tool_call["id"] + if tool_call.get("status") is not None: + item["status"] = tool_call["status"] + return item + + +def _responses_function_output_item( + message: Mapping[str, Any], + location: str, + *, + require_call_id: bool = False, + provider: str = "openai", +) -> dict[str, Any]: + call_id = message.get("call_id") + if call_id is None and not require_call_id: + call_id = message.get("tool_call_id") + if not isinstance(call_id, str) or not call_id.strip(): + raise ValueError(f"{location} 缺少 tool_call_id/call_id。") + output = message.get("output", message.get("content")) + if output is None: + output_text = "" + elif isinstance(output, str): + output_text = output + elif isinstance(output, Sequence) and not isinstance(output, (bytes, bytearray)): + return { + "type": "function_call_output", + "call_id": call_id, + "output": [ + _responses_content_part( + part, + f"{location}.output[{index}]", + provider=provider, + ) + for index, part in enumerate(output) + ], + **( + {"status": message["status"]} + if "status" in message and message["status"] is not None + else {} + ), + } + else: + output_text = _responses_json_text(output, f"{location}.output") + item: dict[str, Any] = { + "type": "function_call_output", + "call_id": call_id, + "output": output_text, + } + if "status" in message and message["status"] is not None: + item["status"] = message["status"] + return item + + +def normalize_responses_input( + prompt: str | None, + system_prompt: str | None, + messages: Sequence[Mapping[str, Any]] | None, + *, + provider: str = "openai", +) -> str | list[dict[str, Any]]: + """将统一消息转换为 OpenAI/Ark Responses 输入项。 + + Chat Completions 的 assistant.tool_calls 与 role=tool 不是 Responses + 输入消息类型,必须分别变成 function_call 和 function_call_output。 + 原生 Responses item 会保留,但其关键字段仍在边界处校验。``provider`` + 为 ``ark``/``volcengine`` 时启用 Ark 独有的音视频内容块。 + + Responses 输入项要求每个 ``function_call`` 带 ``call_id``,因此这里强制 + assistant 历史的工具调用提供 id;缺少关联 ID 会在请求前显式报错。 + """ + normalized = normalize_messages( + prompt, + system_prompt, + messages, + require_tool_call_ids=True, + # Responses 需要区分 custom_tool_call 与 function_call:前者的 input + # 是任意文本,不做 JSON 规范化。Chat 路径不开启该开关。 + keep_responses_type=True, + ) + if messages is None and prompt is not None: + return prompt + + converted: list[dict[str, Any]] = [] + for index, message in enumerate(normalized): + location = f"Responses messages[{index}]" + message_type = message.get("type") + if message_type == "function_call": + converted.append( + _responses_function_call_item( + message, + location, + require_call_id=True, + ) + ) + continue + if message_type == "function_call_output": + converted.append( + _responses_function_output_item( + message, + location, + require_call_id=True, + provider=provider, + ) + ) + continue + + role = message.get("role") + tool_calls = message.get("tool_calls") + if role == "assistant" and tool_calls is not None: + if ( + isinstance(tool_calls, (str, bytes, bytearray)) + or not isinstance(tool_calls, Sequence) + ): + raise ValueError(f"{location}.tool_calls 必须是工具调用序列。") + content = message.get("content") + if _responses_has_content(content): + assistant_message: dict[str, Any] = { + "role": "assistant", + "content": _responses_message_content( + content, + location, + provider=provider, + ), + } + for key in ("type", "status", "phase"): + if key in message: + assistant_message[key] = message[key] + converted.append(assistant_message) + for call_index, tool_call in enumerate(tool_calls): + if not isinstance(tool_call, Mapping): + raise ValueError( + f"{location}.tool_calls[{call_index}] 必须是对象。" + ) + converted.append( + _responses_function_call_item( + tool_call, + f"{location}.tool_calls[{call_index}]", + ) + ) + continue + if role == "tool": + converted.append( + _responses_function_output_item( + message, + location, + provider=provider, + ) + ) + continue + if role == "function": + raise ValueError(f"{location} 的 function 消息缺少 tool_call_id。") + + normalized_message = dict(message) + if "content" in normalized_message: + normalized_message["content"] = _responses_message_content( + normalized_message["content"], + location, + provider=provider, + ) + else: + # 原生 Responses item 形态(例如 ``{"type": "input_audio", ...}``) + # 不带 role/content,因此不会经过 _responses_message_content 的 + # 内容块转换。同一个 Ark 专属块写成 content 会被拒、写成顶层 item + # 却静默发出,等于绕过了 variant 守卫,因此这里补一次等价检查。 + _reject_unsupported_responses_item(normalized_message, location, provider) + converted.append(normalized_message) + return converted + + + +def normalize_usage(usage: Any) -> dict[str, Any]: + """将常见供应商 usage 结构规范为统一 token 字段。""" + if usage is None: + return {} + def usage_key(prefix: str) -> str: + return f"{prefix}_tokens" + + aliases = { + "prompt_token_count": usage_key("input"), + "candidates_token_count": usage_key("output"), + "total_token_count": usage_key("total"), + "input_token_count": usage_key("input"), + "output_token_count": usage_key("output"), + "cached_content_token_count": usage_key("cached"), + "thoughts_token_count": usage_key("reasoning"), + "tool_use_prompt_token_count": usage_key("tool_use_prompt"), + } + result: dict[str, Any] = {} + for source, target in aliases.items(): + value = field(usage, source) + if value is not None: + result[target] = value + for key in ( + "prompt_tokens", "completion_tokens", "total_tokens", "input_tokens", + "output_tokens", "cached_tokens", "reasoning_tokens", + "tool_use_prompt_tokens", + "cache_creation_input_tokens", "cache_read_input_tokens", + ): + value = field(usage, key) + if value is not None: + result[key] = value + input_details = field(usage, "input_tokens_details") + output_details = field(usage, "output_tokens_details") + if (value := field(input_details, "cached_tokens")) is not None: + result.setdefault("cached_tokens", value) + if (value := field(output_details, "reasoning_tokens")) is not None: + result.setdefault("reasoning_tokens", value) + return result + + +def extract_system_prompt(messages: Sequence[Mapping[str, Any]] | None) -> str | None: + """提取消息列表中的 system 内容,供无 system role 的 SDK 使用。""" + if not messages: + return None + values = [content_to_text(message.get("content", "")) for message in messages if message.get("role") == "system"] + value = "\n\n".join(item for item in values if item) + return value or None + + +def content_to_text(content: Any) -> str: + """从常见 SDK 内容块中提取文本,保留未知块的显式空结果。""" + if isinstance(content, str): + return content + if isinstance(content, Mapping): + text = content.get("text") + return text if isinstance(text, str) else "" + if isinstance(content, Sequence) and not isinstance(content, (bytes, bytearray)): + return "".join(content_to_text(item) for item in content) + return "" + + +def normalize_tool_arguments(value: Any) -> str: + """将 Responses 工具参数统一为 JSON 字符串。""" + if value is None: + return "" + if isinstance(value, str): + return value + try: + return json.dumps(value, ensure_ascii=False, separators=(",", ":")) + except (TypeError, ValueError) as exc: + raise ValueError("工具调用 arguments 必须可序列化为 JSON。") from exc + + +def validate_complete_tool_call(tool_call: Mapping[str, Any], provider: str) -> dict[str, Any]: + """校验流式工具调用在发送 completed 事件前已经完整。""" + name = tool_call.get("name") + if not isinstance(name, str) or not name.strip(): + raise RuntimeError(f"{provider} 工具调用缺少 name,流提前结束。") + tool_type = tool_call.get("type", "function_call") + arguments = tool_call.get("arguments") + # Responses custom tools intentionally accept arbitrary text, including an + # empty input. Function calls still require a complete JSON arguments value. + if tool_type != "custom_tool_call" and (arguments is None or arguments == ""): + raise RuntimeError(f"{provider} 工具调用 {name} 缺少完整 arguments,流提前结束。") + if tool_type == "function_call" and isinstance(arguments, str): + try: + json.loads(arguments) + except json.JSONDecodeError as exc: + raise RuntimeError( + f"{provider} 工具调用 {name} 的 arguments 不是完整 JSON。" + ) from exc + return dict(tool_call) + + +def normalize_embedding_vector(value: Any) -> list[float]: + """将 SDK 返回的 list 或 OpenAI base64 embedding 统一为浮点列表。""" + if hasattr(value, "tolist"): + value = value.tolist() + if isinstance(value, (list, tuple)): + return [float(item) for item in value] + if isinstance(value, (bytes, bytearray, memoryview)): + encoded = bytes(value) + elif isinstance(value, str): + try: + encoded = base64.b64decode(value, validate=True) + except (binascii.Error, ValueError) as exc: + raise ValueError("Embedding base64 内容无效。") from exc + else: + raise TypeError(f"Embedding 返回了不支持的类型: {type(value).__name__}") + + if not encoded or len(encoded) % 4: + raise ValueError("Embedding base64 内容不是有效的 float32 字节序列。") + count = len(encoded) // 4 + return list(struct.unpack(f"<{count}f", encoded)) + + +def field(value: Any, name: str, default: Any = None) -> Any: + """兼容 Pydantic SDK 对象和字典响应。""" + if isinstance(value, Mapping): + return value.get(name, default) + return getattr(value, name, default) + + +def is_retryable_error(error: BaseException) -> bool: + """只对连接、限流和服务端错误重试,避免参数/认证错误重复发送请求。""" + if isinstance(error, (TimeoutError, ConnectionError)): + return True + status_values = ( + field(error, "status_code"), + field(error, "code"), + field(field(error, "response"), "status_code"), + ) + for status in status_values: + if isinstance(status, bool): + continue + if isinstance(status, int): + return status == 429 or status >= 500 + if isinstance(status, str) and status.isdigit(): + numeric_status = int(status) + return numeric_status == 429 or numeric_status >= 500 + name = error.__class__.__name__.lower() + return any(token in name for token in ("timeout", "connection", "ratelimit", "internalserver", "serviceunavailable")) + + +def reject_unsupported_kwargs(provider: str, values: Mapping[str, Any]) -> None: + """拒绝适配器未实现的非空请求字段,避免参数被静默丢弃。""" + unsupported = sorted(key for key, value in values.items() if value is not None) + if unsupported: + raise ValueError(f"{provider} 不支持请求参数: {', '.join(unsupported)}") + + +def validate_secret_free_options( + options: Mapping[str, Any] | None, + provider: str, +) -> dict[str, Any]: + """校验直接构造 Provider 时的模型级选项不会携带凭证。""" + return _validate_secret_free_options(options, provider) + + +def validate_secret_free_request_overrides( + extra_headers: Mapping[str, Any] | None = None, + extra_query: Mapping[str, Any] | None = None, + provider: str = "Provider", +) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: + """校验并复制请求级 HTTP 扩展。""" + + return _validate_secret_free_request_overrides( + extra_headers, + extra_query, + provider, + ) + + +def coerce_completion_request( + request: CompletionRequest | None, + kwargs: Mapping[str, Any], + operation: str = "请求", +) -> CompletionRequest: + """规范化统一请求,并拒绝混用对象参数和关键字参数。 + + 两种调用形式分别用于不同的调用场景。混用时如果静默丢弃关键字参数, + 调用方看到的请求和实际发送的请求会不一致,因此在进入 SDK 前显式失败。 + """ + if request is not None and kwargs: + raise ValueError(f"{operation} 不能同时传 request 和关键字参数。") + if request is not None: + return request + return CompletionRequest(**dict(kwargs)) + + +async def iterate_async(value: Any) -> AsyncGenerator[Any, None]: + """统一消费 SDK 的异步流和同步迭代器返回值。 + + 官方异步客户端通常返回 ``AsyncIterable``,但某些 SDK 版本、兼容网关 + 或测试替身会直接返回普通迭代器。两者都应保持同一异步 Provider 契约。 + """ + try: + if isinstance(value, AsyncIterable) or hasattr(value, "__aiter__"): + async for item in value: + yield item + return + if isinstance(value, Iterable): + for item in value: + yield item + return + raise TypeError("SDK 流式响应既不是异步迭代器也不是同步迭代器。") + finally: + close = getattr(value, "aclose", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + else: + close = getattr(value, "close", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + + +T = TypeVar("T") +_STREAM_END = object() + + +class StreamEventError(RuntimeError): + """SSE 错误事件,保留 HTTP 状态供首帧安全重试判断。""" + + def __init__(self, message: str, status_code: int | None = None): + super().__init__(message) + self.status_code = status_code + + +def raise_for_stream_error_event(event: Any, provider: str) -> None: + """将协议层 error 事件转换为带状态码的异常。""" + event_type = field(event, "type", "") + embedded_error = field(event, "error") + if event_type not in {"error", "response.error"} and embedded_error is None: + return + error = embedded_error or event + pending: list[Any] = [error] + visited: set[int] = set() + message: str | None = None + while pending and not message: + current = pending.pop(0) + if current is None or id(current) in visited: + continue + visited.add(id(current)) + if isinstance(current, str) and current.strip(): + message = current.strip() + break + if isinstance(current, Mapping): + values = [current.get(key) for key in ("message", "detail", "reason", "error", "body")] + else: + values = [field(current, key) for key in ("message", "detail", "reason", "error", "body")] + for value in values: + if isinstance(value, str) and value.strip(): + message = value.strip() + break + if not message: + pending.extend(value for value in values if value is not None) + if not message: + message = field(error, "code") or f"{provider} 流返回错误" + status: int | None = None + for value in ( + field(error, "status_code"), + field(error, "status"), + field(event, "status_code"), + field(event, "status"), + ): + if isinstance(value, bool): + continue + if isinstance(value, int): + status = value + break + if isinstance(value, str) and value.isdigit(): + status = int(value) + break + raise StreamEventError(f"{provider} 流返回错误: {message}", status) + + +def retry_sync_call(call: Callable[[], T]) -> T: + """在生成器消费阶段重试同步 SDK 请求建立。""" + for attempt in Retrying( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=2, max=10), + retry=retry_if_exception(is_retryable_error), + reraise=True, + ): + with attempt: + return call() + raise RuntimeError("同步 SDK 请求重试未返回结果。") + + +async def retry_async_call(call: Callable[[], Any]) -> Any: + """在生成器消费阶段重试异步 SDK 请求建立。""" + async for attempt in AsyncRetrying( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=2, max=10), + retry=retry_if_exception(is_retryable_error), + reraise=True, + ): + with attempt: + result = call() + return await result if inspect.isawaitable(result) else result + raise RuntimeError("异步 SDK 请求重试未返回结果。") + + +def retry_sync_stream( + factory: Callable[[], Any], + first_event_validator: Callable[[Any], None] | None = None, +) -> Generator[Any, None, None]: + """只重试同步流在首个事件前的建立阶段,避免重复已输出内容。""" + def open_and_peek() -> tuple[Any, Any]: + iterator = iter(factory()) + try: + first = next(iterator) + if first is not _STREAM_END and first_event_validator is not None: + first_event_validator(first) + return iterator, first + except StopIteration: + return iterator, _STREAM_END + except BaseException: + close = getattr(iterator, "close", None) + if close is not None: + close() + raise + + iterator, first = retry_sync_call(open_and_peek) + try: + if first is not _STREAM_END: + yield first + yield from iterator + finally: + close = getattr(iterator, "close", None) + if close is not None: + close() + + +async def retry_async_stream( + factory: Callable[[], Any], + first_event_validator: Callable[[Any], None] | None = None, +) -> AsyncGenerator[Any, None]: + """只重试异步流在首个事件前的建立阶段,避免重复已输出内容。""" + async def open_and_peek() -> tuple[Any, Any]: + response = factory() + response = await response if inspect.isawaitable(response) else response + iterator = iterate_async(response).__aiter__() + try: + first = await iterator.__anext__() + if first is not _STREAM_END and first_event_validator is not None: + first_event_validator(first) + return iterator, first + except StopAsyncIteration: + return iterator, _STREAM_END + except BaseException: + close = getattr(iterator, "aclose", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + raise + + iterator, first = await retry_async_call(open_and_peek) + try: + if first is not _STREAM_END: + yield first + async for item in iterator: + yield item + finally: + close = getattr(iterator, "aclose", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + + +@contextmanager +def retry_sync_stream_context(factory: Callable[[], Any]) -> Any: + """重试同步流上下文的进入阶段,不重试已经开始的事件消费。""" + def enter() -> tuple[Any, Any]: + manager = factory() + try: + return manager, manager.__enter__() + except BaseException: + close = getattr(manager, "close", None) + if close is not None: + close() + raise + + manager, response = retry_sync_call(enter) + try: + yield response + except BaseException as error: + if not manager.__exit__(type(error), error, error.__traceback__): + raise + else: + manager.__exit__(None, None, None) + + +@asynccontextmanager +async def retry_async_stream_context(factory: Callable[[], Any]) -> Any: + """重试异步流上下文的进入阶段,不重试已经开始的事件消费。""" + async def enter() -> tuple[Any, Any]: + manager = factory() + try: + return manager, await manager.__aenter__() + except BaseException: + close = getattr(manager, "aclose", None) + if close is not None: + result = close() + if inspect.isawaitable(result): + await result + raise + + manager, response = await retry_async_call(enter) + try: + yield response + except BaseException as error: + if not await manager.__aexit__(type(error), error, error.__traceback__): + raise + else: + await manager.__aexit__(None, None, None) + + +_RESOURCE_GUARD_EXCLUDED = frozenset( + { + # These entry points already coerce and validate a CompletionRequest; + # wrapping them would validate the same values twice and would make + # their streaming generators harder to introspect. + "complete", + "acomplete", + "invoke", + "ainvoke", + "stream_events", + "astream_events", + "embed_documents", + "aembed_documents", + "embed_query", + "aembed_query", + } +) + + +def _provider_label(provider: Any) -> str: + return str(getattr(provider, "_provider", None) or type(provider).__name__) + + +def _provider_call_accepts_arguments( + method: Callable[..., Any], + provider: Any, + args: tuple[Any, ...], + kwargs: Mapping[str, Any], +) -> bool: + """判断 Provider 自身的调用签名是否完整。""" + + try: + inspect.signature(method).bind(provider, *args, **dict(kwargs)) + except (TypeError, ValueError): + return False + return True + + +def _is_missing_required_argument_error(error: TypeError) -> bool: + message = str(error).lower() + return "missing" in message and "required" in message and "argument" in message + + +def _is_duplicate_argument_error(error: TypeError) -> bool: + """识别 SDK 调用中由显式参数与 ``**kwargs`` 重复造成的 TypeError。""" + + message = str(error).lower() + return "multiple values for" in message and ( + "argument" in message or "keyword" in message + ) + + +def _sanitize_resource_call( + method: Callable[..., Any], + provider: Any, + args: tuple[Any, ...], + kwargs: Mapping[str, Any], +) -> tuple[tuple[Any, ...], dict[str, Any]]: + """校验资源方法的显式扩展参数和 ``**kwargs``。""" + + label = f"{_provider_label(provider)} 资源" + copied_args = _validate_secret_free_resource_args(args, label) + copied_kwargs = dict(kwargs) + try: + signature = inspect.signature(method) + bound = signature.bind_partial(provider, *copied_args, **copied_kwargs) + except (TypeError, ValueError): + # Dynamic SDK wrappers can expose no inspectable signature. The + # keyword and positional paths are still safe and preserve the + # original call shape. + return copied_args, _validate_secret_free_resource_kwargs(copied_kwargs, label) + + extension_names = {"extra_headers", "extra_query", "extra_body"} + explicit = { + name: bound.arguments[name] + for name in extension_names + if name in bound.arguments + } + if explicit: + bound.arguments.update( + _validate_secret_free_resource_kwargs(explicit, label) + ) + for name, parameter in signature.parameters.items(): + if parameter.kind is inspect.Parameter.VAR_KEYWORD and name in bound.arguments: + bound.arguments[name] = _validate_secret_free_resource_kwargs( + bound.arguments[name], label + ) + # ``bound.args`` includes the instance we supplied for binding; the + # wrapper passes that instance separately when invoking the original + # function. + return bound.args[1:], dict(bound.kwargs) + + +def _guard_provider_resource_method(method: Callable[..., Any]) -> Callable[..., Any]: + """给 Provider 资源入口安装统一的请求扩展校验。""" + + if getattr(method, "_resource_guarded", False): + return method + if inspect.iscoroutinefunction(method): + + @wraps(method) + async def async_wrapper(provider: Any, *args: Any, **kwargs: Any) -> Any: + # 凭证校验先行:它约束参数本身,与渠道是否具备该能力无关。 + # 顺序反了会让携带凭证的调用报出能力错误,把安全问题掩盖成配置问题。 + safe_args, safe_kwargs = _sanitize_resource_call( + method, provider, args, kwargs + ) + resource_guard = getattr(provider, "_require_provider_resource", None) + if callable(resource_guard): + resource_guard(method.__name__) + provider_call_is_valid = _provider_call_accepts_arguments( + method, provider, safe_args, safe_kwargs + ) + try: + return await method(provider, *safe_args, **safe_kwargs) + except TypeError as exc: + if "unexpected keyword argument" in str(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数不匹配: {exc}" + ) from exc + if _is_duplicate_argument_error(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数重复: {exc}" + ) from exc + if provider_call_is_valid and _is_missing_required_argument_error(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数不匹配: {exc}" + ) from exc + raise + + setattr(async_wrapper, "_resource_guarded", True) # noqa: B010 + return async_wrapper + + @wraps(method) + def wrapper(provider: Any, *args: Any, **kwargs: Any) -> Any: + # 与 async_wrapper 保持同一顺序:先凭证,后能力。 + safe_args, safe_kwargs = _sanitize_resource_call( + method, provider, args, kwargs + ) + resource_guard = getattr(provider, "_require_provider_resource", None) + if callable(resource_guard): + resource_guard(method.__name__) + provider_call_is_valid = _provider_call_accepts_arguments( + method, provider, safe_args, safe_kwargs + ) + try: + return method(provider, *safe_args, **safe_kwargs) + except TypeError as exc: + if "unexpected keyword argument" in str(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数不匹配: {exc}" + ) from exc + if _is_duplicate_argument_error(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数重复: {exc}" + ) from exc + if provider_call_is_valid and _is_missing_required_argument_error(exc): + raise ValueError( + f"{_provider_label(provider)} 资源 SDK 参数不匹配: {exc}" + ) from exc + raise + + setattr(wrapper, "_resource_guarded", True) # noqa: B010 + return wrapper + + +def _is_provider_resource_method( + name: str, method: Any +) -> bool: + if name.startswith("_") or name in _RESOURCE_GUARD_EXCLUDED: + return False + if not callable(method): + return False + try: + parameters = inspect.signature(method).parameters.values() + except (TypeError, ValueError): + return False + return any( + parameter.kind is inspect.Parameter.VAR_KEYWORD + or parameter.name in {"extra_headers", "extra_query", "extra_body"} + for parameter in parameters + ) + + +def close_resource_sync(resource: Any, label: str) -> None: + """在同步生命周期中可靠释放 Provider/服务资源。""" + close = getattr(resource, "close", None) + if not callable(close): + close = getattr(resource, "aclose", None) + if not callable(close): + return + result = close() + if not inspect.isawaitable(result): + return + try: + asyncio.get_running_loop() + except RuntimeError: + asyncio.run(_await_resource_close(result)) + return + # A synchronous close invoked from an active event loop cannot block on + # the async client without risking a nested-loop error. Close a coroutine + # object where possible to avoid a RuntimeWarning, then fail explicitly. + close_coroutine = getattr(result, "close", None) + if callable(close_coroutine): + close_coroutine() + raise RuntimeError(f"{label} 需要异步关闭;请调用 aclose()。") + + +async def _await_resource_close(result: Awaitable[Any]) -> None: + """将任意可等待关闭结果包装为 asyncio.run 可接受的协程。""" + await result class LargeLanguageModel(ABC): """语言模型抽象基类""" + capabilities = frozenset({"chat", "stream"}) + + def __init_subclass__(cls, **kwargs: Any) -> None: + """为 Provider 的原生资源入口统一安装安全边界。""" + + super().__init_subclass__(**kwargs) + for name, method in list(cls.__dict__.items()): + if _is_provider_resource_method(name, method): + setattr(cls, name, _guard_provider_resource_method(method)) + + @classmethod + def supports(cls, capability: str) -> bool: + return capability in cls.capabilities + + def _require_provider_resource(self, method_name: str) -> None: + """为 Provider 原生资源入口提供可选的能力门禁。""" + return + + @property + def sdk_client(self) -> Any: + """返回底层同步 SDK 客户端,供原生资源调用。""" + getter = getattr(self, "_get_client", None) + if getter is None: + raise AttributeError(f"{type(self).__name__} 没有同步 SDK 客户端。") + return getter() + + @property + def async_sdk_client(self) -> Any: + """返回底层异步 SDK 客户端,供原生资源调用。""" + getter = getattr(self, "_get_aclient", None) + if getter is not None: + client = getter() + if client is None: + raise AttributeError(f"{type(self).__name__} 没有异步 SDK 客户端。") + return client + client_getter = getattr(self, "_get_client", None) + client = client_getter() if client_getter is not None else None + if client is None: + raise AttributeError(f"{type(self).__name__} 没有异步 SDK 客户端。") + aio = getattr(client, "aio", None) + if aio is None: + raise AttributeError(f"{type(self).__name__} 没有异步 SDK 客户端。") + return aio + @abstractmethod def invoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7, ) -> Generator[str, None, None]: """同步调用语言模型。""" - pass @abstractmethod - async def ainvoke( + def ainvoke( self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7, - ) -> AsyncGenerator[str, None]: + ) -> AsyncIterator[str]: """异步调用语言模型。""" - pass + + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """兼容层:聚合文本流;具体 Provider 可覆盖以返回 tool calls/usage。""" + request = coerce_completion_request(request, kwargs, "complete") + # 不修改调用方传入的 dataclass;Provider 的 complete 实现通常需要 + # 将流式请求改为非流式,但请求对象可能会被复用。 + request = request.copy_with(stream=False) + invoke_kwargs = request.to_invoke_kwargs() + accepted = inspect.signature(self.invoke).parameters + if not any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in accepted.values()): + invoke_kwargs = {key: value for key, value in invoke_kwargs.items() if key in accepted} + text = "".join(self.invoke(**invoke_kwargs)) + return CompletionResult(text=text) + + async def acomplete( + self, request: CompletionRequest | None = None, **kwargs: Any + ) -> CompletionResult: + """异步兼容层;具体 Provider 可覆盖以返回完整元数据。""" + request = coerce_completion_request(request, kwargs, "acomplete") + request = request.copy_with(stream=False) + chunks: list[str] = [] + invoke_kwargs = request.to_invoke_kwargs() + accepted = inspect.signature(self.ainvoke).parameters + if not any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in accepted.values()): + invoke_kwargs = {key: value for key, value in invoke_kwargs.items() if key in accepted} + async for chunk in self.ainvoke(**invoke_kwargs): + chunks.append(chunk) + return CompletionResult(text="".join(chunks)) + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + """以统一事件形式读取流;旧 Provider 默认包装文本流。""" + request = coerce_completion_request(request, kwargs, "stream_events") + request = request.copy_with(stream=True) + invoke_kwargs = request.to_invoke_kwargs() + accepted = inspect.signature(self.invoke).parameters + if not any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in accepted.values()): + invoke_kwargs = {key: value for key, value in invoke_kwargs.items() if key in accepted} + for chunk in self.invoke(**invoke_kwargs): + yield StreamEvent(type="text_delta", text=chunk) + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + """异步统一事件流;旧 Provider 默认包装文本流。""" + request = coerce_completion_request(request, kwargs, "astream_events") + request = request.copy_with(stream=True) + invoke_kwargs = request.to_invoke_kwargs() + accepted = inspect.signature(self.ainvoke).parameters + if not any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in accepted.values()): + invoke_kwargs = {key: value for key, value in invoke_kwargs.items() if key in accepted} + async for chunk in self.ainvoke(**invoke_kwargs): + yield StreamEvent(type="text_delta", text=chunk) + + def close(self) -> None: + """释放 Provider 持有的同步 SDK 客户端。""" + return + + async def aclose(self) -> None: + """释放 Provider 持有的异步 SDK 客户端。""" + return class TextEmbeddingModel(ABC): """文本向量化模型抽象基类""" @abstractmethod - def embed_documents(self, texts: List[str]) -> List[List[float]]: + def embed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """同步将文档列表向量化。""" - pass @abstractmethod - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + async def aembed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """异步将文档列表向量化。""" - pass + + def embed_query(self, text: str, **kwargs: Any) -> list[float]: + """默认的单文本查询向量化实现。""" + vectors = self.embed_documents([text], **kwargs) + if not vectors: + raise ValueError("Embedding provider 返回空结果。") + return vectors[0] + + async def aembed_query(self, text: str, **kwargs: Any) -> list[float]: + vectors = await self.aembed_documents([text], **kwargs) + if not vectors: + raise ValueError("Embedding provider 返回空结果。") + return vectors[0] + + def close(self) -> None: + """释放向量模型持有的同步资源;默认实现为空操作。""" + return + + async def aclose(self) -> None: + """释放向量模型持有的异步资源;默认实现为空操作。""" + return class RerankModel(ABC): """Rerank模型抽象基类""" @abstractmethod - def rerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + def rerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """同步对文档列表进行重排序。""" - pass @abstractmethod - async def arerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + async def arerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """异步对文档列表进行重排序。""" - pass \ No newline at end of file + + def close(self) -> None: + """释放重排模型持有的同步资源;默认实现为空操作。""" + return + + async def aclose(self) -> None: + """释放重排模型持有的异步资源;默认实现为空操作。""" + return diff --git a/src/providers/anthropic.py b/src/providers/anthropic.py index e1efddd..29c4469 100644 --- a/src/providers/anthropic.py +++ b/src/providers/anthropic.py @@ -1,13 +1,45 @@ -# -*- coding: utf-8 -*- +import base64 +import binascii +import inspect +import json +import re import time -from typing import Any, Dict, Generator, List, AsyncGenerator +from collections.abc import AsyncGenerator, Generator, Mapping +from typing import Any import anthropic -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type -from src.providers.__base__.model_provider import LargeLanguageModel +from src.providers.__base__.model_provider import ( + CompletionRequest, + CompletionResult, + LargeLanguageModel, + StreamEvent, + close_resource_sync, + coerce_completion_request, + content_to_text, + extract_system_prompt, + field, + iterate_async, + merge_tool_call_fragment, + normalize_messages, + normalize_usage, + raise_for_stream_error_event, + reject_unsupported_kwargs, + retry_async_call, + retry_async_stream_context, + retry_sync_call, + retry_sync_stream_context, + validate_secret_free_options, + validate_secret_free_request_overrides, +) +from src.providers.resources import AnthropicResources, AsyncAnthropicResources from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import ( + redact_sensitive_text, + validate_secret_free_payload, + validate_secret_free_resource_kwargs, +) logger = get_module_logger(__name__) @@ -17,8 +49,44 @@ class AnthropicProvider(LargeLanguageModel): 已增加 tenacity 重试机制与性能传感器。 """ - def __init__(self, model_name: str): + capabilities = frozenset( + { + "chat", "stream", "messages", "multimodal", "tools", "structured_output", + "usage", "token_count", "batches", "files", "models", "parse", + } + ) + + # Anthropic 同时使用两种模型命名:``claude--``(如 + # ``claude-sonnet-4-6``)与 ``claude-[-]-``(如 + # ``claude-3-5-sonnet-20240620``)。家族名不限于 opus/sonnet/haiku—— + # 5 代引入了 ``claude-fable-5``、``claude-mythos-5`` 等新家族,因此家族段 + # 按任意字母串匹配,避免新家族漏判采样控制字段的弃用契约。 + _MODEL_VERSION_RE = re.compile( + r"(?[a-z]+)[-_](?P\d+)" + r"(?:[-_.](?P\d{1,2}))?" + r"(?:[-_]\d{8})?" + r"|" + r"(?P\d+)" + r"(?:[-_.](?P\d{1,2}))?" + r"[-_](?:[a-z]+)" + r"(?:[-_]\d{8})?" + r")" + r"(?=$|[-_.])", + re.IGNORECASE, + ) + + # 无版本号的现役家族名(如 ``claude-mythos-preview``)。这些是 5 代命名, + # 官方端点同样拒绝 legacy 采样控制字段。 + _UNVERSIONED_MODERN_MODEL_RE = re.compile( + r"(? AnthropicResources: + return AnthropicResources(self) + + @property + def async_resources(self) -> AsyncAnthropicResources: + return AsyncAnthropicResources(self) + def _get_client(self) -> anthropic.Anthropic: if self._client is None: - self._client = anthropic.Anthropic(api_key=self._api_key) + validate_secret_free_options(self._options, "Anthropic") + client_options = { + key: value + for key, value in self._options.items() + if key in {"timeout", "max_retries"} + } + self._client = anthropic.Anthropic(api_key=self._api_key, **client_options) return self._client def _get_aclient(self) -> anthropic.AsyncAnthropic: if self._aclient is None: - self._aclient = anthropic.AsyncAnthropic(api_key=self._api_key) + validate_secret_free_options(self._options, "Anthropic") + client_options = { + key: value + for key, value in self._options.items() + if key in {"timeout", "max_retries"} + } + self._aclient = anthropic.AsyncAnthropic(api_key=self._api_key, **client_options) return self._aclient - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) + @staticmethod + async def _resolve_async_result(result: Any) -> Any: + """兼容异步客户端返回的协程和异步分页器。""" + if inspect.isawaitable(result): + return await result + return result + + @staticmethod + def _safe_resource_kwargs(kwargs: Mapping[str, Any], operation: str) -> dict[str, Any]: + """校验直连资源方法的请求扩展,避免绕过 Resource Facade。""" + return validate_secret_free_resource_kwargs(kwargs, f"Anthropic {operation}") + + @classmethod + def _sampling_controls_deprecated(cls, model_name: str) -> bool: + """识别 Claude 4.5+ 模型对采样控制字段的弃用契约。 + + 4.7 起官方端点对非默认值直接返回 400,且 Python SDK v1.0+ 已从请求 + 签名移除 ``temperature``/``top_p``/``top_k``,透传会抛 ``TypeError``。 + 因此识别失败会让请求直接失败,必须覆盖全部命名形式。 + """ + name = str(model_name) + match = cls._MODEL_VERSION_RE.search(name) + if match is None: + # 无版本号的家族名(如 ``claude-mythos-preview``)出现在 5 代命名 + # 中,按当前主力家族处理,避免把弃用字段透传给新模型。 + return cls._UNVERSIONED_MODERN_MODEL_RE.search(name) is not None + major = int(match.group("family_major") or match.group("version_major")) + minor_value = match.group("family_minor") or match.group("version_minor") + minor = int(minor_value) if minor_value is not None else 0 + return major > 4 or (major == 4 and minor >= 5) + + def supports_sampling_option(self, name: str) -> bool: + """供上层判断当前模型是否仍接受 temperature/top_p/top_k。""" + if name not in {"temperature", "top_p", "top_k"}: + raise ValueError(f"Anthropic 不支持的采样字段: {name}") + return ( + not self._sampling_controls_deprecated(self._model_name) + or getattr(self, "_options", {}).get("allow_deprecated_sampling") is True + ) + + def _normalize_sampling_value(self, name: str, value: Any) -> Any: + if value is None or not self._sampling_controls_deprecated(self._model_name): + return value + # Official Claude 4.5+ endpoints reject legacy sampling controls, but + # deployments behind an Anthropic-compatible gateway may intentionally + # retain them. Keep the strict default and require an explicit model + # option before forwarding those values. + if getattr(self, "_options", {}).get("allow_deprecated_sampling") is True: + return value + if name == "temperature": + try: + numeric = float(value) + except (TypeError, ValueError) as exc: + raise ValueError("Anthropic temperature 必须是数字。") from exc + if numeric in {0.7, 1.0}: + # 0.7 is the application's historical default; 1.0 is the + # backwards-compatible server value. Omitting either avoids a + # request field that modern Claude models no longer use. + return None + raise ValueError( + "当前 Claude 模型不支持该 temperature;请省略该字段或使用 1.0。" + ) + if name == "top_p": + try: + numeric = float(value) + except (TypeError, ValueError) as exc: + raise ValueError("Anthropic top_p 必须是数字。") from exc + if numeric >= 0.99: + return None + raise ValueError( + "当前 Claude 模型不支持该 top_p;仅允许省略或使用不低于 0.99 的兼容值。" + ) + raise ValueError("当前 Claude 模型不支持 top_k;请移除该字段。") + + def _default_max_tokens(self) -> int: + configured = getattr(self, "_options", {}).get("max_tokens") + if configured is not None: + if isinstance(configured, bool) or not isinstance(configured, (int, float)): + raise ValueError("Anthropic options.max_tokens 必须是正整数。") + configured_int = int(configured) + if configured_int <= 0 or configured_int != configured: + raise ValueError("Anthropic options.max_tokens 必须是正整数。") + return configured_int + model = str(getattr(self, "_model_name", "")).lower() + if "claude-3-opus" in model or "claude-3-haiku" in model: + return 4096 + if "claude-3-5" in model or "claude-3.5" in model: + return 8192 + if self._sampling_controls_deprecated(model): + return 16384 + return 8192 + + @staticmethod + def _user_profile_fields( + user: str | None, + extra_headers: Mapping[str, str] | None = None, + betas: Any = None, + *, + beta: bool, + ) -> tuple[dict[str, str] | None, Any]: + """补齐 user profile 所需的 SDK beta 资源声明。""" + if user is None: + return dict(extra_headers) if extra_headers is not None else None, betas + header = "user-profiles-2026-08-18" + if beta: + if isinstance(betas, str): + values = [betas] + else: + values = list(betas or []) + if header not in values: + values.append(header) + return dict(extra_headers) if extra_headers is not None else None, values + headers = dict(extra_headers or {}) + existing = headers.get("anthropic-beta", "") + values = [item.strip() for item in existing.split(",") if item.strip()] + if header not in values: + values.append(header) + headers["anthropic-beta"] = ",".join(values) + return headers, betas + + @staticmethod + def _convert_tools(tools: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None: + if not tools: + return None + + converted: list[dict[str, Any]] = [] + for index, tool in enumerate(tools): + if not isinstance(tool, Mapping): + raise ValueError(f"Anthropic 工具定义[{index}] 必须是对象。") + tool_type = tool.get("type") + # Anthropic 的 server_* 工具不是 function schema,原样传递其 + # 专属字段,避免统一 OpenAI schema 破坏服务端工具配置。 + if isinstance(tool_type, str) and ( + tool_type.startswith("server_") or tool_type in { + "computer_20250124", "bash_20250124", "text_editor_20250124", + } + ): + converted.append(dict(tool)) + continue + function = tool.get("function", tool) + if not isinstance(function, Mapping): + raise ValueError( + f"Anthropic 工具定义[{index}].function 必须是对象。" + ) + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"Anthropic 工具定义[{index}] 缺少 function.name。") + description = function.get("description", "") + if description is None: + description = "" + if not isinstance(description, str): + raise ValueError( + f"Anthropic 工具定义[{index}].description 必须是字符串。" + ) + input_schema = function.get( + "parameters", + function.get("input_schema", {"type": "object", "properties": {}}), + ) + if not isinstance(input_schema, Mapping): + raise ValueError( + f"Anthropic 工具定义[{index}] 的 parameters/input_schema 必须是对象。" + ) + converted.append( + { + "name": name, + "description": description, + "input_schema": dict(input_schema), + } + ) + return converted + + @staticmethod + def _convert_tool_choice(tool_choice: Any) -> Any: + if tool_choice is None: + return None + if isinstance(tool_choice, str): + normalized = tool_choice.strip().lower() + if normalized in {"auto", "any", "none"}: + return {"type": normalized} + if normalized in {"required", "force"}: + return {"type": "any"} + raise ValueError(f"Anthropic 不支持 tool_choice: {tool_choice}") + if not isinstance(tool_choice, Mapping): + raise ValueError("Anthropic tool_choice 必须是字符串或对象。") + choice = dict(tool_choice) + choice_type = choice.get("type") + if choice_type in {"auto", "any", "none"}: + return choice + if choice_type == "tool": + name = choice.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError("Anthropic tool_choice 的 tool 缺少 name。") + return choice + if choice_type == "function": + function = choice.get("function", choice) + if not isinstance(function, Mapping): + raise ValueError("Anthropic tool_choice.function 必须是对象。") + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError("Anthropic tool_choice 的 function 缺少 name。") + return {"type": "tool", "name": name} + if choice_type is None and "name" in choice: + name = choice.get("name") + if isinstance(name, str) and name.strip(): + return {"type": "tool", "name": name} + raise ValueError( + "Anthropic tool_choice 必须是 auto、any、none、tool 或 function 对象。" + ) + + @staticmethod + def _convert_content(content: Any) -> Any: + if isinstance(content, str): + return content + if not isinstance(content, list): + return content + converted = [] + for part in content: + if not isinstance(part, dict): + converted.append(part) + continue + part_type = part.get("type") + if part_type in {"text", "input_text"}: + converted.append({"type": "text", "text": part.get("text", "")}) + elif part_type in {"image_url", "input_image"}: + image_url = part.get("image_url", part.get("url")) + if isinstance(image_url, Mapping): + image_url = image_url.get("url", image_url.get("image_url")) + if not isinstance(image_url, str) or not image_url.strip(): + raise ValueError("Anthropic 图片内容缺少有效 image_url。") + image_url = image_url.strip() + if image_url.startswith("data:"): + if "," not in image_url: + raise ValueError("Anthropic 图片 data URI 缺少逗号分隔符。") + header, data = image_url.split(",", 1) + header_parts = header.split(";", 1) + media_type = header_parts[0].removeprefix("data:") + if not media_type or len(header_parts) == 1 or header_parts[1].lower() != "base64": + raise ValueError( + "Anthropic 图片 data URI 必须包含 MIME 类型和 base64 标记。" + ) + try: + base64.b64decode(data, validate=True) + except (binascii.Error, ValueError) as exc: + raise ValueError("Anthropic 图片 data URI 不是有效 Base64。") from exc + converted.append( + { + "type": "image", + "source": { + "type": "base64", + "media_type": media_type, + "data": data, + }, + } + ) + else: + converted.append( + {"type": "image", "source": {"type": "url", "url": image_url}} + ) + elif part_type == "tool_result": + converted.append(part) + else: + converted.append(part) + return converted + + @classmethod + def _convert_messages(cls, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + converted: list[dict[str, Any]] = [] + pending_tool_results: list[dict[str, Any]] = [] + + def flush_tool_results() -> None: + if pending_tool_results: + converted.append({"role": "user", "content": list(pending_tool_results)}) + pending_tool_results.clear() + + for message in messages: + role = message.get("role", "user") + content = message.get("content", "") + if role == "system": + continue + if role == "tool": + tool_call_id = message.get("tool_call_id") + if not isinstance(tool_call_id, str) or not tool_call_id.strip(): + raise ValueError( + "Anthropic 工具结果缺少 tool_call_id,无法关联对应的 tool_use。" + ) + tool_content = content + if not isinstance(tool_content, (str, list)): + tool_content = content_to_text(tool_content) + tool_result: dict[str, Any] = { + "type": "tool_result", + "tool_use_id": tool_call_id, + "content": tool_content, + } + if message.get("is_error") is not None: + tool_result["is_error"] = bool(message["is_error"]) + pending_tool_results.append(tool_result) + continue + + flush_tool_results() + if role == "assistant" and message.get("tool_calls"): + blocks: list[Any] = [] + if content: + blocks.extend(cls._convert_content(content) if isinstance(content, list) else [{"type": "text", "text": content}]) + for index, call in enumerate(message.get("tool_calls", [])): + if not isinstance(call, Mapping): + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 必须是对象。" + ) + function = call.get("function", call) + if not isinstance(function, Mapping): + raise ValueError( + f"Anthropic assistant tool_calls[{index}].function 必须是对象。" + ) + call_id = call.get("id") + name = function.get("name") + if not isinstance(call_id, str) or not call_id.strip(): + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 缺少 id。" + ) + if not isinstance(name, str) or not name.strip(): + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 缺少 function.name。" + ) + arguments = function.get("arguments", function.get("input", {})) + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError as exc: + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 的 arguments 不是有效 JSON。" + ) from exc + if not isinstance(arguments, Mapping): + raise ValueError( + f"Anthropic assistant tool_calls[{index}] 的 arguments 必须是 JSON 对象。" + ) + blocks.append({ + "type": "tool_use", + "id": call_id, + "name": name, + "input": dict(arguments), + }) + content = blocks + converted.append({"role": "assistant" if role == "assistant" else "user", "content": cls._convert_content(content)}) + flush_tool_results() + return converted + + @staticmethod + def _response_format_config(response_format: dict[str, Any] | None) -> dict[str, Any] | None: + if not response_format: + return None + if response_format.get("type") == "json_schema": + schema = response_format.get("json_schema", response_format) + return {"type": "json_schema", "schema": schema.get("schema", schema)} + if response_format.get("type") == "json_object": + return {"type": "json_schema", "schema": {"type": "object"}} + return response_format + + def _build_message_params( + self, + prompt: str | None = None, + system_prompt: str | None = None, + tools: list[dict[str, Any]] | None = None, + temperature: float | None = None, + *, + stream: bool | None = None, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + seed: int | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + user: str | None = None, + timeout: float | None = None, + cache_control: dict[str, Any] | None = None, + container: str | dict[str, Any] | None = None, + inference_geo: str | None = None, + output_config: dict[str, Any] | None = None, + output_format: dict[str, Any] | None = None, + beta: bool = False, + for_parse: bool = False, + for_tool_runner: bool = False, + context_management: dict[str, Any] | None = None, + mcp_servers: Any = None, + speed: str | None = None, + betas: Any = None, + diagnostics: dict[str, Any] | None = None, + fallback_credit_token: str | None = None, + fallbacks: Any = None, + **ignored: Any, + ) -> dict[str, Any]: + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + "Anthropic Messages", + ) + beta_values = { + "context_management": context_management, + "mcp_servers": mcp_servers, + "speed": speed, + "betas": betas, + "diagnostics": diagnostics, + "fallback_credit_token": fallback_credit_token, + "fallbacks": fallbacks, + } + if seed is not None: + raise ValueError("Anthropic Messages 不支持请求参数: seed") + if not beta: + ignored = { + **ignored, + **{key: value for key, value in beta_values.items() if value is not None}, + } + reject_unsupported_kwargs("Anthropic Messages", ignored) + normalized = normalize_messages(prompt, system_prompt, messages) + params: dict[str, Any] = { + "model": self._model_name, + "max_tokens": max_tokens if max_tokens is not None else self._default_max_tokens(), + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(messages) or system_prompt + if effective_system: + params["system"] = effective_system + converted_tools = self._convert_tools(tools) + if converted_tools: + params["tools"] = converted_tools + if stop is not None: + params["stop_sequences"] = [stop] if isinstance(stop, str) else stop + if tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(tool_choice) + if metadata is not None: + params["metadata"] = metadata + if cache_control is not None: + params["cache_control"] = cache_control + if container is not None: + params["container"] = container + if inference_geo is not None: + params["inference_geo"] = inference_geo + if thinking is not None: + params["thinking"] = thinking + if service_tier is not None: + params["service_tier"] = service_tier + if user is not None: + if for_parse and not beta: + raise ValueError( + "Anthropic Messages parse 不支持 user;请改用 beta_parse。" + ) + # The SDK translates this argument into the required header. Add + # the beta opt-in explicitly because ordinary Messages calls do + # not otherwise advertise user-profile support. + params["user_profile_id"] = user + extra_headers, configured_betas = self._user_profile_fields( + user, extra_headers, beta_values.get("betas"), beta=beta + ) + if beta: + beta_values["betas"] = configured_betas + if timeout is not None: + params["timeout"] = timeout + if extra_headers is not None: + params["extra_headers"] = extra_headers + if extra_query is not None: + params["extra_query"] = extra_query + # Client construction options must never leak into messages.create. + # Anthropic's SDK validates request kwargs locally and would otherwise + # report a misleading unknown parameter error. + # Keep this defensive filter for callers that construct a Provider + # through ``object.__new__`` or mutate an existing options mapping + # after initialization. Normal construction rejects these fields at + # the configuration boundary, but they must never reach messages.create. + client_only = { + "base_url", "timeout", "max_retries", "default_headers", + "default_query", "http_client", + } + options = { + key: value + for key, value in getattr(self, "_options", {}).items() + if key not in client_only + } + allow_deprecated_sampling = options.pop("allow_deprecated_sampling", False) + if not isinstance(allow_deprecated_sampling, bool): + raise ValueError("Anthropic options.allow_deprecated_sampling 必须是布尔值。") + configured_response_format = options.get("response_format") + if configured_response_format is not None: + if not isinstance(configured_response_format, Mapping): + raise ValueError("Anthropic options.response_format 必须是对象。") + if response_format is not None: + raise ValueError("Anthropic response_format 与配置项重复。") + response_format = dict(configured_response_format) + options.pop("response_format", None) + configured_output_format = options.pop("output_format", None) + if for_parse: + configured_output_format = None + if configured_output_format is not None: + if output_format is not None: + raise ValueError("Anthropic output_format 与配置项重复。") + output_format = configured_output_format + format_config = self._response_format_config(response_format) + if format_config: + params["output_config"] = {"format": format_config} + if output_config is not None: + params["output_config"] = output_config + if not for_parse: + if output_format is not None and stream is not True and not (beta and for_tool_runner): + raise ValueError( + "Anthropic Messages.create 不支持 output_format;请使用 parse 或 stream。" + ) + if output_format is not None and (stream is True or (beta and for_tool_runner)): + if isinstance(output_format, Mapping): + raise ValueError( + "Anthropic output_format 必须是 Python 类型;字典 schema 请放入 output_config。" + ) + params["output_format"] = output_format + if beta: + params.update({key: value for key, value in beta_values.items() if value is not None}) + configured_body = validate_secret_free_payload( + options.pop("extra_body", None), + "Anthropic Messages", + "options.extra_body", + ) + request_body = validate_secret_free_payload( + extra_body, + "Anthropic Messages", + "extra_body", + ) + overlap = sorted(set(configured_body).intersection(request_body)) + if overlap: + raise ValueError(f"Anthropic extra_body 不允许覆盖配置字段: {', '.join(overlap)}") + + sampling_options = { + key: options.pop(key, None) + for key in ("temperature", "top_p", "top_k") + } + sampling_values = { + "temperature": ( + temperature + if temperature is not None + else sampling_options["temperature"] + if sampling_options["temperature"] is not None + else None + ), + "top_p": top_p, + "top_k": top_k, + } + sampling_keys = set(sampling_values) + for key in sampling_keys: + if key in configured_body or key in request_body: + if sampling_options[key] is not None or sampling_values[key] is not None: + raise ValueError(f"Anthropic 请求字段 {key} 与 extra_body 重复。") + continue + value = sampling_values[key] + if value is None: + value = sampling_options[key] + value = self._normalize_sampling_value(key, value) + if value is not None: + request_body[key] = value + + body = {**configured_body, **request_body} + if body: + reserved = set(params).intersection(body) + if reserved: + raise ValueError(f"Anthropic extra_body 不允许覆盖请求字段: {', '.join(sorted(reserved))}") + params["extra_body"] = body + for key in ( + "model", "messages", "system", "max_tokens", "tool_choice", "tools", + ): + options.pop(key, None) + options.pop("max_tokens", None) + option_aliases = { + "stop": "stop_sequences", + "user": "user_profile_id", + } + allowed_options = { + "metadata", "cache_control", "container", "inference_geo", "thinking", + "service_tier", "extra_headers", "extra_query", "output_config", + "stop_sequences", "user_profile_id", + } + if beta: + allowed_options.update( + { + "context_management", "mcp_servers", "speed", "betas", + "diagnostics", "fallback_credit_token", "fallbacks", + } + ) + allowed_options.update(option_aliases) + reject_unsupported_kwargs( + "Anthropic Messages options", + {key: value for key, value in options.items() if key not in allowed_options}, + ) + for key, value in options.items(): + params.setdefault(option_aliases.get(key, key), value) + return params + + @classmethod + def _extract_result(cls, response: Any) -> CompletionResult: + blocks = field(response, "content", []) or [] + text = "".join(field(block, "text", "") for block in blocks if field(block, "type") == "text") + reasoning = "".join( + field(block, "thinking", "") + for block in blocks + if field(block, "type") in {"thinking", "redacted_thinking"} + ) + tool_calls = [ + { + "id": field(block, "id"), + "type": "function", + "name": field(block, "name"), + "arguments": field(block, "input", {}), + } + for block in blocks + if field(block, "type") == "tool_use" + ] + usage_dict = normalize_usage(field(response, "usage")) + refusal = next( + ( + field(block, "reason", "") or field(block, "text", "") + for block in blocks + if field(block, "type") == "refusal" + ), + None, + ) + if not refusal: + stop_reason = field(response, "stop_reason") + stop_details = field(response, "stop_details") or field(response, "refusal_stop_details") + details_type = field(stop_details, "type") + if stop_reason == "refusal" or details_type == "refusal": + explanation = field(stop_details, "explanation") + category = field(stop_details, "category") + refusal = str(explanation or category or "Anthropic 请求被安全策略拒绝") + return CompletionResult( + text=text, + tool_calls=tool_calls, + usage=usage_dict, + finish_reason=field(response, "stop_reason"), + response_id=field(response, "id"), + reasoning=reasoning, + refusal=refusal, + raw=response, + ) + + @classmethod + def _raise_for_refusal(cls, response: Any) -> None: + """将 Anthropic 的拒答/安全拦截转换为显式异常。""" + result = cls._extract_result(response) + if result.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {result.refusal}") + + @classmethod + def _raise_for_stream_snapshot_refusal(cls, response: Any) -> None: + """在文本流输出前检查 SDK 当前快照中的拒答。""" + try: + snapshot = response.current_message_snapshot + except (AttributeError, NotImplementedError): + return + cls._raise_for_refusal(snapshot) + + @classmethod + def _iter_invoke_stream_text(cls, response: Any) -> Generator[str, None, None]: + """消费消息流文本,并保留拒答事件的可观测性。 + + 官方 MessageStream 暴露 ``current_message_snapshot``,文本迭代完成后 + 从最终快照读取 stop_details;简单测试替身通常只提供 text_stream 或 + 原始事件迭代,因此分别兼容这两种形态。 + """ + stream_iter = getattr(response, "__iter__", None) + if callable(stream_iter): + saw_event = False + message_stop_seen = False + for event in response: + saw_event = True + message_stop_seen = message_stop_seen or field(event, "type") == "message_stop" + converted = cls._stream_event(event) + if converted is None: + continue + if converted.type == "refusal_delta" and converted.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {converted.refusal}") + if converted.type == "text_delta" and converted.text: + yield converted.text + if saw_event: + if not message_stop_seen: + raise RuntimeError("Anthropic Messages 流在 message_stop 之前结束。") + return + + if hasattr(type(response), "current_message_snapshot"): + cls._raise_for_stream_snapshot_refusal(response) + for text in response.text_stream: + cls._raise_for_stream_snapshot_refusal(response) + if text: + yield text + cls._raise_for_stream_snapshot_refusal(response) + return + + for text in response.text_stream: + if text: + yield text + + @classmethod + async def _aiter_invoke_stream_text(cls, response: Any) -> AsyncGenerator[str, None]: + """异步版本的消息流文本消费与拒答检查。""" + stream_iter = getattr(response, "__aiter__", None) + if callable(stream_iter): + saw_event = False + message_stop_seen = False + async for event in iterate_async(response): + saw_event = True + message_stop_seen = message_stop_seen or field(event, "type") == "message_stop" + converted = cls._stream_event(event) + if converted is None: + continue + if converted.type == "refusal_delta" and converted.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {converted.refusal}") + if converted.type == "text_delta" and converted.text: + yield converted.text + if saw_event: + if not message_stop_seen: + raise RuntimeError("Anthropic Messages 流在 message_stop 之前结束。") + return + + if hasattr(type(response), "current_message_snapshot"): + cls._raise_for_stream_snapshot_refusal(response) + async for text in iterate_async(response.text_stream): + cls._raise_for_stream_snapshot_refusal(response) + if text: + yield text + cls._raise_for_stream_snapshot_refusal(response) + return + + async for text in iterate_async(response.text_stream): + if text: + yield text + + @classmethod + def _stream_event(cls, event: Any, response_id: str | None = None) -> StreamEvent | None: + event_type = field(event, "type", "") + if event_type == "message_start": + message = field(event, "message") + message_id = field(message, "id") or response_id + usage = normalize_usage(field(message, "usage")) + return StreamEvent(type="start", usage=usage, response_id=message_id, raw=event) + if event_type == "content_block_start": + block = field(event, "content_block") + block_type = field(block, "type") + if block_type == "refusal" or field(block, "refusal"): + refusal = field(block, "refusal") or field(block, "text") or field(block, "reason") + return StreamEvent( + type="refusal_delta", + refusal=refusal if isinstance(refusal, str) else str(refusal or ""), + response_id=response_id, + raw=event, + ) + if block_type == "tool_use": + initial_input = field(block, "input", {}) + if isinstance(initial_input, Mapping): + initial_arguments = json.dumps(initial_input, ensure_ascii=False, separators=(",", ":")) if initial_input else "" + else: + initial_arguments = str(initial_input) if initial_input else "" + return StreamEvent( + type="tool_call_delta", + tool_call={ + "id": field(block, "id"), + "index": field(event, "index"), + "name": field(block, "name"), + "arguments": initial_arguments, + }, + response_id=response_id, + raw=event, + ) + if block_type in {"thinking", "redacted_thinking"}: + thinking = field(block, "thinking", "") + return StreamEvent( + type="reasoning_delta", + reasoning=thinking if isinstance(thinking, str) else "", + response_id=response_id, + raw=event, + ) + return None + if event_type == "content_block_delta": + delta = field(event, "delta") + delta_type = field(delta, "type") + if delta_type == "text_delta": + return StreamEvent(type="text_delta", text=field(delta, "text", ""), response_id=response_id, raw=event) + if delta_type == "thinking_delta": + return StreamEvent(type="reasoning_delta", reasoning=field(delta, "thinking", ""), response_id=response_id, raw=event) + if delta_type in {"refusal_delta", "refusal"}: + refusal = field(delta, "refusal") or field(delta, "text") or field(delta, "reason") + return StreamEvent( + type="refusal_delta", + refusal=refusal if isinstance(refusal, str) else str(refusal or ""), + response_id=response_id, + raw=event, + ) + if delta_type == "input_json_delta": + return StreamEvent( + type="tool_call_delta", + tool_call={ + "index": field(event, "index"), + "arguments": field(delta, "partial_json", ""), + }, + response_id=response_id, + raw=event, + ) + return None + if event_type == "message_delta": + usage = normalize_usage(field(event, "usage")) + delta = field(event, "delta") + reason = field(delta, "stop_reason") + stop_details = field(delta, "stop_details") or field(delta, "refusal_stop_details") + details_type = field(stop_details, "type") + if reason == "refusal" or details_type == "refusal": + explanation = field(stop_details, "explanation") + category = field(stop_details, "category") + refusal = str(explanation or category or "Anthropic 请求被安全策略拒绝") + return StreamEvent( + type="refusal_delta", + refusal=refusal, + response_id=response_id, + raw=event, + ) + if reason: + return StreamEvent( + type="finish", + usage=usage, + finish_reason=str(reason), + response_id=response_id, + raw=event, + ) + if usage: + return StreamEvent(type="usage", usage=usage, response_id=response_id, raw=event) + return None + if event_type == "message_stop": + return StreamEvent(type="finish", response_id=response_id, raw=event) + if event_type == "error" or event_type.endswith(".error"): + raise_for_stream_error_event(event, "Anthropic Messages") + message = cls._stream_error_message(event) + raise RuntimeError(redact_sensitive_text(message)) + return None + + @staticmethod + def _stream_error_message(event: Any) -> str: + """从 SDK 错误事件的嵌套对象中保留最具体的错误信息。""" + pending: list[Any] = [event] + visited: set[int] = set() + preferred_keys = ("message", "detail", "reason", "error", "body") + while pending: + current = pending.pop(0) + if current is None or id(current) in visited: + continue + visited.add(id(current)) + if isinstance(current, str) and current.strip(): + return current.strip() + if isinstance(current, Mapping): + for key in preferred_keys: + value = current.get(key) + if isinstance(value, str) and value.strip(): + return value.strip() + pending.extend(current.get(key) for key in preferred_keys if key in current) + continue + for key in preferred_keys: + value = field(current, key) + if isinstance(value, str) and value.strip(): + return value.strip() + if value is not None: + pending.append(value) + return "Anthropic Messages 流返回错误" + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + request = coerce_completion_request(request, kwargs, "Anthropic stream_events") + request = request.copy_with(stream=True) + params = self._build_message_params(**request.to_invoke_kwargs()) + if not request.stream: + result = self.complete(request) + if result.text: + yield StreamEvent(type="text_delta", text=result.text, response_id=result.response_id, raw=result.raw) + if result.reasoning: + yield StreamEvent(type="reasoning_delta", reasoning=result.reasoning, response_id=result.response_id, raw=result.raw) + if result.refusal: + yield StreamEvent(type="refusal_delta", refusal=result.refusal, response_id=result.response_id, raw=result.raw) + for call in result.tool_calls: + yield StreamEvent(type="tool_call_delta", tool_call=call, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="tool_call_completed", tool_call=call, response_id=result.response_id, raw=result.raw) + if result.usage: + yield StreamEvent(type="usage", usage=result.usage, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="finish", finish_reason=result.finish_reason, response_id=result.response_id, raw=result.raw) + return + response_id: str | None = None + finish_emitted = False + message_stop_seen = False + tool_calls: dict[Any, dict[str, Any]] = {} + completed_tool_calls: set[Any] = set() + with retry_sync_stream_context( + lambda: self._get_client().messages.stream(**params) + ) as stream: + for event in stream: + if field(event, "type") == "message_stop": + message_stop_seen = True + converted = self._stream_event(event, response_id) + if converted: + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(tool_calls, converted.tool_call) + elif converted.type == "finish": + for key, tool_call in tool_calls.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=response_id, + raw=converted.raw, + ) + if converted.type == "finish": + if finish_emitted: + continue + finish_emitted = True + response_id = converted.response_id or response_id + yield converted + if not message_stop_seen: + raise RuntimeError("Anthropic Messages 流在 message_stop 之前结束。") + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + request = coerce_completion_request(request, kwargs, "Anthropic astream_events") + request = request.copy_with(stream=True) + params = self._build_message_params(**request.to_invoke_kwargs()) + if not request.stream: + result = await self.acomplete(request) + if result.text: + yield StreamEvent(type="text_delta", text=result.text, response_id=result.response_id, raw=result.raw) + if result.reasoning: + yield StreamEvent(type="reasoning_delta", reasoning=result.reasoning, response_id=result.response_id, raw=result.raw) + if result.refusal: + yield StreamEvent(type="refusal_delta", refusal=result.refusal, response_id=result.response_id, raw=result.raw) + for call in result.tool_calls: + yield StreamEvent(type="tool_call_delta", tool_call=call, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="tool_call_completed", tool_call=call, response_id=result.response_id, raw=result.raw) + if result.usage: + yield StreamEvent(type="usage", usage=result.usage, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="finish", finish_reason=result.finish_reason, response_id=result.response_id, raw=result.raw) + return + response_id: str | None = None + finish_emitted = False + message_stop_seen = False + tool_calls: dict[Any, dict[str, Any]] = {} + completed_tool_calls: set[Any] = set() + async with retry_async_stream_context( + lambda: self._get_aclient().messages.stream(**params) + ) as stream: + async for event in iterate_async(stream): + if field(event, "type") == "message_stop": + message_stop_seen = True + converted = self._stream_event(event, response_id) + if converted: + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(tool_calls, converted.tool_call) + elif converted.type == "finish": + for key, tool_call in tool_calls.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=response_id, + raw=converted.raw, + ) + if converted.type == "finish": + if finish_emitted: + continue + finish_emitted = True + response_id = converted.response_id or response_id + yield converted + if not message_stop_seen: + raise RuntimeError("Anthropic Messages 异步流在 message_stop 之前结束。") + + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + request = coerce_completion_request(request, kwargs, "Anthropic complete") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, stream=False) + response = retry_sync_call(lambda: self._get_client().messages.create(**params)) + return self._extract_result(response) + + async def acomplete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """使用 AsyncAnthropic 聚合完整结果,保留工具、thinking 和 usage。""" + request = coerce_completion_request(request, kwargs, "Anthropic acomplete") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, stream=False) + response = await retry_async_call(lambda: self._get_aclient().messages.create(**params)) + return self._extract_result(response) + + def parse( + self, + output_format: Any, + request: CompletionRequest | None = None, + **kwargs: Any, + ) -> Any: + """调用 Anthropic Messages 的原生结构化输出解析入口。""" + if output_format is None: + raise ValueError("Anthropic parse 必须提供 output_format。") + request = coerce_completion_request(request, kwargs, "Anthropic parse") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("output_format", None) + params = self._build_message_params(**values, stream=False, for_parse=True) + if request.output_config is None: + params.pop("output_config", None) + params = self._parse_params(params, beta=False) + return self._get_client().messages.parse(output_format=output_format, **params) + + @staticmethod + def _parse_params(params: dict[str, Any], *, beta: bool) -> dict[str, Any]: + """只保留当前 SDK 的 Messages.parse 参数。""" + allowed = { + "model", "max_tokens", "messages", "metadata", "output_config", + "output_format", "service_tier", "stop_sequences", "system", + "thinking", "tool_choice", "tools", + "extra_headers", "extra_query", "extra_body", "timeout", + } + if beta: + allowed.update( + { + "cache_control", "container", "context_management", "diagnostics", + "fallback_credit_token", "fallbacks", "inference_geo", "mcp_servers", + "speed", "betas", "user_profile_id", + } + ) + reject_unsupported_kwargs( + "Anthropic Beta Messages parse" if beta else "Anthropic Messages parse", + {key: value for key, value in params.items() if key not in allowed}, + ) + return {key: value for key, value in params.items() if key in allowed} + + @staticmethod + def _validate_token_count_request(request: CompletionRequest, *, beta: bool) -> None: + """拒绝不会被当前 Messages count_tokens SDK 使用的请求字段。""" + allowed = { + "prompt", "system_prompt", "messages", "tools", "tool_choice", + "cache_control", "response_format", "output_config", + # CompletionRequest defaults temperature for generation. It has no + # effect on token counting, so accept the shared default without + # forwarding it to the count_tokens endpoint. + "thinking", "user", "extra_body", "extra_headers", "extra_query", + "timeout", "stream", "temperature", + } + if request.output_format is not None: + mode = "Beta " if beta else "" + raise ValueError( + f"Anthropic {mode}Messages token count 不支持 output_format;" + "请使用 output_config。" + ) + if beta: + allowed.update({"context_management", "mcp_servers", "speed", "betas"}) + reject_unsupported_kwargs( + "Anthropic Beta Messages token count" if beta else "Anthropic Messages token count", + { + key: value + for key, value in request.to_invoke_kwargs().items() + if key not in allowed + }, + ) + + def count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Anthropic count_tokens") + self._validate_token_count_request(request, beta=False) + normalized = normalize_messages(request.prompt, request.system_prompt, request.messages) + params: dict[str, Any] = { + "model": self._model_name, + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(request.messages) or request.system_prompt + if effective_system: + params["system"] = effective_system + tools = self._convert_tools(request.tools) + if tools: + params["tools"] = tools + if request.tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(request.tool_choice) + if request.cache_control is not None: + params["cache_control"] = request.cache_control + if request.user is not None: + params["user_profile_id"] = request.user + if request.output_config is not None: + params["output_config"] = request.output_config + else: + format_config = self._response_format_config(request.response_format) + if format_config: + params["output_config"] = {"format": format_config} + if request.thinking is not None: + params["thinking"] = request.thinking + if request.extra_body is not None: + params["extra_body"] = validate_secret_free_payload( + request.extra_body, + "Anthropic Messages token count", + "extra_body", + ) + profile_headers, _ = self._user_profile_fields( + request.user, request.extra_headers, beta=False + ) + if profile_headers is not None: + params["extra_headers"] = profile_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.timeout is not None: + params["timeout"] = request.timeout + response = self._get_client().messages.count_tokens(**params) + value = field(response, "input_tokens") + if value is None: + raise RuntimeError("Anthropic token count 响应缺少 input_tokens。") + return int(value) + + def beta_create(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """调用 Anthropic Beta Messages,保留 Beta 专属参数转发能力。""" + request = coerce_completion_request(request, kwargs, "Anthropic beta_create") + if request.output_format is not None: + raise ValueError( + "Anthropic Beta Messages.create 不支持 output_format;请使用 beta_parse 或 beta_stream。" + ) + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=False) + return self._get_client().beta.messages.create(**params) + + def beta_parse(self, output_format: Any, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + if output_format is None: + raise ValueError("Anthropic Beta parse 必须提供 output_format。") + request = coerce_completion_request(request, kwargs, "Anthropic beta_parse") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("output_format", None) + params = self._build_message_params(**values, beta=True, stream=False, for_parse=True) + if request.output_config is None: + params.pop("output_config", None) + params.pop("output_format", None) + params = self._parse_params(params, beta=True) + return self._get_client().beta.messages.parse(output_format=output_format, **params) + + def beta_count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Anthropic beta_count_tokens") + self._validate_token_count_request(request, beta=True) + normalized = normalize_messages(request.prompt, request.system_prompt, request.messages) + params: dict[str, Any] = { + "model": self._model_name, + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(request.messages) or request.system_prompt + if effective_system: + params["system"] = effective_system + if request.tools: + params["tools"] = self._convert_tools(request.tools) + if request.cache_control is not None: + params["cache_control"] = request.cache_control + if request.context_management is not None: + params["context_management"] = request.context_management + if request.mcp_servers is not None: + params["mcp_servers"] = request.mcp_servers + if request.tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(request.tool_choice) + if request.thinking is not None: + params["thinking"] = request.thinking + if request.output_config is not None: + params["output_config"] = request.output_config + else: + format_config = self._response_format_config(request.response_format) + if format_config: + params["output_config"] = {"format": format_config} + if request.speed is not None: + params["speed"] = request.speed + if request.user is not None: + params["user_profile_id"] = request.user + _, profile_betas = self._user_profile_fields( + request.user, request.extra_headers, request.betas, beta=True + ) + if profile_betas is not None: + params["betas"] = profile_betas + if request.extra_body is not None: + params["extra_body"] = validate_secret_free_payload( + request.extra_body, + "Anthropic Beta Messages token count", + "extra_body", + ) + if request.extra_headers is not None: + params["extra_headers"] = request.extra_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.timeout is not None: + params["timeout"] = request.timeout + response = self._get_client().beta.messages.count_tokens(**params) + value = field(response, "input_tokens") + if value is None: + raise RuntimeError("Anthropic Beta token count 响应缺少 input_tokens。") + return int(value) + + def beta_stream(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """返回 Anthropic Beta Messages 流式上下文管理器。""" + request = coerce_completion_request(request, kwargs, "Anthropic beta_stream") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=True) + return retry_sync_stream_context( + lambda: self._get_client().beta.messages.stream(**params) + ) + + def beta_tool_runner( + self, tools: Any, request: CompletionRequest | None = None, **kwargs: Any + ) -> Any: + """创建 Anthropic Beta 工具运行器,保留 SDK 原生 runner 对象。""" + if "compaction_control" in kwargs: + raise ValueError( + "Anthropic Beta tool_runner 当前 SDK 不支持 compaction_control。" + ) + runner_options = { + key: kwargs.pop(key) + for key in ("max_iterations",) + if key in kwargs + } + request = coerce_completion_request(request, kwargs, "Anthropic beta_tool_runner") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("tools", None) + params = self._build_message_params( + **values, beta=True, stream=request.stream, for_tool_runner=True + ) + params["tools"] = tools + params["stream"] = request.stream + params.update(runner_options) + return self._get_client().beta.messages.tool_runner(**params) + + def create_batch(self, requests: Any, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.create(requests=requests, **kwargs) + + def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.retrieve(batch_id, **kwargs) + + def batch_results(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.results(batch_id, **kwargs) + + def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.cancel(batch_id, **kwargs) + + def list_batches(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.list(**kwargs) + + def delete_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return self._get_client().messages.batches.delete(batch_id, **kwargs) + + def upload_file(self, file: Any, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.upload(file=file, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.list(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.retrieve_metadata(file_id, **kwargs) + + def download_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.download(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return self._get_client().files.delete(file_id, **kwargs) + + def list_models(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "模型") + return self._get_client().models.list(**kwargs) + + def retrieve_model(self, model_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "模型") + return self._get_client().models.retrieve(model_id, **kwargs) + + async def async_count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Anthropic async_count_tokens") + self._validate_token_count_request(request, beta=False) + normalized = normalize_messages(request.prompt, request.system_prompt, request.messages) + params: dict[str, Any] = { + "model": self._model_name, + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(request.messages) or request.system_prompt + if effective_system: + params["system"] = effective_system + tools = self._convert_tools(request.tools) + if tools: + params["tools"] = tools + if request.tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(request.tool_choice) + if request.cache_control is not None: + params["cache_control"] = request.cache_control + if request.user is not None: + params["user_profile_id"] = request.user + if request.output_config is not None: + params["output_config"] = request.output_config + format_config = self._response_format_config(request.response_format) + if format_config and "output_config" not in params: + params["output_config"] = {"format": format_config} + if request.thinking is not None: + params["thinking"] = request.thinking + if request.extra_body is not None: + params["extra_body"] = validate_secret_free_payload( + request.extra_body, + "Anthropic Messages token count", + "extra_body", + ) + profile_headers, _ = self._user_profile_fields( + request.user, request.extra_headers, beta=False + ) + if profile_headers is not None: + params["extra_headers"] = profile_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.timeout is not None: + params["timeout"] = request.timeout + response = await self._resolve_async_result(self._get_aclient().messages.count_tokens(**params)) + value = field(response, "input_tokens") + if value is None: + raise RuntimeError("Anthropic token count 响应缺少 input_tokens。") + return int(value) + + async def async_beta_create(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_create") + if request.output_format is not None: + raise ValueError( + "Anthropic Beta Messages.create 不支持 output_format;请使用 beta_parse 或 beta_stream。" + ) + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=False) + return await self._resolve_async_result(self._get_aclient().beta.messages.create(**params)) + + async def async_beta_parse(self, output_format: Any, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + if output_format is None: + raise ValueError("Anthropic Beta parse 必须提供 output_format。") + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_parse") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=False, for_parse=True) + if request.output_config is None: + params.pop("output_config", None) + params.pop("output_format", None) + params = self._parse_params(params, beta=True) + return await self._resolve_async_result( + self._get_aclient().beta.messages.parse(output_format=output_format, **params) + ) + + async def async_beta_count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_count_tokens") + self._validate_token_count_request(request, beta=True) + normalized = normalize_messages(request.prompt, request.system_prompt, request.messages) + params: dict[str, Any] = { + "model": self._model_name, + "messages": self._convert_messages(normalized), + } + effective_system = extract_system_prompt(request.messages) or request.system_prompt + if effective_system: + params["system"] = effective_system + if request.tools: + params["tools"] = self._convert_tools(request.tools) + if request.cache_control is not None: + params["cache_control"] = request.cache_control + if request.context_management is not None: + params["context_management"] = request.context_management + if request.mcp_servers is not None: + params["mcp_servers"] = request.mcp_servers + if request.tool_choice is not None: + params["tool_choice"] = self._convert_tool_choice(request.tool_choice) + if request.thinking is not None: + params["thinking"] = request.thinking + if request.output_config is not None: + params["output_config"] = request.output_config + else: + format_config = self._response_format_config(request.response_format) + if format_config: + params["output_config"] = {"format": format_config} + if request.speed is not None: + params["speed"] = request.speed + if request.user is not None: + params["user_profile_id"] = request.user + _, profile_betas = self._user_profile_fields( + request.user, request.extra_headers, request.betas, beta=True + ) + if profile_betas is not None: + params["betas"] = profile_betas + if request.extra_body is not None: + params["extra_body"] = validate_secret_free_payload( + request.extra_body, + "Anthropic Beta Messages token count", + "extra_body", + ) + if request.extra_headers is not None: + params["extra_headers"] = request.extra_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.timeout is not None: + params["timeout"] = request.timeout + response = await self._resolve_async_result( + self._get_aclient().beta.messages.count_tokens(**params) + ) + value = field(response, "input_tokens") + if value is None: + raise RuntimeError("Anthropic Beta token count 响应缺少 input_tokens。") + return int(value) + + def async_beta_stream(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """返回 AsyncAnthropic Beta 流式上下文管理器。""" + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_stream") + values = request.to_invoke_kwargs() + values.pop("stream", None) + params = self._build_message_params(**values, beta=True, stream=True) + return retry_async_stream_context( + lambda: self._get_aclient().beta.messages.stream(**params) + ) + + def async_beta_tool_runner( + self, tools: Any, request: CompletionRequest | None = None, **kwargs: Any + ) -> Any: + """创建 AsyncAnthropic Beta 工具运行器。""" + if "compaction_control" in kwargs: + raise ValueError( + "Anthropic Beta tool_runner 当前 SDK 不支持 compaction_control。" + ) + runner_options = { + key: kwargs.pop(key) + for key in ("max_iterations",) + if key in kwargs + } + request = coerce_completion_request(request, kwargs, "Anthropic async_beta_tool_runner") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("tools", None) + params = self._build_message_params( + **values, beta=True, stream=request.stream, for_tool_runner=True + ) + params["tools"] = tools + params["stream"] = request.stream + params.update(runner_options) + return self._get_aclient().beta.messages.tool_runner(**params) + + async def async_parse( + self, + output_format: Any, + request: CompletionRequest | None = None, + **kwargs: Any, + ) -> Any: + """异步调用 Anthropic Messages 的原生结构化输出解析入口。""" + if output_format is None: + raise ValueError("Anthropic parse 必须提供 output_format。") + request = coerce_completion_request(request, kwargs, "Anthropic async_parse") + values = request.to_invoke_kwargs() + values.pop("stream", None) + values.pop("output_format", None) + params = self._build_message_params(**values, stream=False, for_parse=True) + if request.output_config is None: + params.pop("output_config", None) + params = self._parse_params(params, beta=False) + return await self._resolve_async_result( + self._get_aclient().messages.parse(output_format=output_format, **params) + ) + + def close(self) -> None: + """释放同步和异步 Anthropic SDK 客户端。 + + 异步客户端在没有运行中事件循环时通过同步生命周期桥接器关闭; + 若当前已处于事件循环,则保留引用并显式要求调用 ``aclose``。 + """ + client = self._client + async_client = self._aclient + self._client = None + first_error: Exception | None = None + if client is not None: + try: + client.close() + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + if async_client is not None: + try: + close_resource_sync(async_client, "Anthropic 异步客户端") + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + else: + self._aclient = None + else: + self._aclient = None + if first_error is not None: + raise first_error + + async def aclose(self) -> None: + async_client = self._aclient + sync_client = self._client + self._aclient = None + self._client = None + first_error: Exception | None = None + closed_clients: set[int] = set() + for client in (async_client, sync_client): + if client is None or id(client) in closed_clients: + continue + closed_clients.add(id(client)) + try: + result = client.close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + if first_error is not None: + raise first_error + + async def async_create_batch(self, requests: Any, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result( + self._get_aclient().messages.batches.create(requests=requests, **kwargs) + ) + + async def async_retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.retrieve(batch_id, **kwargs)) + + async def async_batch_results(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.results(batch_id, **kwargs)) + + async def async_cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.cancel(batch_id, **kwargs)) + + async def async_list_batches(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.list(**kwargs)) + + async def async_delete_batch(self, batch_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "批处理") + return await self._resolve_async_result(self._get_aclient().messages.batches.delete(batch_id, **kwargs)) + + async def async_upload_file(self, file: Any, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.upload(file=file, **kwargs)) + + async def async_list_files(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.list(**kwargs)) + + async def async_retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.retrieve_metadata(file_id, **kwargs)) + + async def async_download_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.download(file_id, **kwargs)) + + async def async_delete_file(self, file_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "文件") + return await self._resolve_async_result(self._get_aclient().files.delete(file_id, **kwargs)) + + async def async_list_models(self, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "模型") + return await self._resolve_async_result(self._get_aclient().models.list(**kwargs)) + + async def async_retrieve_model(self, model_id: str, **kwargs: Any) -> Any: + kwargs = self._safe_resource_kwargs(kwargs, "模型") + return await self._resolve_async_result(self._get_aclient().models.retrieve(model_id, **kwargs)) + def invoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = None, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + user: str | None = None, + timeout: float | None = None, + **kwargs: Any, ) -> Generator[str, None, None]: """同步调用 Anthropic Claude LLM (CSE Sensor)。""" logger.info(f"调用 Anthropic LLM ({self._model_name}),流式: {stream}") client = self._get_client() start_time = time.perf_counter() - + try: - params = { - "model": self._model_name, - "max_tokens": 4096, - "messages": [{"role": "user", "content": prompt}], - "temperature": temperature, - } - if system_prompt: - params["system"] = system_prompt + params = self._build_message_params( + prompt, + system_prompt, + tools, + temperature, + messages=messages, + max_tokens=max_tokens, + top_p=top_p, + top_k=top_k, + stop=stop, + response_format=response_format, + tool_choice=tool_choice, + extra_body=extra_body, + metadata=metadata, + thinking=thinking, + service_tier=service_tier, + user=user, + timeout=timeout, + stream=stream, + **kwargs, + ) if stream: - with client.messages.stream(**params) as response: - for text in response.text_stream: - yield text + with retry_sync_stream_context( + lambda: client.messages.stream(**params) + ) as response: + yield from self._iter_invoke_stream_text(response) else: - response = client.messages.create(**params) - if response.content: - yield response.content[0].text - + response = retry_sync_call(lambda: client.messages.create(**params)) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {result.refusal}") + if result.text: + yield result.text + duration = time.perf_counter() - start_time logger.info(f"Anthropic LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"Anthropic LLM ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Anthropic LLM (%s) 出错: %s", + self._model_name, + error_text, + ) raise - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) async def ainvoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = None, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + user: str | None = None, + timeout: float | None = None, + **kwargs: Any, ) -> AsyncGenerator[str, None]: """异步调用 Anthropic Claude LLM (CSE Sensor)。""" logger.info(f"异步调用 Anthropic LLM ({self._model_name}),流式: {stream}") aclient = self._get_aclient() start_time = time.perf_counter() - + try: - params = { - "model": self._model_name, - "max_tokens": 4096, - "messages": [{"role": "user", "content": prompt}], - "temperature": temperature, - } - if system_prompt: - params["system"] = system_prompt + params = self._build_message_params( + prompt, + system_prompt, + tools, + temperature, + messages=messages, + max_tokens=max_tokens, + top_p=top_p, + top_k=top_k, + stop=stop, + response_format=response_format, + tool_choice=tool_choice, + extra_body=extra_body, + metadata=metadata, + thinking=thinking, + service_tier=service_tier, + user=user, + timeout=timeout, + stream=stream, + **kwargs, + ) if stream: - async with aclient.messages.stream(**params) as response: - async for text in response.text_stream: + async with retry_async_stream_context( + lambda: aclient.messages.stream(**params) + ) as response: + async for text in self._aiter_invoke_stream_text(response): yield text else: - response = await aclient.messages.create(**params) - if response.content: - yield response.content[0].text - + response = await retry_async_call( + lambda: aclient.messages.create(**params) + ) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Anthropic 请求被拒绝: {result.refusal}") + if result.text: + yield result.text + duration = time.perf_counter() - start_time logger.info(f"Anthropic LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"Anthropic LLM ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Anthropic LLM (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise diff --git a/src/providers/deepseek.py b/src/providers/deepseek.py index 5b2fa26..2829c45 100644 --- a/src/providers/deepseek.py +++ b/src/providers/deepseek.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,9 @@ class DeepSeekProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="deepseek") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="deepseek", protocol=protocol, options=options) + + def _chat_max_tokens_key(self) -> str: + """DeepSeek Chat Completions 仍使用旧版 max_tokens 字段。""" + return "max_tokens" diff --git a/src/providers/factory.py b/src/providers/factory.py index 447c6be..501de05 100644 --- a/src/providers/factory.py +++ b/src/providers/factory.py @@ -1,42 +1,183 @@ -# -*- coding: utf-8 -*- import importlib -from typing import Any, Dict, Type +import inspect +from collections.abc import Mapping +from functools import cache +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as package_version +from typing import Any, ClassVar from src.providers.__base__.model_provider import ( LargeLanguageModel, RerankModel, TextEmbeddingModel, ) -from src.utils.config import get_settings # 导入 get_settings 函数 -from src.utils.log_manager import get_module_logger # 导入日志管理器 +from src.utils.config import ( # 导入 get_settings 函数 + ModelDetail, + ModelProtocol, + get_settings, +) +from src.utils.log_manager import get_module_logger # 导入日志管理器 +from src.utils.security import redact_sensitive_text, validate_secret_free_options logger = get_module_logger(__name__) # 获取当前模块的日志器 class ModelProviderFactory: """模型提供商工厂""" - _provider_map: Dict[str, Dict[str, Any]] = { + _roles = frozenset({"llm", "embedding", "rerank"}) + + _role_aliases: ClassVar[dict[str, dict[str, str]]] = { + "rerank": { + # SiliconFlow 的聊天/嵌入和 Rerank 使用不同的协议实现。 + "siliconflow": "siliconflow_rerank", + } + } + + _provider_map: ClassVar[dict[str, dict[str, Any]]] = { # LLM & Embedding Providers - "google": {"module": "src.providers.google", "class": "GoogleProvider"}, - "openai": {"module": "src.providers.openai", "class": "OpenAIProvider"}, - "anthropic": {"module": "src.providers.anthropic", "class": "AnthropicProvider"}, - "qwen": {"module": "src.providers.qwen", "class": "QwenProvider"}, - "volcengine": {"module": "src.providers.volcengine", "class": "VolcengineProvider"}, - "siliconflow": {"module": "src.providers.siliconflow", "class": "SiliconflowProvider"}, - "ollama": {"module": "src.providers.ollama", "class": "OllamaProvider"}, - "lm-studio": {"module": "src.providers.lm_studio", "class": "LMStudioProvider"}, - "deepseek": {"module": "src.providers.deepseek", "class": "DeepSeekProvider"}, - "grok": {"module": "src.providers.grok", "class": "GrokProvider"}, - "local-hash": {"module": "src.providers.local_hash", "class": "LocalHashEmbeddingProvider"}, + "google": { + "module": "src.providers.google", + "class": "GoogleProvider", + "sdk_package": "google-genai", + "integration": "native_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": {ModelProtocol.GENERATE_CONTENT.value}, + }, + "openai": { + "module": "src.providers.openai", + "class": "OpenAIProvider", + "sdk_package": "openai", + "integration": "native_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + # 这里不把 SDK 契约冒充成任意配置 Base URL 的服务端验收。 + # `protocol_status` 仅在显式登记真实渠道证据后返回 true。 + "server_verified_protocols": set(), + }, + "anthropic": { + "module": "src.providers.anthropic", + "class": "AnthropicProvider", + "sdk_package": "anthropic", + "integration": "native_sdk", + "capabilities": {"llm"}, + "protocols": {ModelProtocol.MESSAGES.value}, + }, + "qwen": { + "module": "src.providers.qwen", + "class": "QwenProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "volcengine": { + "module": "src.providers.volcengine", + "class": "VolcengineProvider", + "sdk_package": "volcengine-python-sdk", + "integration": "native_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.ARK.value, + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "siliconflow": { + "module": "src.providers.siliconflow", + "class": "SiliconflowProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "ollama": { + "module": "src.providers.ollama", + "class": "OllamaProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "lm-studio": { + "module": "src.providers.lm_studio", + "class": "LMStudioProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm", "embedding"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "deepseek": { + "module": "src.providers.deepseek", + "class": "DeepSeekProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "grok": { + "module": "src.providers.grok", + "class": "GrokProvider", + "sdk_package": "openai", + "integration": "openai_compatible_sdk", + "capabilities": {"llm"}, + "protocols": { + ModelProtocol.CHAT_COMPLETIONS.value, + ModelProtocol.RESPONSES.value, + }, + "server_verified_protocols": set(), + }, + "local-hash": { + "module": "src.providers.local_hash", + "class": "LocalHashEmbeddingProvider", + "integration": "local", + "capabilities": {"embedding"}, + }, # Rerank Providers - "jina": {"module": "src.providers.jina", "class": "JinaProvider"}, - "siliconflow_rerank": {"module": "src.providers.siliconflow_rerank", "class": "SiliconflowRerankProvider"}, + "jina": { + "module": "src.providers.jina", + "class": "JinaProvider", + "sdk_package": "httpx", + "integration": "http_json", + "capabilities": {"rerank"}, + }, + "siliconflow_rerank": { + "module": "src.providers.siliconflow_rerank", + "class": "SiliconflowRerankProvider", + "sdk_package": "httpx", + "integration": "http_json", + "capabilities": {"rerank"}, + }, } @staticmethod - def _get_provider_class(provider_name: str) -> Type: - """动态导入并返回提供商类""" + @cache + def _get_provider_class(provider_name: str) -> type: + """动态导入并缓存提供商类,避免每次创建实例都重复导入模块。""" if provider_name not in ModelProviderFactory._provider_map: logger.error(f"不支持的模型提供商: {provider_name}") raise ValueError(f"不支持的模型提供商: {provider_name}") @@ -48,80 +189,402 @@ def _get_provider_class(provider_name: str) -> Type: logger.debug(f"成功加载提供商类: {provider_info['class']} from {provider_info['module']}") return ProviderClass except ImportError as e: - logger.error(f"导入提供商模块失败: {provider_info['module']} - {e}") + logger.error( + "导入提供商模块失败: %s - %s", + provider_info["module"], + redact_sensitive_text(str(e)), + ) raise ImportError(f"无法加载提供商模块: {provider_info['module']}") from e except AttributeError as e: - logger.error(f"在模块中找不到提供商类: {provider_info['class']} in {provider_info['module']} - {e}") + logger.error( + "在模块中找不到提供商类: %s in %s - %s", + provider_info["class"], + provider_info["module"], + redact_sensitive_text(str(e)), + ) raise AttributeError(f"无法找到提供商类: {provider_info['class']}") from e @staticmethod - def get_llm_provider(provider_key: str) -> LargeLanguageModel: - """获取一个语言模型提供商实例""" - current_settings = get_settings() # 获取当前配置 - if provider_key not in current_settings.llm_configurations: - logger.error(f"在LLM配置中未找到key: {provider_key}") - raise ValueError(f"在LLM配置中未找到key: {provider_key}") + def _resolve_provider_name(provider_name: str, role: str) -> str: + return ModelProviderFactory._role_aliases.get(role, {}).get(provider_name, provider_name) + + @classmethod + def _provider_info_for_role(cls, provider_name: str, role: str) -> tuple[str, dict[str, Any]]: + normalized_role = str(role).strip().lower() + if normalized_role not in cls._roles: + allowed = ", ".join(sorted(cls._roles)) + raise ValueError(f"不支持的模型角色: {role}。可选值: {allowed}") + resolved = cls._resolve_provider_name(provider_name, normalized_role) + info = cls._provider_map.get(resolved) + if info is None: + raise ValueError(f"不支持的模型提供商: {provider_name}") + capabilities = info.get("capabilities", set()) + if normalized_role not in capabilities: + raise TypeError(f"提供商 {provider_name} 不支持 {normalized_role} 能力。") + return resolved, info + + @staticmethod + def _create_provider( + provider_name: str, + model_name: str, + role: str, + expected_type: type, + protocol: ModelProtocol | str | None = None, + options: Mapping[str, Any] | None = None, + ) -> Any: + # ``ModelDetail`` normally normalizes protocol values before they reach + # the factory, but this internal helper is also used directly by + # integrations and tests. Normalize again at the boundary so a plain + # string does not fail later with ``AttributeError: value``. + protocol_value: str | None = None + if protocol is not None: + normalized_protocol = ModelDetail.normalize_protocol(protocol) + if normalized_protocol is None: + raise ValueError("provider protocol 不能为空。") + protocol_value = normalized_protocol.value + resolved_provider_name = ModelProviderFactory._resolve_provider_name(provider_name, role) + provider_info = ModelProviderFactory._provider_map.get(resolved_provider_name) + if provider_info is not None: + capabilities = provider_info.get("capabilities") + if capabilities is not None and role not in capabilities: + raise TypeError(f"提供商 {provider_name} 不支持 {role} 能力。") + if protocol_value is not None: + supported_protocols = provider_info.get("protocols", set()) + if protocol_value not in supported_protocols: + raise ValueError(f"提供商 {provider_name} 不支持 {protocol_value} 协议。") + provider_class = ModelProviderFactory._get_provider_class(resolved_provider_name) + if not issubclass(provider_class, expected_type): + raise TypeError( + f"提供商 {provider_name} 不支持 {role} 能力," + f"需要实现 {expected_type.__name__}。" + ) + provider_options = dict(options or {}) + parameters = inspect.signature(provider_class).parameters + accepts_options = "options" in parameters or any( + parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters.values() + ) + kwargs: dict[str, Any] = {"model_name": model_name} + if accepts_options: + kwargs["options"] = provider_options + elif provider_options: + unsupported = ", ".join(sorted(str(key) for key in provider_options)) + raise ValueError( + f"提供商 {provider_name} 的构造函数不支持 options,但配置提供了: {unsupported}" + ) + if protocol_value is None: + instance = provider_class(**kwargs) + return instance + + # Google/Anthropic/Volcengine 的协议固定在各自 SDK 中,不接受 + # protocol 构造参数;OpenAI 兼容 provider 才需要显式选择协议。 + parameters = inspect.signature(provider_class).parameters + if "protocol" not in parameters and not any( + parameter.kind is inspect.Parameter.VAR_KEYWORD + for parameter in parameters.values() + ): + if ( + protocol_value is not None + and provider_info is not None + and protocol_value not in provider_info.get("protocols", set()) + ): + raise ValueError( + f"提供商 {provider_name} 的构造函数不支持 protocol={protocol_value}。" + ) + instance = provider_class(**kwargs) + return instance + kwargs["protocol"] = protocol_value + instance = provider_class(**kwargs) + if protocol_value == ModelProtocol.RESPONSES.value: + # OpenAI-compatible adapters need explicit remote capability evidence + # before exposing Responses resources. Native providers (for example + # Google and Anthropic) do not implement this guard. + require = getattr(instance, "_require_responses_resource", None) + if callable(require): + require("factory") + return instance + + @staticmethod + def _runtime_verified_protocols( + options: Mapping[str, Any] | None, + supported_protocols: set[str] | frozenset[str] | None = None, + provider_name: str | None = None, + ) -> frozenset[str]: + """读取诊断调用使用的实例级服务端协议登记。""" + if options is None: + return frozenset() + if str(provider_name or "").strip().lower() == "google" and "http_options" in options: + # ``http_options`` is a supported google-genai SDK container. Keep + # the general options boundary strict while allowing only this + # explicitly supported container and its safe trace headers. + remaining = { + key: value for key, value in options.items() if key != "http_options" + } + validated = validate_secret_free_options(remaining, "Provider") + validated["http_options"] = validate_secret_free_options( + {"http_options": options["http_options"]}, + "Provider", + allowed_containers={"http_options", "headers"}, + )["http_options"] + else: + validated = validate_secret_free_options(options, "Provider") + configured = validated.get("server_verified_protocols", ()) + if isinstance(configured, str): + configured = (configured,) + if not isinstance(configured, (list, tuple, set, frozenset)): + raise ValueError("server_verified_protocols 必须是字符串或协议序列。") + aliases = { + "chat": ModelProtocol.CHAT_COMPLETIONS.value, + "completion": ModelProtocol.CHAT_COMPLETIONS.value, + "chat_completion": ModelProtocol.CHAT_COMPLETIONS.value, + "response": ModelProtocol.RESPONSES.value, + "anthropic": ModelProtocol.MESSAGES.value, + "anthropic_messages": ModelProtocol.MESSAGES.value, + "gemini": ModelProtocol.GENERATE_CONTENT.value, + "generate": ModelProtocol.GENERATE_CONTENT.value, + } + normalized: set[str] = set() + for value in configured: + protocol = str(value).strip().lower().replace("-", "_") + protocol = aliases.get(protocol, protocol) + try: + normalized_protocol = ModelProtocol(protocol).value + except ValueError as exc: + supported = ", ".join(item.value for item in ModelProtocol) + raise ValueError( + f"不支持的 server_verified_protocols 协议: {value}。可选值: {supported}" + ) from exc + if supported_protocols is not None and normalized_protocol not in supported_protocols: + supported = ", ".join(sorted(supported_protocols)) or "无" + raise ValueError( + f"server_verified_protocols 中的协议 {value} 不适用于当前提供商;" + f"支持: {supported}" + ) + normalized.add(normalized_protocol) + return frozenset(normalized) + + @classmethod + def capabilities(cls, provider_name: str, role: str = "llm") -> frozenset[str]: + """返回指定角色的渠道能力集合,供 UI/配置校验使用。""" + normalized_role = str(role).strip().lower() + resolved, _info = cls._provider_info_for_role(provider_name, normalized_role) + provider_class = cls._get_provider_class(resolved) + if normalized_role in {"embedding", "rerank"}: + return frozenset({normalized_role}) + declared: Any = getattr(provider_class, "capabilities", frozenset()) + # The factory's role map is the authoritative source for which model + # kinds can be instantiated. Do not leak a shared adapter's embedding + # or rerank methods into an LLM capability report. + return (frozenset(declared) - {"embedding", "rerank"}) | {normalized_role} + + @classmethod + def capability_report( + cls, + provider_name: str, + role: str = "llm", + options: Mapping[str, Any] | None = None, + ) -> dict[str, Any]: + """返回可供配置界面和诊断使用的机器可读能力报告。 + + ``capabilities`` 表示本地适配器已经实现的能力;协议的 + ``server_verified`` 仍只表示目标服务端有明确验证证据,不会由 SDK + 的本地资源树自动推导。 + """ + normalized_role = str(role).strip().lower() + resolved, info = cls._provider_info_for_role(provider_name, normalized_role) + provider_class = cls._get_provider_class(resolved) + sdk_package = info.get("sdk_package") + try: + sdk_version = package_version(sdk_package) if sdk_package else None + except PackageNotFoundError: + sdk_version = None + # `protocols` describes LLM wire protocols. Embedding and Rerank use + # their own endpoint contracts and must not inherit chat protocol names. + protocols = sorted(info.get("protocols", set())) if normalized_role == "llm" else [] + verified = set(info.get("server_verified_protocols", set())) + verified.update( + cls._runtime_verified_protocols( + options, + set(info.get("protocols", set())), + resolved, + ) + ) + verified_list = sorted(verified) + return { + "provider": provider_name, + "resolved_provider": resolved, + "role": normalized_role, + "provider_class": f"{provider_class.__module__}.{provider_class.__name__}", + "integration": info.get("integration", "unknown"), + "sdk": { + "package": sdk_package, + "version": sdk_version, + }, + "capabilities": sorted(cls.capabilities(provider_name, normalized_role)), + "protocols": protocols, + "server_verified_protocols": verified_list, + "protocol_status": { + protocol: { + "adapter_supported": True, + "server_verified": protocol in verified, + } + for protocol in protocols + }, + } + + @classmethod + def protocol_status( + cls, + provider_name: str, + protocol: str, + options: Mapping[str, Any] | None = None, + role: str | None = None, + ) -> dict[str, bool]: + """区分本地适配器支持与目标服务端已验证的协议。 + + LLM 是默认角色;未显式指定时,对 embedding/rerank-only provider + 自动选择其唯一角色,以便诊断工具可以统一查询所有渠道。 + """ + if role is None: + candidates = ("llm", "embedding", "rerank") + normalized_role = next( + ( + candidate + for candidate in candidates + if candidate in cls._provider_map.get( + cls._resolve_provider_name(provider_name, candidate), {} + ).get("capabilities", set()) + ), + None, + ) + if normalized_role is None: + raise ValueError(f"无法为提供商 {provider_name} 推断模型角色,请显式传入 role。") + else: + normalized_role = str(role).strip().lower() + resolved, info = cls._provider_info_for_role(provider_name, normalized_role) + normalized = str(protocol).strip().lower().replace("-", "_") + normalized = { + "chat": ModelProtocol.CHAT_COMPLETIONS.value, + "completion": ModelProtocol.CHAT_COMPLETIONS.value, + "chat_completion": ModelProtocol.CHAT_COMPLETIONS.value, + "chat_completions": ModelProtocol.CHAT_COMPLETIONS.value, + "response": ModelProtocol.RESPONSES.value, + "responses": ModelProtocol.RESPONSES.value, + "anthropic": ModelProtocol.MESSAGES.value, + "anthropic_messages": ModelProtocol.MESSAGES.value, + "gemini": ModelProtocol.GENERATE_CONTENT.value, + "generate": ModelProtocol.GENERATE_CONTENT.value, + "generate_content": ModelProtocol.GENERATE_CONTENT.value, + }.get(normalized, normalized) + adapter_supported = normalized in info.get("protocols", set()) + verified = set(info.get("server_verified_protocols", set())) + verified.update( + cls._runtime_verified_protocols( + options, + set(info.get("protocols", set())) if normalized_role == "llm" else set(), + resolved, + ) + ) + server_verified = normalized in verified + return {"adapter_supported": adapter_supported, "server_verified": server_verified} - config = current_settings.llm_configurations[provider_key] + @staticmethod + def _get_model_detail( + provider_key: str, + configurations: Mapping[str, ModelDetail] | None, + settings_field: str, + role_label: str, + ) -> ModelDetail: + if configurations is None: + configurations = getattr(get_settings(), settings_field) + if provider_key not in configurations: + logger.error(f"在{role_label}配置中未找到key: {provider_key}") + raise ValueError(f"在{role_label}配置中未找到key: {provider_key}") + return configurations[provider_key] + + @staticmethod + def get_llm_provider( + provider_key: str, + configurations: Mapping[str, ModelDetail] | None = None, + ) -> LargeLanguageModel: + """获取一个语言模型提供商实例""" + config = ModelProviderFactory._get_model_detail( + provider_key, configurations, "llm_configurations", "LLM" + ) provider_name = config.provider model_name = config.model_name logger.info(f"正在获取LLM提供商: {provider_name}, 模型: {model_name}") try: - ProviderClass = ModelProviderFactory._get_provider_class(provider_name) - instance = ProviderClass(model_name=model_name) + instance = ModelProviderFactory._create_provider( + provider_name, model_name, "llm", LargeLanguageModel, config.protocol, config.options + ) logger.info(f"成功获取LLM提供商实例: {provider_name} ({model_name})") return instance except Exception as e: - logger.error(f"获取LLM提供商实例失败: {provider_name} ({model_name}) - {e}") + logger.error( + "获取LLM提供商实例失败: %s (%s) - %s", + provider_name, + model_name, + redact_sensitive_text(str(e)), + ) raise @staticmethod - def get_embedding_provider(provider_key: str) -> TextEmbeddingModel: + def get_embedding_provider( + provider_key: str, + configurations: Mapping[str, ModelDetail] | None = None, + ) -> TextEmbeddingModel: """获取一个文本向量化模型提供商实例""" - current_settings = get_settings() # 获取当前配置 - if provider_key not in current_settings.embedding_configurations: - logger.error(f"在Embedding配置中未找到key: {provider_key}") - raise ValueError(f"在Embedding配置中未找到key: {provider_key}") - - config = current_settings.embedding_configurations[provider_key] + config = ModelProviderFactory._get_model_detail( + provider_key, configurations, "embedding_configurations", "Embedding" + ) provider_name = config.provider model_name = config.model_name logger.info(f"正在获取Embedding提供商: {provider_name}, 模型: {model_name}") try: - ProviderClass = ModelProviderFactory._get_provider_class(provider_name) - instance = ProviderClass(model_name=model_name) + if config.protocol is not None: + raise ValueError("Embedding 配置不支持 protocol;协议只能配置在 llm_configurations 中。") + instance = ModelProviderFactory._create_provider( + provider_name, model_name, "embedding", TextEmbeddingModel, options=config.options + ) logger.info(f"成功获取Embedding提供商实例: {provider_name} ({model_name})") return instance except Exception as e: - logger.error(f"获取Embedding提供商实例失败: {provider_name} ({model_name}) - {e}") + logger.error( + "获取Embedding提供商实例失败: %s (%s) - %s", + provider_name, + model_name, + redact_sensitive_text(str(e)), + ) raise @staticmethod - def get_rerank_provider(provider_key: str) -> RerankModel: + def get_rerank_provider( + provider_key: str, + configurations: Mapping[str, ModelDetail] | None = None, + ) -> RerankModel: """获取一个Rerank模型提供商实例""" - current_settings = get_settings() # 获取当前配置 - if provider_key not in current_settings.rerank_configurations: - logger.error(f"在Rerank配置中未找到key: {provider_key}") - raise ValueError(f"在Rerank配置中未找到key: {provider_key}") - - config = current_settings.rerank_configurations[provider_key] - # Rerank提供商的key可能与LLM/Embedding提供商的key冲突(如siliconflow) - # 因此,我们在这里使用一个特殊的key,或者直接在配置中指定provider_map的key - # 为了简单起见,我们假设rerank的provider name是唯一的 + config = ModelProviderFactory._get_model_detail( + provider_key, configurations, "rerank_configurations", "Rerank" + ) provider_name = config.provider - if provider_name == "siliconflow": - provider_name = "siliconflow_rerank" # 映射到唯一的rerank provider - model_name = config.model_name logger.info(f"正在获取Rerank提供商: {provider_name}, 模型: {model_name}") try: - ProviderClass = ModelProviderFactory._get_provider_class(provider_name) - instance = ProviderClass(model_name=model_name) + if config.protocol is not None: + raise ValueError("Rerank 配置不支持 protocol;协议只能配置在 llm_configurations 中。") + instance = ModelProviderFactory._create_provider( + provider_name, model_name, "rerank", RerankModel, options=config.options + ) logger.info(f"成功获取Rerank提供商实例: {provider_name} ({model_name})") return instance except Exception as e: - logger.error(f"获取Rerank提供商实例失败: {provider_name} ({model_name}) - {e}") + logger.error( + "获取Rerank提供商实例失败: %s (%s) - %s", + provider_name, + model_name, + redact_sensitive_text(str(e)), + ) raise diff --git a/src/providers/google.py b/src/providers/google.py index 9cf5ad5..4803f54 100644 --- a/src/providers/google.py +++ b/src/providers/google.py @@ -1,166 +1,3861 @@ -# -*- coding: utf-8 -*- +import base64 +import binascii +import inspect +import json +import math +import os +import re import time -from typing import Any, Dict, Generator, List, cast, AsyncGenerator +from collections.abc import AsyncGenerator, Generator, Mapping, Sequence +from enum import Enum +from typing import Any, ClassVar +from urllib.parse import parse_qsl, quote, urlsplit from google import genai +from google.auth.credentials import Credentials from google.genai import types -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential from src.providers.__base__.model_provider import ( + _UNSET_TEMPERATURE, + CompletionRequest, + CompletionResult, LargeLanguageModel, + StreamEvent, TextEmbeddingModel, + close_resource_sync, + coerce_completion_request, + extract_system_prompt, + field, + is_retryable_error, + iterate_async, + normalize_embedding_vector, + normalize_messages, + normalize_usage, + raise_for_stream_error_event, + reject_unsupported_kwargs, + retry_async_call, + retry_async_stream, + retry_sync_call, + retry_sync_stream, + validate_complete_tool_call, ) +from src.providers.resources import AsyncGoogleResources, GoogleResources from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import ( + _model_dump_mapping, + redact_sensitive_text, + validate_secret_free_payload, + validate_secret_free_request_overrides, +) logger = get_module_logger(__name__) + class GoogleProvider(LargeLanguageModel, TextEmbeddingModel): """ Google模型提供商,统一处理Gemini LLM和Embedding。 已升级为使用最新的 google-genai SDK (v1.0+)。 """ - def __init__(self, model_name: str): + capabilities = frozenset( + { + "chat", "stream", "messages", "multimodal", "tools", "structured_output", + "usage", "token_count", "embedding", "files", "batches", "cache", + "models", "tuning", "images", "videos", "file_search", + "interactions", "agents", "webhooks", "environments", "triggers", "live", + "auth_tokens", "operations", "chats", + } + ) + + _EXPERIMENTAL_RESOURCE_LABELS: ClassVar[dict[str, str]] = { + "auth_tokens": "Auth Token", + "interactions": "Interactions", + "agents": "Agents", + "webhooks": "Webhooks", + "environments": "Environments", + "triggers": "Triggers", + } + + _CLIENT_OPTION_KEYS = frozenset( + { + "enterprise", + "vertexai", + "project", + "location", + "credentials", + "http_options", + "debug_config", + } + ) + _OPTION_KEYS = _CLIENT_OPTION_KEYS | frozenset( + { + "extra_body", + "response_format", + "timeout", + "max_tokens", + "task_type", + "output_dimensionality", + "title", + "mime_type", + "auto_truncate", + "document_ocr", + "audio_track_extraction", + "temperature", + "top_p", + "top_k", + "candidate_count", + "max_output_tokens", + "stop_sequences", + "presence_penalty", + "frequency_penalty", + "seed", + "response_mime_type", + "response_schema", + "response_json_schema", + "safety_settings", + "cached_content", + "response_modalities", + "thinking", + "thinking_config", + "automatic_function_calling", + "speech_config", + "image_config", + "labels", + "routing_config", + "model_selection_config", + "media_resolution", + "enable_enhanced_civic_answers", + "service_tier", + "logprobs", + "response_logprobs", + "audio_timestamp", + "audio_transcription_config", + "model_armor_config", + "should_return_http_response", + } + ) + + _HTTP_OPTIONS_SENSITIVE_EXACT = frozenset( + { + "apikey", "xapikey", "accesskey", "secretkey", "token", "password", + "passwd", "authorization", "auth", "bearer", "accesstoken", + "xaccesstoken", "authtoken", "xauthtoken", "authkey", "credential", + "credentials", "cookie", "setcookie", "proxyauthorization", "secret", + "privatekey", "httpclient", "httpxclient", "httpxasyncclient", + "aiohttpclient", "clientargs", "asyncclientargs", "baseurl", + "baseurlresourcescope", "proxy", "proxies", "transport", "verify", + "cert", "mounts", + } + ) + def __init__(self, model_name: str, options: dict[str, Any] | None = None): self._model_name = model_name + if options is not None and not isinstance(options, Mapping): + raise ValueError("Google options 必须是对象。") + self._options = dict(options or {}) + self._validate_options(self._options) logger.info(f"初始化 GoogleProvider (google-genai),模型: {model_name}") - self._client: genai.Client | None = None + self._client: Any | None = None - def _get_client(self) -> genai.Client: + @property + def resources(self) -> GoogleResources: + return GoogleResources(self) + + @property + def async_resources(self) -> AsyncGoogleResources: + return AsyncGoogleResources(self) + + def _get_client(self) -> Any: """延迟初始化并返回 genai.Client 实例。""" - if not self._client: + # Some test doubles and SDK client wrappers define false-y equality or + # length semantics. Only ``None`` means that initialization has not + # happened yet; a valid but false-y client must be reused. + if self._client is None: + self._validate_options(self._options) settings = get_settings() - if not settings.google_api_key: + credentials = self._options.get("credentials") + vertexai = self._configured_vertex_mode() + if credentials is None and os.getenv("GOOGLE_APPLICATION_CREDENTIALS") is None: + credential_path = self._configured_setting( + settings, + "google_application_credentials", + "GOOGLE_APPLICATION_CREDENTIALS", + ) + if credential_path: + credentials = self._load_credentials_file(credential_path) + api_key = self._configured_api_key(settings) + if credentials is not None and vertexai is False: + raise ValueError( + "Google credentials 需要 Vertex/Enterprise 模式;" + "请将 vertexai 或 enterprise 设为 true。" + ) + if credentials is not None and vertexai is None: + vertexai = True + if credentials is None and not api_key and vertexai is not True: logger.error("Google API Key 未设置。") - raise ValueError("GOOGLE_API_KEY is required for GoogleProvider") + raise ValueError( + "GOOGLE_API_KEY is required for GoogleProvider unless options.credentials is provided " + "or Vertex/Enterprise mode enables Application Default Credentials" + ) logger.debug("正在初始化 google-genai Client") - self._client = genai.Client(api_key=settings.google_api_key) + client_options = { + key: value + for key, value in self._options.items() + if key in self._CLIENT_OPTION_KEYS + } + for key in ("enterprise", "vertexai"): + if key in client_options: + client_options[key] = self._as_bool(client_options[key]) + if "enterprise" not in client_options and "vertexai" not in client_options: + configured_flags = self._configured_environment_vertex_flags(settings) + if "GOOGLE_GENAI_USE_ENTERPRISE" in configured_flags: + client_options["enterprise"] = configured_flags[ + "GOOGLE_GENAI_USE_ENTERPRISE" + ] + elif "GOOGLE_GENAI_USE_VERTEXAI" in configured_flags: + client_options["vertexai"] = configured_flags[ + "GOOGLE_GENAI_USE_VERTEXAI" + ] + if "debug_config" in client_options: + client_options["debug_config"] = self._coerce_debug_config( + client_options["debug_config"] + ) + if credentials is not None: + if "credentials" not in client_options: + client_options["credentials"] = credentials + if vertexai is True and "enterprise" not in client_options and "vertexai" not in client_options: + # google-genai treats explicit credentials as Vertex ADC + # credentials only when the Vertex transport is selected. + # Make that implication explicit instead of letting the SDK + # fall through to Developer API key validation. + client_options["vertexai"] = True + if vertexai is True: + for option_name, setting_name, env_name in ( + ("project", "google_cloud_project", "GOOGLE_CLOUD_PROJECT"), + ("location", "google_cloud_location", "GOOGLE_CLOUD_LOCATION"), + ): + if option_name not in client_options: + configured_value = self._configured_setting( + settings, setting_name, env_name + ) + if configured_value: + client_options[option_name] = configured_value + # ``google-genai`` supports both ADC and Vertex express-mode API + # keys. Only explicit credentials and an API key are mutually + # exclusive; an API key remains valid when the Vertex endpoint is + # selected and must not be dropped when it came from config.toml. + if credentials is None and api_key: + self._client = genai.Client(api_key=api_key, **client_options) + else: + # google-genai can resolve Vertex ADC when neither api_key nor + # explicit credentials is supplied; credentials and api_key + # must never be passed together. + self._client = genai.Client(**client_options) logger.info("google-genai Client 初始化成功。") return self._client - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + @staticmethod + def _configured_api_key(settings: Any) -> str | None: + """读取 Google SDK 支持的两个环境键,优先 GOOGLE_API_KEY。""" + for name in ("google_api_key", "gemini_api_key"): + value = getattr(settings, name, None) + # MagicMock-style test doubles expose arbitrary attributes; only + # actual strings are valid SDK API keys here. + if isinstance(value, str) and value.strip(): + return value + return None + + @staticmethod + def _configured_setting(settings: Any, field_name: str, env_name: str) -> str | None: + """读取环境变量或 Settings 中的非空字符串配置。""" + value = os.getenv(env_name) + if value is None: + value = getattr(settings, field_name, None) + if isinstance(value, str) and value.strip(): + return value.strip() + return None + + @staticmethod + def _load_credentials_file(path: str) -> Any: + """从 Settings 中配置的服务账号路径加载 ADC 凭证。""" + try: + from google.auth import load_credentials_from_file + except ImportError as exc: # pragma: no cover - google-genai supplies google-auth + raise RuntimeError("当前环境缺少 google-auth,无法加载 Google ADC 凭证。") from exc + try: + credentials, _ = load_credentials_from_file(path) + except Exception as exc: + raise ValueError("Google GOOGLE_APPLICATION_CREDENTIALS 文件无法加载。") from exc + return credentials + + @staticmethod + def _as_bool(value: Any) -> bool: + """将配置文件中的布尔字符串规范为 SDK 需要的 bool。""" + if isinstance(value, bool): + return value + if isinstance(value, (int, float)): + return value != 0 + if isinstance(value, str): + return value.strip().lower() in {"1", "true", "yes", "on"} + return bool(value) + + @classmethod + def _validate_options(cls, options: Mapping[str, Any]) -> None: + if not isinstance(options, Mapping): + raise ValueError("Google options 必须是对象。") + unknown = sorted(set(options).difference(cls._OPTION_KEYS)) + if unknown: + raise ValueError( + "Google options 包含不支持的字段: " + ", ".join(unknown) + ) + if "extra_body" in options: + validate_secret_free_payload( + options["extra_body"], + "Google", + "options.extra_body", + ) + if "http_options" in options: + cls._validate_http_options(options["http_options"]) + if "timeout" in options: + cls._validate_timeout(options["timeout"], "Google options.timeout") + configured_body = options.get("extra_body") + model_conflicts: list[str] = [] + if options.get("http_options") is not None: + if options.get("timeout") is not None: + model_conflicts.append("timeout") + if configured_body: + model_conflicts.append("extra_body") + if model_conflicts: + raise ValueError( + "Google options.http_options 不能与模型级 options 同时配置: " + + ", ".join(model_conflicts) + ) + + @staticmethod + def _require_resource(client: Any, name: str, label: str) -> Any: + """读取 SDK 资源并把版本差异转换成明确的能力错误。""" + + try: + resource = getattr(client, name) + except (AttributeError, NotImplementedError) as exc: + raise NotImplementedError( + f"当前 google-genai SDK 未提供 {label}资源;请升级 SDK 或使用官方支持的接口。" + ) from exc + if resource is None: + raise NotImplementedError( + f"当前 google-genai SDK 未提供 {label}资源;请升级 SDK 或使用官方支持的接口。" + ) + return resource + + def _require_provider_resource(self, method_name: str) -> None: + """把实验性资源的版本差异转换成明确的能力错误。""" + name = method_name.removeprefix("async_") + resource_name: str | None = None + for candidate in self._EXPERIMENTAL_RESOURCE_LABELS: + singular = candidate.removesuffix("s") + if name.startswith(singular) or f"_{singular}" in name: + resource_name = candidate + break + if resource_name is None: + if name == "create_auth_token": + resource_name = "auth_tokens" + else: + return + client = self._get_client() + if method_name.startswith("async_"): + try: + client = client.aio + except (AttributeError, NotImplementedError) as exc: + raise NotImplementedError( + f"当前 google-genai SDK 未提供异步 {self._EXPERIMENTAL_RESOURCE_LABELS[resource_name]}资源。" + ) from exc + self._require_resource( + client, + resource_name, + self._EXPERIMENTAL_RESOURCE_LABELS[resource_name], + ) + + @classmethod + def _validate_http_options(cls, value: Any) -> None: + """校验 Google SDK HTTP 扩展,允许普通追踪头但拒绝凭证。""" + if value is None: + return + if isinstance(value, Mapping): + payload: Any = dict(value) + else: + model_dump = getattr(value, "model_dump", None) + if not callable(model_dump): + raise ValueError( + "Google options.http_options 必须是对象表或 google-genai HttpOptions。" + ) + try: + payload = model_dump(exclude_none=True) + except TypeError: + try: + payload = model_dump() + except Exception as exc: + raise ValueError("Google options.http_options 无法读取。") from exc + except Exception as exc: + raise ValueError("Google options.http_options 无法读取。") from exc + if not isinstance(payload, Mapping): + raise ValueError("Google options.http_options 必须是对象。") + + if "timeout" in payload and payload["timeout"] is not None: + timeout = payload["timeout"] + if ( + isinstance(timeout, bool) + or not isinstance(timeout, (int, float)) + or not math.isfinite(float(timeout)) + or float(timeout) <= 0 + or int(float(timeout)) != float(timeout) + ): + raise ValueError( + "Google options.http_options.timeout 必须是正整数毫秒。" + ) + + found: list[str] = [] + + def normalized_key(key: Any) -> str: + return "".join(char for char in str(key).lower() if char.isalnum()) + + def is_sensitive_key(key: Any) -> bool: + normalized = normalized_key(key) + if normalized in cls._HTTP_OPTIONS_SENSITIVE_EXACT: + return True + return any( + normalized.endswith(marker) + for marker in ( + "apikey", "accesskey", "accesstoken", "authtoken", + "secret", "secretkey", "clientsecret", "password", + "credential", "cookie", + ) + ) + + def check_url_query(value: Any, path: str) -> None: + if not isinstance(value, str): + return + try: + query = parse_qsl(urlsplit(value).query, keep_blank_values=True) + except ValueError: + return + for key, _ in query: + if is_sensitive_key(key): + found.append(f"{path}?{key}") + + def visit(item: Any, path: str = "") -> None: + if isinstance(item, Mapping): + for key, nested in item.items(): + current = f"{path}.{key}" if path else str(key) + if nested is None: + continue + if is_sensitive_key(key): + found.append(current) + continue + visit(nested, current) + elif isinstance(item, (list, tuple, set, frozenset)): + for index, nested in enumerate(item): + visit(nested, f"{path}[{index}]") + elif isinstance(item, str): + if redact_sensitive_text(item) != item: + found.append(path or "value") + check_url_query(item, path) + try: + parsed = urlsplit(item) + except ValueError: + parsed = None + if parsed is not None and (parsed.username is not None or parsed.password is not None): + found.append(f"{path} userinfo") + + visit(payload) + if found: + raise ValueError( + "Google options.http_options 不允许包含凭证、自定义客户端或连接覆盖: " + + ", ".join(sorted(found)) + ) + + @staticmethod + def _validate_timeout(value: Any, field_name: str) -> float: + """将请求 timeout 规范为有限正数,避免底层比较抛出 TypeError。""" + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(float(value)) + or float(value) <= 0 + ): + raise ValueError(f"{field_name} 必须是正数。") + return float(value) + + @classmethod + def _validate_resource_config(cls, config: Any) -> None: + """检查资源 config 中的 HTTP 配置,但不误判业务字段。""" + if config is None: + return + if isinstance(config, Mapping): + payload: Any = dict(config) + else: + payload = _model_dump_mapping(config) + if not isinstance(payload, Mapping): + return + + def visit(value: Any) -> None: + if isinstance(value, Mapping): + for key, nested in value.items(): + normalized = "".join( + char for char in str(key).lower() if char.isalnum() + ) + if normalized == "httpoptions": + cls._validate_http_options(nested) + continue + visit(nested) + return + dumped = _model_dump_mapping(value) + if dumped is not None: + visit(dumped) + elif isinstance(value, (list, tuple, set, frozenset)): + for nested in value: + visit(nested) + + visit(payload) + + @staticmethod + def _coerce_debug_config(value: Any) -> Any: + """将配置文件中的 DebugConfig 表转换为 SDK 对象。""" + if value is None or not isinstance(value, Mapping): + return value + allowed = {"client_mode", "replays_directory", "replay_id"} + unknown = sorted(set(value).difference(allowed)) + if unknown: + raise ValueError( + "Google debug_config 包含不支持的字段: " + ", ".join(unknown) + ) + try: + from google.genai.client import DebugConfig + except ImportError as exc: + raise RuntimeError("当前 google-genai SDK 不支持 debug_config。") from exc + try: + return DebugConfig(**dict(value)) + except Exception as exc: + raise ValueError("Google debug_config 配置无效。") from exc + + def _configured_vertex_mode(self) -> bool | None: + """读取显式或 SDK 支持的环境级 Vertex/Enterprise 开关。""" + options = getattr(self, "_options", {}) or {} + explicit_values = { + key: self._as_bool(options[key]) + for key in ("enterprise", "vertexai") + if key in options and options[key] is not None + } + if explicit_values: + if len(set(explicit_values.values())) > 1: + raise ValueError( + "Google options.enterprise 与 options.vertexai 的值冲突;" + "请只设置一个,或将两者设为相同值。" + ) + return next(iter(explicit_values.values())) + + settings = get_settings() + environment_values = self._configured_environment_vertex_flags(settings) + if len(set(environment_values.values())) > 1: + raise ValueError( + "GOOGLE_GENAI_USE_ENTERPRISE 与 GOOGLE_GENAI_USE_VERTEXAI 的值冲突;" + "请只保留一个,或将两者设为相同值。" + ) + # 未配置开关时保持 unknown,而不是武断地当作 Developer API。 + # 这样测试替身、旧 SDK 或由 SDK 自己决定 endpoint 的客户端仍能 + # 继续执行能力探测;明确设置 false 时才拒绝 Vertex-only 能力。 + return next(iter(environment_values.values())) if environment_values else None + + @classmethod + def _configured_environment_vertex_flags(cls, settings: Any) -> dict[str, bool]: + """读取进程环境或 ``.env`` 载入的 Vertex/Enterprise 开关。""" + + values: dict[str, bool] = {} + for env_name, field_name in ( + ("GOOGLE_GENAI_USE_ENTERPRISE", "google_genai_use_enterprise"), + ("GOOGLE_GENAI_USE_VERTEXAI", "google_genai_use_vertexai"), + ): + raw_value = os.getenv(env_name) + if raw_value is None: + raw_value = getattr(settings, field_name, None) + # MagicMock-style test doubles expose arbitrary attributes; + # those are not valid Settings values and must be ignored. + if not isinstance(raw_value, (bool, int, float, str)): + raw_value = None + if raw_value is not None: + values[env_name] = cls._as_bool(raw_value) + return values + + @staticmethod + def _client_vertex_mode(client: Any) -> bool | None: + """返回已构造客户端的真实模式;测试替身没有该属性时返回 None。""" + client_values = vars(client) if hasattr(client, "__dict__") else {} + api_client = client_values.get("_api_client") + api_values = vars(api_client) if hasattr(api_client, "__dict__") else {} + if api_client is not None and "vertexai" in api_values: + return bool(api_client.vertexai) + if "vertexai" in client_values: + return bool(client_values["vertexai"]) + return None + + def _effective_vertex_mode(self, client: Any) -> bool | None: + """优先读取 SDK 实际模式,测试替身或旧 SDK 缺失时回退到配置。""" + actual = self._client_vertex_mode(client) + return self._configured_vertex_mode() if actual is None else actual + + def _vertex_embed_content_only(self) -> bool: + """判断当前模型是否只能通过 Vertex 的单内容 embedContent 调用。""" + model = self._model_name.lower() + return ( + "gemini" in model + ) or "maas" in model + + @staticmethod + def _validate_token_count_fields(request: CompletionRequest) -> None: + """拒绝 token 统计不会接受或不会生效的生成参数。""" + unsupported = { + key: value + for key, value in request.to_invoke_kwargs().items() + if key in { + "max_tokens", "top_p", "top_k", "frequency_penalty", + "presence_penalty", "n", "logit_bias", "logprobs", + "top_logprobs", "modalities", "audio", "prediction", + "web_search_options", "seed", "stop", "response_format", + "tool_choice", "reasoning", "thinking", "store", "background", + "parallel_tool_calls", "max_tool_calls", "conversation", "session", + "context_management", "caching", "expire_at", "prompt_cache_options", + "safety_identifier", "moderation", "verbosity", "prompt_cache_key", + "prompt_cache_retention", "service_tier", "truncation", "stream_options", + "cache_control", "container", "inference_geo", "mcp_servers", + "output_config", "output_format", "speed", "betas", "diagnostics", + "fallback_credit_token", "fallbacks", "user", "personality", + } + and value is not None + } + reject_unsupported_kwargs("Google token count", unsupported) + + @staticmethod + def _validate_compute_token_fields(request: CompletionRequest) -> None: + """拒绝 computeTokens 不接受或不会生效的请求字段。""" + unsupported = { + key: value + for key, value in request.to_invoke_kwargs().items() + if key in { + "max_tokens", "top_p", "top_k", "frequency_penalty", + "presence_penalty", "n", "logit_bias", "logprobs", "top_logprobs", + "modalities", "audio", "prediction", "web_search_options", "seed", + "stop", "response_format", "tool_choice", "reasoning", "thinking", + "store", "background", "parallel_tool_calls", "max_tool_calls", + "conversation", "session", "context_management", "caching", "expire_at", + "prompt_cache_options", "safety_identifier", "moderation", "verbosity", + "prompt_cache_key", "prompt_cache_retention", "service_tier", "truncation", + "stream_options", "cache_control", "container", "inference_geo", "mcp_servers", + "output_config", "output_format", "speed", "betas", "diagnostics", + "fallback_credit_token", "fallbacks", "user", "personality", "tools", + } + and value is not None + } + reject_unsupported_kwargs("Google compute tokens", unsupported) + + @staticmethod + def _token_count_system_prompt(request: CompletionRequest) -> str | None: + """返回 token 统计请求中真正显式提供的 system prompt。""" + message_system = extract_system_prompt(request.messages) + if message_system: + return message_system + default_system_prompt = CompletionRequest.__dataclass_fields__["system_prompt"].default + return None if request.system_prompt == default_system_prompt else request.system_prompt + + def _embedding_options(self) -> dict[str, Any]: + supported = { + "task_type", "output_dimensionality", "title", "mime_type", + "auto_truncate", "document_ocr", "audio_track_extraction", + } + return {key: value for key, value in self._options.items() if key in supported} + + @staticmethod + def _resource_config(config_type: Any, config: Any, values: dict[str, Any]) -> Any: + """将 Facade 的关键字配置转换为 google-genai 的 ``config=`` 参数。""" + GoogleProvider._validate_resource_config(config) + values = dict(values) + request_options = { + key: values.pop(key, None) + for key in ("extra_headers", "extra_body", "extra_query", "timeout") + } + http_options = GoogleProvider._build_http_options(**request_options) + if http_options is not None: + if "http_options" in values: + raise ValueError("Google SDK 的 http_options 不能与请求级 HTTP 配置同时传入。") + values["http_options"] = http_options + if config is not None: + if values: + raise ValueError("Google SDK 的 config 不能与同级配置关键字同时传入。") + return config + if not values: + return None + return config_type(**values) + + @classmethod + def _resource_config_kwargs( + cls, config_type: Any, config: Any, values: dict[str, Any] + ) -> dict[str, Any]: + built = cls._resource_config(config_type, config, values) + return {"config": built} if built is not None else {} + + @staticmethod + def _validate_embedding_kwargs(kwargs: dict[str, Any]) -> None: + allowed = { + "task_type", "output_dimensionality", "title", "mime_type", + "auto_truncate", "document_ocr", "audio_track_extraction", + } + unsupported = sorted(key for key, value in kwargs.items() if value is not None and key not in allowed) + if unsupported: + raise ValueError(f"Google Embedding 不支持请求参数: {', '.join(unsupported)}") + + @staticmethod + def _validate_request(request: CompletionRequest) -> None: + supported = { + "prompt", "system_prompt", "messages", "tools", "stream", "temperature", + "max_tokens", "top_p", "top_k", "frequency_penalty", "presence_penalty", + "n", "seed", "modalities", "stop", "response_format", "tool_choice", + "extra_body", "extra_headers", "extra_query", "thinking", "service_tier", "timeout", + } + reject_unsupported_kwargs( + "Google SDK", + { + key: value + for key, value in request.to_invoke_kwargs().items() + if key not in supported + }, + ) + + @staticmethod + def _build_http_options( + *, + extra_headers: dict[str, str] | None = None, + extra_body: dict[str, Any] | None = None, + extra_query: dict[str, Any] | None = None, + timeout: float | None = None, + ) -> Any | None: + """将统一请求的 HTTP 扩展转换为 google-genai 的 HttpOptions。""" + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + "Google", + ) + if extra_query is not None: + raise ValueError( + "Google SDK 不支持请求级 extra_query;请在 options.http_options 中配置。" + ) + extra_body = validate_secret_free_payload( + extra_body, + "Google", + "extra_body", + ) if extra_body is not None else None + values: dict[str, Any] = {} + if extra_headers is not None: + values["headers"] = dict(extra_headers) + if extra_body is not None: + values["extra_body"] = dict(extra_body) + if timeout is not None: + timeout = GoogleProvider._validate_timeout(timeout, "Google 请求 timeout") + values["timeout"] = max(1, int(timeout * 1000)) + return types.HttpOptions(**values) if values else None + + @staticmethod + async def _resolve_async_result(result: Any) -> Any: + return await result if inspect.isawaitable(result) else result + + def _generation_options(self) -> dict[str, Any]: + supported = { + "temperature", "top_p", "top_k", "candidate_count", "max_output_tokens", + "stop_sequences", "presence_penalty", "frequency_penalty", "seed", + "response_mime_type", "response_schema", "response_json_schema", + "safety_settings", "cached_content", "response_modalities", "thinking_config", + "automatic_function_calling", "speech_config", "image_config", "labels", + "routing_config", "model_selection_config", "media_resolution", + "enable_enhanced_civic_answers", "service_tier", + "logprobs", "response_logprobs", "audio_timestamp", "audio_transcription_config", + "model_armor_config", "should_return_http_response", + } + return {key: value for key, value in self._options.items() if key in supported} + + @staticmethod + def _response_format_values(response_format: Any) -> dict[str, Any]: + """将统一 response_format 转换为 Gemini GenerateContentConfig 字段。""" + if not isinstance(response_format, Mapping): + raise ValueError("Google response_format 必须是对象。") + format_type = response_format.get("type") + if format_type == "json_object": + return {"response_mime_type": "application/json"} + if format_type == "json_schema": + schema = response_format.get("json_schema", response_format) + if not isinstance(schema, Mapping): + raise ValueError("Google response_format.json_schema 必须是对象。") + payload = schema.get("schema", response_format.get("schema")) + if not isinstance(payload, Mapping): + raise ValueError( + "Google response_format.json_schema.schema 必须是 JSON Schema 对象。" + ) + return { + "response_mime_type": "application/json", + "response_json_schema": dict(payload), + } + raise ValueError( + "Google response_format 仅支持 json_object 或 json_schema。" + ) + + def _generation_http_options( + self, + *, + extra_body: dict[str, Any] | None, + extra_headers: dict[str, str] | None, + extra_query: dict[str, Any] | None, + timeout: float | None, + ) -> tuple[dict[str, Any] | None, dict[str, str] | None, dict[str, Any] | None, float | None, Any | None]: + """合并模型级与调用级 HTTP 扩展,拒绝同名字段静默覆盖。""" + options = getattr(self, "_options", {}) or {} + configured_body = options.get("extra_body") + if configured_body is not None and not isinstance(configured_body, Mapping): + raise ValueError("Google options.extra_body 必须是对象。") + configured_body = validate_secret_free_payload( + configured_body, + "Google", + "options.extra_body", + ) + if extra_body is not None and not isinstance(extra_body, Mapping): + raise ValueError("Google extra_body 必须是对象。") + configured_values = dict(configured_body or {}) + request_values = validate_secret_free_payload( + extra_body, + "Google", + "extra_body", + ) + overlap = sorted(set(configured_values).intersection(request_values)) + if overlap: + raise ValueError( + "Google extra_body 与模型 options 重复: " + ", ".join(overlap) + ) + merged_body = {**configured_values, **request_values} or None + effective_timeout = timeout + if effective_timeout is None: + effective_timeout = options.get("timeout") + configured_http_options = options.get("http_options") + request_body_configured = request_values if request_values else None + if configured_http_options is not None and any( + value is not None + for value in (extra_headers, request_body_configured, extra_query, timeout) + ): + raise ValueError( + "Google options.http_options 不能与请求级 extra_headers、extra_body、" + "extra_query 或 timeout 同时传入。" + ) + return ( + merged_body, + dict(extra_headers) if extra_headers is not None else None, + dict(extra_query) if extra_query is not None else None, + effective_timeout, + configured_http_options, + ) + + def _http_options_for_request( + self, + *, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + timeout: float | None = None, + ) -> Any | None: + """构造模型级与请求级 HTTP 配置共用的 ``HttpOptions``。 + + Embedding、token-count 和 compute-token 接口不经过生成配置构造器, + 因此必须显式复用同一套模型级 ``extra_body``、``timeout`` 和 + ``http_options`` 语义,避免这些入口悄悄丢弃配置。 + """ + merged_body, request_headers, request_query, effective_timeout, configured = ( + self._generation_http_options( + extra_body=extra_body, + extra_headers=extra_headers, + extra_query=extra_query, + timeout=timeout, + ) + ) + if configured is not None: + return configured + return self._build_http_options( + extra_headers=request_headers, + extra_body=merged_body, + extra_query=request_query, + timeout=effective_timeout, + ) + + @staticmethod + def _convert_tools(tools: list[dict[str, Any]] | None) -> list[Any] | None: + if not tools: + return None + + converted_tools: list[Any] = [] + declarations = [] + for tool in tools: + if not isinstance(tool, dict): + converted_tools.append(tool) + continue + native_keys = { + "retrieval", "computer_use", "file_search", "google_search", "google_maps", + "code_execution", "enterprise_web_search", "google_search_retrieval", + "parallel_ai_search", "url_context", "mcp_servers", "exa_ai_search", + } + type_name = tool.get("type") + type_aliases = { + "google_search": "google_search", + "code_execution": "code_execution", + "url_context": "url_context", + "file_search": "file_search", + } + if type_name in type_aliases: + native_value = tool.get(type_name, {}) + if native_value is True or native_value is None: + native_value = {} + converted_tools.append(types.Tool(**{type_aliases[type_name]: native_value})) + continue + if native_keys.intersection(tool): + converted_tools.append(types.Tool(**tool)) + continue + function = tool.get("function", tool) + name = function.get("name") + if not name: + raise ValueError("Google 工具定义缺少 function.name。") + declarations.append( + types.FunctionDeclaration( + name=name, + description=function.get("description"), + parameters_json_schema=function.get( + "parameters", {"type": "object", "properties": {}} + ), + ) + ) + if declarations: + converted_tools.insert(0, types.Tool(function_declarations=declarations)) + return converted_tools + + @staticmethod + def _convert_tool_choice(tool_choice: Any) -> Any: + if tool_choice is None: + return None + if isinstance(tool_choice, dict) and "function_calling_config" in tool_choice: + return types.ToolConfig(**tool_choice) + if isinstance(tool_choice, dict): + if tool_choice.get("type") == "function": + function = tool_choice.get("function", {}) + if not isinstance(function, Mapping): + raise ValueError("Google function tool_choice 的 function 必须是对象。") + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError("Google function tool_choice 缺少 function.name。") + return types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=types.FunctionCallingConfigMode.ANY, + allowed_function_names=[name], + ) + ) + tool_choice = tool_choice.get("type", tool_choice.get("mode", "AUTO")) + if isinstance(tool_choice, str): + mode = {"auto": "AUTO", "required": "ANY", "any": "ANY", "none": "NONE"}.get( + tool_choice.lower(), tool_choice.upper() + ) + return types.ToolConfig( + function_calling_config=types.FunctionCallingConfig( + mode=types.FunctionCallingConfigMode(mode) + ) + ) + return tool_choice + + def _build_generation_config( + self, + system_prompt: str | None, + tools: list[dict[str, Any]] | None, + temperature: float | None, + *, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + candidate_count: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + seed: int | None = None, + modalities: list[str] | None = None, + stop: Sequence[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + timeout: float | None = None, + ) -> Any: + values: dict[str, Any] = self._generation_options() + configured_max_tokens = self._options.get("max_tokens") + configured_max_output_tokens = values.get("max_output_tokens") + if configured_max_tokens is not None and configured_max_output_tokens is not None: + raise ValueError( + "Google options.max_tokens 与 options.max_output_tokens 重复。" + ) + if configured_max_tokens is not None: + values["max_output_tokens"] = configured_max_tokens + configured_thinking = self._options.get("thinking") + configured_thinking_config = values.get("thinking_config") + if configured_thinking is not None and configured_thinking_config is not None: + raise ValueError( + "Google options.thinking 与 options.thinking_config 重复。" + ) + if configured_thinking is not None: + values["thinking_config"] = configured_thinking + configured_response_format = self._options.get("response_format") + if configured_response_format is not None: + if response_format is not None: + raise ValueError("Google response_format 与模型 options 重复。") + response_format = configured_response_format + values.update({ + "system_instruction": system_prompt, + "tools": self._convert_tools(tools), + }) + if temperature is not None or "temperature" not in values: + values["temperature"] = 0.7 if temperature is None else temperature + if max_tokens is not None: + values["max_output_tokens"] = max_tokens + if top_p is not None: + values["top_p"] = top_p + if top_k is not None: + values["top_k"] = top_k + if candidate_count is not None: + values["candidate_count"] = candidate_count + if frequency_penalty is not None: + values["frequency_penalty"] = frequency_penalty + if presence_penalty is not None: + values["presence_penalty"] = presence_penalty + if seed is not None: + values["seed"] = seed + if modalities is not None: + values["response_modalities"] = modalities + if stop is not None: + values["stop_sequences"] = [stop] if isinstance(stop, str) else stop + if response_format is not None: + values.update(self._response_format_values(response_format)) + if tool_choice is not None: + values["tool_config"] = self._convert_tool_choice(tool_choice) + if thinking is not None: + values["thinking_config"] = thinking + if service_tier is not None: + values["service_tier"] = service_tier + merged_body, request_headers, request_query, effective_timeout, configured_http_options = ( + self._generation_http_options( + extra_body=extra_body, + extra_headers=extra_headers, + extra_query=extra_query, + timeout=timeout, + ) + ) + if configured_http_options is not None: + values["http_options"] = configured_http_options + else: + http_options = self._build_http_options( + extra_headers=request_headers, + extra_body=merged_body, + extra_query=request_query, + timeout=effective_timeout, + ) + if http_options is not None: + values["http_options"] = http_options + return types.GenerateContentConfig(**{key: value for key, value in values.items() if value is not None}) + + @staticmethod + def _decode_data_uri(value: str, context: str) -> tuple[str, bytes]: + """严格解码 data URI,避免 ``b64decode`` 静默丢弃非法字符。""" + if "," not in value: + raise ValueError(f"Google {context} 的 data URI 缺少逗号分隔符。") + header, encoded = value.split(",", 1) + mime_type = header.split(";", 1)[0].removeprefix("data:") + if not mime_type: + raise ValueError(f"Google {context} 的 data URI 缺少 MIME 类型。") + try: + decoded = base64.b64decode(encoded, validate=True) + except (binascii.Error, ValueError) as exc: + raise ValueError(f"Google {context} 的 data URI 不是有效 Base64。") from exc + return mime_type, decoded + + @staticmethod + def _contents( + messages: Any, + prompt: str | None, + system_prompt: str | None, + *, + vertex_mode: bool | None = None, + ) -> Any: + """将统一消息转换为 Gemini 内容,并按客户端模式校验媒体 URI。 + + Developer API 支持 Gemini Files API 的 ``file_id`` 映射;Vertex/Enterprise + 只接受 Cloud Storage URI 或可公开读取的 HTTPS URI。模式未知时保持 + Developer API 的历史兼容行为,实际 provider 调用会传入 SDK 检测结果。 + """ + normalized = normalize_messages(prompt, system_prompt, messages) + if messages is None and prompt is not None: + return prompt + contents: list[Any] = [] + tool_call_names: dict[str, str] = {} + pending_tool_parts: list[Any] = [] + + def append_content(role: str, parts: list[Any]) -> None: + """合并连续同角色消息,满足 Gemini user/model 交替约束。""" + if contents and field(contents[-1], "role") == role: + contents[-1].parts.extend(parts) + return + contents.append(types.Content(role=role, parts=parts)) + + def file_uri(value: Any, context: str) -> str: + """规范化媒体 URI,并拒绝当前端点不支持的凭证引用。""" + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"Google {context} 缺少有效 URI。") + normalized = value.strip() + if normalized.startswith("gs://"): + return normalized + google_files_prefix = "https://generativelanguage.googleapis.com/v1beta/files/" + file_name = normalized.removeprefix("files/") + if normalized.startswith("files/") or "://" not in normalized: + if ( + not file_name + or "/" in file_name + or not re.fullmatch(r"[A-Za-z0-9._~-]+", file_name) + ): + raise ValueError(f"Google {context} 的 file_id 无效。") + if vertex_mode is True: + raise ValueError( + f"Google {context} 在 Vertex/Enterprise 模式不支持 Gemini Files file_id;" + "请使用 gs:// URI 或可公开访问的 HTTPS URI。" + ) + return google_files_prefix + quote(file_name, safe="-._~") + try: + parsed = urlsplit(normalized) + except ValueError as exc: + raise ValueError(f"Google {context} URI 无效。") from exc + is_google_files_uri = ( + parsed.scheme.lower() == "https" + and parsed.hostname + and parsed.hostname.lower() == "generativelanguage.googleapis.com" + and re.search(r"/files/[A-Za-z0-9._~-]+$", parsed.path) + and not parsed.query + and not parsed.fragment + ) + if is_google_files_uri: + if vertex_mode is True: + raise ValueError( + f"Google {context} 在 Vertex/Enterprise 模式不支持 Gemini Files URI;" + "请使用 gs:// URI 或可公开访问的 HTTPS URI。" + ) + return normalized + if ( + vertex_mode is True + and parsed.scheme.lower() == "https" + and parsed.hostname + and parsed.username is None + and parsed.password is None + and not parsed.fragment + ): + # Vertex accepts externally hosted media only when the service + # can fetch it. We cannot prove public reachability locally, + # but reject malformed/userinfo URLs before handing them to + # the SDK; the service remains the authority on accessibility. + return normalized + if vertex_mode is True: + raise ValueError( + f"Google {context} 在 Vertex/Enterprise 模式仅支持 gs:// URI 或可公开访问的 HTTPS URI。" + ) + raise ValueError( + f"Google {context} 仅支持 data: URI、gs:// URI 或 Google Files URI,也可传 file_id。" + ) + + def media_mime_type(*sources: Any) -> str | None: + """读取媒体块显式提供的 MIME 类型,保持 SDK 的 MIME 透传。""" + for source in sources: + if not isinstance(source, Mapping): + continue + value = source.get( + "mime_type", + source.get("mimeType", source.get("media_type")), + ) + if value is None: + continue + if not isinstance(value, str) or not value.strip(): + raise ValueError("Google 媒体内容的 mime_type 必须是非空字符串。") + return value.strip() + return None + + def file_data(value: Any, context: str, mime_type: str | None = None) -> Any: + values: dict[str, Any] = {"file_uri": file_uri(value, context)} + if mime_type is not None: + values["mime_type"] = mime_type + return types.FileData(**values) + + def function_response_value(value: Any) -> Any: + if isinstance(value, str): + stripped = value.strip() + # Tool results are often plain text. Attempt JSON decoding for + # JSON-shaped values so malformed structured results still fail + # loudly, while preserving ordinary text as a valid output. + numeric_json = bool( + stripped + and stripped[0] in "-0123456789" + and re.fullmatch( + r"-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?", + stripped, + ) + ) + looks_like_json = ( + stripped.startswith(("{", "[", '"')) + or stripped in {"true", "false", "null"} + or numeric_json + ) + if not looks_like_json: + return {"output": value} + try: + parsed = json.loads(value) + except json.JSONDecodeError as exc: + raise ValueError("Google 工具结果的 content 不是有效 JSON。") from exc + return dict(parsed) if isinstance(parsed, Mapping) else {"output": parsed} + if isinstance(value, Mapping): + return dict(value) + return {"output": value} + + for message in normalized: + if message.get("role") == "system": + continue + message_role = message.get("role") + if message_role == "tool": + tool_call_id = message.get("tool_call_id") or message.get("id") + explicit_name = message.get("name") or message.get("tool_name") + name = explicit_name + if not name and isinstance(tool_call_id, str): + name = tool_call_names.get(tool_call_id) + if not name: + raise ValueError( + "Google 工具结果缺少可关联的 tool_call_id/name。" + ) + raw_content = message.get("content", message.get("response", {})) + response_parts = raw_content if isinstance(raw_content, list) else [raw_content] + parts = [] + for response_part in response_parts: + if isinstance(response_part, dict) and response_part.get("type") in { + "tool_result", "function_result", "function_response" + }: + response = response_part.get( + "response", response_part.get("content", {}) + ) + part_name = response_part.get("name") or name + part_id = response_part.get("tool_call_id") or tool_call_id + else: + response = response_part + part_name = name + part_id = tool_call_id + response_value = function_response_value(response) + if part_id is not None: + parts.append( + types.Part( + function_response=types.FunctionResponse( + id=part_id, + name=part_name, + response=response_value, + ) + ) + ) + else: + parts.append( + types.Part( + function_response=types.FunctionResponse( + name=part_name, + response=response_value, + ) + ) + ) + # Gemini represents parallel function results as one user turn. + # Keep consecutive standard ``role=tool`` messages together so + # the model receives a single FunctionResponse content block. + pending_tool_parts.extend(parts) + continue + + if pending_tool_parts: + append_content("user", pending_tool_parts) + pending_tool_parts = [] + + role = "model" if message_role == "assistant" else "user" + raw_content = message.get("content", "") + if raw_content is None: + parts = [] + elif isinstance(raw_content, str): + parts = [types.Part(text=raw_content)] + elif isinstance(raw_content, list): + parts = [] + for part in raw_content: + if isinstance(part, str): + parts.append(types.Part(text=part)) + continue + if not isinstance(part, dict): + parts.append(part) + continue + part_type = part.get("type") + if part_type in {"text", "input_text"}: + parts.append(types.Part(text=part.get("text", ""))) + elif part_type in {"image_url", "input_image"}: + image_value = part.get("image_url") + image_file_id = part.get("file_id") + if isinstance(image_value, Mapping): + image_url = image_value.get( + "url", + image_value.get("image_url", image_value.get("file_uri")), + ) + image_file_id = image_file_id or image_value.get("file_id") + else: + image_url = image_value or part.get("url") + if image_url is None and image_file_id is not None: + image_url = image_file_id + if not isinstance(image_url, str) or not image_url.strip(): + raise ValueError("Google 图片内容缺少有效 URI。") + image_url = image_url.strip() + if isinstance(image_url, str) and image_url.startswith("data:"): + mime_type, data = GoogleProvider._decode_data_uri(image_url, "图片") + parts.append( + types.Part( + inline_data=types.Blob(mime_type=mime_type, data=data) + ) + ) + else: + parts.append( + types.Part( + file_data=file_data( + image_url, + "图片", + media_mime_type(part, image_value), + ) + ) + ) + elif part_type in {"audio_url", "input_audio", "video_url", "input_video", "file"}: + media_value = part.get( + "audio_url", + part.get("video_url", part.get("url", part.get("file_uri"))), + ) + media_file_id = part.get("file_id") + if part_type == "file" and media_value is None: + media_value = part.get("file") + if isinstance(media_value, Mapping): + media = media_value.get( + "url", + media_value.get( + "file_uri", + media_value.get("audio_url", media_value.get("video_url")), + ), + ) + media_file_id = media_file_id or media_value.get("file_id") + else: + media = media_value + if media is None and media_file_id is not None: + media = media_file_id + if not isinstance(media, str) or not media.strip(): + raise ValueError("Google 媒体内容缺少有效 URI。") + media = media.strip() + if isinstance(media, str) and media.startswith("data:"): + mime_type, data = GoogleProvider._decode_data_uri(media, "媒体") + parts.append( + types.Part( + inline_data=types.Blob( + mime_type=mime_type, + data=data, + ) + ) + ) + else: + parts.append( + types.Part( + file_data=file_data( + media, + "媒体", + media_mime_type(part, media_value), + ) + ) + ) + elif part_type in {"tool_result", "function_result"}: + part_id = part.get("tool_call_id") or part.get("id") + part_name = part.get("name") or part.get("tool_name") + if not part_name and isinstance(part_id, str): + part_name = tool_call_names.get(part_id) + if not isinstance(part_name, str) or not part_name.strip(): + raise ValueError( + "Google 工具结果缺少可关联的 tool_call_id/name。" + ) + parts.append( + types.Part( + function_response=types.FunctionResponse( + id=part_id, + name=part_name, + response=function_response_value( + part.get("response", part.get("content", {})) + ), + ) + ) + ) + elif part_type == "function_call": + call_id = part.get("id") or part.get("call_id") + function_name = part.get("name") + if not isinstance(function_name, str) or not function_name.strip(): + raise ValueError("Google function_call 缺少 name。") + if call_id and function_name: + tool_call_names[call_id] = function_name + arguments = part.get("arguments", part.get("args", {})) + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError as exc: + raise ValueError( + "Google function_call 的 arguments 不是有效 JSON。" + ) from exc + if not isinstance(arguments, Mapping): + raise ValueError("Google function_call 的 arguments 必须是 JSON 对象。") + parts.append(types.Part(function_call=types.FunctionCall( + id=call_id, name=function_name, args=dict(arguments) + ))) + elif part_type == "function_response": + part_id = part.get("tool_call_id") or part.get("id") + part_name = part.get("name") or part.get("tool_name") + if not part_name and isinstance(part_id, str): + part_name = tool_call_names.get(part_id) + if not isinstance(part_name, str) or not part_name.strip(): + raise ValueError( + "Google function_response 缺少可关联的 tool_call_id/name。" + ) + parts.append(types.Part(function_response=types.FunctionResponse( + id=part_id, + name=part_name, + response=function_response_value(part.get("response", {})), + ))) + else: + raise ValueError( + f"Google 不支持的消息内容块类型: {part_type!r}。" + ) + else: + parts = [types.Part(text=str(raw_content))] + for index, call in enumerate(message.get("tool_calls", []) or []): + if not isinstance(call, Mapping): + raise ValueError(f"Google assistant tool_calls[{index}] 必须是对象。") + function = call.get("function", call) + if not isinstance(function, Mapping): + raise ValueError( + f"Google assistant tool_calls[{index}].function 必须是对象。" + ) + call_id = call.get("id") or call.get("call_id") + function_name = function.get("name", "") + if not isinstance(function_name, str) or not function_name.strip(): + raise ValueError( + f"Google assistant tool_calls[{index}] 缺少 function.name。" + ) + if call_id and function_name: + tool_call_names[call_id] = function_name + arguments = function.get("arguments", function.get("args", {})) + if isinstance(arguments, str): + try: + arguments = json.loads(arguments) + except json.JSONDecodeError as exc: + raise ValueError( + f"Google assistant tool_calls[{index}] 的 arguments 不是有效 JSON。" + ) from exc + if not isinstance(arguments, Mapping): + raise ValueError("Google assistant tool call 的 arguments 必须是 JSON 对象。") + parts.append( + types.Part( + function_call=types.FunctionCall( + id=call_id, + name=function.get("name", ""), args=dict(arguments) + ) + ) + ) + append_content(role, parts) + if pending_tool_parts: + append_content("user", pending_tool_parts) + return contents + + @staticmethod + def _response_text(response: Any) -> str: + """读取文本而不触发 Google SDK 纯工具响应的 ValueError。""" + candidates = field(response, "candidates", []) or [] + if candidates: + chunks: list[str] = [] + saw_parts = False + for candidate in candidates: + content = field(candidate, "content") + for part in field(content, "parts", []) or []: + saw_parts = True + if field(part, "thought", False): + continue + text = field(part, "text", "") + if isinstance(text, str): + chunks.append(text) + if saw_parts: + return "".join(chunks) + try: + value = field(response, "text", "") + except Exception: # noqa: BLE001 - SDK response accessors are dynamic + value = "" + if isinstance(value, str): + return value + chunks = [] + for candidate in candidates: + content = field(candidate, "content") + for part in field(content, "parts", []) or []: + if field(part, "thought", False): + continue + text = field(part, "text", "") + if isinstance(text, str): + chunks.append(text) + return "".join(chunks) + + _REFUSAL_FINISH_REASONS = frozenset( + { + "SAFETY", + "BLOCKLIST", + "PROHIBITED_CONTENT", + "SPII", + "RECITATION", + "IMAGE_SAFETY", + "MODEL_ARMOR", + } ) + + @classmethod + def _candidate_refusal(cls, candidate: Any) -> str | None: + """读取 Gemini 内容块或安全拦截元数据中的拒答原因。""" + content = field(candidate, "content") + for part in field(content, "parts", []) or []: + value = field(part, "refusal") + if not value and field(part, "type") == "refusal": + value = field(part, "text") or field(part, "reason") + if isinstance(value, str) and value: + return value + + for key in ("finish_message", "block_reason_message"): + value = field(candidate, key) + if isinstance(value, str) and value: + return value + + finish_reason = field(candidate, "finish_reason") + normalized_reason = cls._normalize_finish_reason(finish_reason) or "" + if normalized_reason in cls._REFUSAL_FINISH_REASONS: + return f"Google 内容被安全策略拦截: {normalized_reason}" + return None + + @staticmethod + def _normalize_finish_reason(value: Any) -> str | None: + """将 google-genai 枚举或原始值统一为协议中的字符串。""" + if value is None: + return None + name = getattr(value, "name", None) + if isinstance(name, str) and name.strip(): + return name.strip().upper() + raw = getattr(value, "value", value) + text = str(raw).strip() + if not text: + return None + if "." in text: + text = text.rsplit(".", 1)[-1] + return text.upper() + + @classmethod + def _extract_refusal(cls, response: Any) -> str | None: + """提取 Gemini 响应中的显式拒答或安全拦截信息。""" + for candidate in field(response, "candidates", []) or []: + refusal = cls._candidate_refusal(candidate) + if refusal: + return refusal + feedback = field(response, "prompt_feedback") + block_reason = field(feedback, "block_reason") + if isinstance(block_reason, Enum): + block_reason_text = block_reason.name or str(block_reason) + elif isinstance(block_reason, str): + block_reason_text = block_reason.strip() + else: + block_reason_text = "" + if block_reason_text: + message = field(feedback, "block_reason_message") + if not isinstance(message, str) or not message.strip(): + message = field(feedback, "message") + return str(message or f"Google 提示被安全策略拦截: {block_reason_text}") + return None + + @staticmethod + def _partial_arg_value(partial: Any) -> Any: + """读取 Vertex ``PartialArg`` 的标量值。""" + for name in ("bool_value", "number_value", "string_value"): + value = field(partial, name) + if value is not None: + return value + if field(partial, "null_value") is not None: + return None + raise ValueError("Google FunctionCall.partial_args 缺少有效值。") + + @staticmethod + def _partial_arg_tokens(json_path: str) -> list[str | int]: + """解析 Vertex PartialArg 使用的常见 JSONPath 子集。""" + if not isinstance(json_path, str) or not json_path.startswith("$"): + raise ValueError(f"Google FunctionCall.partial_args 的 json_path 无效: {json_path!r}") + if json_path in {"", "$"}: + return [] + rest = json_path[1:] + tokens: list[str | int] = [] + while rest: + dotted = re.match(r"^\.([A-Za-z_][A-Za-z0-9_]*)", rest) + if dotted: + tokens.append(dotted.group(1)) + rest = rest[dotted.end():] + continue + indexed = re.match(r"^\[(\d+)\]", rest) + if indexed: + tokens.append(int(indexed.group(1))) + rest = rest[indexed.end():] + continue + quoted = re.match(r"^\[['\"]([^'\"]+)['\"]\]", rest) + if quoted: + tokens.append(quoted.group(1)) + rest = rest[quoted.end():] + continue + raise ValueError( + f"Google FunctionCall.partial_args 不支持的 json_path: {json_path!r}" + ) + return tokens + + @classmethod + def _partial_args_to_mapping(cls, partial_args: Any) -> dict[str, Any]: + """将 Vertex 的 PartialArg 列表还原为普通 JSON 对象。""" + if isinstance(partial_args, Mapping): + return dict(partial_args) + if not isinstance(partial_args, (list, tuple)): + raise ValueError("Google FunctionCall.partial_args 必须是列表。") + result: dict[str, Any] = {} + for partial in partial_args: + json_path = field(partial, "json_path") + tokens = cls._partial_arg_tokens(json_path or "$") + value = cls._partial_arg_value(partial) + if not tokens: + if not isinstance(value, Mapping): + raise ValueError( + "Google FunctionCall.partial_args 的根值必须是 JSON 对象。" + ) + result.update(dict(value)) + continue + current: Any = result + for position, token in enumerate(tokens): + last = position == len(tokens) - 1 + if isinstance(token, int): + if not isinstance(current, list) or token < 0: + raise ValueError( + f"Google FunctionCall.partial_args 的 json_path 无法写入: {json_path!r}" + ) + while len(current) <= token: + current.append(None) + if last: + current[token] = value + elif current[token] is None: + current[token] = [] if isinstance(tokens[position + 1], int) else {} + current = current[token] + else: + if not isinstance(current, dict): + raise ValueError( + f"Google FunctionCall.partial_args 的 json_path 无法写入: {json_path!r}" + ) + if last: + current[token] = value + else: + current.setdefault( + token, + [] if isinstance(tokens[position + 1], int) else {}, + ) + current = current[token] + return result + + @classmethod + def _function_call_arguments(cls, function_call: Any) -> Any: + """统一读取完整 args 与 Vertex 流式 partial_args。""" + arguments = field(function_call, "args") + if arguments is not None: + return arguments + partial_args = field(function_call, "partial_args") + if partial_args is not None: + return cls._partial_args_to_mapping(partial_args) + return {} + + @staticmethod + def _function_call_index(function_call: Any) -> Any: + """读取仅由兼容字典载荷提供的工具调用 index。 + + ``google-genai`` 的 ``types.FunctionCall`` 没有 index 字段;并行 + 调用的 index 不能用 Candidate.index 代替,因为后者标识候选答案, + 不是候选内部的工具调用。保留字典读取仅用于中转网关的扩展载荷。 + """ + return function_call.get("index") if isinstance(function_call, Mapping) else None + + @classmethod + def _merge_tool_arguments(cls, existing: Any, incoming: Any) -> Any: + """递归合并工具参数分片,保留不同分片中的嵌套字段。""" + if isinstance(existing, Mapping) and isinstance(incoming, Mapping): + merged_mapping: dict[Any, Any] = dict(existing) + for key, value in incoming.items(): + merged_mapping[key] = ( + cls._merge_tool_arguments(merged_mapping[key], value) + if key in merged_mapping + else value + ) + return merged_mapping + if isinstance(existing, list) and isinstance(incoming, list): + merged_list: list[Any] = list(existing) + for index, value in enumerate(incoming): + if index >= len(merged_list): + merged_list.extend([None] * (index + 1 - len(merged_list))) + if value is not None: + merged_list[index] = ( + cls._merge_tool_arguments(merged_list[index], value) + if merged_list[index] is not None + else value + ) + return merged_list + return incoming + + @classmethod + def _merge_tool_call( + cls, + accumulator: dict[Any, dict[str, Any]], + tool_call: Mapping[str, Any], + fallback_index: Any = None, + *, + merge_arguments: bool = True, + ) -> tuple[Any, dict[str, Any]]: + """累积 Gemini FunctionCall 分片,兼容字典参数与字符串参数。 + + ``tool_call_completed`` 有时会紧跟同一个 chunk 的 delta,并重复携带 + 完整 arguments。完成事件只需要复用已累积的元数据;调用方可将 + ``merge_arguments`` 设为 ``False``,避免字符串参数被追加两次。 + """ + call_id = tool_call.get("id") or tool_call.get("call_id") + index = tool_call.get("index", fallback_index) + name = tool_call.get("name") + + def has_identity(candidate: Mapping[str, Any]) -> bool: + return bool(candidate.get("id") or candidate.get("call_id")) + + def candidate_matches_id(candidate: Mapping[str, Any]) -> bool: + candidate_id = candidate.get("id") or candidate.get("call_id") + return bool(call_id and candidate_id == call_id) + + def name_matches(candidate: Mapping[str, Any]) -> bool: + candidate_name = candidate.get("name") + return name is None or candidate_name in {None, name} + + def move_candidate(candidate_key: Any, target_key: Any) -> Any: + if candidate_key == target_key: + return target_key + if target_key in accumulator: + raise RuntimeError( + "Google Gemini 工具调用分片的 id/index 指向多个调用。" + ) + accumulator[target_key] = accumulator.pop(candidate_key) + return target_key + + # google-genai's FunctionCall does not expose an index in the current + # SDK. Never synthesize one from a part position: that position resets + # in every response chunk and would merge independent parallel calls. + # An explicit index is the most stable stream identity. If a later + # fragment supplies an id, attach it to the existing indexed record + # instead of moving the accumulated arguments to a new id key. + if index is not None: + candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if candidate.get("index") == index and name_matches(candidate) + ] + if len(candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片的 index 指向多个调用。" + ) + if candidates: + key = candidates[0] + else: + # A provider may emit the id first and the index only on a + # later fragment. Match that id across the whole accumulator + # before falling back to a name-only, index-less record. + id_candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if candidate_matches_id(candidate) + ] + if len(id_candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片的 id 指向多个调用。" + ) + if id_candidates: + key = move_candidate(id_candidates[0], ("index", index)) + else: + candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if candidate.get("index") is None and name_matches(candidate) + ] + if len(candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片缺少稳定 id/index,无法关联并行调用。" + ) + if len(candidates) == 1: + key = move_candidate(candidates[0], ("index", index)) + else: + key = ("index", index) + elif call_id: + exact_key = ("id", call_id) + id_candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if candidate_matches_id(candidate) + ] + if len(id_candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片的 id 指向多个调用。" + ) + if id_candidates: + # Keep an existing explicit index as the accumulator key. This + # handles streams where the first fragment has both id/index + # and later fragments repeat only the id. + key = id_candidates[0] + else: + candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if not has_identity(candidate) and name_matches(candidate) + ] + if len(candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片缺少稳定 id/index,无法关联并行调用。" + ) + if len(candidates) == 1: + # Preserve an existing explicit index. A position-only + # record has no stronger identity, so promote it to id. + candidate_key = candidates[0] + candidate = accumulator[candidate_key] + key = ( + candidate_key + if candidate.get("index") is not None + else move_candidate(candidate_key, exact_key) + ) + else: + key = exact_key + else: + candidates = [ + candidate_key + for candidate_key, candidate in accumulator.items() + if name_matches(candidate) + ] + if len(candidates) > 1: + raise RuntimeError( + "Google Gemini 工具调用分片缺少稳定 id/index,无法关联并行调用。" + ) + if candidates: + candidate = accumulator[candidates[0]] + if ( + has_identity(candidate) + or tool_call.get("will_continue") is True + or candidate.get("will_continue") is True + ): + key = candidates[0] + else: + raise RuntimeError( + "Google Gemini 工具调用分片缺少稳定 id/index,无法确认是否为同一调用。" + ) + else: + position = len(accumulator) + key = ("position", position) + while key in accumulator: + position += 1 + key = ("position", position) + merged = accumulator.setdefault( + key, + {"type": "function", "arguments": {}}, + ) + existing_id = merged.get("id") or merged.get("call_id") + if call_id and existing_id and existing_id != call_id: + raise RuntimeError( + "Google Gemini 工具调用分片的 id 指向多个调用。" + ) + existing_index = merged.get("index") + if index is not None and existing_index is not None and existing_index != index: + raise RuntimeError( + "Google Gemini 工具调用分片的 index 指向多个调用。" + ) + existing_name = merged.get("name") + if name and existing_name and existing_name != name: + raise RuntimeError( + "Google Gemini 工具调用分片的 name 指向多个调用。" + ) + for name in ("id", "call_id", "type", "name", "index", "will_continue"): + value = tool_call.get(name) + if value is not None and value != "": + merged[name] = value + if merge_arguments: + arguments = tool_call.get("arguments") + if isinstance(arguments, Mapping): + existing = merged.get("arguments") + if not isinstance(existing, Mapping): + existing = {} + merged["arguments"] = cls._merge_tool_arguments(existing, arguments) + elif isinstance(arguments, str) and arguments: + existing = merged.get("arguments") + merged["arguments"] = (existing if isinstance(existing, str) else "") + arguments + elif arguments is not None: + merged["arguments"] = arguments + return key, merged + + @staticmethod + def _complete_stream_tool_calls( + accumulator: dict[Any, dict[str, Any]], + completed: set[Any], + response_id: str | None, + raw: Any, + ) -> list[StreamEvent]: + events: list[StreamEvent] = [] + for key, tool_call in accumulator.items(): + if key in completed: + continue + validate_complete_tool_call(tool_call, "Google Gemini") + completed.add(key) + events.append( + StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=response_id, + raw=raw, + ) + ) + return events + + @staticmethod + def _extract_result(response: Any) -> CompletionResult: + usage = field(response, "usage_metadata") or field(response, "usage") + usage_dict = normalize_usage(usage) + calls = [] + reasoning: list[str] = [] + for candidate in field(response, "candidates", []) or []: + content = field(candidate, "content") + for part in field(content, "parts", []) or []: + function_call = field(part, "function_call") + if function_call: + calls.append( + { + "id": field(function_call, "id"), + "type": "function", + "name": field(function_call, "name"), + "arguments": GoogleProvider._function_call_arguments(function_call), + } + ) + if field(part, "thought") and isinstance(field(part, "text"), str): + reasoning.append(field(part, "text")) + return CompletionResult( + text=GoogleProvider._response_text(response), + tool_calls=calls, + usage=usage_dict, + finish_reason=GoogleProvider._normalize_finish_reason( + field((field(response, "candidates", []) or [None])[0], "finish_reason") + ), + response_id=field(response, "response_id"), + refusal=GoogleProvider._extract_refusal(response), + reasoning="".join(reasoning), + raw=response, + ) + + @classmethod + def _stream_events_from_chunk(cls, chunk: Any) -> list[StreamEvent]: + events: list[StreamEvent] = [] + response_id = field(chunk, "response_id") + for candidate in field(chunk, "candidates", []) or []: + content = field(candidate, "content") + parts = field(content, "parts", []) or [] + refusal_emitted = False + for part in parts: + function_call = field(part, "function_call") + if function_call: + tool_call = { + "id": field(function_call, "id"), + "index": cls._function_call_index(function_call), + "type": "function", + "name": field(function_call, "name"), + "arguments": cls._function_call_arguments(function_call), + } + events.append( + StreamEvent( + type="tool_call_delta", + tool_call=tool_call, + response_id=response_id, + raw=chunk, + ) + ) + # Without id/index there is no safe way to distinguish an + # immediate completion event from a duplicate delta. The + # stream accumulator emits the completed call at finish, + # where the same ambiguity is checked explicitly. + if ( + field(function_call, "will_continue") is False + and ( + field(function_call, "id") + or field(function_call, "call_id") + or cls._function_call_index(function_call) is not None + ) + ): + events.append( + StreamEvent( + type="tool_call_completed", + tool_call=tool_call, + response_id=response_id, + raw=chunk, + ) + ) + + part_type = field(part, "type") + part_refusal = field(part, "refusal") + if not part_refusal and part_type == "refusal": + part_refusal = field(part, "text") or field(part, "reason") + if isinstance(part_refusal, str) and part_refusal: + events.append( + StreamEvent( + type="refusal_delta", + refusal=part_refusal, + response_id=response_id, + raw=chunk, + ) + ) + refusal_emitted = True + continue + + text = field(part, "text") + if isinstance(text, str) and text: + if field(part, "thought"): + events.append( + StreamEvent( + type="reasoning_delta", + reasoning=text, + response_id=response_id, + raw=chunk, + ) + ) + else: + events.append( + StreamEvent( + type="text_delta", + text=text, + response_id=response_id, + raw=chunk, + ) + ) + + candidate_refusal = cls._candidate_refusal(candidate) + if candidate_refusal and not refusal_emitted: + events.append( + StreamEvent( + type="refusal_delta", + refusal=candidate_refusal, + response_id=response_id, + raw=chunk, + ) + ) + finish_reason = field(candidate, "finish_reason") + if finish_reason: + events.append( + StreamEvent( + type="finish", + finish_reason=cls._normalize_finish_reason(finish_reason), + response_id=response_id, + raw=chunk, + ) + ) + + # Safety blocks can arrive as ``prompt_feedback`` with no candidates. + # Treat that metadata as an explicit refusal and terminal event instead + # of allowing an empty stream to look like a successful response. + feedback = field(chunk, "prompt_feedback") + block_reason = field(feedback, "block_reason") + if block_reason: + message = field(feedback, "block_reason_message") or field(feedback, "message") + refusal = str(message or f"Google 提示被安全策略拦截: {block_reason}") + if not any(event.type == "refusal_delta" for event in events): + events.append( + StreamEvent( + type="refusal_delta", + refusal=refusal, + response_id=response_id, + raw=chunk, + ) + ) + if not any(event.type == "finish" for event in events): + events.append( + StreamEvent( + type="finish", + finish_reason=cls._normalize_finish_reason(block_reason), + response_id=response_id, + raw=chunk, + ) + ) + top_level_finish_reason = field(chunk, "finish_reason") + if ( + top_level_finish_reason is not None + and not any(event.type == "finish" for event in events) + ): + events.append( + StreamEvent( + type="finish", + finish_reason=cls._normalize_finish_reason(top_level_finish_reason), + response_id=response_id, + raw=chunk, + ) + ) + usage = normalize_usage(field(chunk, "usage_metadata") or field(chunk, "usage")) + if usage: + events.append(StreamEvent(type="usage", usage=usage, response_id=response_id, raw=chunk)) + return events + + @staticmethod + def _stream_chunk_has_terminal_event(chunk: Any) -> bool: + """判断 Gemini 流块是否明确表示本次生成已经终止。""" + if field(chunk, "finish_reason", None) is not None: + return True + feedback = field(chunk, "prompt_feedback") + if field(feedback, "block_reason", None) is not None: + return True + return any( + field(candidate, "finish_reason", None) is not None + for candidate in field(chunk, "candidates", []) or [] + ) + + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + request = coerce_completion_request(request, kwargs, "Google complete") + self._validate_request(request) + system_prompt = extract_system_prompt(request.messages) or request.system_prompt + config = self._build_generation_config( + system_prompt, + request.tools, + request.temperature if request.temperature_is_explicit else None, + max_tokens=request.max_tokens, + top_p=request.top_p, + top_k=request.top_k, + candidate_count=request.n, + frequency_penalty=request.frequency_penalty, + presence_penalty=request.presence_penalty, + seed=request.seed, + modalities=request.modalities, + stop=request.stop, + response_format=request.response_format, + tool_choice=request.tool_choice, + extra_body=request.extra_body, + extra_headers=request.extra_headers, + extra_query=request.extra_query, + thinking=request.thinking, + service_tier=request.service_tier, + timeout=request.timeout, + ) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + response = retry_sync_call(lambda: client.models.generate_content( + model=self._model_name, + contents=self._contents( + request.messages, + request.prompt, + request.system_prompt, + vertex_mode=vertexai, + ), + config=config, + )) + return self._extract_result(response) + + async def acomplete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """使用 google-genai 异步模型接口聚合完整结果。""" + request = coerce_completion_request(request, kwargs, "Google acomplete") + self._validate_request(request) + effective_system = extract_system_prompt(request.messages) or request.system_prompt + config = self._build_generation_config( + effective_system, + request.tools, + request.temperature if request.temperature_is_explicit else None, + max_tokens=request.max_tokens, + top_p=request.top_p, + top_k=request.top_k, + candidate_count=request.n, + frequency_penalty=request.frequency_penalty, + presence_penalty=request.presence_penalty, + seed=request.seed, + modalities=request.modalities, + stop=request.stop, + response_format=request.response_format, + tool_choice=request.tool_choice, + extra_body=request.extra_body, + extra_headers=request.extra_headers, + extra_query=request.extra_query, + thinking=request.thinking, + service_tier=request.service_tier, + timeout=request.timeout, + ) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + response = await retry_async_call(lambda: client.aio.models.generate_content( + model=self._model_name, + contents=self._contents( + request.messages, + request.prompt, + request.system_prompt, + vertex_mode=vertexai, + ), + config=config, + )) + return self._extract_result(response) + + def count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Google count_tokens") + self._validate_token_count_fields(request) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + config_values: dict[str, Any] = {} + # ``CompletionRequest`` keeps the legacy chat default system prompt for + # normal generation. It must not silently become an explicit + # ``system_instruction`` for a plain Developer API token-count call, + # where that field is unsupported. A caller-provided message-level + # system prompt remains explicit and is still validated below. + configured_system = self._token_count_system_prompt(request) + effective_system = configured_system + if vertexai is False and effective_system: + raise ValueError( + "Gemini Developer API 的 count_tokens 不支持 system_prompt;" + "请设置 system_prompt=None,或改用 Vertex/Enterprise 模式。" + ) + if vertexai is False and request.tools: + raise ValueError( + "Gemini Developer API 的 count_tokens 不支持 tools;请改用 Vertex/Enterprise 模式。" + ) + if vertexai is not False and effective_system: + config_values["system_instruction"] = effective_system + if vertexai is not False and request.tools: + config_values["tools"] = self._convert_tools(request.tools) + http_options = self._http_options_for_request( + extra_headers=request.extra_headers, + extra_body=request.extra_body, + extra_query=request.extra_query, + timeout=request.timeout, + ) + if http_options is not None: + config_values["http_options"] = http_options + contents = self._contents( + request.messages, + request.prompt, + None if vertexai is False else configured_system, + vertex_mode=vertexai, + ) + response = retry_sync_call(lambda: client.models.count_tokens( + model=self._model_name, + contents=contents, + config=types.CountTokensConfig(**config_values) if config_values else None, + )) + value = field(response, "total_tokens") + if value is None: + raise RuntimeError("Google token count 响应缺少 total_tokens。") + return int(value) + + def compute_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + request = coerce_completion_request(request, kwargs, "Google compute_tokens") + self._validate_compute_token_fields(request) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + if vertexai is False: + raise ValueError( + "Google compute_tokens 仅支持 Vertex/Enterprise 模式;" + "Gemini Developer API 不支持该能力。" + ) + effective_system = self._token_count_system_prompt(request) + if vertexai is True and effective_system: + raise ValueError( + "Google compute_tokens 不支持 system_prompt;请在 contents 中显式传入要统计的文本。" + ) + if vertexai is True and request.tools: + raise ValueError( + "Google compute_tokens 不支持 tools;请在 contents 中显式传入要统计的内容。" + ) + http_options = self._http_options_for_request( + extra_headers=request.extra_headers, + extra_body=request.extra_body, + extra_query=request.extra_query, + timeout=request.timeout, + ) + config = types.ComputeTokensConfig(http_options=http_options) if http_options is not None else None + contents = self._contents( + request.messages, + request.prompt, + None, + vertex_mode=vertexai, + ) + return retry_sync_call(lambda: client.models.compute_tokens( + model=self._model_name, + contents=contents, + config=config, + )) + + def upload_file(self, file: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().files.upload( + file=file, + **self._resource_config_kwargs(types.UploadFileConfig, config, kwargs), + ) + + @staticmethod + def _validate_register_files_auth(auth: Any) -> Credentials: + """限制文件注册凭证为 google-auth 的官方 Credentials 对象。""" + if not isinstance(auth, Credentials): + raise ValueError( + "Google register_files auth 必须是 google.auth.credentials.Credentials 对象。" + ) + return auth + + def register_files(self, auth: Any, uris: list[str], config: Any = None, **kwargs: Any) -> Any: + auth = self._validate_register_files_auth(auth) + return self._get_client().files.register_files( + auth=auth, + uris=uris, + **self._resource_config_kwargs(types.RegisterFilesConfig, config, kwargs), + ) + + def get_file(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().files.get( + name=name, + **self._resource_config_kwargs(types.GetFileConfig, config, kwargs), + ) + + def list_files(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().files.list( + **self._resource_config_kwargs(types.ListFilesConfig, config, kwargs), + ) + + def download_file(self, file: Any, config: Any = None, **kwargs: Any) -> bytes: + return self._get_client().files.download( + file=file, + **self._resource_config_kwargs(types.DownloadFileConfig, config, kwargs), + ) + + def delete_file(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().files.delete( + name=name, + **self._resource_config_kwargs(types.DeleteFileConfig, config, kwargs), + ) + + def list_file_search_documents(self, parent: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.documents.list( + parent=parent, + **self._resource_config_kwargs(types.ListDocumentsConfig, config, kwargs), + ) + + def get_file_search_document(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.documents.get( + name=name, + **self._resource_config_kwargs(types.GetDocumentConfig, config, kwargs), + ) + + def delete_file_search_document(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.documents.delete( + name=name, + **self._resource_config_kwargs(types.DeleteDocumentConfig, config, kwargs), + ) + + async def async_upload_file(self, file: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.files.upload( + file=file, + **self._resource_config_kwargs(types.UploadFileConfig, config, kwargs), + )) + + async def async_register_files(self, auth: Any, uris: list[str], config: Any = None, **kwargs: Any) -> Any: + auth = self._validate_register_files_auth(auth) + return await self._resolve_async_result( + self._get_client().aio.files.register_files( + auth=auth, + uris=uris, + **self._resource_config_kwargs(types.RegisterFilesConfig, config, kwargs), + ) + ) + + async def async_get_file(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.files.get( + name=name, + **self._resource_config_kwargs(types.GetFileConfig, config, kwargs), + )) + + async def async_list_files(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.files.list( + **self._resource_config_kwargs(types.ListFilesConfig, config, kwargs), + )) + + async def async_download_file(self, file: Any, config: Any = None, **kwargs: Any) -> bytes: + return await self._resolve_async_result(self._get_client().aio.files.download( + file=file, + **self._resource_config_kwargs(types.DownloadFileConfig, config, kwargs), + )) + + async def async_delete_file(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.files.delete( + name=name, + **self._resource_config_kwargs(types.DeleteFileConfig, config, kwargs), + )) + + async def async_list_file_search_documents(self, parent: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result( + self._get_client().aio.file_search_stores.documents.list( + parent=parent, + **self._resource_config_kwargs(types.ListDocumentsConfig, config, kwargs), + ) + ) + + async def async_get_file_search_document(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result( + self._get_client().aio.file_search_stores.documents.get( + name=name, + **self._resource_config_kwargs(types.GetDocumentConfig, config, kwargs), + ) + ) + + async def async_delete_file_search_document(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result( + self._get_client().aio.file_search_stores.documents.delete( + name=name, + **self._resource_config_kwargs(types.DeleteDocumentConfig, config, kwargs), + ) + ) + + async def async_count_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "Google async_count_tokens") + self._validate_token_count_fields(request) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + config_values: dict[str, Any] = {} + configured_system = self._token_count_system_prompt(request) + effective_system = configured_system + if vertexai is False and effective_system: + raise ValueError( + "Gemini Developer API 的 count_tokens 不支持 system_prompt;" + "请设置 system_prompt=None,或改用 Vertex/Enterprise 模式。" + ) + if vertexai is False and request.tools: + raise ValueError( + "Gemini Developer API 的 count_tokens 不支持 tools;请改用 Vertex/Enterprise 模式。" + ) + if vertexai is not False and effective_system: + config_values["system_instruction"] = effective_system + if vertexai is not False and request.tools: + config_values["tools"] = self._convert_tools(request.tools) + http_options = self._http_options_for_request( + extra_headers=request.extra_headers, + extra_body=request.extra_body, + extra_query=request.extra_query, + timeout=request.timeout, + ) + if http_options is not None: + config_values["http_options"] = http_options + contents = self._contents( + request.messages, + request.prompt, + None if vertexai is False else configured_system, + vertex_mode=vertexai, + ) + response = await retry_async_call(lambda: client.aio.models.count_tokens( + model=self._model_name, + contents=contents, + config=types.CountTokensConfig(**config_values) if config_values else None, + )) + value = field(response, "total_tokens") + if value is None: + raise RuntimeError("Google token count 响应缺少 total_tokens。") + return int(value) + + async def async_compute_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + request = coerce_completion_request(request, kwargs, "Google async_compute_tokens") + self._validate_compute_token_fields(request) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + if vertexai is False: + raise ValueError( + "Google compute_tokens 仅支持 Vertex/Enterprise 模式;" + "Gemini Developer API 不支持该能力。" + ) + effective_system = self._token_count_system_prompt(request) + if vertexai is True and effective_system: + raise ValueError( + "Google compute_tokens 不支持 system_prompt;请在 contents 中显式传入要统计的文本。" + ) + if vertexai is True and request.tools: + raise ValueError( + "Google compute_tokens 不支持 tools;请在 contents 中显式传入要统计的内容。" + ) + http_options = self._http_options_for_request( + extra_headers=request.extra_headers, + extra_body=request.extra_body, + extra_query=request.extra_query, + timeout=request.timeout, + ) + config = types.ComputeTokensConfig(http_options=http_options) if http_options is not None else None + contents = self._contents( + request.messages, + request.prompt, + None, + vertex_mode=vertexai, + ) + return await retry_async_call(lambda: client.aio.models.compute_tokens( + model=self._model_name, + contents=contents, + config=config, + )) + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + request = coerce_completion_request(request, kwargs, "Google stream_events") + request = request.copy_with(stream=True) + self._validate_request(request) + effective_system = extract_system_prompt(request.messages) or request.system_prompt + config = self._build_generation_config( + effective_system, + request.tools, + request.temperature if request.temperature_is_explicit else None, + max_tokens=request.max_tokens, + top_p=request.top_p, + top_k=request.top_k, + candidate_count=request.n, + frequency_penalty=request.frequency_penalty, + presence_penalty=request.presence_penalty, + seed=request.seed, + modalities=request.modalities, + stop=request.stop, + response_format=request.response_format, + tool_choice=request.tool_choice, + extra_body=request.extra_body, + extra_headers=request.extra_headers, + extra_query=request.extra_query, + thinking=request.thinking, + service_tier=request.service_tier, + timeout=request.timeout, + ) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + contents = self._contents( + request.messages, + request.prompt, + request.system_prompt, + vertex_mode=vertexai, + ) + if request.stream: + response = retry_sync_stream( + lambda: client.models.generate_content_stream( + model=self._model_name, contents=contents, config=config + ), + first_event_validator=lambda event: raise_for_stream_error_event( + event, "Google Gemini" + ), + ) + tool_calls: dict[Any, dict[str, Any]] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + for chunk in response: + for event in self._stream_events_from_chunk(chunk): + if event.type == "finish": + terminal_seen = True + yield from self._complete_stream_tool_calls( + tool_calls, + completed_tool_calls, + event.response_id, + event.raw, + ) + yield event + continue + if event.type == "tool_call_delta" and event.tool_call: + key, merged = self._merge_tool_call(tool_calls, event.tool_call) + event.tool_call = dict(merged) + yield event + continue + if event.type == "tool_call_completed" and event.tool_call: + previous_entries = tuple(tool_calls.values()) + key, merged = self._merge_tool_call( + tool_calls, + event.tool_call, + merge_arguments=False, + ) + if not any(merged is entry for entry in previous_entries): + # No matching delta was accumulated. Keep the + # normal merge path so a completion-only event + # still carries its arguments exactly once. + _, merged = self._merge_tool_call(tool_calls, event.tool_call) + if key in completed_tool_calls: + continue + validate_complete_tool_call(merged, "Google Gemini") + completed_tool_calls.add(key) + event.tool_call = dict(merged) + yield event + continue + yield event + if not terminal_seen: + raise RuntimeError("Google Gemini 流在终止事件之前结束。") + return + result = self.complete(request) + if result.text: + yield StreamEvent(type="text_delta", text=result.text, response_id=result.response_id, raw=result.raw) + if result.reasoning: + yield StreamEvent(type="reasoning_delta", reasoning=result.reasoning, raw=result.raw) + if result.refusal: + yield StreamEvent(type="refusal_delta", refusal=result.refusal, response_id=result.response_id, raw=result.raw) + for call in result.tool_calls: + yield StreamEvent(type="tool_call_delta", tool_call=call, raw=result.raw) + yield StreamEvent(type="tool_call_completed", tool_call=call, response_id=result.response_id, raw=result.raw) + if result.usage: + yield StreamEvent(type="usage", usage=result.usage, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="finish", finish_reason=result.finish_reason, response_id=result.response_id, raw=result.raw) + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + request = coerce_completion_request(request, kwargs, "Google astream_events") + request = request.copy_with(stream=True) + self._validate_request(request) + effective_system = extract_system_prompt(request.messages) or request.system_prompt + config = self._build_generation_config( + effective_system, + request.tools, + request.temperature if request.temperature_is_explicit else None, + max_tokens=request.max_tokens, + top_p=request.top_p, + top_k=request.top_k, + candidate_count=request.n, + frequency_penalty=request.frequency_penalty, + presence_penalty=request.presence_penalty, + seed=request.seed, + modalities=request.modalities, + stop=request.stop, + response_format=request.response_format, + tool_choice=request.tool_choice, + extra_body=request.extra_body, + extra_headers=request.extra_headers, + extra_query=request.extra_query, + thinking=request.thinking, + service_tier=request.service_tier, + timeout=request.timeout, + ) + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + contents = self._contents( + request.messages, + request.prompt, + request.system_prompt, + vertex_mode=vertexai, + ) + if request.stream: + response = retry_async_stream( + lambda: client.aio.models.generate_content_stream( + model=self._model_name, contents=contents, config=config + ), + first_event_validator=lambda event: raise_for_stream_error_event( + event, "Google Gemini" + ), + ) + tool_calls: dict[Any, dict[str, Any]] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + async for chunk in iterate_async(response): + for event in self._stream_events_from_chunk(chunk): + if event.type == "finish": + terminal_seen = True + for completed_event in self._complete_stream_tool_calls( + tool_calls, + completed_tool_calls, + event.response_id, + event.raw, + ): + yield completed_event + yield event + continue + if event.type == "tool_call_delta" and event.tool_call: + key, merged = self._merge_tool_call(tool_calls, event.tool_call) + event.tool_call = dict(merged) + yield event + continue + if event.type == "tool_call_completed" and event.tool_call: + previous_entries = tuple(tool_calls.values()) + key, merged = self._merge_tool_call( + tool_calls, + event.tool_call, + merge_arguments=False, + ) + if not any(merged is entry for entry in previous_entries): + # No matching delta was accumulated. Keep the + # normal merge path so a completion-only event + # still carries its arguments exactly once. + _, merged = self._merge_tool_call(tool_calls, event.tool_call) + if key in completed_tool_calls: + continue + validate_complete_tool_call(merged, "Google Gemini") + completed_tool_calls.add(key) + event.tool_call = dict(merged) + yield event + continue + yield event + if not terminal_seen: + raise RuntimeError("Google Gemini 异步流在终止事件之前结束。") + return + result = await self.acomplete(request) + if result.text: + yield StreamEvent(type="text_delta", text=result.text, response_id=result.response_id, raw=result.raw) + if result.reasoning: + yield StreamEvent(type="reasoning_delta", reasoning=result.reasoning, raw=result.raw) + if result.refusal: + yield StreamEvent(type="refusal_delta", refusal=result.refusal, response_id=result.response_id, raw=result.raw) + for call in result.tool_calls: + yield StreamEvent(type="tool_call_delta", tool_call=call, raw=result.raw) + yield StreamEvent(type="tool_call_completed", tool_call=call, response_id=result.response_id, raw=result.raw) + if result.usage: + yield StreamEvent(type="usage", usage=result.usage, response_id=result.response_id, raw=result.raw) + yield StreamEvent(type="finish", finish_reason=result.finish_reason, response_id=result.response_id, raw=result.raw) + + def create_cache(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.create( + model=kwargs.pop("model", self._model_name), + **self._resource_config_kwargs(types.CreateCachedContentConfig, config, kwargs), + ) + + def get_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.get( + name=name, **self._resource_config_kwargs(types.GetCachedContentConfig, config, kwargs) + ) + + def list_caches(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.list( + **self._resource_config_kwargs(types.ListCachedContentsConfig, config, kwargs) + ) + + def update_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.update( + name=name, **self._resource_config_kwargs(types.UpdateCachedContentConfig, config, kwargs) + ) + + def delete_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().caches.delete( + name=name, **self._resource_config_kwargs(types.DeleteCachedContentConfig, config, kwargs) + ) + + async def async_create_cache(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.create( + model=kwargs.pop("model", self._model_name), + **self._resource_config_kwargs(types.CreateCachedContentConfig, config, kwargs), + )) + + async def async_get_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.get( + name=name, **self._resource_config_kwargs(types.GetCachedContentConfig, config, kwargs) + )) + + async def async_list_caches(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.list( + **self._resource_config_kwargs(types.ListCachedContentsConfig, config, kwargs) + )) + + async def async_update_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.update( + name=name, **self._resource_config_kwargs(types.UpdateCachedContentConfig, config, kwargs) + )) + + async def async_delete_cache(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.caches.delete( + name=name, **self._resource_config_kwargs(types.DeleteCachedContentConfig, config, kwargs) + )) + + def create_batch(self, src: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.create( + model=kwargs.pop("model", self._model_name), src=src, + **self._resource_config_kwargs(types.CreateBatchJobConfig, config, kwargs), + ) + + def create_embedding_batch(self, src: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.create_embeddings( + model=kwargs.pop("model", self._model_name), src=src, + **self._resource_config_kwargs(types.CreateEmbeddingsBatchJobConfig, config, kwargs), + ) + + def get_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.get( + name=name, **self._resource_config_kwargs(types.GetBatchJobConfig, config, kwargs) + ) + + def list_batches(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.list( + **self._resource_config_kwargs(types.ListBatchJobsConfig, config, kwargs) + ) + + def cancel_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.cancel( + name=name, **self._resource_config_kwargs(types.CancelBatchJobConfig, config, kwargs) + ) + + def delete_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().batches.delete( + name=name, **self._resource_config_kwargs(types.DeleteBatchJobConfig, config, kwargs) + ) + + async def async_create_batch(self, src: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.create( + model=kwargs.pop("model", self._model_name), src=src, + **self._resource_config_kwargs(types.CreateBatchJobConfig, config, kwargs), + )) + + async def async_create_embedding_batch(self, src: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.create_embeddings( + model=kwargs.pop("model", self._model_name), src=src, + **self._resource_config_kwargs(types.CreateEmbeddingsBatchJobConfig, config, kwargs), + )) + + async def async_get_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.get( + name=name, **self._resource_config_kwargs(types.GetBatchJobConfig, config, kwargs) + )) + + async def async_list_batches(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.list( + **self._resource_config_kwargs(types.ListBatchJobsConfig, config, kwargs) + )) + + async def async_cancel_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.cancel( + name=name, **self._resource_config_kwargs(types.CancelBatchJobConfig, config, kwargs) + )) + + async def async_delete_batch(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.batches.delete( + name=name, **self._resource_config_kwargs(types.DeleteBatchJobConfig, config, kwargs) + )) + + def list_models(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.list( + **self._resource_config_kwargs(types.ListModelsConfig, config, kwargs) + ) + + def get_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.get( + model=model, **self._resource_config_kwargs(types.GetModelConfig, config, kwargs) + ) + + def delete_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.delete( + model=model, **self._resource_config_kwargs(types.DeleteModelConfig, config, kwargs) + ) + + def update_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.update( + model=model, **self._resource_config_kwargs(types.UpdateModelConfig, config, kwargs) + ) + + def tune(self, base_model: str, training_dataset: Any, config: Any = None, **kwargs: Any) -> Any: + """创建 Google GenAI 调优任务。""" + tunings = self._require_resource( + self._get_client(), "tunings", "调优" + ) + return tunings.tune( + base_model=base_model, + training_dataset=training_dataset, + **self._resource_config_kwargs(types.CreateTuningJobConfig, config, kwargs), + ) + + def get_tuning(self, name: str, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client(), "tunings", "调优") + return tunings.get( + name=name, **self._resource_config_kwargs(types.GetTuningJobConfig, config, kwargs) + ) + + def list_tunings(self, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client(), "tunings", "调优") + return tunings.list( + **self._resource_config_kwargs(types.ListTuningJobsConfig, config, kwargs) + ) + + def cancel_tuning(self, name: str, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client(), "tunings", "调优") + return tunings.cancel( + name=name, **self._resource_config_kwargs(types.CancelTuningJobConfig, config, kwargs) + ) + + def validate_tuning_reward( + self, + parent: str, + sample_response: Any, + example: Any, + single_reward_config: Any = None, + composite_reward_config: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + tunings = self._require_resource(self._get_client(), "tunings", "调优") + if single_reward_config is not None and composite_reward_config is not None: + raise ValueError( + "Google validate_reward 的 single_reward_config 与 " + "composite_reward_config 不能同时传入。" + ) + reward_kwargs: dict[str, Any] = {} + if single_reward_config is not None: + reward_kwargs["single_reward_config"] = single_reward_config + if composite_reward_config is not None: + reward_kwargs["composite_reward_config"] = composite_reward_config + reward_kwargs.update( + self._resource_config_kwargs(types.ValidateRewardConfig, config, kwargs) + ) + return tunings.validate_reward( + parent=parent, + sample_response=sample_response, + example=example, + **reward_kwargs, + ) + + def recontext_image(self, source: Any, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.recontext_image( + model=model or self._model_name, source=source, + **self._resource_config_kwargs(types.RecontextImageConfig, config, kwargs), + ) + + async def async_list_models(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.list( + **self._resource_config_kwargs(types.ListModelsConfig, config, kwargs) + )) + + async def async_get_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.get( + model=model, **self._resource_config_kwargs(types.GetModelConfig, config, kwargs) + )) + + async def async_delete_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.delete( + model=model, **self._resource_config_kwargs(types.DeleteModelConfig, config, kwargs) + )) + + async def async_update_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.update( + model=model, **self._resource_config_kwargs(types.UpdateModelConfig, config, kwargs) + )) + + async def async_tune(self, base_model: str, training_dataset: Any, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + return await self._resolve_async_result(tunings.tune( + base_model=base_model, + training_dataset=training_dataset, + **self._resource_config_kwargs(types.CreateTuningJobConfig, config, kwargs), + )) + + async def async_get_tuning(self, name: str, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + return await self._resolve_async_result(tunings.get( + name=name, **self._resource_config_kwargs(types.GetTuningJobConfig, config, kwargs) + )) + + async def async_list_tunings(self, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + return await self._resolve_async_result(tunings.list( + **self._resource_config_kwargs(types.ListTuningJobsConfig, config, kwargs) + )) + + async def async_cancel_tuning(self, name: str, config: Any = None, **kwargs: Any) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + return await self._resolve_async_result(tunings.cancel( + name=name, **self._resource_config_kwargs(types.CancelTuningJobConfig, config, kwargs) + )) + + async def async_validate_tuning_reward( + self, + parent: str, + sample_response: Any, + example: Any, + single_reward_config: Any = None, + composite_reward_config: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + tunings = self._require_resource(self._get_client().aio, "tunings", "异步调优") + if single_reward_config is not None and composite_reward_config is not None: + raise ValueError( + "Google validate_reward 的 single_reward_config 与 " + "composite_reward_config 不能同时传入。" + ) + reward_kwargs: dict[str, Any] = {} + if single_reward_config is not None: + reward_kwargs["single_reward_config"] = single_reward_config + if composite_reward_config is not None: + reward_kwargs["composite_reward_config"] = composite_reward_config + reward_kwargs.update( + self._resource_config_kwargs(types.ValidateRewardConfig, config, kwargs) + ) + return await self._resolve_async_result(tunings.validate_reward( + parent=parent, + sample_response=sample_response, + example=example, + **reward_kwargs, + )) + + async def async_recontext_image(self, source: Any, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.recontext_image( + model=model or self._model_name, source=source, + **self._resource_config_kwargs(types.RecontextImageConfig, config, kwargs), + )) + + def generate_images(self, prompt: str, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.generate_images( + model=model or self._model_name, prompt=prompt, + **self._resource_config_kwargs(types.GenerateImagesConfig, config, kwargs), + ) + + def edit_image(self, prompt: str, reference_images: list[Any], model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.edit_image( + model=model or self._model_name, prompt=prompt, reference_images=reference_images, + **self._resource_config_kwargs(types.EditImageConfig, config, kwargs), + ) + + def upscale_image(self, image: Any, upscale_factor: str, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.upscale_image( + model=model or self._model_name, image=image, upscale_factor=upscale_factor, + **self._resource_config_kwargs(types.UpscaleImageConfig, config, kwargs), + ) + + def segment_image(self, source: Any, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().models.segment_image( + model=model or self._model_name, source=source, + **self._resource_config_kwargs(types.SegmentImageConfig, config, kwargs), + ) + + async def async_generate_images(self, prompt: str, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.generate_images( + model=model or self._model_name, prompt=prompt, + **self._resource_config_kwargs(types.GenerateImagesConfig, config, kwargs), + )) + + async def async_edit_image(self, prompt: str, reference_images: list[Any], model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.edit_image( + model=model or self._model_name, + prompt=prompt, + reference_images=reference_images, + **self._resource_config_kwargs(types.EditImageConfig, config, kwargs), + )) + + async def async_upscale_image(self, image: Any, upscale_factor: str, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.upscale_image( + model=model or self._model_name, + image=image, + upscale_factor=upscale_factor, + **self._resource_config_kwargs(types.UpscaleImageConfig, config, kwargs), + )) + + async def async_segment_image(self, source: Any, model: str | None = None, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.segment_image( + model=model or self._model_name, source=source, + **self._resource_config_kwargs(types.SegmentImageConfig, config, kwargs), + )) + + def generate_videos( + self, + prompt: str | None = None, + model: str | None = None, + image: Any = None, + video: Any = None, + source: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return self._get_client().models.generate_videos( + model=model or self._model_name, prompt=prompt, image=image, video=video, source=source, + **self._resource_config_kwargs(types.GenerateVideosConfig, config, kwargs), + ) + + def get_operation(self, operation: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().operations.get( + operation, **self._resource_config_kwargs(types.GetOperationConfig, config, kwargs) + ) + + async def async_generate_videos( + self, + prompt: str | None = None, + model: str | None = None, + image: Any = None, + video: Any = None, + source: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return await self._resolve_async_result(self._get_client().aio.models.generate_videos( + model=model or self._model_name, prompt=prompt, image=image, video=video, source=source, + **self._resource_config_kwargs(types.GenerateVideosConfig, config, kwargs), + )) + + async def async_get_operation(self, operation: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.operations.get( + operation, **self._resource_config_kwargs(types.GetOperationConfig, config, kwargs) + )) + + def create_chat( + self, + history: list[Any] | None = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return self._get_client().chats.create( + model=kwargs.pop("model", self._model_name), + history=history, + **self._resource_config_kwargs(types.GenerateContentConfig, config, kwargs), + ) + + async def async_create_chat( + self, + history: list[Any] | None = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + # google-genai returns an AsyncChat object synchronously; subsequent + # send_message calls on that object are awaitable. + return await self._resolve_async_result(self._get_client().aio.chats.create( + model=kwargs.pop("model", self._model_name), + history=history, + **self._resource_config_kwargs(types.GenerateContentConfig, config, kwargs), + )) + + def create_file_search_store(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.create( + **self._resource_config_kwargs(types.CreateFileSearchStoreConfig, config, kwargs) + ) + + def create_auth_token(self, config: Any = None, **kwargs: Any) -> Any: + """创建 Google GenAI Auth Token。""" + self._require_provider_resource("create_auth_token") + return self._get_client().auth_tokens.create( + **self._resource_config_kwargs(types.CreateAuthTokenConfig, config, kwargs) + ) + + # The next-generation Google resources are intentionally exposed as + # provider-specific methods. Their request/response types are experimental + # and should not be forced into the shared completion contract. + def create_interaction(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_interaction") + return self._get_client().interactions.create(**kwargs) + + def get_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_interaction") + return self._get_client().interactions.get(id=interaction_id, **kwargs) + + def cancel_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("cancel_interaction") + return self._get_client().interactions.cancel(id=interaction_id, **kwargs) + + def delete_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_interaction") + return self._get_client().interactions.delete(id=interaction_id, **kwargs) + + def create_agent(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_agent") + return self._get_client().agents.create(**kwargs) + + def get_agent(self, agent_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_agent") + return self._get_client().agents.get(agent_id, **kwargs) + + def list_agents(self, **kwargs: Any) -> Any: + self._require_provider_resource("list_agents") + return self._get_client().agents.list(**kwargs) + + def delete_agent(self, agent_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_agent") + return self._get_client().agents.delete(agent_id, **kwargs) + + def create_webhook(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_webhook") + return self._get_client().webhooks.create(**kwargs) + + def get_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_webhook") + return self._get_client().webhooks.get(webhook_id, **kwargs) + + def list_webhooks(self, **kwargs: Any) -> Any: + self._require_provider_resource("list_webhooks") + return self._get_client().webhooks.list(**kwargs) + + def update_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("update_webhook") + return self._get_client().webhooks.update(webhook_id, **kwargs) + + def delete_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_webhook") + return self._get_client().webhooks.delete(webhook_id, **kwargs) + + def ping_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("ping_webhook") + return self._get_client().webhooks.ping(webhook_id, **kwargs) + + def rotate_webhook_signing_secret(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("rotate_webhook_signing_secret") + return self._get_client().webhooks.rotate_signing_secret(webhook_id, **kwargs) + + def create_environment(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_environment") + return self._get_client().environments.create_environment(**kwargs) + + def get_environment(self, environment_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_environment") + return self._get_client().environments.get_environment(id=environment_id, **kwargs) + + def list_environments(self, **kwargs: Any) -> Any: + self._require_provider_resource("list_environments") + return self._get_client().environments.list_environments(**kwargs) + + def delete_environment(self, environment_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_environment") + return self._get_client().environments.delete_environment(id=environment_id, **kwargs) + + def get_environment_files(self, environment_id: str, path: str, **kwargs: Any) -> Any: + """列出环境快照中的文件或目录内容。""" + self._require_provider_resource("get_environment_files") + return self._get_client().environments.files.list(environment_id, path, **kwargs) + + def create_trigger(self, **kwargs: Any) -> Any: + self._require_provider_resource("create_trigger") + return self._get_client().triggers.create(**kwargs) + + def get_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("get_trigger") + return self._get_client().triggers.get(trigger_id, **kwargs) + + def list_triggers(self, **kwargs: Any) -> Any: + self._require_provider_resource("list_triggers") + return self._get_client().triggers.list(**kwargs) + + def update_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("update_trigger") + return self._get_client().triggers.update(trigger_id, **kwargs) + + def delete_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("delete_trigger") + return self._get_client().triggers.delete(trigger_id, **kwargs) + + def run_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("run_trigger") + return self._get_client().triggers.run(trigger_id, **kwargs) + + def list_trigger_executions(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("list_trigger_executions") + return self._get_client().triggers.list_executions(trigger_id, **kwargs) + + def connect_live(self, *, model: str | None = None, config: Any = None) -> Any: + """返回 Google Live API 的异步上下文管理器。 + + google-genai 只在 ``client.aio.live`` 提供 Live 连接,不能把它伪装成 + 普通的 ``await`` 方法;调用方应使用 ``async with`` 管理会话生命周期。 + """ + self._validate_resource_config(config) + if not model: + model = self._model_name + client = self._get_client() + try: + aio = client.aio + except (AttributeError, NotImplementedError) as exc: + raise NotImplementedError( + "当前 google-genai SDK 未提供异步 Live 资源。" + ) from exc + live = self._require_resource(aio, "live", "Live") + connect = self._require_resource(live, "connect", "Live 连接") + if not callable(connect): + raise NotImplementedError( + "当前 google-genai SDK 未提供可调用的 Live 连接资源。" + ) + return connect(model=model, config=config) + + def async_connect_live(self, *, model: str | None = None, config: Any = None) -> Any: + """返回异步 Live API 上下文管理器,调用方使用 `async with`。""" + self._validate_resource_config(config) + return self.connect_live(model=model, config=config) + + def get_file_search_store(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.get( + name=name, **self._resource_config_kwargs(types.GetFileSearchStoreConfig, config, kwargs) + ) + + def list_file_search_stores(self, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.list( + **self._resource_config_kwargs(types.ListFileSearchStoresConfig, config, kwargs) + ) + + def delete_file_search_store(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.delete( + name=name, **self._resource_config_kwargs(types.DeleteFileSearchStoreConfig, config, kwargs) + ) + + def import_file_to_file_search_store( + self, + file_search_store_name: str, + file_name: str, + config: Any = None, + **kwargs: Any, + ) -> Any: + return self._get_client().file_search_stores.import_file( + file_search_store_name=file_search_store_name, + file_name=file_name, + **self._resource_config_kwargs(types.ImportFileConfig, config, kwargs), + ) + + def upload_to_file_search_store(self, file_search_store_name: str, file: Any, config: Any = None, **kwargs: Any) -> Any: + return self._get_client().file_search_stores.upload_to_file_search_store( + file_search_store_name=file_search_store_name, file=file, + **self._resource_config_kwargs(types.UploadToFileSearchStoreConfig, config, kwargs), + ) + + def download_file_search_media(self, media_id: str, config: Any = None, **kwargs: Any) -> bytes: + return self._get_client().file_search_stores.download_media( + media_id=media_id, **self._resource_config_kwargs(types.DownloadMediaConfig, config, kwargs) + ) + + async def async_create_file_search_store(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.create( + **self._resource_config_kwargs(types.CreateFileSearchStoreConfig, config, kwargs) + )) + + async def async_create_auth_token(self, config: Any = None, **kwargs: Any) -> Any: + """异步创建 Google GenAI Auth Token。""" + self._require_provider_resource("async_create_auth_token") + return await self._resolve_async_result(self._get_client().aio.auth_tokens.create( + **self._resource_config_kwargs(types.CreateAuthTokenConfig, config, kwargs) + )) + + async def async_create_interaction(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_interaction") + return await self._resolve_async_result(self._get_client().aio.interactions.create(**kwargs)) + + async def async_get_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_interaction") + return await self._resolve_async_result(self._get_client().aio.interactions.get(id=interaction_id, **kwargs)) + + async def async_cancel_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_cancel_interaction") + return await self._resolve_async_result(self._get_client().aio.interactions.cancel(id=interaction_id, **kwargs)) + + async def async_delete_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_interaction") + return await self._resolve_async_result(self._get_client().aio.interactions.delete(id=interaction_id, **kwargs)) + + async def async_create_agent(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_agent") + return await self._resolve_async_result(self._get_client().aio.agents.create(**kwargs)) + + async def async_get_agent(self, agent_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_agent") + return await self._resolve_async_result(self._get_client().aio.agents.get(agent_id, **kwargs)) + + async def async_list_agents(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_agents") + return await self._resolve_async_result(self._get_client().aio.agents.list(**kwargs)) + + async def async_delete_agent(self, agent_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_agent") + return await self._resolve_async_result(self._get_client().aio.agents.delete(agent_id, **kwargs)) + + async def async_create_webhook(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.create(**kwargs)) + + async def async_get_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.get(webhook_id, **kwargs)) + + async def async_list_webhooks(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_webhooks") + return await self._resolve_async_result(self._get_client().aio.webhooks.list(**kwargs)) + + async def async_update_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_update_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.update(webhook_id, **kwargs)) + + async def async_delete_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.delete(webhook_id, **kwargs)) + + async def async_ping_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_ping_webhook") + return await self._resolve_async_result(self._get_client().aio.webhooks.ping(webhook_id, **kwargs)) + + async def async_rotate_webhook_signing_secret(self, webhook_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_rotate_webhook_signing_secret") + return await self._resolve_async_result( + self._get_client().aio.webhooks.rotate_signing_secret(webhook_id, **kwargs) + ) + + async def async_create_environment(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_environment") + return await self._resolve_async_result( + self._get_client().aio.environments.create_environment(**kwargs) + ) + + async def async_get_environment(self, environment_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_environment") + return await self._resolve_async_result( + self._get_client().aio.environments.get_environment(id=environment_id, **kwargs) + ) + + async def async_list_environments(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_environments") + return await self._resolve_async_result( + self._get_client().aio.environments.list_environments(**kwargs) + ) + + async def async_delete_environment(self, environment_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_environment") + return await self._resolve_async_result( + self._get_client().aio.environments.delete_environment(id=environment_id, **kwargs) + ) + + async def async_get_environment_files( + self, environment_id: str, path: str, **kwargs: Any + ) -> Any: + """异步列出环境快照中的文件或目录内容。""" + self._require_provider_resource("async_get_environment_files") + return await self._resolve_async_result( + self._get_client().aio.environments.files.list(environment_id, path, **kwargs) + ) + + async def async_create_trigger(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_create_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.create(**kwargs)) + + async def async_get_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_get_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.get(trigger_id, **kwargs)) + + async def async_list_triggers(self, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_triggers") + return await self._resolve_async_result(self._get_client().aio.triggers.list(**kwargs)) + + async def async_update_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_update_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.update(trigger_id, **kwargs)) + + async def async_delete_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_delete_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.delete(trigger_id, **kwargs)) + + async def async_run_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_run_trigger") + return await self._resolve_async_result(self._get_client().aio.triggers.run(trigger_id, **kwargs)) + + async def async_list_trigger_executions(self, trigger_id: str, **kwargs: Any) -> Any: + self._require_provider_resource("async_list_trigger_executions") + return await self._resolve_async_result( + self._get_client().aio.triggers.list_executions(trigger_id, **kwargs) + ) + + async def async_get_file_search_store(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.get( + name=name, **self._resource_config_kwargs(types.GetFileSearchStoreConfig, config, kwargs) + )) + + async def async_list_file_search_stores(self, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.list( + **self._resource_config_kwargs(types.ListFileSearchStoresConfig, config, kwargs) + )) + + async def async_delete_file_search_store(self, name: str, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.delete( + name=name, **self._resource_config_kwargs(types.DeleteFileSearchStoreConfig, config, kwargs) + )) + + async def async_import_file_to_file_search_store( + self, + file_search_store_name: str, + file_name: str, + config: Any = None, + **kwargs: Any, + ) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.import_file( + file_search_store_name=file_search_store_name, + file_name=file_name, + **self._resource_config_kwargs(types.ImportFileConfig, config, kwargs), + )) + + async def async_upload_to_file_search_store(self, file_search_store_name: str, file: Any, config: Any = None, **kwargs: Any) -> Any: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.upload_to_file_search_store( + file_search_store_name=file_search_store_name, file=file, + **self._resource_config_kwargs(types.UploadToFileSearchStoreConfig, config, kwargs), + )) + + async def async_download_file_search_media(self, media_id: str, config: Any = None, **kwargs: Any) -> bytes: + return await self._resolve_async_result(self._get_client().aio.file_search_stores.download_media( + media_id=media_id, + **self._resource_config_kwargs(types.DownloadMediaConfig, config, kwargs), + )) + def invoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = None, - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + n: int | None = None, + seed: int | None = None, + modalities: list[str] | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + timeout: float | None = None, ) -> Generator[str, None, None]: """同步调用 Google LLM (CSE Sensor)。""" + temperature = None if temperature is _UNSET_TEMPERATURE else temperature logger.info(f"调用 Google LLM ({self._model_name}),流式: {stream}") client = self._get_client() start_time = time.perf_counter() - config = types.GenerateContentConfig(system_instruction=system_prompt, temperature=temperature) + effective_system_prompt = extract_system_prompt(messages) or system_prompt + config = self._build_generation_config(effective_system_prompt, tools, temperature, max_tokens=max_tokens, + top_p=top_p, top_k=top_k, candidate_count=n, + frequency_penalty=frequency_penalty, + presence_penalty=presence_penalty, seed=seed, + modalities=modalities, stop=stop, response_format=response_format, + tool_choice=tool_choice, extra_body=extra_body, + extra_headers=extra_headers, extra_query=extra_query, + timeout=timeout, + thinking=thinking, service_tier=service_tier) + contents = self._contents( + messages, + prompt, + system_prompt, + vertex_mode=self._effective_vertex_mode(client), + ) try: if stream: - response = client.models.generate_content_stream(model=self._model_name, contents=prompt, config=config) - for chunk in response: - if chunk.text: - yield chunk.text + terminal_seen = False + for chunk in retry_sync_stream( + lambda: client.models.generate_content_stream( + model=self._model_name, contents=contents, config=config + ), + first_event_validator=lambda event: raise_for_stream_error_event( + event, "Google Gemini" + ), + ): + raise_for_stream_error_event(chunk, "Google Gemini") + terminal_seen = terminal_seen or self._stream_chunk_has_terminal_event(chunk) + refusal = self._extract_refusal(chunk) + if refusal: + raise RuntimeError(f"Google 请求被拒绝: {refusal}") + text = self._response_text(chunk) + if text: + yield text + if not terminal_seen: + raise RuntimeError("Google Gemini 流在终止事件之前结束。") else: - response = client.models.generate_content(model=self._model_name, contents=prompt, config=config) - if response.text: - yield response.text + response = retry_sync_call( + lambda: client.models.generate_content( + model=self._model_name, contents=contents, config=config + ) + ) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"Google 请求被拒绝: {refusal}") + text = self._response_text(response) + if text: + yield text duration = time.perf_counter() - start_time logger.info(f"Google LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"Google LLM ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Google LLM (%s) 出错: %s", + self._model_name, + error_text, + ) raise - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) async def ainvoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = None, - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + n: int | None = None, + seed: int | None = None, + modalities: list[str] | None = None, + stop: list[str] | str | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + service_tier: str | None = None, + timeout: float | None = None, ) -> AsyncGenerator[str, None]: """异步调用 Google LLM (CSE Sensor)。""" + temperature = None if temperature is _UNSET_TEMPERATURE else temperature logger.info(f"异步调用 Google LLM ({self._model_name}),流式: {stream}") client = self._get_client() start_time = time.perf_counter() - config = types.GenerateContentConfig(system_instruction=system_prompt, temperature=temperature) + effective_system_prompt = extract_system_prompt(messages) or system_prompt + config = self._build_generation_config(effective_system_prompt, tools, temperature, max_tokens=max_tokens, + top_p=top_p, top_k=top_k, candidate_count=n, + frequency_penalty=frequency_penalty, + presence_penalty=presence_penalty, seed=seed, + modalities=modalities, stop=stop, response_format=response_format, + tool_choice=tool_choice, extra_body=extra_body, + extra_headers=extra_headers, extra_query=extra_query, + timeout=timeout, + thinking=thinking, service_tier=service_tier) + contents = self._contents( + messages, + prompt, + system_prompt, + vertex_mode=self._effective_vertex_mode(client), + ) try: if stream: - response = await client.aio.models.generate_content_stream(model=self._model_name, contents=prompt, config=config) - async for chunk in response: - if chunk.text: - yield chunk.text + terminal_seen = False + async for chunk in retry_async_stream( + lambda: client.aio.models.generate_content_stream( + model=self._model_name, contents=contents, config=config + ), + first_event_validator=lambda event: raise_for_stream_error_event( + event, "Google Gemini" + ), + ): + raise_for_stream_error_event(chunk, "Google Gemini") + terminal_seen = terminal_seen or self._stream_chunk_has_terminal_event(chunk) + refusal = self._extract_refusal(chunk) + if refusal: + raise RuntimeError(f"Google 请求被拒绝: {refusal}") + text = self._response_text(chunk) + if text: + yield text + if not terminal_seen: + raise RuntimeError("Google Gemini 异步流在终止事件之前结束。") else: - response = await client.aio.models.generate_content(model=self._model_name, contents=prompt, config=config) - if response.text: - yield response.text + response = await retry_async_call(lambda: client.aio.models.generate_content( + model=self._model_name, contents=contents, config=config + )) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"Google 请求被拒绝: {refusal}") + text = self._response_text(response) + if text: + yield text duration = time.perf_counter() - start_time logger.info(f"Google LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"Google LLM ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Google LLM (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - def embed_documents(self, texts: List[str]) -> List[List[float]]: + def embed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """同步向量化文档。""" logger.info(f"调用 Google Embedding ({self._model_name}),数量: {len(texts)}") - client = self._get_client() start_time = time.perf_counter() try: - response = client.models.embed_content( - model=self._model_name, - contents=texts, - config=types.EmbedContentConfig(task_type="RETRIEVAL_DOCUMENT") - ) - embeddings = [list(e.values) for e in response.embeddings] + config_values = self._embedding_options() + overlap = sorted({"model", "contents", "config"}.intersection(kwargs)) + if overlap: + raise ValueError(f"Google Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + request_options = { + key: kwargs.pop(key, None) + for key in ("extra_headers", "extra_body", "extra_query", "timeout") + } + self._validate_embedding_kwargs(kwargs) + config_values.update(kwargs) + config_values.setdefault("task_type", "RETRIEVAL_DOCUMENT") + http_options = self._http_options_for_request(**request_options) + if http_options is not None: + config_values["http_options"] = http_options + config = types.EmbedContentConfig(**config_values) + # Validate and construct the complete request before initializing + # the SDK client, so invalid request extensions fail closed. + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + if vertexai is True and self._vertex_embed_content_only(): + embeddings = [] + for text in texts: + response = client.models.embed_content( + model=self._model_name, contents=[text], config=config + ) + values = list(field(response, "embeddings", []) or []) + if len(values) != 1: + raise RuntimeError( + "Google Vertex Embedding 单内容请求未返回唯一 embedding。" + ) + embeddings.append(normalize_embedding_vector(field(values[0], "values"))) + else: + response = client.models.embed_content( + model=self._model_name, contents=texts, config=config + ) + values = list(field(response, "embeddings", []) or []) + if len(values) != len(texts): + raise RuntimeError( + "Google Embedding 返回数量与输入文本数量不一致。" + ) + embeddings = [normalize_embedding_vector(field(item, "values")) for item in values] duration = time.perf_counter() - start_time logger.info(f"Google Embedding ({self._model_name}) 完成,耗时: {duration:.2f}s") return embeddings except Exception as e: - logger.error(f"Google Embedding ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Google Embedding (%s) 出错: %s", + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + async def aembed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """异步向量化文档。""" logger.info(f"异步调用 Google Embedding ({self._model_name}),数量: {len(texts)}") - client = self._get_client() start_time = time.perf_counter() try: - response = await client.aio.models.embed_content( - model=self._model_name, - contents=texts, - config=types.EmbedContentConfig(task_type="RETRIEVAL_DOCUMENT") - ) - embeddings = [list(e.values) for e in response.embeddings] + config_values = self._embedding_options() + overlap = sorted({"model", "contents", "config"}.intersection(kwargs)) + if overlap: + raise ValueError(f"Google Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + request_options = { + key: kwargs.pop(key, None) + for key in ("extra_headers", "extra_body", "extra_query", "timeout") + } + self._validate_embedding_kwargs(kwargs) + config_values.update(kwargs) + config_values.setdefault("task_type", "RETRIEVAL_DOCUMENT") + http_options = self._http_options_for_request(**request_options) + if http_options is not None: + config_values["http_options"] = http_options + config = types.EmbedContentConfig(**config_values) + # Keep async embedding subject to the same fail-closed ordering as + # the sync entry point. + client = self._get_client() + vertexai = self._effective_vertex_mode(client) + if vertexai is True and self._vertex_embed_content_only(): + embeddings = [] + for text in texts: + response = await self._resolve_async_result( + client.aio.models.embed_content( + model=self._model_name, contents=[text], config=config + ) + ) + values = list(field(response, "embeddings", []) or []) + if len(values) != 1: + raise RuntimeError( + "Google Vertex Embedding 单内容请求未返回唯一 embedding。" + ) + embeddings.append(normalize_embedding_vector(field(values[0], "values"))) + else: + response = await self._resolve_async_result( + client.aio.models.embed_content( + model=self._model_name, contents=texts, config=config + ) + ) + values = list(field(response, "embeddings", []) or []) + if len(values) != len(texts): + raise RuntimeError( + "Google Embedding 返回数量与输入文本数量不一致。" + ) + embeddings = [normalize_embedding_vector(field(item, "values")) for item in values] duration = time.perf_counter() - start_time logger.info(f"Google Embedding ({self._model_name}) 异步完成,耗时: {duration:.2f}s") return embeddings except Exception as e: - logger.error(f"Google Embedding ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Google Embedding (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise + + def embed_query(self, text: str, **kwargs: Any) -> list[float]: + kwargs.setdefault("task_type", "RETRIEVAL_QUERY") + return self.embed_documents([text], **kwargs)[0] + + async def aembed_query(self, text: str, **kwargs: Any) -> list[float]: + kwargs.setdefault("task_type", "RETRIEVAL_QUERY") + return (await self.aembed_documents([text], **kwargs))[0] + + def close(self) -> None: + """释放同步 Client 及其关联的异步资源。 + + google-genai 的 ``Client.close`` 不会关闭 ``Client.aio``;同步上下文 + 使用生命周期桥接器等待异步关闭,活动事件循环中则保留 Client 并 + 显式要求调用 ``aclose``。 + """ + client = self._client + if client is None: + return + first_error: Exception | None = None + try: + client.close() + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + try: + close_resource_sync(client.aio, "Google 异步客户端") + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + else: + self._client = None + if first_error is not None: + raise first_error + + async def aclose(self) -> None: + """释放 google-genai 的异步 Client,并在异常后清除状态。""" + client = self._client + self._client = None + if client is None: + return + first_error: Exception | None = None + try: + result = client.aio.aclose() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + try: + result = client.close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + if first_error is not None: + raise first_error diff --git a/src/providers/grok.py b/src/providers/grok.py index a37eaf2..f46b6a0 100644 --- a/src/providers/grok.py +++ b/src/providers/grok.py @@ -1,9 +1,10 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider class GrokProvider(OpenAICompatibleProvider): """Grok 模型提供商。""" - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="grok") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="grok", protocol=protocol, options=options) diff --git a/src/providers/jina.py b/src/providers/jina.py index 5804877..30a41ac 100644 --- a/src/providers/jina.py +++ b/src/providers/jina.py @@ -1,13 +1,18 @@ -# -*- coding: utf-8 -*- +import math import time -from typing import Any, Dict, List, Tuple +from collections.abc import Mapping +from typing import Any -import requests -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential -from src.providers.__base__.model_provider import RerankModel +from src.providers.__base__.model_provider import ( + RerankModel, + is_retryable_error, + validate_secret_free_options, +) from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import redact_sensitive_text, validate_secret_free_payload logger = get_module_logger(__name__) @@ -17,8 +22,16 @@ class JinaProvider(RerankModel): 已注入 CSE 性能传感器与 tenacity 重试机制。 """ - def __init__(self, model_name: str): + _OPTION_KEYS = frozenset( + {"return_documents", "max_chunks_per_doc", "late_chunking", "truncate", "embedding_type", "extra_body", "timeout"} + ) + + def __init__(self, model_name: str, options: dict[str, Any] | None = None): self._model_name = model_name + self._options = validate_secret_free_options(options, "Jina Rerank") + unknown = sorted(set(self._options).difference(self._OPTION_KEYS)) + if unknown: + raise ValueError("Jina Rerank options 包含不支持的字段: " + ", ".join(unknown)) settings = get_settings() self._api_key = settings.jina_api_key if not self._api_key: @@ -27,85 +40,192 @@ def __init__(self, model_name: str): self._base_url = "https://api.jina.ai/v1/rerank" logger.info(f"初始化 JinaProvider,模型: {model_name}") - def _get_headers(self) -> Dict[str, str]: + def _get_headers(self) -> dict[str, str]: return {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - def _prepare_payload(self, query: str, documents: List[str], top_n: int) -> Dict[str, Any]: - return { + def _prepare_payload(self, query: str, documents: list[str], top_n: int) -> dict[str, Any]: + self._validate_inputs(query, documents, top_n) + payload: dict[str, Any] = { "query": query, "documents": documents, "model": self._model_name, - "top_n": top_n + "top_n": top_n, + # 默认请求返回文档内容;与 SiliconFlow Rerank 保持一致的不可覆盖字段。 + "return_documents": True, } + options = dict(self._options) + options.pop("timeout", None) + extra_body = validate_secret_free_payload( + options.pop("extra_body", None), + "Jina Rerank", + "options.extra_body", + ) + overlap = sorted(set(payload).intersection(options)) + if overlap: + raise ValueError("Jina Rerank options 不允许覆盖请求字段: " + ", ".join(overlap)) + if extra_body: + option_overlap = sorted(set(options).intersection(extra_body)) + if option_overlap: + raise ValueError( + "Jina Rerank options 与 options.extra_body 重复: " + + ", ".join(option_overlap) + ) + body_overlap = sorted(set(payload).intersection(extra_body)) + if body_overlap: + raise ValueError( + "Jina Rerank options.extra_body 不允许覆盖请求字段: " + + ", ".join(body_overlap) + ) + options.update(dict(extra_body)) + payload.update(options) + return payload - def _parse_response(self, results: List[Dict], documents: List[str]) -> Tuple[List[int], List[float]]: + def _request_timeout(self) -> float: + timeout = self._options.get("timeout", 30.0) + if ( + isinstance(timeout, bool) + or not isinstance(timeout, (int, float)) + or not math.isfinite(float(timeout)) + or timeout <= 0 + ): + raise ValueError("Jina Rerank options.timeout 必须是正数。") + return float(timeout) + + @staticmethod + def _validate_inputs(query: str, documents: list[str], top_n: int) -> None: + if not isinstance(query, str) or not query.strip(): + raise ValueError("Jina Rerank query 必须是非空字符串。") + if not isinstance(documents, list) or not documents: + raise ValueError("Jina Rerank documents 必须是非空列表。") + if any(not isinstance(document, str) for document in documents): + raise ValueError("Jina Rerank documents 必须全部是字符串。") + if not isinstance(top_n, int) or isinstance(top_n, bool) or top_n < 1: + raise ValueError("Jina Rerank top_n 必须是大于等于 1 的整数。") + + def _parse_response(self, results: list[dict], documents: list[str]) -> tuple[list[int], list[float]]: + if not isinstance(results, list): + raise RuntimeError("Jina Rerank 响应缺少有效的 results 列表。") # 创建内容到索引的映射 - content_to_index = {content: i for i, content in enumerate(documents)} + positions: dict[str, list[int]] = {} + for index, content in enumerate(documents): + positions.setdefault(content, []).append(index) indices = [] scores = [] for res in results: - doc_content = res.get("document", {}).get("text") if isinstance(res.get("document"), dict) else res.get("document") - if doc_content in content_to_index: - indices.append(content_to_index[doc_content]) - scores.append(res.get("relevance_score", 0.0)) + if not isinstance(res, Mapping): + raise RuntimeError("Jina Rerank 响应包含无效结果项。") + if "index" in res: + direct_index = res["index"] + if ( + not isinstance(direct_index, int) + or isinstance(direct_index, bool) + or not 0 <= direct_index < len(documents) + ): + raise RuntimeError("Jina Rerank 响应 index 无效。") + if direct_index in indices: + raise RuntimeError("Jina Rerank 响应包含重复 index。") + index = direct_index + document = res.get("document") + document_text = ( + document.get("text") if isinstance(document, Mapping) else document + ) + if document_text is not None and document_text != documents[index]: + raise RuntimeError("Jina Rerank 响应 index 与 document 不匹配。") + positions.get(documents[index], []).remove(index) + else: + document = res.get("document") + doc_content = ( + document.get("text") if isinstance(document, Mapping) else document + ) + if not isinstance(doc_content, str) or not positions.get(doc_content): + raise RuntimeError("Jina Rerank 响应无法映射到输入文档。") + index = positions[doc_content].pop(0) + try: + score = float(res["relevance_score"]) + except (KeyError, TypeError, ValueError) as exc: + raise RuntimeError("Jina Rerank 响应 relevance_score 无效。") from exc + if not math.isfinite(score): + raise RuntimeError("Jina Rerank 响应 relevance_score 必须是有限数值。") + indices.append(index) + scores.append(score) return indices, scores @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - def rerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + def rerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """同步 Rerank (CSE Sensor)。""" logger.info(f"调用 Jina Rerank ({self._model_name}),文档数: {len(documents)}") import httpx start_time = time.perf_counter() try: - with httpx.Client(timeout=30.0) as client: + with httpx.Client(timeout=self._request_timeout()) as client: response = client.post( self._base_url, headers=self._get_headers(), json=self._prepare_payload(query, documents, top_n) ) response.raise_for_status() - results = response.json().get("results", []) + payload = response.json() + if not isinstance(payload, Mapping): + raise RuntimeError("Jina Rerank 响应必须是 JSON 对象。") + results = payload.get("results") + if not isinstance(results, list): + raise RuntimeError("Jina Rerank 响应缺少有效的 results 列表。") indices, scores = self._parse_response(results, documents) duration = time.perf_counter() - start_time logger.info(f"Jina Rerank ({self._model_name}) 完成,耗时: {duration:.2f}s") return indices, scores except Exception as e: - logger.error(f"Jina Rerank ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Jina Rerank (%s) 出错: %s", + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - async def arerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + async def arerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """异步 Rerank (CSE Sensor)。""" logger.info(f"异步调用 Jina Rerank ({self._model_name}),文档数: {len(documents)}") import httpx start_time = time.perf_counter() try: - async with httpx.AsyncClient(timeout=30.0) as aclient: + async with httpx.AsyncClient(timeout=self._request_timeout()) as aclient: response = await aclient.post( self._base_url, headers=self._get_headers(), json=self._prepare_payload(query, documents, top_n) ) response.raise_for_status() - results = response.json().get("results", []) + payload = response.json() + if not isinstance(payload, Mapping): + raise RuntimeError("Jina Rerank 响应必须是 JSON 对象。") + results = payload.get("results") + if not isinstance(results, list): + raise RuntimeError("Jina Rerank 响应缺少有效的 results 列表。") indices, scores = self._parse_response(results, documents) duration = time.perf_counter() - start_time logger.info(f"Jina Rerank ({self._model_name}) 异步完成,耗时: {duration:.2f}s") return indices, scores except Exception as e: - logger.error(f"Jina Rerank ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "Jina Rerank (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise diff --git a/src/providers/lm_studio.py b/src/providers/lm_studio.py index 080a213..ba99b45 100644 --- a/src/providers/lm_studio.py +++ b/src/providers/lm_studio.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,5 @@ class LMStudioProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="lm-studio") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="lm-studio", protocol=protocol, options=options) diff --git a/src/providers/local_hash.py b/src/providers/local_hash.py index 20690ae..01718ed 100644 --- a/src/providers/local_hash.py +++ b/src/providers/local_hash.py @@ -1,11 +1,9 @@ -# -*- coding: utf-8 -*- import asyncio import hashlib import math import re -from typing import List -import jieba +import jieba # type: ignore[import-untyped] from src.providers.__base__.model_provider import TextEmbeddingModel @@ -20,12 +18,12 @@ def _parse_dimension(model_name: str) -> int: return int(suffix) if suffix.isdigit() and int(suffix) > 0 else DEFAULT_DIMENSION -def _tokenize(text: str) -> List[str]: +def _tokenize(text: str) -> list[str]: tokens = [token.strip() for token in jieba.lcut(text) if token.strip()] return tokens if tokens else TOKEN_PATTERN.findall(text) -def _normalize(vector: List[float]) -> List[float]: +def _normalize(vector: list[float]) -> list[float]: norm = math.sqrt(sum(value * value for value in vector)) if norm == 0: return vector @@ -39,7 +37,7 @@ def __init__(self, model_name: str): self.model_name = model_name self.dimension = _parse_dimension(model_name) - def _embed_text(self, text: str) -> List[float]: + def _embed_text(self, text: str) -> list[float]: vector = [0.0] * self.dimension for token in _tokenize(text): digest = hashlib.blake2b(token.encode("utf-8"), digest_size=16).digest() @@ -51,8 +49,18 @@ def _embed_text(self, text: str) -> List[float]: vector[secondary_index] += secondary_sign return _normalize(vector) - def embed_documents(self, texts: List[str]) -> List[List[float]]: + @staticmethod + def _validate_kwargs(kwargs: dict) -> None: + if kwargs: + unsupported = ", ".join(sorted(str(key) for key in kwargs)) + raise ValueError( + "LocalHash Embedding 不支持请求参数: " + unsupported + ) + + def embed_documents(self, texts: list[str], **kwargs) -> list[list[float]]: + self._validate_kwargs(kwargs) return [self._embed_text(text) for text in texts] - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + async def aembed_documents(self, texts: list[str], **kwargs) -> list[list[float]]: + self._validate_kwargs(kwargs) return await asyncio.to_thread(self.embed_documents, texts) diff --git a/src/providers/ollama.py b/src/providers/ollama.py index 8e485f0..da85710 100644 --- a/src/providers/ollama.py +++ b/src/providers/ollama.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,5 @@ class OllamaProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="ollama") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="ollama", protocol=protocol, options=options) diff --git a/src/providers/openai.py b/src/providers/openai.py index a05b88e..b9c137d 100644 --- a/src/providers/openai.py +++ b/src/providers/openai.py @@ -1,173 +1,44 @@ -# -*- coding: utf-8 -*- -import time -from typing import Any, Dict, Generator, List, AsyncGenerator +from typing import Any -import openai -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type - -from src.providers.__base__.model_provider import ( - LargeLanguageModel, - TextEmbeddingModel, -) +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.resources import AsyncOpenAIResources, OpenAIResources from src.utils.config import get_settings from src.utils.log_manager import get_module_logger logger = get_module_logger(__name__) -class OpenAIProvider(LargeLanguageModel, TextEmbeddingModel): - """ - OpenAI模型提供商,统一处理GPT LLM和Embedding。 - 已增加 tenacity 重试机制与性能传感器。 - """ - - def __init__(self, model_name: str): - self._model_name = model_name - settings = get_settings() - self._api_key = settings.openai_api_key - self._base_url = settings.openai_api_base - - if not self._api_key: - logger.error("OpenAI配置不完整:缺少 OPENAI_API_KEY。") - raise ValueError("OpenAI配置不完整:缺少 OPENAI_API_KEY。") - - self._client: Optional[openai.OpenAI] = None - self._aclient: Optional[openai.AsyncOpenAI] = None - logger.info(f"OpenAIProvider 初始化成功,模型: {model_name}") - - def _get_client(self) -> openai.OpenAI: - if self._client is None: - self._client = openai.OpenAI(api_key=self._api_key, base_url=self._base_url) - return self._client - def _get_aclient(self) -> openai.AsyncOpenAI: - if self._aclient is None: - self._aclient = openai.AsyncOpenAI(api_key=self._api_key, base_url=self._base_url) - return self._aclient +class OpenAIProvider(OpenAICompatibleProvider): + """OpenAI 官方渠道,复用 OpenAI 兼容协议适配器。""" - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + capabilities = OpenAICompatibleProvider.capabilities | frozenset( + { + "files", "batches", "vector_stores", "responses", "moderation", + "audio", "images", "videos", "uploads", "parse", "conversations", + "containers", "fine_tuning", "evals", "skills", "realtime", + "webhooks", "admin", "content_provenance_checks", + } ) - def invoke( - self, - prompt: str, - system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, - stream: bool = True, - temperature: float = 0.7, - ) -> Generator[str, None, None]: - """同步调用 OpenAI LLM (CSE Sensor)。""" - logger.info(f"调用 OpenAI LLM ({self._model_name}),流式: {stream}") - client = self._get_client() - start_time = time.perf_counter() - - try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = client.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream - ) - - if stream: - for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content - else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content - - duration = time.perf_counter() - start_time - logger.info(f"OpenAI LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") - except Exception as e: - logger.error(f"OpenAI LLM ({self._model_name}) 出错: {e}", exc_info=True) - raise - - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + _resource_capabilities = frozenset( + { + "files", "batches", "vector_stores", "models", "moderation", + "images", "audio", "videos", "uploads", "conversations", + "containers", "fine_tuning", "evals", + } ) - async def ainvoke( - self, - prompt: str, - system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, - stream: bool = True, - temperature: float = 0.7, - ) -> AsyncGenerator[str, None]: - """异步调用 OpenAI LLM (CSE Sensor)。""" - logger.info(f"异步调用 OpenAI LLM ({self._model_name}),流式: {stream}") - aclient = self._get_aclient() - start_time = time.perf_counter() - - try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = await aclient.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream - ) - - if stream: - async for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content - else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content - - duration = time.perf_counter() - start_time - logger.info(f"OpenAI LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") - except Exception as e: - logger.error(f"OpenAI LLM ({self._model_name}) 异步出错: {e}", exc_info=True) - raise - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) - def embed_documents(self, texts: List[str]) -> List[List[float]]: - """同步向量化文档。""" - logger.info(f"调用 OpenAI Embedding ({self._model_name}),数量: {len(texts)}") - client = self._get_client() - start_time = time.perf_counter() - - try: - response = client.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] - duration = time.perf_counter() - start_time - logger.info(f"OpenAI Embedding ({self._model_name}) 完成,耗时: {duration:.2f}s") - return embeddings - except Exception as e: - logger.error(f"OpenAI Embedding ({self._model_name}) 出错: {e}", exc_info=True) - raise + @property + def resources(self) -> OpenAIResources: + return OpenAIResources(self) - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: - """异步向量化文档。""" - logger.info(f"异步调用 OpenAI Embedding ({self._model_name}),数量: {len(texts)}") - aclient = self._get_aclient() - start_time = time.perf_counter() - - try: - response = await aclient.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] - duration = time.perf_counter() - start_time - logger.info(f"OpenAI Embedding ({self._model_name}) 异步完成,耗时: {duration:.2f}s") - return embeddings - except Exception as e: - logger.error(f"OpenAI Embedding ({self._model_name}) 异步出错: {e}", exc_info=True) - raise + @property + def async_resources(self) -> AsyncOpenAIResources: + return AsyncOpenAIResources(self) + + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + # 保留本模块的配置入口,方便调用方在测试或运行时注入 settings。 + settings = get_settings() + if not settings.openai_api_key: + logger.error("OpenAI配置不完整:缺少 OPENAI_API_KEY。") + raise ValueError("OpenAI配置不完整:缺少 OPENAI_API_KEY。") + super().__init__(model_name=model_name, provider="openai", settings=settings, protocol=protocol, options=options) diff --git a/src/providers/openai_compatible.py b/src/providers/openai_compatible.py index fbcd0ea..c7e8974 100644 --- a/src/providers/openai_compatible.py +++ b/src/providers/openai_compatible.py @@ -1,16 +1,53 @@ -# -*- coding: utf-8 -*- +import asyncio +import inspect +import math import time -from typing import Any, Dict, Generator, List, AsyncGenerator +from collections.abc import AsyncGenerator, AsyncIterator, Generator, Iterator, Mapping +from typing import Any, cast +from urllib.parse import urlsplit import openai -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential from src.providers.__base__.model_provider import ( + _UNSET_TEMPERATURE, + CompletionRequest, + CompletionResult, LargeLanguageModel, + StreamEvent, TextEmbeddingModel, + close_resource_sync, + coerce_completion_request, + content_to_text, + is_retryable_error, + merge_tool_call_fragment, + normalize_embedding_vector, + normalize_messages, + normalize_responses_input, + normalize_tool_arguments, + normalize_usage, + raise_for_stream_error_event, + reject_unsupported_kwargs, + retry_async_call, + retry_async_stream, + retry_sync_call, + retry_sync_stream, + validate_complete_tool_call, + validate_secret_free_options, + validate_secret_free_request_overrides, +) +from src.providers.resources import ( + AsyncOpenAICompatibleResources, + OpenAICompatibleResources, ) from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import ( + redact_sensitive_text, + validate_secret_free_payload, + validate_secret_free_resource_args, + validate_secret_free_resource_kwargs, +) logger = get_module_logger(__name__) @@ -20,16 +57,126 @@ class OpenAICompatibleProvider(LargeLanguageModel, TextEmbeddingModel): 已注入 CSE 性能传感器与 tenacity 重试机制。 """ - def __init__(self, model_name: str, provider: str): + capabilities = frozenset( + { + "chat", "stream", "messages", "multimodal", "tools", + "structured_output", "usage", "embedding", "responses", + } + ) + # OpenAI's resource methods live on this shared adapter for code reuse, but + # only the official provider (or an explicitly verified Responses endpoint) + # may expose them. Keeping the declaration separate from the general + # capability set prevents compatible channels from accidentally advertising + # or dispatching official-only resources. + _resource_capabilities: frozenset[str] = frozenset() + # Tests and lightweight integrations sometimes instantiate this shared + # adapter directly with ``provider="openai"`` instead of using the + # OpenAIProvider subclass. Keep that explicit provider identity + # equivalent to the official adapter for resource gating, while leaving + # all other compatible providers fail-closed. + _OFFICIAL_OPENAI_RESOURCE_CAPABILITIES = frozenset( + { + "files", "batches", "vector_stores", "models", "moderation", + "images", "audio", "videos", "uploads", "conversations", + "containers", "fine_tuning", "evals", + } + ) + # These fields are accepted by OpenAI Chat Completions but are not part of + # the Responses request shape. Keep this guard protocol-specific: the + # current OpenAI SDK also exposes the fields below on ``responses.create`` + # (with ``verbosity`` normalized into ``text`` by this adapter). + _OPENAI_ONLY_CHAT_FIELDS = frozenset( + { + "modalities", + "audio", + "prediction", + "web_search_options", + } + ) + _OPENAI_ONLY_RESPONSES_FIELDS: frozenset[str] = frozenset() + _OPENAI_ONLY_CHAT_COMPAT_FIELDS = frozenset( + { + "store", + "prompt_cache_key", + "prompt_cache_options", + "prompt_cache_retention", + "safety_identifier", + "moderation", + "verbosity", + } + ) + _BACKGROUND_OPTION_KEYS = frozenset( + { + "background", + "background_poll_interval", + "background_timeout", + "responses_poll_interval", + "responses_poll_timeout", + } + ) + # Model-level options are defaults for the corresponding SDK request. Keep + # the allow-list separate from request-level ``extra_body`` so a typo cannot + # reach the OpenAI SDK as an unknown keyword argument. + _CHAT_MODEL_OPTION_KEYS = frozenset( + { + "temperature", "max_tokens", "max_completion_tokens", "top_p", + "frequency_penalty", "presence_penalty", "n", "logit_bias", + "logprobs", "top_logprobs", "modalities", "audio", "prediction", + "web_search_options", "seed", "stop", "response_format", + "tool_choice", "metadata", "user", "reasoning_effort", + "stream_options", "parallel_tool_calls", "store", "service_tier", + "prompt_cache_key", "prompt_cache_options", "prompt_cache_retention", + "safety_identifier", "moderation", "verbosity", "extra_body", + } + ) + _RESPONSES_MODEL_OPTION_KEYS = frozenset( + { + "background", "context_management", "conversation", "include", + "max_output_tokens", "max_tool_calls", "metadata", "moderation", + "parallel_tool_calls", "previous_response_id", "prompt", + "prompt_cache_key", "prompt_cache_options", "prompt_cache_retention", + "reasoning", "reasoning_effort", "safety_identifier", "service_tier", + "store", "stream_options", "temperature", "text", "tool_choice", + "tools", "top_logprobs", "top_p", "truncation", "user", + # Portable/common names normalized by this adapter. + "max_tokens", "max_completion_tokens", "response_format", "stop", "seed", + "verbosity", + "top_k", "frequency_penalty", "presence_penalty", "extra_body", + } + ) + _EMBEDDING_MODEL_OPTION_KEYS = frozenset( + {"dimensions", "encoding_format", "user", "extra_body"} + ) + _EMBEDDING_CLIENT_OPTION_KEYS = frozenset( + {"timeout", "max_retries", "server_verified_protocols"} + ) + + def __init__( + self, + model_name: str, + provider: str, + settings: Any | None = None, + protocol: str = "chat_completions", + options: dict[str, Any] | None = None, + ): self._model_name = model_name self._provider = provider - settings = get_settings() + self._protocol = self._normalize_protocol(protocol) + self._options = validate_secret_free_options(options, provider) + self._server_verified_protocols = self._normalize_verified_protocols( + self._options.get("server_verified_protocols", ()) + ) + settings = settings if settings is not None else get_settings() - api_key_name = f"{provider.lower()}_api_key" - base_url_name = f"{provider.lower()}_base_url" + settings_prefix = provider.lower().replace("-", "_") + api_key_name = f"{settings_prefix}_api_key" + base_url_name = f"{settings_prefix}_base_url" self._api_key = getattr(settings, api_key_name, None) self._base_url = getattr(settings, base_url_name, None) + if self._base_url is None: + # OpenAI 旧配置字段使用 `openai_api_base`,继续兼容现有配置文件。 + self._base_url = getattr(settings, f"{settings_prefix}_api_base", None) if provider in ["ollama", "lm-studio"] and not self._api_key: self._api_key = "no-key-required" @@ -37,133 +184,5004 @@ def __init__(self, model_name: str, provider: str): if not self._api_key: logger.error(f"{provider} API Key 未设置。") raise ValueError(f"{api_key_name} is required for {provider}") + if provider != "openai" and not self._base_url: + logger.error(f"{provider} Base URL 未设置。") + raise ValueError(f"{base_url_name} is required for {provider}") - self._client: Optional[openai.OpenAI] = None - self._aclient: Optional[openai.AsyncOpenAI] = None + self._client: openai.OpenAI | None = None + self._aclient: openai.AsyncOpenAI | None = None logger.info(f"初始化 OpenAICompatibleProvider ({provider}),模型: {model_name}") + @property + def resources(self) -> OpenAICompatibleResources: + return OpenAICompatibleResources(self) + + @property + def async_resources(self) -> AsyncOpenAICompatibleResources: + return AsyncOpenAICompatibleResources(self) + + @staticmethod + def _normalize_protocol(protocol: str) -> str: + normalized = str(protocol).strip().lower().replace("-", "_") + if normalized in {"chat", "completion", "chat_completion", "chat_completions"}: + return "chat_completions" + if normalized in {"response", "responses"}: + return "responses" + raise ValueError( + f"OpenAI 兼容渠道不支持 {protocol} 协议。可选值: chat_completions, responses" + ) + + @classmethod + def _normalize_verified_protocols(cls, configured: Any) -> frozenset[str]: + """规范化服务端协议登记,并在配置边界给出明确错误。""" + if configured is None: + return frozenset() + if isinstance(configured, str): + configured = (configured,) + elif not isinstance(configured, (list, tuple, set, frozenset)): + raise ValueError( + "server_verified_protocols 必须是字符串或协议序列。" + ) + normalized: set[str] = set() + for value in configured: + if not isinstance(value, str) or not value.strip(): + raise ValueError( + "server_verified_protocols 中的协议必须是非空字符串。" + ) + normalized.add(cls._normalize_protocol(value)) + return frozenset(normalized) + def _get_client(self) -> openai.OpenAI: if self._client is None: - self._client = openai.OpenAI(api_key=self._api_key, base_url=self._base_url) + client_options = { + key: value + for key, value in self._options.items() + if key in {"timeout", "max_retries"} + } + self._client = openai.OpenAI(api_key=self._api_key, base_url=self._base_url, **client_options) return self._client def _get_aclient(self) -> openai.AsyncOpenAI: if self._aclient is None: - self._aclient = openai.AsyncOpenAI(api_key=self._api_key, base_url=self._base_url) + client_options = { + key: value + for key, value in self._options.items() + if key in {"timeout", "max_retries"} + } + self._aclient = openai.AsyncOpenAI(api_key=self._api_key, base_url=self._base_url, **client_options) return self._aclient - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) + @staticmethod + async def _resolve_async_result(result: Any) -> Any: + """兼容 SDK 的协程方法和同步分页器返回值。""" + if inspect.isawaitable(result): + return await result + return result + + @staticmethod + def _validate_sdk_resource_call( + method: Any, + args: tuple[Any, ...], + kwargs: Mapping[str, Any], + operation: str, + ) -> None: + """在调用资源 SDK 前校验参数名和必填字段。 + + 官方 SDK 方法通常没有 ``**kwargs``,因此 ``Signature.bind`` 可以在 + 请求发出前同时拦截未知参数和缺失必填参数。测试替身或未来 SDK 若 + 暴露动态签名,则保留原有转发能力。 + """ + try: + signature = inspect.signature(method) + except (TypeError, ValueError): + return + try: + # Bind partially first so an unknown keyword is reported even when + # the same call also omits another required argument. This keeps + # the actionable caller error stable instead of depending on the + # order in which ``inspect.Signature`` enumerates parameters. + signature.bind_partial(*args, **dict(kwargs)) + signature.bind(*args, **dict(kwargs)) + except TypeError as exc: + raise ValueError(f"{operation} SDK 参数不匹配: {exc}") from exc + except ValueError as exc: + # A few extension/builtin signatures raise ValueError while + # binding positional-only arguments. Do not silently skip this + # validation boundary; surface the SDK contract failure instead. + raise ValueError(f"{operation} SDK 参数无法验证: {exc}") from exc + + def _call_sdk_resource( + self, + method: Any, + args: tuple[Any, ...] = (), + kwargs: Mapping[str, Any] | None = None, + operation: str = "OpenAI 资源", + ) -> Any: + """校验并调用一个 OpenAI SDK 资源方法。 + + Provider 资源入口保留 ``**kwargs`` 是为了兼容 SDK 的扩展字段; + 这里把实际 SDK 方法签名作为最后一道边界,确保未知参数和缺失的 + 必填参数在请求发出前转换成项目统一的 ``ValueError``。对无法 + introspect 的第三方替身仍保持原有转发能力。 + """ + call_args = validate_secret_free_resource_args( + args, + f"{getattr(self, '_provider', 'OpenAI')} {operation}", + ) + call_kwargs = validate_secret_free_resource_kwargs( + kwargs or {}, + f"{getattr(self, '_provider', 'OpenAI')} {operation}", + ) + self._validate_sdk_resource_call(method, call_args, call_kwargs, operation) + return method(*call_args, **call_kwargs) + + async def _call_async_sdk_resource( + self, + method: Any, + args: tuple[Any, ...] = (), + kwargs: Mapping[str, Any] | None = None, + operation: str = "OpenAI 资源", + ) -> Any: + """异步版本的 SDK 资源签名校验与调用。""" + return await self._resolve_async_result( + self._call_sdk_resource(method, args, kwargs, operation) + ) + + def _request_options(self) -> dict[str, Any]: + """返回调用方配置的受控扩展参数;凭证不会进入 ModelDetail.options。""" + validate_secret_free_options( + getattr(self, "_options", {}) or {}, + getattr(self, "_provider", "openai"), + ) + client_only = {"timeout", "max_retries", "server_verified_protocols"} + non_openai_top_level = {"top_k", "repetition_penalty"} + return { + key: value + for key, value in (getattr(self, "_options", {}) or {}).items() + if key not in client_only + and key not in non_openai_top_level + and key not in self._BACKGROUND_OPTION_KEYS + } + + @staticmethod + def _validate_model_options( + options: Mapping[str, Any], + allowed: frozenset[str], + endpoint: str, + ) -> None: + unknown = { + key: value + for key, value in options.items() + if key not in allowed + } + reject_unsupported_kwargs(endpoint, unknown) + + def _background_poll_config(self) -> tuple[float, float]: + options = getattr(self, "_options", {}) or {} + interval = options.get( + "background_poll_interval", + options.get("responses_poll_interval", 1.0), + ) + timeout = options.get( + "background_timeout", + options.get("responses_poll_timeout", 300.0), + ) + if ( + isinstance(interval, bool) + or not isinstance(interval, (int, float)) + or not math.isfinite(float(interval)) + or interval <= 0 + ): + raise ValueError("Responses background_poll_interval 必须是正数。") + if ( + isinstance(timeout, bool) + or not isinstance(timeout, (int, float)) + or not math.isfinite(float(timeout)) + or timeout <= 0 + ): + raise ValueError("Responses background_timeout 必须是正数。") + return float(interval), float(timeout) + + @classmethod + def _response_retrieve_kwargs(cls, params: Mapping[str, Any]) -> dict[str, Any]: + return { + key: params[key] + for key in ("include", "extra_headers", "extra_query", "extra_body", "timeout") + if key in params and params[key] is not None + } + + def _poll_background_response(self, response: Any, params: Mapping[str, Any]) -> Any: + status = self._field(response, "status") + if status not in {"queued", "in_progress", "pending"}: + return response + response_id = self._field(response, "id") + if not response_id: + raise RuntimeError("Responses background 响应缺少 id,无法轮询。") + interval, timeout = self._background_poll_config() + deadline = time.monotonic() + timeout + retrieve_kwargs = self._response_retrieve_kwargs(params) + while status in {"queued", "in_progress", "pending"}: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError( + f"Responses background 响应 {response_id} 在 {timeout:g}s 内未完成。" + ) + if interval: + time.sleep(min(interval, remaining)) + response = retry_sync_call( + lambda: self._get_client().responses.retrieve(response_id, **retrieve_kwargs) + ) + status = self._field(response, "status") + return response + + async def _poll_background_response_async(self, response: Any, params: Mapping[str, Any]) -> Any: + status = self._field(response, "status") + if status not in {"queued", "in_progress", "pending"}: + return response + response_id = self._field(response, "id") + if not response_id: + raise RuntimeError("Responses background 响应缺少 id,无法轮询。") + interval, timeout = self._background_poll_config() + deadline = time.monotonic() + timeout + retrieve_kwargs = self._response_retrieve_kwargs(params) + while status in {"queued", "in_progress", "pending"}: + remaining = deadline - time.monotonic() + if remaining <= 0: + raise TimeoutError( + f"Responses background 响应 {response_id} 在 {timeout:g}s 内未完成。" + ) + if interval: + await asyncio.sleep(min(interval, remaining)) + response = await retry_async_call( + lambda: self._get_aclient().responses.retrieve(response_id, **retrieve_kwargs) + ) + status = self._field(response, "status") + return response + + def _compat_extra_options(self) -> dict[str, Any]: + return { + key: value + for key, value in (getattr(self, "_options", {}) or {}).items() + if key in {"top_k", "repetition_penalty"} + } + + def _validate_openai_field_compatibility( + self, values: Mapping[str, Any], endpoint: str, protocol: str | None = None + ) -> None: + """拒绝当前协议不支持或兼容端点未声明的 OpenAI 字段。""" + if self._uses_official_openai_endpoint(): + return + effective_protocol = protocol or getattr(self, "_protocol", "chat_completions") + if effective_protocol == "responses": + unsupported_fields = self._OPENAI_ONLY_RESPONSES_FIELDS + else: + unsupported_fields = ( + self._OPENAI_ONLY_CHAT_FIELDS | self._OPENAI_ONLY_CHAT_COMPAT_FIELDS + ) + unsupported = { + key: value + for key, value in values.items() + if key in unsupported_fields and value is not None + } + reject_unsupported_kwargs( + f"{getattr(self, '_provider', 'compatible')} {endpoint}", + unsupported, + ) + + def _validate_chat_field_compatibility(self, values: Mapping[str, Any]) -> None: + self._validate_openai_field_compatibility(values, "Chat Completions") + + @staticmethod + def _resource_capability_for_method(method_name: str) -> str | None: + """将 OpenAI 资源入口映射到统一的能力名称。""" + name = method_name.removeprefix("async_") + if name in { + "retrieve_response", + "cancel_response", + "delete_response", + "list_response_input_items", + "connect_responses", + "stream_responses", + "count_input_tokens", + "compact_responses", + }: + return "responses" + if name in { + "upload_file", + "list_files", + "retrieve_file", + "file_content", + "retrieve_file_content", + "delete_file", + "wait_for_file", + }: + return "files" + if name in { + "create_upload", + "complete_upload", + "cancel_upload", + "create_upload_part", + "upload_file_chunked", + }: + return "uploads" + if name == "text_to_speech": + return "audio" + prefixes = ( + ("vector_store", "vector_stores"), + ("fine_tuning", "fine_tuning"), + ("eval", "evals"), + ("conversation", "conversations"), + ("container", "containers"), + ("upload", "uploads"), + ("video", "videos"), + ("image", "images"), + ("audio", "audio"), + ("moderation", "moderation"), + ("model", "models"), + ("batch", "batches"), + ) + for prefix, capability in prefixes: + if name.startswith(prefix) or f"_{prefix}" in name: + return capability + return None + + def _require_provider_resource(self, method_name: str) -> None: + """在原生资源方法真正触达 SDK 前校验渠道能力。""" + capability = self._resource_capability_for_method(method_name) + if capability is None: + return + if capability == "responses": + self._require_responses_resource(method_name) + return + # An object created with ``object.__new__`` has no instance metadata; + # in that case there is no provider identity to gate and the SDK + # signature/resource checks below should remain testable. A real + # OpenAI provider is identified explicitly by ``_provider`` and gets + # the same resource set as the dedicated subclass. + provider_name = getattr(self, "_provider", None) + if provider_name is None: + return + declared: frozenset[str] = getattr(self, "_resource_capabilities", frozenset()) + if provider_name == "openai" and not declared: + declared = self._OFFICIAL_OPENAI_RESOURCE_CAPABILITIES + if capability not in declared: + raise NotImplementedError( + f"{provider_name} 未声明 {capability} 资源能力。" + ) + + def _chat_max_tokens_key(self) -> str: + """返回当前 Chat Completions 端点的输出长度字段名。""" + # OpenAI 官方 SDK 3.x 已将该字段更名为 max_completion_tokens。 + # 绝大多数兼容网关(Qwen、DeepSeek、Ollama、LM Studio、Grok、 + # SiliconFlow)仍按 Chat Completions 契约使用 max_tokens。 + provider = getattr(self, "_provider", "openai") + return ( + "max_completion_tokens" + if provider == "openai" and self._uses_official_openai_endpoint() + else "max_tokens" + ) + + def _embedding_options(self) -> dict[str, Any]: + """校验并返回 Embeddings 端点接受的模型级参数。 + + 聊天参数不能复用到 Embeddings,否则 OpenAI SDK 会在本地校验阶段 + 抛出 TypeError,兼容渠道也可能收到无效字段。客户端级配置仍可 + 放在同一份 provider options 中,但不会被转发为 Embeddings 请求字段。 + """ + options = getattr(self, "_options", {}) or {} + supported = self._EMBEDDING_MODEL_OPTION_KEYS + allowed = supported | self._EMBEDDING_CLIENT_OPTION_KEYS + reject_unsupported_kwargs( + f"{getattr(self, '_provider', 'OpenAI-compatible')} Embedding 模型 options", + {key: value for key, value in options.items() if key not in allowed}, + ) + return { + key: value + for key, value in options.items() + if key in supported + } + + @staticmethod + def _validate_embedding_kwargs(kwargs: Mapping[str, Any]) -> None: + allowed = { + "dimensions", + "encoding_format", + "user", + "extra_headers", + "extra_query", + "extra_body", + "timeout", + } + client_only = { + "base_url", + "default_headers", + "default_query", + "http_client", + "max_retries", + "server_verified_protocols", + } + unsupported = { + key: value + for key, value in kwargs.items() + if key not in allowed or key in client_only + } + reject_unsupported_kwargs("OpenAI Embedding", unsupported) + validate_secret_free_request_overrides( + kwargs.get("extra_headers"), + kwargs.get("extra_query"), + "OpenAI Embedding", + ) + + def _uses_official_openai_endpoint(self) -> bool: + """只把 OpenAI 官方 Responses 端点视为默认已知资源契约。 + + ``provider='openai'`` 也可用于代理或自建兼容网关,因此不能仅凭 + provider 名称放行 SDK 的专属资源。缺少 ``base_url`` 时沿用 SDK + 默认端点语义;显式 URL 则必须精确指向 ``https://api.openai.com/v1``。 + """ + base_url = getattr(self, "_base_url", None) + if not base_url: + # 只有 OpenAI Provider 省略 Base URL 时才会落到 SDK 官方默认端点。 + return getattr(self, "_provider", "openai") == "openai" + try: + parsed = urlsplit(str(base_url).strip()) + port = parsed.port + except ValueError: + return False + return ( + parsed.scheme.lower() == "https" + and parsed.hostname is not None + and parsed.hostname.lower() == "api.openai.com" + and (port in (None, 443)) + and parsed.path.rstrip("/") == "/v1" + and not parsed.query + and not parsed.fragment + ) + + def _require_responses_resource(self, operation: str) -> None: + """阻止兼容渠道把本地 SDK 资源误当成远端 Responses 能力。""" + if self._protocol != "responses": + raise ValueError(f"{operation} 仅适用于 responses 协议。") + # 官方端点由其 SDK 契约覆盖;代理、自建网关和其它兼容渠道必须 + # 显式声明已验证的服务端协议,避免本地 SDK 资源导致远端 404/405。 + provider = getattr(self, "_provider", "openai") + verified = "responses" in getattr(self, "_server_verified_protocols", ()) + if not verified and not self._uses_official_openai_endpoint(): + raise ValueError( + f"{provider} 未验证 Responses {operation} 的服务端能力;" + "请在 provider options 中显式配置 server_verified_protocols=['responses']。" + ) + + @staticmethod + def _merge_options(request: dict[str, Any], options: dict[str, Any]) -> None: + reserved = {"model", "messages", "input", "stream", "instructions", "tools"} + overlap = sorted(reserved.intersection(options)) + if overlap: + raise ValueError(f"Provider options 不允许覆盖请求字段: {', '.join(overlap)}") + # options 是模型级默认值;调用级字段在后续构造阶段覆盖它们。 + for key, value in options.items(): + request.setdefault(key, value) + + @staticmethod + def _validated_extra_body( + value: Any, + endpoint: str, + reserved: set[str], + ) -> dict[str, Any]: + """校验 SDK 的扩展请求体,避免字符串或保留字段穿透到请求边界。""" + if value is None: + return {} + if not isinstance(value, Mapping): + raise ValueError(f"{endpoint} extra_body 必须是对象。") + value = validate_secret_free_payload(value, endpoint, "extra_body") + overlap = sorted(set(value).intersection(reserved)) + if overlap: + raise ValueError( + f"{endpoint} extra_body 不允许覆盖请求字段: {', '.join(overlap)}" + ) + return dict(value) + + @staticmethod + def _merge_extra_body( + configured: Mapping[str, Any] | None, + extensions: Mapping[str, Any] | None, + endpoint: str, + ) -> dict[str, Any]: + """合并模型级扩展,拒绝同一字段的静默覆盖。""" + configured_values = dict(configured or {}) + extension_values = dict(extensions or {}) + overlap = sorted(set(configured_values).intersection(extension_values)) + if overlap: + raise ValueError( + f"{endpoint} 模型 options 的扩展字段重复: {', '.join(overlap)}" + ) + return {**configured_values, **extension_values} + + @staticmethod + def _merge_resource_kwargs( + fixed: Mapping[str, Any], + overrides: Mapping[str, Any] | None, + operation: str, + *, + omit_none: bool = False, + ) -> dict[str, Any]: + """合并资源入口参数,拒绝调用方覆盖 Facade 已绑定的字段。""" + fixed_values = { + key: value + for key, value in fixed.items() + if not omit_none or value is not None + } + override_values = dict(overrides or {}) + overlap = sorted(set(fixed_values).intersection(override_values)) + if overlap: + raise ValueError( + f"{operation} 资源 SDK 参数重复: {', '.join(overlap)}" + ) + return {**fixed_values, **override_values} + + def _build_chat_request( + self, + prompt: str | None = None, + system_prompt: str | None = None, + tools: list[dict[str, Any]] | None = None, + temperature: float | None = None, + stream: bool = True, + *, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + repetition_penalty: float | None = None, + n: int | None = None, + logit_bias: dict[str, int] | None = None, + logprobs: bool | None = None, + top_logprobs: int | None = None, + modalities: list[str] | None = None, + audio: dict[str, Any] | None = None, + prediction: dict[str, Any] | None = None, + web_search_options: dict[str, Any] | None = None, + seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + user: str | None = None, + timeout: float | None = None, + reasoning: dict[str, Any] | None = None, + stream_options: dict[str, Any] | None = None, + parallel_tool_calls: bool | None = None, + store: bool | None = None, + prompt_cache_key: str | None = None, + prompt_cache_options: dict[str, Any] | None = None, + prompt_cache_retention: str | None = None, + safety_identifier: str | None = None, + moderation: dict[str, Any] | None = None, + service_tier: str | None = None, + verbosity: str | None = None, + **ignored: Any, + ) -> dict[str, Any]: + reject_unsupported_kwargs("OpenAI Chat Completions", ignored) + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + f"{getattr(self, '_provider', 'openai')} Chat Completions", + ) + # Chat Completions 要求 assistant 历史的每个 tool_call 都带 id; + # 这里显式校验,其它协议继续接受 Gemini 这类无 id 的历史。 + normalized_messages = normalize_messages( + prompt, + system_prompt, + messages, + require_tool_call_ids=True, + ) + + request: dict[str, Any] = { + "model": self._model_name, + "messages": normalized_messages, + "stream": stream, + } + configured_options = self._request_options() + self._validate_model_options( + configured_options, + self._CHAT_MODEL_OPTION_KEYS, + f"{getattr(self, '_provider', 'compatible')} Chat Completions", + ) + configured_max_tokens = configured_options.pop("max_tokens", None) + configured_max_completion_tokens = configured_options.pop("max_completion_tokens", None) + if configured_max_tokens is not None and configured_max_completion_tokens is not None: + raise ValueError( + "Chat Completions options 不能同时设置 max_tokens 和 " + "max_completion_tokens。" + ) + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Chat Completions", + {"model", "messages", "input", "stream", "instructions", "tools"}, + ) + configured_explicit_extra_keys = set(configured_extra_body) + self._validate_chat_field_compatibility(configured_options) + self._merge_options(request, configured_options) + configured_extra = self._compat_extra_options() + merged_configured_extra = self._merge_extra_body( + configured_extra_body, configured_extra, "Chat Completions" + ) + if merged_configured_extra: + request["extra_body"] = merged_configured_extra + if temperature is not None: + request["temperature"] = temperature + elif "temperature" not in request: + request["temperature"] = 0.7 + if tools: + request["tools"] = tools + max_tokens_key = self._chat_max_tokens_key() + if max_tokens is not None: + request[max_tokens_key] = max_tokens + request.pop("max_tokens" if max_tokens_key != "max_tokens" else "max_completion_tokens", None) + elif configured_max_tokens is not None and max_tokens_key not in request: + request[max_tokens_key] = configured_max_tokens + request.pop("max_tokens" if max_tokens_key != "max_tokens" else "max_completion_tokens", None) + elif configured_max_completion_tokens is not None and max_tokens_key not in request: + request[max_tokens_key] = configured_max_completion_tokens + request.pop("max_tokens" if max_tokens_key != "max_tokens" else "max_completion_tokens", None) + if top_p is not None: + request["top_p"] = top_p + request_extensions = { + "frequency_penalty": frequency_penalty, + "presence_penalty": presence_penalty, + "n": n, + "logit_bias": logit_bias, + "logprobs": logprobs, + "top_logprobs": top_logprobs, + "modalities": modalities, + "audio": audio, + "prediction": prediction, + "web_search_options": web_search_options, + "parallel_tool_calls": parallel_tool_calls, + "store": store, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "safety_identifier": safety_identifier, + "moderation": moderation, + "service_tier": service_tier, + "verbosity": verbosity, + } + self._validate_chat_field_compatibility(request_extensions) + for key, value in request_extensions.items(): + if value is not None: + request[key] = value + if repetition_penalty is not None: + # repetition_penalty 是部分 OpenAI 兼容服务的扩展参数, + # OpenAI 官方 Chat Completions 不接受它作为顶层字段。 + if "repetition_penalty" in configured_explicit_extra_keys: + raise ValueError( + "Chat Completions repetition_penalty 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["repetition_penalty"] = repetition_penalty + if top_k is not None and self._protocol == "chat_completions": + if "top_k" in configured_explicit_extra_keys: + raise ValueError( + "Chat Completions top_k 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["top_k"] = top_k + if seed is not None: + if "seed" in configured_explicit_extra_keys: + raise ValueError("Chat Completions seed 与模型 options.extra_body 重复。") + request["seed"] = seed + if stop is not None: + request["stop"] = stop + if response_format is not None: + request["response_format"] = response_format + if tool_choice is not None: + request["tool_choice"] = tool_choice + if metadata is not None: + request["metadata"] = metadata + if user is not None: + request["user"] = user + if timeout is not None: + request["timeout"] = timeout + if extra_headers is not None: + request["extra_headers"] = extra_headers + if extra_query is not None: + request["extra_query"] = extra_query + if reasoning is not None: + effort = reasoning.get("effort") if isinstance(reasoning, Mapping) else None + if effort is not None: + if "reasoning" in configured_explicit_extra_keys: + raise ValueError( + "Chat Completions reasoning 与模型 options.extra_body 重复。" + ) + request["reasoning_effort"] = effort + else: + if "reasoning" in configured_explicit_extra_keys: + raise ValueError( + "Chat Completions reasoning 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["reasoning"] = reasoning + if stream_options is not None: + request["stream_options"] = stream_options + request_extra_body = self._validated_extra_body( + extra_body, + "Chat Completions", + set(request).difference({"extra_body"}), + ) + if request_extra_body: + configured_overlap = sorted( + set(request.get("extra_body", {})).intersection(request_extra_body) + ) + if configured_overlap: + raise ValueError( + "Chat Completions extra_body 与模型 options 重复: " + + ", ".join(configured_overlap) + ) + request.setdefault("extra_body", {}).update(request_extra_body) + if request.get("extra_body"): + request["extra_body"] = self._validated_extra_body( + request["extra_body"], + "Chat Completions", + set(request).difference({"extra_body"}), + ) + return request + + @staticmethod + def _convert_responses_tools(tools: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None: + if not tools: + return None + + converted: list[dict[str, Any]] = [] + for index, tool in enumerate(tools): + if not isinstance(tool, Mapping): + raise ValueError(f"Responses 工具定义[{index}] 必须是对象。") + if "function" not in tool: + native_tool = dict(tool) + tool_type = native_tool.get("type") + if not isinstance(tool_type, str) or not tool_type.strip(): + raise ValueError( + f"Responses 工具定义[{index}] 缺少有效 type。" + ) + if tool_type == "function": + name = native_tool.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError( + f"Responses 工具定义[{index}] 缺少 name。" + ) + parameters = native_tool.get( + "parameters", {"type": "object", "properties": {}} + ) + if not isinstance(parameters, Mapping): + raise ValueError( + f"Responses 工具定义[{index}].parameters 必须是对象。" + ) + native_tool["parameters"] = dict(parameters) + native_tool.setdefault("strict", False) + converted.append(native_tool) + continue + + function = tool["function"] + if not isinstance(function, Mapping): + raise ValueError( + f"Responses 工具定义[{index}].function 必须是对象。" + ) + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError(f"Responses 工具定义[{index}] 缺少 function.name。") + parameters = function.get( + "parameters", {"type": "object", "properties": {}} + ) + if not isinstance(parameters, Mapping): + raise ValueError( + f"Responses 工具定义[{index}].function.parameters 必须是对象。" + ) + response_tool: dict[str, Any] = { + "type": "function", + "name": name, + "parameters": dict(parameters), + "strict": function.get("strict", tool.get("strict", False)), + } + if "description" in function: + response_tool["description"] = function["description"] + for key in ("allowed_callers", "defer_loading", "output_schema"): + if key in function: + response_tool[key] = function[key] + converted.append(response_tool) + return converted + + @staticmethod + def _convert_responses_tool_choice(tool_choice: Any) -> Any: + """将 Chat Completions 风格的工具选择转换为 Responses 形状。 + + Responses 的指定函数形式是扁平的 ``{type, name}``,而统一请求 + 也接受 ``{type: function, function: {name}}``。原生 Responses + 工具选择以及字符串模式保持不变,便于调用方直接使用 SDK 形状。 + """ + if tool_choice is None or isinstance(tool_choice, str): + return tool_choice + if not isinstance(tool_choice, Mapping): + raise ValueError("Responses tool_choice 必须是字符串或对象。") + + choice = dict(tool_choice) + if choice.get("type") != "function": + return choice + function = choice.get("function") + if isinstance(function, Mapping): + name = function.get("name") + if not name: + raise ValueError("Responses 的 function tool_choice 缺少 name。") + converted = {key: value for key, value in choice.items() if key != "function"} + converted["name"] = name + return converted + if isinstance(choice.get("name"), str) and choice["name"]: + return choice + raise ValueError("Responses 的 function tool_choice 缺少 name。") + + def _build_responses_request( + self, + prompt: str | None = None, + system_prompt: str | None = None, + tools: list[dict[str, Any]] | None = None, + temperature: float | None = None, + stream: bool = True, + *, + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + frequency_penalty: float | None = None, + presence_penalty: float | None = None, + seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + user: str | None = None, + timeout: float | None = None, + previous_response_id: str | None = None, + include: list[str] | None = None, + reasoning: dict[str, Any] | None = None, + store: bool | None = None, + background: bool | None = None, + parallel_tool_calls: bool | None = None, + max_tool_calls: int | None = None, + conversation: str | dict[str, Any] | None = None, + session: dict[str, Any] | None = None, + context_management: dict[str, Any] | None = None, + caching: dict[str, Any] | None = None, + expire_at: int | None = None, + prompt_cache_options: dict[str, Any] | None = None, + safety_identifier: str | None = None, + moderation: dict[str, Any] | None = None, + top_logprobs: int | None = None, + verbosity: str | None = None, + prompt_cache_key: str | None = None, + prompt_cache_retention: str | None = None, + service_tier: str | None = None, + truncation: str | None = None, + stream_options: dict[str, Any] | None = None, + thinking: dict[str, Any] | None = None, + **ignored: Any, + ) -> dict[str, Any]: + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + f"{getattr(self, '_provider', 'openai')} Responses", + ) + reject_unsupported_kwargs( + "OpenAI Responses", + { + "thinking": thinking, + "caching": caching, + "session": session, + "expire_at": expire_at, + }, + ) + reject_unsupported_kwargs("OpenAI Responses", ignored) + input_value = normalize_responses_input( + prompt, + system_prompt, + messages, + provider=getattr(self, "_provider", "openai"), + ) + request: dict[str, Any] = { + "model": self._model_name, + "input": input_value, + "stream": stream, + } + configured_options = self._request_options() + self._validate_model_options( + configured_options, + self._RESPONSES_MODEL_OPTION_KEYS, + f"{getattr(self, '_provider', 'compatible')} Responses", + ) + # Keep the common model configuration ergonomic while emitting the + # current Responses SDK shape. These names are valid for Chat + # Completions, but Responses expects them under a different field. + configured_response_format = configured_options.pop("response_format", None) + configured_verbosity = configured_options.pop("verbosity", None) + configured_max_tokens = configured_options.pop("max_tokens", None) + configured_max_completion_tokens = configured_options.pop("max_completion_tokens", None) + configured_max_output_tokens = configured_options.get("max_output_tokens") + configured_limits = { + key: value + for key, value in { + "max_output_tokens": configured_max_output_tokens, + "max_tokens": configured_max_tokens, + "max_completion_tokens": configured_max_completion_tokens, + }.items() + if value is not None + } + if len(configured_limits) > 1: + raise ValueError( + "Responses options 不能同时设置多个输出长度字段: " + + ", ".join(configured_limits) + ) + if configured_max_output_tokens is not None: + configured_options.pop("max_output_tokens", None) + configured_frequency_penalty = configured_options.pop("frequency_penalty", None) + configured_presence_penalty = configured_options.pop("presence_penalty", None) + configured_top_logprobs = configured_options.pop("top_logprobs", None) + configured_unsupported = { + key: configured_options.pop(key, None) + for key in ("thinking", "caching", "session", "expire_at") + } + reject_unsupported_kwargs("OpenAI Responses", configured_unsupported) + configured_background = (getattr(self, "_options", {}) or {}).get("background") + configured_stop = configured_options.pop("stop", None) + configured_seed = configured_options.pop("seed", None) + configured_reasoning_effort = configured_options.pop("reasoning_effort", None) + configured_text = configured_options.pop("text", None) + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Responses", + {"model", "messages", "input", "stream", "instructions", "tools"}, + ) + configured_explicit_extra_keys = set(configured_extra_body) + self._validate_openai_field_compatibility(configured_options, "Responses") + if configured_text is not None: + if not isinstance(configured_text, Mapping): + raise ValueError("Responses options.text 必须是对象。") + request["text"] = dict(configured_text) + self._merge_options(request, configured_options) + configured_extra = self._compat_extra_options() + for key, value in (("stop", configured_stop), ("seed", configured_seed)): + if value is None: + continue + if key in configured_extra_body or key in configured_extra: + raise ValueError(f"Responses options.{key} 与 extra_body 重复。") + configured_extra[key] = value + merged_configured_extra = self._merge_extra_body( + configured_extra_body, configured_extra, "Responses" + ) + if merged_configured_extra: + request["extra_body"] = merged_configured_extra + if temperature is not None: + request["temperature"] = temperature + if system_prompt and messages is None: + request["instructions"] = system_prompt + converted_tools = self._convert_responses_tools(tools) + if converted_tools: + request["tools"] = converted_tools + if max_tokens is not None: + request["max_output_tokens"] = max_tokens + elif configured_limits: + request["max_output_tokens"] = next(iter(configured_limits.values())) + if top_p is not None: + request["top_p"] = top_p + effective_frequency_penalty = ( + frequency_penalty if frequency_penalty is not None else configured_frequency_penalty + ) + effective_presence_penalty = ( + presence_penalty if presence_penalty is not None else configured_presence_penalty + ) + unsupported_sampling = { + key: value + for key, value in { + "frequency_penalty": effective_frequency_penalty, + "presence_penalty": effective_presence_penalty, + }.items() + if value is not None + } + # OpenAI Responses 3.3 has no top-level stop/seed/frequency/presence + # fields. Keep these useful extensions for compatible gateways, but fail + # early for the official endpoint instead of sending a misleading request. + if self._uses_official_openai_endpoint(): + reject_unsupported_kwargs("OpenAI Responses", unsupported_sampling) + for key, value in unsupported_sampling.items(): + if key in configured_explicit_extra_keys: + raise ValueError(f"Responses {key} 与模型 options.extra_body 重复。") + request.setdefault("extra_body", {})[key] = value + # OpenAI Responses 3.3 has no top-level stop/seed/top_k fields. Keep + # these useful extensions for compatible gateways, but fail early for + # the official endpoint instead of sending a misleading request. + unsupported_official = { + key: value + for key, value in { + "stop": stop, + "seed": seed, + "top_k": top_k, + "frequency_penalty": frequency_penalty, + "presence_penalty": presence_penalty, + }.items() + if value is not None + } + configured_unsupported_official: dict[str, Any] = { + key: value + for key, value in { + **{ + key: self._options.get(key) + for key in ("stop", "seed", "top_k", "frequency_penalty", "presence_penalty") + if key in getattr(self, "_options", {}) + }, + **{ + key: merged_configured_extra.get(key) + for key in ( + "stop", + "seed", + "top_k", + "frequency_penalty", + "presence_penalty", + ) + if key in merged_configured_extra + }, + }.items() + if value is not None + } + if configured_unsupported_official and self._uses_official_openai_endpoint(): + reject_unsupported_kwargs("OpenAI Responses", configured_unsupported_official) + if unsupported_official and self._uses_official_openai_endpoint(): + reject_unsupported_kwargs("OpenAI Responses", unsupported_official) + if top_k is not None: + if "top_k" in configured_explicit_extra_keys: + raise ValueError("Responses top_k 与模型 options.extra_body 重复。") + request.setdefault("extra_body", {})["top_k"] = top_k + if seed is not None: + if "seed" in configured_explicit_extra_keys: + raise ValueError("Responses seed 与模型 options.extra_body 重复。") + request.setdefault("extra_body", {})["seed"] = seed + if stop is not None: + if "stop" in configured_explicit_extra_keys: + raise ValueError("Responses stop 与模型 options.extra_body 重复。") + request.setdefault("extra_body", {})["stop"] = stop + effective_response_format = ( + response_format if response_format is not None else configured_response_format + ) + if effective_response_format is not None: + request.setdefault("text", {}).update( + self._convert_responses_format(effective_response_format) + ) + response_extensions = { + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "safety_identifier": safety_identifier, + "moderation": moderation, + "verbosity": verbosity, + "store": store, + } + self._validate_openai_field_compatibility( + response_extensions, "Responses", protocol="responses" + ) + if tool_choice is not None: + request["tool_choice"] = self._convert_responses_tool_choice(tool_choice) + elif "tool_choice" in request: + request["tool_choice"] = self._convert_responses_tool_choice(request["tool_choice"]) + if metadata is not None: + request["metadata"] = metadata + if user is not None: + request["user"] = user + if timeout is not None: + request["timeout"] = timeout + if extra_headers is not None: + request["extra_headers"] = extra_headers + if extra_query is not None: + request["extra_query"] = extra_query + if previous_response_id is not None: + request["previous_response_id"] = previous_response_id + if include is not None: + request["include"] = include + if reasoning is not None: + if "reasoning" in configured_explicit_extra_keys: + raise ValueError("Responses reasoning 与模型 options.extra_body 重复。") + request["reasoning"] = reasoning + elif configured_reasoning_effort is not None: + if "reasoning" in configured_explicit_extra_keys: + raise ValueError( + "Responses reasoning_effort 与模型 options.extra_body 重复。" + ) + request["reasoning"] = {"effort": configured_reasoning_effort} + if store is not None: + request["store"] = store + if background is not None: + request["background"] = background + if parallel_tool_calls is not None: + request["parallel_tool_calls"] = parallel_tool_calls + if max_tool_calls is not None: + request["max_tool_calls"] = max_tool_calls + if conversation is not None: + request["conversation"] = conversation + if context_management is not None: + request["context_management"] = context_management + if prompt_cache_options is not None: + request["prompt_cache_options"] = prompt_cache_options + if safety_identifier is not None: + request["safety_identifier"] = safety_identifier + if moderation is not None: + request["moderation"] = moderation + effective_top_logprobs = ( + top_logprobs if top_logprobs is not None else configured_top_logprobs + ) + if effective_top_logprobs is not None: + request["top_logprobs"] = effective_top_logprobs + effective_verbosity = verbosity if verbosity is not None else configured_verbosity + if effective_verbosity is not None: + # openai>=3 accepts Responses verbosity inside `text`; it is not + # a valid top-level argument for `responses.create()`. + request.setdefault("text", {})["verbosity"] = effective_verbosity + if prompt_cache_key is not None: + request["prompt_cache_key"] = prompt_cache_key + if prompt_cache_retention is not None: + request["prompt_cache_retention"] = prompt_cache_retention + if service_tier is not None: + request["service_tier"] = service_tier + if truncation is not None: + request["truncation"] = truncation + if stream_options is not None: + request["stream_options"] = stream_options + effective_background = background if background is not None else configured_background + if effective_background is not None: + if not isinstance(effective_background, bool): + raise ValueError("Responses background 必须是布尔值。") + if effective_background and stream: + raise ValueError("Responses background 请求不支持流式模式。") + request["background"] = effective_background + request_extra_body = self._validated_extra_body( + extra_body, + "Responses", + set(request).difference({"extra_body"}), + ) + if request_extra_body: + configured_overlap = sorted( + set(request.get("extra_body", {})).intersection(request_extra_body) + ) + if configured_overlap: + raise ValueError( + "Responses extra_body 与模型 options 重复: " + + ", ".join(configured_overlap) + ) + request.setdefault("extra_body", {}).update(request_extra_body) + if request.get("extra_body"): + request["extra_body"] = self._validated_extra_body( + request["extra_body"], + "Responses", + set(request).difference({"extra_body"}), + ) + return request + + @staticmethod + def _convert_responses_format(response_format: dict[str, Any]) -> dict[str, Any]: + if response_format.get("type") == "json_schema": + schema = response_format.get("json_schema", response_format) + json_schema = schema.get("schema", {}) if isinstance(schema, Mapping) else {} + return { + "format": { + "type": "json_schema", + "name": schema.get("name", "response") if isinstance(schema, Mapping) else "response", + "schema": json_schema or response_format.get("schema", schema), + "strict": schema.get("strict", True) if isinstance(schema, Mapping) else True, + } + } + if response_format.get("type") == "json_object": + return {"format": {"type": "json_object"}} + if "format" in response_format: + return response_format + raise ValueError("Responses response_format 仅支持 json_schema 或 json_object。") + + @staticmethod + def _field(value: Any, name: str, default: Any = None) -> Any: + if isinstance(value, Mapping): + return value.get(name, default) + return getattr(value, name, default) + + @classmethod + def _extract_response_text(cls, response: Any) -> str: + output_text = cls._field(response, "output_text") + if isinstance(output_text, str): + return output_text + + chunks: list[str] = [] + choices = cls._field(response, "choices", []) or [] + for choice in choices: + message = cls._field(choice, "message") or cls._field(choice, "delta") + text = cls._field(message, "content") + if text: + chunks.append(content_to_text(text)) + if chunks: + return "".join(chunks) + for item in cls._field(response, "output", []) or []: + for content in cls._field(item, "content", []) or []: + text = cls._field(content, "text") + if isinstance(text, str): + chunks.append(text) + return "".join(chunks) + + @classmethod + def _extract_tool_calls(cls, response: Any) -> list[dict[str, Any]]: + calls: list[dict[str, Any]] = [] + choices = cls._field(response, "choices", []) or [] + for choice in choices: + message = cls._field(choice, "message") or cls._field(choice, "delta") + for call in cls._field(message, "tool_calls", []) or []: + function = cls._field(call, "function") + calls.append( + { + "id": cls._field(call, "id"), + "type": cls._field(call, "type", "function"), + "name": cls._field(function, "name"), + "arguments": normalize_tool_arguments(cls._field(function, "arguments", "")), + } + ) + for item in cls._field(response, "output", []) or []: + if cls._field(item, "type") in {"function_call", "custom_tool_call"}: + calls.append( + { + "id": cls._field(item, "call_id") or cls._field(item, "id"), + "type": cls._field(item, "type"), + "name": cls._field(item, "name"), + "arguments": normalize_tool_arguments( + cls._field(item, "arguments", cls._field(item, "input", "")) + ), + } + ) + return calls + + @classmethod + def _extract_usage(cls, response: Any) -> dict[str, Any]: + return normalize_usage(cls._field(response, "usage")) + + @classmethod + def _extract_reasoning(cls, response: Any) -> str: + values: list[str] = [] + choices = cls._field(response, "choices", []) or [] + for choice in choices: + message = cls._field(choice, "message") or cls._field(choice, "delta") + for name in ("reasoning_content", "reasoning"): + value = cls._field(message, name) + if isinstance(value, str): + values.append(value) + for item in cls._field(response, "output", []) or []: + if cls._field(item, "type") in {"reasoning", "reasoning_item"}: + summary = cls._field(item, "summary") + if isinstance(summary, str): + values.append(summary) + elif isinstance(summary, (list, tuple)): + for entry in summary: + text = cls._field(entry, "text") + if isinstance(text, str): + values.append(text) + for content in cls._field(item, "content", []) or []: + text = cls._field(content, "text") + if isinstance(text, str): + values.append(text) + fallback = cls._field(item, "text") + if isinstance(fallback, str): + values.append(fallback) + return "".join(values) + + @classmethod + def _extract_refusal(cls, response: Any) -> str | None: + """提取 Chat Completions 与 Responses 的统一拒答文本。""" + choices = cls._field(response, "choices", []) or [] + for choice in choices: + message = cls._field(choice, "message") or cls._field(choice, "delta") + refusal = cls._field(message, "refusal") + if isinstance(refusal, str) and refusal: + return refusal + for item in cls._field(response, "output", []) or []: + refusal = cls._field(item, "refusal") + if isinstance(refusal, str) and refusal: + return refusal + for content in cls._field(item, "content", []) or []: + if cls._field(content, "type") != "refusal": + continue + refusal = cls._field(content, "refusal") or cls._field(content, "text") + if isinstance(refusal, str) and refusal: + return refusal + return None + + @classmethod + def _chat_status_is_error(cls, status: Any) -> bool: + """识别兼容网关放在 HTTP 200 响应体中的业务失败状态。""" + if status is None or isinstance(status, bool): + return False + if isinstance(status, int): + return status >= 400 + normalized = str(status).strip().lower() + if not normalized: + return False + if normalized.isdigit(): + return int(normalized) >= 400 + return normalized in { + "error", + "failed", + "failure", + "cancelled", + "canceled", + "incomplete", + "rejected", + "denied", + } or normalized.startswith(("error_", "failed_", "failure_")) + + @classmethod + def _chat_response_error_detail(cls, response: Any) -> str | None: + """提取兼容网关常见的顶层业务错误字段。""" + for name in ("error", "msg", "message", "detail", "reason", "code", "error_code"): + value = cls._field(response, name) + if value is None or value == "": + continue + if name == "error": + return redact_sensitive_text(cls._error_message(value, "未知错误。")) + return redact_sensitive_text(str(value)) + return None + + @classmethod + def _raise_for_chat_response_error( + cls, + response: Any, + *, + allow_empty_choices: bool = False, + provider: str = "Chat Completions", + ) -> None: + """把 Chat Completions 的协议错误和网关业务错误转换为异常。 + + 部分兼容网关会以 HTTP 200 返回 status/msg,并将 choices 设为 + None。如果只按 SDK 的异常或 choices 提取,这类失败会被误报为空文本。 + 流式响应允许没有 choices 的 usage chunk,但只要出现错误状态或错误 + 字段仍必须显式失败。 + """ + detail = cls._chat_response_error_detail(response) + status = cls._field(response, "status") + if detail is not None and cls._field(response, "error") is not None: + raise RuntimeError(f"{provider} 返回错误: {detail}") + if cls._chat_status_is_error(status): + suffix = f": {detail}" if detail else "。" + raise RuntimeError(f"{provider} 返回业务错误 (status={status}){suffix}") + + choices = cls._field(response, "choices") + if allow_empty_choices: + if choices is None and detail is not None: + raise RuntimeError(f"{provider} 返回错误: {detail}") + return + if choices is None or (isinstance(choices, (list, tuple)) and not choices): + suffix = f": {detail}" if detail else "。" + raise RuntimeError(f"{provider} 响应缺少有效 choices{suffix}") + + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible complete") + request = request.copy_with(stream=False) + if self._protocol == "responses": + self._require_responses_resource("complete") + params = self._build_responses_request(**request.to_invoke_kwargs()) + response = retry_sync_call( + lambda: self._get_client().responses.create(**params) + ) + if params.get("background"): + response = self._poll_background_response(response, params) + self._raise_for_response_error(response) + else: + params = self._build_chat_request(**request.to_invoke_kwargs()) + response = retry_sync_call( + lambda: self._get_client().chat.completions.create(**params) + ) + self._raise_for_chat_response_error( + response, + provider=f"{getattr(self, '_provider', 'OpenAI-compatible')} Chat Completions", + ) + choices = self._field(response, "choices", []) or [] + finish_reason = ( + self._field(response, "status") + if self._protocol == "responses" + else self._field(choices[0], "finish_reason") if choices else None + ) + return CompletionResult( + text=self._extract_response_text(response), + tool_calls=self._extract_tool_calls(response), + usage=self._extract_usage(response), + finish_reason=finish_reason, + response_id=self._field(response, "id"), + refusal=self._extract_refusal(response), + reasoning=self._extract_reasoning(response), + raw=response, + ) + + async def acomplete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """使用 AsyncOpenAI 聚合完整结果,保留 SDK 元数据。""" + request = coerce_completion_request(request, kwargs, "OpenAI-compatible acomplete") + request = request.copy_with(stream=False) + if self._protocol == "responses": + self._require_responses_resource("acomplete") + params = self._build_responses_request(**request.to_invoke_kwargs()) + response = await retry_async_call( + lambda: self._get_aclient().responses.create(**params) + ) + if params.get("background"): + response = await self._poll_background_response_async(response, params) + self._raise_for_response_error(response) + else: + params = self._build_chat_request(**request.to_invoke_kwargs()) + response = await retry_async_call( + lambda: self._get_aclient().chat.completions.create(**params) + ) + self._raise_for_chat_response_error( + response, + provider=f"{getattr(self, '_provider', 'OpenAI-compatible')} Chat Completions", + ) + choices = self._field(response, "choices", []) or [] + finish_reason = ( + self._field(response, "status") + if self._protocol == "responses" + else self._field(choices[0], "finish_reason") if choices else None + ) + return CompletionResult( + text=self._extract_response_text(response), + tool_calls=self._extract_tool_calls(response), + usage=self._extract_usage(response), + finish_reason=finish_reason, + response_id=self._field(response, "id"), + refusal=self._extract_refusal(response), + reasoning=self._extract_reasoning(response), + raw=response, + ) + + @classmethod + def _chat_stream_events(cls, chunk: Any) -> list[StreamEvent]: + raise_for_stream_error_event(chunk, "Chat Completions") + cls._raise_for_chat_response_error(chunk, allow_empty_choices=True) + choices = cls._field(chunk, "choices", []) or [] + usage = cls._extract_usage(chunk) + if not choices: + return [StreamEvent(type="usage", usage=usage, raw=chunk)] if usage else [] + choice = choices[0] + delta = cls._field(choice, "delta") + text = content_to_text(cls._field(delta, "content", "")) + reasoning = cls._field(delta, "reasoning_content") or cls._field(delta, "reasoning") or "" + refusal = cls._field(delta, "refusal") + tool_calls = cls._field(delta, "tool_calls", []) or [] + finish_reason = cls._field(choice, "finish_reason") + events: list[StreamEvent] = [] + + if isinstance(refusal, str) and refusal: + events.append(StreamEvent(type="refusal_delta", refusal=refusal, raw=chunk)) + + response_id = cls._field(chunk, "id") + if text: + events.append( + StreamEvent( + type="text_delta", + text=text, + response_id=response_id, + raw=chunk, + ) + ) + if isinstance(reasoning, str) and reasoning: + events.append( + StreamEvent( + type="reasoning_delta", + reasoning=reasoning, + response_id=response_id, + raw=chunk, + ) + ) + if tool_calls: + for position, call in enumerate(tool_calls): + function = cls._field(call, "function") + events.append( + StreamEvent( + type="tool_call_delta", + tool_call={ + "id": cls._field(call, "id"), + "index": cls._field(call, "index", position), + "type": cls._field(call, "type", "function"), + "name": cls._field(function, "name"), + "arguments": cls._field(function, "arguments", ""), + }, + response_id=response_id, + raw=chunk, + ) + ) + + if finish_reason: + events.append( + StreamEvent( + type="finish", + usage=usage, + finish_reason=finish_reason, + response_id=cls._field(chunk, "id"), + raw=chunk, + ) + ) + elif usage: + if events: + events[-1].usage = usage + else: + events.append(StreamEvent(type="usage", usage=usage, raw=chunk)) + return events + + @classmethod + def _chat_stream_event(cls, chunk: Any) -> StreamEvent | None: + """兼容旧调用方,返回当前 chunk 的第一个统一事件。""" + events = cls._chat_stream_events(chunk) + return events[0] if events else None + + @classmethod + def _response_tool_keys(cls, event: Any, item: Any = None) -> list[Any]: + """返回 Responses 工具事件可用的稳定关联键。""" + item = item if item is not None else event + item_id = cls._field(event, "item_id") + if item_id is None: + item_id = cls._field(item, "id") + output_index = cls._field(event, "output_index") + if output_index is None: + output_index = cls._field(item, "output_index") + call_id = cls._field(event, "call_id") + if call_id is None: + call_id = cls._field(item, "call_id") + keys: list[Any] = [] + for prefix, value in ( + ("item_id", item_id), + ("index", output_index), + ("call_id", call_id), + ): + if value is None: + continue + try: + hash(value) + except TypeError: + continue + keys.append((prefix, value)) + return keys + + @classmethod + def _remember_response_tool_metadata( + cls, + event: Any, + metadata: dict[Any, dict[str, Any]], + ) -> None: + """记录 output_item.added 中后续 delta/done 事件需要的工具元数据。""" + item = cls._field(event, "item") or event + item_type = cls._field(item, "type") + if item_type not in {"function_call", "custom_tool_call"}: + return + item_id = cls._field(item, "id") or cls._field(event, "item_id") + call_id = cls._field(item, "call_id") or cls._field(event, "call_id") + output_index = cls._field(event, "output_index") + if output_index is None: + output_index = cls._field(item, "output_index") + values: dict[str, Any] = { + "id": call_id or item_id, + "call_id": call_id, + "index": output_index, + "type": item_type, + "name": cls._field(item, "name"), + } + for field_name in ("arguments", "input"): + value = cls._field(item, field_name) + if value is not None: + values["arguments"] = normalize_tool_arguments(value) + break + for key in cls._response_tool_keys(event, item): + stored = metadata.setdefault(key, {}) + for name, value in values.items(): + if value is not None and value != "": + stored[name] = value + + @classmethod + def _response_tool_metadata( + cls, + event: Any, + metadata: Mapping[Any, Mapping[str, Any]] | None, + item: Any = None, + ) -> dict[str, Any]: + """按 item_id/output_index/call_id 合并已记录的工具元数据。""" + if not metadata: + return {} + merged: dict[str, Any] = {} + for key in cls._response_tool_keys(event, item): + values = metadata.get(key) + if not values: + continue + for name, value in values.items(): + merged.setdefault(name, value) + return merged + + @classmethod + def _responses_stream_event( + cls, + event: Any, + tool_metadata: dict[Any, dict[str, Any]] | None = None, + *, + error_provider: str = "Responses API", + response_error_handler: Any | None = None, + ) -> StreamEvent | None: + event_type = cls._field(event, "type", "") + if event_type == "response.output_item.added": + if tool_metadata is not None: + cls._remember_response_tool_metadata(event, tool_metadata) + return None + if event_type in {"response.output_text.delta", "response.refusal.delta"}: + delta = cls._field(event, "delta") + is_refusal = event_type.endswith("refusal.delta") + return StreamEvent( + type="refusal_delta" if is_refusal else "text_delta", + text="" if is_refusal else (delta if isinstance(delta, str) else ""), + refusal=delta if is_refusal and isinstance(delta, str) else None, + raw=event, + ) + if event_type in {"response.reasoning_summary_text.delta", "response.reasoning_text.delta"}: + delta = cls._field(event, "delta") + return StreamEvent(type="reasoning_delta", reasoning=delta if isinstance(delta, str) else "", raw=event) + if event_type.endswith(".delta") and ( + "function_call_arguments" in event_type or "custom_tool_call_input" in event_type + ): + metadata = cls._response_tool_metadata(event, tool_metadata) + item_type = metadata.get( + "type", + "custom_tool_call" if "custom_tool_call_input" in event_type else "function_call", + ) + item_id = cls._field(event, "item_id") + call_id = cls._field(event, "call_id") or metadata.get("call_id") + output_index = cls._field(event, "output_index") + if output_index is None: + output_index = metadata.get("index") + return StreamEvent( + type="tool_call_delta", + tool_call={ + "id": item_id or cls._field(event, "call_id") or metadata.get("id"), + "call_id": call_id, + "index": output_index if output_index is not None else cls._field(event, "index"), + "type": item_type, + "name": cls._field(event, "name") or metadata.get("name"), + "arguments": normalize_tool_arguments(cls._field(event, "delta", "")), + }, + raw=event, + ) + if event_type in { + "response.output_item.done", + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + item = cls._field(event, "item") + metadata = cls._response_tool_metadata(event, tool_metadata, item) + item_type = cls._field(item, "type") if item is not None else None + item_type = item_type or metadata.get("type") + if ( + item_type in {"function_call", "custom_tool_call"} + or "function_call_arguments" in event_type + or "custom_tool_call_input" in event_type + ): + call_id = cls._field(item, "call_id") or cls._field(event, "call_id") or metadata.get("call_id") + output_index = cls._field(event, "output_index") + if output_index is None: + output_index = metadata.get("index") + argument_value: Any = None + for source in (item, event): + for field_name in ("arguments", "input"): + value = cls._field(source, field_name) + if value is not None: + argument_value = value + break + if argument_value is not None: + break + if argument_value is None: + argument_value = metadata.get("arguments", "") + tool_call: dict[str, Any] = { + "id": cls._field(item, "call_id") + or cls._field(item, "id") + or cls._field(event, "item_id") + or metadata.get("id"), + "index": output_index if output_index is not None else cls._field(event, "index"), + "type": item_type if item_type in {"function_call", "custom_tool_call"} else "function_call", + "name": cls._field(item, "name") or cls._field(event, "name") or metadata.get("name"), + "arguments": normalize_tool_arguments(argument_value), + } + if call_id is not None: + tool_call["call_id"] = call_id + return StreamEvent( + type="tool_call_completed", + tool_call=tool_call, + raw=event, + ) + if event_type in {"error", "response.error"}: + raise_for_stream_error_event(event, error_provider) + if event_type in { + "response.completed", "response.incomplete", "response.failed", "response.cancelled" + }: + response = cls._field(event, "response") or event + usage = cls._extract_usage(response) + status = cls._field(response, "status") + if status in {"failed", "incomplete", "cancelled"}: + handler = response_error_handler or cls._raise_for_response_error + handler(response) + if event_type != "response.completed": + raise RuntimeError(f"{error_provider} 收到终止事件: {event_type}。") + return StreamEvent( + type="finish", + usage=usage, + finish_reason=status, + response_id=cls._field(response, "id"), + raw=event, + ) + return None + + @classmethod + def _merge_response_tool_call( + cls, + accumulator: dict[Any, dict[str, Any]], + tool_call: Mapping[str, Any], + ) -> tuple[Any, dict[str, Any]]: + """合并 Responses 的完整工具调用元数据。 + + ``function_call_arguments.done`` 使用 item_id,而 + ``output_item.done`` 通常带 call_id;两者必须按 output_index 归并。 + """ + index = tool_call.get("index") + call_id = tool_call.get("call_id") or tool_call.get("id") + key: Any = ("index", index) if index is not None else ("id", call_id) + if key == ("id", None): + key = ("position", len(accumulator)) + merged = accumulator.setdefault(key, {}) + for name in ("id", "call_id", "index", "type", "name"): + value = tool_call.get(name) + if value is not None and value != "": + merged[name] = value + arguments = tool_call.get("arguments") + if arguments is not None and arguments != "": + merged["arguments"] = normalize_tool_arguments(arguments) + merged.setdefault("type", "function_call") + merged.setdefault("arguments", "") + return key, merged + + @classmethod + def _chat_completion_events( + cls, + converted: StreamEvent, + accumulator: dict[Any, dict[str, Any]], + completed: set[Any], + ) -> list[StreamEvent]: + """在 Chat Completions finish 前发出一次完整工具调用事件。""" + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(accumulator, converted.tool_call) + return [converted] + if converted.type != "finish": + return [converted] + + events: list[StreamEvent] = [] + for key, tool_call in accumulator.items(): + if key in completed: + continue + validate_complete_tool_call(tool_call, "OpenAI Chat Completions") + completed.add(key) + events.append( + StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + usage={}, + finish_reason=converted.finish_reason, + response_id=converted.response_id, + raw=converted.raw, + ) + ) + events.append(converted) + return events + + @classmethod + def _responses_completion_event( + cls, + converted: StreamEvent, + accumulator: dict[Any, dict[str, Any]], + ) -> tuple[Any, StreamEvent] | None: + if converted.type != "tool_call_completed" or not converted.tool_call: + return None + key, merged = cls._merge_response_tool_call(accumulator, converted.tool_call) + validate_complete_tool_call(merged, "OpenAI Responses") + return key, StreamEvent( + type="tool_call_completed", + tool_call=dict(merged), + response_id=converted.response_id, + raw=converted.raw, + ) + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible stream_events").copy_with( + stream=True, + ) + if self._protocol == "responses": + self._require_responses_resource("stream_events") + params = self._build_responses_request(**request.to_invoke_kwargs()) + response_tool_metadata: dict[Any, dict[str, Any]] = {} + response_tool_calls: dict[Any, dict[str, Any]] = {} + pending_completions: dict[Any, StreamEvent] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + for event in retry_sync_stream( + lambda: self._get_client().responses.create(**params), + lambda event: raise_for_stream_error_event(event, self._provider), + ): + if terminal_seen: + continue + event_type = self._field(event, "type", "") + converted = self._responses_stream_event(event, response_tool_metadata) + if converted: + if converted.type == "finish": + terminal_seen = True + for key, pending in pending_completions.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield pending + for key, tool_call in response_tool_calls.items(): + if key not in completed_tool_calls: + validate_complete_tool_call(tool_call, "OpenAI Responses") + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=converted.response_id, + raw=converted.raw, + ) + yield converted + # Drain the SDK stream to EOF before closing it. Some + # async transports keep a response-body generator open + # after the terminal event; breaking here can surface + # noisy generator-close errors and leak a connection. + continue + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(response_tool_calls, converted.tool_call) + yield converted + continue + if converted.type == "tool_call_completed": + completed = self._responses_completion_event(converted, response_tool_calls) + if completed is None: + continue + key, completed_event = completed + if event_type in { + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + pending_completions[key] = completed_event + continue + pending_completions.pop(key, None) + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield completed_event + continue + yield converted + if not terminal_seen: + raise RuntimeError("Responses API 流在 response.completed 之前结束。") + return + params = self._build_chat_request(**request.to_invoke_kwargs()) + tool_calls: dict[Any, dict[str, Any]] = {} + completed_chat_tool_calls: set[Any] = set() + terminal_seen = False + for chunk in retry_sync_stream( + lambda: self._get_client().chat.completions.create(**params), + lambda chunk: raise_for_stream_error_event( + chunk, getattr(self, "_provider", "OpenAI-compatible") + ), + ): + raise_for_stream_error_event( + chunk, getattr(self, "_provider", "OpenAI-compatible") + ) + for converted in self._chat_stream_events(chunk): + if converted.type == "finish": + terminal_seen = True + yield from self._chat_completion_events(converted, tool_calls, completed_chat_tool_calls) + if not terminal_seen: + raise RuntimeError("Chat Completions 流在 finish 事件之前结束。") + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible astream_events").copy_with( + stream=True, + ) + if self._protocol == "responses": + self._require_responses_resource("astream_events") + params = self._build_responses_request(**request.to_invoke_kwargs()) + response_tool_metadata: dict[Any, dict[str, Any]] = {} + response_tool_calls: dict[Any, dict[str, Any]] = {} + pending_completions: dict[Any, StreamEvent] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + async for event in retry_async_stream( + lambda: self._get_aclient().responses.create(**params), + lambda event: raise_for_stream_error_event(event, self._provider), + ): + if terminal_seen: + continue + event_type = self._field(event, "type", "") + converted = self._responses_stream_event(event, response_tool_metadata) + if converted: + if converted.type == "finish": + terminal_seen = True + for key, pending in pending_completions.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield pending + for key, tool_call in response_tool_calls.items(): + if key not in completed_tool_calls: + validate_complete_tool_call(tool_call, "OpenAI Responses") + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=converted.response_id, + raw=converted.raw, + ) + yield converted + # Drain the SDK stream to EOF before closing it. Some + # async transports keep the response body generator + # open after the terminal event. + continue + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(response_tool_calls, converted.tool_call) + yield converted + continue + if converted.type == "tool_call_completed": + completed = self._responses_completion_event(converted, response_tool_calls) + if completed is None: + continue + key, completed_event = completed + if event_type in { + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + pending_completions[key] = completed_event + continue + pending_completions.pop(key, None) + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield completed_event + continue + yield converted + if not terminal_seen: + raise RuntimeError("Responses API 异步流在 response.completed 之前结束。") + return + params = self._build_chat_request(**request.to_invoke_kwargs()) + tool_calls: dict[Any, dict[str, Any]] = {} + completed_chat_tool_calls: set[Any] = set() + terminal_seen = False + async for chunk in retry_async_stream( + lambda: self._get_aclient().chat.completions.create(**params), + lambda chunk: raise_for_stream_error_event( + chunk, getattr(self, "_provider", "OpenAI-compatible") + ), + ): + raise_for_stream_error_event( + chunk, getattr(self, "_provider", "OpenAI-compatible") + ) + for converted in self._chat_stream_events(chunk): + if converted.type == "finish": + terminal_seen = True + for event in self._chat_completion_events(converted, tool_calls, completed_chat_tool_calls): + yield event + if not terminal_seen: + raise RuntimeError("Chat Completions 异步流在 finish 事件之前结束。") + + def parse( + self, + text_format: Any, + request: CompletionRequest | None = None, + **kwargs: Any, + ) -> Any: + """调用 OpenAI Chat/Responses 的原生结构化解析入口。""" + if text_format is None: + raise ValueError("OpenAI parse 必须提供 text_format。") + request = coerce_completion_request(request, kwargs, "OpenAI-compatible parse") + request = request.copy_with(stream=False) + values = request.to_invoke_kwargs() + if self._protocol == "responses": + self._require_responses_resource("parse") + params = self._build_responses_request(**values) + if params.get("background"): + raise ValueError( + "Responses parse 不支持 background=true;" + "请使用 complete/acomplete 等待后台任务完成后再处理结果。" + ) + # ``stream`` is an internal unified-request control flag. The + # Responses parse helper always returns one parsed result and its + # SDK signature does not accept this flag. + params.pop("stream", None) + text_config = params.pop("text", None) + if isinstance(text_config, Mapping) and "format" in text_config: + raise ValueError( + "Responses parse 不能同时使用 request.response_format 和 text_format。" + ) + if text_config: + params["text"] = text_config + return self._get_client().responses.parse(text_format=text_format, **params) + params = self._build_chat_request(**values) + params.pop("response_format", None) + params.pop("stream", None) + return self._get_client().chat.completions.parse(response_format=text_format, **params) + + async def async_parse( + self, + text_format: Any, + request: CompletionRequest | None = None, + **kwargs: Any, + ) -> Any: + """异步调用 OpenAI Chat/Responses 的原生结构化解析入口。""" + if text_format is None: + raise ValueError("OpenAI parse 必须提供 text_format。") + request = coerce_completion_request(request, kwargs, "OpenAI-compatible async_parse") + request = request.copy_with(stream=False) + values = request.to_invoke_kwargs() + if self._protocol == "responses": + self._require_responses_resource("parse") + params = self._build_responses_request(**values) + if params.get("background"): + raise ValueError( + "Responses async_parse 不支持 background=true;" + "请使用 acomplete 等待后台任务完成后再处理结果。" + ) + params.pop("stream", None) + text_config = params.pop("text", None) + if isinstance(text_config, Mapping) and "format" in text_config: + raise ValueError( + "Responses parse 不能同时使用 request.response_format 和 text_format。" + ) + if text_config: + params["text"] = text_config + return await self._resolve_async_result( + self._get_aclient().responses.parse(text_format=text_format, **params) + ) + params = self._build_chat_request(**values) + params.pop("response_format", None) + params.pop("stream", None) + return await self._resolve_async_result( + self._get_aclient().chat.completions.parse(response_format=text_format, **params) + ) + + @classmethod + def _error_message(cls, error: Any, default: str) -> str: + if isinstance(error, str) and error: + return error + message = cls._field(error, "message") + if message: + return str(message) + code = cls._field(error, "code") + if code: + return f"{code}" + return default + + @classmethod + def _raise_for_response_error(cls, response: Any) -> None: + error = cls._field(response, "error") + if error: + raise RuntimeError( + f"Responses API 返回错误: {cls._error_message(error, '未知错误。')}" + ) + + status = cls._field(response, "status") + if status not in {"failed", "incomplete", "cancelled"}: + return + + if status == "incomplete": + details = cls._field(response, "incomplete_details") + reason = cls._field(details, "reason") + detail = f"原因: {reason}" if reason else "未提供原因。" + else: + detail = "未提供原因。" + raise RuntimeError(f"Responses API 响应状态为 {status}: {detail}") + + @classmethod + def _stream_delta(cls, event: Any) -> str: + event_type = cls._field(event, "type", "") + if event_type in {"response.output_text.delta", "response.refusal.delta"}: + delta = cls._field(event, "delta") + return delta if isinstance(delta, str) else "" + if event_type in {"error", "response.error"}: + raise_for_stream_error_event(event, "Responses API") + if event_type in { + "response.completed", "response.failed", "response.incomplete", "response.cancelled" + }: + response = cls._field(event, "response") or event + cls._raise_for_response_error(response) + if event_type != "response.completed": + raise RuntimeError(f"Responses API 收到终止事件: {event_type}。") + return "" + + def _invoke_responses(self, request: dict[str, Any]) -> Iterator[str]: + if request["stream"]: + terminal_seen = False + for event in retry_sync_stream( + lambda: self._get_client().responses.create(**request), + lambda event: raise_for_stream_error_event(event, self._provider), + ): + event_type = self._field(event, "type", "") + if event_type == "response.refusal.delta": + refusal = self._field(event, "delta") + if refusal: + raise RuntimeError( + f"{self._provider} Responses 返回拒答: {refusal}" + ) + delta = self._stream_delta(event) + if delta: + yield delta + if event_type == "response.completed": + terminal_seen = True + if not terminal_seen: + raise RuntimeError("Responses API 流在 response.completed 之前结束。") + return + response = retry_sync_call(lambda: self._get_client().responses.create(**request)) + if request.get("background"): + response = self._poll_background_response(response, request) + self._raise_for_response_error(response) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"{self._provider} Responses 返回拒答: {refusal}") + text = self._extract_response_text(response) + if text: + yield text + + async def _ainvoke_responses(self, request: dict[str, Any]) -> AsyncIterator[str]: + if request["stream"]: + terminal_seen = False + async for event in retry_async_stream( + lambda: self._get_aclient().responses.create(**request), + lambda event: raise_for_stream_error_event(event, self._provider), + ): + event_type = self._field(event, "type", "") + if event_type == "response.refusal.delta": + refusal = self._field(event, "delta") + if refusal: + raise RuntimeError( + f"{self._provider} Responses 返回拒答: {refusal}" + ) + delta = self._stream_delta(event) + if delta: + yield delta + if event_type == "response.completed": + terminal_seen = True + if not terminal_seen: + raise RuntimeError("Responses API 异步流在 response.completed 之前结束。") + return + response = await retry_async_call(lambda: self._get_aclient().responses.create(**request)) + if request.get("background"): + response = await self._poll_background_response_async(response, request) + self._raise_for_response_error(response) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"{self._provider} Responses 返回拒答: {refusal}") + text = self._extract_response_text(response) + if text: + yield text + def invoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + user: str | None = None, + timeout: float | None = None, + **kwargs: Any, ) -> Generator[str, None, None]: """同步调用 OpenAI 兼容 LLM (CSE Sensor)。""" + temperature = None if temperature is _UNSET_TEMPERATURE else temperature logger.info(f"调用 {self._provider} LLM ({self._model_name}),流式: {stream}") + if self._protocol == "responses": + self._require_responses_resource("invoke") + if tools: + raise ValueError( + f"{self._provider} Responses invoke 仅返回文本,不支持 tools;" + "请使用 complete 或 stream_events 获取工具调用。" + ) client = self._get_client() start_time = time.perf_counter() try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = client.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream + if self._protocol == "responses": + yield from self._invoke_responses( + self._build_responses_request(prompt, system_prompt, tools, temperature, stream, + messages=messages, max_tokens=max_tokens, top_p=top_p, + top_k=top_k, seed=seed, + stop=stop, response_format=response_format, tool_choice=tool_choice, + extra_body=extra_body, metadata=metadata, user=user, timeout=timeout, + **kwargs) + ) + return + + request = self._build_chat_request( + prompt, system_prompt, tools, temperature, stream, + messages=messages, max_tokens=max_tokens, top_p=top_p, stop=stop, + top_k=top_k, seed=seed, response_format=response_format, + tool_choice=tool_choice, extra_body=extra_body, metadata=metadata, + user=user, timeout=timeout, **kwargs, ) - if stream: - for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content + terminal_seen = False + for chunk in retry_sync_stream( + lambda: client.chat.completions.create(**request), + lambda chunk: raise_for_stream_error_event(chunk, self._provider), + ): + raise_for_stream_error_event(chunk, self._provider) + for event in self._chat_stream_events(chunk): + if event.type == "finish": + terminal_seen = True + if event.type == "refusal_delta" and event.refusal: + raise RuntimeError( + f"{self._provider} Chat Completions 返回拒答: {event.refusal}" + ) + if event.type == "text_delta" and event.text: + yield event.text + if not terminal_seen: + raise RuntimeError("Chat Completions 流在 finish 事件之前结束。") else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content + response = retry_sync_call(lambda: client.chat.completions.create(**request)) + self._raise_for_chat_response_error( + response, + provider=f"{getattr(self, '_provider', 'OpenAI-compatible')} Chat Completions", + ) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"{self._provider} Chat Completions 返回拒答: {refusal}") + choices = self._field(response, "choices", []) or [] + if choices: + message = self._field(choices[0], "message") + content = self._field(message, "content") + if content: + yield content_to_text(content) duration = time.perf_counter() - start_time logger.info(f"{self._provider} LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"{self._provider} LLM ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "%s LLM (%s) 出错: %s", + self._provider, + self._model_name, + error_text, + ) raise async def ainvoke( self, - prompt: str, + prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, + tools: list[dict[str, Any]] | None = None, stream: bool = True, - temperature: float = 0.7, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, + max_tokens: int | None = None, + top_p: float | None = None, + top_k: int | None = None, + seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, + tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, + metadata: dict[str, Any] | None = None, + user: str | None = None, + timeout: float | None = None, + **kwargs: Any, ) -> AsyncGenerator[str, None]: """异步调用 OpenAI 兼容 LLM (CSE Sensor)。""" + temperature = None if temperature is _UNSET_TEMPERATURE else temperature logger.info(f"异步调用 {self._provider} LLM ({self._model_name}),流式: {stream}") + if self._protocol == "responses": + self._require_responses_resource("ainvoke") + if tools: + raise ValueError( + f"{self._provider} Responses ainvoke 仅返回文本,不支持 tools;" + "请使用 acomplete 或 astream_events 获取工具调用。" + ) aclient = self._get_aclient() start_time = time.perf_counter() try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = await aclient.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream + if self._protocol == "responses": + async for chunk in self._ainvoke_responses( + self._build_responses_request(prompt, system_prompt, tools, temperature, stream, + messages=messages, max_tokens=max_tokens, top_p=top_p, + top_k=top_k, seed=seed, + stop=stop, response_format=response_format, tool_choice=tool_choice, + extra_body=extra_body, metadata=metadata, user=user, timeout=timeout, + **kwargs) + ): + yield chunk + return + + request = self._build_chat_request( + prompt, system_prompt, tools, temperature, stream, + messages=messages, max_tokens=max_tokens, top_p=top_p, stop=stop, + top_k=top_k, seed=seed, response_format=response_format, + tool_choice=tool_choice, extra_body=extra_body, metadata=metadata, + user=user, timeout=timeout, **kwargs, ) - if stream: - async for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content + terminal_seen = False + async for chunk in retry_async_stream( + lambda: aclient.chat.completions.create(**request), + lambda chunk: raise_for_stream_error_event(chunk, self._provider), + ): + raise_for_stream_error_event(chunk, self._provider) + for event in self._chat_stream_events(chunk): + if event.type == "finish": + terminal_seen = True + if event.type == "refusal_delta" and event.refusal: + raise RuntimeError( + f"{self._provider} Chat Completions 返回拒答: {event.refusal}" + ) + if event.type == "text_delta" and event.text: + yield event.text + if not terminal_seen: + raise RuntimeError("Chat Completions 异步流在 finish 事件之前结束。") else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content + response = await retry_async_call(lambda: aclient.chat.completions.create(**request)) + self._raise_for_chat_response_error( + response, + provider=f"{getattr(self, '_provider', 'OpenAI-compatible')} Chat Completions", + ) + refusal = self._extract_refusal(response) + if refusal: + raise RuntimeError(f"{self._provider} Chat Completions 返回拒答: {refusal}") + choices = self._field(response, "choices", []) or [] + if choices: + message = self._field(choices[0], "message") + content = self._field(message, "content") + if content: + yield content_to_text(content) duration = time.perf_counter() - start_time logger.info(f"{self._provider} LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") except Exception as e: - logger.error(f"{self._provider} LLM ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "%s LLM (%s) 异步出错: %s", + self._provider, + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - def embed_documents(self, texts: List[str]) -> List[List[float]]: + def embed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """同步向量化文档。""" logger.info(f"调用 {self._provider} 嵌入 ({self._model_name}),数量: {len(texts)}") - client = self._get_client() start_time = time.perf_counter() + overlap = sorted({"input", "model"}.intersection(kwargs)) + if overlap: + raise ValueError(f"{self._provider} Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + self._validate_embedding_kwargs(kwargs) + client = self._get_client() try: - response = client.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] + request: dict[str, Any] = {"input": texts, "model": self._model_name} + configured_options = self._embedding_options() + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + f"{self._provider} Embedding", + set(request).union(configured_options), + ) + request_options = dict(kwargs) + request_extra_body = self._validated_extra_body( + request_options.pop("extra_body", None), + f"{self._provider} Embedding", + set(request).union(configured_options).union(request_options), + ) + option_overlap = sorted(set(configured_options).intersection(request_options)) + if option_overlap: + raise ValueError( + f"{self._provider} Embedding 请求参数与模型 options 重复: " + + ", ".join(option_overlap) + ) + request.update(configured_options) + request.update(request_options) + merged_extra_body = self._merge_extra_body( + configured_extra_body, request_extra_body, f"{self._provider} Embedding" + ) + if merged_extra_body: + request["extra_body"] = merged_extra_body + response = client.embeddings.create(**request) + embeddings = [normalize_embedding_vector(item.embedding) for item in response.data] duration = time.perf_counter() - start_time logger.info(f"{self._provider} 嵌入 ({self._model_name}) 完成,耗时: {duration:.2f}s") return embeddings except Exception as e: - logger.error(f"{self._provider} 嵌入 ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "%s 嵌入 (%s) 出错: %s", + self._provider, + self._model_name, + error_text, + ) raise - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + @retry( + stop=stop_after_attempt(3), + wait=wait_exponential(multiplier=1, min=2, max=10), + retry=retry_if_exception(is_retryable_error), + reraise=True + ) + async def aembed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: """异步向量化文档。""" logger.info(f"异步调用 {self._provider} 嵌入 ({self._model_name}),数量: {len(texts)}") - aclient = self._get_aclient() start_time = time.perf_counter() + overlap = sorted({"input", "model"}.intersection(kwargs)) + if overlap: + raise ValueError(f"{self._provider} Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + self._validate_embedding_kwargs(kwargs) + aclient = self._get_aclient() try: - response = await aclient.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] + request: dict[str, Any] = {"input": texts, "model": self._model_name} + configured_options = self._embedding_options() + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + f"{self._provider} Embedding", + set(request).union(configured_options), + ) + request_options = dict(kwargs) + request_extra_body = self._validated_extra_body( + request_options.pop("extra_body", None), + f"{self._provider} Embedding", + set(request).union(configured_options).union(request_options), + ) + option_overlap = sorted(set(configured_options).intersection(request_options)) + if option_overlap: + raise ValueError( + f"{self._provider} Embedding 请求参数与模型 options 重复: " + + ", ".join(option_overlap) + ) + request.update(configured_options) + request.update(request_options) + merged_extra_body = self._merge_extra_body( + configured_extra_body, request_extra_body, f"{self._provider} Embedding" + ) + if merged_extra_body: + request["extra_body"] = merged_extra_body + response = await self._resolve_async_result(aclient.embeddings.create(**request)) + embeddings = [normalize_embedding_vector(item.embedding) for item in response.data] duration = time.perf_counter() - start_time logger.info(f"{self._provider} 嵌入 ({self._model_name}) 异步完成,耗时: {duration:.2f}s") return embeddings except Exception as e: - logger.error(f"{self._provider} 嵌入 ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "%s 嵌入 (%s) 异步出错: %s", + self._provider, + self._model_name, + error_text, + ) raise + + def upload_file(self, file: Any, purpose: str = "assistants", **kwargs: Any) -> Any: + method = self._get_client().files.create + return self._call_sdk_resource( + method, + kwargs=self._merge_resource_kwargs( + {"file": file, "purpose": cast(Any, purpose)}, + kwargs, + "OpenAI 文件上传", + ), + operation="OpenAI 文件上传", + ) + + def list_files(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().files.list, kwargs=kwargs, operation="OpenAI 文件列表" + ) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().files.retrieve, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件读取", + ) + + def file_content(self, file_id: str, **kwargs: Any) -> Any: + # openai>=3 exposes binary/text file content as `files.content`. + files = getattr(self._get_client(), "files", None) + method = getattr(files, "content", None) + if not callable(method): + raise NotImplementedError( + "当前 OpenAI SDK 不提供文件内容读取资源(files.content)。" + ) + return self._call_sdk_resource( + method, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件内容读取", + ) + + def retrieve_file_content(self, file_id: str, **kwargs: Any) -> Any: + """读取文件文本内容;兼容 SDK 的 retrieve_content 命名。""" + method = getattr(self._get_client().files, "retrieve_content", None) + if method is not None: + return self._call_sdk_resource( + method, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件文本读取", + ) + return self.file_content(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().files.delete, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件删除", + ) + + def wait_for_file( + self, + file_id: str, + *, + poll_interval: float | None = None, + max_wait_seconds: float | None = None, + **kwargs: Any, + ) -> Any: + """等待文件处理完成,仅转发 OpenAI SDK 的轮询参数。""" + reject_unsupported_kwargs("OpenAI 文件等待", kwargs) + poll_kwargs = { + key: value + for key, value in { + "poll_interval": poll_interval, + "max_wait_seconds": max_wait_seconds, + }.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().files.wait_for_processing, + kwargs=self._merge_resource_kwargs( + {"id": file_id}, poll_kwargs, "OpenAI 文件等待" + ), + operation="OpenAI 文件等待", + ) + + def create_batch(self, input_file_id: str, endpoint: str | None = None, **kwargs: Any) -> Any: + if self._protocol == "responses" and ( + endpoint is None or endpoint.rstrip("/") == "/v1/responses" + ): + self._require_responses_resource("create_batch") + if endpoint is None: + endpoint = "/v1/responses" if self._protocol == "responses" else "/v1/chat/completions" + kwargs.setdefault("completion_window", "24h") + return self._call_sdk_resource( + self._get_client().batches.create, + kwargs=self._merge_resource_kwargs( + { + "input_file_id": input_file_id, + "endpoint": cast(Any, endpoint), + }, + kwargs, + "OpenAI Batch 创建", + ), + operation="OpenAI Batch 创建", + ) + + def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().batches.retrieve, + args=(batch_id,), + kwargs=kwargs, + operation="OpenAI Batch 读取", + ) + + def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().batches.cancel, + args=(batch_id,), + kwargs=kwargs, + operation="OpenAI Batch 取消", + ) + + def list_batches(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().batches.list, kwargs=kwargs, operation="OpenAI Batch 列表" + ) + + def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("retrieve_response") + return self._call_sdk_resource( + self._get_client().responses.retrieve, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 读取", + ) + + def cancel_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("cancel_response") + return self._call_sdk_resource( + self._get_client().responses.cancel, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 取消", + ) + + def delete_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("delete_response") + return self._call_sdk_resource( + self._get_client().responses.delete, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 删除", + ) + + def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_response_input_items") + return self._call_sdk_resource( + self._get_client().responses.input_items.list, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 输入项列表", + ) + + def connect_responses(self, **kwargs: Any) -> Any: + """打开官方 Responses WebSocket 连接管理器。""" + self._require_responses_resource("connect_responses") + return self._call_sdk_resource( + self._get_client().responses.connect, + kwargs=kwargs, + operation="OpenAI Responses 连接", + ) + + def _prepare_responses_stream_params(self, params: dict[str, Any]) -> dict[str, Any]: + """将统一参数收敛到 ``responses.stream`` 的 SDK 契约。""" + # The SDK uses response_id/starting_after to continue an existing + # response and rejects model in that mode. A native caller may pass + # either value explicitly, so remove the provider default as well as + # an explicitly supplied model before dispatch. + if "response_id" in params or "starting_after" in params: + params.pop("model", None) + else: + params.setdefault("model", self._model_name) + params.pop("stream", None) + if "verbosity" in params: + verbosity = params.pop("verbosity") + if verbosity is not None: + params.setdefault("text", {}).setdefault("verbosity", verbosity) + return params + + def stream_responses(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """返回官方 Responses 流上下文管理器,支持统一请求或原生参数。""" + self._require_responses_resource("stream_responses") + if request is not None and kwargs: + raise ValueError("stream_responses 不能同时传 request 和原生关键字参数。") + if request is not None: + params = self._build_responses_request(**request.copy_with(stream=True).to_invoke_kwargs()) + params.pop("stream", None) + else: + params = dict(kwargs) + # `responses.stream` is a helper with a narrower contract than + # `responses.create`: it does not accept the create-only `stream` + # switch, and it uses `starting_after` for continuation. Keep the + # provider's unified request shape while validating the helper boundary. + params = self._prepare_responses_stream_params(params) + return self._call_sdk_resource( + self._get_client().responses.stream, + kwargs=params, + operation="OpenAI Responses 流", + ) + + def _build_token_count_request(self, request: CompletionRequest) -> dict[str, Any]: + validate_secret_free_request_overrides( + request.extra_headers, + request.extra_query, + f"{getattr(self, '_provider', 'openai')} Responses input token count", + ) + unsupported = { + key: value + for key, value in request.to_invoke_kwargs().items() + if key not in { + "prompt", "system_prompt", "messages", "tools", "tool_choice", + "parallel_tool_calls", "reasoning", "previous_response_id", + "personality", "response_format", "truncation", "conversation", + "extra_headers", "extra_query", "extra_body", "timeout", + # Shared generation defaults are not part of the token-count + # request, but accepting and dropping them is necessary for + # CompletionRequest compatibility. They are never forwarded. + "stream", "temperature", + } + } + reject_unsupported_kwargs("OpenAI Responses input token count", unsupported) + params: dict[str, Any] = { + "model": self._model_name, + "input": normalize_responses_input( + request.prompt, + request.system_prompt, + request.messages, + provider=getattr(self, "_provider", "openai"), + ), + } + if request.system_prompt and request.messages is None: + params["instructions"] = request.system_prompt + if request.conversation is not None: + params["conversation"] = request.conversation + if request.tools: + params["tools"] = self._convert_responses_tools(request.tools) + if request.tool_choice is not None: + params["tool_choice"] = self._convert_responses_tool_choice(request.tool_choice) + if request.parallel_tool_calls is not None: + params["parallel_tool_calls"] = request.parallel_tool_calls + if request.reasoning is not None: + params["reasoning"] = request.reasoning + if request.previous_response_id is not None: + params["previous_response_id"] = request.previous_response_id + if request.personality is not None: + params["personality"] = request.personality + if request.response_format is not None: + params["text"] = self._convert_responses_format(request.response_format) + if request.truncation is not None: + params["truncation"] = request.truncation + if request.extra_headers is not None: + params["extra_headers"] = request.extra_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + if request.extra_body is not None: + params["extra_body"] = request.extra_body + if request.timeout is not None: + params["timeout"] = request.timeout + return params + + def count_input_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible count_input_tokens") + self._require_responses_resource("count_input_tokens") + params = self._build_token_count_request(request) + response = self._call_sdk_resource( + self._get_client().responses.input_tokens.count, + kwargs=params, + operation="OpenAI Responses 输入 token 统计", + ) + value = self._field(response, "input_tokens") + if value is None: + raise RuntimeError("OpenAI token count 响应缺少 input_tokens。") + return int(value) + + def create_vector_store(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.create, + kwargs=kwargs, + operation="OpenAI Vector Store 创建", + ) + + def retrieve_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.retrieve, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 读取", + ) + + def list_vector_stores(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.list, + kwargs=kwargs, + operation="OpenAI Vector Store 列表", + ) + + def search_vector_store(self, vector_store_id: str, query: str | list[str], **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.search, + args=(vector_store_id,), + kwargs=self._merge_resource_kwargs( + {"query": query}, kwargs, "OpenAI Vector Store 搜索" + ), + operation="OpenAI Vector Store 搜索", + ) + + def delete_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.delete, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 删除", + ) + + def update_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.update, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 更新", + ) + + def create_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.create, + args=(vector_store_id,), + kwargs=self._merge_resource_kwargs( + {"file_id": file_id}, kwargs, "OpenAI Vector Store 文件创建" + ), + operation="OpenAI Vector Store 文件创建", + ) + + def create_vector_store_file_and_poll(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.create_and_poll, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件创建轮询", + ), + operation="OpenAI Vector Store 文件创建轮询", + ) + + def upload_vector_store_file( + self, + vector_store_id: str, + file: Any, + *, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """上传并挂载向量库文件,仅转发 SDK 支持的分块策略。""" + reject_unsupported_kwargs("OpenAI 向量库文件上传", kwargs) + upload_kwargs = ( + {"chunking_strategy": chunking_strategy} + if chunking_strategy is not None + else {} + ) + return self._call_sdk_resource( + self._get_client().vector_stores.files.upload, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "file": file}, + upload_kwargs, + "OpenAI Vector Store 文件上传", + ), + operation="OpenAI Vector Store 文件上传", + ) + + def upload_vector_store_file_and_poll( + self, + vector_store_id: str, + file: Any, + *, + attributes: Mapping[str, str | float | bool] | None = None, + poll_interval_ms: int | None = None, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """上传单个文件到向量库并等待处理完成。""" + reject_unsupported_kwargs("OpenAI 向量库文件上传轮询", kwargs) + upload_kwargs = { + key: value + for key, value in { + "attributes": dict(attributes) if attributes is not None else None, + "poll_interval_ms": poll_interval_ms, + "chunking_strategy": chunking_strategy, + }.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().vector_stores.files.upload_and_poll, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "file": file}, + upload_kwargs, + "OpenAI Vector Store 文件上传轮询", + ), + operation="OpenAI Vector Store 文件上传轮询", + ) + + def retrieve_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.retrieve, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件读取", + ), + operation="OpenAI Vector Store 文件读取", + ) + + def list_vector_store_files(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.list, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件列表", + ) + + def update_vector_store_file( + self, + vector_store_id: str, + file_id: str, + *, + attributes: Mapping[str, str | float | bool] | None, + extra_headers: Mapping[str, str] | None = None, + extra_query: Mapping[str, Any] | None = None, + extra_body: Mapping[str, Any] | None = None, + timeout: Any = None, + **kwargs: Any, + ) -> Any: + """更新向量库文件属性,严格遵循 SDK 的必填 ``attributes`` 契约。""" + reject_unsupported_kwargs("OpenAI 向量库文件更新", kwargs) + request = { + "vector_store_id": vector_store_id, + "attributes": dict(attributes) if attributes is not None else None, + } + request.update( + { + key: value + for key, value in { + "extra_headers": extra_headers, + "extra_query": extra_query, + "extra_body": extra_body, + "timeout": timeout, + }.items() + if value is not None + } + ) + return self._call_sdk_resource( + self._get_client().vector_stores.files.update, + args=(file_id,), + kwargs=request, + operation="OpenAI Vector Store 文件更新", + ) + + def delete_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.delete, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件删除", + ), + operation="OpenAI Vector Store 文件删除", + ) + + def vector_store_file_content(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.files.content, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件内容读取", + ), + operation="OpenAI Vector Store 文件内容读取", + ) + + def poll_vector_store_file( + self, + vector_store_id: str, + file_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """轮询向量库文件,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 向量库文件轮询", kwargs) + return self._call_sdk_resource( + self._get_client().vector_stores.files.poll, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + {"poll_interval_ms": poll_interval_ms} + if poll_interval_ms is not None + else {}, + "OpenAI Vector Store 文件轮询", + ), + operation="OpenAI Vector Store 文件轮询", + ) + + def create_vector_store_file_batch(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.create, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件批次创建", + ) + + def create_vector_store_file_batch_and_poll(self, vector_store_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.create_and_poll, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件批次创建轮询", + ) + + def retrieve_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.retrieve, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次读取", + ), + operation="OpenAI Vector Store 文件批次读取", + ) + + def cancel_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.cancel, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次取消", + ), + operation="OpenAI Vector Store 文件批次取消", + ) + + def poll_vector_store_file_batch( + self, + vector_store_id: str, + batch_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """轮询向量库文件批次,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 向量库文件批次轮询", kwargs) + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.poll, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + {"poll_interval_ms": poll_interval_ms} + if poll_interval_ms is not None + else {}, + "OpenAI Vector Store 文件批次轮询", + ), + operation="OpenAI Vector Store 文件批次轮询", + ) + + def list_vector_store_file_batch_files(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().vector_stores.file_batches.list_files, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次文件列表", + ), + operation="OpenAI Vector Store 文件批次文件列表", + ) + + def upload_vector_store_file_batch_and_poll( + self, + vector_store_id: str, + files: Any, + *, + max_concurrency: int | None = None, + file_ids: Any | None = None, + poll_interval_ms: int | None = None, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """上传并轮询向量库文件批次,严格匹配 SDK 辅助方法参数。""" + reject_unsupported_kwargs("OpenAI 向量库文件批次上传轮询", kwargs) + poll_kwargs = { + key: value + for key, value in { + "max_concurrency": max_concurrency, + "file_ids": file_ids, + "poll_interval_ms": poll_interval_ms, + "chunking_strategy": chunking_strategy, + }.items() + if value is not None + } + upload_and_poll = cast(Any, self._get_client().vector_stores.file_batches.upload_and_poll) + return self._call_sdk_resource( + upload_and_poll, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "files": files}, + poll_kwargs, + "OpenAI Vector Store 文件批次上传轮询", + ), + operation="OpenAI Vector Store 文件批次上传轮询", + ) + + def list_models(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().models.list, kwargs=kwargs, operation="OpenAI 模型列表" + ) + + def retrieve_model(self, model: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().models.retrieve, + args=(model,), + kwargs=kwargs, + operation="OpenAI 模型读取", + ) + + def delete_model(self, model: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().models.delete, + args=(model,), + kwargs=kwargs, + operation="OpenAI 模型删除", + ) + + def create_moderation(self, input: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().moderations.create, + kwargs=self._merge_resource_kwargs( + {"input": input}, kwargs, "OpenAI Moderation 创建" + ), + operation="OpenAI Moderation 创建", + ) + + def generate_image(self, prompt: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().images.generate, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt}, kwargs, "OpenAI 图片生成" + ), + operation="OpenAI 图片生成", + ) + + def edit_image(self, image: Any, prompt: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().images.edit, + kwargs=self._merge_resource_kwargs( + {"image": image, "prompt": prompt}, kwargs, "OpenAI 图片编辑" + ), + operation="OpenAI 图片编辑", + ) + + def create_image_variation(self, image: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().images.create_variation, + kwargs=self._merge_resource_kwargs( + {"image": image}, kwargs, "OpenAI 图片变体" + ), + operation="OpenAI 图片变体", + ) + + def text_to_speech(self, text: str, model: str, voice: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().audio.speech.create, + kwargs=self._merge_resource_kwargs( + {"input": text, "model": model, "voice": voice}, + kwargs, + "OpenAI 语音合成", + ), + operation="OpenAI 语音合成", + ) + + def transcribe_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().audio.transcriptions.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "model": model}, kwargs, "OpenAI 音频转写" + ), + operation="OpenAI 音频转写", + ) + + def translate_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().audio.translations.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "model": model}, kwargs, "OpenAI 音频翻译" + ), + operation="OpenAI 音频翻译", + ) + + def create_video(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.create, kwargs=kwargs, operation="OpenAI 视频创建" + ) + + def create_video_and_poll(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.create_and_poll, + kwargs=kwargs, + operation="OpenAI 视频创建轮询", + ) + + def retrieve_video(self, video_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.retrieve, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频读取", + ) + + def list_videos(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.list, kwargs=kwargs, operation="OpenAI 视频列表" + ) + + def delete_video(self, video_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.delete, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频删除", + ) + + def download_video(self, video_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.download_content, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频下载", + ) + + def create_video_character(self, name: str, video: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.create_character, + kwargs=self._merge_resource_kwargs( + {"name": name, "video": video}, kwargs, "OpenAI 视频角色创建" + ), + operation="OpenAI 视频角色创建", + ) + + def retrieve_video_character(self, character_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.get_character, + args=(character_id,), + kwargs=kwargs, + operation="OpenAI 视频角色读取", + ) + + def edit_video(self, prompt: str, video: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.edit, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt, "video": video}, kwargs, "OpenAI 视频编辑" + ), + operation="OpenAI 视频编辑", + ) + + def extend_video(self, prompt: str, seconds: Any, video: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.extend, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt, "seconds": seconds, "video": video}, + kwargs, + "OpenAI 视频续写", + ), + operation="OpenAI 视频续写", + ) + + def remix_video(self, video_id: str, prompt: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().videos.remix, + args=(video_id,), + kwargs=self._merge_resource_kwargs( + {"prompt": prompt}, kwargs, "OpenAI 视频混剪" + ), + operation="OpenAI 视频混剪", + ) + + def poll_video( + self, + video_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """轮询视频处理状态,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 视频轮询", kwargs) + return self._call_sdk_resource( + self._get_client().videos.poll, + args=(video_id,), + kwargs={ + "poll_interval_ms": poll_interval_ms, + } if poll_interval_ms is not None else {}, + operation="OpenAI 视频轮询", + ) + + def create_upload(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().uploads.create, + kwargs=kwargs, + operation="OpenAI Upload 创建", + ) + + def complete_upload(self, upload_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().uploads.complete, + args=(upload_id,), + kwargs=kwargs, + operation="OpenAI Upload 完成", + ) + + def cancel_upload(self, upload_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().uploads.cancel, + args=(upload_id,), + kwargs=kwargs, + operation="OpenAI Upload 取消", + ) + + def create_upload_part(self, upload_id: str, data: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().uploads.parts.create, + args=(upload_id,), + kwargs=self._merge_resource_kwargs( + {"data": data}, kwargs, "OpenAI Upload 分片创建" + ), + operation="OpenAI Upload 分片创建", + ) + + def upload_file_chunked( + self, + *, + file: Any, + mime_type: str, + purpose: str, + filename: str | None = None, + bytes: int | None = None, + part_size: int | None = None, + md5: str | None = None, + **kwargs: Any, + ) -> Any: + """按 SDK 的窄参数契约执行分片上传。""" + reject_unsupported_kwargs("OpenAI 分片文件上传", kwargs) + request = { + key: value + for key, value in { + "file": file, + "mime_type": mime_type, + "purpose": purpose, + "filename": filename, + "bytes": bytes, + "part_size": part_size, + "md5": md5, + }.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().uploads.upload_file_chunked, + kwargs=request, + operation="OpenAI 分片文件上传", + ) + + def compact_responses( + self, + *, + model: str | None = None, + input: Any = None, + instructions: str | None = None, + previous_response_id: str | None = None, + prompt_cache_key: str | None = None, + prompt_cache_options: Any = None, + prompt_cache_retention: str | None = None, + service_tier: str | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + extra_body: dict[str, Any] | None = None, + timeout: Any = None, + **kwargs: Any, + ) -> Any: + """压缩 Responses 会话上下文,严格匹配 OpenAI SDK 参数。""" + self._require_responses_resource("compact_responses") + reject_unsupported_kwargs("OpenAI Responses compact", kwargs) + compact_kwargs = { + key: value + for key, value in { + "model": model or self._model_name, + "input": input, + "instructions": instructions, + "previous_response_id": previous_response_id, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "service_tier": service_tier, + "extra_headers": extra_headers, + "extra_query": extra_query, + "extra_body": extra_body, + "timeout": timeout, + }.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().responses.compact, + kwargs=compact_kwargs, + operation="OpenAI Responses compact", + ) + + def create_conversation(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.create, + kwargs=kwargs, + operation="OpenAI Conversation 创建", + ) + + def retrieve_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.retrieve, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 读取", + ) + + def update_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.update, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 更新", + ) + + def delete_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.delete, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 删除", + ) + + def list_conversation_items(self, conversation_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.items.list, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 项目列表", + ) + + def create_conversation_items(self, conversation_id: str, items: Any, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.items.create, + args=(conversation_id,), + kwargs=self._merge_resource_kwargs( + {"items": items}, kwargs, "OpenAI Conversation 项目创建" + ), + operation="OpenAI Conversation 项目创建", + ) + + def retrieve_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.items.retrieve, + args=(item_id,), + kwargs=self._merge_resource_kwargs( + {"conversation_id": conversation_id}, + kwargs, + "OpenAI Conversation 项目读取", + ), + operation="OpenAI Conversation 项目读取", + ) + + def delete_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().conversations.items.delete, + args=(item_id,), + kwargs=self._merge_resource_kwargs( + {"conversation_id": conversation_id}, + kwargs, + "OpenAI Conversation 项目删除", + ), + operation="OpenAI Conversation 项目删除", + ) + + def create_container(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.create, + kwargs=kwargs, + operation="OpenAI Container 创建", + ) + + def retrieve_container(self, container_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.retrieve, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 读取", + ) + + def list_containers(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.list, + kwargs=kwargs, + operation="OpenAI Container 列表", + ) + + def delete_container(self, container_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.delete, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 删除", + ) + + def create_container_file( + self, + container_id: str, + file: Any = None, + file_id: str | None = None, + **kwargs: Any, + ) -> Any: + file_kwargs = { + key: value + for key, value in {"file": file, "file_id": file_id}.items() + if value is not None + } + return self._call_sdk_resource( + self._get_client().containers.files.create, + args=(container_id,), + kwargs=self._merge_resource_kwargs( + file_kwargs, + kwargs, + "OpenAI Container 文件创建", + ), + operation="OpenAI Container 文件创建", + ) + + def list_container_files(self, container_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.files.list, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 文件列表", + ) + + def retrieve_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.files.retrieve, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件读取", + ), + operation="OpenAI Container 文件读取", + ) + + def delete_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().containers.files.delete, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件删除", + ), + operation="OpenAI Container 文件删除", + ) + + def container_file_content(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + files = getattr(getattr(self._get_client(), "containers", None), "files", None) + content = getattr(files, "content", None) + method = getattr(content, "retrieve", None) + if not callable(method): + raise NotImplementedError( + "当前 OpenAI SDK 不提供容器文件内容读取资源 " + "(containers.files.content.retrieve)。" + ) + return self._call_sdk_resource( + method, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件内容读取", + ), + operation="OpenAI Container 文件内容读取", + ) + + def create_fine_tuning_job(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.create, + kwargs=kwargs, + operation="OpenAI Fine-tuning 创建", + ) + + def list_fine_tuning_jobs(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.list, + kwargs=kwargs, + operation="OpenAI Fine-tuning 列表", + ) + + def retrieve_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.retrieve, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 读取", + ) + + def cancel_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.cancel, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 取消", + ) + + def pause_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.pause, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 暂停", + ) + + def resume_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.resume, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 恢复", + ) + + def list_fine_tuning_events(self, job_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().fine_tuning.jobs.list_events, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 事件列表", + ) + + def create_eval(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.create, + kwargs=kwargs, + operation="OpenAI Eval 创建", + ) + + def list_evals(self, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.list, + kwargs=kwargs, + operation="OpenAI Eval 列表", + ) + + def retrieve_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.retrieve, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 读取", + ) + + def update_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.update, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 更新", + ) + + def delete_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.delete, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 删除", + ) + + def create_eval_run(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.create, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval Run 创建", + ) + + def list_eval_runs(self, eval_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.list, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval Run 列表", + ) + + def retrieve_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.retrieve, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 读取" + ), + operation="OpenAI Eval Run 读取", + ) + + def cancel_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.cancel, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 取消" + ), + operation="OpenAI Eval Run 取消", + ) + + def delete_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.delete, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 删除" + ), + operation="OpenAI Eval Run 删除", + ) + + def list_eval_run_output_items(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.output_items.list, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, + kwargs, + "OpenAI Eval Run 输出项列表", + ), + operation="OpenAI Eval Run 输出项列表", + ) + + def retrieve_eval_run_output_item( + self, eval_id: str, run_id: str, output_item_id: str, **kwargs: Any + ) -> Any: + return self._call_sdk_resource( + self._get_client().evals.runs.output_items.retrieve, + args=(output_item_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id, "run_id": run_id}, + kwargs, + "OpenAI Eval Run 输出项读取", + ), + operation="OpenAI Eval Run 输出项读取", + ) + + async def async_upload_file(self, file: Any, purpose: str = "assistants", **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().files.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "purpose": cast(Any, purpose)}, + kwargs, + "OpenAI 文件上传", + ), + operation="OpenAI 文件上传", + ) + + async def async_list_files(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().files.list, kwargs=kwargs, operation="OpenAI 文件列表" + ) + + async def async_retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().files.retrieve, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件读取", + ) + + async def async_file_content(self, file_id: str, **kwargs: Any) -> Any: + files = getattr(self._get_aclient(), "files", None) + method = getattr(files, "content", None) + if not callable(method): + raise NotImplementedError( + "当前 OpenAI SDK 不提供文件内容读取资源(files.content)。" + ) + return await self._call_async_sdk_resource( + method, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件内容读取", + ) + + async def async_retrieve_file_content(self, file_id: str, **kwargs: Any) -> Any: + files = self._get_aclient().files + method = getattr(files, "retrieve_content", None) + if method is not None: + return await self._call_async_sdk_resource( + method, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件文本读取", + ) + return await self.async_file_content(file_id, **kwargs) + + async def async_delete_file(self, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().files.delete, + args=(file_id,), + kwargs=kwargs, + operation="OpenAI 文件删除", + ) + + async def async_wait_for_file( + self, + file_id: str, + *, + poll_interval: float | None = None, + max_wait_seconds: float | None = None, + **kwargs: Any, + ) -> Any: + """异步等待文件处理完成,仅转发 AsyncOpenAI SDK 的轮询参数。""" + reject_unsupported_kwargs("OpenAI 文件等待", kwargs) + poll_kwargs = { + key: value + for key, value in { + "poll_interval": poll_interval, + "max_wait_seconds": max_wait_seconds, + }.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().files.wait_for_processing, + kwargs=self._merge_resource_kwargs( + {"id": file_id}, poll_kwargs, "OpenAI 文件等待" + ), + operation="OpenAI 文件等待", + ) + + async def async_create_batch(self, input_file_id: str, endpoint: str | None = None, **kwargs: Any) -> Any: + if self._protocol == "responses" and ( + endpoint is None or endpoint.rstrip("/") == "/v1/responses" + ): + self._require_responses_resource("create_batch") + if endpoint is None: + endpoint = "/v1/responses" if self._protocol == "responses" else "/v1/chat/completions" + kwargs.setdefault("completion_window", "24h") + return await self._call_async_sdk_resource( + self._get_aclient().batches.create, + kwargs=self._merge_resource_kwargs( + { + "input_file_id": input_file_id, + "endpoint": cast(Any, endpoint), + }, + kwargs, + "OpenAI Batch 创建", + ), + operation="OpenAI Batch 创建", + ) + + async def async_retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().batches.retrieve, + args=(batch_id,), + kwargs=kwargs, + operation="OpenAI Batch 读取", + ) + + async def async_cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().batches.cancel, + args=(batch_id,), + kwargs=kwargs, + operation="OpenAI Batch 取消", + ) + + async def async_list_batches(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().batches.list, kwargs=kwargs, operation="OpenAI Batch 列表" + ) + + async def async_retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("retrieve_response") + return await self._call_async_sdk_resource( + self._get_aclient().responses.retrieve, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 读取", + ) + + async def async_cancel_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("cancel_response") + return await self._call_async_sdk_resource( + self._get_aclient().responses.cancel, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 取消", + ) + + async def async_delete_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("delete_response") + return await self._call_async_sdk_resource( + self._get_aclient().responses.delete, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 删除", + ) + + async def async_list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_response_input_items") + return await self._call_async_sdk_resource( + self._get_aclient().responses.input_items.list, + args=(response_id,), + kwargs=kwargs, + operation="OpenAI Responses 输入项列表", + ) + + def async_connect_responses(self, **kwargs: Any) -> Any: + """返回 AsyncOpenAI Responses WebSocket 连接管理器。""" + self._require_responses_resource("connect_responses") + return self._call_sdk_resource( + self._get_aclient().responses.connect, + kwargs=kwargs, + operation="OpenAI Responses 连接", + ) + + def async_stream_responses(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """返回 AsyncOpenAI Responses 流上下文管理器。""" + self._require_responses_resource("stream_responses") + if request is not None and kwargs: + raise ValueError("async_stream_responses 不能同时传 request 和原生关键字参数。") + if request is not None: + params = self._build_responses_request(**request.copy_with(stream=True).to_invoke_kwargs()) + params.pop("stream", None) + else: + params = dict(kwargs) + params = self._prepare_responses_stream_params(params) + return self._call_sdk_resource( + self._get_aclient().responses.stream, + kwargs=params, + operation="OpenAI Responses 流", + ) + + async def async_count_input_tokens(self, request: CompletionRequest | None = None, **kwargs: Any) -> int: + request = coerce_completion_request(request, kwargs, "OpenAI-compatible async_count_input_tokens") + self._require_responses_resource("count_input_tokens") + params = self._build_token_count_request(request) + response = await self._call_async_sdk_resource( + self._get_aclient().responses.input_tokens.count, + kwargs=params, + operation="OpenAI Responses 输入 token 统计", + ) + value = self._field(response, "input_tokens") + if value is None: + raise RuntimeError("OpenAI token count 响应缺少 input_tokens。") + return int(value) + + async def async_create_vector_store(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.create, + kwargs=kwargs, + operation="OpenAI Vector Store 创建", + ) + + async def async_retrieve_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.retrieve, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 读取", + ) + + async def async_list_vector_stores(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.list, + kwargs=kwargs, + operation="OpenAI Vector Store 列表", + ) + + async def async_search_vector_store(self, vector_store_id: str, query: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.search, + args=(vector_store_id,), + kwargs=self._merge_resource_kwargs( + {"query": query}, kwargs, "OpenAI Vector Store 搜索" + ), + operation="OpenAI Vector Store 搜索", + ) + + async def async_delete_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.delete, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 删除", + ) + + async def async_update_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.update, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 更新", + ) + + async def async_create_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.create, + args=(vector_store_id,), + kwargs=self._merge_resource_kwargs( + {"file_id": file_id}, kwargs, "OpenAI Vector Store 文件创建" + ), + operation="OpenAI Vector Store 文件创建", + ) + + async def async_create_vector_store_file_and_poll(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.create_and_poll, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件创建轮询", + ), + operation="OpenAI Vector Store 文件创建轮询", + ) + + async def async_upload_vector_store_file( + self, + vector_store_id: str, + file: Any, + *, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """异步上传并挂载向量库文件,仅转发 SDK 支持的分块策略。""" + reject_unsupported_kwargs("OpenAI 向量库文件上传", kwargs) + upload_kwargs = ( + {"chunking_strategy": chunking_strategy} + if chunking_strategy is not None + else {} + ) + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.upload, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "file": file}, + upload_kwargs, + "OpenAI Vector Store 文件上传", + ), + operation="OpenAI Vector Store 文件上传", + ) + + async def async_upload_vector_store_file_and_poll( + self, + vector_store_id: str, + file: Any, + *, + attributes: Mapping[str, str | float | bool] | None = None, + poll_interval_ms: int | None = None, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """异步上传单个文件到向量库并等待处理完成。""" + reject_unsupported_kwargs("OpenAI 向量库文件上传轮询", kwargs) + upload_kwargs = { + key: value + for key, value in { + "attributes": dict(attributes) if attributes is not None else None, + "poll_interval_ms": poll_interval_ms, + "chunking_strategy": chunking_strategy, + }.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.upload_and_poll, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "file": file}, + upload_kwargs, + "OpenAI Vector Store 文件上传轮询", + ), + operation="OpenAI Vector Store 文件上传轮询", + ) + + async def async_retrieve_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.retrieve, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件读取", + ), + operation="OpenAI Vector Store 文件读取", + ) + + async def async_list_vector_store_files(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.list, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件列表", + ) + + async def async_update_vector_store_file( + self, + vector_store_id: str, + file_id: str, + *, + attributes: Mapping[str, str | float | bool] | None, + extra_headers: Mapping[str, str] | None = None, + extra_query: Mapping[str, Any] | None = None, + extra_body: Mapping[str, Any] | None = None, + timeout: Any = None, + **kwargs: Any, + ) -> Any: + """异步更新向量库文件属性,严格遵循 SDK 的必填 ``attributes`` 契约。""" + reject_unsupported_kwargs("OpenAI 向量库文件更新", kwargs) + request = { + "vector_store_id": vector_store_id, + "attributes": dict(attributes) if attributes is not None else None, + } + request.update( + { + key: value + for key, value in { + "extra_headers": extra_headers, + "extra_query": extra_query, + "extra_body": extra_body, + "timeout": timeout, + }.items() + if value is not None + } + ) + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.update, + args=(file_id,), + kwargs=request, + operation="OpenAI Vector Store 文件更新", + ) + + async def async_delete_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.delete, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件删除", + ), + operation="OpenAI Vector Store 文件删除", + ) + + async def async_vector_store_file_content(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.content, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件内容读取", + ), + operation="OpenAI Vector Store 文件内容读取", + ) + + async def async_poll_vector_store_file( + self, + vector_store_id: str, + file_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """异步轮询向量库文件,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 向量库文件轮询", kwargs) + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.files.poll, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + {"poll_interval_ms": poll_interval_ms} + if poll_interval_ms is not None + else {}, + "OpenAI Vector Store 文件轮询", + ), + operation="OpenAI Vector Store 文件轮询", + ) + + async def async_create_vector_store_file_batch(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.create, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件批次创建", + ) + + async def async_create_vector_store_file_batch_and_poll(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.create_and_poll, + args=(vector_store_id,), + kwargs=kwargs, + operation="OpenAI Vector Store 文件批次创建轮询", + ) + + async def async_retrieve_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.retrieve, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次读取", + ), + operation="OpenAI Vector Store 文件批次读取", + ) + + async def async_cancel_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.cancel, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次取消", + ), + operation="OpenAI Vector Store 文件批次取消", + ) + + async def async_poll_vector_store_file_batch( + self, + vector_store_id: str, + batch_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """异步轮询向量库文件批次,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 向量库文件批次轮询", kwargs) + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.poll, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + {"poll_interval_ms": poll_interval_ms} + if poll_interval_ms is not None + else {}, + "OpenAI Vector Store 文件批次轮询", + ), + operation="OpenAI Vector Store 文件批次轮询", + ) + + async def async_list_vector_store_file_batch_files(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().vector_stores.file_batches.list_files, + args=(batch_id,), + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id}, + kwargs, + "OpenAI Vector Store 文件批次文件列表", + ), + operation="OpenAI Vector Store 文件批次文件列表", + ) + + async def async_upload_vector_store_file_batch_and_poll( + self, + vector_store_id: str, + files: Any, + *, + max_concurrency: int | None = None, + file_ids: Any | None = None, + poll_interval_ms: int | None = None, + chunking_strategy: Any | None = None, + **kwargs: Any, + ) -> Any: + """异步上传并轮询向量库文件批次,严格匹配 SDK 参数。""" + reject_unsupported_kwargs("OpenAI 向量库文件批次上传轮询", kwargs) + poll_kwargs = { + key: value + for key, value in { + "max_concurrency": max_concurrency, + "file_ids": file_ids, + "poll_interval_ms": poll_interval_ms, + "chunking_strategy": chunking_strategy, + }.items() + if value is not None + } + upload_and_poll = cast( + Any, self._get_aclient().vector_stores.file_batches.upload_and_poll + ) + return await self._call_async_sdk_resource( + upload_and_poll, + kwargs=self._merge_resource_kwargs( + {"vector_store_id": vector_store_id, "files": files}, + poll_kwargs, + "OpenAI Vector Store 文件批次上传轮询", + ), + operation="OpenAI Vector Store 文件批次上传轮询", + ) + + async def async_list_models(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().models.list, kwargs=kwargs, operation="OpenAI 模型列表" + ) + + async def async_retrieve_model(self, model: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().models.retrieve, + args=(model,), + kwargs=kwargs, + operation="OpenAI 模型读取", + ) + + async def async_delete_model(self, model: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().models.delete, + args=(model,), + kwargs=kwargs, + operation="OpenAI 模型删除", + ) + + async def async_create_moderation(self, input: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().moderations.create, + kwargs=self._merge_resource_kwargs( + {"input": input}, kwargs, "OpenAI Moderation 创建" + ), + operation="OpenAI Moderation 创建", + ) + + async def async_generate_image(self, prompt: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().images.generate, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt}, kwargs, "OpenAI 图片生成" + ), + operation="OpenAI 图片生成", + ) + + async def async_edit_image(self, image: Any, prompt: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().images.edit, + kwargs=self._merge_resource_kwargs( + {"image": image, "prompt": prompt}, kwargs, "OpenAI 图片编辑" + ), + operation="OpenAI 图片编辑", + ) + + async def async_create_image_variation(self, image: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().images.create_variation, + kwargs=self._merge_resource_kwargs( + {"image": image}, kwargs, "OpenAI 图片变体" + ), + operation="OpenAI 图片变体", + ) + + async def async_text_to_speech(self, text: str, model: str, voice: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().audio.speech.create, + kwargs=self._merge_resource_kwargs( + {"input": text, "model": model, "voice": voice}, + kwargs, + "OpenAI 语音合成", + ), + operation="OpenAI 语音合成", + ) + + async def async_transcribe_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().audio.transcriptions.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "model": model}, kwargs, "OpenAI 音频转写" + ), + operation="OpenAI 音频转写", + ) + + async def async_translate_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().audio.translations.create, + kwargs=self._merge_resource_kwargs( + {"file": file, "model": model}, kwargs, "OpenAI 音频翻译" + ), + operation="OpenAI 音频翻译", + ) + + async def async_create_video(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.create, + kwargs=kwargs, + operation="OpenAI 视频创建", + ) + + async def async_create_video_and_poll(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.create_and_poll, + kwargs=kwargs, + operation="OpenAI 视频创建轮询", + ) + + async def async_retrieve_video(self, video_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.retrieve, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频读取", + ) + + async def async_list_videos(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.list, kwargs=kwargs, operation="OpenAI 视频列表" + ) + + async def async_delete_video(self, video_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.delete, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频删除", + ) + + async def async_download_video(self, video_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.download_content, + args=(video_id,), + kwargs=kwargs, + operation="OpenAI 视频下载", + ) + + async def async_create_video_character(self, name: str, video: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.create_character, + kwargs=self._merge_resource_kwargs( + {"name": name, "video": video}, kwargs, "OpenAI 视频角色创建" + ), + operation="OpenAI 视频角色创建", + ) + + async def async_retrieve_video_character(self, character_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.get_character, + args=(character_id,), + kwargs=kwargs, + operation="OpenAI 视频角色读取", + ) + + async def async_edit_video(self, prompt: str, video: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.edit, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt, "video": video}, kwargs, "OpenAI 视频编辑" + ), + operation="OpenAI 视频编辑", + ) + + async def async_extend_video(self, prompt: str, seconds: Any, video: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.extend, + kwargs=self._merge_resource_kwargs( + {"prompt": prompt, "seconds": seconds, "video": video}, + kwargs, + "OpenAI 视频续写", + ), + operation="OpenAI 视频续写", + ) + + async def async_remix_video(self, video_id: str, prompt: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().videos.remix, + args=(video_id,), + kwargs=self._merge_resource_kwargs( + {"prompt": prompt}, kwargs, "OpenAI 视频混剪" + ), + operation="OpenAI 视频混剪", + ) + + async def async_poll_video( + self, + video_id: str, + *, + poll_interval_ms: int | None = None, + **kwargs: Any, + ) -> Any: + """异步轮询视频处理状态,仅支持 SDK 的 ``poll_interval_ms``。""" + reject_unsupported_kwargs("OpenAI 视频轮询", kwargs) + return await self._call_async_sdk_resource( + self._get_aclient().videos.poll, + args=(video_id,), + kwargs={ + "poll_interval_ms": poll_interval_ms, + } if poll_interval_ms is not None else {}, + operation="OpenAI 视频轮询", + ) + + async def async_create_upload(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().uploads.create, + kwargs=kwargs, + operation="OpenAI Upload 创建", + ) + + async def async_complete_upload(self, upload_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().uploads.complete, + args=(upload_id,), + kwargs=kwargs, + operation="OpenAI Upload 完成", + ) + + async def async_cancel_upload(self, upload_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().uploads.cancel, + args=(upload_id,), + kwargs=kwargs, + operation="OpenAI Upload 取消", + ) + + async def async_create_upload_part(self, upload_id: str, data: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().uploads.parts.create, + args=(upload_id,), + kwargs=self._merge_resource_kwargs( + {"data": data}, kwargs, "OpenAI Upload 分片创建" + ), + operation="OpenAI Upload 分片创建", + ) + + async def async_upload_file_chunked( + self, + *, + file: Any, + mime_type: str, + purpose: str, + filename: str | None = None, + bytes: int | None = None, + part_size: int | None = None, + md5: str | None = None, + **kwargs: Any, + ) -> Any: + """异步分片上传,严格匹配 SDK 的窄参数契约。""" + reject_unsupported_kwargs("OpenAI 分片文件上传", kwargs) + request = { + key: value + for key, value in { + "file": file, + "mime_type": mime_type, + "purpose": purpose, + "filename": filename, + "bytes": bytes, + "part_size": part_size, + "md5": md5, + }.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().uploads.upload_file_chunked, + kwargs=request, + operation="OpenAI 分片文件上传", + ) + + async def async_compact_responses( + self, + *, + model: str | None = None, + input: Any = None, + instructions: str | None = None, + previous_response_id: str | None = None, + prompt_cache_key: str | None = None, + prompt_cache_options: Any = None, + prompt_cache_retention: str | None = None, + service_tier: str | None = None, + extra_headers: dict[str, str] | None = None, + extra_query: dict[str, Any] | None = None, + extra_body: dict[str, Any] | None = None, + timeout: Any = None, + **kwargs: Any, + ) -> Any: + """异步压缩 Responses 会话,严格匹配 AsyncOpenAI SDK 参数。""" + self._require_responses_resource("compact_responses") + reject_unsupported_kwargs("OpenAI Responses compact", kwargs) + compact_kwargs = { + key: value + for key, value in { + "model": model or self._model_name, + "input": input, + "instructions": instructions, + "previous_response_id": previous_response_id, + "prompt_cache_key": prompt_cache_key, + "prompt_cache_options": prompt_cache_options, + "prompt_cache_retention": prompt_cache_retention, + "service_tier": service_tier, + "extra_headers": extra_headers, + "extra_query": extra_query, + "extra_body": extra_body, + "timeout": timeout, + }.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().responses.compact, + kwargs=compact_kwargs, + operation="OpenAI Responses compact", + ) + + async def async_create_conversation(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.create, + kwargs=kwargs, + operation="OpenAI Conversation 创建", + ) + + async def async_retrieve_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.retrieve, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 读取", + ) + + async def async_update_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.update, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 更新", + ) + + async def async_delete_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.delete, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 删除", + ) + + async def async_list_conversation_items(self, conversation_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.items.list, + args=(conversation_id,), + kwargs=kwargs, + operation="OpenAI Conversation 项目列表", + ) + + async def async_create_conversation_items(self, conversation_id: str, items: Any, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.items.create, + args=(conversation_id,), + kwargs=self._merge_resource_kwargs( + {"items": items}, kwargs, "OpenAI Conversation 项目创建" + ), + operation="OpenAI Conversation 项目创建", + ) + + async def async_retrieve_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.items.retrieve, + args=(item_id,), + kwargs=self._merge_resource_kwargs( + {"conversation_id": conversation_id}, + kwargs, + "OpenAI Conversation 项目读取", + ), + operation="OpenAI Conversation 项目读取", + ) + + async def async_delete_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().conversations.items.delete, + args=(item_id,), + kwargs=self._merge_resource_kwargs( + {"conversation_id": conversation_id}, + kwargs, + "OpenAI Conversation 项目删除", + ), + operation="OpenAI Conversation 项目删除", + ) + + async def async_create_container(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.create, + kwargs=kwargs, + operation="OpenAI Container 创建", + ) + + async def async_retrieve_container(self, container_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.retrieve, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 读取", + ) + + async def async_list_containers(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.list, + kwargs=kwargs, + operation="OpenAI Container 列表", + ) + + async def async_delete_container(self, container_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.delete, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 删除", + ) + + async def async_create_container_file( + self, + container_id: str, + file: Any = None, + file_id: str | None = None, + **kwargs: Any, + ) -> Any: + file_kwargs = { + key: value + for key, value in {"file": file, "file_id": file_id}.items() + if value is not None + } + return await self._call_async_sdk_resource( + self._get_aclient().containers.files.create, + args=(container_id,), + kwargs=self._merge_resource_kwargs( + file_kwargs, + kwargs, + "OpenAI Container 文件创建", + ), + operation="OpenAI Container 文件创建", + ) + + async def async_list_container_files(self, container_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.files.list, + args=(container_id,), + kwargs=kwargs, + operation="OpenAI Container 文件列表", + ) + + async def async_retrieve_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.files.retrieve, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件读取", + ), + operation="OpenAI Container 文件读取", + ) + + async def async_delete_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().containers.files.delete, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件删除", + ), + operation="OpenAI Container 文件删除", + ) + + async def async_container_file_content(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + files = getattr(getattr(self._get_aclient(), "containers", None), "files", None) + content = getattr(files, "content", None) + method = getattr(content, "retrieve", None) + if not callable(method): + raise NotImplementedError( + "当前 OpenAI SDK 不提供容器文件内容读取资源 " + "(containers.files.content.retrieve)。" + ) + return await self._call_async_sdk_resource( + method, + args=(file_id,), + kwargs=self._merge_resource_kwargs( + {"container_id": container_id}, + kwargs, + "OpenAI Container 文件内容读取", + ), + operation="OpenAI Container 文件内容读取", + ) + + async def async_create_fine_tuning_job(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.create, + kwargs=kwargs, + operation="OpenAI Fine-tuning 创建", + ) + + async def async_list_fine_tuning_jobs(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.list, + kwargs=kwargs, + operation="OpenAI Fine-tuning 列表", + ) + + async def async_retrieve_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.retrieve, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 读取", + ) + + async def async_cancel_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.cancel, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 取消", + ) + + async def async_pause_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.pause, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 暂停", + ) + + async def async_resume_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.resume, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 恢复", + ) + + async def async_list_fine_tuning_events(self, job_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().fine_tuning.jobs.list_events, + args=(job_id,), + kwargs=kwargs, + operation="OpenAI Fine-tuning 事件列表", + ) + + async def async_create_eval(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.create, + kwargs=kwargs, + operation="OpenAI Eval 创建", + ) + + async def async_list_evals(self, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.list, + kwargs=kwargs, + operation="OpenAI Eval 列表", + ) + + async def async_retrieve_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.retrieve, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 读取", + ) + + async def async_update_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.update, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 更新", + ) + + async def async_delete_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.delete, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval 删除", + ) + + async def async_create_eval_run(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.create, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval Run 创建", + ) + + async def async_list_eval_runs(self, eval_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.list, + args=(eval_id,), + kwargs=kwargs, + operation="OpenAI Eval Run 列表", + ) + + async def async_retrieve_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.retrieve, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 读取" + ), + operation="OpenAI Eval Run 读取", + ) + + async def async_cancel_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.cancel, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 取消" + ), + operation="OpenAI Eval Run 取消", + ) + + async def async_delete_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.delete, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, kwargs, "OpenAI Eval Run 删除" + ), + operation="OpenAI Eval Run 删除", + ) + + async def async_list_eval_run_output_items(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.output_items.list, + args=(run_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id}, + kwargs, + "OpenAI Eval Run 输出项列表", + ), + operation="OpenAI Eval Run 输出项列表", + ) + + async def async_retrieve_eval_run_output_item( + self, eval_id: str, run_id: str, output_item_id: str, **kwargs: Any + ) -> Any: + return await self._call_async_sdk_resource( + self._get_aclient().evals.runs.output_items.retrieve, + args=(output_item_id,), + kwargs=self._merge_resource_kwargs( + {"eval_id": eval_id, "run_id": run_id}, + kwargs, + "OpenAI Eval Run 输出项读取", + ), + operation="OpenAI Eval Run 输出项读取", + ) + + def close(self) -> None: + """释放同步和异步 OpenAI SDK 客户端。 + + 同步上下文中会桥接等待异步客户端的关闭;活动事件循环中则显式 + 要求调用 ``aclose``,并保留未关闭的异步客户端供后续清理。 + """ + client = self._client + async_client = self._aclient + self._client = None + first_error: Exception | None = None + closed_ids: set[int] = set() + if client is not None: + try: + client.close() + closed_ids.add(id(client)) + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + if async_client is not None: + if id(async_client) in closed_ids: + self._aclient = None + else: + try: + close_resource_sync(async_client, "OpenAI 异步客户端") + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + else: + self._aclient = None + else: + self._aclient = None + if first_error is not None: + raise first_error + + async def aclose(self) -> None: + """释放同步和异步 OpenAI SDK 客户端,即使其中一个关闭失败。""" + async_client = self._aclient + sync_client = self._client + self._aclient = None + self._client = None + first_error: Exception | None = None + closed_clients: set[int] = set() + for client in (async_client, sync_client): + if client is None or id(client) in closed_clients: + continue + closed_clients.add(id(client)) + try: + result = client.close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + if first_error is not None: + raise first_error diff --git a/src/providers/qwen.py b/src/providers/qwen.py index 3585ee8..e9fa2ac 100644 --- a/src/providers/qwen.py +++ b/src/providers/qwen.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,5 @@ class QwenProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="qwen") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="qwen", protocol=protocol, options=options) diff --git a/src/providers/resources.py b/src/providers/resources.py new file mode 100644 index 0000000..2523705 --- /dev/null +++ b/src/providers/resources.py @@ -0,0 +1,1936 @@ +"""显式的供应商资源能力包装。 + +这些 Facade 只负责把调用转发到已经初始化的 SDK 客户端,不把文件、批处理 +或缓存生命周期混入普通聊天请求。 +""" + +import inspect +from collections.abc import Callable +from functools import wraps +from typing import Any, TypeVar + +from src.utils.security import ( + validate_secret_free_resource_args, + validate_secret_free_resource_kwargs, +) + +_FacadeMethod = TypeVar("_FacadeMethod", bound=Callable[..., Any]) + + +_NATIVE_SCALARS = (str, bytes, bytearray, int, float, bool, complex, type(None)) +_RESOURCE_TREE_RETURNING_METHODS = frozenset({"with_options", "copy"}) + + +def _allow_resource_business_parameters(*names: str) -> Callable[[_FacadeMethod], _FacadeMethod]: + """声明官方 SDK 明确定义、但名称类似凭证的业务参数。""" + + allowed = frozenset(names) + + def decorator(method: _FacadeMethod) -> _FacadeMethod: + method._resource_business_parameters = allowed # type: ignore[attr-defined] + return method + + return decorator + + +class _NativeResourceProxy: + """为 SDK 原生资源树增加请求扩展校验,同时保持结果对象原样返回。 + + `.native` 仍然返回官方客户端本身;只有通过 Facade 动态访问的资源节点 + 使用此代理。这样既能兼容 SDK 新增资源,又不会让 `extra_headers`、 + `extra_query` 或 `extra_body` 绕过统一凭证边界。 + """ + + __slots__ = ("_children", "_path", "_provider", "_value") + + def __init__(self, value: Any, provider: Any, path: str): + self._value = value + self._provider = provider + self._path = path + self._children: dict[str, Any] = {} + + def __getattr__(self, name: str) -> Any: + if name in self._children: + return self._children[name] + value = getattr(self._value, name) + wrapped = _wrap_native_resource_value( + value, + self._provider, + f"{self._path}.{name}", + ) + self._children[name] = wrapped + return wrapped + + def __call__(self, *args: Any, **kwargs: Any) -> Any: + # 先做凭证校验:它约束的是「参数能不能出现在请求里」,与渠道是否具备 + # 该能力无关,也是更根本的边界。顺序反了会让携带凭证的调用报出能力 + # 错误,把真正的安全问题掩盖成配置问题。 + safe_args = validate_secret_free_resource_args( + args, + f"{_resource_provider_label(self._provider)} 原生资源 {self._path}", + ) + safe_kwargs = validate_secret_free_resource_kwargs( + kwargs, + f"{_resource_provider_label(self._provider)} 原生资源 {self._path}", + ) + # 再做能力门禁:动态访问的资源节点必须和显式 Facade 方法受同一套约束。 + # 只做凭证校验不够:``resources.responses.create(...)`` 这类路径会绕过 + # server_verified_protocols 检查,把本地 SDK 资源直接发往未验证的服务端。 + resource_guard = getattr(self._provider, "_require_provider_resource", None) + if callable(resource_guard): + # 传完整路径(``volcengine.responses.create``)而不是末段方法名: + # ``files.create`` 与 ``responses.create`` 末段相同,只看方法名无法 + # 区分该走哪条能力门禁。 + resource_guard(self._path) + # Call results are deliberately not wrapped. They are response models, + # pagers, streams, or context managers rather than mutable resource + # trees; preserving their SDK identity keeps normal client code intact. + result = self._value(*safe_args, **safe_kwargs) + # ``with_options``/``copy`` return another SDK client (and some SDK + # resource nodes expose the same helpers). Keep that resource tree + # behind the proxy so subsequent calls cannot bypass the same guard. + method_name = self._path.rsplit(".", 1)[-1] + if method_name in _RESOURCE_TREE_RETURNING_METHODS: + return _wrap_native_resource_value(result, self._provider, self._path) + return result + + def __repr__(self) -> str: + return repr(self._value) + + +def _wrap_native_resource_value(value: Any, provider: Any, path: str) -> Any: + if isinstance(value, _NativeResourceProxy): + return value + if isinstance(value, _NATIVE_SCALARS): + return value + if isinstance(value, (dict, list, tuple, set, frozenset)): + return value + if inspect.ismodule(value) or inspect.isclass(value): + return value + return _NativeResourceProxy(value, provider, path) + + +def _resource_provider_label(provider: Any) -> str: + return str( + getattr(provider, "_provider", None) + or provider.__class__.__name__ + ) + + +def _guard_resource_method(method: _FacadeMethod) -> _FacadeMethod: + """在所有显式 Facade 方法前统一校验 SDK 扩展参数。""" + + allowed_business_parameters = frozenset( + getattr(method, "_resource_business_parameters", ()) + ) + + def sanitize_call( + self: Any, + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> tuple[tuple[Any, ...], dict[str, Any]]: + label = f"{_resource_provider_label(object.__getattribute__(self, 'provider'))} 资源" + if not allowed_business_parameters: + return ( + validate_secret_free_resource_args(args, label), + validate_secret_free_resource_kwargs(kwargs, label), + ) + + signature = inspect.signature(method) + bound = signature.bind_partial(self, *args, **kwargs) + for name, value in list(bound.arguments.items()): + if name == "self" or name in allowed_business_parameters: + continue + parameter = signature.parameters[name] + if parameter.kind is inspect.Parameter.VAR_POSITIONAL: + bound.arguments[name] = validate_secret_free_resource_args(value, label) + elif parameter.kind is inspect.Parameter.VAR_KEYWORD: + bound.arguments[name] = validate_secret_free_resource_kwargs(value, label) + else: + bound.arguments[name] = validate_secret_free_resource_kwargs( + {name: value}, label + )[name] + return bound.args[1:], dict(bound.kwargs) + + if inspect.iscoroutinefunction(method): + + @wraps(method) + async def async_wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + safe_args, safe_kwargs = sanitize_call(self, args, kwargs) + return await method(self, *safe_args, **safe_kwargs) + + return async_wrapper # type: ignore[return-value] + + @wraps(method) + def wrapper(self: Any, *args: Any, **kwargs: Any) -> Any: + safe_args, safe_kwargs = sanitize_call(self, args, kwargs) + return method(self, *safe_args, **safe_kwargs) + + return wrapper # type: ignore[return-value] + + +class _NativeFacade: + """保留官方 SDK 的完整资源面,Facade 只负责稳定的常用别名。""" + + _delegate_native = False + _delegate_client_name = "sdk_client" + + def __init_subclass__(cls, **kwargs: Any) -> None: + """为子类显式资源方法安装统一的请求扩展校验。""" + + super().__init_subclass__(**kwargs) + for name, method in list(cls.__dict__.items()): + if name.startswith("_") or not callable(method): + continue + setattr(cls, name, _guard_resource_method(method)) + + def __init__(self, provider: Any): + self.provider = provider + + @property + def native(self) -> Any: + return self.provider.sdk_client + + @property + def async_native(self) -> Any: + return self.provider.async_sdk_client + + def _delegated_resource(self, name: str, *, asynchronous: bool = False) -> Any: + """读取 SDK 资源并套用请求参数安全代理。 + + ``native``/``async_native`` 是有意保留的原生客户端出口;Facade 的 + 其它资源节点则必须经过同一个代理,避免显式属性绕过动态资源的 + ``extra_headers``、``extra_query`` 和 ``extra_body`` 校验。 + """ + client_name = "async_sdk_client" if asynchronous else "sdk_client" + client = getattr(self.provider, client_name) + try: + value = getattr(client, name) + except (AttributeError, NotImplementedError) as exc: + raise NotImplementedError( + f"{_resource_provider_label(self.provider)} SDK 未提供 {name} 资源。" + ) from exc + if value is None: + raise NotImplementedError( + f"{_resource_provider_label(self.provider)} SDK 未提供 {name} 资源。" + ) + label = _resource_provider_label(self.provider) + return _wrap_native_resource_value(value, self.provider, f"{label}.{name}") + + @property + def beta(self) -> Any: + """访问供应商 SDK 的 beta 资源树(若该 SDK 提供)。""" + if not self._delegate_native: + raise AttributeError("beta") + return self._delegated_resource("beta") + + @property + def async_beta(self) -> Any: + """访问异步 SDK 的 beta 资源树(若该 SDK 提供)。""" + if not self._delegate_native: + raise AttributeError("async_beta") + return self._delegated_resource("beta", asynchronous=True) + + def __getattr__(self, name: str) -> Any: + """Expose the complete official SDK tree without widening compatible channels. + + Explicit facade methods remain the stable cross-provider surface. Official + providers additionally allow direct access to resources introduced by the + installed SDK after this module was released (for example Realtime, + Webhooks, Live, Interactions, or Beta resources). Dynamic nodes are + proxied so every callable still enforces the credential boundary; use + `.native` when an unwrapped SDK client is explicitly required. + """ + # Python introspection, copy/deepcopy and pickle probe dunder names via + # getattr. Never forward those probes to a third-party SDK resource. + if name.startswith("__") and name.endswith("__"): + raise AttributeError(name) + if not self._delegate_native: + raise AttributeError(name) + client = getattr(self.provider, self._delegate_client_name) + try: + value = getattr(client, name) + except (AttributeError, NotImplementedError) as exc: + raise AttributeError(name) from exc + if value is None: + raise AttributeError(name) + return _wrap_native_resource_value( + value, + self.provider, + f"{_resource_provider_label(self.provider)}.{name}", + ) + + +class OpenAICompatibleResources(_NativeFacade): + """OpenAI-compatible channels expose only their native client by default.""" + + +class AsyncOpenAICompatibleResources(_NativeFacade): + """Async native-client access for OpenAI-compatible channels.""" + + +class _AsyncOnlyResourceFacade(_NativeFacade): + """Expose async-only SDK resources while retaining native delegation.""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + +class OpenAIResources(OpenAICompatibleResources): + """OpenAI 官方资源 Facade;兼容渠道不会继承此类能力声明。""" + + _delegate_native = True + + @property + def skills(self) -> Any: + """访问 OpenAI Skills 资源树。""" + return self._delegated_resource("skills") + + @property + def realtime(self) -> Any: + """访问 OpenAI Realtime 资源树。""" + return self._delegated_resource("realtime") + + @property + def webhooks(self) -> Any: + """访问 OpenAI Webhooks 资源树。""" + return self._delegated_resource("webhooks") + + @property + def admin(self) -> Any: + """访问 OpenAI Admin 资源树。""" + return self._delegated_resource("admin") + + @property + def content_provenance_checks(self) -> Any: + """访问 OpenAI 内容来源校验资源树。""" + return self._delegated_resource("content_provenance_checks") + + def upload_file(self, file: Any, purpose: str = "assistants", **kwargs: Any) -> Any: + return self.provider.upload_file(file, purpose=purpose, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + return self.provider.list_files(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_file(file_id, **kwargs) + + def file_content(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.file_content(file_id, **kwargs) + + def retrieve_file_content(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_file_content(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_file(file_id, **kwargs) + + def wait_for_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.wait_for_file(file_id, **kwargs) + + def create_batch(self, input_file_id: str, endpoint: str | None = None, **kwargs: Any) -> Any: + return self.provider.create_batch(input_file_id, endpoint=endpoint, **kwargs) + + def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_batch(batch_id, **kwargs) + + def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_batch(batch_id, **kwargs) + + def list_batches(self, **kwargs: Any) -> Any: + return self.provider.list_batches(**kwargs) + + def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_response(response_id, **kwargs) + + def cancel_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_response(response_id, **kwargs) + + def delete_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.delete_response(response_id, **kwargs) + + def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.list_response_input_items(response_id, **kwargs) + + def connect_responses(self, **kwargs: Any) -> Any: + return self.provider.connect_responses(**kwargs) + + def stream_responses(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.stream_responses(request, **kwargs) + + def count_input_tokens(self, request: Any = None, **kwargs: Any) -> int: + return self.provider.count_input_tokens(request, **kwargs) + + def create_vector_store(self, **kwargs: Any) -> Any: + return self.provider.create_vector_store(**kwargs) + + def retrieve_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_vector_store(vector_store_id, **kwargs) + + def list_vector_stores(self, **kwargs: Any) -> Any: + return self.provider.list_vector_stores(**kwargs) + + def search_vector_store(self, vector_store_id: str, query: Any, **kwargs: Any) -> Any: + return self.provider.search_vector_store(vector_store_id, query, **kwargs) + + def delete_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.delete_vector_store(vector_store_id, **kwargs) + + def update_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.update_vector_store(vector_store_id, **kwargs) + + def create_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.create_vector_store_file(vector_store_id, file_id, **kwargs) + + def create_vector_store_file_and_poll(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.create_vector_store_file_and_poll(vector_store_id, file_id, **kwargs) + + def upload_vector_store_file(self, vector_store_id: str, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_vector_store_file(vector_store_id, file, **kwargs) + + def upload_vector_store_file_and_poll(self, vector_store_id: str, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_vector_store_file_and_poll(vector_store_id, file, **kwargs) + + def retrieve_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_vector_store_file(vector_store_id, file_id, **kwargs) + + def list_vector_store_files(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.list_vector_store_files(vector_store_id, **kwargs) + + def update_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.update_vector_store_file(vector_store_id, file_id, **kwargs) + + def delete_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_vector_store_file(vector_store_id, file_id, **kwargs) + + def vector_store_file_content(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.vector_store_file_content(vector_store_id, file_id, **kwargs) + + def poll_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.poll_vector_store_file(vector_store_id, file_id, **kwargs) + + def create_vector_store_file_batch(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.create_vector_store_file_batch(vector_store_id, **kwargs) + + def create_vector_store_file_batch_and_poll(self, vector_store_id: str, **kwargs: Any) -> Any: + return self.provider.create_vector_store_file_batch_and_poll(vector_store_id, **kwargs) + + def retrieve_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + def cancel_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + def poll_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self.provider.poll_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + def list_vector_store_file_batch_files(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return self.provider.list_vector_store_file_batch_files(vector_store_id, batch_id, **kwargs) + + def upload_vector_store_file_batch_and_poll(self, vector_store_id: str, files: Any, **kwargs: Any) -> Any: + return self.provider.upload_vector_store_file_batch_and_poll(vector_store_id, files, **kwargs) + + def list_models(self, **kwargs: Any) -> Any: + return self.provider.list_models(**kwargs) + + def retrieve_model(self, model: str, **kwargs: Any) -> Any: + return self.provider.retrieve_model(model, **kwargs) + + def delete_model(self, model: str, **kwargs: Any) -> Any: + return self.provider.delete_model(model, **kwargs) + + def create_moderation(self, input: Any, **kwargs: Any) -> Any: + return self.provider.create_moderation(input, **kwargs) + + def generate_image(self, prompt: str, **kwargs: Any) -> Any: + return self.provider.generate_image(prompt, **kwargs) + + def edit_image(self, image: Any, prompt: str, **kwargs: Any) -> Any: + return self.provider.edit_image(image, prompt, **kwargs) + + def create_image_variation(self, image: Any, **kwargs: Any) -> Any: + return self.provider.create_image_variation(image, **kwargs) + + def text_to_speech(self, text: str, model: str, voice: str, **kwargs: Any) -> Any: + return self.provider.text_to_speech(text, model, voice, **kwargs) + + def transcribe_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return self.provider.transcribe_audio(file, model, **kwargs) + + def translate_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return self.provider.translate_audio(file, model, **kwargs) + + def create_video(self, **kwargs: Any) -> Any: + return self.provider.create_video(**kwargs) + + def create_video_and_poll(self, **kwargs: Any) -> Any: + return self.provider.create_video_and_poll(**kwargs) + + def retrieve_video(self, video_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_video(video_id, **kwargs) + + def list_videos(self, **kwargs: Any) -> Any: + return self.provider.list_videos(**kwargs) + + def delete_video(self, video_id: str, **kwargs: Any) -> Any: + return self.provider.delete_video(video_id, **kwargs) + + def download_video(self, video_id: str, **kwargs: Any) -> Any: + return self.provider.download_video(video_id, **kwargs) + + def create_video_character(self, name: str, video: Any, **kwargs: Any) -> Any: + return self.provider.create_video_character(name, video, **kwargs) + + def retrieve_video_character(self, character_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_video_character(character_id, **kwargs) + + def edit_video(self, prompt: str, video: Any, **kwargs: Any) -> Any: + return self.provider.edit_video(prompt, video, **kwargs) + + def extend_video(self, prompt: str, seconds: Any, video: Any, **kwargs: Any) -> Any: + return self.provider.extend_video(prompt, seconds, video, **kwargs) + + def remix_video(self, video_id: str, prompt: str, **kwargs: Any) -> Any: + return self.provider.remix_video(video_id, prompt, **kwargs) + + def poll_video(self, video_id: str, **kwargs: Any) -> Any: + return self.provider.poll_video(video_id, **kwargs) + + def create_upload(self, **kwargs: Any) -> Any: + return self.provider.create_upload(**kwargs) + + def complete_upload(self, upload_id: str, **kwargs: Any) -> Any: + return self.provider.complete_upload(upload_id, **kwargs) + + def cancel_upload(self, upload_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_upload(upload_id, **kwargs) + + def create_upload_part(self, upload_id: str, data: Any, **kwargs: Any) -> Any: + return self.provider.create_upload_part(upload_id, data, **kwargs) + + def upload_file_chunked(self, **kwargs: Any) -> Any: + return self.provider.upload_file_chunked(**kwargs) + + def parse(self, text_format: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.parse(text_format, request, **kwargs) + + def compact_responses(self, **kwargs: Any) -> Any: + return self.provider.compact_responses(**kwargs) + + def create_conversation(self, **kwargs: Any) -> Any: + return self.provider.create_conversation(**kwargs) + + def retrieve_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_conversation(conversation_id, **kwargs) + + def update_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self.provider.update_conversation(conversation_id, **kwargs) + + def delete_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return self.provider.delete_conversation(conversation_id, **kwargs) + + def list_conversation_items(self, conversation_id: str, **kwargs: Any) -> Any: + return self.provider.list_conversation_items(conversation_id, **kwargs) + + def create_conversation_items(self, conversation_id: str, items: Any, **kwargs: Any) -> Any: + return self.provider.create_conversation_items(conversation_id, items, **kwargs) + + def retrieve_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_conversation_item(conversation_id, item_id, **kwargs) + + def delete_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return self.provider.delete_conversation_item(conversation_id, item_id, **kwargs) + + def create_container(self, **kwargs: Any) -> Any: + return self.provider.create_container(**kwargs) + + def retrieve_container(self, container_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_container(container_id, **kwargs) + + def list_containers(self, **kwargs: Any) -> Any: + return self.provider.list_containers(**kwargs) + + def delete_container(self, container_id: str, **kwargs: Any) -> Any: + return self.provider.delete_container(container_id, **kwargs) + + def create_container_file(self, container_id: str, file: Any = None, file_id: str | None = None, **kwargs: Any) -> Any: + return self.provider.create_container_file(container_id, file=file, file_id=file_id, **kwargs) + + def list_container_files(self, container_id: str, **kwargs: Any) -> Any: + return self.provider.list_container_files(container_id, **kwargs) + + def retrieve_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_container_file(container_id, file_id, **kwargs) + + def delete_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_container_file(container_id, file_id, **kwargs) + + def container_file_content(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return self.provider.container_file_content(container_id, file_id, **kwargs) + + def create_fine_tuning_job(self, **kwargs: Any) -> Any: + return self.provider.create_fine_tuning_job(**kwargs) + + def list_fine_tuning_jobs(self, **kwargs: Any) -> Any: + return self.provider.list_fine_tuning_jobs(**kwargs) + + def retrieve_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_fine_tuning_job(job_id, **kwargs) + + def cancel_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_fine_tuning_job(job_id, **kwargs) + + def pause_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.pause_fine_tuning_job(job_id, **kwargs) + + def resume_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.resume_fine_tuning_job(job_id, **kwargs) + + def list_fine_tuning_events(self, job_id: str, **kwargs: Any) -> Any: + return self.provider.list_fine_tuning_events(job_id, **kwargs) + + def create_eval(self, **kwargs: Any) -> Any: + return self.provider.create_eval(**kwargs) + + def list_evals(self, **kwargs: Any) -> Any: + return self.provider.list_evals(**kwargs) + + def retrieve_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_eval(eval_id, **kwargs) + + def update_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.update_eval(eval_id, **kwargs) + + def delete_eval(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.delete_eval(eval_id, **kwargs) + + def create_eval_run(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.create_eval_run(eval_id, **kwargs) + + def list_eval_runs(self, eval_id: str, **kwargs: Any) -> Any: + return self.provider.list_eval_runs(eval_id, **kwargs) + + def retrieve_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_eval_run(eval_id, run_id, **kwargs) + + def cancel_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_eval_run(eval_id, run_id, **kwargs) + + def delete_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self.provider.delete_eval_run(eval_id, run_id, **kwargs) + + def list_eval_run_output_items(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return self.provider.list_eval_run_output_items(eval_id, run_id, **kwargs) + + def retrieve_eval_run_output_item( + self, eval_id: str, run_id: str, output_item_id: str, **kwargs: Any + ) -> Any: + return self.provider.retrieve_eval_run_output_item( + eval_id, run_id, output_item_id, **kwargs + ) + + +class AsyncOpenAIResources(AsyncOpenAICompatibleResources): + """OpenAI AsyncOpenAI 资源的显式异步 Facade。""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + @property + def skills(self) -> Any: + """访问 AsyncOpenAI Skills 资源树。""" + return self._delegated_resource("skills", asynchronous=True) + + @property + def realtime(self) -> Any: + """访问 AsyncOpenAI Realtime 资源树。""" + return self._delegated_resource("realtime", asynchronous=True) + + @property + def webhooks(self) -> Any: + """访问 AsyncOpenAI Webhooks 资源树。""" + return self._delegated_resource("webhooks", asynchronous=True) + + @property + def admin(self) -> Any: + """访问 AsyncOpenAI Admin 资源树。""" + return self._delegated_resource("admin", asynchronous=True) + + @property + def content_provenance_checks(self) -> Any: + """访问 AsyncOpenAI 内容来源校验资源树。""" + return self._delegated_resource("content_provenance_checks", asynchronous=True) + + async def upload_file(self, file: Any, purpose: str = "assistants", **kwargs: Any) -> Any: + return await self.provider.async_upload_file(file, purpose=purpose, **kwargs) + + async def list_files(self, **kwargs: Any) -> Any: + return await self.provider.async_list_files(**kwargs) + + async def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_file(file_id, **kwargs) + + async def file_content(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_file_content(file_id, **kwargs) + + async def retrieve_file_content(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_file_content(file_id, **kwargs) + + async def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file(file_id, **kwargs) + + async def wait_for_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_wait_for_file(file_id, **kwargs) + + async def create_batch(self, input_file_id: str, endpoint: str | None = None, **kwargs: Any) -> Any: + return await self.provider.async_create_batch(input_file_id, endpoint=endpoint, **kwargs) + + async def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_batch(batch_id, **kwargs) + + async def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_batch(batch_id, **kwargs) + + async def list_batches(self, **kwargs: Any) -> Any: + return await self.provider.async_list_batches(**kwargs) + + async def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_response(response_id, **kwargs) + + async def cancel_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_response(response_id, **kwargs) + + async def delete_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_response(response_id, **kwargs) + + async def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_response_input_items(response_id, **kwargs) + + def connect_responses(self, **kwargs: Any) -> Any: + return self.provider.async_connect_responses(**kwargs) + + def stream_responses(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.async_stream_responses(request, **kwargs) + + async def count_input_tokens(self, request: Any = None, **kwargs: Any) -> int: + return await self.provider.async_count_input_tokens(request, **kwargs) + + async def create_vector_store(self, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store(**kwargs) + + async def retrieve_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_vector_store(vector_store_id, **kwargs) + + async def list_vector_stores(self, **kwargs: Any) -> Any: + return await self.provider.async_list_vector_stores(**kwargs) + + async def search_vector_store(self, vector_store_id: str, query: Any, **kwargs: Any) -> Any: + return await self.provider.async_search_vector_store(vector_store_id, query, **kwargs) + + async def delete_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_vector_store(vector_store_id, **kwargs) + + async def update_vector_store(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_vector_store(vector_store_id, **kwargs) + + async def create_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store_file(vector_store_id, file_id, **kwargs) + + async def create_vector_store_file_and_poll(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store_file_and_poll(vector_store_id, file_id, **kwargs) + + async def upload_vector_store_file(self, vector_store_id: str, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_vector_store_file(vector_store_id, file, **kwargs) + + async def upload_vector_store_file_and_poll(self, vector_store_id: str, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_vector_store_file_and_poll(vector_store_id, file, **kwargs) + + async def create_vector_store_file_batch(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store_file_batch(vector_store_id, **kwargs) + + async def create_vector_store_file_batch_and_poll(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_vector_store_file_batch_and_poll(vector_store_id, **kwargs) + + async def retrieve_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_vector_store_file(vector_store_id, file_id, **kwargs) + + async def list_vector_store_files(self, vector_store_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_vector_store_files(vector_store_id, **kwargs) + + async def update_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_vector_store_file(vector_store_id, file_id, **kwargs) + + async def delete_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_vector_store_file(vector_store_id, file_id, **kwargs) + + async def vector_store_file_content(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_vector_store_file_content(vector_store_id, file_id, **kwargs) + + async def poll_vector_store_file(self, vector_store_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_poll_vector_store_file(vector_store_id, file_id, **kwargs) + + async def retrieve_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + async def cancel_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + async def poll_vector_store_file_batch(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_poll_vector_store_file_batch(vector_store_id, batch_id, **kwargs) + + async def list_vector_store_file_batch_files(self, vector_store_id: str, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_vector_store_file_batch_files(vector_store_id, batch_id, **kwargs) + + async def upload_vector_store_file_batch_and_poll(self, vector_store_id: str, files: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_vector_store_file_batch_and_poll(vector_store_id, files, **kwargs) + + async def list_models(self, **kwargs: Any) -> Any: + return await self.provider.async_list_models(**kwargs) + + async def retrieve_model(self, model: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_model(model, **kwargs) + + async def delete_model(self, model: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_model(model, **kwargs) + + async def create_moderation(self, input: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_moderation(input, **kwargs) + + async def generate_image(self, prompt: str, **kwargs: Any) -> Any: + return await self.provider.async_generate_image(prompt, **kwargs) + + async def edit_image(self, image: Any, prompt: str, **kwargs: Any) -> Any: + return await self.provider.async_edit_image(image, prompt, **kwargs) + + async def create_image_variation(self, image: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_image_variation(image, **kwargs) + + async def text_to_speech(self, text: str, model: str, voice: str, **kwargs: Any) -> Any: + return await self.provider.async_text_to_speech(text, model, voice, **kwargs) + + async def transcribe_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return await self.provider.async_transcribe_audio(file, model, **kwargs) + + async def translate_audio(self, file: Any, model: str, **kwargs: Any) -> Any: + return await self.provider.async_translate_audio(file, model, **kwargs) + + async def create_video(self, **kwargs: Any) -> Any: + return await self.provider.async_create_video(**kwargs) + + async def create_video_and_poll(self, **kwargs: Any) -> Any: + return await self.provider.async_create_video_and_poll(**kwargs) + + async def retrieve_video(self, video_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_video(video_id, **kwargs) + + async def list_videos(self, **kwargs: Any) -> Any: + return await self.provider.async_list_videos(**kwargs) + + async def delete_video(self, video_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_video(video_id, **kwargs) + + async def download_video(self, video_id: str, **kwargs: Any) -> Any: + return await self.provider.async_download_video(video_id, **kwargs) + + async def create_video_character(self, name: str, video: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_video_character(name, video, **kwargs) + + async def retrieve_video_character(self, character_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_video_character(character_id, **kwargs) + + async def edit_video(self, prompt: str, video: Any, **kwargs: Any) -> Any: + return await self.provider.async_edit_video(prompt, video, **kwargs) + + async def extend_video(self, prompt: str, seconds: Any, video: Any, **kwargs: Any) -> Any: + return await self.provider.async_extend_video(prompt, seconds, video, **kwargs) + + async def remix_video(self, video_id: str, prompt: str, **kwargs: Any) -> Any: + return await self.provider.async_remix_video(video_id, prompt, **kwargs) + + async def poll_video(self, video_id: str, **kwargs: Any) -> Any: + return await self.provider.async_poll_video(video_id, **kwargs) + + async def create_upload(self, **kwargs: Any) -> Any: + return await self.provider.async_create_upload(**kwargs) + + async def complete_upload(self, upload_id: str, **kwargs: Any) -> Any: + return await self.provider.async_complete_upload(upload_id, **kwargs) + + async def cancel_upload(self, upload_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_upload(upload_id, **kwargs) + + async def create_upload_part(self, upload_id: str, data: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_upload_part(upload_id, data, **kwargs) + + async def upload_file_chunked(self, **kwargs: Any) -> Any: + return await self.provider.async_upload_file_chunked(**kwargs) + + async def parse(self, text_format: Any, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_parse(text_format, request, **kwargs) + + async def compact_responses(self, **kwargs: Any) -> Any: + return await self.provider.async_compact_responses(**kwargs) + + async def create_conversation(self, **kwargs: Any) -> Any: + return await self.provider.async_create_conversation(**kwargs) + + async def retrieve_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_conversation(conversation_id, **kwargs) + + async def update_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_conversation(conversation_id, **kwargs) + + async def delete_conversation(self, conversation_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_conversation(conversation_id, **kwargs) + + async def list_conversation_items(self, conversation_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_conversation_items(conversation_id, **kwargs) + + async def create_conversation_items(self, conversation_id: str, items: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_conversation_items(conversation_id, items, **kwargs) + + async def retrieve_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_conversation_item(conversation_id, item_id, **kwargs) + + async def delete_conversation_item(self, conversation_id: str, item_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_conversation_item(conversation_id, item_id, **kwargs) + + async def create_container(self, **kwargs: Any) -> Any: + return await self.provider.async_create_container(**kwargs) + + async def retrieve_container(self, container_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_container(container_id, **kwargs) + + async def list_containers(self, **kwargs: Any) -> Any: + return await self.provider.async_list_containers(**kwargs) + + async def delete_container(self, container_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_container(container_id, **kwargs) + + async def create_container_file(self, container_id: str, file: Any = None, file_id: str | None = None, **kwargs: Any) -> Any: + return await self.provider.async_create_container_file(container_id, file=file, file_id=file_id, **kwargs) + + async def list_container_files(self, container_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_container_files(container_id, **kwargs) + + async def retrieve_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_container_file(container_id, file_id, **kwargs) + + async def delete_container_file(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_container_file(container_id, file_id, **kwargs) + + async def container_file_content(self, container_id: str, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_container_file_content(container_id, file_id, **kwargs) + + async def create_fine_tuning_job(self, **kwargs: Any) -> Any: + return await self.provider.async_create_fine_tuning_job(**kwargs) + + async def list_fine_tuning_jobs(self, **kwargs: Any) -> Any: + return await self.provider.async_list_fine_tuning_jobs(**kwargs) + + async def retrieve_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_fine_tuning_job(job_id, **kwargs) + + async def cancel_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_fine_tuning_job(job_id, **kwargs) + + async def pause_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_pause_fine_tuning_job(job_id, **kwargs) + + async def resume_fine_tuning_job(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_resume_fine_tuning_job(job_id, **kwargs) + + async def list_fine_tuning_events(self, job_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_fine_tuning_events(job_id, **kwargs) + + async def create_eval(self, **kwargs: Any) -> Any: + return await self.provider.async_create_eval(**kwargs) + + async def list_evals(self, **kwargs: Any) -> Any: + return await self.provider.async_list_evals(**kwargs) + + async def retrieve_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_eval(eval_id, **kwargs) + + async def update_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_eval(eval_id, **kwargs) + + async def delete_eval(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_eval(eval_id, **kwargs) + + async def create_eval_run(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_create_eval_run(eval_id, **kwargs) + + async def list_eval_runs(self, eval_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_eval_runs(eval_id, **kwargs) + + async def retrieve_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_eval_run(eval_id, run_id, **kwargs) + + async def cancel_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_eval_run(eval_id, run_id, **kwargs) + + async def delete_eval_run(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_eval_run(eval_id, run_id, **kwargs) + + async def list_eval_run_output_items(self, eval_id: str, run_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_eval_run_output_items(eval_id, run_id, **kwargs) + + async def retrieve_eval_run_output_item( + self, eval_id: str, run_id: str, output_item_id: str, **kwargs: Any + ) -> Any: + return await self.provider.async_retrieve_eval_run_output_item( + eval_id, run_id, output_item_id, **kwargs + ) + + +class GoogleResources(_NativeFacade): + + _delegate_native = True + + @property + def auth_tokens(self) -> Any: + """访问 google-genai 的 API token 管理资源。""" + return self._delegated_resource("auth_tokens") + + def create_auth_token(self, **kwargs: Any) -> Any: + return self.provider.create_auth_token(**kwargs) + + def upload_file(self, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_file(file, **kwargs) + + @_allow_resource_business_parameters("auth") + def register_files(self, auth: Any, uris: list[str], **kwargs: Any) -> Any: + return self.provider.register_files(auth=auth, uris=uris, **kwargs) + + def compute_tokens(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.compute_tokens(request, **kwargs) + + def count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return self.provider.count_tokens(request, **kwargs) + + def get_file(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_file(name, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + return self.provider.list_files(**kwargs) + + def download_file(self, file: Any, **kwargs: Any) -> bytes: + return self.provider.download_file(file, **kwargs) + + def delete_file(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_file(name, **kwargs) + + def create_cache(self, **kwargs: Any) -> Any: + return self.provider.create_cache(**kwargs) + + def get_cache(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_cache(name, **kwargs) + + def list_caches(self, **kwargs: Any) -> Any: + return self.provider.list_caches(**kwargs) + + def update_cache(self, name: str, **kwargs: Any) -> Any: + return self.provider.update_cache(name, **kwargs) + + def delete_cache(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_cache(name, **kwargs) + + def create_batch(self, src: Any, **kwargs: Any) -> Any: + return self.provider.create_batch(src, **kwargs) + + def create_embedding_batch(self, src: Any, **kwargs: Any) -> Any: + return self.provider.create_embedding_batch(src, **kwargs) + + def get_batch(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_batch(name, **kwargs) + + def list_batches(self, **kwargs: Any) -> Any: + return self.provider.list_batches(**kwargs) + + def cancel_batch(self, name: str, **kwargs: Any) -> Any: + return self.provider.cancel_batch(name, **kwargs) + + def delete_batch(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_batch(name, **kwargs) + + def list_models(self, **kwargs: Any) -> Any: + return self.provider.list_models(**kwargs) + + def get_model(self, model: str, **kwargs: Any) -> Any: + return self.provider.get_model(model, **kwargs) + + def delete_model(self, model: str, **kwargs: Any) -> Any: + return self.provider.delete_model(model, **kwargs) + + def update_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return self.provider.update_model(model, config=config, **kwargs) + + def tune( + self, base_model: str, training_dataset: Any, config: Any = None, **kwargs: Any + ) -> Any: + return self.provider.tune( + base_model, training_dataset, config=config, **kwargs + ) + + def get_tuning(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_tuning(name, **kwargs) + + def list_tunings(self, **kwargs: Any) -> Any: + return self.provider.list_tunings(**kwargs) + + def cancel_tuning(self, name: str, **kwargs: Any) -> Any: + return self.provider.cancel_tuning(name, **kwargs) + + def validate_tuning_reward( + self, + parent: str, + sample_response: Any, + example: Any, + single_reward_config: Any = None, + composite_reward_config: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return self.provider.validate_tuning_reward( + parent, + sample_response, + example, + single_reward_config=single_reward_config, + composite_reward_config=composite_reward_config, + config=config, + **kwargs, + ) + + def recontext_image(self, source: Any, **kwargs: Any) -> Any: + return self.provider.recontext_image(source, **kwargs) + + def generate_images(self, prompt: str, **kwargs: Any) -> Any: + return self.provider.generate_images(prompt, **kwargs) + + def edit_image(self, prompt: str, reference_images: list[Any], **kwargs: Any) -> Any: + return self.provider.edit_image(prompt, reference_images, **kwargs) + + def upscale_image(self, image: Any, upscale_factor: str, **kwargs: Any) -> Any: + return self.provider.upscale_image(image, upscale_factor, **kwargs) + + def segment_image(self, source: Any, **kwargs: Any) -> Any: + return self.provider.segment_image(source, **kwargs) + + def generate_videos(self, **kwargs: Any) -> Any: + return self.provider.generate_videos(**kwargs) + + def get_operation(self, operation: Any, **kwargs: Any) -> Any: + return self.provider.get_operation(operation, **kwargs) + + def list_file_search_documents(self, parent: str, **kwargs: Any) -> Any: + return self.provider.list_file_search_documents(parent, **kwargs) + + def get_file_search_document(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_file_search_document(name, **kwargs) + + def delete_file_search_document(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_file_search_document(name, **kwargs) + + def create_chat(self, **kwargs: Any) -> Any: + return self.provider.create_chat(**kwargs) + + def create_interaction(self, **kwargs: Any) -> Any: + return self.provider.create_interaction(**kwargs) + + def get_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return self.provider.get_interaction(interaction_id, **kwargs) + + def cancel_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_interaction(interaction_id, **kwargs) + + def delete_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return self.provider.delete_interaction(interaction_id, **kwargs) + + def create_agent(self, **kwargs: Any) -> Any: + return self.provider.create_agent(**kwargs) + + def get_agent(self, agent_id: str, **kwargs: Any) -> Any: + return self.provider.get_agent(agent_id, **kwargs) + + def list_agents(self, **kwargs: Any) -> Any: + return self.provider.list_agents(**kwargs) + + def delete_agent(self, agent_id: str, **kwargs: Any) -> Any: + return self.provider.delete_agent(agent_id, **kwargs) + + def create_webhook(self, **kwargs: Any) -> Any: + return self.provider.create_webhook(**kwargs) + + def get_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.get_webhook(webhook_id, **kwargs) + + def list_webhooks(self, **kwargs: Any) -> Any: + return self.provider.list_webhooks(**kwargs) + + def update_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.update_webhook(webhook_id, **kwargs) + + def delete_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.delete_webhook(webhook_id, **kwargs) + + def ping_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.ping_webhook(webhook_id, **kwargs) + + def rotate_webhook_signing_secret(self, webhook_id: str, **kwargs: Any) -> Any: + return self.provider.rotate_webhook_signing_secret(webhook_id, **kwargs) + + def create_environment(self, **kwargs: Any) -> Any: + return self.provider.create_environment(**kwargs) + + def get_environment(self, environment_id: str, **kwargs: Any) -> Any: + return self.provider.get_environment(environment_id, **kwargs) + + def list_environments(self, **kwargs: Any) -> Any: + return self.provider.list_environments(**kwargs) + + def delete_environment(self, environment_id: str, **kwargs: Any) -> Any: + return self.provider.delete_environment(environment_id, **kwargs) + + def get_environment_files(self, environment_id: str, path: str, **kwargs: Any) -> Any: + return self.provider.get_environment_files(environment_id, path, **kwargs) + + def create_trigger(self, **kwargs: Any) -> Any: + return self.provider.create_trigger(**kwargs) + + def get_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.get_trigger(trigger_id, **kwargs) + + def list_triggers(self, **kwargs: Any) -> Any: + return self.provider.list_triggers(**kwargs) + + def update_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.update_trigger(trigger_id, **kwargs) + + def delete_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.delete_trigger(trigger_id, **kwargs) + + def run_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.run_trigger(trigger_id, **kwargs) + + def list_trigger_executions(self, trigger_id: str, **kwargs: Any) -> Any: + return self.provider.list_trigger_executions(trigger_id, **kwargs) + + def connect_live(self, *, model: str | None = None, config: Any = None) -> Any: + return self.provider.connect_live(model=model, config=config) + + def create_file_search_store(self, **kwargs: Any) -> Any: + return self.provider.create_file_search_store(**kwargs) + + def get_file_search_store(self, name: str, **kwargs: Any) -> Any: + return self.provider.get_file_search_store(name, **kwargs) + + def list_file_search_stores(self, **kwargs: Any) -> Any: + return self.provider.list_file_search_stores(**kwargs) + + def delete_file_search_store(self, name: str, **kwargs: Any) -> Any: + return self.provider.delete_file_search_store(name, **kwargs) + + def import_file_to_file_search_store(self, file_search_store_name: str, file_name: str, **kwargs: Any) -> Any: + return self.provider.import_file_to_file_search_store(file_search_store_name, file_name, **kwargs) + + def upload_to_file_search_store(self, file_search_store_name: str, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_to_file_search_store(file_search_store_name, file, **kwargs) + + def download_file_search_media(self, media_id: str, **kwargs: Any) -> bytes: + return self.provider.download_file_search_media(media_id, **kwargs) + + +class AsyncGoogleResources(_NativeFacade): + """google-genai `Client.aio` 资源的显式异步 Facade。""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + @property + def auth_tokens(self) -> Any: + """访问 google-genai 异步 API token 管理资源。""" + return self._delegated_resource("auth_tokens", asynchronous=True) + + async def create_auth_token(self, **kwargs: Any) -> Any: + return await self.provider.async_create_auth_token(**kwargs) + + async def upload_file(self, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_file(file, **kwargs) + + @_allow_resource_business_parameters("auth") + async def register_files(self, auth: Any, uris: list[str], **kwargs: Any) -> Any: + return await self.provider.async_register_files(auth=auth, uris=uris, **kwargs) + + async def count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return await self.provider.async_count_tokens(request, **kwargs) + + async def compute_tokens(self, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_compute_tokens(request, **kwargs) + + async def get_file(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_file(name, **kwargs) + + async def list_files(self, **kwargs: Any) -> Any: + return await self.provider.async_list_files(**kwargs) + + async def download_file(self, file: Any, **kwargs: Any) -> bytes: + return await self.provider.async_download_file(file, **kwargs) + + async def delete_file(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file(name, **kwargs) + + async def create_interaction(self, **kwargs: Any) -> Any: + return await self.provider.async_create_interaction(**kwargs) + + async def get_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_interaction(interaction_id, **kwargs) + + async def cancel_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_interaction(interaction_id, **kwargs) + + async def delete_interaction(self, interaction_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_interaction(interaction_id, **kwargs) + + async def create_agent(self, **kwargs: Any) -> Any: + return await self.provider.async_create_agent(**kwargs) + + async def get_agent(self, agent_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_agent(agent_id, **kwargs) + + async def list_agents(self, **kwargs: Any) -> Any: + return await self.provider.async_list_agents(**kwargs) + + async def delete_agent(self, agent_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_agent(agent_id, **kwargs) + + async def create_webhook(self, **kwargs: Any) -> Any: + return await self.provider.async_create_webhook(**kwargs) + + async def get_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_webhook(webhook_id, **kwargs) + + async def list_webhooks(self, **kwargs: Any) -> Any: + return await self.provider.async_list_webhooks(**kwargs) + + async def update_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_webhook(webhook_id, **kwargs) + + async def delete_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_webhook(webhook_id, **kwargs) + + async def ping_webhook(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_ping_webhook(webhook_id, **kwargs) + + async def rotate_webhook_signing_secret(self, webhook_id: str, **kwargs: Any) -> Any: + return await self.provider.async_rotate_webhook_signing_secret(webhook_id, **kwargs) + + async def create_environment(self, **kwargs: Any) -> Any: + return await self.provider.async_create_environment(**kwargs) + + async def get_environment(self, environment_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_environment(environment_id, **kwargs) + + async def list_environments(self, **kwargs: Any) -> Any: + return await self.provider.async_list_environments(**kwargs) + + async def delete_environment(self, environment_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_environment(environment_id, **kwargs) + + async def get_environment_files(self, environment_id: str, path: str, **kwargs: Any) -> Any: + return await self.provider.async_get_environment_files(environment_id, path, **kwargs) + + async def create_trigger(self, **kwargs: Any) -> Any: + return await self.provider.async_create_trigger(**kwargs) + + async def get_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_trigger(trigger_id, **kwargs) + + async def list_triggers(self, **kwargs: Any) -> Any: + return await self.provider.async_list_triggers(**kwargs) + + async def update_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_update_trigger(trigger_id, **kwargs) + + async def delete_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_trigger(trigger_id, **kwargs) + + async def run_trigger(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_run_trigger(trigger_id, **kwargs) + + async def list_trigger_executions(self, trigger_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_trigger_executions(trigger_id, **kwargs) + + async def create_cache(self, **kwargs: Any) -> Any: + return await self.provider.async_create_cache(**kwargs) + + async def get_cache(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_cache(name, **kwargs) + + async def list_caches(self, **kwargs: Any) -> Any: + return await self.provider.async_list_caches(**kwargs) + + async def update_cache(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_update_cache(name, **kwargs) + + async def delete_cache(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_cache(name, **kwargs) + + async def create_batch(self, src: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_batch(src, **kwargs) + + async def create_embedding_batch(self, src: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_embedding_batch(src, **kwargs) + + async def get_batch(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_batch(name, **kwargs) + + async def list_batches(self, **kwargs: Any) -> Any: + return await self.provider.async_list_batches(**kwargs) + + async def cancel_batch(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_batch(name, **kwargs) + + async def delete_batch(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_batch(name, **kwargs) + + async def list_models(self, **kwargs: Any) -> Any: + return await self.provider.async_list_models(**kwargs) + + async def get_model(self, model: str, **kwargs: Any) -> Any: + return await self.provider.async_get_model(model, **kwargs) + + async def delete_model(self, model: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_model(model, **kwargs) + + async def update_model(self, model: str, config: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_update_model(model, config=config, **kwargs) + + async def tune( + self, base_model: str, training_dataset: Any, config: Any = None, **kwargs: Any + ) -> Any: + return await self.provider.async_tune( + base_model, training_dataset, config=config, **kwargs + ) + + async def get_tuning(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_tuning(name, **kwargs) + + async def list_tunings(self, **kwargs: Any) -> Any: + return await self.provider.async_list_tunings(**kwargs) + + async def cancel_tuning(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_tuning(name, **kwargs) + + async def validate_tuning_reward( + self, + parent: str, + sample_response: Any, + example: Any, + single_reward_config: Any = None, + composite_reward_config: Any = None, + config: Any = None, + **kwargs: Any, + ) -> Any: + return await self.provider.async_validate_tuning_reward( + parent, + sample_response, + example, + single_reward_config=single_reward_config, + composite_reward_config=composite_reward_config, + config=config, + **kwargs, + ) + + async def recontext_image(self, source: Any, **kwargs: Any) -> Any: + return await self.provider.async_recontext_image(source, **kwargs) + + async def generate_images(self, prompt: str, **kwargs: Any) -> Any: + return await self.provider.async_generate_images(prompt, **kwargs) + + async def edit_image(self, prompt: str, reference_images: list[Any], **kwargs: Any) -> Any: + return await self.provider.async_edit_image(prompt, reference_images, **kwargs) + + async def upscale_image(self, image: Any, upscale_factor: str, **kwargs: Any) -> Any: + return await self.provider.async_upscale_image(image, upscale_factor, **kwargs) + + async def segment_image(self, source: Any, **kwargs: Any) -> Any: + return await self.provider.async_segment_image(source, **kwargs) + + async def generate_videos(self, **kwargs: Any) -> Any: + return await self.provider.async_generate_videos(**kwargs) + + async def get_operation(self, operation: Any, **kwargs: Any) -> Any: + return await self.provider.async_get_operation(operation, **kwargs) + + async def list_file_search_documents(self, parent: str, **kwargs: Any) -> Any: + return await self.provider.async_list_file_search_documents(parent, **kwargs) + + async def get_file_search_document(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_file_search_document(name, **kwargs) + + async def delete_file_search_document(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file_search_document(name, **kwargs) + + async def create_chat(self, **kwargs: Any) -> Any: + return await self.provider.async_create_chat(**kwargs) + + def connect_live(self, *, model: str | None = None, config: Any = None) -> Any: + """返回异步 Live API 上下文管理器,调用方使用 `async with`。""" + return self.provider.async_connect_live(model=model, config=config) + + async def create_file_search_store(self, **kwargs: Any) -> Any: + return await self.provider.async_create_file_search_store(**kwargs) + + async def get_file_search_store(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_get_file_search_store(name, **kwargs) + + async def list_file_search_stores(self, **kwargs: Any) -> Any: + return await self.provider.async_list_file_search_stores(**kwargs) + + async def delete_file_search_store(self, name: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file_search_store(name, **kwargs) + + async def import_file_to_file_search_store(self, file_search_store_name: str, file_name: str, **kwargs: Any) -> Any: + return await self.provider.async_import_file_to_file_search_store( + file_search_store_name, file_name, **kwargs + ) + + async def upload_to_file_search_store(self, file_search_store_name: str, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_to_file_search_store( + file_search_store_name, file, **kwargs + ) + + async def download_file_search_media(self, media_id: str, **kwargs: Any) -> bytes: + return await self.provider.async_download_file_search_media(media_id, **kwargs) + + +class AnthropicResources(_NativeFacade): + + _delegate_native = True + + def _beta_resource(self, name: str) -> Any: + """返回 Anthropic Beta 资源,保留 SDK 的完整方法树。""" + return getattr(self.beta, name) + + @property + def beta_agents(self) -> Any: + return self._beta_resource("agents") + + @property + def beta_deployments(self) -> Any: + return self._beta_resource("deployments") + + @property + def beta_deployment_runs(self) -> Any: + return self._beta_resource("deployment_runs") + + @property + def beta_dreams(self) -> Any: + return self._beta_resource("dreams") + + @property + def beta_environments(self) -> Any: + return self._beta_resource("environments") + + @property + def beta_files(self) -> Any: + return self._beta_resource("files") + + @property + def beta_memory_stores(self) -> Any: + return self._beta_resource("memory_stores") + + @property + def beta_models(self) -> Any: + return self._beta_resource("models") + + @property + def beta_sessions(self) -> Any: + return self._beta_resource("sessions") + + @property + def beta_skills(self) -> Any: + return self._beta_resource("skills") + + @property + def beta_tunnels(self) -> Any: + return self._beta_resource("tunnels") + + @property + def beta_user_profiles(self) -> Any: + return self._beta_resource("user_profiles") + + @property + def beta_vaults(self) -> Any: + return self._beta_resource("vaults") + + @property + def beta_webhooks(self) -> Any: + return self._beta_resource("webhooks") + + def count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return self.provider.count_tokens(request, **kwargs) + + def create_batch(self, requests: Any, **kwargs: Any) -> Any: + return self.provider.create_batch(requests, **kwargs) + + def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_batch(batch_id, **kwargs) + + def batch_results(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.batch_results(batch_id, **kwargs) + + def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.cancel_batch(batch_id, **kwargs) + + def list_batches(self, **kwargs: Any) -> Any: + return self.provider.list_batches(**kwargs) + + def delete_batch(self, batch_id: str, **kwargs: Any) -> Any: + return self.provider.delete_batch(batch_id, **kwargs) + + def upload_file(self, file: Any, **kwargs: Any) -> Any: + return self.provider.upload_file(file, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + return self.provider.list_files(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_file(file_id, **kwargs) + + def download_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.download_file(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_file(file_id, **kwargs) + + def list_models(self, **kwargs: Any) -> Any: + return self.provider.list_models(**kwargs) + + def retrieve_model(self, model_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_model(model_id, **kwargs) + + def parse(self, output_format: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.parse(output_format, request, **kwargs) + + def beta_create(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.beta_create(request, **kwargs) + + def beta_parse(self, output_format: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.beta_parse(output_format, request, **kwargs) + + def beta_count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return self.provider.beta_count_tokens(request, **kwargs) + + def beta_stream(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.beta_stream(request, **kwargs) + + def beta_tool_runner(self, tools: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.beta_tool_runner(tools, request, **kwargs) + + +class AsyncAnthropicResources(_NativeFacade): + """Anthropic AsyncAnthropic 资源的显式异步 Facade。""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + def _beta_resource(self, name: str) -> Any: + """返回 AsyncAnthropic Beta 资源,保留 SDK 的完整方法树。""" + return getattr(self.async_beta, name) + + @property + def beta_agents(self) -> Any: + return self._beta_resource("agents") + + @property + def beta_deployments(self) -> Any: + return self._beta_resource("deployments") + + @property + def beta_deployment_runs(self) -> Any: + return self._beta_resource("deployment_runs") + + @property + def beta_dreams(self) -> Any: + return self._beta_resource("dreams") + + @property + def beta_environments(self) -> Any: + return self._beta_resource("environments") + + @property + def beta_files(self) -> Any: + return self._beta_resource("files") + + @property + def beta_memory_stores(self) -> Any: + return self._beta_resource("memory_stores") + + @property + def beta_models(self) -> Any: + return self._beta_resource("models") + + @property + def beta_sessions(self) -> Any: + return self._beta_resource("sessions") + + @property + def beta_skills(self) -> Any: + return self._beta_resource("skills") + + @property + def beta_tunnels(self) -> Any: + return self._beta_resource("tunnels") + + @property + def beta_user_profiles(self) -> Any: + return self._beta_resource("user_profiles") + + @property + def beta_vaults(self) -> Any: + return self._beta_resource("vaults") + + @property + def beta_webhooks(self) -> Any: + return self._beta_resource("webhooks") + + async def count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return await self.provider.async_count_tokens(request, **kwargs) + + async def create_batch(self, requests: Any, **kwargs: Any) -> Any: + return await self.provider.async_create_batch(requests, **kwargs) + + async def retrieve_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_batch(batch_id, **kwargs) + + async def batch_results(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_batch_results(batch_id, **kwargs) + + async def cancel_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_cancel_batch(batch_id, **kwargs) + + async def list_batches(self, **kwargs: Any) -> Any: + return await self.provider.async_list_batches(**kwargs) + + async def delete_batch(self, batch_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_batch(batch_id, **kwargs) + + async def upload_file(self, file: Any, **kwargs: Any) -> Any: + return await self.provider.async_upload_file(file, **kwargs) + + async def list_files(self, **kwargs: Any) -> Any: + return await self.provider.async_list_files(**kwargs) + + async def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_file(file_id, **kwargs) + + async def download_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_download_file(file_id, **kwargs) + + async def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file(file_id, **kwargs) + + async def list_models(self, **kwargs: Any) -> Any: + return await self.provider.async_list_models(**kwargs) + + async def retrieve_model(self, model_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_model(model_id, **kwargs) + + async def parse(self, output_format: Any, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_parse(output_format, request, **kwargs) + + async def beta_create(self, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_beta_create(request, **kwargs) + + async def beta_parse(self, output_format: Any, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_beta_parse(output_format, request, **kwargs) + + async def beta_count_tokens(self, request: Any = None, **kwargs: Any) -> int: + return await self.provider.async_beta_count_tokens(request, **kwargs) + + def beta_stream(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.async_beta_stream(request, **kwargs) + + def beta_tool_runner(self, tools: Any, request: Any = None, **kwargs: Any) -> Any: + return self.provider.async_beta_tool_runner(tools, request, **kwargs) + + +class ArkResources(_NativeFacade): + + _delegate_native = True + + def upload_file(self, file: Any, purpose: str, **kwargs: Any) -> Any: + return self.provider.upload_file(file, purpose, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + return self.provider.list_files(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_file(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.delete_file(file_id, **kwargs) + + def wait_for_file(self, file_id: str, **kwargs: Any) -> Any: + return self.provider.wait_for_file(file_id, **kwargs) + + def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.retrieve_response(response_id, **kwargs) + + def create_response(self, request: Any = None, **kwargs: Any) -> Any: + return self.provider.create_response(request, **kwargs) + + def delete_response(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.delete_response(response_id, **kwargs) + + def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + return self.provider.list_response_input_items(response_id, **kwargs) + + def list_input_items(self, response_id: str, **kwargs: Any) -> Any: + """Ark SDK 顶层 input_items 资源的直观别名。""" + return self.provider.list_input_items(response_id, **kwargs) + + def create_batch_chat(self, **kwargs: Any) -> Any: + return self.provider.create_batch_chat(**kwargs) + + def generate_batch_chat(self, **kwargs: Any) -> Any: + return self.provider.generate_batch_chat(**kwargs) + + def create_batch_embedding(self, **kwargs: Any) -> Any: + return self.provider.create_batch_embedding(**kwargs) + + def create_batch_multimodal_embedding(self, **kwargs: Any) -> Any: + return self.provider.create_batch_multimodal_embedding(**kwargs) + + def create_context(self, **kwargs: Any) -> Any: + return self.provider.create_context(**kwargs) + + def context_complete(self, **kwargs: Any) -> Any: + return self.provider.context_complete(**kwargs) + + def create_content_generation_task(self, **kwargs: Any) -> Any: + return self.provider.create_content_generation_task(**kwargs) + + def get_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + return self.provider.get_content_generation_task(task_id, **kwargs) + + def list_content_generation_tasks(self, **kwargs: Any) -> Any: + return self.provider.list_content_generation_tasks(**kwargs) + + def delete_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + return self.provider.delete_content_generation_task(task_id, **kwargs) + + def count_tokens(self, text: str | list[str], **kwargs: Any) -> int: + return self.provider.count_tokens(text, **kwargs) + + def multimodal_embed(self, inputs: Any, **kwargs: Any) -> Any: + return self.provider.multimodal_embed(inputs, **kwargs) + + def generate_image(self, **kwargs: Any) -> Any: + return self.provider.generate_image(**kwargs) + + def beta_chat_parse(self, **kwargs: Any) -> Any: + return self.provider.beta_chat_parse(**kwargs) + + def beta_chat_stream(self, **kwargs: Any) -> Any: + return self.provider.beta_chat_stream(**kwargs) + + def bot_chat(self, **kwargs: Any) -> Any: + return self.provider.bot_chat(**kwargs) + + def classify( + self, query: str, labels: list[str], model: str | None = None, **kwargs: Any + ) -> Any: + return self.provider.classify(query, labels, model=model, **kwargs) + + +class AsyncArkResources(_NativeFacade): + """Ark AsyncArk 资源的显式异步 Facade。""" + + _delegate_native = True + _delegate_client_name = "async_sdk_client" + + async def upload_file(self, file: Any, purpose: str, **kwargs: Any) -> Any: + return await self.provider.async_upload_file(file, purpose, **kwargs) + + async def list_files(self, **kwargs: Any) -> Any: + return await self.provider.async_list_files(**kwargs) + + async def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_file(file_id, **kwargs) + + async def delete_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_file(file_id, **kwargs) + + async def wait_for_file(self, file_id: str, **kwargs: Any) -> Any: + return await self.provider.async_wait_for_file(file_id, **kwargs) + + async def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_retrieve_response(response_id, **kwargs) + + async def create_response(self, request: Any = None, **kwargs: Any) -> Any: + return await self.provider.async_create_response(request, **kwargs) + + async def delete_response(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_response(response_id, **kwargs) + + async def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_response_input_items(response_id, **kwargs) + + async def list_input_items(self, response_id: str, **kwargs: Any) -> Any: + return await self.provider.async_list_input_items(response_id, **kwargs) + + async def create_batch_chat(self, **kwargs: Any) -> Any: + return await self.provider.async_create_batch_chat(**kwargs) + + async def generate_batch_chat(self, **kwargs: Any) -> Any: + return await self.provider.async_generate_batch_chat(**kwargs) + + async def create_batch_embedding(self, **kwargs: Any) -> Any: + return await self.provider.async_create_batch_embedding(**kwargs) + + async def create_batch_multimodal_embedding(self, **kwargs: Any) -> Any: + return await self.provider.async_create_batch_multimodal_embedding(**kwargs) + + async def create_context(self, **kwargs: Any) -> Any: + return await self.provider.async_create_context(**kwargs) + + async def context_complete(self, **kwargs: Any) -> Any: + return await self.provider.async_context_complete(**kwargs) + + async def create_content_generation_task(self, **kwargs: Any) -> Any: + return await self.provider.async_create_content_generation_task(**kwargs) + + async def get_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + return await self.provider.async_get_content_generation_task(task_id, **kwargs) + + async def list_content_generation_tasks(self, **kwargs: Any) -> Any: + return await self.provider.async_list_content_generation_tasks(**kwargs) + + async def delete_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + return await self.provider.async_delete_content_generation_task(task_id, **kwargs) + + async def count_tokens(self, text: str | list[str], **kwargs: Any) -> int: + return await self.provider.async_count_tokens(text, **kwargs) + + async def multimodal_embed(self, inputs: Any, **kwargs: Any) -> Any: + return await self.provider.async_multimodal_embed(inputs, **kwargs) + + async def generate_image(self, **kwargs: Any) -> Any: + return await self.provider.async_generate_image(**kwargs) + + async def beta_chat_parse(self, **kwargs: Any) -> Any: + return await self.provider.async_beta_chat_parse(**kwargs) + + def beta_chat_stream(self, **kwargs: Any) -> Any: + """返回可直接用于 ``async with`` 的 Ark 异步流上下文管理器。""" + return self.provider.async_beta_chat_stream(**kwargs) + + async def bot_chat(self, **kwargs: Any) -> Any: + return await self.provider.async_bot_chat(**kwargs) + + async def classify( + self, query: str, labels: list[str], model: str | None = None, **kwargs: Any + ) -> Any: + return await self.provider.async_classify(query, labels, model=model, **kwargs) diff --git a/src/providers/siliconflow.py b/src/providers/siliconflow.py index d9506c4..67e3d58 100644 --- a/src/providers/siliconflow.py +++ b/src/providers/siliconflow.py @@ -1,4 +1,5 @@ -# -*- coding: utf-8 -*- +from typing import Any + from src.providers.openai_compatible import OpenAICompatibleProvider @@ -8,5 +9,5 @@ class SiliconflowProvider(OpenAICompatibleProvider): 通过继承OpenAICompatibleProvider来复用与OpenAI API兼容的逻辑。 """ - def __init__(self, model_name: str): - super().__init__(model_name=model_name, provider="siliconflow") + def __init__(self, model_name: str, protocol: str = "chat_completions", options: dict[str, Any] | None = None): + super().__init__(model_name=model_name, provider="siliconflow", protocol=protocol, options=options) diff --git a/src/providers/siliconflow_rerank.py b/src/providers/siliconflow_rerank.py index 8842757..3c457b8 100644 --- a/src/providers/siliconflow_rerank.py +++ b/src/providers/siliconflow_rerank.py @@ -1,13 +1,18 @@ -# -*- coding: utf-8 -*- +import math import time -from typing import Any, Dict, List, Tuple, cast +from collections.abc import Mapping +from typing import Any -import requests -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential -from src.providers.__base__.model_provider import RerankModel +from src.providers.__base__.model_provider import ( + RerankModel, + is_retryable_error, + validate_secret_free_options, +) from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import redact_sensitive_text, validate_secret_free_payload logger = get_module_logger(__name__) @@ -17,8 +22,14 @@ class SiliconflowRerankProvider(RerankModel): 已注入 CSE 性能传感器与 tenacity 重试机制。 """ - def __init__(self, model_name: str): + _OPTION_KEYS = frozenset({"return_documents", "truncate", "extra_body", "timeout"}) + + def __init__(self, model_name: str, options: dict[str, Any] | None = None): self._model_name = model_name + self._options = validate_secret_free_options(options, "SiliconFlow Rerank") + unknown = sorted(set(self._options).difference(self._OPTION_KEYS)) + if unknown: + raise ValueError("SiliconFlow Rerank options 包含不支持的字段: " + ", ".join(unknown)) settings = get_settings() self._api_key = settings.siliconflow_api_key self._base_url = settings.siliconflow_base_url @@ -32,36 +43,123 @@ def __init__(self, model_name: str): logger.info(f"初始化 SiliconflowRerankProvider,模型: {model_name}") - def _get_headers(self) -> Dict[str, str]: + def _get_headers(self) -> dict[str, str]: return {"Authorization": f"Bearer {self._api_key}", "Content-Type": "application/json"} - def _prepare_payload(self, query: str, documents: List[str], top_n: int) -> Dict[str, Any]: - return { + def _prepare_payload(self, query: str, documents: list[str], top_n: int) -> dict[str, Any]: + self._validate_inputs(query, documents, top_n) + payload: dict[str, Any] = { "query": query, "documents": documents, "model": self._model_name, "top_n": top_n, "return_documents": True } + options = dict(self._options) + options.pop("timeout", None) + extra_body = validate_secret_free_payload( + options.pop("extra_body", None), + "SiliconFlow Rerank", + "options.extra_body", + ) + overlap = sorted(set(payload).intersection(options)) + if overlap: + raise ValueError( + "SiliconFlow Rerank options 不允许覆盖请求字段: " + ", ".join(overlap) + ) + if extra_body: + option_overlap = sorted(set(options).intersection(extra_body)) + if option_overlap: + raise ValueError( + "SiliconFlow Rerank options 与 options.extra_body 重复: " + + ", ".join(option_overlap) + ) + body_overlap = sorted(set(payload).intersection(extra_body)) + if body_overlap: + raise ValueError( + "SiliconFlow Rerank options.extra_body 不允许覆盖请求字段: " + + ", ".join(body_overlap) + ) + options.update(dict(extra_body)) + payload.update(options) + return payload + + def _request_timeout(self) -> float: + timeout = self._options.get("timeout", 30.0) + if ( + isinstance(timeout, bool) + or not isinstance(timeout, (int, float)) + or not math.isfinite(float(timeout)) + or timeout <= 0 + ): + raise ValueError("SiliconFlow Rerank options.timeout 必须是正数。") + return float(timeout) - def _parse_response(self, results: List[Dict], documents: List[str]) -> Tuple[List[int], List[float]]: - content_to_index = {content: i for i, content in enumerate(documents)} + @staticmethod + def _validate_inputs(query: str, documents: list[str], top_n: int) -> None: + if not isinstance(query, str) or not query.strip(): + raise ValueError("SiliconFlow Rerank query 必须是非空字符串。") + if not isinstance(documents, list) or not documents: + raise ValueError("SiliconFlow Rerank documents 必须是非空列表。") + if any(not isinstance(document, str) for document in documents): + raise ValueError("SiliconFlow Rerank documents 必须全部是字符串。") + if not isinstance(top_n, int) or isinstance(top_n, bool) or top_n < 1: + raise ValueError("SiliconFlow Rerank top_n 必须是大于等于 1 的整数。") + + def _parse_response(self, results: list[dict], documents: list[str]) -> tuple[list[int], list[float]]: + positions: dict[str, list[int]] = {} + for index, content in enumerate(documents): + positions.setdefault(content, []).append(index) indices = [] scores = [] + if not isinstance(results, list): + raise RuntimeError("SiliconFlow Rerank 响应缺少有效的 results 列表。") for res in results: - doc_content = res.get("document", {}).get("text") if isinstance(res.get("document"), dict) else res.get("document") - if doc_content in content_to_index: - indices.append(content_to_index[doc_content]) - scores.append(res.get("relevance_score", 0.0)) + if not isinstance(res, Mapping): + raise RuntimeError("SiliconFlow Rerank 响应包含无效结果项。") + if "index" in res: + direct_index = res["index"] + if ( + not isinstance(direct_index, int) + or isinstance(direct_index, bool) + or not 0 <= direct_index < len(documents) + ): + raise RuntimeError("SiliconFlow Rerank 响应 index 无效。") + if direct_index in indices: + raise RuntimeError("SiliconFlow Rerank 响应包含重复 index。") + index = direct_index + document = res.get("document") + document_text = ( + document.get("text") if isinstance(document, Mapping) else document + ) + if document_text is not None and document_text != documents[index]: + raise RuntimeError("SiliconFlow Rerank 响应 index 与 document 不匹配。") + positions.get(documents[index], []).remove(index) + else: + document = res.get("document") + doc_content = ( + document.get("text") if isinstance(document, Mapping) else document + ) + if not isinstance(doc_content, str) or not positions.get(doc_content): + raise RuntimeError("SiliconFlow Rerank 响应无法映射到输入文档。") + index = positions[doc_content].pop(0) + try: + score = float(res["relevance_score"]) + except (KeyError, TypeError, ValueError) as exc: + raise RuntimeError("SiliconFlow Rerank 响应 relevance_score 无效。") from exc + if not math.isfinite(score): + raise RuntimeError("SiliconFlow Rerank 响应 relevance_score 必须是有限数值。") + indices.append(index) + scores.append(score) return indices, scores @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - def rerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + def rerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """同步 Rerank (CSE Sensor)。""" logger.info(f"调用 SiliconFlow Rerank ({self._model_name}),文档数: {len(documents)}") import httpx @@ -69,30 +167,40 @@ def rerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int url = f"{self._base_url.rstrip('/')}/rerank" try: - with httpx.Client(timeout=30.0) as client: + with httpx.Client(timeout=self._request_timeout()) as client: response = client.post( url, headers=self._get_headers(), json=self._prepare_payload(query, documents, top_n) ) response.raise_for_status() - results = response.json().get("results", []) + payload = response.json() + if not isinstance(payload, Mapping): + raise RuntimeError("SiliconFlow Rerank 响应必须是 JSON 对象。") + results = payload.get("results") + if not isinstance(results, list): + raise RuntimeError("SiliconFlow Rerank 响应缺少有效的 results 列表。") indices, scores = self._parse_response(results, documents) duration = time.perf_counter() - start_time logger.info(f"SiliconFlow Rerank ({self._model_name}) 完成,耗时: {duration:.2f}s") return indices, scores except Exception as e: - logger.error(f"SiliconFlow Rerank ({self._model_name}) 出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "SiliconFlow Rerank (%s) 出错: %s", + self._model_name, + error_text, + ) raise @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), + retry=retry_if_exception(is_retryable_error), reraise=True ) - async def arerank(self, query: str, documents: List[str], top_n: int) -> Tuple[List[int], List[float]]: + async def arerank(self, query: str, documents: list[str], top_n: int) -> tuple[list[int], list[float]]: """异步 Rerank (CSE Sensor)。""" logger.info(f"异步调用 SiliconFlow Rerank ({self._model_name}),文档数: {len(documents)}") import httpx @@ -100,19 +208,29 @@ async def arerank(self, query: str, documents: List[str], top_n: int) -> Tuple[L url = f"{self._base_url.rstrip('/')}/rerank" try: - async with httpx.AsyncClient(timeout=30.0) as aclient: + async with httpx.AsyncClient(timeout=self._request_timeout()) as aclient: response = await aclient.post( url, headers=self._get_headers(), json=self._prepare_payload(query, documents, top_n) ) response.raise_for_status() - results = response.json().get("results", []) + payload = response.json() + if not isinstance(payload, Mapping): + raise RuntimeError("SiliconFlow Rerank 响应必须是 JSON 对象。") + results = payload.get("results") + if not isinstance(results, list): + raise RuntimeError("SiliconFlow Rerank 响应缺少有效的 results 列表。") indices, scores = self._parse_response(results, documents) duration = time.perf_counter() - start_time logger.info(f"SiliconFlow Rerank ({self._model_name}) 异步完成,耗时: {duration:.2f}s") return indices, scores except Exception as e: - logger.error(f"SiliconFlow Rerank ({self._model_name}) 异步出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "SiliconFlow Rerank (%s) 异步出错: %s", + self._model_name, + error_text, + ) raise diff --git a/src/providers/volcengine.py b/src/providers/volcengine.py index 307496d..05702a7 100644 --- a/src/providers/volcengine.py +++ b/src/providers/volcengine.py @@ -1,172 +1,2439 @@ -import time -from typing import Any, AsyncGenerator, Dict, Generator, List, Optional +import inspect +import math +from collections.abc import AsyncGenerator, Collection, Generator, Mapping +from typing import Any -from volcenginesdkarkruntime import Ark, AsyncArk -from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type +from tenacity import retry, retry_if_exception, stop_after_attempt, wait_exponential from src.providers.__base__.model_provider import ( + _UNSET_TEMPERATURE, + CompletionRequest, + CompletionResult, LargeLanguageModel, + StreamEvent, TextEmbeddingModel, + close_resource_sync, + coerce_completion_request, + content_to_text, + field, + is_retryable_error, + merge_tool_call_fragment, + normalize_embedding_vector, + normalize_messages, + normalize_responses_input, + normalize_tool_arguments, + normalize_usage, + raise_for_stream_error_event, + reject_unsupported_kwargs, + retry_async_call, + retry_async_stream, + retry_sync_call, + retry_sync_stream, + validate_complete_tool_call, + validate_secret_free_options, + validate_secret_free_request_overrides, ) +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.resources import ArkResources, AsyncArkResources from src.utils.config import get_settings from src.utils.log_manager import get_module_logger +from src.utils.security import validate_secret_free_payload logger = get_module_logger(__name__) -class VolcengineProvider(LargeLanguageModel, TextEmbeddingModel): - """ - 火山引擎模型提供商 (豆包 Ark SDK)。 - 已注入 CSE 性能传感器与 tenacity 重试机制。 + +def _load_ark_clients() -> tuple[type[Any], type[Any]]: + """按需加载 Ark SDK,避免导入 provider 时扫描整个生成式类型树。""" + try: + from volcenginesdkarkruntime import ( # type: ignore[import-untyped] + Ark, + AsyncArk, + ) + except ImportError as exc: + raise RuntimeError( + "未安装 volcengine-python-sdk[ark],无法使用 VolcengineProvider。" + ) from exc + return Ark, AsyncArk + + + +def _ark_responses_output_text(response: Any) -> str: + """从 Responses 的 ``output[].content[]`` 提取助手正文。 + + Ark 的 ``Response`` 模型没有 ``output_text`` 字段(OpenAI SDK 把它实现为 + 聚合 property),正文只在 ``output`` 项的 ``content`` 里。只取 + ``output_text``/``text`` 类型的内容块,避免把 reasoning 与 refusal 文本 + 混进正文。 """ + chunks: list[str] = [] + for item in field(response, "output", []) or []: + for content in field(item, "content", []) or []: + if field(content, "type") not in {"output_text", "text"}: + continue + piece = field(content, "text") + if isinstance(piece, str): + chunks.append(piece) + return "".join(chunks) + + +class VolcengineProvider(LargeLanguageModel, TextEmbeddingModel): + """火山引擎 Ark Provider,支持 Chat Completions、Responses 和 Embeddings。""" + + _ARK_CHAT_MODEL_OPTION_KEYS = frozenset( + { + "frequency_penalty", "function_call", "logit_bias", "logprobs", + "max_completion_tokens", "max_tokens", "n", "parallel_tool_calls", + "presence_penalty", "reasoning_effort", "repetition_penalty", + "response_format", "service_tier", "stop", "stream_options", + "temperature", "thinking", "tool_choice", "top_logprobs", "top_p", + "user", "extra_body", "top_k", "seed", + } + ) + _ARK_RESPONSES_MODEL_OPTION_KEYS = frozenset( + { + "caching", "context_management", "conversation", "expire_at", + "extra_body", "frequency_penalty", "max_completion_tokens", + "max_output_tokens", "max_tokens", "max_tool_calls", + "parallel_tool_calls", "previous_response_id", "presence_penalty", + "reasoning", "reasoning_effort", "response_format", "service_tier", + "session", "store", "temperature", "text", "thinking", "tool_choice", + "top_k", "top_p", "seed", + } + ) + _ARK_RESPONSES_CREATE_KEYS = frozenset( + { + "input", "model", "instructions", "max_output_tokens", + "parallel_tool_calls", "previous_response_id", "thinking", "store", + "caching", "stream", "temperature", "text", "tool_choice", "tools", + "top_p", "max_tool_calls", "context_management", "expire_at", + "extra_headers", "extra_query", "extra_body", "timeout", "reasoning", + "session", "service_tier", + } + ) + _ARK_RESPONSES_RETRIEVE_KEYS = frozenset( + {"extra_headers", "extra_query", "extra_body", "timeout"} + ) + _ARK_RESPONSES_INPUT_ITEMS_KEYS = frozenset( + { + "after", "before", "include", "limit", "order", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_BATCH_MULTIMODAL_EMBEDDING_KEYS = frozenset( + { + "input", "model", "encoding_format", "dimensions", "instructions", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_BATCH_CHAT_KEYS = frozenset( + { + "messages", "model", "frequency_penalty", "function_call", + "logit_bias", "logprobs", "max_tokens", "presence_penalty", + "stop", "temperature", "tools", "top_logprobs", "top_p", + "repetition_penalty", "n", "parallel_tool_calls", "service_tier", + "tool_choice", "response_format", "thinking", "max_completion_tokens", + "user", "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_BATCH_EMBEDDING_KEYS = frozenset( + { + "input", "model", "encoding_format", "user", "extra_headers", + "extra_query", "extra_body", "timeout", + } + ) + _ARK_BATCH_CHAT_ASYNC_KEYS = _ARK_BATCH_CHAT_KEYS + _ARK_CONTEXT_CREATE_KEYS = frozenset( + { + "model", "messages", "ttl", "mode", "truncation_strategy", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_CONTEXT_COMPLETION_KEYS = frozenset( + { + "context_id", "messages", "model", "frequency_penalty", + "function_call", "logit_bias", "logprobs", "max_tokens", + "presence_penalty", "stop", "stream", "stream_options", + "temperature", "tools", "top_logprobs", "top_p", + "repetition_penalty", "n", "tool_choice", "response_format", + "user", "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_CLASSIFICATION_KEYS = frozenset( + {"query", "model", "labels", "user", "extra_headers", "extra_query", "extra_body", "timeout"} + ) + _ARK_CONTENT_GENERATION_CREATE_KEYS = frozenset( + { + "model", "content", "safety_identifier", "callback_url", + "return_last_frame", "service_tier", "execution_expires_after", + "priority", "generate_audio", "draft", "camera_fixed", "watermark", + "seed", "resolution", "ratio", "duration", "frames", "tools", + "output_format", "omni_reference_task_type", "extra_headers", + "extra_query", "extra_body", "timeout", + } + ) + _ARK_CONTENT_GENERATION_LIST_KEYS = frozenset( + { + "page_num", "page_size", "status", "task_ids", "model", "service_tier", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_FILE_CREATE_KEYS = frozenset( + { + "expire_at", "preprocess_configs", "url", "tos", "extra_headers", + "extra_query", "extra_body", "timeout", + } + ) + _ARK_FILE_LIST_KEYS = frozenset( + { + "after", "limit", "order", "purpose", "extra_headers", "extra_query", + "extra_body", "timeout", + } + ) + _ARK_IMAGE_GENERATE_KEYS = frozenset( + { + "model", "prompt", "image", "response_format", "size", "seed", + "guidance_scale", "watermark", "optimize_prompt", + "optimize_prompt_options", "extra_headers", "extra_query", "extra_body", + "timeout", "sequential_image_generation", + "sequential_image_generation_options", "tools", "output_format", + "layer_decomposition", "stream", + } + ) + _ARK_BETA_CHAT_KEYS = frozenset( + { + "messages", "model", "response_format", "frequency_penalty", "logit_bias", + "logprobs", "max_tokens", "n", "parallel_tool_calls", "presence_penalty", + "service_tier", "stop", "stream_options", "temperature", "tool_choice", + "tools", "top_logprobs", "top_p", "user", "reasoning_effort", + "extra_headers", "extra_query", "extra_body", "timeout", + } + ) + _ARK_BOT_CHAT_KEYS = frozenset( + { + "messages", "model", "frequency_penalty", "function_call", "logit_bias", + "logprobs", "max_tokens", "presence_penalty", "stop", "stream", + "stream_options", "temperature", "tools", "top_logprobs", "top_p", + "repetition_penalty", "n", "parallel_tool_calls", "service_tier", + "tool_choice", "response_format", "user", "metadata", "extra_headers", + "extra_query", "extra_body", "timeout", + } + ) + + capabilities = frozenset( + { + "chat", "stream", "messages", "multimodal", "tools", "structured_output", + "usage", "embedding", "responses", "files", "batches", "token_count", + "images", "multimodal_embedding", "context", "content_generation", + "beta_chat", "bot_chat", "classification", + } + ) - def __init__(self, model_name: str): + def __init__(self, model_name: str, protocol: str = "ark", options: dict[str, Any] | None = None): + normalized = str(protocol).strip().lower().replace("-", "_") + if normalized in {"ark", "chat", "chat_completion", "chat_completions"}: + self._protocol = "chat_completions" + elif normalized in {"response", "responses"}: + self._protocol = "responses" + else: + raise ValueError("Volcengine 仅支持 ark/chat_completions 或 responses 协议。") self._model_name = model_name + self._options = validate_secret_free_options(options, "Volcengine") + self._options = self._normalize_client_options(self._options) + self._server_verified_protocols = self._normalize_verified_protocols( + self._options.get("server_verified_protocols", ()) + ) settings = get_settings() - self._api_key = settings.volc_access_key # 映射为 ak/sk 或直接使用 api_key (Ark 支持 API Key) - # 注意:Ark SDK 推荐使用 API Key 模式,对应 volc_access_key - if not self._api_key: - logger.error("火山引擎 API Key (VOLC_ACCESS_KEY) 未设置。") - raise ValueError("VOLC_ACCESS_KEY is required for VolcengineProvider") - - self._client: Optional[Ark] = None - self._aclient: Optional[AsyncArk] = None - logger.info(f"初始化 VolcengineProvider (Ark),模型: {model_name}") - - def _get_client(self) -> Ark: + self._api_key = settings.volc_access_key + self._secret_key = getattr(settings, "volc_secret_key", None) + self._ark_api_key = getattr(settings, "ark_api_key", None) + self._base_url = getattr(settings, "volc_base_url", None) + has_ak_sk = bool(self._api_key and self._secret_key) + has_partial_ak_sk = bool(self._api_key or self._secret_key) + if not has_ak_sk and not self._ark_api_key: + if has_partial_ak_sk: + raise ValueError( + "VolcengineProvider 需要同时配置 VOLC_ACCESS_KEY 和 " + "VOLC_SECRET_KEY,或改用 ARK_API_KEY。" + ) + raise ValueError( + "VolcengineProvider 需要配置 ARK_API_KEY,或同时配置 " + "VOLC_ACCESS_KEY 和 VOLC_SECRET_KEY。" + ) + self._client: Any | None = None + self._aclient: Any | None = None + + @property + def resources(self) -> ArkResources: + return ArkResources(self) + + @property + def async_resources(self) -> AsyncArkResources: + return AsyncArkResources(self) + + def _client_options(self) -> dict[str, Any]: + options = self._normalize_client_options( + getattr(self, "_options", {}) or {} + ) + result: dict[str, Any] = {"base_url": self._base_url} if self._base_url else {} + if self._api_key and self._secret_key: + result.update({"ak": self._api_key, "sk": self._secret_key}) + elif getattr(self, "_ark_api_key", None): + result["api_key"] = self._ark_api_key + else: + raise ValueError( + "VolcengineProvider 认证配置不完整:需要 ARK_API_KEY,或同时提供 " + "VOLC_ACCESS_KEY 和 VOLC_SECRET_KEY。" + ) + for key in ("timeout", "max_retries", "region"): + if key in options: + result[key] = options[key] + return result + + @staticmethod + def _normalize_client_options(options: Mapping[str, Any]) -> dict[str, Any]: + """规范化 Ark 客户端级 timeout、重试次数和 region 参数。""" + normalized = dict(options) + if "timeout" in normalized: + value = normalized["timeout"] + if isinstance(value, bool): + raise ValueError("Volcengine options.timeout 必须是正数。") + if isinstance(value, str): + try: + value = float(value.strip()) + except ValueError as exc: + raise ValueError("Volcengine options.timeout 必须是正数。") from exc + if not isinstance(value, (int, float)) or not math.isfinite(float(value)): + # httpx.Timeout is accepted by the Ark SDK but cannot be + # represented in TOML. Preserve such explicit SDK objects. + if not hasattr(value, "connect"): + raise ValueError("Volcengine options.timeout 必须是正数或 httpx.Timeout。") + elif float(value) <= 0: + raise ValueError("Volcengine options.timeout 必须是正数。") + else: + normalized["timeout"] = float(value) + + if "max_retries" in normalized: + value = normalized["max_retries"] + if isinstance(value, bool): + raise ValueError("Volcengine options.max_retries 必须是大于等于 0 的整数。") + if isinstance(value, str): + text = value.strip() + try: + parsed = int(text) + except ValueError as exc: + raise ValueError( + "Volcengine options.max_retries 必须是大于等于 0 的整数。" + ) from exc + if text not in {str(parsed), f"+{parsed}"}: + raise ValueError( + "Volcengine options.max_retries 必须是大于等于 0 的整数。" + ) + value = parsed + elif isinstance(value, float) and math.isfinite(value) and value.is_integer(): + value = int(value) + if not isinstance(value, int) or value < 0: + raise ValueError("Volcengine options.max_retries 必须是大于等于 0 的整数。") + normalized["max_retries"] = value + + if "region" in normalized: + value = normalized["region"] + if not isinstance(value, str) or not value.strip(): + raise ValueError("Volcengine options.region 必须是非空字符串。") + normalized["region"] = value.strip() + return normalized + + @classmethod + def _normalize_verified_protocols(cls, configured: Any) -> frozenset[str]: + """规范化 Ark 渠道的服务端协议登记。""" + if configured is None: + return frozenset() + if isinstance(configured, str): + configured = (configured,) + elif not isinstance(configured, (list, tuple, set, frozenset)): + raise ValueError("server_verified_protocols 必须是字符串或协议序列。") + aliases = { + "chat": "chat_completions", + "completion": "chat_completions", + "chat_completion": "chat_completions", + "response": "responses", + } + supported = {"ark", "chat_completions", "responses"} + normalized: set[str] = set() + for value in configured: + if not isinstance(value, str) or not value.strip(): + raise ValueError( + "server_verified_protocols 中的协议必须是非空字符串。" + ) + protocol = value.strip().lower().replace("-", "_") + protocol = aliases.get(protocol, protocol) + if protocol not in supported: + raise ValueError( + f"Volcengine 不支持登记服务端协议: {value}。" + "可选值: ark, chat_completions, responses" + ) + normalized.add(protocol) + return frozenset(normalized) + + def _require_provider_resource(self, method_name: str) -> None: + """在原生资源方法触达 SDK 前校验 Ark 渠道能力。 + + ``method_name`` 可能是显式方法名(``create_response``),也可能是动态 + 资源树路径(``volcengine.responses.create``)。按路径分段判断,避免把 + ``files.create`` 这类同名但无关的调用一并拦下。 + """ + segments = method_name.split(".") + if "responses" in segments or method_name.endswith("_response") or "response" in segments: + self._require_responses_resource(method_name) + + def _require_responses_resource(self, operation: str) -> None: + """阻止未验证的 Ark Responses 请求到达远端。""" + if self._protocol != "responses": + raise ValueError(f"Ark {operation} 仅适用于 responses 协议。") + verified = getattr(self, "_server_verified_protocols", None) + if verified is None: + options = getattr(self, "_options", {}) or {} + verified = self._normalize_verified_protocols( + options.get("server_verified_protocols", ()) + ) + if "responses" not in verified: + raise ValueError( + f"Ark 未验证 Responses {operation} 的服务端能力;" + "请在 provider options 中显式配置 " + "server_verified_protocols=['responses']。" + ) + + def _get_client(self) -> Any: if self._client is None: - self._client = Ark(api_key=self._api_key) + ark_class, _ = _load_ark_clients() + self._client = ark_class(**self._client_options()) return self._client - def _get_aclient(self) -> AsyncArk: + def _get_aclient(self) -> Any: if self._aclient is None: - self._aclient = AsyncArk(api_key=self._api_key) + _, async_ark_class = _load_ark_clients() + self._aclient = async_ark_class(**self._client_options()) return self._aclient + @staticmethod + async def _resolve_async_result(result: Any) -> Any: + """兼容 Ark 异步客户端的协程和同步分页器返回值。""" + if inspect.isawaitable(result): + return await result + return result + + @staticmethod + def _merge_options(request: dict[str, Any], options: dict[str, Any]) -> None: + reserved = {"model", "messages", "input", "stream", "instructions", "tools"} + overlap = sorted(reserved.intersection(options)) + if overlap: + raise ValueError(f"Provider options 不允许覆盖请求字段: {', '.join(overlap)}") + for key, value in options.items(): + request.setdefault(key, value) + + @staticmethod + def _validate_model_options( + options: Mapping[str, Any], + allowed: frozenset[str], + endpoint: str, + ) -> None: + unknown = {key: value for key, value in options.items() if key not in allowed} + reject_unsupported_kwargs(endpoint, unknown) + + def _request_options(self) -> dict[str, Any]: + """返回不会误传给 Ark 请求的模型级选项。""" + validate_secret_free_options(getattr(self, "_options", {}) or {}, "Volcengine") + return { + key: value + for key, value in self._options.items() + if key not in { + "timeout", "max_retries", "region", + "server_verified_protocols", + } + } + + @staticmethod + def _validated_extra_body( + value: Any, + endpoint: str, + reserved: set[str], + ) -> dict[str, Any]: + if value is None: + return {} + if not isinstance(value, Mapping): + raise ValueError(f"{endpoint} extra_body 必须是对象。") + value = validate_secret_free_payload(value, endpoint, "extra_body") + overlap = sorted(set(value).intersection(reserved)) + if overlap: + raise ValueError( + f"{endpoint} extra_body 不允许覆盖请求字段: {', '.join(overlap)}" + ) + return dict(value) + + @staticmethod + def _merge_extra_body( + configured: Mapping[str, Any] | None, + extensions: Mapping[str, Any] | None, + endpoint: str, + ) -> dict[str, Any]: + """合并模型级扩展,拒绝同一字段的静默覆盖。""" + configured_values = dict(configured or {}) + extension_values = dict(extensions or {}) + overlap = sorted(set(configured_values).intersection(extension_values)) + if overlap: + raise ValueError( + f"{endpoint} 模型 options 的扩展字段重复: {', '.join(overlap)}" + ) + return {**configured_values, **extension_values} + + @staticmethod + def _convert_responses_format(response_format: dict[str, Any]) -> dict[str, Any]: + if response_format.get("type") == "json_schema": + schema = response_format.get("json_schema", response_format) + if not isinstance(schema, dict): + raise ValueError("Ark Responses 的 json_schema 必须是对象。") + return { + "format": { + "type": "json_schema", + "json_schema": { + "name": schema.get("name", "response"), + "schema": schema.get("schema", response_format.get("schema", {})), + "strict": schema.get("strict", True), + }, + } + } + if response_format.get("type") == "json_object": + return {"format": {"type": "json_object"}} + if "format" in response_format: + return response_format + raise ValueError("Ark Responses 的 response_format 仅支持 json_schema 或 json_object。") + + @staticmethod + def _convert_responses_tools(tools: list[dict[str, Any]] | None) -> list[dict[str, Any]] | None: + """将统一的 Chat 工具 schema 转换为 Ark Responses 工具 schema。""" + if not tools: + return None + converted: list[dict[str, Any]] = [] + for index, tool in enumerate(tools): + if not isinstance(tool, Mapping): + raise ValueError(f"Ark Responses 工具定义[{index}] 必须是对象。") + if "function" not in tool: + native_tool = dict(tool) + tool_type = native_tool.get("type") + if not isinstance(tool_type, str) or not tool_type.strip(): + raise ValueError( + f"Ark Responses 工具定义[{index}] 缺少有效 type。" + ) + if tool_type == "function": + name = native_tool.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError( + f"Ark Responses 工具定义[{index}] 缺少 name。" + ) + parameters = native_tool.get( + "parameters", {"type": "object", "properties": {}} + ) + if not isinstance(parameters, Mapping): + raise ValueError( + f"Ark Responses 工具定义[{index}].parameters 必须是对象。" + ) + native_tool["parameters"] = dict(parameters) + converted.append(native_tool) + continue + function = tool["function"] + if not isinstance(function, Mapping): + raise ValueError( + f"Ark Responses 工具定义[{index}].function 必须是对象。" + ) + name = function.get("name") + if not isinstance(name, str) or not name.strip(): + raise ValueError("Ark Responses 工具定义缺少 function.name。") + parameters = function.get( + "parameters", {"type": "object", "properties": {}} + ) + if not isinstance(parameters, Mapping): + raise ValueError( + f"Ark Responses 工具定义[{index}].function.parameters 必须是对象。" + ) + item: dict[str, Any] = { + "type": "function", + "name": name, + "parameters": dict(parameters), + } + if function.get("description") is not None: + item["description"] = function["description"] + if "strict" in function or "strict" in tool: + item["strict"] = function.get("strict", tool.get("strict")) + converted.append(item) + return converted + + @staticmethod + def _convert_responses_tool_choice(tool_choice: Any) -> Any: + """将 OpenAI 风格的工具选择转换为 Ark Responses schema。""" + if tool_choice is None: + return None + if isinstance(tool_choice, str): + aliases = {"required": "required", "auto": "auto", "none": "none"} + normalized = tool_choice.lower() + if normalized in aliases: + return aliases[normalized] + raise ValueError(f"Ark Responses 不支持 tool_choice: {tool_choice}") + if isinstance(tool_choice, dict): + if tool_choice.get("type") == "function": + function = tool_choice.get("function", tool_choice) + if not isinstance(function, Mapping): + raise ValueError("Ark Responses 的 function tool_choice 必须是对象。") + name = function.get("name") + if not name: + raise ValueError("Ark Responses 的 function tool_choice 缺少 name。") + return {"type": "function", "name": name} + if tool_choice.get("type") in { + "mcp", "web_search", "knowledge_search" + }: + return dict(tool_choice) + raise ValueError("Ark Responses 的 tool_choice 必须是 auto、none、required 或工具对象。") + + def _build_chat_request(self, prompt: str | None = None, system_prompt: str | None = None, + tools: list[dict[str, Any]] | None = None, temperature: float | None = None, + stream: bool = True, *, messages: Any = None, max_tokens: int | None = None, + top_p: float | None = None, top_k: int | None = None, seed: int | None = None, + stop: str | list[str] | None = None, + response_format: dict[str, Any] | None = None, tool_choice: Any = None, + extra_body: dict[str, Any] | None = None, user: str | None = None, + extra_headers: dict[str, str] | None = None, extra_query: dict[str, Any] | None = None, + frequency_penalty: float | None = None, presence_penalty: float | None = None, + repetition_penalty: float | None = None, n: int | None = None, + logit_bias: dict[str, int] | None = None, logprobs: bool | None = None, + top_logprobs: int | None = None, parallel_tool_calls: bool | None = None, + service_tier: str | None = None, thinking: dict[str, Any] | None = None, + reasoning: dict[str, Any] | None = None, stream_options: dict[str, Any] | None = None, + timeout: float | None = None, **ignored: Any) -> dict[str, Any]: + reject_unsupported_kwargs("Ark Chat Completions", ignored) + extra_headers, extra_query = validate_secret_free_request_overrides( + extra_headers, + extra_query, + "Ark Chat Completions", + ) + request: dict[str, Any] = { + "model": self._model_name, + # Ark Chat Completions 同样要求 assistant 历史的 tool_call 带 id。 + "messages": normalize_messages( + prompt, + system_prompt, + messages, + require_tool_call_ids=True, + ), + "stream": stream, + } + configured_options = self._request_options() + self._validate_model_options( + configured_options, + self._ARK_CHAT_MODEL_OPTION_KEYS, + "Ark Chat Completions", + ) + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Ark Chat Completions", + {"model", "messages", "input", "stream"}, + ) + configured_max_tokens = configured_options.pop("max_tokens", None) + configured_max_completion_tokens = configured_options.pop("max_completion_tokens", None) + if configured_max_tokens is not None and configured_max_completion_tokens is not None: + raise ValueError( + "Ark Chat Completions options 不能同时设置 max_tokens 和 " + "max_completion_tokens。" + ) + configured_top_k = configured_options.pop("top_k", None) + configured_seed = configured_options.pop("seed", None) + configured_scalar_extra_keys: set[str] = set() + if configured_top_k is not None: + if "top_k" in configured_extra_body: + raise ValueError( + "Ark Chat Completions 模型 options 的 top_k 与 extra_body 重复。" + ) + configured_extra_body["top_k"] = configured_top_k + configured_scalar_extra_keys.add("top_k") + if configured_seed is not None: + if "seed" in configured_extra_body: + raise ValueError( + "Ark Chat Completions 模型 options 的 seed 与 extra_body 重复。" + ) + configured_extra_body["seed"] = configured_seed + configured_scalar_extra_keys.add("seed") + self._merge_options(request, configured_options) + if temperature is None and "temperature" not in request: + request["temperature"] = 0.7 + if max_tokens is None: + if configured_max_tokens is not None: + request["max_tokens"] = configured_max_tokens + elif configured_max_completion_tokens is not None: + request["max_completion_tokens"] = configured_max_completion_tokens + if configured_extra_body: + request["extra_body"] = dict(configured_extra_body) + if temperature is not None: + request["temperature"] = temperature + if tools: + request["tools"] = tools + if max_tokens is not None: + request["max_tokens"] = max_tokens + if top_p is not None: + request["top_p"] = top_p + for key, value in { + "frequency_penalty": frequency_penalty, + "presence_penalty": presence_penalty, + "repetition_penalty": repetition_penalty, + "n": n, + "logit_bias": logit_bias, + "logprobs": logprobs, + "top_logprobs": top_logprobs, + "parallel_tool_calls": parallel_tool_calls, + "service_tier": service_tier, + "thinking": thinking, + "reasoning_effort": (reasoning or {}).get("effort") if isinstance(reasoning, dict) else None, + "stream_options": stream_options, + "timeout": timeout, + "extra_headers": extra_headers, + "extra_query": extra_query, + }.items(): + if value is not None: + request[key] = value + if top_k is not None: + if "top_k" in configured_extra_body and "top_k" not in configured_scalar_extra_keys: + raise ValueError( + "Ark Chat Completions top_k 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["top_k"] = top_k + if seed is not None: + if "seed" in configured_extra_body and "seed" not in configured_scalar_extra_keys: + raise ValueError( + "Ark Chat Completions seed 与模型 options.extra_body 重复。" + ) + request.setdefault("extra_body", {})["seed"] = seed + if stop is not None: + request["stop"] = stop + if response_format is not None: + request["response_format"] = response_format + if tool_choice is not None: + request["tool_choice"] = tool_choice + if user is not None: + request["user"] = user + request_extra_body = self._validated_extra_body( + extra_body, + "Ark Chat Completions", + set(request).difference({"extra_body"}), + ) + if request_extra_body: + configured_overlap = sorted( + set(request.get("extra_body", {})).intersection(request_extra_body) + ) + if configured_overlap: + raise ValueError( + "Ark Chat Completions extra_body 与模型 options 重复: " + + ", ".join(configured_overlap) + ) + request.setdefault("extra_body", {}).update(request_extra_body) + if request.get("extra_body"): + request["extra_body"] = self._validated_extra_body( + request["extra_body"], + "Ark Chat Completions", + set(request).difference({"extra_body"}), + ) + return request + + def _build_responses_request(self, request: CompletionRequest) -> dict[str, Any]: + validate_secret_free_request_overrides( + getattr(request, "extra_headers", None), + getattr(request, "extra_query", None), + "Ark Responses", + ) + unsupported_fields = ( + "repetition_penalty", "n", "logit_bias", "logprobs", "modalities", "audio", + "prediction", "web_search_options", "stop", "include", "background", + "metadata", "user", "moderation", "prompt_cache_options", "top_logprobs", + "safety_identifier", "prompt_cache_key", "prompt_cache_retention", + "truncation", "stream_options", "cache_control", "container", + "inference_geo", "mcp_servers", "output_config", "output_format", + "speed", "betas", "diagnostics", "fallback_credit_token", "fallbacks", + ) + reject_unsupported_kwargs( + "Ark Responses", + { + key: getattr(request, key) + for key in unsupported_fields + if getattr(request, key, None) is not None + }, + ) + normalized_input = normalize_responses_input( + request.prompt, + request.system_prompt, + request.messages, + provider="ark", + ) + params: dict[str, Any] = { + "model": self._model_name, + "input": normalized_input, + "stream": request.stream, + } + configured_options = self._request_options() + self._validate_model_options( + configured_options, + self._ARK_RESPONSES_MODEL_OPTION_KEYS, + "Ark Responses", + ) + configured_max_tokens = configured_options.pop("max_tokens", None) + configured_max_completion_tokens = configured_options.pop("max_completion_tokens", None) + configured_max_output_tokens = configured_options.pop("max_output_tokens", None) + configured_limits = { + key: value + for key, value in { + "max_output_tokens": configured_max_output_tokens, + "max_tokens": configured_max_tokens, + "max_completion_tokens": configured_max_completion_tokens, + }.items() + if value is not None + } + if len(configured_limits) > 1: + raise ValueError( + "Ark Responses options 不能同时设置多个输出长度字段: " + + ", ".join(configured_limits) + ) + configured_response_format = configured_options.pop("response_format", None) + configured_reasoning_effort = configured_options.pop("reasoning_effort", None) + configured_frequency_penalty = configured_options.pop("frequency_penalty", None) + configured_presence_penalty = configured_options.pop("presence_penalty", None) + configured_tool_choice = configured_options.pop("tool_choice", None) + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Ark Responses", + {"model", "messages", "input", "stream", "instructions", "tools"}, + ) + configured_top_k = configured_options.pop("top_k", None) + configured_seed = configured_options.pop("seed", None) + configured_conversation = configured_options.pop("conversation", None) + configured_session = configured_options.pop("session", None) + if configured_conversation is not None and configured_session is not None: + raise ValueError( + "Ark Responses 模型 options 不能同时设置 conversation 和 session。" + ) + configured_scalar_extra_keys: set[str] = set() + if configured_top_k is not None: + if "top_k" in configured_extra_body: + raise ValueError( + "Ark Responses 模型 options 的 top_k 与 extra_body 重复。" + ) + configured_extra_body["top_k"] = configured_top_k + configured_scalar_extra_keys.add("top_k") + if configured_seed is not None: + if "seed" in configured_extra_body: + raise ValueError( + "Ark Responses 模型 options 的 seed 与 extra_body 重复。" + ) + configured_extra_body["seed"] = configured_seed + configured_scalar_extra_keys.add("seed") + if request.system_prompt and request.messages is None: + params["instructions"] = request.system_prompt + if getattr(request, "_temperature_explicit", True) and request.temperature is not None: + params["temperature"] = request.temperature + if request.max_tokens is not None: + params["max_output_tokens"] = request.max_tokens + elif configured_limits: + params["max_output_tokens"] = next(iter(configured_limits.values())) + if request.top_p is not None: + params["top_p"] = request.top_p + sampling_extensions = { + "frequency_penalty": ( + request.frequency_penalty + if request.frequency_penalty is not None + else configured_frequency_penalty + ), + "presence_penalty": ( + request.presence_penalty + if request.presence_penalty is not None + else configured_presence_penalty + ), + } + for key, value in sampling_extensions.items(): + if value is None: + continue + if key in configured_extra_body: + raise ValueError(f"Ark Responses {key} 与模型 options.extra_body 重复。") + configured_extra_body[key] = value + reject_unsupported_kwargs("Ark Responses", {"top_logprobs": request.top_logprobs}) + if request.top_k is not None: + if "top_k" in configured_extra_body and "top_k" not in configured_scalar_extra_keys: + raise ValueError( + "Ark Responses top_k 与模型 options.extra_body 重复。" + ) + configured_extra_body["top_k"] = request.top_k + if request.seed is not None: + if "seed" in configured_extra_body and "seed" not in configured_scalar_extra_keys: + raise ValueError( + "Ark Responses seed 与模型 options.extra_body 重复。" + ) + configured_extra_body["seed"] = request.seed + converted_tools = self._convert_responses_tools(request.tools) + if converted_tools: + params["tools"] = converted_tools + effective_response_format = request.response_format or configured_response_format + if effective_response_format: + params["text"] = self._convert_responses_format(effective_response_format) + if request.tool_choice is not None: + params["tool_choice"] = self._convert_responses_tool_choice(request.tool_choice) + elif configured_tool_choice is not None: + params["tool_choice"] = self._convert_responses_tool_choice( + configured_tool_choice + ) + for key in ("previous_response_id", "reasoning", "thinking", "store", "caching", "parallel_tool_calls", "max_tool_calls", "context_management", "expire_at", "service_tier"): + value = getattr(request, key, None) + if value is not None: + params[key] = value + if request.reasoning is None and configured_reasoning_effort is not None: + params["reasoning"] = {"effort": configured_reasoning_effort} + if request.conversation is not None and request.session is not None: + raise ValueError( + "Ark Responses 不能同时传 conversation 和 session。" + ) + # Ark calls the response conversation state a session. Keep + # `conversation` as the portable request field for OpenAI-compatible + # channels while mapping it to the SDK's actual parameter here. + request_session = request.session + request_conversation = request.conversation + if request_session is not None: + if not isinstance(request_session, Mapping): + raise ValueError("Ark Responses session 必须是对象。") + params["session"] = dict(request_session) + elif request_conversation is not None: + if not isinstance(request_conversation, Mapping): + raise ValueError("Ark Responses conversation 必须是对象。") + params["session"] = dict(request_conversation) + elif configured_session is not None: + if not isinstance(configured_session, Mapping): + raise ValueError("Ark Responses 模型 options.session 必须是对象。") + params["session"] = dict(configured_session) + elif configured_conversation is not None: + if not isinstance(configured_conversation, Mapping): + raise ValueError("Ark Responses 模型 options.conversation 必须是对象。") + params["session"] = dict(configured_conversation) + if request.timeout is not None: + params["timeout"] = request.timeout + if request.extra_headers is not None: + params["extra_headers"] = request.extra_headers + if request.extra_query is not None: + params["extra_query"] = request.extra_query + self._merge_options(params, configured_options) + if configured_extra_body: + params["extra_body"] = dict(configured_extra_body) + request_extra_body = self._validated_extra_body( + request.extra_body, + "Ark Responses", + set(params).difference({"extra_body"}), + ) + if request_extra_body: + configured_overlap = sorted( + set(params.get("extra_body", {})).intersection(request_extra_body) + ) + if configured_overlap: + raise ValueError( + "Ark Responses extra_body 与模型 options 重复: " + + ", ".join(configured_overlap) + ) + params.setdefault("extra_body", {}).update(request_extra_body) + if params.get("extra_body"): + params["extra_body"] = self._validated_extra_body( + params["extra_body"], + "Ark Responses", + set(params).difference({"extra_body"}), + ) + self._reject_instructions_with_enabled_caching(params) + return params + + @staticmethod + def _reject_instructions_with_enabled_caching(params: Mapping[str, Any]) -> None: + """拒绝 ``instructions`` 与 ``caching={"type": "enabled"}`` 同时出现。 + + 官方文档明确二者互斥:配置 ``instructions`` 后本轮请求无法写入或使用 + 缓存,``caching`` 为 ``enabled`` 时服务端直接报错。SDK 不做本地校验, + 会原样发到服务端,因此在构造阶段显式拒绝,避免用户从远端 400 反推。 + + ``caching`` 有两条来源:顶层参数,以及 ``extra_body``(Ark SDK 在 + ``_base_client`` 里把 ``extra_body`` 合并进请求体,服务端看到的仍是 + ``caching=enabled``)。两条都要检查,否则该守卫可被绕过。 + """ + if params.get("instructions") is None: + return + candidates = [params.get("caching")] + extra_body = params.get("extra_body") + if isinstance(extra_body, Mapping): + candidates.append(extra_body.get("caching")) + for candidate in candidates: + if isinstance(candidate, Mapping) and candidate.get("type") == "enabled": + raise ValueError( + "Ark Responses 的 instructions 与 caching={\"type\": \"enabled\"} 互斥:" + "官方规定配置 instructions 后本轮请求无法写入或使用缓存,caching 为 " + "enabled 时请求会直接报错。instructions 来自 system_prompt," + "注意 CompletionRequest.system_prompt 有兼容默认值" + "(未显式传入时为 \"You are a helpful assistant.\");" + "请显式传入 system_prompt=None 并改用 messages 携带系统提示," + "或移除 caching。" + ) + + @classmethod + def _validate_native_response_kwargs(cls, kwargs: Mapping[str, Any]) -> dict[str, Any]: + """校验 Ark SDK 原生 Responses 参数,不改变其必填字段语义。""" + params = dict(kwargs) + unsupported = { + key: value + for key, value in params.items() + if key not in cls._ARK_RESPONSES_CREATE_KEYS + } + reject_unsupported_kwargs("Ark Responses", unsupported) + if "model" not in params or not isinstance(params.get("model"), str) or not params["model"].strip(): + raise ValueError("Ark Responses 原生调用必须显式提供 model。") + if "input" not in params or params.get("input") is None: + raise ValueError("Ark Responses 创建需要 input。") + headers, query = validate_secret_free_request_overrides( + params.get("extra_headers"), + params.get("extra_query"), + "Ark Responses", + ) + if "extra_headers" in params: + params["extra_headers"] = headers + if "extra_query" in params: + params["extra_query"] = query + if "extra_body" in params and params["extra_body"] is not None: + params["extra_body"] = validate_secret_free_payload( + params["extra_body"], "Ark Responses", "extra_body" + ) + if "session" in params and params["session"] is not None: + if not isinstance(params["session"], Mapping): + raise ValueError("Ark Responses session 必须是对象。") + params["session"] = dict(params["session"]) + # 原生入口同样要执行 instructions 与 caching 的互斥校验,否则用户 + # 绕过 Facade 直接调用 create_response/async_create_response 时会从 + # 远端 400 才得知参数冲突。 + cls._reject_instructions_with_enabled_caching(params) + return params + + def _embedding_options(self) -> dict[str, Any]: + """返回 Ark Embeddings 接受的模型级参数,排除聊天选项。""" + supported = {"dimensions", "encoding_format", "user", "extra_body"} + return {key: value for key, value in self._options.items() if key in supported} + + @staticmethod + def _validate_resource_kwargs( + resource: str, + kwargs: dict[str, Any], + supported: Collection[str], + ) -> None: + validate_secret_free_request_overrides( + kwargs.get("extra_headers"), + kwargs.get("extra_query"), + f"Ark {resource}", + ) + if "extra_body" in kwargs: + validate_secret_free_payload( + kwargs.get("extra_body"), + f"Ark {resource}", + "extra_body", + ) + unsupported = sorted(key for key in kwargs if key not in supported) + if unsupported: + raise ValueError(f"Ark {resource} 不支持请求参数: {', '.join(unsupported)}") + + @staticmethod + def _validate_file_wait_args( + file_id: str, + poll_interval: float | None, + max_wait_seconds: float | None, + ) -> tuple[str, float | None, float | None]: + """校验 Ark 文件轮询参数,避免无界等待或底层类型错误。""" + file_id = VolcengineProvider._require_non_empty_string( + file_id, + "Ark 文件等待的 file_id", + ) + + def positive_finite(value: float | None, name: str) -> float | None: + if value is None: + return None + if ( + isinstance(value, bool) + or not isinstance(value, (int, float)) + or not math.isfinite(float(value)) + or float(value) <= 0 + ): + raise ValueError(f"Ark 文件等待的 {name} 必须是有限正数。") + return float(value) + + return ( + file_id, + positive_finite(poll_interval, "poll_interval"), + positive_finite(max_wait_seconds, "max_wait_seconds"), + ) + + @staticmethod + def _require_non_empty_string(value: Any, label: str) -> str: + """校验 SDK 资源 ID、模型名等字符串参数。""" + if not isinstance(value, str) or not value.strip(): + raise ValueError(f"{label} 必须是非空字符串。") + return value.strip() + + @classmethod + def _prepare_batch_kwargs( + cls, + kwargs: dict[str, Any], + model_name: str, + resource: str, + supported: frozenset[str], + required: tuple[str, ...], + ) -> dict[str, Any]: + params = dict(kwargs) + if params.get("model") is None: + params["model"] = model_name + missing = [ + name + for name in required + if name not in params or params[name] is None + ] + if missing: + raise ValueError( + f"Ark {resource} 缺少必填参数: {', '.join(missing)}" + ) + model = params.get("model") + if not isinstance(model, str) or not model.strip(): + raise ValueError(f"Ark {resource} 的 model 必须是非空字符串。") + cls._validate_resource_kwargs(resource, params, supported) + return params + + @staticmethod + def _extract_result(response: Any) -> CompletionResult: + choices = field(response, "choices", []) or [] + text = "" + calls: list[dict[str, Any]] = [] + if choices: + message = field(choices[0], "message") + text = content_to_text(field(message, "content", "")) + for call in field(message, "tool_calls", []) or []: + fn = field(call, "function") + calls.append({ + "id": field(call, "id"), + "name": field(fn, "name"), + "arguments": normalize_tool_arguments(field(fn, "arguments", "")), + }) + output_text = field(response, "output_text") + if isinstance(output_text, str): + text = output_text + if not text: + # Ark 的 Response 没有 OpenAI 那样的 ``output_text`` 聚合属性, + # 正文位于 ``output[].content[].text``。缺了这一步,所有非流式 + # 入口都会返回空文本而不报错。仅在没有 choices 文本时启用,避免 + # 覆盖 Chat Completions 分支已经取到的内容。 + text = _ark_responses_output_text(response) + for item in field(response, "output", []) or []: + item_type = field(item, "type") + if item_type in {"function_call", "custom_tool_call"}: + calls.append( + { + "id": field(item, "call_id") or field(item, "id"), + "type": item_type, + "name": field(item, "name"), + "arguments": normalize_tool_arguments( + field(item, "arguments", field(item, "input", "")) + ), + } + ) + usage_dict = normalize_usage(field(response, "usage")) + reasoning = "" + refusal: str | None = None + if choices: + message = field(choices[0], "message") + for name in ("reasoning_content", "reasoning"): + value = field(message, name) + if isinstance(value, str): + reasoning += value + candidate_refusal = field(message, "refusal") + if isinstance(candidate_refusal, str) and candidate_refusal: + refusal = candidate_refusal + for item in field(response, "output", []) or []: + if field(item, "type") in {"reasoning", "reasoning_item"}: + summary = field(item, "summary") + if isinstance(summary, str): + reasoning += summary + elif isinstance(summary, (list, tuple)): + for entry in summary: + reasoning_text = field(entry, "text") + if isinstance(reasoning_text, str): + reasoning += reasoning_text + for content in field(item, "content", []) or []: + reasoning_text = field(content, "text") + if isinstance(reasoning_text, str): + reasoning += reasoning_text + fallback = field(item, "text") + if isinstance(fallback, str): + reasoning += fallback + if field(item, "type") == "refusal": + candidate_refusal = field(item, "refusal") or field(item, "text") + if isinstance(candidate_refusal, str) and candidate_refusal: + refusal = candidate_refusal + for content in field(item, "content", []) or []: + if field(content, "type") != "refusal": + continue + candidate_refusal = field(content, "refusal") or field(content, "text") + if isinstance(candidate_refusal, str) and candidate_refusal: + refusal = candidate_refusal + if ( + not choices + and not text + and not calls + and not refusal + and not reasoning + and field(response, "status") == "completed" + ): + # 项目规则禁止用占位结果掩盖失败。Responses 响应标记为 completed + # 却既无正文、无工具调用、也无拒答与推理,说明响应结构与预期不符 + # (例如 SDK 改了字段形状)。此时静默返回空文本会让上层拿到空串后 + # 继续,最终表现为难以定位的「空结果」错误,因此在边界显式失败。 + raise RuntimeError( + "Ark Responses 响应已完成但未包含任何正文、工具调用或拒答内容;" + "请检查响应结构与 SDK 版本是否匹配。" + ) + return CompletionResult(text=text, tool_calls=calls, usage=usage_dict, + finish_reason=field(choices[0], "finish_reason") if choices else field(response, "status"), + response_id=field(response, "id"), refusal=refusal, + reasoning=reasoning, raw=response) + + @staticmethod + def _raise_for_response_error(response: Any) -> None: + status = field(response, "status") + if status == "failed": + error = field(response, "error") + message = error if isinstance(error, str) else field(error, "message", "Ark Responses 请求失败") + raise RuntimeError(str(message)) + if status == "incomplete": + details = field(response, "incomplete_details") + reason = field(details, "reason", "unknown") + raise RuntimeError(f"Ark Responses 响应不完整: {reason}") + if status == "cancelled": + raise RuntimeError("Ark Responses 请求已取消") + + @staticmethod + def _chat_stream_events(chunk: Any) -> list[StreamEvent]: + raise_for_stream_error_event(chunk, "Ark Chat Completions") + OpenAICompatibleProvider._raise_for_chat_response_error( + chunk, + allow_empty_choices=True, + provider="Ark Chat Completions", + ) + choices = field(chunk, "choices", []) or [] + usage = normalize_usage(field(chunk, "usage")) + if not choices: + return [StreamEvent(type="usage", usage=usage, raw=chunk)] if usage else [] + choice = choices[0] + delta = field(choice, "delta") + text = content_to_text(field(delta, "content", "")) + reasoning = field(delta, "reasoning_content") or field(delta, "reasoning") or "" + refusal = field(delta, "refusal") + calls = field(delta, "tool_calls", []) or [] + finish_reason = field(choice, "finish_reason") + events: list[StreamEvent] = [] + if isinstance(refusal, str) and refusal: + events.append(StreamEvent(type="refusal_delta", refusal=refusal, raw=chunk)) + response_id = field(chunk, "id") + if text: + events.append( + StreamEvent(type="text_delta", text=text, response_id=response_id, raw=chunk) + ) + if isinstance(reasoning, str) and reasoning: + events.append( + StreamEvent(type="reasoning_delta", reasoning=reasoning, response_id=response_id, raw=chunk) + ) + if calls: + for position, call in enumerate(calls): + function = field(call, "function") + events.append( + StreamEvent( + type="tool_call_delta", + tool_call={ + "id": field(call, "id"), + "index": field(call, "index", position), + "type": field(call, "type", "function"), + "name": field(function, "name"), + "arguments": field(function, "arguments", ""), + }, + response_id=response_id, + raw=chunk, + ) + ) + if finish_reason: + events.append( + StreamEvent( + type="finish", + usage=usage, + finish_reason=finish_reason, + response_id=field(chunk, "id"), + raw=chunk, + ) + ) + elif usage: + if events: + events[-1].usage = usage + else: + events.append(StreamEvent(type="usage", usage=usage, raw=chunk)) + return events + + @staticmethod + def _chat_stream_event(chunk: Any) -> StreamEvent | None: + events = VolcengineProvider._chat_stream_events(chunk) + return events[0] if events else None + + @staticmethod + def _merge_response_tool_call( + accumulator: dict[Any, dict[str, Any]], + tool_call: Mapping[str, Any], + ) -> tuple[Any, dict[str, Any]]: + index = tool_call.get("index") + call_id = tool_call.get("call_id") or tool_call.get("id") + key: Any = ("index", index) if index is not None else ("id", call_id) + if key == ("id", None): + key = ("position", len(accumulator)) + merged = accumulator.setdefault(key, {}) + for name in ("id", "call_id", "index", "type", "name"): + value = tool_call.get(name) + if value is not None and value != "": + merged[name] = value + arguments = tool_call.get("arguments") + if arguments is not None and arguments != "": + merged["arguments"] = normalize_tool_arguments(arguments) + merged.setdefault("type", "function_call") + merged.setdefault("arguments", "") + return key, merged + + @classmethod + def _responses_completion_event( + cls, + converted: StreamEvent, + accumulator: dict[Any, dict[str, Any]], + ) -> tuple[Any, StreamEvent] | None: + if converted.type != "tool_call_completed" or not converted.tool_call: + return None + key, merged = cls._merge_response_tool_call(accumulator, converted.tool_call) + validate_complete_tool_call(merged, "Ark Responses") + return key, StreamEvent( + type="tool_call_completed", + tool_call=dict(merged), + response_id=converted.response_id, + raw=converted.raw, + ) + + def _responses_stream_event( + self, + event: Any, + tool_metadata: dict[Any, dict[str, Any]] | None = None, + ) -> StreamEvent | None: + """复用 Responses 事件转换,确保 Ark 与 OpenAI 的工具关联一致。""" + return OpenAICompatibleProvider._responses_stream_event( + event, + tool_metadata, + error_provider="Ark Responses", + response_error_handler=self._raise_for_response_error, + ) + + def stream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> Generator[StreamEvent, None, None]: + request = coerce_completion_request(request, kwargs, "Ark stream_events").copy_with( + stream=True, + ) + if self._protocol == "responses": + self._require_responses_resource("stream_events") + response_tool_metadata: dict[Any, dict[str, Any]] = {} + response_tool_calls: dict[Any, dict[str, Any]] = {} + pending_completions: dict[Any, StreamEvent] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + for event in retry_sync_stream( + lambda: self._get_client().responses.create( + **self._build_responses_request(request) + ), + lambda event: raise_for_stream_error_event(event, "Ark Responses"), + ): + if terminal_seen: + continue + event_type = field(event, "type", "") + converted = self._responses_stream_event(event, response_tool_metadata) + if converted: + if converted.type == "finish": + terminal_seen = True + for key, pending in pending_completions.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield pending + for key, tool_call in response_tool_calls.items(): + if key not in completed_tool_calls: + validate_complete_tool_call(tool_call, "Ark Responses") + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=converted.response_id, + raw=converted.raw, + ) + yield converted + # Drain the SDK stream to EOF before closing it. Some + # async transports keep a response-body generator open + # after the terminal event; breaking here can surface + # noisy generator-close errors and leak a connection. + continue + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(response_tool_calls, converted.tool_call) + yield converted + continue + if converted.type == "tool_call_completed": + completed = self._responses_completion_event(converted, response_tool_calls) + if completed is None: + continue + key, completed_event = completed + if event_type in { + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + pending_completions[key] = completed_event + continue + pending_completions.pop(key, None) + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield completed_event + continue + yield converted + if not terminal_seen: + raise RuntimeError("Ark Responses 流在 response.completed 之前结束。") + return + tool_calls: dict[Any, dict[str, Any]] = {} + completed_chat_tool_calls: set[Any] = set() + terminal_seen = False + for chunk in retry_sync_stream( + lambda: self._get_client().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ), + lambda chunk: raise_for_stream_error_event(chunk, "Ark Chat Completions"), + ): + raise_for_stream_error_event(chunk, "Ark Chat Completions") + for converted in self._chat_stream_events(chunk): + if converted.type == "finish": + terminal_seen = True + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(tool_calls, converted.tool_call) + if converted.type == "finish": + for key, tool_call in tool_calls.items(): + if key not in completed_chat_tool_calls: + validate_complete_tool_call(tool_call, "Ark Chat Completions") + completed_chat_tool_calls.add(key) + yield StreamEvent(type="tool_call_completed", tool_call=dict(tool_call), response_id=converted.response_id, raw=converted.raw) + yield converted + if not terminal_seen: + raise RuntimeError("Ark Chat Completions 流在 finish 事件之前结束。") + + async def astream_events(self, request: CompletionRequest | None = None, **kwargs: Any) -> AsyncGenerator[StreamEvent, None]: + request = coerce_completion_request(request, kwargs, "Ark astream_events").copy_with( + stream=True, + ) + if self._protocol == "responses": + self._require_responses_resource("astream_events") + response_tool_metadata: dict[Any, dict[str, Any]] = {} + response_tool_calls: dict[Any, dict[str, Any]] = {} + pending_completions: dict[Any, StreamEvent] = {} + completed_tool_calls: set[Any] = set() + terminal_seen = False + async for event in retry_async_stream( + lambda: self._get_aclient().responses.create( + **self._build_responses_request(request) + ), + lambda event: raise_for_stream_error_event(event, "Ark Responses"), + ): + if terminal_seen: + continue + event_type = field(event, "type", "") + converted = self._responses_stream_event(event, response_tool_metadata) + if converted: + if converted.type == "finish": + terminal_seen = True + for key, pending in pending_completions.items(): + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield pending + for key, tool_call in response_tool_calls.items(): + if key not in completed_tool_calls: + validate_complete_tool_call(tool_call, "Ark Responses") + completed_tool_calls.add(key) + yield StreamEvent( + type="tool_call_completed", + tool_call=dict(tool_call), + response_id=converted.response_id, + raw=converted.raw, + ) + yield converted + # Drain the SDK stream to EOF before closing it. Some + # async transports keep the response body generator + # open after the terminal event. + continue + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(response_tool_calls, converted.tool_call) + yield converted + continue + if converted.type == "tool_call_completed": + completed = self._responses_completion_event(converted, response_tool_calls) + if completed is None: + continue + key, completed_event = completed + if event_type in { + "response.function_call_arguments.done", + "response.custom_tool_call_input.done", + }: + pending_completions[key] = completed_event + continue + pending_completions.pop(key, None) + if key not in completed_tool_calls: + completed_tool_calls.add(key) + yield completed_event + continue + yield converted + if not terminal_seen: + raise RuntimeError("Ark Responses 异步流在 response.completed 之前结束。") + return + tool_calls: dict[Any, dict[str, Any]] = {} + completed_chat_tool_calls: set[Any] = set() + terminal_seen = False + async for chunk in retry_async_stream( + lambda: self._get_aclient().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ), + lambda chunk: raise_for_stream_error_event(chunk, "Ark Chat Completions"), + ): + raise_for_stream_error_event(chunk, "Ark Chat Completions") + for converted in self._chat_stream_events(chunk): + if converted.type == "finish": + terminal_seen = True + if converted.type == "tool_call_delta" and converted.tool_call: + merge_tool_call_fragment(tool_calls, converted.tool_call) + if converted.type == "finish": + for key, tool_call in tool_calls.items(): + if key not in completed_chat_tool_calls: + validate_complete_tool_call(tool_call, "Ark Chat Completions") + completed_chat_tool_calls.add(key) + yield StreamEvent(type="tool_call_completed", tool_call=dict(tool_call), response_id=converted.response_id, raw=converted.raw) + yield converted + if not terminal_seen: + raise RuntimeError("Ark Chat Completions 异步流在 finish 事件之前结束。") + @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + retry=retry_if_exception(is_retryable_error), + reraise=True, ) - def invoke( - self, - prompt: str, - system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, - stream: bool = True, - temperature: float = 0.7, - ) -> Generator[str, None, None]: - """同步调用火山引擎 LLM (CSE Sensor)。""" - logger.info(f"调用火山引擎 LLM ({self._model_name}),流式: {stream}") - client = self._get_client() - start_time = time.perf_counter() - - try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = client.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream - ) - + def complete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + request = coerce_completion_request(request, kwargs, "Ark complete") + request = request.copy_with(stream=False) + if self._protocol == "responses": + self._require_responses_resource("complete") + response = self._get_client().responses.create( + **self._build_responses_request(request) + ) + self._raise_for_response_error(response) + else: + response = self._get_client().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ) + OpenAICompatibleProvider._raise_for_chat_response_error( + response, + provider="Ark Chat Completions", + ) + return self._extract_result(response) + + async def acomplete(self, request: CompletionRequest | None = None, **kwargs: Any) -> CompletionResult: + """使用 AsyncArk 聚合完整结果,保留 Responses/Chat 元数据。""" + request = coerce_completion_request(request, kwargs, "Ark acomplete") + request = request.copy_with(stream=False) + if self._protocol == "responses": + self._require_responses_resource("acomplete") + response = await retry_async_call( + lambda: self._get_aclient().responses.create( + **self._build_responses_request(request) + ) + ) + self._raise_for_response_error(response) + else: + response = await retry_async_call( + lambda: self._get_aclient().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ) + ) + OpenAICompatibleProvider._raise_for_chat_response_error( + response, + provider="Ark Chat Completions", + ) + return self._extract_result(response) + + def invoke(self, prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", + tools: list[dict[str, Any]] | None = None, stream: bool = True, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + messages: Any = None, max_tokens: int | None = None, top_p: float | None = None, + stop: str | list[str] | None = None, response_format: dict[str, Any] | None = None, + tool_choice: Any = None, extra_body: dict[str, Any] | None = None, **kwargs: Any) -> Generator[str, None, None]: + request = CompletionRequest(prompt=prompt, system_prompt=system_prompt, messages=messages, tools=tools, stream=stream, temperature=temperature, max_tokens=max_tokens, top_p=top_p, stop=stop, response_format=response_format, tool_choice=tool_choice, extra_body=extra_body, **kwargs) + if self._protocol == "responses": + self._require_responses_resource("invoke") + if tools: + raise ValueError( + "Ark Responses invoke 仅返回文本,不支持 tools;" + "请使用 complete 或 stream_events 获取工具调用。" + ) if stream: - for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content + terminal_seen = False + for event in retry_sync_stream( + lambda: self._get_client().responses.create( + **self._build_responses_request(request) + ), + lambda event: raise_for_stream_error_event(event, "Ark Responses"), + ): + raise_for_stream_error_event(event, "Ark Responses") + converted = self._responses_stream_event(event) + if field(event, "type") == "response.completed": + terminal_seen = True + if converted and converted.type == "refusal_delta" and converted.refusal: + raise RuntimeError(f"Ark Responses 返回拒答: {converted.refusal}") + if converted and converted.text: + yield converted.text + if not terminal_seen: + raise RuntimeError("Ark Responses 流在 response.completed 之前结束。") + else: + response = retry_sync_call( + lambda: self._get_client().responses.create( + **self._build_responses_request(request) + ) + ) + self._raise_for_response_error(response) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Ark Responses 返回拒答: {result.refusal}") + if result.text: + yield result.text + return + if stream: + terminal_seen = False + for chunk in retry_sync_stream( + lambda: self._get_client().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ), + lambda chunk: raise_for_stream_error_event(chunk, "Ark Chat Completions"), + ): + raise_for_stream_error_event(chunk, "Ark Chat Completions") + for event in self._chat_stream_events(chunk): + if event.type == "finish": + terminal_seen = True + if event.type == "refusal_delta" and event.refusal: + raise RuntimeError(f"Ark Chat Completions 返回拒答: {event.refusal}") + if event.type == "text_delta" and event.text: + yield event.text + if not terminal_seen: + raise RuntimeError("Ark Chat Completions 流在 finish 事件之前结束。") + else: + response = retry_sync_call( + lambda: self._get_client().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ) + ) + OpenAICompatibleProvider._raise_for_chat_response_error( + response, + provider="Ark Chat Completions", + ) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Ark Chat Completions 返回拒答: {result.refusal}") + if result.text: + yield result.text + + async def ainvoke(self, prompt: str | None = None, system_prompt: str | None = "You are a helpful assistant.", + tools: list[dict[str, Any]] | None = None, stream: bool = True, + temperature: float | None = _UNSET_TEMPERATURE, # type: ignore[assignment] + **kwargs: Any) -> AsyncGenerator[str, None]: + if prompt is not None: + kwargs["prompt"] = prompt + kwargs["system_prompt"] = system_prompt + if tools is not None: + kwargs["tools"] = tools + kwargs["stream"] = stream + kwargs["temperature"] = temperature + request = CompletionRequest(**kwargs) + if self._protocol == "responses": + self._require_responses_resource("ainvoke") + if request.tools: + raise ValueError( + "Ark Responses ainvoke 仅返回文本,不支持 tools;" + "请使用 acomplete 或 astream_events 获取工具调用。" + ) + if request.stream: + terminal_seen = False + async for event in retry_async_stream( + lambda: self._get_aclient().responses.create( + **self._build_responses_request(request) + ), + lambda event: raise_for_stream_error_event(event, "Ark Responses"), + ): + raise_for_stream_error_event(event, "Ark Responses") + converted = self._responses_stream_event(event) + if field(event, "type") == "response.completed": + terminal_seen = True + if converted and converted.type == "refusal_delta" and converted.refusal: + raise RuntimeError(f"Ark Responses 返回拒答: {converted.refusal}") + if converted and converted.text: + yield converted.text + if not terminal_seen: + raise RuntimeError("Ark Responses 异步流在 response.completed 之前结束。") else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content - - duration = time.perf_counter() - start_time - logger.info(f"火山引擎 LLM ({self._model_name}) 调用完成,耗时: {duration:.2f}s") - except Exception as e: - logger.error(f"火山引擎 LLM ({self._model_name}) 出错: {e}", exc_info=True) - raise + response = await retry_async_call( + lambda: self._get_aclient().responses.create( + **self._build_responses_request(request) + ) + ) + self._raise_for_response_error(response) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Ark Responses 返回拒答: {result.refusal}") + if result.text: + yield result.text + return + if request.stream: + terminal_seen = False + async for chunk in retry_async_stream( + lambda: self._get_aclient().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ), + lambda chunk: raise_for_stream_error_event(chunk, "Ark Chat Completions"), + ): + raise_for_stream_error_event(chunk, "Ark Chat Completions") + for event in self._chat_stream_events(chunk): + if event.type == "finish": + terminal_seen = True + if event.type == "refusal_delta" and event.refusal: + raise RuntimeError(f"Ark Chat Completions 返回拒答: {event.refusal}") + if event.type == "text_delta" and event.text: + yield event.text + if not terminal_seen: + raise RuntimeError("Ark Chat Completions 异步流在 finish 事件之前结束。") + else: + response = await retry_async_call( + lambda: self._get_aclient().chat.completions.create( + **self._build_chat_request(**request.to_invoke_kwargs()) + ) + ) + OpenAICompatibleProvider._raise_for_chat_response_error( + response, + provider="Ark Chat Completions", + ) + result = self._extract_result(response) + if result.refusal: + raise RuntimeError(f"Ark Chat Completions 返回拒答: {result.refusal}") + if result.text: + yield result.text @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + retry=retry_if_exception(is_retryable_error), + reraise=True, ) - async def ainvoke( - self, - prompt: str, - system_prompt: str | None = "You are a helpful assistant.", - tools: List[Dict[str, Any]] | None = None, - stream: bool = True, - temperature: float = 0.7, - ) -> AsyncGenerator[str, None]: - """异步调用火山引擎 LLM (CSE Sensor)。""" - logger.info(f"异步调用 火山引擎 LLM ({self._model_name}),流式: {stream}") - aclient = self._get_aclient() - start_time = time.perf_counter() - - try: - messages = [{"role": "system", "content": system_prompt}, {"role": "user", "content": prompt}] - response = await aclient.chat.completions.create( - model=self._model_name, - messages=messages, - temperature=temperature, - stream=stream - ) - - if stream: - async for chunk in response: - if chunk.choices and chunk.choices[0].delta.content: - yield chunk.choices[0].delta.content - else: - if response.choices and response.choices[0].message.content: - yield response.choices[0].message.content - - duration = time.perf_counter() - start_time - logger.info(f"火山引擎 LLM ({self._model_name}) 异步调用完成,耗时: {duration:.2f}s") - except Exception as e: - logger.error(f"火山引擎 LLM ({self._model_name}) 异步出错: {e}", exc_info=True) - raise + def embed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: + overlap = sorted({"input", "model"}.intersection(kwargs)) + if overlap: + raise ValueError(f"Ark Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + request: dict[str, Any] = {"input": texts, "model": self._model_name} + configured_options = self._embedding_options() + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Ark Embedding", + set(request).union(configured_options), + ) + request_options = dict(kwargs) + self._validate_resource_kwargs( + "Embedding", + request_options, + {"encoding_format", "dimensions", "user", "extra_body", "extra_headers", "extra_query", "timeout"}, + ) + request_extra_body = self._validated_extra_body( + request_options.pop("extra_body", None), + "Ark Embedding", + set(request).union(configured_options).union(request_options), + ) + request.update(configured_options) + request.update(request_options) + merged_extra_body = self._merge_extra_body( + configured_extra_body, request_extra_body, "Ark Embedding" + ) + if merged_extra_body: + request["extra_body"] = merged_extra_body + response = self._get_client().embeddings.create(**request) + return [normalize_embedding_vector(item.embedding) for item in response.data] @retry( stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True + retry=retry_if_exception(is_retryable_error), + reraise=True, ) - def embed_documents(self, texts: List[str]) -> List[List[float]]: - """同步向量化文档。""" - logger.info(f"调用火山引擎嵌入 ({self._model_name}),数量: {len(texts)}") + async def aembed_documents(self, texts: list[str], **kwargs: Any) -> list[list[float]]: + overlap = sorted({"input", "model"}.intersection(kwargs)) + if overlap: + raise ValueError(f"Ark Embedding 不允许覆盖请求字段: {', '.join(overlap)}") + request: dict[str, Any] = {"input": texts, "model": self._model_name} + configured_options = self._embedding_options() + configured_extra_body = self._validated_extra_body( + configured_options.pop("extra_body", None), + "Ark Embedding", + set(request).union(configured_options), + ) + request_options = dict(kwargs) + self._validate_resource_kwargs( + "Embedding", + request_options, + {"encoding_format", "dimensions", "user", "extra_body", "extra_headers", "extra_query", "timeout"}, + ) + request_extra_body = self._validated_extra_body( + request_options.pop("extra_body", None), + "Ark Embedding", + set(request).union(configured_options).union(request_options), + ) + request.update(configured_options) + request.update(request_options) + merged_extra_body = self._merge_extra_body( + configured_extra_body, request_extra_body, "Ark Embedding" + ) + if merged_extra_body: + request["extra_body"] = merged_extra_body + response = await self._resolve_async_result(self._get_aclient().embeddings.create(**request)) + return [normalize_embedding_vector(item.embedding) for item in response.data] + + def upload_file(self, file: Any, purpose: str, **kwargs: Any) -> Any: + purpose = self._require_non_empty_string(purpose, "Ark 文件 purpose") + self._validate_resource_kwargs("文件创建", kwargs, self._ARK_FILE_CREATE_KEYS) + return self._get_client().files.create(file=file, purpose=purpose, **kwargs) + + def list_files(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("文件列表", kwargs, self._ARK_FILE_LIST_KEYS) + return self._get_client().files.list(**kwargs) + + def retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + file_id = self._require_non_empty_string(file_id, "Ark 文件 file_id") + self._validate_resource_kwargs("文件获取", kwargs, {"extra_headers", "extra_query", "extra_body", "timeout"}) + return self._get_client().files.retrieve(file_id, **kwargs) + + def delete_file(self, file_id: str, **kwargs: Any) -> Any: + file_id = self._require_non_empty_string(file_id, "Ark 文件 file_id") + self._validate_resource_kwargs("文件删除", kwargs, {"extra_headers", "extra_query", "extra_body", "timeout"}) + return self._get_client().files.delete(file_id, **kwargs) + + def wait_for_file( + self, + file_id: str, + *, + poll_interval: float | None = None, + max_wait_seconds: float | None = None, + **kwargs: Any, + ) -> Any: + """等待 Ark 文件处理完成,仅转发 SDK 的轮询参数。""" + file_id, poll_interval, max_wait_seconds = self._validate_file_wait_args( + file_id, + poll_interval, + max_wait_seconds, + ) + self._validate_resource_kwargs("文件等待", kwargs, set()) + poll_kwargs = { + key: value + for key, value in { + "poll_interval": poll_interval, + "max_wait_seconds": max_wait_seconds, + }.items() + if value is not None + } + return self._get_client().files.wait_for_processing(id=file_id, **poll_kwargs) + + def retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("retrieve_response") + self._validate_resource_kwargs( + "Responses retrieve_response", kwargs, self._ARK_RESPONSES_RETRIEVE_KEYS + ) + return self._get_client().responses.retrieve(response_id, **kwargs) + + def create_response(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """创建 Ark Responses;可传统一请求或原生 SDK 参数。""" + self._require_responses_resource("create_response") + if request is not None: + if kwargs: + raise ValueError("Ark Responses 不能同时传 request 和原生关键字参数。") + params = self._build_responses_request(request) + else: + params = self._validate_native_response_kwargs(kwargs) + return self._get_client().responses.create(**params) + + def delete_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("delete_response") + self._validate_resource_kwargs( + "Responses delete_response", kwargs, self._ARK_RESPONSES_RETRIEVE_KEYS + ) + return self._get_client().responses.delete(response_id, **kwargs) + + def list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_response_input_items") + self._validate_resource_kwargs( + "Responses list_response_input_items", + kwargs, + self._ARK_RESPONSES_INPUT_ITEMS_KEYS, + ) + return self._get_client().input_items.list(response_id, **kwargs) + + def create_batch_chat(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Chat", + self._ARK_BATCH_CHAT_KEYS, + ("messages",), + ) + return self._get_client().batch.chat.completions.create(**params) + + def create_batch_embedding(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Embedding", + self._ARK_BATCH_EMBEDDING_KEYS, + ("input",), + ) + return self._get_client().batch.embeddings.create(**params) + + def create_batch_multimodal_embedding(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Multimodal Embedding", + self._ARK_BATCH_MULTIMODAL_EMBEDDING_KEYS, + ("input",), + ) + return self._get_client().batch.multimodal_embeddings.create(**params) + + def create_context(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Context", + self._ARK_CONTEXT_CREATE_KEYS, + ("messages",), + ) + return self._get_client().context.create(**params) + + def context_complete(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Context Completion", + self._ARK_CONTEXT_COMPLETION_KEYS, + ("context_id", "messages"), + ) + params["context_id"] = self._require_non_empty_string( + params["context_id"], + "Ark Context Completion 的 context_id", + ) + return self._get_client().context.completions.create(**params) + + def create_content_generation_task(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Content Generation", + self._ARK_CONTENT_GENERATION_CREATE_KEYS, + ("content",), + ) + return self._get_client().content_generation.tasks.create(**params) + + def get_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + task_id = self._require_non_empty_string(task_id, "Ark Content Generation 的 task_id") + self._validate_resource_kwargs( + "Content Generation 获取", + kwargs, + {"extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return self._get_client().content_generation.tasks.get(task_id=task_id, **kwargs) + + def list_content_generation_tasks(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs( + "Content Generation 列表", + kwargs, + self._ARK_CONTENT_GENERATION_LIST_KEYS, + ) + return self._get_client().content_generation.tasks.list(**kwargs) + + def delete_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + task_id = self._require_non_empty_string(task_id, "Ark Content Generation 的 task_id") + self._validate_resource_kwargs( + "Content Generation 删除", + kwargs, + {"extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return self._get_client().content_generation.tasks.delete(task_id=task_id, **kwargs) + + def count_tokens(self, text: str | list[str], **kwargs: Any) -> int: + self._validate_resource_kwargs("Tokenization", kwargs, {"user", "extra_headers", "extra_query", "extra_body", "timeout"}) + response = self._get_client().tokenization.create(text=text, model=self._model_name, **kwargs) + values = field(response, "data", []) or [] + if not values: + raise RuntimeError("Ark tokenization 响应缺少 data。") + return sum(int(field(item, "total_tokens", 0)) for item in values) + + def multimodal_embed(self, inputs: Any, **kwargs: Any) -> Any: + self._validate_resource_kwargs( + "Multimodal Embedding", + kwargs, + {"encoding_format", "dimensions", "instructions", "sparse_embedding", "extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return self._get_client().multimodal_embeddings.create(input=inputs, model=self._model_name, **kwargs) + + def list_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_input_items") + self._validate_resource_kwargs( + "Responses list_input_items", kwargs, self._ARK_RESPONSES_INPUT_ITEMS_KEYS + ) + return self._get_client().input_items.list(response_id, **kwargs) + + def generate_batch_chat(self, **kwargs: Any) -> Any: + """兼容旧名称,转发到唯一的 Batch Chat 实现。""" + return self.create_batch_chat(**kwargs) + + def generate_image(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("Images", kwargs, self._ARK_IMAGE_GENERATE_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_client().images.generate(**kwargs) + + def beta_chat_parse(self, **kwargs: Any) -> Any: + """调用 Ark beta.chat 的结构化解析接口。""" + self._validate_resource_kwargs("Beta Chat Parse", kwargs, self._ARK_BETA_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_client().beta.chat.completions.parse(**kwargs) + + def beta_chat_stream(self, **kwargs: Any) -> Any: + """调用 Ark beta.chat 的原生流式上下文管理器。""" + self._validate_resource_kwargs("Beta Chat Stream", kwargs, self._ARK_BETA_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_client().beta.chat.completions.stream(**kwargs) + + def bot_chat(self, **kwargs: Any) -> Any: + """调用 Ark Bot Chat 原生接口。""" + self._validate_resource_kwargs("Bot Chat", kwargs, self._ARK_BOT_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_client().bot_chat.completions.create(**kwargs) + + def _get_classification_resource(self) -> Any: + """返回 Ark Classification 资源。 + + Ark SDK 当前版本包含 classification 模块,但客户端构造函数没有将其 + 挂载到顶层。优先使用未来版本可能提供的顶层资源,否则显式实例化 + SDK 官方资源类;导入失败时保留可定位的 NotImplementedError。 + """ client = self._get_client() - start_time = time.perf_counter() - + resource = getattr(client, "classification", None) + if resource is not None: + return resource + resource = getattr(self, "_classification_resource", None) + if resource is not None: + return resource try: - # Ark SDK 嵌入调用 - response = client.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] - duration = time.perf_counter() - start_time - logger.info(f"火山引擎嵌入 ({self._model_name}) 完成,耗时: {duration:.2f}s") - return embeddings - except Exception as e: - logger.error(f"火山引擎嵌入 ({self._model_name}) 出错: {e}", exc_info=True) - raise + from volcenginesdkarkruntime.resources.classification import ( # type: ignore[import-untyped] + Classification, + ) + except (ImportError, AttributeError) as exc: + raise NotImplementedError( + "当前 volcengine-python-sdk 未提供 Ark Classification 资源。" + ) from exc + resource = Classification(client) + self._classification_resource = resource + return resource - @retry( - stop=stop_after_attempt(3), - wait=wait_exponential(multiplier=1, min=2, max=10), - retry=retry_if_exception_type(Exception), - reraise=True - ) - async def aembed_documents(self, texts: List[str]) -> List[List[float]]: - """异步向量化文档。""" - logger.info(f"异步调用 火山引擎嵌入 ({self._model_name}),数量: {len(texts)}") - aclient = self._get_aclient() - start_time = time.perf_counter() - + def classify( + self, + query: str, + labels: list[str], + model: str | None = None, + **kwargs: Any, + ) -> Any: + """调用 Ark Classification 接口。""" + query = self._require_non_empty_string(query, "Ark Classification 的 query") + if not isinstance(labels, list) or not labels or any( + not isinstance(label, str) or not label.strip() for label in labels + ): + raise ValueError("Ark Classification 的 labels 必须是非空字符串列表。") + labels = [label.strip() for label in labels] + effective_model = ( + self._model_name + if model is None + else self._require_non_empty_string(model, "Ark Classification 的 model") + ) + self._validate_resource_kwargs( + "Classification", + kwargs, + {"user", "extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return self._get_classification_resource().create( + query=query, + model=effective_model, + labels=labels, + **kwargs, + ) + + async def async_upload_file(self, file: Any, purpose: str, **kwargs: Any) -> Any: + purpose = self._require_non_empty_string(purpose, "Ark 文件 purpose") + self._validate_resource_kwargs("文件创建", kwargs, self._ARK_FILE_CREATE_KEYS) + return await self._resolve_async_result(self._get_aclient().files.create(file=file, purpose=purpose, **kwargs)) + + async def async_list_files(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("文件列表", kwargs, self._ARK_FILE_LIST_KEYS) + return await self._resolve_async_result(self._get_aclient().files.list(**kwargs)) + + async def async_retrieve_file(self, file_id: str, **kwargs: Any) -> Any: + file_id = self._require_non_empty_string(file_id, "Ark 文件 file_id") + self._validate_resource_kwargs("文件获取", kwargs, {"extra_headers", "extra_query", "extra_body", "timeout"}) + return await self._resolve_async_result(self._get_aclient().files.retrieve(file_id, **kwargs)) + + async def async_delete_file(self, file_id: str, **kwargs: Any) -> Any: + file_id = self._require_non_empty_string(file_id, "Ark 文件 file_id") + self._validate_resource_kwargs("文件删除", kwargs, {"extra_headers", "extra_query", "extra_body", "timeout"}) + return await self._resolve_async_result(self._get_aclient().files.delete(file_id, **kwargs)) + + async def async_wait_for_file( + self, + file_id: str, + *, + poll_interval: float | None = None, + max_wait_seconds: float | None = None, + **kwargs: Any, + ) -> Any: + """异步等待 Ark 文件处理完成,仅转发 SDK 的轮询参数。""" + file_id, poll_interval, max_wait_seconds = self._validate_file_wait_args( + file_id, + poll_interval, + max_wait_seconds, + ) + self._validate_resource_kwargs("文件等待", kwargs, set()) + poll_kwargs = { + key: value + for key, value in { + "poll_interval": poll_interval, + "max_wait_seconds": max_wait_seconds, + }.items() + if value is not None + } + return await self._resolve_async_result( + self._get_aclient().files.wait_for_processing(id=file_id, **poll_kwargs) + ) + + async def async_retrieve_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("retrieve_response") + self._validate_resource_kwargs( + "Responses retrieve_response", kwargs, self._ARK_RESPONSES_RETRIEVE_KEYS + ) + return await self._resolve_async_result(self._get_aclient().responses.retrieve(response_id, **kwargs)) + + async def async_create_response(self, request: CompletionRequest | None = None, **kwargs: Any) -> Any: + """异步创建 Ark Responses;可传统一请求或原生 SDK 参数。""" + self._require_responses_resource("create_response") + if request is not None: + if kwargs: + raise ValueError("Ark Responses 不能同时传 request 和原生关键字参数。") + params = self._build_responses_request(request) + else: + params = self._validate_native_response_kwargs(kwargs) + return await self._resolve_async_result(self._get_aclient().responses.create(**params)) + + async def async_delete_response(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("delete_response") + self._validate_resource_kwargs( + "Responses delete_response", kwargs, self._ARK_RESPONSES_RETRIEVE_KEYS + ) + return await self._resolve_async_result(self._get_aclient().responses.delete(response_id, **kwargs)) + + async def async_list_response_input_items(self, response_id: str, **kwargs: Any) -> Any: + self._require_responses_resource("list_response_input_items") + self._validate_resource_kwargs( + "Responses list_response_input_items", + kwargs, + self._ARK_RESPONSES_INPUT_ITEMS_KEYS, + ) + return await self._resolve_async_result( + self._get_aclient().input_items.list(response_id, **kwargs) + ) + + async def async_list_input_items(self, response_id: str, **kwargs: Any) -> Any: + """Ark 顶层 input_items 资源的异步直观别名。""" + self._require_responses_resource("list_input_items") + self._validate_resource_kwargs( + "Responses list_input_items", kwargs, self._ARK_RESPONSES_INPUT_ITEMS_KEYS + ) + return await self._resolve_async_result( + self._get_aclient().input_items.list(response_id, **kwargs) + ) + + async def async_create_batch_chat(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Chat", + self._ARK_BATCH_CHAT_ASYNC_KEYS, + ("messages",), + ) + return await self._resolve_async_result( + self._get_aclient().batch.chat.completions.create(**params) + ) + + async def async_generate_batch_chat(self, **kwargs: Any) -> Any: + """兼容旧名称,转发到当前 Ark Batch Chat 异步资源。""" + return await self.async_create_batch_chat(**kwargs) + + async def async_create_batch_embedding(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Embedding", + self._ARK_BATCH_EMBEDDING_KEYS, + ("input",), + ) + return await self._resolve_async_result( + self._get_aclient().batch.embeddings.create(**params) + ) + + async def async_create_batch_multimodal_embedding(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Batch Multimodal Embedding", + self._ARK_BATCH_MULTIMODAL_EMBEDDING_KEYS, + ("input",), + ) + return await self._resolve_async_result( + self._get_aclient().batch.multimodal_embeddings.create(**params) + ) + + async def async_create_context(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Context", + self._ARK_CONTEXT_CREATE_KEYS, + ("messages",), + ) + return await self._resolve_async_result(self._get_aclient().context.create(**params)) + + async def async_context_complete(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Context Completion", + self._ARK_CONTEXT_COMPLETION_KEYS, + ("context_id", "messages"), + ) + params["context_id"] = self._require_non_empty_string( + params["context_id"], + "Ark Context Completion 的 context_id", + ) + return await self._resolve_async_result( + self._get_aclient().context.completions.create(**params) + ) + + async def async_create_content_generation_task(self, **kwargs: Any) -> Any: + params = self._prepare_batch_kwargs( + kwargs, + self._model_name, + "Content Generation", + self._ARK_CONTENT_GENERATION_CREATE_KEYS, + ("content",), + ) + return await self._resolve_async_result( + self._get_aclient().content_generation.tasks.create(**params) + ) + + async def async_get_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + task_id = self._require_non_empty_string(task_id, "Ark Content Generation 的 task_id") + self._validate_resource_kwargs( + "Content Generation 获取", + kwargs, + {"extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return await self._resolve_async_result( + self._get_aclient().content_generation.tasks.get(task_id=task_id, **kwargs) + ) + + async def async_list_content_generation_tasks(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs( + "Content Generation 列表", + kwargs, + self._ARK_CONTENT_GENERATION_LIST_KEYS, + ) + return await self._resolve_async_result( + self._get_aclient().content_generation.tasks.list(**kwargs) + ) + + async def async_delete_content_generation_task(self, task_id: str, **kwargs: Any) -> Any: + task_id = self._require_non_empty_string(task_id, "Ark Content Generation 的 task_id") + self._validate_resource_kwargs( + "Content Generation 删除", + kwargs, + {"extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return await self._resolve_async_result( + self._get_aclient().content_generation.tasks.delete(task_id=task_id, **kwargs) + ) + + async def async_count_tokens(self, text: str | list[str], **kwargs: Any) -> int: + self._validate_resource_kwargs("Tokenization", kwargs, {"user", "extra_headers", "extra_query", "extra_body", "timeout"}) + response = await self._resolve_async_result(self._get_aclient().tokenization.create( + text=text, model=self._model_name, **kwargs + )) + values = field(response, "data", []) or [] + if not values: + raise RuntimeError("Ark tokenization 响应缺少 data。") + return sum(int(field(item, "total_tokens", 0)) for item in values) + + async def async_multimodal_embed(self, inputs: Any, **kwargs: Any) -> Any: + self._validate_resource_kwargs( + "Multimodal Embedding", + kwargs, + {"encoding_format", "dimensions", "instructions", "sparse_embedding", "extra_headers", "extra_query", "extra_body", "timeout"}, + ) + return await self._resolve_async_result(self._get_aclient().multimodal_embeddings.create( + input=inputs, model=self._model_name, **kwargs + )) + + async def async_generate_image(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("Images", kwargs, self._ARK_IMAGE_GENERATE_KEYS) + kwargs.setdefault("model", self._model_name) + return await self._resolve_async_result(self._get_aclient().images.generate(**kwargs)) + + async def async_beta_chat_parse(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("Beta Chat Parse", kwargs, self._ARK_BETA_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return await self._resolve_async_result(self._get_aclient().beta.chat.completions.parse(**kwargs)) + + def async_beta_chat_stream(self, **kwargs: Any) -> Any: + """返回 Ark 异步 Beta Chat 的原生流式上下文管理器。 + + Ark SDK 的 ``AsyncCompletions.stream`` 是同步方法,但返回的对象实现 + ``__aenter__``/``__aexit__``。这里不能再包一层协程,否则调用方必须 + 先无意义地 await,无法直接按 SDK 约定使用 ``async with``。 + """ + self._validate_resource_kwargs("Beta Chat Stream", kwargs, self._ARK_BETA_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return self._get_aclient().beta.chat.completions.stream(**kwargs) + + async def async_bot_chat(self, **kwargs: Any) -> Any: + self._validate_resource_kwargs("Bot Chat", kwargs, self._ARK_BOT_CHAT_KEYS) + kwargs.setdefault("model", self._model_name) + return await self._resolve_async_result(self._get_aclient().bot_chat.completions.create(**kwargs)) + + async def _get_async_classification_resource(self) -> Any: + """返回 Ark Async Classification 资源,保持与同步入口相同的显式边界。""" + client = self._get_aclient() + resource = getattr(client, "classification", None) + if resource is not None: + return resource + resource = getattr(self, "_async_classification_resource", None) + if resource is not None: + return resource try: - response = await aclient.embeddings.create(input=texts, model=self._model_name) - embeddings = [item.embedding for item in response.data] - duration = time.perf_counter() - start_time - logger.info(f"火山引擎嵌入 ({self._model_name}) 异步完成,耗时: {duration:.2f}s") - return embeddings - except Exception as e: - logger.error(f"火山引擎嵌入 ({self._model_name}) 异步出错: {e}", exc_info=True) - raise + from volcenginesdkarkruntime.resources.classification import ( # type: ignore[import-untyped] + AsyncClassification, + ) + except (ImportError, AttributeError) as exc: + raise NotImplementedError( + "当前 volcengine-python-sdk 未提供 Ark Async Classification 资源。" + ) from exc + resource = AsyncClassification(client) + self._async_classification_resource = resource + return resource + + async def async_classify( + self, + query: str, + labels: list[str], + model: str | None = None, + **kwargs: Any, + ) -> Any: + """异步调用 Ark Classification 接口。""" + query = self._require_non_empty_string(query, "Ark Classification 的 query") + if not isinstance(labels, list) or not labels or any( + not isinstance(label, str) or not label.strip() for label in labels + ): + raise ValueError("Ark Classification 的 labels 必须是非空字符串列表。") + labels = [label.strip() for label in labels] + effective_model = ( + self._model_name + if model is None + else self._require_non_empty_string(model, "Ark Classification 的 model") + ) + self._validate_resource_kwargs( + "Classification", + kwargs, + {"user", "extra_headers", "extra_query", "extra_body", "timeout"}, + ) + resource = await self._get_async_classification_resource() + return await self._resolve_async_result(resource.create( + query=query, + model=effective_model, + labels=labels, + **kwargs, + )) + + def close(self) -> None: + """释放同步和异步 Ark SDK 客户端及分类资源。""" + client = self._client + async_client = self._aclient + self._client = None + self._classification_resource = None + self._async_classification_resource = None + first_error: Exception | None = None + closed_ids: set[int] = set() + if client is not None: + try: + client.close() + closed_ids.add(id(client)) + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = exc + if async_client is not None: + if id(async_client) in closed_ids: + self._aclient = None + else: + try: + close_resource_sync(async_client, "Volcengine 异步客户端") + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + else: + self._aclient = None + else: + self._aclient = None + if first_error is not None: + raise first_error + + async def aclose(self) -> None: + """释放同步和异步 Ark SDK 客户端。""" + async_client = self._aclient + sync_client = self._client + self._aclient = None + self._client = None + self._classification_resource = None + self._async_classification_resource = None + first_error: Exception | None = None + closed_clients: set[int] = set() + for client in (async_client, sync_client): + if client is None or id(client) in closed_clients: + continue + closed_clients.add(id(client)) + try: + result = client.close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - close all resources before reporting + first_error = first_error or exc + if first_error is not None: + raise first_error diff --git a/src/retrieval/retriever.py b/src/retrieval/retriever.py index efe34f3..af83278 100644 --- a/src/retrieval/retriever.py +++ b/src/retrieval/retriever.py @@ -6,7 +6,7 @@ from __future__ import annotations import asyncio -from typing import Dict, List +from copy import deepcopy from rich.console import Console @@ -20,6 +20,14 @@ ) from ..utils.config import RetrievalMethod, get_settings +__all__ = [ + "HybridReranker", + "ModelProviderFactory", + "_merge_hybrid_results", + "aretrieve_documents", + "retrieve_documents", +] + def _build_session_config( retrieval_method: RetrievalMethod, @@ -44,8 +52,8 @@ def _build_session_config( score_threshold=score_threshold, active_llm_configuration=settings.default_llm_provider, active_rerank_configuration=active_rerank_configuration, - llm_configurations=settings.llm_configurations, - rerank_configurations=settings.rerank_configurations, + llm_configurations=deepcopy(settings.llm_configurations), + rerank_configurations=deepcopy(settings.rerank_configurations), chat_temperature=settings.chat_temperature, ) return session_config @@ -64,7 +72,7 @@ def retrieve_documents( score_threshold: float, fusion_strategy: str = "rrf", candidate_multiplier: int = 3, -) -> List[Dict]: +) -> list[dict]: return asyncio.run( aretrieve_documents( query=query, @@ -96,7 +104,7 @@ async def aretrieve_documents( score_threshold: float, fusion_strategy: str = "rrf", candidate_multiplier: int = 3, -) -> List[Dict]: +) -> list[dict]: run_config = build_run_config(get_settings()) session_config = _build_session_config( retrieval_method=retrieval_method, @@ -113,4 +121,7 @@ async def aretrieve_documents( vector_store=vector_store, embedding_service=EmbeddingService(run_config), ) - return await retrieval_service.retrieve(query=query, session_config=session_config, console=console) + try: + return await retrieval_service.retrieve(query=query, session_config=session_config, console=console) + finally: + await retrieval_service.aclose() diff --git a/src/retrieval/snapshot_repository.py b/src/retrieval/snapshot_repository.py index 0c30d62..4fa9774 100644 --- a/src/retrieval/snapshot_repository.py +++ b/src/retrieval/snapshot_repository.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- from __future__ import annotations -import json import shutil import tomllib import uuid from pathlib import Path -from typing import Any, Dict, Optional from src.runtime.contracts import KnowledgeSnapshotManifest, RunConfig @@ -17,14 +14,14 @@ def __init__(self, run_config: RunConfig): self.root = run_config.snapshot_root self.root.mkdir(parents=True, exist_ok=True) - def get_active_snapshot_id(self) -> Optional[str]: + def get_active_snapshot_id(self) -> str | None: marker = self.run_config.active_snapshot_marker if not marker.exists(): return None snapshot_id = marker.read_text(encoding="utf-8").strip() return snapshot_id or None - def get_active_snapshot_dir(self) -> Optional[Path]: + def get_active_snapshot_dir(self) -> Path | None: snapshot_id = self.get_active_snapshot_id() if not snapshot_id: return None @@ -38,6 +35,12 @@ def create_temp_snapshot_dir(self, snapshot_id: str) -> Path: temp_dir.mkdir(parents=True, exist_ok=True) return temp_dir + def cleanup_temp_snapshot_dir(self, snapshot_id: str) -> None: + """删除指定构建任务的临时目录,不触碰正式快照。""" + temp_dir = self.root / f".tmp-{snapshot_id}" + if temp_dir.exists(): + shutil.rmtree(temp_dir) + def finalize_snapshot(self, temp_dir: Path, snapshot_id: str) -> Path: final_dir = self.root / snapshot_id if final_dir.exists(): @@ -64,18 +67,6 @@ def load_manifest(self, snapshot_dir: Path) -> KnowledgeSnapshotManifest: data = tomllib.load(file) return KnowledgeSnapshotManifest.from_mapping(data) - def write_stats(self, snapshot_dir: Path, stats: Dict[str, Any]) -> None: - (snapshot_dir / "stats.json").write_text( - json.dumps(stats, ensure_ascii=False, indent=2), - encoding="utf-8", - ) - - def load_stats(self, snapshot_dir: Path) -> Dict[str, Any]: - stats_path = snapshot_dir / "stats.json" - if not stats_path.exists(): - return {} - return json.loads(stats_path.read_text(encoding="utf-8")) - def validate_snapshot_dir(self, snapshot_dir: Path) -> None: required_files = [ snapshot_dir / "manifest.toml", @@ -88,10 +79,3 @@ def validate_snapshot_dir(self, snapshot_dir: Path) -> None: missing_files = [str(path.name) for path in required_files if not path.exists()] if missing_files: raise FileNotFoundError(f"知识快照不完整,缺少文件: {', '.join(missing_files)}") - - def load_active_manifest(self) -> Optional[KnowledgeSnapshotManifest]: - snapshot_dir = self.get_active_snapshot_dir() - if snapshot_dir is None: - return None - self.validate_snapshot_dir(snapshot_dir) - return self.load_manifest(snapshot_dir) diff --git a/src/retrieval/vdb/base.py b/src/retrieval/vdb/base.py index 97a3367..339b404 100644 --- a/src/retrieval/vdb/base.py +++ b/src/retrieval/vdb/base.py @@ -3,7 +3,8 @@ # 遵循修改后的 Apache License 2.0 许可证。详情请参阅项目根目录下的 DIFY_LICENSE 文件。 from abc import ABC, abstractmethod -from typing import List, Dict, Any +from typing import Any + class VectorStoreBase(ABC): """ @@ -12,49 +13,42 @@ class VectorStoreBase(ABC): """ @abstractmethod - def add_documents(self, documents: List[Dict[str, Any]]): + def add_documents(self, documents: list[dict[str, Any]]): """同步添加文档。""" - pass @abstractmethod - async def aadd_documents(self, documents: List[Dict[str, Any]]): + async def aadd_documents(self, documents: list[dict[str, Any]]): """异步添加文档。""" - pass @abstractmethod - def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[Dict[str, Any]]: + def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: """同步搜索文档。""" - pass @abstractmethod - async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[Dict[str, Any]]: + async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: """异步搜索文档。""" - pass @abstractmethod def save(self, path: str): """保存向量存储。""" - pass @abstractmethod def load(self, path: str): """加载向量存储。""" - pass @abstractmethod def get_embedding_model(self) -> Any: """获取嵌入模型。""" - pass - def upsert_embeddings(self, documents: List[Dict[str, Any]], embeddings: Any): + def upsert_embeddings(self, documents: list[dict[str, Any]], embeddings: Any): """根据外部已生成的向量写入文档。默认未实现。""" raise NotImplementedError("当前向量存储未实现 upsert_embeddings。") - def semantic_search(self, query_embedding: Any, top_k: int = 5) -> List[Dict[str, Any]]: + def semantic_search(self, query_embedding: Any, top_k: int = 5) -> list[dict[str, Any]]: """使用查询向量执行语义检索。默认未实现。""" raise NotImplementedError("当前向量存储未实现 semantic_search。") - def keyword_search(self, query_text: str, top_k: int = 5) -> List[Dict[str, Any]]: + def keyword_search(self, query_text: str, top_k: int = 5) -> list[dict[str, Any]]: """使用原始文本执行关键词检索。默认退回 search。""" return self.search(query_text, top_k=top_k, search_type="keyword") @@ -66,9 +60,9 @@ def load_snapshot(self, snapshot_dir: str): """从快照目录加载状态。默认未实现。""" raise NotImplementedError("当前向量存储未实现 load_snapshot。") - def register_parent_documents(self, parent_documents: Dict[str, Dict[str, Any]]): + def register_parent_documents(self, parent_documents: dict[str, dict[str, Any]]): """注册父分段侧车数据。""" - return None + return def resolve_parent_content(self, parent_id: str | None) -> str | None: """根据父分段 ID 解析父内容。""" diff --git a/src/retrieval/vdb/factory.py b/src/retrieval/vdb/factory.py index 78bcb3e..0f4a24d 100644 --- a/src/retrieval/vdb/factory.py +++ b/src/retrieval/vdb/factory.py @@ -4,11 +4,11 @@ from pathlib import Path -from .base import VectorStoreBase -from .faiss_store import FaissStore -from ..snapshot_repository import SnapshotRepository from ...runtime.contracts import KnowledgeSnapshotManifest, build_run_config from ...utils.config import get_settings +from ..snapshot_repository import SnapshotRepository +from .base import VectorStoreBase +from .faiss_store import FaissStore class VectorStoreFactory: @@ -33,6 +33,25 @@ def _load_existing_state(store: VectorStoreBase, run_config) -> None: active_snapshot_dir = snapshot_repository.get_active_snapshot_dir() if active_snapshot_dir is not None: snapshot_repository.validate_snapshot_dir(active_snapshot_dir) + manifest = snapshot_repository.load_manifest(active_snapshot_dir) + embedding_detail = run_config.embedding_configurations.get( + run_config.default_embedding_provider + ) + if embedding_detail is None: + raise ValueError( + "当前 embedding 配置缺少活动提供商: " + f"{run_config.default_embedding_provider}" + ) + if ( + manifest.embedding_provider != run_config.default_embedding_provider + or manifest.embedding_model != embedding_detail.model_name + ): + raise RuntimeError( + "活动知识快照的 embedding 配置与当前运行配置不一致: " + f"快照={manifest.embedding_provider}/{manifest.embedding_model}," + f"当前={run_config.default_embedding_provider}/{embedding_detail.model_name}。" + "请重建知识快照或切换回原 embedding 配置。" + ) store.load_snapshot(str(active_snapshot_dir)) return diff --git a/src/retrieval/vdb/faiss_store.py b/src/retrieval/vdb/faiss_store.py index 360b918..e3a0b42 100644 --- a/src/retrieval/vdb/faiss_store.py +++ b/src/retrieval/vdb/faiss_store.py @@ -4,14 +4,17 @@ import json import os -import pickle + +# Pickle is retained only for trusted local legacy snapshots. +import pickle # nosec B403 import time +from copy import deepcopy from pathlib import Path -from typing import Any, Dict, List, Optional +from typing import Any -import jieba +import jieba # type: ignore[import-untyped] import numpy as np -from rank_bm25 import BM25Okapi +from rank_bm25 import BM25Okapi # type: ignore[import-untyped] try: import faiss @@ -20,31 +23,31 @@ "FAISS is not installed. Please install it with `pip install faiss-cpu`." ) from exc -from .base import VectorStoreBase from ...utils.log_manager import get_module_logger +from .base import VectorStoreBase logger = get_module_logger(__name__) jieba.setLogLevel(jieba.logging.ERROR) class FaissStore(VectorStoreBase): - def __init__(self, file_path: Optional[str] = None): + def __init__(self, file_path: str | None = None): import asyncio self.file_path = file_path - self.documents: List[Dict[str, Any]] = [] - self.embeddings: Optional[np.ndarray] = None - self.parent_documents: Dict[str, Dict[str, Any]] = {} - self._tokenized_docs_cache: List[List[str]] = [] - self.bm25_index: Optional[BM25Okapi] = None - self.faiss_index: Optional[faiss.IndexFlatL2] = None + self.documents: list[dict[str, Any]] = [] + self.embeddings: np.ndarray | None = None + self.parent_documents: dict[str, dict[str, Any]] = {} + self._tokenized_docs_cache: list[list[str]] = [] + self.bm25_index: BM25Okapi | None = None + self.faiss_index: faiss.Index | None = None self.lock = asyncio.Lock() if self.file_path and os.path.exists(self.file_path): self.load(self.file_path) @staticmethod - def _normalize_parent_document(parent_document: Any) -> Dict[str, Any]: + def _normalize_parent_document(parent_document: Any) -> dict[str, Any]: if isinstance(parent_document, dict): return { "content": parent_document.get("content", ""), @@ -55,7 +58,7 @@ def _normalize_parent_document(parent_document: Any) -> Dict[str, Any]: return {"content": "", "metadata": {}} @staticmethod - def _build_index_text(document: Dict[str, Any]) -> str: + def _build_index_text(document: dict[str, Any]) -> str: metadata = document.get("metadata") or {} source = metadata.get("source", "") source_hint = "" @@ -100,7 +103,7 @@ def _normalize_loaded_documents(self) -> None: document["metadata"] = metadata self.parent_documents = normalized_parent_documents - def register_parent_documents(self, parent_documents: Dict[str, Dict[str, Any]]): + def register_parent_documents(self, parent_documents: dict[str, dict[str, Any]]): for parent_id, parent_document in parent_documents.items(): normalized = self._normalize_parent_document(parent_document) if normalized["content"]: @@ -115,7 +118,7 @@ def resolve_parent_content(self, parent_id: str | None) -> str | None: content = parent_document.get("content") return content if isinstance(content, str) and content else None - def upsert_embeddings(self, documents: List[Dict[str, Any]], embeddings: Any): + def upsert_embeddings(self, documents: list[dict[str, Any]], embeddings: Any): if not documents: return @@ -134,30 +137,39 @@ def upsert_embeddings(self, documents: List[Dict[str, Any]], embeddings: Any): self.embeddings = np.vstack((self.embeddings, new_embeddings)) self._rebuild_indices() - def add_documents(self, documents: List[Dict[str, Any]]): + def add_documents(self, documents: list[dict[str, Any]]): raise RuntimeError("FaissStore.add_documents 已废弃,请使用外部 EmbeddingService 后调用 upsert_embeddings。") - async def aadd_documents(self, documents: List[Dict[str, Any]]): + async def aadd_documents(self, documents: list[dict[str, Any]]): raise RuntimeError("FaissStore.aadd_documents 已废弃,请使用 KnowledgeBuildService。") - def semantic_search(self, query_embedding: Any, top_k: int = 5) -> List[Dict[str, Any]]: + def semantic_search(self, query_embedding: Any, top_k: int = 5) -> list[dict[str, Any]]: if self.faiss_index is None or self.embeddings is None: return [] - query_vector = np.array(query_embedding, dtype=np.float32).reshape(1, -1) + query_vector = np.asarray(query_embedding, dtype=np.float32) + if query_vector.ndim != 1 or query_vector.size == 0: + raise ValueError("查询向量必须是一维且非空。") + expected_dimension = int(self.embeddings.shape[1]) + if query_vector.shape[0] != expected_dimension: + raise ValueError( + f"查询向量维度 {query_vector.shape[0]} 与索引维度 {expected_dimension} 不一致;" + "请重建知识快照或切换回构建该快照的嵌入模型。" + ) + query_vector = query_vector.reshape(1, expected_dimension) distances, indices = self.faiss_index.search(query_vector, top_k) similarities = 1.0 / (1.0 + distances[0]) - results: List[Dict[str, Any]] = [] + results: list[dict[str, Any]] = [] for index, doc_index in enumerate(indices[0]): if doc_index == -1: continue - document = pickle.loads(pickle.dumps(self.documents[doc_index])) + document = deepcopy(self.documents[doc_index]) document["score"] = float(similarities[index]) results.append(document) return results - def keyword_search(self, query_text: str, top_k: int = 5) -> List[Dict[str, Any]]: + def keyword_search(self, query_text: str, top_k: int = 5) -> list[dict[str, Any]]: if self.bm25_index is None: return [] @@ -165,22 +177,22 @@ def keyword_search(self, query_text: str, top_k: int = 5) -> List[Dict[str, Any] doc_scores = self.bm25_index.get_scores(tokenized_query) top_indices = np.argsort(doc_scores)[::-1] - results: List[Dict[str, Any]] = [] + results: list[dict[str, Any]] = [] for index in top_indices: score = float(doc_scores[index]) if score <= 0: break - document = pickle.loads(pickle.dumps(self.documents[index])) + document = deepcopy(self.documents[index]) document["score"] = score results.append(document) if len(results) >= top_k: break return results - def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[Dict[str, Any]]: + def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: raise RuntimeError("FaissStore.search 已废弃,请通过 RetrievalService 调用语义检索或关键词检索。") - async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[Dict[str, Any]]: + async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: raise RuntimeError("FaissStore.asearch 已废弃,请通过 RetrievalService 调用语义检索或关键词检索。") def save(self, path: str): @@ -197,8 +209,10 @@ def save(self, path: str): def load(self, path: str): if not os.path.exists(path): raise FileNotFoundError(f"向量存储文件未找到: {path}") + # Legacy compatibility: this path is only for local files generated by + # PyRAG-Kit. Never point it at an untrusted pickle file. with open(path, "rb") as file: - data = pickle.load(file) + data = pickle.load(file) # nosec B301 self.documents = data.get("documents", []) self.embeddings = data.get("embeddings") self.parent_documents = data.get("parent_documents", {}) @@ -206,6 +220,8 @@ def load(self, path: str): self._rebuild_indices() def save_snapshot(self, snapshot_dir: str): + if self.faiss_index is None or self.embeddings is None: + raise RuntimeError("当前语义索引为空,无法保存快照。") snapshot_path = Path(snapshot_dir) snapshot_path.mkdir(parents=True, exist_ok=True) with (snapshot_path / "chunks.pkl").open("wb") as file: @@ -215,8 +231,6 @@ def save_snapshot(self, snapshot_dir: str): with (snapshot_path / "lexical.index").open("wb") as file: pickle.dump(self._tokenized_docs_cache, file) np.save(snapshot_path / "embeddings.npy", self.embeddings) - if self.faiss_index is None: - raise RuntimeError("当前语义索引为空,无法保存快照。") faiss.write_index(self.faiss_index, str(snapshot_path / "semantic.index")) stats = { "document_count": len({doc.get("metadata", {}).get("source") for doc in self.documents}), @@ -231,10 +245,13 @@ def save_snapshot(self, snapshot_dir: str): def load_snapshot(self, snapshot_dir: str): snapshot_path = Path(snapshot_dir) + # SnapshotRepository accepts only application-managed snapshot dirs; + # keep the pickle extension for backward compatibility with existing + # Dify-derived snapshots and do not load arbitrary external files. with (snapshot_path / "chunks.pkl").open("rb") as file: - self.documents = pickle.load(file) + self.documents = pickle.load(file) # nosec B301 with (snapshot_path / "parents.pkl").open("rb") as file: - self.parent_documents = pickle.load(file) + self.parent_documents = pickle.load(file) # nosec B301 embeddings_path = snapshot_path / "embeddings.npy" self.embeddings = np.load(embeddings_path) if embeddings_path.exists() else None faiss_index_path = snapshot_path / "semantic.index" @@ -242,7 +259,7 @@ def load_snapshot(self, snapshot_dir: str): lexical_path = snapshot_path / "lexical.index" if lexical_path.exists(): with lexical_path.open("rb") as file: - self._tokenized_docs_cache = pickle.load(file) + self._tokenized_docs_cache = pickle.load(file) # nosec B301 self.bm25_index = BM25Okapi(self._tokenized_docs_cache) else: self._rebuild_indices() diff --git a/src/retrieval_test/core.py b/src/retrieval_test/core.py index 40a4b54..961e1de 100644 --- a/src/retrieval_test/core.py +++ b/src/retrieval_test/core.py @@ -1,7 +1,7 @@ -# -*- coding: utf-8 -*- import asyncio +import inspect import os -from typing import Dict, List +from typing import Any from prompt_toolkit import PromptSession from prompt_toolkit.formatted_text import HTML @@ -18,13 +18,14 @@ from ..ui.display_utils import get_relative_path from ..utils.config import get_settings from ..utils.log_manager import get_module_logger +from ..utils.security import safe_exception_text from .excel_logger import ExcelLogger console = Console() logger = get_module_logger(__name__) -def display_results(query: str, documents: List[Dict]): +def display_results(query: str, documents: list[dict]): if not documents: console.print(Panel(f"对查询 “[bold yellow]{query}[/bold yellow]” 无结果。", border_style="red")) return @@ -43,6 +44,20 @@ def display_results(query: str, documents: List[Dict]): console.print(table) + +async def _aclose_quietly(service: Any) -> None: + """释放服务持有的资源,失败只记录警告,不打断退出流程。""" + close = getattr(service, "aclose", None) or getattr(service, "close", None) + if not callable(close): + return + try: + result = close() + if inspect.isawaitable(result): + await result + except Exception as exc: # noqa: BLE001 - teardown must not mask the main flow + logger.warning("释放检索服务失败: %s", safe_exception_text(exc)) + + async def run_retrieval_test_async(): console.print(Panel("进入异步召回测试模式。输入 '/quit' 退出。", border_style="yellow")) session = PromptSession() @@ -50,36 +65,42 @@ async def run_retrieval_test_async(): excel_logger = None try: excel_logger = ExcelLogger() - except Exception as exc: - logger.error("ExcelLogger 初始化失败: %s", exc) + except Exception as exc: # noqa: BLE001 - interactive setup reports all failures + logger.error("ExcelLogger 初始化失败: %s", safe_exception_text(exc)) try: run_config = build_run_config(get_settings()) session_config = build_session_config(get_settings()) vector_store = VectorStoreFactory.get_default_vector_store() retrieval_service = RetrievalService(vector_store, EmbeddingService(run_config)) - except Exception as exc: - console.print(f"[red]数据库加载失败: {exc}[/red]") + except Exception as exc: # noqa: BLE001 - interactive setup reports all failures + console.print(f"[red]数据库加载失败: {safe_exception_text(exc)}[/red]") return - while True: - try: - query = await session.prompt_async(HTML('测试查询: ')) - if query.lower() == "/quit": + try: + while True: + try: + query = await session.prompt_async(HTML('测试查询: ')) + if query.lower() == "/quit": + break + if not query: + continue + + with console.status("[bold green]正在异步检索...[/bold green]"): + documents = await retrieval_service.retrieve(query, session_config, console=console) + display_results(query, documents) + if excel_logger: + excel_logger.log_results(query, documents) + except (KeyboardInterrupt, EOFError): break - if not query: - continue - - with console.status("[bold green]正在异步检索...[/bold green]"): - documents = await retrieval_service.retrieve(query, session_config, console=console) - display_results(query, documents) - if excel_logger: - excel_logger.log_results(query, documents) - except (KeyboardInterrupt, EOFError): - break - except Exception as exc: - console.print(f"[red]召回测试出错: {exc}[/red]") - logger.error("召回测试出错: %s", exc) + except Exception as exc: # noqa: BLE001 - interactive loop remains usable + console.print(f"[red]召回测试出错: {safe_exception_text(exc)}[/red]") + logger.error("召回测试出错: %s", safe_exception_text(exc)) + finally: + # 与 chat 路径保持一致:Embedding provider 与向量存储持有 SDK 客户端和 + # 连接池,退出时不释放会留下未关闭的资源。释放失败不掩盖主流程结果, + # 只记录警告。 + await _aclose_quietly(retrieval_service) def run_retrieval_test(): diff --git a/src/retrieval_test/excel_logger.py b/src/retrieval_test/excel_logger.py index 8ddb87b..3be8971 100644 --- a/src/retrieval_test/excel_logger.py +++ b/src/retrieval_test/excel_logger.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- -import os import json +import os from datetime import datetime -from typing import List, Dict, Any -from openpyxl import Workbook -from openpyxl.worksheet.worksheet import Worksheet +from typing import Any + +from openpyxl import Workbook # type: ignore[import-untyped] +from openpyxl.worksheet.worksheet import Worksheet # type: ignore[import-untyped] from ..utils.log_manager import get_module_logger @@ -25,7 +25,7 @@ def __init__(self, log_dir: str = "data/logs"): os.makedirs(log_dir, exist_ok=True) # 生成带时间戳的唯一文件名 - timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + timestamp = datetime.now().astimezone().strftime("%Y%m%d_%H%M%S") self.filepath = os.path.join(log_dir, f"recall_test_log_{timestamp}.xlsx") # 创建一个新的 Excel 工作簿和工作表 @@ -53,7 +53,7 @@ def _write_header(self): self.worksheet.append(headers) self.workbook.save(self.filepath) - def log_results(self, query: str, results: List[Dict[str, Any]]): + def log_results(self, query: str, results: list[dict[str, Any]]): """ 将单次查询的结果记录到 Excel 文件中。 @@ -61,8 +61,9 @@ def log_results(self, query: str, results: List[Dict[str, Any]]): query (str):用户的查询。 results (List[Dict[str, Any]]): 从检索器返回的文档列表。 """ - timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + timestamp = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S") + row: list[Any] if not results: # 如果没有结果,也记录一条信息 row = [timestamp, query, "N/A", "N/A", "无结果", "N/A", "N/A", "N/A"] @@ -100,5 +101,5 @@ def log_results(self, query: str, results: List[Dict[str, Any]]): try: self.workbook.save(self.filepath) logger.debug(f"成功将查询 '{query}' 的 {len(results)} 条结果记录到 {self.filepath}") - except Exception as e: - logger.error(f"保存 Excel 日志文件失败: {e}", exc_info=True) + except Exception: + logger.exception("保存 Excel 日志文件失败") diff --git a/src/runtime/__init__.py b/src/runtime/__init__.py index 633f866..8b13789 100644 --- a/src/runtime/__init__.py +++ b/src/runtime/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- diff --git a/src/runtime/contracts.py b/src/runtime/contracts.py index ae703c3..808cbfc 100644 --- a/src/runtime/contracts.py +++ b/src/runtime/contracts.py @@ -1,15 +1,14 @@ -# -*- coding: utf-8 -*- from __future__ import annotations +from collections.abc import Iterator, MutableMapping from copy import deepcopy from dataclasses import dataclass -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path -from typing import Any, Dict, Iterator, MutableMapping +from typing import Any from src.utils.config import ModelDetail, RetrievalMethod, Settings - SCHEMA_VERSION = "2" @@ -24,9 +23,9 @@ class RunConfig: default_llm_provider: str default_embedding_provider: str default_rerank_provider: str - llm_configurations: Dict[str, ModelDetail] - embedding_configurations: Dict[str, ModelDetail] - rerank_configurations: Dict[str, ModelDetail] + llm_configurations: dict[str, ModelDetail] + embedding_configurations: dict[str, ModelDetail] + rerank_configurations: dict[str, ModelDetail] chat_temperature: float kb_embedding_batch_size: int kb_chunk_size: int @@ -52,8 +51,8 @@ def __init__( score_threshold: float, active_llm_configuration: str, active_rerank_configuration: str, - llm_configurations: Dict[str, ModelDetail], - rerank_configurations: Dict[str, ModelDetail], + llm_configurations: dict[str, ModelDetail], + rerank_configurations: dict[str, ModelDetail], chat_temperature: float, ): self.retrieval_method = retrieval_method @@ -85,7 +84,7 @@ def __iter__(self) -> Iterator[str]: def __len__(self) -> int: return len(self.to_dict()) - def to_dict(self) -> Dict[str, Any]: + def to_dict(self) -> dict[str, Any]: return { "retrieval_method": self.retrieval_method, "vector_weight": self.vector_weight, @@ -127,11 +126,11 @@ def create( source_digest: str, document_count: int, chunk_count: int, - ) -> "KnowledgeSnapshotManifest": + ) -> KnowledgeSnapshotManifest: return cls( schema_version=SCHEMA_VERSION, snapshot_id=snapshot_id, - created_at=datetime.now(timezone.utc).isoformat(), + created_at=datetime.now(UTC).isoformat(), store_type=store_type, embedding_provider=embedding_provider, embedding_model=embedding_model, @@ -159,7 +158,7 @@ def to_toml(self) -> str: ) @classmethod - def from_mapping(cls, data: Dict[str, Any]) -> "KnowledgeSnapshotManifest": + def from_mapping(cls, data: dict[str, Any]) -> KnowledgeSnapshotManifest: return cls( schema_version=str(data["schema_version"]), snapshot_id=str(data["snapshot_id"]), diff --git a/src/services/__init__.py b/src/services/__init__.py index 633f866..8b13789 100644 --- a/src/services/__init__.py +++ b/src/services/__init__.py @@ -1,2 +1 @@ -# -*- coding: utf-8 -*- diff --git a/src/services/chat_service.py b/src/services/chat_service.py index 279b5c4..4cc7aa6 100644 --- a/src/services/chat_service.py +++ b/src/services/chat_service.py @@ -1,12 +1,13 @@ -# -*- coding: utf-8 -*- from __future__ import annotations -from typing import Any, AsyncGenerator, Dict, List +from collections.abc import AsyncGenerator +from typing import Any from src.providers.__base__.model_provider import LargeLanguageModel from src.runtime.contracts import SessionConfig from src.services.retrieval_service import RetrievalService from src.utils.log_manager import get_chat_logger +from src.utils.security import redact_sensitive_text class ChatService: @@ -15,18 +16,29 @@ def __init__(self, llm_model: LargeLanguageModel, retrieval_service: RetrievalSe self.retrieval_service = retrieval_service self.logger = get_chat_logger() + def _temperature_kwargs(self, temperature: float) -> dict[str, Any]: + supports_sampling = getattr(self.llm_model, "supports_sampling_option", None) + if callable(supports_sampling) and supports_sampling("temperature"): + return {"temperature": temperature} + return {} + async def identify_intent(self, user_query: str) -> str: prompt = f"你是一个意图识别助手。判断用户意图。\n用户问题: {user_query}\n直接输出意图简述。" response = "" try: - async for chunk in self.llm_model.ainvoke(prompt=prompt, stream=False, temperature=0.1): + invoke_kwargs: dict[str, Any] = {"prompt": prompt, "stream": False} + invoke_kwargs.update(self._temperature_kwargs(0.1)) + async for chunk in self.llm_model.ainvoke(**invoke_kwargs): response += chunk except Exception as exc: - self.logger.error("异步意图识别失败: %s", exc) - return user_query - return response.strip() or user_query + self.logger.error("异步意图识别失败: %s", redact_sensitive_text(str(exc))) + raise RuntimeError("意图识别失败,无法继续检索。") from exc + intent = response.strip() + if not intent: + raise RuntimeError("意图识别返回空结果,无法继续检索。") + return intent - async def retrieve(self, user_input: str, session_config: SessionConfig, console: Any) -> tuple[str, List[Dict[str, Any]]]: + async def retrieve(self, user_input: str, session_config: SessionConfig, console: Any) -> tuple[str, list[dict[str, Any]]]: intent = await self.identify_intent(user_input) documents = await self.retrieval_service.retrieve(intent, session_config, console=console) return intent, documents @@ -35,21 +47,20 @@ async def generate_reply( self, user_input: str, intent: str, - documents: List[Dict[str, Any]], + documents: list[dict[str, Any]], session_config: SessionConfig, ) -> AsyncGenerator[str, None]: prompt = self._build_prompt(user_input, intent, documents) - async for chunk in self.llm_model.ainvoke( - prompt=prompt, - stream=True, - temperature=session_config.chat_temperature, - ): + invoke_kwargs: dict[str, Any] = {"prompt": prompt, "stream": True} + invoke_kwargs.update(self._temperature_kwargs(session_config.chat_temperature)) + async for chunk in self.llm_model.ainvoke(**invoke_kwargs): yield chunk @staticmethod - def _build_prompt(user_input: str, intent: str, documents: List[Dict[str, Any]]) -> str: + def _build_prompt(user_input: str, intent: str, documents: list[dict[str, Any]]) -> str: context = "\n".join( - f"来源: {doc['metadata']['source']}\n内容: {doc.get('page_content', '')}" + f"来源: {(doc.get('metadata') or {}).get('source') or '未知来源'}\n" + f"内容: {doc.get('page_content', '')}" for doc in documents ) if context: diff --git a/src/services/embedding_service.py b/src/services/embedding_service.py index bd8fc03..d876e7b 100644 --- a/src/services/embedding_service.py +++ b/src/services/embedding_service.py @@ -1,11 +1,11 @@ -# -*- coding: utf-8 -*- from __future__ import annotations -import asyncio -from typing import Any, List +import inspect +from typing import Any import numpy as np +from src.providers.__base__.model_provider import close_resource_sync from src.providers.factory import ModelProviderFactory from src.runtime.contracts import RunConfig @@ -25,21 +25,29 @@ def embedding_model_detail(self): def _get_model(self): if self._embedding_model is None: - self._embedding_model = ModelProviderFactory.get_embedding_provider(self.embedding_provider_key) + self._embedding_model = ModelProviderFactory.get_embedding_provider( + self.embedding_provider_key, + self.run_config.embedding_configurations, + ) return self._embedding_model - async def embed_texts(self, texts: List[str]) -> np.ndarray: + async def embed_texts(self, texts: list[str]) -> np.ndarray: model = self._get_model() embeddings = await model.aembed_documents(texts) - return self._as_float32_matrix(embeddings) + return self._as_float32_matrix(embeddings, expected_rows=len(texts)) async def embed_query(self, query: str) -> np.ndarray: - embeddings = await self.embed_texts([query]) - return embeddings[0] + model = self._get_model() + options: dict[str, Any] = {} + if self.embedding_model_detail.provider == "google": + options.setdefault("task_type", "RETRIEVAL_QUERY") + vector = await model.aembed_query(query, **options) + matrix = self._as_float32_matrix([vector], expected_rows=1) + return matrix[0] - async def embed_in_batches(self, texts: List[str]) -> np.ndarray: + async def embed_in_batches(self, texts: list[str]) -> np.ndarray: batch_size = max(1, self.run_config.kb_embedding_batch_size) - batches: List[np.ndarray] = [] + batches: list[np.ndarray] = [] for start in range(0, len(texts), batch_size): batch = texts[start : start + batch_size] batches.append(await self.embed_texts(batch)) @@ -47,11 +55,42 @@ async def embed_in_batches(self, texts: List[str]) -> np.ndarray: return np.empty((0, 0), dtype=np.float32) return np.vstack(batches) + def close(self) -> None: + """释放按需创建的 Embedding Provider。""" + model = self._embedding_model + self._embedding_model = None + if model is None: + return + close_resource_sync(model, "Embedding Provider") + + async def aclose(self) -> None: + """异步释放按需创建的 Embedding Provider。""" + model = self._embedding_model + self._embedding_model = None + if model is None: + return + close = getattr(model, "aclose", None) + if callable(close): + result = close() + if inspect.isawaitable(result): + await result + return + close = getattr(model, "close", None) + if callable(close): + result = close() + if inspect.isawaitable(result): + await result + @staticmethod - def _as_float32_matrix(embeddings: Any) -> np.ndarray: + def _as_float32_matrix(embeddings: Any, expected_rows: int | None = None) -> np.ndarray: matrix = np.array(embeddings, dtype=np.float32) if matrix.ndim != 2: raise ValueError("EmbeddingService 期望二维向量矩阵。") if matrix.shape[0] == 0 or matrix.shape[1] == 0: raise ValueError("EmbeddingService 收到空向量矩阵。") + if expected_rows is not None and matrix.shape[0] != expected_rows: + raise ValueError( + "EmbeddingService 收到的向量数量与输入文本数量不一致。" + f" 输入 {expected_rows} 条,返回 {matrix.shape[0]} 条。" + ) return matrix diff --git a/src/services/knowledge_build_service.py b/src/services/knowledge_build_service.py index 3c84b0e..2a809c5 100644 --- a/src/services/knowledge_build_service.py +++ b/src/services/knowledge_build_service.py @@ -1,9 +1,11 @@ -# -*- coding: utf-8 -*- from __future__ import annotations import hashlib +from collections.abc import Iterable from pathlib import Path -from typing import Dict, Iterable, List +from typing import Any + +import numpy as np from src.etl.pipeline import Pipeline from src.models.document import Document @@ -29,65 +31,87 @@ def __init__( self.embedding_service = embedding_service self.snapshot_repository = snapshot_repository - async def build(self, splitter_structure_mode: str) -> Dict[str, object]: + async def build(self, splitter_structure_mode: str) -> dict[str, object]: markdown_files = sorted(self.run_config.knowledge_base_path.glob("*.md")) if not markdown_files: raise FileNotFoundError(f"知识库目录 '{self.run_config.knowledge_base_path}' 中未找到 Markdown 文件。") snapshot_id = self.snapshot_repository.generate_snapshot_id() temp_dir = self.snapshot_repository.create_temp_snapshot_dir(snapshot_id) - pipeline = Pipeline.from_file_path(markdown_files[0], splitter_structure_mode=splitter_structure_mode) + finalized = False + try: + pipeline = Pipeline.from_file_path(markdown_files[0], splitter_structure_mode=splitter_structure_mode) - chunk_count = 0 - for file_path in markdown_files: - chunks = self._process_file(pipeline, file_path, splitter_structure_mode) - await self._persist_chunks(chunks) - chunk_count += len(chunks) + chunk_count = 0 + pending_documents: list[dict[str, Any]] = [] + pending_embeddings: list[np.ndarray] = [] + pending_parent_documents: dict[str, dict[str, Any]] = {} + for file_path in markdown_files: + chunks = self._process_file(pipeline, file_path) + if splitter_structure_mode == "hierarchical" and hasattr(pipeline.splitter, "parent_documents"): + parent_documents = getattr(pipeline.splitter, "parent_documents", {}) + if isinstance(parent_documents, dict): + pending_parent_documents.update(parent_documents) + if chunks: + documents = [ + {"page_content": chunk.content, "metadata": chunk.metadata} + for chunk in chunks + ] + texts = [chunk.content for chunk in chunks] + pending_documents.extend(documents) + pending_embeddings.append(await self.embedding_service.embed_in_batches(texts)) + chunk_count += len(chunks) - manifest = KnowledgeSnapshotManifest.create( - snapshot_id=snapshot_id, - store_type=self.run_config.default_vector_store, - embedding_provider=self.embedding_service.embedding_provider_key, - embedding_model=self.embedding_service.embedding_model_detail.model_name, - chunk_mode=splitter_structure_mode, - source_digest=self._compute_source_digest(markdown_files), - document_count=len(markdown_files), - chunk_count=chunk_count, - ) - self.vector_store.save_snapshot(str(temp_dir)) - self.snapshot_repository.write_manifest(temp_dir, manifest) - self.snapshot_repository.finalize_snapshot(temp_dir, snapshot_id) - final_dir = self.snapshot_repository.get_active_snapshot_dir() - if final_dir is None: - raise RuntimeError("活动知识快照切换失败。") - self.snapshot_repository.validate_snapshot_dir(final_dir) - return { - "snapshot_id": snapshot_id, - "snapshot_dir": str(final_dir), - "document_count": len(markdown_files), - "chunk_count": chunk_count, - "embedding_provider": self.embedding_service.embedding_provider_key, - "embedding_model": self.embedding_service.embedding_model_detail.model_name, - "chunk_mode": splitter_structure_mode, - } + if pending_documents: + embeddings = np.vstack(pending_embeddings) + if len(pending_documents) != len(embeddings): + raise ValueError("知识快照中的文档数量与 embedding 数量不一致。") + self.vector_store.upsert_embeddings(pending_documents, embeddings) + if pending_parent_documents: + self.vector_store.register_parent_documents(pending_parent_documents) - def _process_file(self, pipeline: Pipeline, file_path: Path, splitter_structure_mode: str) -> List[Document]: + manifest = KnowledgeSnapshotManifest.create( + snapshot_id=snapshot_id, + store_type=self.run_config.default_vector_store, + embedding_provider=self.embedding_service.embedding_provider_key, + embedding_model=self.embedding_service.embedding_model_detail.model_name, + chunk_mode=splitter_structure_mode, + source_digest=self._compute_source_digest(markdown_files), + document_count=len(markdown_files), + chunk_count=chunk_count, + ) + self.vector_store.save_snapshot(str(temp_dir)) + self.snapshot_repository.write_manifest(temp_dir, manifest) + self.snapshot_repository.validate_snapshot_dir(temp_dir) + self.snapshot_repository.finalize_snapshot(temp_dir, snapshot_id) + final_dir = self.snapshot_repository.get_active_snapshot_dir() + if final_dir is None: + raise RuntimeError("活动知识快照切换失败。") + self.snapshot_repository.validate_snapshot_dir(final_dir) + finalized = True + return { + "snapshot_id": snapshot_id, + "snapshot_dir": str(final_dir), + "document_count": len(markdown_files), + "chunk_count": chunk_count, + "embedding_provider": self.embedding_service.embedding_provider_key, + "embedding_model": self.embedding_service.embedding_model_detail.model_name, + "chunk_mode": splitter_structure_mode, + } + finally: + if not finalized: + try: + self.snapshot_repository.cleanup_temp_snapshot_dir(snapshot_id) + except Exception: + logger.exception("清理临时知识快照失败: %s", temp_dir) + + def _process_file(self, pipeline: Pipeline, file_path: Path) -> list[Document]: logger.info("正在处理文件: %s", file_path) content = file_path.read_text(encoding="utf-8") document = Document(content=content, metadata={"source": str(file_path)}) chunks = pipeline.process(document) - if splitter_structure_mode == "hierarchical" and hasattr(pipeline.splitter, "parent_documents"): - self.vector_store.register_parent_documents(getattr(pipeline.splitter, "parent_documents", {})) return chunks - async def _persist_chunks(self, chunks: List[Document]) -> None: - if not chunks: - return - documents = [{"page_content": chunk.content, "metadata": chunk.metadata} for chunk in chunks] - texts = [doc["page_content"] for doc in documents] - embeddings = await self.embedding_service.embed_in_batches(texts) - self.vector_store.upsert_embeddings(documents, embeddings) - @staticmethod def _compute_source_digest(markdown_files: Iterable[Path]) -> str: digest = hashlib.sha256() diff --git a/src/services/retrieval_service.py b/src/services/retrieval_service.py index bc530d2..740365f 100644 --- a/src/services/retrieval_service.py +++ b/src/services/retrieval_service.py @@ -1,11 +1,16 @@ -# -*- coding: utf-8 -*- from __future__ import annotations +import asyncio import copy -from typing import Any, Callable, Dict, List, Optional +import inspect +import json +import math +from collections.abc import Callable, Mapping +from typing import Any from rich.console import Console +from src.providers.__base__.model_provider import RerankModel, close_resource_sync from src.providers.factory import ModelProviderFactory from src.retrieval.vdb.base import VectorStoreBase from src.runtime.contracts import SessionConfig @@ -27,7 +32,7 @@ def __init__(self, vector_weight: float, keyword_weight: float, fusion_strategy: raise ValueError(f"不支持的混合检索融合策略: {fusion_strategy}") @staticmethod - def _normalize_scores(scores: List[float]) -> List[float]: + def _normalize_scores(scores: list[float]) -> list[float]: if not scores: return [] max_score = max(scores) @@ -37,7 +42,7 @@ def _normalize_scores(scores: List[float]) -> List[float]: score_range = max_score - min_score return [max(0.0, min(1.0, (score - min_score) / score_range)) for score in scores] - def rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def rerank(self, documents: list[dict[str, Any]]) -> list[dict[str, Any]]: if self.fusion_strategy == "weighted": return self._weighted_rerank(documents) return self._rrf_rerank(documents) @@ -48,7 +53,7 @@ def _weight_tuple(self) -> tuple[float, float]: return 0.0, 0.0 return self.vector_weight / total, self.keyword_weight / total - def _weighted_rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _weighted_rerank(self, documents: list[dict[str, Any]]) -> list[dict[str, Any]]: vector_weight, keyword_weight = self._weight_tuple() scored_documents = [copy.deepcopy(doc) for doc in documents] keyword_scores = [float(doc.get("keyword_score", 0) or 0) for doc in scored_documents] @@ -59,7 +64,7 @@ def _weighted_rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, An doc["score"] = vector_weight * normalized_semantic[index] + keyword_weight * normalized_keyword[index] return sorted(scored_documents, key=lambda item: item["score"], reverse=True) - def _rrf_rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + def _rrf_rerank(self, documents: list[dict[str, Any]]) -> list[dict[str, Any]]: vector_weight, keyword_weight = self._weight_tuple() scored_documents = [copy.deepcopy(doc) for doc in documents] for doc in scored_documents: @@ -71,7 +76,7 @@ def _rrf_rerank(self, documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: return sorted(scored_documents, key=lambda item: item["score"], reverse=True) -def _document_key(document: Dict[str, Any]) -> str: +def _document_key(document: dict[str, Any]) -> str: metadata = document.get("metadata") or {} chunk_id = metadata.get("chunk_id") or metadata.get("doc_id") if chunk_id: @@ -83,8 +88,8 @@ def _document_key(document: Dict[str, Any]) -> str: return f"{source}:{document.get('page_content', '')[:64]}" -def _merge_hybrid_results(semantic_results: List[Dict[str, Any]], keyword_results: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - merged: Dict[str, Dict[str, Any]] = {} +def _merge_hybrid_results(semantic_results: list[dict[str, Any]], keyword_results: list[dict[str, Any]]) -> list[dict[str, Any]]: + merged: dict[str, dict[str, Any]] = {} for rank, document in enumerate(semantic_results, start=1): merged_doc = copy.deepcopy(document) merged_doc["semantic_score"] = float(merged_doc.get("score", 0) or 0) @@ -105,8 +110,8 @@ def _merge_hybrid_results(semantic_results: List[Dict[str, Any]], keyword_result return list(merged.values()) -def _promote_parent_context(documents: List[Dict[str, Any]], resolver: Optional[Callable[[str | None], Optional[str]]]) -> List[Dict[str, Any]]: - promoted_documents: List[Dict[str, Any]] = [] +def _promote_parent_context(documents: list[dict[str, Any]], resolver: Callable[[str | None], str | None] | None) -> list[dict[str, Any]]: + promoted_documents: list[dict[str, Any]] = [] for document in documents: promoted_doc = copy.deepcopy(document) metadata = promoted_doc.get("metadata") or {} @@ -119,8 +124,8 @@ def _promote_parent_context(documents: List[Dict[str, Any]], resolver: Optional[ return promoted_documents -def _deduplicate_parent_documents(documents: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - deduplicated: Dict[str, Dict[str, Any]] = {} +def _deduplicate_parent_documents(documents: list[dict[str, Any]]) -> list[dict[str, Any]]: + deduplicated: dict[str, dict[str, Any]] = {} for document in documents: metadata = document.get("metadata") or {} parent_key = metadata.get("parent_id") @@ -135,8 +140,9 @@ class RetrievalService: def __init__(self, vector_store: VectorStoreBase, embedding_service: EmbeddingService): self.vector_store = vector_store self.embedding_service = embedding_service + self._rerank_providers: dict[tuple[str, str, str, str], RerankModel] = {} - async def retrieve(self, query: str, session_config: SessionConfig, console: Optional[Console] = None) -> List[Dict[str, Any]]: + async def retrieve(self, query: str, session_config: SessionConfig, console: Console | None = None) -> list[dict[str, Any]]: retrieval_method = session_config.retrieval_method effective_top_k = max(1, session_config.top_k) * max(1, session_config.retrieval_candidate_multiplier) parent_resolver = getattr(self.vector_store, "resolve_parent_content", None) @@ -160,56 +166,213 @@ async def retrieve(self, query: str, session_config: SessionConfig, console: Opt ranked_results = _promote_parent_context(ranked_results, parent_resolver) ranked_results = _deduplicate_parent_documents(ranked_results) ranked_results = await self._rerank_if_needed(query, ranked_results, session_config) - return ranked_results[: session_config.top_k] + return ranked_results[: max(1, session_config.top_k)] - async def _gather_hybrid_results(self, query: str, top_k: int) -> tuple[List[Dict[str, Any]], List[Dict[str, Any]]]: - semantic_results = await self._semantic_retrieve(query, top_k) - keyword_results = await self._keyword_retrieve(query, top_k) + async def _gather_hybrid_results(self, query: str, top_k: int) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]: + semantic_results, keyword_results = await asyncio.gather( + self._semantic_retrieve(query, top_k), + self._keyword_retrieve(query, top_k), + ) return semantic_results, keyword_results - async def _semantic_retrieve(self, query: str, top_k: int) -> List[Dict[str, Any]]: - try: - query_embedding = await self.embedding_service.embed_query(query) - return await self._call_vector_store("semantic_search", query_embedding, top_k) - except (AttributeError, NotImplementedError): + async def _semantic_retrieve(self, query: str, top_k: int) -> list[dict[str, Any]]: + # Embedding failures must propagate. Only use the legacy path when the + # vector store truly does not expose the newer semantic_search method; + # an AttributeError/NotImplementedError raised inside a real provider is + # a defect or an upstream failure, not a compatibility signal. + if self._get_vector_store_method("semantic_search") is None: return await self._legacy_search(query, top_k, "semantic") + query_embedding = await self.embedding_service.embed_query(query) + return await self._call_vector_store("semantic_search", query_embedding, top_k) - async def _keyword_retrieve(self, query: str, top_k: int) -> List[Dict[str, Any]]: - try: - return await self._call_vector_store("keyword_search", query, top_k) - except (AttributeError, NotImplementedError): + async def _keyword_retrieve(self, query: str, top_k: int) -> list[dict[str, Any]]: + if self._get_vector_store_method("keyword_search") is None: return await self._legacy_search(query, top_k, "keyword") - - async def _call_vector_store(self, method_name: str, *args) -> List[Dict[str, Any]]: - import asyncio - - method = getattr(self.vector_store, method_name) + return await self._call_vector_store("keyword_search", query, top_k) + + def _get_vector_store_method(self, method_name: str) -> Callable[..., Any] | None: + """Return an implemented vector-store method, excluding base stubs.""" + method = getattr(self.vector_store, method_name, None) + if not callable(method): + return None + implementation = getattr(type(self.vector_store), method_name, None) + base_implementation = getattr(VectorStoreBase, method_name, None) + if implementation is base_implementation: + return None + return method + + async def _call_vector_store(self, method_name: str, *args) -> list[dict[str, Any]]: + method = self._get_vector_store_method(method_name) + if method is None: + raise NotImplementedError(f"向量存储未实现 {method_name} 接口。") return await asyncio.to_thread(method, *args) - async def _legacy_search(self, query: str, top_k: int, search_type: str) -> List[Dict[str, Any]]: - import asyncio - - if hasattr(self.vector_store, "asearch"): - return await self.vector_store.asearch(query, top_k, search_type=search_type) - return await asyncio.to_thread(self.vector_store.search, query, top_k, search_type) - - async def _rerank_if_needed(self, query: str, ranked_results: List[Dict[str, Any]], session_config: SessionConfig) -> List[Dict[str, Any]]: + async def _legacy_search(self, query: str, top_k: int, search_type: str) -> list[dict[str, Any]]: + async_search = getattr(self.vector_store, "asearch", None) + if callable(async_search): + return await async_search(query, top_k, search_type=search_type) + sync_search = getattr(self.vector_store, "search", None) + if not callable(sync_search): + raise NotImplementedError("向量存储未实现兼容 search 接口。") + return await asyncio.to_thread(sync_search, query, top_k, search_type) + + def _get_rerank_provider( + self, + provider_key: str, + configurations: Mapping[str, Any] | None = None, + ) -> RerankModel: + detail = configurations.get(provider_key) if configurations is not None else None + options = getattr(detail, "options", {}) if detail is not None else {} + try: + options_fingerprint = json.dumps( + options or {}, sort_keys=True, default=str, separators=(",", ":") + ) + except (TypeError, ValueError): + options_fingerprint = repr(options) + cache_key = ( + provider_key, + getattr(detail, "provider", ""), + getattr(detail, "model_name", ""), + options_fingerprint, + ) + provider = self._rerank_providers.get(cache_key) + if provider is None: + # A configuration reload can keep the same key while changing the + # endpoint, model or options. Drop the old instance before creating + # its replacement so stale transports and credentials are not kept + # alive indefinitely. + stale_keys = [ + key for key in self._rerank_providers + if key[0] == provider_key and key != cache_key + ] + for stale_key in stale_keys: + stale_provider = self._rerank_providers.pop(stale_key) + close_resource_sync(stale_provider, "Rerank Provider") + if configurations is None: + provider = ModelProviderFactory.get_rerank_provider(provider_key) + else: + provider = ModelProviderFactory.get_rerank_provider(provider_key, configurations) + self._rerank_providers[cache_key] = provider + return provider + + def clear_rerank_cache(self) -> None: + """关闭并清空已缓存的 Rerank Provider。""" + providers = list(self._rerank_providers.values()) + self._rerank_providers.clear() + closed: set[int] = set() + for provider in providers: + if id(provider) in closed: + continue + closed.add(id(provider)) + close_resource_sync(provider, "Rerank Provider") + + def close(self) -> None: + """释放重排缓存及 EmbeddingService 持有的 Provider。""" + self.clear_rerank_cache() + close_embedding = getattr(self.embedding_service, "close", None) + if callable(close_embedding): + close_embedding() + + async def aclose(self) -> None: + """异步释放重排缓存及 EmbeddingService 持有的 Provider。""" + providers = list(self._rerank_providers.values()) + self._rerank_providers.clear() + closed: set[int] = set() + for provider in providers: + if id(provider) in closed: + continue + closed.add(id(provider)) + close = getattr(provider, "aclose", None) + if callable(close): + result = close() + if inspect.isawaitable(result): + await result + continue + close = getattr(provider, "close", None) + if callable(close): + result = close() + if inspect.isawaitable(result): + await result + close_embedding = getattr(self.embedding_service, "aclose", None) + if not callable(close_embedding): + close_embedding = getattr(self.embedding_service, "close", None) + if callable(close_embedding): + result = close_embedding() + if inspect.isawaitable(result): + await result + + async def _rerank_if_needed(self, query: str, ranked_results: list[dict[str, Any]], session_config: SessionConfig) -> list[dict[str, Any]]: if not session_config.rerank_enabled or not ranked_results: return ranked_results - rerank_provider = ModelProviderFactory.get_rerank_provider(session_config.active_rerank_configuration) + rerank_provider = self._get_rerank_provider( + session_config.active_rerank_configuration, + session_config.rerank_configurations, + ) docs_to_rerank = [doc.get("page_content", "") for doc in ranked_results] - reranked_indices, reranked_scores = await rerank_provider.arerank(query, docs_to_rerank, top_n=session_config.top_k) - reranked_docs: List[Dict[str, Any]] = [] + # Keep the requested output bounded by the available candidates after + # thresholding and parent de-duplication. + rerank_top_n = max(1, min(session_config.top_k, len(docs_to_rerank))) + reranked_indices, reranked_scores = await rerank_provider.arerank( + query, docs_to_rerank, top_n=rerank_top_n + ) + reranked_indices, reranked_scores = self._validate_rerank_output( + reranked_indices, + reranked_scores, + document_count=len(ranked_results), + ) + reranked_docs: list[dict[str, Any]] = [] for index, score in zip(reranked_indices, reranked_scores): - if index < len(ranked_results): - document = copy.deepcopy(ranked_results[index]) - document["score"] = score - reranked_docs.append(document) + document = copy.deepcopy(ranked_results[index]) + document["score"] = score + reranked_docs.append(document) return sorted(reranked_docs, key=lambda item: item["score"], reverse=True) + @staticmethod + def _validate_rerank_output( + indices: Any, + scores: Any, + document_count: int, + ) -> tuple[list[int], list[float]]: + """Validate a provider result before it can alter retrieval ordering.""" + try: + normalized_indices = list(indices) + normalized_scores = list(scores) + except TypeError as exc: + raise RuntimeError("Rerank provider 返回值必须是可迭代的索引和分数。") from exc + + if len(normalized_indices) != len(normalized_scores): + raise RuntimeError("Rerank provider 返回的索引和分数数量不一致。") + if not normalized_indices: + raise RuntimeError("Rerank provider 未返回可用结果。") + + seen_indices: set[int] = set() + validated_scores: list[float] = [] + for position, (index, score) in enumerate(zip(normalized_indices, normalized_scores)): + if ( + isinstance(index, bool) + or not isinstance(index, int) + or not 0 <= index < document_count + ): + raise RuntimeError(f"Rerank provider 返回的 index 无效: position={position}。") + if index in seen_indices: + raise RuntimeError(f"Rerank provider 返回重复 index: {index}。") + seen_indices.add(index) + if isinstance(score, bool): + raise RuntimeError(f"Rerank provider 返回的 score 无效: index={index}。") + try: + normalized_score = float(score) + except (TypeError, ValueError) as exc: + raise RuntimeError(f"Rerank provider 返回的 score 无效: index={index}。") from exc + if not math.isfinite(normalized_score): + raise RuntimeError(f"Rerank provider 返回的 score 必须是有限数值: index={index}。") + validated_scores.append(normalized_score) + + return normalized_indices, validated_scores + @staticmethod def _should_apply_score_threshold(session_config: SessionConfig) -> bool: - if session_config.retrieval_method == RetrievalMethod.HYBRID_SEARCH and session_config.hybrid_fusion_strategy == "rrf": - return False - return True + return not ( + session_config.retrieval_method == RetrievalMethod.HYBRID_SEARCH + and session_config.hybrid_fusion_strategy == "rrf" + ) diff --git a/src/ui/config_menu.py b/src/ui/config_menu.py index 6dfa5a5..a00d54b 100644 --- a/src/ui/config_menu.py +++ b/src/ui/config_menu.py @@ -1,17 +1,17 @@ -# -*- coding: utf-8 -*- +from typing import Any + import questionary from rich.console import Console from rich.panel import Panel -from typing import Dict, Any, Tuple -from ..utils.config import get_settings, RetrievalMethod # 导入 get_settings 函数 +from ..utils.config import RetrievalMethod +from ..utils.log_manager import get_module_logger # 导入日志管理器 from .display_utils import display_chat_config -from ..utils.log_manager import get_module_logger # 导入日志管理器 logger = get_module_logger(__name__) # 获取当前模块的日志器 console = Console() -def edit_retrieval_params(chat_config: Dict[str, Any]) -> None: +def edit_retrieval_params(chat_config: dict[str, Any]) -> None: """编辑检索相关参数。""" logger.info("进入检索参数编辑菜单。") while True: @@ -132,9 +132,8 @@ def is_float_between_0_and_1(text): console.print(f"[green]候选过量招募倍率已更新为: {new_multiplier}[/green]") logger.info(f"候选过量招募倍率已更新为: {new_multiplier}") # 检索参数的更改不需要重载任何模型 - return None -def edit_model_params(chat_config: Dict[str, Any]) -> bool: +def edit_model_params(chat_config: dict[str, Any]) -> bool: """编辑模型相关参数。""" logger.info("进入模型参数编辑菜单。") llm_changed = False @@ -185,7 +184,7 @@ def edit_model_params(chat_config: Dict[str, Any]) -> bool: return llm_changed -def launch_config_editor(chat_config: Dict[str, Any]) -> Tuple[bool, Dict[str, Any]]: +def launch_config_editor(chat_config: dict[str, Any]) -> tuple[bool, dict[str, Any]]: """ 启动交互式配置编辑器。 返回一个元组 (llm_needs_reload, updated_config) diff --git a/src/ui/display_utils.py b/src/ui/display_utils.py index 24edb77..21b5117 100644 --- a/src/ui/display_utils.py +++ b/src/ui/display_utils.py @@ -1,12 +1,12 @@ -# -*- coding: utf-8 -*- import os -from typing import Optional, Dict, Any +from typing import Any + from rich.console import Console, Group from rich.panel import Panel from rich.table import Table -from ..utils.config import get_settings, ROOT_DIR # 导入 get_settings 函数 -from ..utils.log_manager import get_module_logger # 导入日志管理器 +from ..utils.config import ROOT_DIR, get_settings # 导入 get_settings 函数 +from ..utils.log_manager import get_module_logger # 导入日志管理器 logger = get_module_logger(__name__) # 获取当前模块的日志器 @@ -25,15 +25,15 @@ def get_relative_path(absolute_path: str) -> str: logger.warning(f"无法将路径 '{absolute_path}' 转换为相对路径,返回原始路径。") return str(absolute_path) -def display_chat_config(console: Console, chat_config: Dict[str, Any]): +def display_chat_config(console: Console, chat_config: dict[str, Any]): """显示从传入的chat_config字典中获取的聊天机器人配置。""" logger.info("正在显示聊天机器人配置。") - def mask_api_key(key: Optional[str]) -> str: + def mask_api_key(key: str | None) -> str: if not key: logger.debug("API Key 未设置,显示为 '[dim]未设置[/dim]'。") return "[dim]未设置[/dim]" - logger.debug("API Key 已设置,显示部分掩码。") - return f"{key[:10]}..." + logger.debug("API Key 已设置,隐藏全部值。") + return "[dim]已设置(值已隐藏)[/dim]" # --- 聊天配置 --- chat_table = Table(title="[bold green]聊天机器人配置[/bold green]", show_header=False, box=None, padding=(0, 1)) diff --git a/src/utils/cleanup.py b/src/utils/cleanup.py index e132118..60a27e3 100644 --- a/src/utils/cleanup.py +++ b/src/utils/cleanup.py @@ -1,10 +1,10 @@ -# -*- coding: utf-8 -*- -import os -import glob import atexit +import os import shutil -from .config import get_settings # 导入 get_settings 函数 -from .log_manager import get_module_logger # 导入日志管理器 + +from .config import get_settings # 导入 get_settings 函数 +from .log_manager import get_module_logger # 导入日志管理器 +from .security import redact_sensitive_text logger = get_module_logger(__name__) # 获取当前模块的日志器 @@ -23,9 +23,14 @@ def cleanup_temp_files(): shutil.rmtree(cache_dir) logger.info(f"已成功删除缓存目录: {cache_dir}") except OSError as e: - logger.error(f"删除缓存目录 {cache_dir} 时出错: {e}", exc_info=True) + error_text = redact_sensitive_text(str(e)) + logger.exception( + "删除缓存目录 %s 时出错: %s", + cache_dir, + error_text, + ) else: logger.info("未找到 .cache 目录,无需清理。") # 注册函数,使其在程序正常退出时被调用 -atexit.register(cleanup_temp_files) \ No newline at end of file +atexit.register(cleanup_temp_files) diff --git a/src/utils/config.py b/src/utils/config.py index b9ada26..16068e8 100644 --- a/src/utils/config.py +++ b/src/utils/config.py @@ -1,14 +1,21 @@ -# -*- coding: utf-8 -*- import functools import sys import tomllib +import warnings +from collections.abc import Callable, Mapping from enum import Enum from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple +from typing import Any, ClassVar, cast -from pydantic import BaseModel, Field, field_validator, ValidationError +from pydantic import BaseModel, Field, ValidationInfo, field_validator, model_validator from pydantic.fields import FieldInfo -from pydantic_settings import BaseSettings, SettingsConfigDict, PydanticBaseSettingsSource +from pydantic_settings import ( + BaseSettings, + PydanticBaseSettingsSource, + SettingsConfigDict, +) + +from src.utils.security import validate_secret_free_options # ================================================================= # 1. 基础定义 (DEFINITIONS) @@ -37,12 +44,84 @@ class RetrievalMethod(str, Enum): FULL_TEXT_SEARCH = "全文检索" HYBRID_SEARCH = "混合检索" + +class ModelProtocol(str, Enum): + """模型渠道使用的线协议。""" + + CHAT_COMPLETIONS = "chat_completions" + RESPONSES = "responses" + MESSAGES = "messages" + GENERATE_CONTENT = "generate_content" + ARK = "ark" + + class ModelDetail(BaseModel): """定义单个模型配置的结构。""" provider: str model_name: str + protocol: ModelProtocol | None = None + options: dict[str, Any] = Field(default_factory=dict) - model_config = SettingsConfigDict(protected_namespaces=()) + model_config = SettingsConfigDict( + protected_namespaces=(), + extra="forbid", + ) + + @field_validator("options", mode="before") + @classmethod + def validate_options(cls, value: Any, info: ValidationInfo) -> dict[str, Any]: + if value is None: + return {} + if not isinstance(value, Mapping): + raise ValueError("模型 options 必须是 TOML 表或字典。") + provider = str(info.data.get("provider", "")).strip().lower() + try: + if provider == "google" and "http_options" in value: + http_options = value["http_options"] + remaining = { + key: nested + for key, nested in value.items() + if key != "http_options" + } + validated = validate_secret_free_options(remaining, "模型") + validated["http_options"] = validate_secret_free_options( + {"http_options": http_options}, + "模型", + allowed_containers={"http_options", "headers"}, + )["http_options"] + return validated + return validate_secret_free_options(value, "模型") + except ValueError as exc: + raise ValueError(str(exc).replace("模型 options 不允许包含凭证或请求头/query 配置", "模型 options 不允许包含凭证或连接字段")) from exc + + @field_validator("protocol", mode="before") + @classmethod + def normalize_protocol(cls, value: Any) -> ModelProtocol | None: + if value is None or value == "": + return None + if isinstance(value, ModelProtocol): + return value + + normalized = str(value).strip().lower().replace("-", "_") + aliases = { + "chat": ModelProtocol.CHAT_COMPLETIONS, + "completion": ModelProtocol.CHAT_COMPLETIONS, + "chat_completion": ModelProtocol.CHAT_COMPLETIONS, + "chat_completions": ModelProtocol.CHAT_COMPLETIONS, + "response": ModelProtocol.RESPONSES, + "responses": ModelProtocol.RESPONSES, + "anthropic": ModelProtocol.MESSAGES, + "anthropic_messages": ModelProtocol.MESSAGES, + "gemini": ModelProtocol.GENERATE_CONTENT, + "generate": ModelProtocol.GENERATE_CONTENT, + "generate_content": ModelProtocol.GENERATE_CONTENT, + } + try: + alias = aliases.get(normalized) + return alias if alias is not None else ModelProtocol(normalized) + except ValueError as exc: + supported = ", ".join(protocol.value for protocol in ModelProtocol) + raise ValueError(f"不支持的模型协议: {value}。可选值: {supported}") from exc # ================================================================= # 2. 主配置模型 (MAIN SETTINGS MODEL) @@ -54,26 +133,68 @@ class Settings(BaseSettings): 加载顺序: 环境变量 > .env 文件 > config.toml 文件 > 模型中定义的默认值。 """ # --- [API_KEYS] --- - anthropic_api_key: Optional[str] = None - google_api_key: Optional[str] = None - siliconflow_api_key: Optional[str] = None - openai_api_key: Optional[str] = None - qwen_api_key: Optional[str] = None - volc_access_key: Optional[str] = None - volc_secret_key: Optional[str] = None - jina_api_key: Optional[str] = None - deepseek_api_key: Optional[str] = None - grok_api_key: Optional[str] = None - lm_studio_api_key: Optional[str] = "lm-studio" + anthropic_api_key: str | None = Field(default=None, repr=False) + google_api_key: str | None = Field(default=None, repr=False) + gemini_api_key: str | None = Field(default=None, repr=False) + google_genai_use_vertexai: bool | None = Field(default=None) + google_genai_use_enterprise: bool | None = Field(default=None) + google_cloud_project: str | None = Field(default=None) + google_cloud_location: str | None = Field(default=None) + google_application_credentials: str | None = Field(default=None, repr=False) + siliconflow_api_key: str | None = Field(default=None, repr=False) + openai_api_key: str | None = Field(default=None, repr=False) + qwen_api_key: str | None = Field(default=None, repr=False) + ark_api_key: str | None = Field(default=None, repr=False) + volc_access_key: str | None = Field(default=None, repr=False) + volc_secret_key: str | None = Field(default=None, repr=False) + jina_api_key: str | None = Field(default=None, repr=False) + deepseek_api_key: str | None = Field(default=None, repr=False) + grok_api_key: str | None = Field(default=None, repr=False) + lm_studio_api_key: str | None = Field(default=None, repr=False) + + _secret_fields: ClassVar[frozenset[str]] = frozenset( + { + "anthropic_api_key", "google_api_key", "siliconflow_api_key", + "gemini_api_key", + "google_application_credentials", + "openai_api_key", "qwen_api_key", "ark_api_key", "volc_access_key", + "volc_secret_key", "jina_api_key", "deepseek_api_key", "grok_api_key", + "lm_studio_api_key", + } + ) + + def _safe_dump_exclude(self, exclude: Any, include_secrets: bool) -> Any: + if include_secrets: + return exclude + secret_exclude = set(self._secret_fields) + if exclude is None: + return secret_exclude + if isinstance(exclude, Mapping): + return {**exclude, **{name: True for name in secret_exclude}} + return set(exclude) | secret_exclude + + def model_dump(self, *args: Any, **kwargs: Any) -> dict[str, Any]: + include_secrets = bool(kwargs.pop("include_secrets", False)) + exclude = self._safe_dump_exclude(kwargs.pop("exclude", None), include_secrets) + if exclude is not None: + kwargs["exclude"] = exclude + return super().model_dump(*args, **kwargs) + + def model_dump_json(self, *args: Any, **kwargs: Any) -> str: + include_secrets = bool(kwargs.pop("include_secrets", False)) + exclude = self._safe_dump_exclude(kwargs.pop("exclude", None), include_secrets) + if exclude is not None: + kwargs["exclude"] = exclude + return super().model_dump_json(*args, **kwargs) # --- [BASE_URLS] --- openai_api_base: str = "https://api.openai.com/v1" siliconflow_base_url: str = "https://api.siliconflow.cn/v1" - qwen_base_url: str = "https://dashscope.aliyuncs.com/api/v1" + qwen_base_url: str = "https://dashscope.aliyuncs.com/compatible-mode/v1" deepseek_base_url: str = "https://api.deepseek.com" ollama_base_url: str = "http://localhost:11434/v1" lm_studio_base_url: str = "http://localhost:1234/v1" - volc_base_url: str = "https://maas-api.ml-platform-cn-beijing.volces.com" + volc_base_url: str = "https://ark.cn-beijing.volces.com/api/v3" grok_base_url: str = "https://api.x.ai/v1" # --- [GENERAL] --- @@ -92,7 +213,7 @@ class Settings(BaseSettings): kb_remove_spaces: bool = False kb_remove_urls: bool = False kb_use_qa_segmentation: bool = False - kb_splitter_separators: List[str] = Field(default_factory=lambda: ["###"]) + kb_splitter_separators: list[str] = Field(default_factory=lambda: ["###"]) kb_chunk_size: int = 1500 kb_chunk_overlap: int = 150 kb_child_chunk_size: int = 300 @@ -117,31 +238,46 @@ class Settings(BaseSettings): chat_temperature: float = 0.7 # 将 chat_temperature 移到这里 # --- [MODEL_CONFIGURATIONS] --- - embedding_configurations: Dict[str, ModelDetail] = Field(default_factory=lambda: { + embedding_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { + # 与 llm_configurations 同理:兜底值必须写成当前有效的官方 ID。 "local-hash": ModelDetail(provider="local-hash", model_name="local-hash-256"), - "google": ModelDetail(provider="google", model_name="embedding-001"), - "jina": ModelDetail(provider="jina", model_name="jina-embeddings-v2-base-zh"), - "siliconflow": ModelDetail(provider="siliconflow", model_name="alibaba/bge-large-zh-v1.5"), + "google": ModelDetail(provider="google", model_name="gemini-embedding-2"), + "siliconflow": ModelDetail(provider="siliconflow", model_name="BAAI/bge-large-zh-v1.5"), "openai": ModelDetail(provider="openai", model_name="text-embedding-3-small"), }) - rerank_configurations: Dict[str, ModelDetail] = Field(default_factory=lambda: { - "siliconflow": ModelDetail(provider="siliconflow", model_name="alibaba/bge-reranker-large"), + rerank_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { + "siliconflow": ModelDetail(provider="siliconflow", model_name="BAAI/bge-reranker-v2-m3"), }) - llm_configurations: Dict[str, ModelDetail] = Field(default_factory=lambda: { - "google": ModelDetail(provider="google", model_name="gemini-1.5-pro-latest"), - "anthropic": ModelDetail(provider="anthropic", model_name="claude-3-opus-20240229"), - "qwen": ModelDetail(provider="qwen", model_name="qwen-turbo"), - "deepseek": ModelDetail(provider="deepseek", model_name="deepseek-chat"), - "grok": ModelDetail(provider="grok", model_name="grok-1"), - "volcengine": ModelDetail(provider="volcengine", model_name="Doubao-pro-32k"), - "siliconflow": ModelDetail(provider="siliconflow", model_name="deepseek-ai/DeepSeek-V2-Chat"), - "openai": ModelDetail(provider="openai", model_name="gpt-4o"), - "iflow-qwen3-max": ModelDetail(provider="openai", model_name="qwen3-max"), - "ollama": ModelDetail(provider="ollama", model_name="llama3"), + llm_configurations: dict[str, ModelDetail] = Field(default_factory=lambda: { + # 默认条目只作为缺失配置时的兜底;模型名保持在写就时仍可用的现行 ID, + # 避免新用户照抄到已退役模型。 + "google": ModelDetail(provider="google", model_name="gemini-2.5-flash"), + "anthropic": ModelDetail(provider="anthropic", model_name="claude-sonnet-4-6"), + "qwen": ModelDetail(provider="qwen", model_name="qwen3.8-max"), + "deepseek": ModelDetail(provider="deepseek", model_name="deepseek-v4-pro"), + "grok": ModelDetail(provider="grok", model_name="grok-4.6"), + "volcengine": ModelDetail(provider="volcengine", model_name="doubao-seed-2-0-lite-260428"), + "siliconflow": ModelDetail(provider="siliconflow", model_name="deepseek-ai/DeepSeek-V3.2"), + "openai": ModelDetail(provider="openai", model_name="gpt-5.6-sol"), + "ollama": ModelDetail(provider="ollama", model_name="llama3.1"), "lm-studio": ModelDetail(provider="lm-studio", model_name="LM-Studio-Community/Meta-Llama-3-8B-Instruct-GGUF"), }) # --- [VALIDATORS] --- + @field_validator("chat_top_k") + @classmethod + def validate_chat_top_k(cls, value: int) -> int: + if isinstance(value, bool) or value < 1: + raise ValueError("chat_top_k 必须是大于等于 1 的整数。") + return value + + @field_validator("chat_score_threshold") + @classmethod + def validate_chat_score_threshold(cls, value: float) -> float: + if isinstance(value, bool) or not 0 <= value <= 1: + raise ValueError("chat_score_threshold 必须在 0 到 1 之间。") + return value + @field_validator('log_level', mode='before') @classmethod def validate_log_level(cls, v: str) -> str: @@ -151,14 +287,33 @@ def validate_log_level(cls, v: str) -> str: raise ValueError(f"无效的日志级别: {v}. 必须是 {', '.join(valid_levels)} 中的一个。") return v.upper() + @field_validator("embedding_configurations", "rerank_configurations") + @classmethod + def validate_non_llm_protocol( + cls, + value: dict[str, ModelDetail], + info: ValidationInfo, + ) -> dict[str, ModelDetail]: + """Embedding/Rerank 配置不允许携带 LLM 线协议。""" + invalid = sorted( + key for key, detail in value.items() if detail.protocol is not None + ) + if invalid: + role = "Embedding" if info.field_name == "embedding_configurations" else "Rerank" + raise ValueError( + f"{role} 配置不支持 protocol;协议只能配置在 llm_configurations 中。" + f" 无效条目: {', '.join(invalid)}" + ) + return value + @field_validator('chat_temperature', mode='before') @classmethod def validate_chat_temperature(cls, v: Any) -> float: """验证聊天温度在 0.0 到 1.0 之间。""" try: value = float(v) - except (ValueError, TypeError): - raise ValueError(f"无法将聊天温度 '{v}' 转换为数字。") + except (ValueError, TypeError) as exc: + raise ValueError(f"无法将聊天温度 '{v}' 转换为数字。") from exc if not (0.0 <= value <= 1.0): raise ValueError(f"聊天温度必须在 0.0 到 1.0 之间,但得到 {value}。") @@ -177,14 +332,56 @@ def validate_hybrid_fusion_strategy(cls, v: Any) -> str: raise ValueError(f"无效的混合检索融合策略: {v}. 必须是 {', '.join(sorted(valid_strategies))}。") return normalized + @model_validator(mode="after") + def warn_retired_base_urls(self) -> "Settings": + """对已知失效的旧 base URL 发出显式升级警告。 + + 1.4.0 把 Qwen 默认端点从 ``dashscope.aliyuncs.com/api/v1`` 换成 + ``compatible-mode/v1``,并把火山默认域名从 + ``maas-api.ml-platform-cn-beijing.volces.com`` 换成 + ``ark.cn-beijing.volces.com/api/v3``。旧的 ``config.toml`` 会覆盖新默认值, + 使请求在运行期失败。这里在配置加载时就明确提示如何修正,而不是让用户从 + 错误响应反推原因;显式配置的自建或代理端点不受影响。 + + 两处旧值的失效方式不同:DashScope 原生 ``/api/v1`` 本身仍在服务,但路径是 + ``/services/aigc/text-generation/generation``,而本项目按 OpenAI 兼容协议请求 + ``{base_url}/chat/completions``,因此该值在本项目内不可用;火山旧域名则确实 + 已下线。 + + 只警告不阻断:旧值是可修复的配置问题而非安全边界,直接失败会让仍在使用 + 旧配置的部署完全无法启动。 + """ + retired = { + "qwen_base_url": ( + "https://dashscope.aliyuncs.com/api/v1", + "https://dashscope.aliyuncs.com/compatible-mode/v1", + "该端点不提供 {base_url}/chat/completions", + ), + "volc_base_url": ( + "https://maas-api.ml-platform-cn-beijing.volces.com", + "https://ark.cn-beijing.volces.com/api/v3", + "旧域名已停止服务", + ), + } + for field_name, (old_url, new_url, reason) in retired.items(): + current = getattr(self, field_name, None) + if isinstance(current, str) and current.rstrip("/") == old_url: + warnings.warn( + f"{field_name} 指向旧端点 {old_url}({reason})," + f"请改为 {new_url}。", + UserWarning, + stacklevel=2, + ) + return self + @field_validator('retrieval_candidate_multiplier', mode='before') @classmethod def validate_retrieval_candidate_multiplier(cls, v: Any) -> int: """验证检索候选过量招募倍率。""" try: value = int(v) - except (ValueError, TypeError): - raise ValueError(f"无法将检索候选倍率 '{v}' 转换为整数。") + except (ValueError, TypeError) as exc: + raise ValueError(f"无法将检索候选倍率 '{v}' 转换为整数。") from exc if value < 1: raise ValueError(f"检索候选倍率必须大于等于 1,但得到 {value}。") @@ -192,7 +389,7 @@ def validate_retrieval_candidate_multiplier(cls, v: Any) -> int: @field_validator('kb_splitter_separators', mode='before') @classmethod - def split_separators(cls, v: Any) -> List[str]: + def split_separators(cls, v: Any) -> list[str]: """ 如果分隔符是字符串,则按逗号分割成列表。 如果输入值为空(None或空字符串),或者分割后为空列表,则使用字段的默认值。 @@ -200,7 +397,7 @@ def split_separators(cls, v: Any) -> List[str]: field_info = cls.model_fields["kb_splitter_separators"] default_factory = field_info.default_factory if default_factory is not None: - default_value = default_factory() + default_value = cast(Callable[[], Any], default_factory)() else: default_value = field_info.default if default_value is None: @@ -253,7 +450,7 @@ def settings_customise_sources( env_settings: PydanticBaseSettingsSource, dotenv_settings: PydanticBaseSettingsSource, file_secret_settings: PydanticBaseSettingsSource, - ) -> Tuple[PydanticBaseSettingsSource, ...]: + ) -> tuple[PydanticBaseSettingsSource, ...]: """自定义配置加载源,保留环境变量、.env 和 config.toml 三层来源。""" return ( init_settings, @@ -271,7 +468,7 @@ def settings_customise_sources( protected_namespaces=(), ) -def load_toml_config() -> Dict[str, Any]: +def load_toml_config() -> dict[str, Any]: """ 从全局 CONFIG_TOML_PATH 路径加载 config.toml 文件配置。 """ @@ -286,7 +483,7 @@ def normalize_mapping(value: Any, preserve_keys: set[str] | None = None) -> Any: return value preserved = preserve_keys or set() - normalized: Dict[str, Any] = {} + normalized: dict[str, Any] = {} for raw_key, raw_value in value.items(): key = str(raw_key).lower() if key in preserved and isinstance(raw_value, dict): @@ -315,13 +512,21 @@ def normalize_mapping(value: Any, preserve_keys: set[str] | None = None) -> Any: preserve_keys={"embedding_configurations", "rerank_configurations", "llm_configurations"}, ) - flat_config: Dict[str, Any] = {} + flat_config: dict[str, Any] = {} scalar_keys = { "anthropic_api_key", "google_api_key", + "gemini_api_key", + # Vertex/Enterprise endpoint selection and project routing are not + # credentials, so they may be supplied by config.toml as well as env. + "google_genai_use_vertexai", + "google_genai_use_enterprise", + "google_cloud_project", + "google_cloud_location", "siliconflow_api_key", "openai_api_key", "qwen_api_key", + "ark_api_key", "volc_access_key", "volc_secret_key", "jina_api_key", @@ -400,7 +605,7 @@ def __call__(self) -> dict[str, Any]: # 3. 实例化并导出 (INSTANTIATE & EXPORT) # ================================================================= -@functools.lru_cache() +@functools.lru_cache def get_settings() -> Settings: """ 获取 Settings 实例的单例。 @@ -419,7 +624,7 @@ def get_settings() -> Settings: # 策略: 保持旧的配置变量,但使其从新的settings实例派生。 # 后续重构中,应逐步淘汰这些变量,直接使用 `get_settings()` 对象。 -def get_backward_compatible_configs() -> Dict[str, Any]: +def get_backward_compatible_configs() -> dict[str, Any]: """ 获取向后兼容的配置字典。 """ @@ -470,9 +675,11 @@ def get_backward_compatible_configs() -> Dict[str, Any]: API_CONFIG = { "ANTHROPIC_API_KEY": current_settings.anthropic_api_key, "GOOGLE_API_KEY": current_settings.google_api_key, + "GEMINI_API_KEY": current_settings.gemini_api_key, "SILICONFLOW_API_KEY": current_settings.siliconflow_api_key, "OPENAI_API_KEY": current_settings.openai_api_key, "QWEN_API_KEY": current_settings.qwen_api_key, + "ARK_API_KEY": current_settings.ark_api_key, "VOLC_ACCESS_KEY": current_settings.volc_access_key, "VOLC_SECRET_KEY": current_settings.volc_secret_key, "JINA_API_KEY": current_settings.jina_api_key, diff --git a/src/utils/log_manager.py b/src/utils/log_manager.py index 91330c2..c28450b 100644 --- a/src/utils/log_manager.py +++ b/src/utils/log_manager.py @@ -1,11 +1,36 @@ -# -*- coding: utf-8 -*- import logging import os -import re # 导入 re 模块 +import re # 导入 re 模块 from datetime import datetime from logging.handlers import RotatingFileHandler from src.utils.config import get_settings +from src.utils.security import redact_sensitive_text + + +class RedactingFormatter(logging.Formatter): + """在格式化完整 traceback 后脱敏,避免异常链泄漏凭证。 + + ``logging.Formatter.format`` 会把格式化后的 traceback 缓存在 + ``record.exc_text`` 上,后续 handler 直接复用该缓存。如果本 formatter 只是 + 在 ``super().format()`` 之后脱敏,缓存里留下的仍是未脱敏文本,随后执行的 + 非脱敏 handler(例如宿主应用自行挂在 root logger 上的 handler)就会拿到原始 + 凭证。因此这里在格式化前清空缓存、在脱敏后写回,使缓存内容本身也是脱敏的。 + + 边界:Python logging 的 handler 顺序决定,只有在脱敏 handler 先于其它 + handler 执行时才能保护它们。项目自身的 logger 满足该前提(子 logger 的 + handler 先执行,再传播到 root);若宿主在同一个 logger 上把普通 handler + 排在脱敏 handler 之前,那个 handler 已先输出了原始内容,无法追回。 + """ + + def format(self, record: logging.LogRecord) -> str: + record.exc_text = None + formatted = super().format(record) + # Formatter 会把 traceback 写回 ``record.exc_text``;用脱敏结果覆盖, + # 让后续 handler 复用到的缓存也是脱敏后的文本。 + if record.exc_text is not None: + record.exc_text = redact_sensitive_text(record.exc_text) + return redact_sensitive_text(formatted) def get_chat_logger() -> logging.Logger: """ @@ -25,12 +50,12 @@ def get_chat_logger() -> logging.Logger: # 控制台处理器 console_handler = logging.StreamHandler() - console_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + console_formatter = RedactingFormatter('%(asctime)s - %(levelname)s - %(message)s') console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) # 文件处理器 (每天一个文件,最大 1MB,保留 5 个文件) - log_file_name = f"chat_log_{datetime.now().strftime('%Y-%m-%d')}.log" + log_file_name = f"chat_log_{datetime.now().astimezone().strftime('%Y-%m-%d')}.log" file_path = os.path.join(log_dir, log_file_name) file_handler = RotatingFileHandler( file_path, @@ -38,7 +63,7 @@ def get_chat_logger() -> logging.Logger: backupCount=5, encoding='utf-8' ) - file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + file_formatter = RedactingFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) @@ -62,12 +87,12 @@ def get_module_logger(name: str) -> logging.Logger: # 控制台处理器 console_handler = logging.StreamHandler() - console_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') + console_formatter = RedactingFormatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') console_handler.setFormatter(console_formatter) logger.addHandler(console_handler) # 文件处理器 (每天一个文件,最大 1MB,保留 5 个文件) - log_file_name = f"app_log_{datetime.now().strftime('%Y-%m-%d')}.log" + log_file_name = f"app_log_{datetime.now().astimezone().strftime('%Y-%m-%d')}.log" file_path = os.path.join(log_dir, log_file_name) file_handler = RotatingFileHandler( file_path, @@ -75,7 +100,7 @@ def get_module_logger(name: str) -> logging.Logger: backupCount=5, encoding='utf-8' ) - file_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s') + file_formatter = RedactingFormatter('%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d - %(message)s') file_handler.setFormatter(file_formatter) logger.addHandler(file_handler) @@ -92,7 +117,7 @@ def cleanup_old_logs(): if not os.path.exists(log_dir): return - now = datetime.now() + now = datetime.now().astimezone() # 获取所有日志文件 log_files = [f for f in os.listdir(log_dir) if f.endswith(".log")] @@ -104,7 +129,9 @@ def cleanup_old_logs(): match = re.search(r'(\d{4}-\d{2}-\d{2})', filename) if match: file_date_str = match.group(1) - file_date = datetime.strptime(file_date_str, '%Y-%m-%d') + file_date = datetime.strptime(file_date_str, '%Y-%m-%d').replace( + tzinfo=now.tzinfo + ) if (now - file_date).days > log_retention_days: os.remove(file_path) @@ -112,8 +139,10 @@ def cleanup_old_logs(): else: # 如果文件名不符合日期模式,也记录一下,但不删除 get_module_logger(__name__).warning(f"日志文件名不符合日期模式,跳过清理: {filename}") - except Exception as e: - get_module_logger(__name__).error(f"清理日志文件 {filename} 时出错: {e}", exc_info=True) + except Exception: # noqa: BLE001 - cleanup continues independently per file + get_module_logger(__name__).exception( + "清理日志文件 %s 时出错", filename + ) # 示例用法 (可选,用于测试) if __name__ == "__main__": diff --git a/src/utils/security.py b/src/utils/security.py new file mode 100644 index 0000000..9d2d25f --- /dev/null +++ b/src/utils/security.py @@ -0,0 +1,542 @@ +"""配置、请求和日志边界的安全校验工具。""" + +import copy +import re +from collections.abc import Collection, Mapping +from itertools import pairwise +from typing import Any +from urllib.parse import parse_qsl, urlsplit + +FORBIDDEN_OPTION_CONTAINERS = frozenset( + { + "headers", + "defaultheaders", + "setdefaultheaders", + "extraheaders", + "header", + "requestheaders", + "query", + "queryparams", + "params", + "searchparams", + "urlparams", + "requestquery", + "defaultquery", + "setdefaultquery", + "extraquery", + "httpoptions", + } +) + +SENSITIVE_OPTION_KEYS = frozenset( + { + "apikey", + "xapikey", + "accesskey", + "secretkey", + # 火山引擎 SDK 的客户端凭证键名(``ak``/``sk``);本项目自己就用它们 + # 承载 VOLC_ACCESS_KEY / VOLC_SECRET_KEY,因此必须和 ``api_key`` 一样 + # 被识别为凭证。按规范化后的完整键名精确匹配,``task``/``risk``/``ask`` + # 这类普通字段不会命中。 + "ak", + "sk", + # Azure 存储的 ``account_key`` 也是凭证键名。 + "accountkey", + "token", + "password", + "passwd", + "authorization", + "auth", + "bearer", + "accesstoken", + "xaccesstoken", + "authtoken", + "xauthtoken", + "authkey", + "credential", + "credentials", + "cookie", + "setcookie", + "proxyauthorization", + "secret", + "privatekey", + "sshkey", + "sshprivatekey", + "signingkey", + "encryptionkey", + "googkey", + "googlekey", + "clientsecret", + "secretvalue", + "secretcredential", + "adminapikey", + "webhooksecret", + "workloadidentity", + "httpclient", + "httpxclient", + "httpxasyncclient", + "aiohttpclient", + "clientargs", + "asyncclientargs", + "baseurl", + "websocketbaseurl", + } +) + +_RESOURCE_EXTENSION_KEYS = frozenset({"extraheaders", "extraquery", "extrabody"}) +# These names are ordinary business parameters for some official SDK resource +# methods (for example ``vector_stores.search(query=...)``). They are safe only +# at the resource-argument container itself; nested keys and values remain +# subject to credential scanning. +_SAFE_RESOURCE_PARAMETER_CONTAINERS = frozenset({"query", "params"}) + +_BEARER_TEXT_RE = re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._~+/=-]+") +# A punctuation separator is unambiguous. Whitespace-only forms are useful +# for redacting messages such as ``token sk-...``, but treating every following +# word as a credential causes ordinary text (``token usage``) to be rejected by +# ``find_sensitive_option_paths``. Require a credential-like value for that +# form: at least twelve token characters and one digit or token punctuation. +# 文本脱敏与 is_sensitive_option_key 必须使用同一套凭证键名,否则会出现 +# 「配置边界判为敏感、日志里却明文输出」的漏洞(client_secret、private_key、 +# credentials 都曾如此)。下面显式列出可在文本中安全识别的键名,所有文本正则 +# 均由它派生;``[_-]?`` 同时覆盖 ``client_secret`` 与 ``clientsecret``。 +# +# 不含 ak/sk:这两个键太短,需要独立的词边界正则(见 _SHORT_CREDENTIAL_KEY_RE), +# 否则 ``task = value`` 里的 sk 会被误脱敏。 +# 也不含 base_url / client_args / http_client 等连接类键名:它们在配置边界被禁止 +# 是因为会覆盖请求边界,本身不是凭证;纳入文本识别会让普通配置值被误判为凭证。 +_TEXT_CREDENTIAL_KEYS = ( + "secret[_-]?access[_-]?key", + "ssh[_-]?private[_-]?key", + "proxy[_-]?authorization", + "secret[_-]?credential", + "workload[_-]?identity", + "x[_-]?access[_-]?token", + "x[_-]?auth[_-]?token", + "x[_-]?api[_-]?key", + "admin[_-]?api[_-]?key", + "encryption[_-]?key", + "signing[_-]?key", + "private[_-]?key", + "client[_-]?secret", + "secret[_-]?value", + "webhook[_-]?secret", + "account[_-]?key", + "access[_-]?token", + "auth[_-]?token", + "access[_-]?key", + "secret[_-]?key", + "auth[_-]?key", + "set[_-]?cookie", + "google[_-]?key", + "goog[_-]?key", + "ssh[_-]?key", + "api[_-]?key", + "authorization", + "credential", + "credentials", + "password", + "passwd", + "cookie", + "secret", + "token", + "auth", + "bearer", +) +_TEXT_CREDENTIAL_KEY_PATTERN = "|".join(_TEXT_CREDENTIAL_KEYS) + +_KEY_VALUE_TEXT_RE = re.compile( + r"(?i)(? str: + """脱敏异常、日志和请求错误中的常见凭证表示。""" + if not isinstance(value, str): + value = str(value) + # 掩码形态必须最先处理:``_BEARER_TEXT_RE`` 会先吃掉 ``Bearer sk-abc`` + # 的可见前缀,使后续的 ``sk-`` 锚点失效,尾部掩码片段就会残留。 + redacted = _MASKED_CREDENTIAL_RE.sub("[REDACTED]", value) + redacted = _BEARER_TEXT_RE.sub("Bearer [REDACTED]", redacted) + redacted = _KEY_VALUE_TEXT_RE.sub(r"\1=[REDACTED]", redacted) + redacted = _SHORT_CREDENTIAL_KEY_RE.sub(r"\1=[REDACTED]", redacted) + redacted = _URL_USERINFO_RE.sub(r"\1[REDACTED]:[REDACTED]@", redacted) + redacted = _URL_QUERY_SECRET_RE.sub(r"\1[REDACTED]", redacted) + redacted = _OPENAI_KEY_RE.sub("[REDACTED]", redacted) + return _GOOGLE_API_KEY_RE.sub("[REDACTED]", redacted) + + + +def safe_exception_text(exc: BaseException) -> str: + """把异常转成可安全展示的文本:脱敏凭证,并限制长度。 + + 终端与日志是凭证最容易泄漏的出口——SDK 的异常消息常回显请求头或 + URL。所有面向用户的异常输出都应经过这里,而不是直接 ``str(exc)`` + 或把异常对象交给 ``console.print`` / ``logger.error``。 + """ + message = str(exc).strip() + if not message: + return type(exc).__name__ + return f"{type(exc).__name__}: {redact_sensitive_text(message)[:240]}" + + +def _copy_nested(value: Any) -> Any: + """递归复制配置容器,尽量隔离可变的第三方 SDK 对象。""" + if isinstance(value, Mapping): + return {key: _copy_nested(nested) for key, nested in value.items()} + if isinstance(value, list): + return [_copy_nested(nested) for nested in value] + if isinstance(value, tuple): + return tuple(_copy_nested(nested) for nested in value) + if isinstance(value, set): + return {_copy_nested(nested) for nested in value} + if isinstance(value, frozenset): + return frozenset(_copy_nested(nested) for nested in value) + model_copy = getattr(value, "model_copy", None) + if callable(model_copy): + try: + copied = model_copy(deep=True) + except Exception: # noqa: BLE001 - SDK model copy is best effort + copied = value + if copied is not value: + return copied + try: + return copy.deepcopy(value) + except Exception: # noqa: BLE001 - preserve opaque runtime handles + return value + + +def _model_dump_mapping(value: Any) -> Mapping[str, Any] | None: + """读取第三方 SDK 模型的公开字典表示(若可用)。 + + Pydantic 型 SDK 配置对象(例如 ``google.genai.types.HttpOptions``)不是 + ``Mapping``,只遍历普通字典会让其中的 headers/base URL 绕过配置边界。 + 仅使用 SDK 明确提供的 ``model_dump``,无法读取时保持原有兼容行为。 + """ + model_dump = getattr(value, "model_dump", None) + if not callable(model_dump): + return None + try: + dumped = model_dump(exclude_none=True) + except TypeError: + try: + dumped = model_dump() + except Exception: # noqa: BLE001 - third-party model_dump must fail closed + return None + except Exception: # noqa: BLE001 - third-party model_dump must fail closed + return None + if dumped is value or not isinstance(dumped, Mapping): + return None + return dumped + + +def normalize_option_key(key: Any) -> str: + """将配置键规范化为大小写无关、分隔符无关的形式。""" + return "".join(char for char in str(key).lower() if char.isalnum()) + + +def is_sensitive_option_key(key: Any) -> bool: + """识别凭证键,避免把普通单词的 ``secret`` 子串误判为凭证。""" + normalized = normalize_option_key(key) + if normalized in FORBIDDEN_OPTION_CONTAINERS or normalized in SENSITIVE_OPTION_KEYS: + return True + + # 只对明确的词边界进行组合键识别;例如 ``secretary`` 不会命中, + # 而 ``client_secret``、``api-token`` 等显式凭证命名会命中。 + words = [ + part.lower() + for part in re.split( + r"[^A-Za-z0-9]+|(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])", + str(key), + ) + if part + ] + if any( + word in { + "apikey", "accesstoken", "authtoken", "apitoken", "bearer", + "password", "cookie", "token", + } + for word in words + ): + return True + if any(word == "secret" for word in words): + return True + if any( + left in { + "api", "access", "auth", "client", "secret", "private", "ssh", + "signing", "encryption", "goog", "google", + } + and right in {"key", "token", "secret", "credential"} + for left, right in pairwise(words) + ): + return True + compact = "".join(words) + return compact in SENSITIVE_OPTION_KEYS or compact in FORBIDDEN_OPTION_CONTAINERS + + +def _url_credential_paths(value: Any, path: str) -> list[str]: + if not isinstance(value, str): + return [] + try: + parsed = urlsplit(value) + except ValueError: + # urlsplit 对畸形 URL(例如 ``https://[::1``)抛 ValueError。这不是 + # 「值不是 URL」而是「无法解析」,不能就此放弃检查:该分支是叶子值的 + # 唯一入口,直接 return 会让任意凭证随一个畸形前缀整体绕过边界校验。 + # 退回文本脱敏判定,保持与正常 URL 路径一致的严格度。 + return [path or "value"] if redact_sensitive_text(value) != value else [] + found: list[str] = [] + if redact_sensitive_text(value) != value: + found.append(path or "value") + if parsed.username is not None or parsed.password is not None: + found.append(f"{path} userinfo") + if parsed.query and (parsed.scheme or parsed.netloc or "://" in value): + for key, _ in parse_qsl(parsed.query, keep_blank_values=True): + if is_sensitive_option_key(key): + found.append(f"{path}?{key}") + return found + + +def find_sensitive_option_paths( + options: Mapping[str, Any], + *, + allowed_containers: Collection[str] = (), +) -> list[str]: + """递归查找凭证、连接覆盖和 URL 中的凭证路径。 + + ``allowed_containers`` 只用于供应商明确支持的安全容器(例如 Google + ``http_options.headers``);容器内的键和值仍会继续递归检查。 + """ + found: list[str] = [] + allowed = {normalize_option_key(value) for value in allowed_containers} + + def visit(value: Any, path: str = "") -> None: + if isinstance(value, Mapping): + for key, nested in value.items(): + current = f"{path}.{key}" if path else str(key) + if is_sensitive_option_key(key) and normalize_option_key(key) not in allowed: + found.append(current) + continue + visit(nested, current) + return + dumped = _model_dump_mapping(value) + if dumped is not None: + visit(dumped, path) + return + if isinstance(value, (list, tuple, set, frozenset)): + for index, nested in enumerate(value): + visit(nested, f"{path}[{index}]") + return + found.extend(_url_credential_paths(value, path)) + + visit(options) + return sorted(set(found)) + + +def validate_secret_free_options( + options: Mapping[str, Any] | None, + provider: str, + *, + allowed_containers: Collection[str] = (), +) -> dict[str, Any]: + """校验模型级 options 不会携带凭证或请求边界覆盖。 + + 允许的容器只放宽容器本身,绝不会跳过其子项的敏感键检查。 + """ + if options is None: + return {} + if not isinstance(options, Mapping): + raise ValueError(f"{provider} options 必须是对象。") + found = find_sensitive_option_paths(options, allowed_containers=allowed_containers) + if found: + raise ValueError( + f"{provider} options 不允许包含凭证或请求头/query 配置: " + + ", ".join(found) + ) + # 不能只复制最外层:模型配置通常包含 ``extra_body``、嵌套路由或 + # SDK 配置对象。递归复制可以防止调用方在 Provider 初始化后修改 + # 原始字典,进而改变后续请求。 + return _copy_nested(dict(options)) + + +def validate_secret_free_request_overrides( + extra_headers: Mapping[str, Any] | None = None, + extra_query: Mapping[str, Any] | None = None, + provider: str = "Provider", +) -> tuple[dict[str, Any] | None, dict[str, Any] | None]: + """校验请求级 HTTP 覆盖项,不允许携带凭证。 + + 请求级扩展仍可用于追踪头、租户标识等非敏感字段;认证由 Provider + 自己管理,避免调用方通过 ``extra_headers``/``extra_query`` 绕过密钥边界。 + """ + + normalized: dict[str, Mapping[str, Any] | None] = { + "extra_headers": extra_headers, + "extra_query": extra_query, + } + copies: dict[str, dict[str, Any] | None] = {} + for field_name, value in normalized.items(): + if value is None: + copies[field_name] = None + continue + if not isinstance(value, Mapping): + raise ValueError(f"{provider} {field_name} 必须是对象。") + found = find_sensitive_option_paths(value) + if found: + raise ValueError( + f"{provider} 请求头/query 不允许包含凭证: " + f"{field_name}." + ", ".join(found) + ) + copies[field_name] = _copy_nested(dict(value)) + return copies["extra_headers"], copies["extra_query"] + + +def validate_secret_free_payload( + value: Mapping[str, Any] | None, + provider: str, + field_name: str, +) -> dict[str, Any]: + """校验可扩展请求体中的敏感字段并返回递归隔离的副本。""" + + if value is None: + return {} + if not isinstance(value, Mapping): + raise ValueError(f"{provider} {field_name} 必须是对象。") + found = find_sensitive_option_paths(value) + if found: + raise ValueError( + f"{provider} {field_name} 不允许包含凭证或连接字段: " + + ", ".join(found) + ) + return _copy_nested(dict(value)) + + +def validate_secret_free_resource_kwargs( + kwargs: Mapping[str, Any] | None, + provider: str, +) -> dict[str, Any]: + """校验并复制资源 Facade 的 HTTP/body 扩展参数。 + + SDK 资源方法普遍接受 ``extra_headers``、``extra_query`` 和 + ``extra_body``。统一在 Facade 边界校验,避免某个资源方法遗漏安全检查。 + """ + + if kwargs is None: + return {} + if not isinstance(kwargs, Mapping): + raise ValueError(f"{provider} 资源参数必须是对象。") + sanitized = _copy_nested(dict(kwargs)) + + # SDK client/resource methods also expose client-level overrides such as + # ``api_key``, ``default_headers`` and ``base_url``. They are not part of + # the three supported request extensions below and must not pass through a + # Facade just because a particular SDK accepts them. + generic_kwargs = { + key: value + for key, value in sanitized.items() + if normalize_option_key(key) not in _RESOURCE_EXTENSION_KEYS + } + found = find_sensitive_option_paths( + generic_kwargs, + allowed_containers=_SAFE_RESOURCE_PARAMETER_CONTAINERS, + ) + if found: + raise ValueError( + f"{provider} 资源参数不允许包含凭证或请求头/query 配置: " + + ", ".join(found) + ) + + normalized_keys: dict[str, Any] = {} + duplicate_extension_keys: list[str] = [] + for key in sanitized: + normalized = normalize_option_key(key) + if normalized not in _RESOURCE_EXTENSION_KEYS: + continue + if normalized in normalized_keys: + duplicate_extension_keys.append(str(key)) + continue + normalized_keys[normalized] = key + if duplicate_extension_keys: + raise ValueError( + f"{provider} 资源参数包含重复的扩展字段: " + + ", ".join(duplicate_extension_keys) + ) + headers_key = normalized_keys.get("extraheaders") + query_key = normalized_keys.get("extraquery") + body_key = normalized_keys.get("extrabody") + headers, query = validate_secret_free_request_overrides( + sanitized.get(headers_key) if headers_key is not None else None, + sanitized.get(query_key) if query_key is not None else None, + provider, + ) + if headers_key is not None: + sanitized[headers_key] = headers + if query_key is not None: + sanitized[query_key] = query + if body_key is not None and sanitized[body_key] is not None: + sanitized[body_key] = validate_secret_free_payload( + sanitized[body_key], provider, "extra_body" + ) + return sanitized + + +def validate_secret_free_resource_args( + args: tuple[Any, ...] | None, + provider: str, +) -> tuple[Any, ...]: + """校验并复制资源 Facade 的位置参数。 + + 大多数 SDK 资源方法把业务参数声明为关键字参数,但少数入口(例如 + OpenAI Responses WebSocket 的 ``connect``)允许用位置参数传入 + ``extra_query``、``extra_headers`` 或连接选项。统一扫描位置参数可以 + 防止这些入口绕过关键字参数的凭证边界,同时保留普通字符串、ID 和 + 文件对象的转发兼容性。 + """ + + if args is None: + return () + if not isinstance(args, tuple): + raise ValueError(f"{provider} 资源位置参数必须是元组。") + sanitized = _copy_nested(args) + found: list[str] = [] + for index, value in enumerate(sanitized): + # Wrapping the value in a mapping lets the existing recursive scanner + # retain useful paths (for example ``args[0].Authorization``) and the + # same safe business containers used by keyword resource parameters. + found.extend( + find_sensitive_option_paths( + {f"args[{index}]": value}, + allowed_containers=_SAFE_RESOURCE_PARAMETER_CONTAINERS, + ) + ) + if found: + raise ValueError( + f"{provider} 资源位置参数不允许包含凭证或请求头/query 配置: " + + ", ".join(sorted(set(found))) + ) + return sanitized diff --git a/tests/chat/test_core.py b/tests/chat/test_core.py index bf8dbaf..e16d3b9 100644 --- a/tests/chat/test_core.py +++ b/tests/chat/test_core.py @@ -3,9 +3,61 @@ from rich.console import Console +from src.chat.core import Chatbot, start_chat_session_async from src.runtime.contracts import SessionConfig from src.utils.config import ModelDetail, RetrievalMethod -from src.chat.core import Chatbot, start_chat_session_async +from src.utils.log_manager import RedactingFormatter +from src.utils.security import safe_exception_text + + +def test_safe_exception_text_redacts_credentials(): + text = safe_exception_text(RuntimeError("Authorization: Bearer sk-secret-token")) + assert "sk-secret-token" not in text + assert "REDACTED" in text + + +def test_safe_exception_text_redacts_json_credentials(): + text = safe_exception_text(RuntimeError('{"api_key": "secret-value"}')) + assert "secret-value" not in text + assert "REDACTED" in text + + +def test_log_formatter_redacts_credentials_from_traceback(): + import logging + import sys + + try: + raise RuntimeError("Authorization: Bearer sk-secret-token") + except RuntimeError: + record = logging.LogRecord( + "chat_logger", + logging.ERROR, + __file__, + 1, + "request failed", + (), + sys.exc_info(), + ) + + rendered = RedactingFormatter("%(message)s").format(record) + assert "sk-secret-token" not in rendered + assert "[REDACTED]" in rendered + + +def test_chatbot_close_disposes_llm_and_retrieval_service(): + llm = SimpleNamespace(close=lambda: setattr(llm, "closed", True), closed=False) + retrieval = SimpleNamespace(close=lambda: setattr(retrieval, "closed", True), closed=False) + bot = object.__new__(Chatbot) + bot.llm_model = llm + bot.chat_service = object() + bot.retrieval_service = retrieval + + bot.close() + + assert llm.closed is True + assert retrieval.closed is True + assert bot.llm_model is None + assert bot.retrieval_service is None class FakePromptSession: @@ -20,7 +72,7 @@ class FakeChatbot: def __init__(self, _console: Console): self.llm_model = object() self.chat_config = { - "active_llm_configuration": "iflow-qwen3-max", + "active_llm_configuration": "openai", "active_rerank_configuration": "siliconflow", "retrieval_method": SimpleNamespace(value="混合检索"), "vector_weight": 0.3, @@ -31,7 +83,7 @@ def __init__(self, _console: Console): "top_k": 5, "score_threshold": 0.4, "llm_configurations": { - "iflow-qwen3-max": SimpleNamespace(provider="openai", model_name="qwen3-max"), + "openai": SimpleNamespace(provider="openai", model_name="gpt-4o"), }, "rerank_configurations": { "siliconflow": SimpleNamespace(provider="siliconflow", model_name="rerank"), @@ -109,6 +161,7 @@ async def chat_async(self, user_input): def test_chat_async_retrieves_with_intent(): retrieval_queries = [] + model_calls = [] class DummyLogger: def info(self, *_args, **_kwargs): @@ -118,7 +171,8 @@ def error(self, *_args, **_kwargs): return None class DummyLLM: - async def ainvoke(self, *_args, **_kwargs): + async def ainvoke(self, *_args, **kwargs): + model_calls.append(kwargs) yield "回答" async def fake_identify(self, _user_query): @@ -148,6 +202,7 @@ async def consume(): assert retrieval_queries == ["清除浏览器缓存的操作步骤"] assert chunks == ["回答"] + assert "temperature" not in model_calls[0] def test_reload_llm_keeps_existing_model_on_failure(monkeypatch): @@ -165,7 +220,7 @@ async def ainvoke(self, *_args, **_kwargs): monkeypatch.setattr( "src.chat.core.ModelProviderFactory.get_llm_provider", - lambda _llm_key: (_ for _ in ()).throw(RuntimeError("reload failed")), + lambda _llm_key, _configurations=None: (_ for _ in ()).throw(RuntimeError("reload failed")), ) assert bot.reload_llm() is False @@ -173,6 +228,50 @@ async def ainvoke(self, *_args, **_kwargs): assert bot.chat_service is old_chat_service +def test_apply_config_update_async_keeps_runtime_on_chat_service_failure(monkeypatch): + class ClosableModel: + def __init__(self): + self.closed = False + + def close(self): + self.closed = True + + old_model = ClosableModel() + new_model = ClosableModel() + old_chat_service = object() + messages = [] + + class DummySessionConfig: + active_llm_configuration = "new-model" + + def __setitem__(self, key, value): + setattr(self, key, value) + + bot = object.__new__(Chatbot) + bot.console = SimpleNamespace(print=lambda message: messages.append(str(message))) + bot.logger = SimpleNamespace(warning=lambda *_args, **_kwargs: None) + bot.session_config = DummySessionConfig() + bot.retrieval_service = object() + bot.llm_model = old_model + bot.chat_service = old_chat_service + + monkeypatch.setattr( + "src.chat.core.ModelProviderFactory.get_llm_provider", + lambda *_args, **_kwargs: new_model, + ) + monkeypatch.setattr( + "src.chat.core.ChatService", + lambda *_args, **_kwargs: (_ for _ in ()).throw(RuntimeError("service init failed")), + ) + + asyncio.run(bot.apply_config_update_async({"active_llm_configuration": "new-model"}, True)) + + assert bot.llm_model is old_model + assert bot.chat_service is old_chat_service + assert new_model.closed is True + assert any("LLM 切换失败" in message for message in messages) + + def test_apply_config_update_reverts_llm_key_when_reload_fails(monkeypatch): class DummySessionConfig: def __init__(self, active_llm_configuration: str): @@ -195,6 +294,42 @@ def __setitem__(self, key, value): assert any("LLM 切换失败" in message for message in messages) +def test_apply_config_update_restores_all_llm_configurations_on_reload_failure(monkeypatch): + old_detail = ModelDetail(provider="openai", model_name="old-model") + new_detail = ModelDetail(provider="qwen", model_name="new-model") + bot = object.__new__(Chatbot) + bot.console = SimpleNamespace(print=lambda _message: None) + bot.session_config = SessionConfig( + retrieval_method=RetrievalMethod.HYBRID_SEARCH, + vector_weight=0.3, + keyword_weight=0.7, + hybrid_fusion_strategy="rrf", + retrieval_candidate_multiplier=3, + rerank_enabled=False, + top_k=5, + score_threshold=0.4, + active_llm_configuration="old-model", + active_rerank_configuration="siliconflow", + llm_configurations={"old-model": old_detail}, + rerank_configurations={}, + chat_temperature=0.7, + ) + bot.llm_model = object() + bot.chat_service = object() + monkeypatch.setattr(bot, "reload_llm", lambda: False) + + bot.apply_config_update( + { + "active_llm_configuration": "new-model", + "llm_configurations": {"new-model": new_detail}, + }, + llm_needs_reload=True, + ) + + assert bot.session_config.active_llm_configuration == "old-model" + assert bot.session_config.llm_configurations == {"old-model": old_detail} + + def test_start_chat_session_async_reverts_llm_after_editor_mutation(monkeypatch): class RealishChatbot(FakeChatbot): last_instance = None @@ -214,7 +349,7 @@ def __init__(self, _console: Console): score_threshold=0.4, active_llm_configuration="old-model", active_rerank_configuration="siliconflow", - llm_configurations={"old-model": ModelDetail(provider="openai", model_name="qwen3-max")}, + llm_configurations={"old-model": ModelDetail(provider="openai", model_name="gpt-4o")}, rerank_configurations={"siliconflow": ModelDetail(provider="siliconflow", model_name="rerank")}, chat_temperature=0.7, ) @@ -258,3 +393,34 @@ def fake_launch(chat_config): assert edited_configs[0]["active_llm_configuration"] == "new-model" assert RealishChatbot.last_instance.session_config.active_llm_configuration == "old-model" + + +def test_redacting_formatter_scrubs_cached_traceback_for_later_handlers(): + """脱敏 handler 之后的普通 handler 不能拿到原始 traceback。 + + ``logging.Formatter`` 把格式化后的 traceback 缓存在 ``record.exc_text``, + 后续 handler 会复用该缓存。脱敏 formatter 必须在写回缓存前完成脱敏,否则 + 宿主挂在同一 logger 上的普通 handler 仍会输出原始凭证。 + """ + import io + import logging + import sys + + try: + raise RuntimeError("Authorization: Bearer sk-cached-traceback-secret") + except RuntimeError: + exc_info = sys.exc_info() + + redacting = logging.StreamHandler(io.StringIO()) + redacting.setFormatter(RedactingFormatter("%(message)s")) + plain = logging.StreamHandler(io.StringIO()) + plain.setFormatter(logging.Formatter("%(message)s")) + + logger = logging.getLogger("redaction-cache-test") + logger.propagate = False + logger.handlers = [redacting, plain] + logger.setLevel(logging.ERROR) + logger.error("request failed", exc_info=exc_info) + + assert "sk-cached-traceback-secret" not in redacting.stream.getvalue() + assert "sk-cached-traceback-secret" not in plain.stream.getvalue() diff --git a/tests/etl/test_pipeline.py b/tests/etl/test_pipeline.py index 7397311..398adc8 100644 --- a/tests/etl/test_pipeline.py +++ b/tests/etl/test_pipeline.py @@ -1,13 +1,14 @@ -import pytest from pathlib import Path from unittest.mock import MagicMock -from src.etl.pipeline import Pipeline -from src.etl.extractors.markdown_extractor import MarkdownExtractor +import pytest + from src.etl.cleaners.basic_cleaner import BasicCleaner +from src.etl.extractors.markdown_extractor import MarkdownExtractor +from src.etl.pipeline import Pipeline from src.etl.splitters.recursive_text_splitter import RecursiveTextSplitter from src.models.document import Document -from src.utils.config import get_settings # 导入 get_settings 函数 +from src.utils.config import get_settings # 导入 get_settings 函数 # 用于测试的 Markdown 示例内容 SAMPLE_MARKDOWN = """ diff --git a/tests/providers/test_factory.py b/tests/providers/test_factory.py index 6935228..3daba5e 100644 --- a/tests/providers/test_factory.py +++ b/tests/providers/test_factory.py @@ -1,27 +1,34 @@ -import pytest +import sys # 导入 sys 模块 +from collections.abc import AsyncGenerator, Generator +from typing import Any from unittest.mock import MagicMock, patch -from typing import Any, Generator, List, AsyncGenerator -import sys # 导入 sys 模块 -from src.utils.config import Settings, get_settings -from src.providers.__base__.model_provider import LargeLanguageModel, TextEmbeddingModel, RerankModel -from src.models.document import Document -from src.utils.config import ModelDetail -from src.providers.factory import ModelProviderFactory # 在这里导入 ModelProviderFactory +import pytest + +from src.providers.__base__.model_provider import ( + LargeLanguageModel, + RerankModel, + TextEmbeddingModel, +) +from src.providers.factory import ( + ModelProviderFactory, # 在这里导入 ModelProviderFactory +) +from src.utils.config import ModelDetail, Settings + # Mock 类定义 class MockLLMProvider(LargeLanguageModel): def __init__(self, model_name: str): self.model_name = model_name - def invoke(self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", tools: List[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7) -> Generator[str, None, None]: + def invoke(self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", tools: list[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7) -> Generator[str, None, None]: """模拟 LLM 聊天响应""" if stream: yield f"Mock LLM stream response for {self.model_name}" else: yield f"Mock LLM response for {self.model_name}" - async def ainvoke(self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", tools: List[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7) -> AsyncGenerator[str, None]: + async def ainvoke(self, prompt: str, system_prompt: str | None = "You are a helpful assistant.", tools: list[dict[str, Any]] | None = None, stream: bool = True, temperature: float = 0.7) -> AsyncGenerator[str, None]: """模拟异步 LLM 聊天响应""" if stream: yield f"Mock Async LLM stream response for {self.model_name}" @@ -67,13 +74,15 @@ def mock_settings(): mock_embedding_config = ModelDetail(provider="mock_embedding", model_name="mock-embedding-model") mock_rerank_config = ModelDetail(provider="mock_rerank", model_name="mock-rerank-model") mock_siliconflow_rerank_config = ModelDetail(provider="siliconflow", model_name="mock-siliconflow-rerank-model") + mock_qwen_rerank_config = ModelDetail(provider="qwen", model_name="qwen-rerank-model") mock_settings_instance = MagicMock(spec=Settings) mock_settings_instance.llm_configurations = {"default_llm": mock_llm_config} mock_settings_instance.embedding_configurations = {"default_embedding": mock_embedding_config} mock_settings_instance.rerank_configurations = { "default_rerank": mock_rerank_config, - "siliconflow_rerank_key": mock_siliconflow_rerank_config + "siliconflow_rerank_key": mock_siliconflow_rerank_config, + "qwen_rerank_key": mock_qwen_rerank_config, } mock_settings_instance.siliconflow_api_key = "mock_siliconflow_api_key" mock_settings_instance.siliconflow_base_url = "http://mock-siliconflow-base-url.com" @@ -107,7 +116,9 @@ def patch_settings(mock_settings, monkeypatch): del sys.modules[module_name] # 重新导入 ModelProviderFactory,确保它加载的是最新的版本 - from src.providers.factory import ModelProviderFactory as ReloadedModelProviderFactory + from src.providers.factory import ( + ModelProviderFactory as ReloadedModelProviderFactory, + ) # 直接模拟 _get_provider_class 方法 def mock_get_provider_class(provider_name: str): @@ -115,11 +126,9 @@ def mock_get_provider_class(provider_name: str): return MockLLMProvider elif provider_name == "mock_embedding": return MockEmbeddingProvider - elif provider_name == "mock_rerank": + elif provider_name == "mock_rerank" or provider_name == "siliconflow_rerank": return MockRerankProvider - elif provider_name == "siliconflow_rerank": - return MockRerankProvider - elif provider_name == "google": + elif provider_name == "google" or provider_name == "qwen": return MockLLMProvider else: raise ValueError(f"不支持的模型提供商: {provider_name}") @@ -143,6 +152,50 @@ def test_get_llm_provider_not_found(): with pytest.raises(ValueError, match="在LLM配置中未找到key: non_existent_key"): ModelProviderFactory.get_llm_provider("non_existent_key") + +def test_factory_uses_explicit_configuration_mapping(): + configurations = {"custom": ModelDetail(provider="mock_llm", model_name="custom-model")} + + provider = ModelProviderFactory.get_llm_provider("custom", configurations) + + assert provider.model_name == "custom-model" + + +def test_factory_does_not_pass_fixed_protocol_to_provider_constructor(): + configurations = { + "google-model": ModelDetail( + provider="google", model_name="gemini-model", protocol="generate_content" + ) + } + + provider = ModelProviderFactory.get_llm_provider("google-model", configurations) + + assert isinstance(provider, MockLLMProvider) + assert provider.model_name == "gemini-model" + + +def test_factory_normalizes_direct_string_protocol_values(): + provider = ModelProviderFactory._create_provider( + "mock_llm", + "mock-model", + "llm", + LargeLanguageModel, + protocol="chat", + ) + + assert isinstance(provider, MockLLMProvider) + + +def test_factory_rejects_options_for_provider_without_options_parameter(): + with pytest.raises(ValueError, match="不支持 options"): + ModelProviderFactory._create_provider( + "mock_embedding", + "mock-model", + "embedding", + TextEmbeddingModel, + options={"dimensions": 2}, + ) + def test_get_embedding_provider_success(): """测试成功获取 Embedding 提供商""" embedding_provider = ModelProviderFactory.get_embedding_provider("default_embedding") @@ -154,6 +207,35 @@ def test_get_embedding_provider_not_found(): with pytest.raises(ValueError, match="在Embedding配置中未找到key: non_existent_key"): ModelProviderFactory.get_embedding_provider("non_existent_key") + +def test_get_embedding_provider_rejects_llm_only_provider(): + configurations = {"chat-only": ModelDetail(provider="deepseek", model_name="deepseek-chat")} + + with pytest.raises(TypeError, match="提供商 deepseek 不支持 embedding 能力"): + ModelProviderFactory.get_embedding_provider("chat-only", configurations) + + +def test_get_embedding_provider_rejects_protocol_configuration(): + configurations = { + "embedding": ModelDetail( + provider="mock_embedding", model_name="embedding-model", protocol="responses" + ) + } + + with pytest.raises(ValueError, match="Embedding 配置不支持 protocol"): + ModelProviderFactory.get_embedding_provider("embedding", configurations) + + +def test_get_rerank_provider_rejects_protocol_configuration(): + configurations = { + "rerank": ModelDetail( + provider="mock_rerank", model_name="rerank-model", protocol="responses" + ) + } + + with pytest.raises(ValueError, match="Rerank 配置不支持 protocol"): + ModelProviderFactory.get_rerank_provider("rerank", configurations) + def test_get_rerank_provider_success(): """测试成功获取 Rerank 提供商""" rerank_provider = ModelProviderFactory.get_rerank_provider("default_rerank") @@ -171,6 +253,11 @@ def test_get_rerank_provider_not_found(): with pytest.raises(ValueError, match="在Rerank配置中未找到key: non_existent_key"): ModelProviderFactory.get_rerank_provider("non_existent_key") + +def test_get_rerank_provider_rejects_provider_without_rerank_capability(): + with pytest.raises(TypeError, match="提供商 qwen 不支持 rerank 能力"): + ModelProviderFactory.get_rerank_provider("qwen_rerank_key") + def test_unsupported_provider_type(): """测试 _get_provider_class 方法处理不支持的提供商名称""" with pytest.raises(ValueError, match="不支持的模型提供商: unsupported_provider"): diff --git a/tests/providers/test_failure_contracts.py b/tests/providers/test_failure_contracts.py index 378e019..a814ec5 100644 --- a/tests/providers/test_failure_contracts.py +++ b/tests/providers/test_failure_contracts.py @@ -1,15 +1,23 @@ -# -*- coding: utf-8 -*- import asyncio import importlib from types import SimpleNamespace +from unittest.mock import MagicMock import pytest import src.providers.factory as factory_module +from src.providers.__base__.model_provider import ( + CompletionRequest, + LargeLanguageModel, + iterate_async, +) +from src.providers.anthropic import AnthropicProvider from src.providers.factory import ModelProviderFactory +from src.providers.google import GoogleProvider from src.providers.grok import GrokProvider from src.providers.openai import OpenAIProvider from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.volcengine import VolcengineProvider from src.utils.config import ModelDetail @@ -26,6 +34,77 @@ async def create(*_args, **_kwargs): raise RuntimeError("boom-embed") +class RequestCaptureModel(LargeLanguageModel): + def invoke(self, **_kwargs): + yield "ok" + + async def ainvoke(self, **_kwargs): + yield "ok" + + +def test_iterate_async_awaits_async_close_on_sync_iterable(): + class AsyncClosableIterable: + def __init__(self): + self.closed = False + + def __iter__(self): + return iter(["ok"]) + + async def close(self): + self.closed = True + + value = AsyncClosableIterable() + + async def consume() -> list[str]: + return [item async for item in iterate_async(value)] + + assert asyncio.run(consume()) == ["ok"] + assert value.closed is True + + +def test_base_request_and_kwargs_conflict_is_explicit(): + provider = RequestCaptureModel() + request = CompletionRequest(prompt="from-request") + + with pytest.raises(ValueError, match="不能同时传 request"): + provider.complete(request, prompt="from-kwargs") + with pytest.raises(ValueError, match="不能同时传 request"): + asyncio.run(provider.acomplete(request, prompt="from-kwargs")) + with pytest.raises(ValueError, match="不能同时传 request"): + list(provider.stream_events(request, prompt="from-kwargs")) + + async def consume() -> None: + async for _event in provider.astream_events(request, prompt="from-kwargs"): + pass + + with pytest.raises(ValueError, match="不能同时传 request"): + asyncio.run(consume()) + + +@pytest.mark.parametrize( + ("provider_class", "method_name"), + [ + (OpenAICompatibleProvider, "complete"), + (OpenAICompatibleProvider, "stream_events"), + (AnthropicProvider, "complete"), + (AnthropicProvider, "stream_events"), + (GoogleProvider, "complete"), + (GoogleProvider, "count_tokens"), + (VolcengineProvider, "complete"), + (VolcengineProvider, "stream_events"), + ], +) +def test_provider_request_and_kwargs_conflict_is_explicit(provider_class, method_name): + provider = object.__new__(provider_class) + request = CompletionRequest(prompt="from-request") + method = getattr(provider, method_name) + + with pytest.raises(ValueError, match="不能同时传 request"): + result = method(request, prompt="from-kwargs") + if hasattr(result, "__iter__") and not isinstance(result, (str, bytes, dict)): + list(result) + + def test_openai_provider_async_failure_raises(monkeypatch): fake_settings = SimpleNamespace(openai_api_key="token", openai_api_base="http://example.com") monkeypatch.setattr("src.providers.openai.get_settings", lambda: fake_settings) @@ -43,7 +122,6 @@ async def consume() -> None: with pytest.raises(RuntimeError, match="boom-embed"): asyncio.run(provider.aembed_documents(["doc"])) - def test_openai_compatible_provider_async_failure_raises(monkeypatch): fake_settings = SimpleNamespace(deepseek_api_key="token", deepseek_base_url="http://example.com") monkeypatch.setattr("src.providers.openai_compatible.get_settings", lambda: fake_settings) @@ -61,6 +139,60 @@ async def consume() -> None: with pytest.raises(RuntimeError, match="boom-embed"): asyncio.run(provider.aembed_documents(["doc"])) +def test_openai_provider_reuses_compatible_adapter(monkeypatch): + fake_settings = SimpleNamespace(openai_api_key="token", openai_api_base="http://example.com") + monkeypatch.setattr("src.providers.openai.get_settings", lambda: fake_settings) + + provider = OpenAIProvider("demo") + + assert isinstance(provider, OpenAICompatibleProvider) + assert provider._base_url == "http://example.com" + + +def test_openai_provider_preserves_missing_key_error(monkeypatch): + monkeypatch.setattr( + "src.providers.openai.get_settings", + lambda: SimpleNamespace(openai_api_key=None, openai_api_base="http://example.com"), + ) + + with pytest.raises(ValueError, match="OpenAI配置不完整:缺少 OPENAI_API_KEY"): + OpenAIProvider("demo") + + +def test_openai_compatible_provider_normalizes_hyphenated_settings(monkeypatch): + fake_settings = SimpleNamespace( + lm_studio_api_key=None, + lm_studio_base_url="http://localhost:1234/v1", + ) + monkeypatch.setattr("src.providers.openai_compatible.get_settings", lambda: fake_settings) + + provider = OpenAICompatibleProvider("demo", "lm-studio") + + assert provider._api_key == "no-key-required" + assert provider._base_url == "http://localhost:1234/v1" + + +def test_openai_compatible_provider_forwards_tools(monkeypatch): + fake_settings = SimpleNamespace(deepseek_api_key="token", deepseek_base_url="http://example.com") + monkeypatch.setattr("src.providers.openai_compatible.get_settings", lambda: fake_settings) + provider = OpenAICompatibleProvider("demo", "deepseek") + + client = MagicMock() + client.chat.completions.create.return_value = SimpleNamespace( + choices=[SimpleNamespace(message=SimpleNamespace(content="ok"))] + ) + provider._client = client + tools = [{"type": "function", "function": {"name": "lookup"}}] + + assert list(provider.invoke("hello", tools=tools, stream=False)) == ["ok"] + + request = client.chat.completions.create.call_args.kwargs + assert request["tools"] == tools + assert request["messages"] == [ + {"role": "system", "content": "You are a helpful assistant."}, + {"role": "user", "content": "hello"}, + ] + def test_grok_provider_can_be_loaded_by_factory(monkeypatch): fake_settings = SimpleNamespace( @@ -77,6 +209,30 @@ def test_grok_provider_can_be_loaded_by_factory(monkeypatch): assert provider.__class__.__module__ == "src.providers.grok" +def test_factory_caches_provider_class(monkeypatch): + factory = factory_module.ModelProviderFactory + factory._get_provider_class.cache_clear() + monkeypatch.setattr( + factory, + "_provider_map", + {"cached": {"module": "fake.provider", "class": "CachedProvider"}}, + ) + module = SimpleNamespace(CachedProvider=object) + imported_modules = [] + + def import_module(module_name): + imported_modules.append(module_name) + return module + + monkeypatch.setattr(factory_module.importlib, "import_module", import_module) + + first = factory._get_provider_class("cached") + second = factory._get_provider_class("cached") + + assert first is second + assert imported_modules == ["fake.provider"] + + @pytest.mark.parametrize( ("provider_name", "module_name", "class_name"), [ diff --git a/tests/providers/test_google_genai.py b/tests/providers/test_google_genai.py index d9c58a3..bf27544 100644 --- a/tests/providers/test_google_genai.py +++ b/tests/providers/test_google_genai.py @@ -1,8 +1,13 @@ -import pytest +import asyncio +from types import SimpleNamespace from unittest.mock import MagicMock, patch + +import pytest + import src.providers.google as google_module from src.providers.google import GoogleProvider + @pytest.fixture(autouse=True) def mock_settings(monkeypatch): """强制模拟 settings 并在环境中设置 key""" @@ -32,6 +37,399 @@ def test_google_provider_get_client(mock_settings, mock_genai_client): from src.providers.google import genai genai.Client.assert_called_once_with(api_key="fake_key") + +def test_google_provider_uses_credentials_without_api_key(monkeypatch): + credentials = object() + settings = MagicMock() + settings.google_api_key = "should-not-be-forwarded" + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"credentials": credentials, "vertexai": True}, + ) + + provider._get_client() + + client_class.assert_called_once_with(credentials=credentials, vertexai=True) + + +def test_google_provider_accepts_gemini_api_key_alias(monkeypatch): + settings = SimpleNamespace(google_api_key=None, gemini_api_key="gemini-key") + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider(model_name="gemini-1.5-flash") + provider._get_client() + + client_class.assert_called_once_with(api_key="gemini-key") + + +def test_google_api_key_takes_precedence_over_gemini_alias(monkeypatch): + settings = SimpleNamespace( + google_api_key="google-key", gemini_api_key="gemini-key" + ) + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider(model_name="gemini-1.5-flash") + provider._get_client() + + client_class.assert_called_once_with(api_key="google-key") + + +def test_google_provider_accepts_credentials_without_google_api_key(monkeypatch): + credentials = object() + settings = MagicMock() + settings.google_api_key = None + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider( + model_name="gemini-1.5-flash", options={"credentials": credentials} + ) + + provider._get_client() + + client_class.assert_called_once_with(credentials=credentials, vertexai=True) + + +def test_google_credentials_reject_explicit_developer_api_mode(monkeypatch): + credentials = object() + settings = MagicMock() + settings.google_api_key = None + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"credentials": credentials, "vertexai": False}, + ) + with patch.object(google_module.genai, "Client"), pytest.raises( + ValueError, match="Vertex/Enterprise" + ): + provider._get_client() + + +def test_google_vertex_uses_adc_without_api_key(monkeypatch): + settings = MagicMock() + settings.google_api_key = None + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("GEMINI_API_KEY", raising=False) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider( + model_name="gemini-1.5-flash", options={"vertexai": True} + ) + + provider._get_client() + + client_class.assert_called_once_with(vertexai=True) + + +def test_google_vertex_dotenv_settings_are_forwarded_to_sdk(monkeypatch): + settings = SimpleNamespace( + google_api_key=None, + google_genai_use_vertexai=True, + google_genai_use_enterprise=None, + google_cloud_project="dotenv-project", + google_cloud_location="asia-east1", + google_application_credentials=None, + ) + monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False) + monkeypatch.delenv("GOOGLE_CLOUD_PROJECT", raising=False) + monkeypatch.delenv("GOOGLE_CLOUD_LOCATION", raising=False) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("GEMINI_API_KEY", raising=False) + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider(model_name="gemini-1.5-flash") + provider._get_client() + + client_class.assert_called_once_with( + vertexai=True, + project="dotenv-project", + location="asia-east1", + ) + + +def test_google_get_client_requires_explicit_auth_without_key_or_vertex_mode(monkeypatch): + settings = MagicMock() + settings.google_api_key = None + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + monkeypatch.delenv("GOOGLE_GENAI_USE_VERTEXAI", raising=False) + monkeypatch.delenv("GOOGLE_GENAI_USE_ENTERPRISE", raising=False) + monkeypatch.delenv("GOOGLE_API_KEY", raising=False) + monkeypatch.delenv("GEMINI_API_KEY", raising=False) + provider = GoogleProvider(model_name="gemini-1.5-flash", options={}) + + with patch.object(google_module.genai, "Client") as client_class, pytest.raises( + ValueError, match="GOOGLE_API_KEY|Vertex/Enterprise" + ): + provider._get_client() + + client_class.assert_not_called() + + +@pytest.mark.parametrize( + "http_options", + [ + {"headers": {"Authorization": "Bearer secret"}}, + {"headers": {"X-API-Key": "secret"}}, + {"client_args": {"headers": {"X-Auth-Token": "secret"}}}, + {"client_args": {"params": {"access_token": "secret"}}}, + {"async_client_args": {"timeout": 1000}}, + {"async_client_args": {"params": {"trace_id": "trace"}}}, + {"base_url": "https://proxy.invalid/v1"}, + {"base_url": "https://proxy.invalid/v1?api_key=secret"}, + {"base_url": "https://user:password@proxy.invalid/v1"}, + {"httpx_client": object()}, + ], +) +def test_google_http_options_reject_credentials_or_custom_clients(http_options): + with pytest.raises(ValueError, match="凭证|自定义客户端|连接覆盖"): + GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": http_options}, + ) + + +def test_google_http_options_allow_safe_headers_and_sdk_object(): + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": {"headers": {"X-Trace-ID": "trace"}}}, + ) + provider._validate_options(provider._options) + sdk_options = google_module.types.HttpOptions(headers={"X-Trace-ID": "trace"}) + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": sdk_options}, + ) + provider._validate_options(provider._options) + + +def test_google_http_options_scan_header_values_and_avoid_substring_false_positives(): + with pytest.raises(ValueError, match="凭证"): + GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": {"headers": {"X-Trace": "Bearer secret-token"}}}, + ) + + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={ + "http_options": { + "headers": { + "X-Authorization-Count": "1", + "X-Credential-Provider": "internal", + } + } + }, + ) + provider._validate_options(provider._options) + + +def test_google_http_options_reject_connection_override_containers(): + for key in ("client_args", "async_client_args"): + with pytest.raises(ValueError, match="连接覆盖"): + GoogleProvider( + model_name="gemini-1.5-flash", + options={"http_options": {key: {"timeout": 1000}}}, + ) + + +def test_google_invoke_non_stream_refusal_is_explicit_error(): + provider = GoogleProvider(model_name="gemini-1.5-flash") + response = SimpleNamespace( + candidates=[ + SimpleNamespace( + finish_reason="SAFETY", + content=SimpleNamespace(parts=[]), + ) + ] + ) + client = MagicMock() + client.models.generate_content.return_value = response + + with patch.object(provider, "_get_client", return_value=client), pytest.raises( + RuntimeError, match="安全策略|拒答" + ): + list(provider.invoke("hello", stream=False)) + + +def test_google_invoke_stream_refusal_is_explicit_error(): + provider = GoogleProvider(model_name="gemini-1.5-flash") + chunk = SimpleNamespace( + candidates=[ + SimpleNamespace( + finish_reason="SAFETY", + content=SimpleNamespace(parts=[]), + ) + ] + ) + client = MagicMock() + client.models.generate_content_stream.return_value = iter([chunk]) + + with patch.object(provider, "_get_client", return_value=client), pytest.raises( + RuntimeError, match="安全策略|拒答" + ): + list(provider.invoke("hello", stream=True)) + + +def test_google_stream_events_emits_finish_for_top_level_finish_reason(): + events = GoogleProvider._stream_events_from_chunk({"finish_reason": "STOP"}) + + finish_events = [event for event in events if event.type == "finish"] + assert len(finish_events) == 1 + assert finish_events[0].finish_reason == "STOP" + + +def test_google_finish_reason_enum_is_normalized_to_wire_value(): + from google.genai import types + + candidate = SimpleNamespace( + finish_reason=types.FinishReason.STOP, + content=SimpleNamespace(parts=[]), + ) + response = SimpleNamespace(candidates=[candidate]) + + result = GoogleProvider._extract_result(response) + events = GoogleProvider._stream_events_from_chunk(response) + + assert result.finish_reason == "STOP" + assert [event.finish_reason for event in events if event.type == "finish"] == ["STOP"] + + +def test_google_ainvoke_non_stream_refusal_is_explicit_error(): + provider = GoogleProvider(model_name="gemini-1.5-flash") + response = SimpleNamespace( + candidates=[ + SimpleNamespace( + finish_reason="SAFETY", + content=SimpleNamespace(parts=[]), + ) + ] + ) + + class Models: + async def generate_content(self, **_kwargs): + return response + + client = SimpleNamespace(aio=SimpleNamespace(models=Models())) + + async def consume(): + with patch.object(provider, "_get_client", return_value=client): + return [chunk async for chunk in provider.ainvoke("hello", stream=False)] + + with pytest.raises(RuntimeError, match="安全策略|拒答"): + asyncio.run(consume()) + + +def test_google_ainvoke_stream_refusal_is_explicit_error(): + provider = GoogleProvider(model_name="gemini-1.5-flash") + chunk = SimpleNamespace( + candidates=[ + SimpleNamespace( + finish_reason="SAFETY", + content=SimpleNamespace(parts=[]), + ) + ] + ) + + class AsyncChunks: + def __aiter__(self): + async def chunks(): + yield chunk + + return chunks() + + class Models: + def generate_content_stream(self, **_kwargs): + return AsyncChunks() + + client = SimpleNamespace(aio=SimpleNamespace(models=Models())) + + async def consume(): + with patch.object(provider, "_get_client", return_value=client): + return [chunk async for chunk in provider.ainvoke("hello", stream=True)] + + with pytest.raises(RuntimeError, match="安全策略|拒答"): + asyncio.run(consume()) + + +def test_google_debug_config_mapping_is_converted_to_sdk_type(monkeypatch): + settings = MagicMock() + settings.google_api_key = "fake_key" + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + with patch.object(google_module.genai, "Client") as client_class: + client_class.return_value = MagicMock() + provider = GoogleProvider( + model_name="gemini-1.5-flash", + options={"debug_config": {"client_mode": "replay", "replay_id": "r1"}}, + ) + provider._get_client() + + debug_config = client_class.call_args.kwargs["debug_config"] + assert debug_config.client_mode == "replay" + assert debug_config.replay_id == "r1" + + +def test_google_unknown_option_fails_at_provider_boundary(monkeypatch): + settings = MagicMock() + settings.google_api_key = "fake_key" + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + + with pytest.raises(ValueError, match="不支持的字段.*typo_option"): + GoogleProvider(model_name="gemini-1.5-flash", options={"typo_option": True}) + + +@pytest.mark.parametrize("value", ["false", "0", "off", "no", ""]) +def test_google_vertex_mode_parses_false_string_values(value): + provider = GoogleProvider.__new__(GoogleProvider) + provider._options = {"vertexai": value} + + assert provider._configured_vertex_mode() is False + + +def test_google_endpoint_modes_reject_conflicting_explicit_flags(): + provider = GoogleProvider.__new__(GoogleProvider) + provider._options = {"enterprise": True, "vertexai": False} + + with pytest.raises(ValueError, match="enterprise.*vertexai.*冲突"): + provider._configured_vertex_mode() + + +def test_google_endpoint_modes_reject_conflicting_environment_flags(monkeypatch): + provider = GoogleProvider.__new__(GoogleProvider) + provider._options = {} + monkeypatch.setenv("GOOGLE_GENAI_USE_ENTERPRISE", "true") + monkeypatch.setenv("GOOGLE_GENAI_USE_VERTEXAI", "false") + + with pytest.raises(ValueError, match="GOOGLE_GENAI_USE_ENTERPRISE.*冲突"): + provider._configured_vertex_mode() + + +def test_google_get_client_reuses_falsey_client(monkeypatch): + settings = MagicMock() + settings.google_api_key = "fake_key" + monkeypatch.setattr(google_module, "get_settings", lambda: settings) + + class FalseyClient: + def __bool__(self): + return False + + client = FalseyClient() + provider = GoogleProvider.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {} + provider._client = client + + with patch.object(google_module.genai, "Client") as client_class: + assert provider._get_client() is client + client_class.assert_not_called() + def test_google_provider_invoke_non_stream(mock_settings, mock_genai_client): provider = GoogleProvider(model_name="gemini-1.5-flash") @@ -65,3 +463,28 @@ def test_google_provider_embed_documents(mock_settings, mock_genai_client): assert result == [[0.1, 0.2], [0.3, 0.4]] mock_genai_client.models.embed_content.assert_called_once() + + +def test_google_embedding_rejects_request_credentials_before_client_init(mock_settings): + provider = GoogleProvider(model_name="embedding-001") + + with ( + patch.object(provider, "_get_client", side_effect=AssertionError("client must not initialize")), + pytest.raises(ValueError, match="凭证"), + ): + provider.embed_documents( + ["text"], + extra_headers={"Authorization": "Bearer secret-token"}, + ) + + async def consume() -> None: + with ( + patch.object(provider, "_get_client", side_effect=AssertionError("client must not initialize")), + pytest.raises(ValueError, match="凭证"), + ): + await provider.aembed_documents( + ["text"], + extra_headers={"Authorization": "Bearer secret-token"}, + ) + + asyncio.run(consume()) diff --git a/tests/providers/test_protocol_adapters.py b/tests/providers/test_protocol_adapters.py new file mode 100644 index 0000000..15600e1 --- /dev/null +++ b/tests/providers/test_protocol_adapters.py @@ -0,0 +1,3087 @@ +import asyncio +import base64 +import inspect +import struct +from types import SimpleNamespace + +import pytest + +import src.providers.__base__.model_provider as model_provider_module +import src.providers.anthropic as anthropic_provider_module +import src.providers.openai_compatible as openai_compatible_module +import src.providers.volcengine as volcengine_module +from src.providers.__base__.model_provider import ( + CompletionRequest, + normalize_responses_input, +) +from src.providers.anthropic import AnthropicProvider +from src.providers.deepseek import DeepSeekProvider +from src.providers.google import GoogleProvider +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.volcengine import VolcengineProvider + +TOOLS = [ + { + "type": "function", + "function": { + "name": "lookup_order", + "description": "查询订单", + "parameters": { + "type": "object", + "properties": {"order_id": {"type": "string"}}, + "required": ["order_id"], + }, + }, + } +] + + +def test_anthropic_converts_openai_tool_schema(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + params = AnthropicProvider._build_message_params( + provider, "hello", "system", TOOLS, 0.2 + ) + + assert params["tools"] == [ + { + "name": "lookup_order", + "description": "查询订单", + "input_schema": TOOLS[0]["function"]["parameters"], + } + ] + + +def test_anthropic_parse_rejects_user_profile_on_non_beta_sdk_path(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + with pytest.raises(ValueError, match="parse 不支持 user"): + provider._build_message_params( + prompt="hello", + user="profile-1", + for_parse=True, + ) + + +def test_anthropic_rejects_unlinked_tool_result(): + with pytest.raises(ValueError, match="tool_call_id"): + AnthropicProvider._convert_messages( + [{"role": "tool", "content": "{}"}] + ) + + +def test_anthropic_rejects_malformed_tool_call_arguments(): + with pytest.raises(ValueError, match="arguments.*有效 JSON"): + AnthropicProvider._convert_messages( + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call-1", + "function": {"name": "lookup", "arguments": "{"}, + } + ], + } + ] + ) + + +def test_anthropic_rejects_image_content_without_url(): + with pytest.raises(ValueError, match="图片内容缺少有效 image_url"): + AnthropicProvider._convert_content( + [{"type": "image_url", "image_url": {}}] + ) + + +def test_anthropic_rejects_invalid_image_data_uri(): + with pytest.raises(ValueError, match="Base64"): + AnthropicProvider._convert_content( + [{"type": "image_url", "image_url": "data:image/png;base64,invalid"}] + ) + + +def test_anthropic_rejects_tool_call_without_id_or_name(): + with pytest.raises(ValueError, match="缺少 id"): + AnthropicProvider._convert_messages( + [ + { + "role": "assistant", + "tool_calls": [{"function": {"name": "lookup", "arguments": "{}"}}], + } + ] + ) + with pytest.raises(ValueError, match="function.name"): + AnthropicProvider._convert_messages( + [ + { + "role": "assistant", + "tool_calls": [{"id": "call-1", "function": {"arguments": "{}"}}], + } + ] + ) + + +@pytest.mark.parametrize( + "tool", + [ + None, + {"function": "not-an-object"}, + {"function": {"name": "lookup", "parameters": []}}, + ], +) +def test_anthropic_rejects_malformed_tool_definitions(tool): + with pytest.raises(ValueError, match="工具定义"): + AnthropicProvider._convert_tools([tool]) + + +@pytest.mark.parametrize( + "tool_choice", + ["unsupported", {"type": "function"}, {"type": "tool"}], +) +def test_anthropic_rejects_malformed_tool_choices(tool_choice): + with pytest.raises(ValueError, match="tool_choice"): + AnthropicProvider._convert_tool_choice(tool_choice) + + +def test_anthropic_stream_errors_are_explicit(): + with pytest.raises(RuntimeError, match="上游失败"): + AnthropicProvider._stream_event( + SimpleNamespace( + type="session.error", + error=SimpleNamespace(message="上游失败"), + ) + ) + + +def test_google_converts_openai_tool_schema(): + converted = GoogleProvider._convert_tools(TOOLS) + + assert converted is not None + declaration = converted[0].function_declarations[0] + assert declaration.name == "lookup_order" + assert declaration.description == "查询订单" + assert declaration.parameters_json_schema == TOOLS[0]["function"]["parameters"] + + +def test_google_converts_standard_tool_result_to_function_response(): + contents = GoogleProvider._contents( + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call-1", + "function": { + "name": "lookup_order", + "arguments": '{"order_id":"A-1"}', + }, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call-1", + "content": '{"status":"paid"}', + }, + ], + None, + None, + ) + + assert contents[0].role == "model" + function_call = next(part.function_call for part in contents[0].parts if part.function_call) + assert function_call.name == "lookup_order" + function_response = contents[1].parts[0].function_response + assert contents[1].role == "user" + assert function_response.name == "lookup_order" + assert function_response.id == "call-1" + assert function_response.response == {"status": "paid"} + + +def test_google_merges_parallel_tool_results_into_one_user_turn(): + contents = GoogleProvider._contents( + [ + { + "role": "assistant", + "tool_calls": [ + {"id": "call-1", "function": {"name": "lookup_order", "arguments": "{}"}}, + {"id": "call-2", "function": {"name": "lookup_customer", "arguments": "{}"}}, + ], + }, + {"role": "tool", "tool_call_id": "call-1", "content": '{"status":"paid"}'}, + {"role": "tool", "tool_call_id": "call-2", "content": '{"name":"Ada"}'}, + ], + None, + None, + ) + + assert [content.role for content in contents] == ["model", "user"] + assert len(contents[1].parts) == 2 + assert [part.function_response.name for part in contents[1].parts] == [ + "lookup_order", + "lookup_customer", + ] + + +def test_google_merges_consecutive_same_role_messages(): + contents = GoogleProvider._contents( + [ + {"role": "user", "content": "第一条"}, + {"role": "user", "content": "第二条"}, + {"role": "assistant", "content": "答复"}, + ], + None, + None, + ) + + assert [content.role for content in contents] == ["user", "model"] + assert [part.text for part in contents[0].parts] == ["第一条", "第二条"] + + +def test_google_rejects_external_media_urls_and_unknown_part_types(): + with pytest.raises(ValueError, match="仅支持 data: URI、gs:// URI 或 Google Files URI"): + GoogleProvider._contents( + [ + { + "role": "user", + "content": [{"type": "image_url", "image_url": "https://example.com/a.png"}], + } + ], + None, + None, + ) + + allowed = GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + {"type": "image_url", "image_url": "gs://bucket/a.png"}, + { + "type": "file", + "url": "https://generativelanguage.googleapis.com/v1beta/files/file-1", + }, + ], + } + ], + None, + None, + ) + assert allowed[0].parts[0].file_data.file_uri == "gs://bucket/a.png" + assert allowed[0].parts[1].file_data.file_uri.endswith("/files/file-1") + + with pytest.raises(ValueError, match="不支持的消息内容块类型"): + GoogleProvider._contents( + [{"role": "user", "content": [{"type": "unknown_part", "value": "x"}]}], + None, + None, + ) + + +def test_google_media_file_ids_are_resolved_and_mime_types_forwarded(): + contents = GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + { + "type": "input_image", + "image_url": {"file_id": "file-image", "mime_type": "image/png"}, + }, + { + "type": "file", + "file": {"file_id": "file-document", "mime_type": "application/pdf"}, + }, + ], + } + ], + None, + None, + ) + + image_data = contents[0].parts[0].file_data + document_data = contents[0].parts[1].file_data + assert image_data.file_uri.endswith("/v1beta/files/file-image") + assert image_data.mime_type == "image/png" + assert document_data.file_uri.endswith("/v1beta/files/file-document") + assert document_data.mime_type == "application/pdf" + + +def test_google_vertex_rejects_gemini_files_ids(): + for value in ( + "file-image", + "files/file-image", + "https://generativelanguage.googleapis.com/v1beta/files/file-image", + ): + with pytest.raises(ValueError, match="Vertex/Enterprise"): + GoogleProvider._contents( + [{"role": "user", "content": [{"type": "input_image", "image_url": value}]}], + None, + None, + vertex_mode=True, + ) + + +def test_google_vertex_accepts_cloud_storage_and_public_https_media(): + contents = GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + {"type": "input_image", "image_url": "gs://bucket/image.png"}, + {"type": "file", "url": "https://cdn.example.com/document.pdf?version=1"}, + ], + } + ], + None, + None, + vertex_mode=True, + ) + + assert contents[0].parts[0].file_data.file_uri == "gs://bucket/image.png" + assert contents[0].parts[1].file_data.file_uri == "https://cdn.example.com/document.pdf?version=1" + + +@pytest.mark.parametrize("file_id", ["", " ", "files/", "file/id"]) +def test_google_media_file_ids_reject_invalid_values(file_id): + with pytest.raises(ValueError, match="file_id|有效 URI"): + GoogleProvider._contents( + [{"role": "user", "content": [{"type": "input_image", "file_id": file_id}]}], + None, + None, + ) + + +def test_google_rejects_malformed_tool_arguments_and_results(): + with pytest.raises(ValueError, match="arguments.*有效 JSON"): + GoogleProvider._contents( + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "call-1", + "function": {"name": "lookup", "arguments": "{"}, + } + ], + } + ], + None, + None, + ) + + with pytest.raises(ValueError, match="content.*有效 JSON"): + GoogleProvider._contents( + [ + {"role": "tool", "tool_call_id": "call-1", "name": "lookup", "content": "{"} + ], + None, + None, + ) + + +def test_google_rejects_invalid_data_uri_base64(): + with pytest.raises(ValueError, match="Base64"): + GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + {"type": "image_url", "image_url": "data:image/png;base64,not-base64"} + ], + } + ], + None, + None, + ) + + +@pytest.mark.parametrize( + "part", + [ + {"type": "image_url", "image_url": {}}, + {"type": "audio_url"}, + {"type": "video_url", "video_url": {}}, + {"type": "file", "file": {}}, + ], +) +def test_google_rejects_media_content_without_uri(part): + with pytest.raises(ValueError, match="(图片|媒体)内容缺少有效 URI"): + GoogleProvider._contents( + [{"role": "user", "content": [part]}], + None, + None, + ) + + +def test_google_decodes_media_data_uri_exactly_once(): + contents = GoogleProvider._contents( + [ + { + "role": "user", + "content": [ + {"type": "audio_url", "audio_url": "data:audio/wav;base64,YWJj"} + ], + } + ], + None, + None, + ) + + blob = contents[0].parts[0].inline_data + assert blob.mime_type == "audio/wav" + assert blob.data == b"abc" + + +def test_google_tool_call_message_with_null_content_does_not_emit_text(): + contents = GoogleProvider._contents( + [ + { + "role": "assistant", + "content": None, + "tool_calls": [ + { + "id": "call-1", + "function": {"name": "lookup", "arguments": "{}"}, + } + ], + } + ], + None, + None, + ) + + assert len(contents[0].parts) == 1 + assert contents[0].parts[0].function_call.name == "lookup" + + +def test_google_rejects_malformed_function_tool_choice(): + with pytest.raises(ValueError, match="function.name"): + GoogleProvider._convert_tool_choice( + {"type": "function", "function": {}} + ) + + +def test_anthropic_merges_parallel_tool_results_into_one_user_turn(): + converted = AnthropicProvider._convert_messages( + [ + { + "role": "assistant", + "tool_calls": [ + {"id": "call-1", "function": {"name": "lookup_order", "arguments": "{}"}}, + {"id": "call-2", "function": {"name": "lookup_customer", "arguments": "{}"}}, + ], + }, + {"role": "tool", "tool_call_id": "call-1", "content": '{"status":"paid"}'}, + {"role": "tool", "tool_call_id": "call-2", "content": '{"name":"Ada"}'}, + ] + ) + + assert [message["role"] for message in converted] == ["assistant", "user"] + assert [block["tool_use_id"] for block in converted[1]["content"]] == ["call-1", "call-2"] + + +def test_volcengine_uses_ark_endpoint_and_ak_sk(monkeypatch): + settings = SimpleNamespace( + volc_access_key="access", + volc_secret_key="secret", + volc_base_url="https://ark.example/v3", + ) + monkeypatch.setitem(VolcengineProvider.__init__.__globals__, "get_settings", lambda: settings) + + provider = VolcengineProvider("doubao-model") + + assert provider._client_options() == { + "base_url": "https://ark.example/v3", + "ak": "access", + "sk": "secret", + } + assert provider._build_chat_request("hello", None, TOOLS, 0.4, False) == { + "model": "doubao-model", + "messages": [{"role": "user", "content": "hello"}], + "temperature": 0.4, + "stream": False, + "tools": TOOLS, + } + + +def _responses_provider() -> OpenAICompatibleProvider: + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "responses-model" + provider._provider = "openai" + provider._protocol = "responses" + return provider + + +def test_responses_request_converts_chat_tools_and_sets_strict_default(): + provider = _responses_provider() + + assert provider._build_responses_request("hello", "system", TOOLS, 0.4, True) == { + "model": "responses-model", + "input": "hello", + "instructions": "system", + "temperature": 0.4, + "stream": True, + "tools": [ + { + "type": "function", + "name": "lookup_order", + "description": "查询订单", + "parameters": TOOLS[0]["function"]["parameters"], + "strict": False, + } + ], + } + + +def test_responses_request_omits_implicit_temperature(): + provider = _responses_provider() + + request = provider._build_responses_request("hello", stream=False) + + assert "temperature" not in request + + +def test_responses_request_preserves_configured_temperature(): + provider = _responses_provider() + provider._options = {"temperature": 0.2} + + request = provider._build_responses_request("hello", stream=False) + + assert request["temperature"] == 0.2 + + +def _tool_call_history(): + return [ + {"role": "user", "content": "查订单"}, + { + "role": "assistant", + "content": "我来查询。", + "tool_calls": [ + { + "id": "call-1", + "type": "function", + "function": { + "name": "lookup_order", + "arguments": '{"order_id":"A-1"}', + }, + }, + { + "id": "call-2", + "type": "function", + "function": { + "name": "lookup_customer", + "arguments": {"customer_id": "C-1"}, + }, + }, + ], + }, + { + "role": "tool", + "tool_call_id": "call-1", + "content": '{"status":"paid"}', + }, + { + "role": "tool", + "tool_call_id": "call-2", + "content": {"name": "Ada"}, + }, + ] + + +def test_responses_input_converts_chat_tool_messages_and_parallel_calls(): + converted = normalize_responses_input(None, None, _tool_call_history()) + + assert converted == [ + {"role": "user", "content": "查订单"}, + {"role": "assistant", "content": "我来查询。"}, + { + "type": "function_call", + "call_id": "call-1", + "name": "lookup_order", + "arguments": '{"order_id":"A-1"}', + }, + { + "type": "function_call", + "call_id": "call-2", + "name": "lookup_customer", + "arguments": '{"customer_id":"C-1"}', + }, + { + "type": "function_call_output", + "call_id": "call-1", + "output": '{"status":"paid"}', + }, + { + "type": "function_call_output", + "call_id": "call-2", + "output": '{"name":"Ada"}', + }, + ] + + +def test_responses_input_keeps_prompt_string_and_validates_native_items(): + assert normalize_responses_input("hello", "system", None) == "hello" + assert normalize_responses_input( + None, + None, + [ + { + "type": "function_call", + "call_id": "call-1", + "name": "lookup", + "arguments": "{}", + }, + { + "type": "function_call_output", + "call_id": "call-1", + "output": "done", + }, + ], + ) == [ + { + "type": "function_call", + "call_id": "call-1", + "name": "lookup", + "arguments": "{}", + }, + { + "type": "function_call_output", + "call_id": "call-1", + "output": "done", + }, + ] + + +def test_responses_input_converts_chat_multimodal_content_blocks(): + converted = normalize_responses_input( + None, + None, + [ + { + "role": "user", + "content": [ + {"type": "text", "text": "请阅读图片和文件。"}, + { + "type": "image_url", + "image_url": { + "url": "https://example.com/image.png", + "detail": "high", + }, + }, + { + "type": "file", + "file": { + "file_id": "file-1", + "filename": "说明.txt", + }, + }, + ], + } + ], + ) + + assert converted == [ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "请阅读图片和文件。"}, + { + "type": "input_image", + "detail": "high", + "image_url": "https://example.com/image.png", + }, + { + "type": "input_file", + "file_id": "file-1", + "filename": "说明.txt", + }, + ], + } + ] + + from openai.types.responses.response_input_item_param import ResponseInputItemParam + from pydantic import TypeAdapter + + TypeAdapter(list[ResponseInputItemParam]).validate_python(converted) + + +def test_responses_input_keeps_native_multimodal_items_in_sdk_shape(): + converted = normalize_responses_input( + None, + None, + [ + { + "role": "user", + "content": [ + {"type": "input_text", "text": "hello"}, + {"type": "input_image", "file_id": "file-2"}, + {"type": "input_file", "file_url": "https://example.com/a.pdf"}, + ], + } + ], + ) + + assert converted[0]["content"] == [ + {"type": "input_text", "text": "hello"}, + {"type": "input_image", "detail": "auto", "file_id": "file-2"}, + {"type": "input_file", "file_url": "https://example.com/a.pdf"}, + ] + + +def test_responses_input_converts_previous_output_text_for_follow_up_messages(): + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "content": [{"type": "output_text", "text": "上一次答复"}], + }, + { + "role": "tool", + "tool_call_id": "call-1", + "content": [ + {"type": "text", "text": "工具输出"}, + {"type": "file", "url": "https://example.com/result.txt"}, + ], + }, + ], + ) + + assert converted == [ + { + "role": "assistant", + "content": [{"type": "input_text", "text": "上一次答复"}], + }, + { + "type": "function_call_output", + "call_id": "call-1", + "output": [ + {"type": "input_text", "text": "工具输出"}, + { + "type": "input_file", + "file_url": "https://example.com/result.txt", + }, + ], + }, + ] + + +@pytest.mark.parametrize( + "content", + [ + [{"type": "audio_url", "audio_url": "https://example.com/audio.wav"}], + [{"type": "video_url", "video_url": "https://example.com/video.mp4"}], + [{"type": "unknown_part", "value": "x"}], + [{"type": "image_url", "image_url": {}}], + [{"type": "input_file"}], + ], +) +def test_responses_input_rejects_unsupported_or_incomplete_content_blocks(content): + with pytest.raises(ValueError, match="(不受 Responses SDK 支持|缺少)"): + normalize_responses_input( + None, + None, + [{"role": "user", "content": content}], + ) + + +@pytest.mark.parametrize( + "messages", + [ + [{"role": "tool", "content": "missing id"}], + [{"role": "assistant", "tool_calls": [{"function": {"arguments": "{}"}}]}], + [{"role": "assistant", "tool_calls": [{"id": "call-1", "function": {"name": "lookup", "arguments": "{"}}]}], + [{"type": "function_call", "id": "item-1", "name": "lookup", "arguments": "{}"}], + ], +) +def test_responses_input_rejects_unlinked_or_malformed_tool_items(messages): + with pytest.raises(ValueError, match="(call_id|有效 JSON|函数名)"): + normalize_responses_input(None, None, messages) + + +def test_openai_responses_request_converts_tool_history(): + provider = _responses_provider() + + request = provider._build_responses_request( + messages=_tool_call_history(), + stream=False, + ) + + assert request["input"][2]["type"] == "function_call" + assert request["input"][2]["call_id"] == "call-1" + assert request["input"][4] == { + "type": "function_call_output", + "call_id": "call-1", + "output": '{"status":"paid"}', + } + + +def test_openai_responses_token_count_converts_tool_history(): + provider = _responses_provider() + params = provider._build_token_count_request( + CompletionRequest(messages=_tool_call_history(), system_prompt=None) + ) + + assert params["input"][2]["type"] == "function_call" + assert params["input"][4]["type"] == "function_call_output" + + +def test_openai_responses_token_count_converts_multimodal_content(): + provider = _responses_provider() + params = provider._build_token_count_request( + CompletionRequest( + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "看图"}, + {"type": "image_url", "image_url": "https://example.com/a.png"}, + ], + } + ], + system_prompt=None, + ) + ) + + assert params["input"][0]["content"] == [ + {"type": "input_text", "text": "看图"}, + { + "type": "input_image", + "detail": "auto", + "image_url": "https://example.com/a.png", + }, + ] + + +def test_openai_responses_request_converts_multimodal_content(): + provider = _responses_provider() + request = provider._build_responses_request( + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "看图"}, + {"type": "image_url", "image_url": "https://example.com/a.png"}, + ], + } + ], + stream=False, + ) + + assert request["input"][0]["content"] == [ + {"type": "input_text", "text": "看图"}, + { + "type": "input_image", + "detail": "auto", + "image_url": "https://example.com/a.png", + }, + ] + + +def test_ark_responses_request_converts_tool_history(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"server_verified_protocols": ["responses"]} + + request = provider._build_responses_request( + CompletionRequest(messages=_tool_call_history(), system_prompt=None, stream=False) + ) + + assert request["input"][2]["type"] == "function_call" + assert request["input"][2]["call_id"] == "call-1" + assert request["input"][4]["type"] == "function_call_output" + + +def test_ark_responses_request_omits_implicit_temperature(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"server_verified_protocols": ["responses"]} + + request = provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False) + ) + + assert "temperature" not in request + + +def test_ark_responses_request_preserves_configured_temperature(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "server_verified_protocols": ["responses"], + "temperature": 0.2, + } + + request = provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False) + ) + + assert request["temperature"] == 0.2 + + +def test_ark_responses_request_converts_multimodal_content(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"server_verified_protocols": ["responses"]} + + request = provider._build_responses_request( + CompletionRequest( + messages=[ + { + "role": "user", + "content": [ + {"type": "text", "text": "看图"}, + {"type": "image_url", "image_url": "https://example.com/a.png"}, + ], + } + ], + system_prompt=None, + stream=False, + ) + ) + + assert request["input"][0]["content"] == [ + {"type": "input_text", "text": "看图"}, + { + "type": "input_image", + "detail": "auto", + "image_url": "https://example.com/a.png", + }, + ] + + request = provider._build_responses_request( + CompletionRequest( + messages=[ + { + "role": "user", + "content": [ + {"type": "input_audio", "audio_url": "https://example.com/a.wav"}, + {"type": "input_video", "file_id": "file-video", "fps": 1.5}, + ], + } + ], + system_prompt=None, + stream=False, + ) + ) + assert request["input"][0]["content"] == [ + {"type": "input_audio", "audio_url": "https://example.com/a.wav"}, + {"type": "input_video", "file_id": "file-video", "fps": 1.5}, + ] + + +def test_ark_responses_input_supports_audio_video_and_image_pixel_limit(): + converted = normalize_responses_input( + None, + None, + [ + { + "role": "user", + "content": [ + { + "type": "audio_url", + "audio_url": {"url": "https://example.com/a.wav"}, + }, + { + "type": "video_url", + "video_url": "https://example.com/v.mp4", + "fps": 2.0, + }, + { + "type": "image_url", + "image_url": "https://example.com/i.png", + "detail": "high", + "image_pixel_limit": { + "min_pixels": 1024, + "max_pixels": 4096, + }, + }, + ], + } + ], + provider="ark", + ) + + assert converted[0]["content"] == [ + {"type": "input_audio", "audio_url": "https://example.com/a.wav"}, + { + "type": "input_video", + "video_url": "https://example.com/v.mp4", + "fps": 2.0, + }, + { + "type": "input_image", + "detail": "high", + "image_url": "https://example.com/i.png", + "image_pixel_limit": {"min_pixels": 1024, "max_pixels": 4096}, + }, + ] + + from pydantic import TypeAdapter + from volcenginesdkarkruntime.types.responses.response_input_param import ( + ResponseInputParam, + ) + + TypeAdapter(ResponseInputParam).validate_python(converted) + + +def test_openai_responses_rejects_ark_only_multimodal_content(): + with pytest.raises(ValueError, match="不受 Responses SDK 支持"): + normalize_responses_input( + None, + None, + [ + { + "role": "user", + "content": [ + {"type": "input_audio", "audio_url": "https://example.com/a.wav"} + ], + } + ], + ) + + +def test_responses_request_converts_chat_tool_choice_to_flat_function_shape(): + provider = _responses_provider() + + request = provider._build_responses_request( + "hello", + tools=TOOLS, + tool_choice={"type": "function", "function": {"name": "lookup_order"}}, + stream=False, + ) + + assert request["tool_choice"] == {"type": "function", "name": "lookup_order"} + + +def test_responses_request_preserves_native_tool_choice_forms(): + provider = _responses_provider() + + assert provider._build_responses_request("hello", tool_choice="required", stream=False)[ + "tool_choice" + ] == "required" + assert provider._build_responses_request( + "hello", tool_choice={"type": "allowed_tools", "mode": "auto"}, stream=False + )["tool_choice"] == {"type": "allowed_tools", "mode": "auto"} + + +def test_responses_rejects_malformed_tool_definitions(): + provider = _responses_provider() + with pytest.raises(ValueError, match="工具定义"): + provider._build_responses_request("hello", tools=[None], stream=False) + with pytest.raises(ValueError, match="function"): + provider._build_responses_request( + "hello", tools=[{"type": "function", "function": "bad"}], stream=False + ) + + +def test_ark_model_sampling_options_merge_into_extra_body(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "server_verified_protocols": ["responses"], + "top_k": 20, + "seed": 7, + "extra_body": {"vendor_flag": True}, + } + request = provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False, top_k=30) + ) + assert request["extra_body"] == {"vendor_flag": True, "top_k": 30, "seed": 7} + assert "server_verified_protocols" not in request + + +def test_openai_responses_rejects_conflicting_model_length_aliases(): + provider = _responses_provider() + provider._options = {"max_tokens": 64, "max_completion_tokens": 128} + + with pytest.raises(ValueError, match="max_tokens.*max_completion_tokens"): + provider._build_responses_request("hello", stream=False) + + +def test_ark_responses_rejects_conflicting_model_length_aliases(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "max_tokens": 64, + "max_completion_tokens": 128, + } + + with pytest.raises(ValueError, match="max_tokens.*max_completion_tokens"): + provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False) + ) + + +def test_ark_extra_body_sampling_conflicts_are_explicit(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"extra_body": {"seed": 1}} + with pytest.raises(ValueError, match="seed.*重复"): + provider._build_responses_request( + CompletionRequest(prompt="hello", stream=False, seed=2) + ) + + +def test_responses_sync_non_stream_extracts_output_text(): + provider = _responses_provider() + calls = [] + + class Responses: + def create(self, **request): + calls.append(request) + return SimpleNamespace(status="completed", output_text="答复") + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + assert list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": False})) == ["答复"] + assert calls == [{"model": "responses-model", "input": "hello", "stream": False}] + + +def test_responses_complete_uses_response_status_as_finish_reason(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return SimpleNamespace( + id="resp-1", + status="completed", + output_text="答复", + usage=SimpleNamespace(input_tokens=2, output_tokens=3, total_tokens=5), + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + result = provider.complete(CompletionRequest(prompt="hello")) + + assert result.text == "答复" + assert result.finish_reason == "completed" + assert result.response_id == "resp-1" + assert result.usage["total_tokens"] == 5 + + +def test_openai_responses_complete_and_stream_paths_convert_tool_history(): + history = _tool_call_history() + provider = _responses_provider() + calls = [] + + class Responses: + def create(self, **request): + calls.append(request) + if request["stream"]: + return iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ) + ] + ) + return SimpleNamespace(status="completed", output_text="答复") + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + assert provider.complete( + CompletionRequest(messages=history, system_prompt=None) + ).text == "答复" + list(provider.stream_events(CompletionRequest(messages=history, system_prompt=None))) + + assert calls[0]["input"][2]["type"] == "function_call" + assert calls[0]["input"][4]["type"] == "function_call_output" + assert calls[1]["input"] == calls[0]["input"] + + +def test_openai_responses_async_complete_and_stream_paths_convert_tool_history(): + history = _tool_call_history() + provider = _responses_provider() + calls = [] + + class Responses: + async def create(self, **request): + calls.append(request) + if request["stream"]: + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ) + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + return SimpleNamespace(status="completed", output_text="异步答复") + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + result = await provider.acomplete( + CompletionRequest(messages=history, system_prompt=None) + ) + events = [ + event + async for event in provider.astream_events( + CompletionRequest(messages=history, system_prompt=None) + ) + ] + return result, events + + result, events = asyncio.run(collect()) + + assert result.text == "异步答复" + assert events[-1].type == "finish" + assert calls[0]["input"][2]["type"] == "function_call" + assert calls[1]["input"][4]["type"] == "function_call_output" + + +def test_openai_responses_async_stream_drains_after_completion(): + provider = _responses_provider() + streams = [] + + class Responses: + async def create(self, **_request): + class Stream: + def __init__(self): + self._events = iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + SimpleNamespace(type="response.output_text.done"), + ] + ) + self.exhausted = False + self.closed = False + + def __aiter__(self): + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + self.exhausted = True + raise StopAsyncIteration from exc + + async def close(self): + self.closed = True + + stream = Stream() + streams.append(stream) + return stream + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [ + event + async for event in provider.astream_events( + CompletionRequest(prompt="hello") + ) + ] + + events = asyncio.run(collect()) + + assert [event.type for event in events] == ["finish"] + assert streams[0].exhausted is True + assert streams[0].closed is True + + +def test_ark_responses_complete_and_stream_paths_convert_tool_history(): + history = _tool_call_history() + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + def create(self, **request): + calls.append(request) + if request["stream"]: + return iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ) + ] + ) + return SimpleNamespace(status="completed", output_text="答复") + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + assert provider.complete( + CompletionRequest(messages=history, system_prompt=None) + ).text == "答复" + list(provider.stream_events(CompletionRequest(messages=history, system_prompt=None))) + + assert calls[0]["input"][2]["type"] == "function_call" + assert calls[0]["input"][4]["type"] == "function_call_output" + + +def test_ark_responses_async_complete_and_stream_paths_convert_tool_history(): + history = _tool_call_history() + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + async def create(self, **request): + calls.append(request) + if request["stream"]: + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ) + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + return SimpleNamespace(status="completed", output_text="异步答复") + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + result = await provider.acomplete( + CompletionRequest(messages=history, system_prompt=None) + ) + events = [ + event + async for event in provider.astream_events( + CompletionRequest(messages=history, system_prompt=None) + ) + ] + return result, events + + result, events = asyncio.run(collect()) + + assert result.text == "异步答复" + assert events[-1].type == "finish" + assert calls[0]["input"][2]["type"] == "function_call" + assert calls[1]["input"][4]["type"] == "function_call_output" + + +def test_openai_chat_stream_events_include_tool_usage_and_finish(): + provider = OpenAICompatibleProvider.__new__(OpenAICompatibleProvider) + provider._model_name = "chat-model" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content="答", tool_calls=None), + finish_reason=None, + ) + ] + ), + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + content=None, + tool_calls=[ + SimpleNamespace( + id="call-1", + index=0, + function=SimpleNamespace(name="lookup", arguments='{"id":'), + ) + ], + ), + finish_reason=None, + ) + ] + ), + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + content=None, + tool_calls=[ + SimpleNamespace( + id=None, + index=0, + function=SimpleNamespace(name=None, arguments='1}'), + ) + ], + ), + finish_reason=None, + ) + ] + ), + SimpleNamespace( + choices=[SimpleNamespace(delta=SimpleNamespace(content=None), finish_reason="tool_calls")], + usage=SimpleNamespace(prompt_tokens=2, completion_tokens=3, total_tokens=5), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == [ + "text_delta", "tool_call_delta", "tool_call_delta", "tool_call_completed", "finish" + ] + assert events[1].tool_call["name"] == "lookup" + assert events[3].tool_call["arguments"] == '{"id":1}' + assert events[4].finish_reason == "tool_calls" + assert events[4].usage["total_tokens"] == 5 + + +def test_openai_chat_stream_events_preserve_text_and_finish_from_same_chunk(): + events = OpenAICompatibleProvider._chat_stream_events( + SimpleNamespace( + id="chat-1", + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content="答", reasoning_content=None, refusal=None, tool_calls=None), + finish_reason="stop", + ) + ], + ) + ) + + assert [event.type for event in events] == ["text_delta", "finish"] + assert events[0].text == "答" + assert events[1].finish_reason == "stop" + + +def test_openai_chat_stream_error_event_is_explicit(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "chat-model" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="error", + error=SimpleNamespace(message="chat upstream failed", status=400), + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + with pytest.raises(RuntimeError, match="chat upstream failed"): + list(provider.stream_events(CompletionRequest(prompt="hello"))) + + with pytest.raises(RuntimeError, match="chat upstream failed"): + OpenAICompatibleProvider._chat_stream_events( + SimpleNamespace(error=SimpleNamespace(message="chat upstream failed")) + ) + + +def test_ark_chat_stream_error_event_is_explicit(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._provider = "volcengine" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="error", + error=SimpleNamespace(message="ark upstream failed", status=400), + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + with pytest.raises(RuntimeError, match="ark upstream failed"): + list(provider.stream_events(CompletionRequest(prompt="hello"))) + + with pytest.raises(RuntimeError, match="ark upstream failed"): + VolcengineProvider._chat_stream_events( + SimpleNamespace(error=SimpleNamespace(message="ark upstream failed")) + ) + + +def test_openai_async_chat_stream_error_event_is_explicit(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "chat-model" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + async def create(self, **_request): + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + type="error", + error=SimpleNamespace(message="async chat upstream failed", status=400), + ) + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hello"))] + + with pytest.raises(RuntimeError, match="async chat upstream failed"): + asyncio.run(collect()) + + +def test_ark_async_chat_stream_error_event_is_explicit(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + async def create(self, **_request): + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + type="error", + error=SimpleNamespace(message="async ark upstream failed", status=400), + ) + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hello"))] + + with pytest.raises(RuntimeError, match="async ark upstream failed"): + asyncio.run(collect()) + + +def test_ark_chat_stream_events_preserve_text_and_finish_from_same_chunk(): + events = VolcengineProvider._chat_stream_events( + SimpleNamespace( + id="chat-1", + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content="答", reasoning_content=None, tool_calls=None), + finish_reason="stop", + ) + ], + ) + ) + + assert [event.type for event in events] == ["text_delta", "finish"] + + +def test_openai_chat_stream_events_propagate_refusal_delta(): + events = OpenAICompatibleProvider._chat_stream_events( + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content=None, refusal="不能处理"), + finish_reason=None, + ) + ] + ) + ) + + assert len(events) == 1 + assert events[0].type == "refusal_delta" + assert events[0].refusal == "不能处理" + + +def test_openai_chat_stream_events_preserve_parallel_tool_calls(): + chunk = SimpleNamespace( + id="chat-1", + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + content=None, + tool_calls=[ + SimpleNamespace( + id="call-1", index=0, + function=SimpleNamespace(name="lookup", arguments='{"id":'), + ), + SimpleNamespace( + id="call-2", index=1, + function=SimpleNamespace(name="refund", arguments='{"order":'), + ), + ], + ), + finish_reason=None, + ) + ], + ) + + events = OpenAICompatibleProvider._chat_stream_events(chunk) + + assert [event.type for event in events] == ["tool_call_delta", "tool_call_delta"] + assert [event.tool_call["id"] for event in events] == ["call-1", "call-2"] + assert [event.tool_call["index"] for event in events] == [0, 1] + + +def test_openai_responses_stream_exposes_tool_delta_and_completed_events(): + assert OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.output_item.added", + item=SimpleNamespace(type="function_call", call_id="call-1", name="lookup"), + ) + ) is None + + delta = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.function_call_arguments.delta", + item_id="item-1", + output_index=2, + delta='{"id":', + ) + ) + completed = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.function_call_arguments.done", + item_id="item-1", + output_index=2, + item=SimpleNamespace( + type="function_call", + call_id="call-1", + name="lookup", + arguments='{"id":1}', + ), + ) + ) + + assert delta.type == "tool_call_delta" + assert delta.tool_call["index"] == 2 + assert completed.type == "tool_call_completed" + assert completed.tool_call["id"] == "call-1" + assert completed.tool_call["arguments"] == '{"id":1}' + + +def test_openai_responses_stream_deduplicates_tool_completion_events(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + item = SimpleNamespace( + type="function_call", + call_id="call-1", + name="lookup", + arguments='{"id":1}', + ) + return iter( + [ + SimpleNamespace(type="response.output_item.done", item=item, output_index=0), + SimpleNamespace( + type="response.function_call_arguments.done", + item=item, + output_index=0, + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["tool_call_completed", "finish"] + + +def test_openai_responses_stream_flushes_tool_delta_before_finish(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="response.function_call_arguments.delta", + item_id="item-1", + output_index=0, + name="lookup", + delta='{"id":1}', + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["tool_call_delta", "tool_call_completed", "finish"] + + +def test_ark_responses_stream_flushes_tool_delta_before_finish(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="response.function_call_arguments.delta", + item_id="item-1", + output_index=0, + name="lookup", + delta='{"id":1}', + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["tool_call_delta", "tool_call_completed", "finish"] + + +def test_ark_responses_stream_deduplicates_tool_completion_events(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + def create(self, **_request): + item = SimpleNamespace( + type="function_call", + call_id="call-1", + name="lookup", + arguments='{"id":1}', + ) + return iter( + [ + SimpleNamespace( + type="response.function_call_arguments.delta", + item_id="item-1", + output_index=0, + delta='{"id":', + ), + SimpleNamespace( + type="response.function_call_arguments.done", + item_id="item-1", + output_index=0, + name="lookup", + arguments='{"id":1}', + ), + SimpleNamespace( + type="response.output_item.done", + item=item, + output_index=0, + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["tool_call_delta", "tool_call_completed", "finish"] + assert events[1].tool_call["id"] == "call-1" + assert events[1].tool_call["arguments"] == '{"id":1}' + + +def test_openai_responses_arguments_done_is_complete_without_output_item(): + event = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.function_call_arguments.done", + item_id="item-1", + output_index=2, + name="lookup", + arguments='{"id":1}', + ) + ) + + assert event is not None + assert event.type == "tool_call_completed" + assert event.tool_call == { + "id": "item-1", + "index": 2, + "type": "function_call", + "name": "lookup", + "arguments": '{"id":1}', + } + + +def test_openai_responses_custom_tool_stream_keeps_added_metadata_and_raw_input(): + metadata = {} + added = SimpleNamespace( + type="response.output_item.added", + output_index=1, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="", + ), + ) + + assert OpenAICompatibleProvider._responses_stream_event(added, metadata) is None + delta = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.custom_tool_call_input.delta", + item_id="item-1", + output_index=1, + delta="echo ", + ), + metadata, + ) + completed = OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=1, + input="hello world", + ), + metadata, + ) + + assert delta is not None and delta.tool_call == { + "id": "item-1", + "call_id": "call-1", + "index": 1, + "type": "custom_tool_call", + "name": "run_command", + "arguments": "echo ", + } + assert completed is not None and completed.tool_call == { + "id": "item-1", + "index": 1, + "type": "custom_tool_call", + "name": "run_command", + "arguments": "hello world", + "call_id": "call-1", + } + + +def test_openai_responses_custom_tool_stream_events_emit_non_json_completion(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + item = SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="echo hello", + ) + return iter( + [ + SimpleNamespace( + type="response.output_item.added", + output_index=0, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="", + ), + ), + SimpleNamespace( + type="response.custom_tool_call_input.delta", + item_id="item-1", + output_index=0, + delta="echo ", + ), + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=0, + input="echo hello", + ), + SimpleNamespace( + type="response.output_item.done", + output_index=0, + item=item, + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == [ + "tool_call_delta", + "tool_call_completed", + "finish", + ] + assert events[0].tool_call["name"] == "run_command" + assert events[1].tool_call["arguments"] == "echo hello" + assert events[1].tool_call["type"] == "custom_tool_call" + + +def test_openai_responses_custom_tool_empty_input_is_valid(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="response.output_item.added", + output_index=0, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="ping", + input="", + ), + ), + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=0, + input="", + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert events[0].type == "tool_call_completed" + assert events[0].tool_call["arguments"] == "" + + +def test_openai_responses_custom_tool_async_stream_preserves_metadata(): + provider = _responses_provider() + + class Responses: + async def create(self, **_request): + events = [ + SimpleNamespace( + type="response.output_item.added", + output_index=0, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="", + ), + ), + SimpleNamespace( + type="response.custom_tool_call_input.delta", + item_id="item-1", + output_index=0, + delta="echo hello", + ), + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=0, + input="echo hello", + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + + class Stream: + def __aiter__(self): + self._events = iter(events) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hello"))] + + events = asyncio.run(collect()) + assert [event.type for event in events] == [ + "tool_call_delta", + "tool_call_completed", + "finish", + ] + assert events[1].tool_call["type"] == "custom_tool_call" + assert events[1].tool_call["name"] == "run_command" + + +def test_google_response_text_excludes_thought_parts(): + response = SimpleNamespace( + candidates=[ + SimpleNamespace( + content=SimpleNamespace( + parts=[ + SimpleNamespace(text="思考", thought=True), + SimpleNamespace(text="答案", thought=False), + ] + ) + ) + ] + ) + + assert GoogleProvider._response_text(response) == "答案" + + +def test_anthropic_tool_start_arguments_use_string_contract(): + event = AnthropicProvider._stream_event( + SimpleNamespace( + type="content_block_start", + index=0, + content_block=SimpleNamespace( + type="tool_use", id="toolu-1", name="lookup", input={"id": 1} + ), + ), + "msg-1", + ) + + assert event is not None + assert event.tool_call["arguments"] == '{"id":1}' + + +def test_anthropic_user_profile_adds_required_beta_header(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + params = provider._build_message_params(prompt="hi", user="profile-1") + + assert params["user_profile_id"] == "profile-1" + assert params["extra_headers"]["anthropic-beta"] == "user-profiles-2026-08-18" + + +def test_anthropic_stream_deduplicates_message_finish_events(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + class StreamManager: + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def __iter__(self): + return iter( + [ + SimpleNamespace( + type="message_delta", + delta=SimpleNamespace(stop_reason="end_turn"), + usage=SimpleNamespace(output_tokens=1), + ), + SimpleNamespace(type="message_stop"), + ] + ) + + class Messages: + def stream(self, **_kwargs): + return StreamManager() + + provider._get_client = lambda: SimpleNamespace(messages=Messages()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == ["finish"] + + +def test_anthropic_non_refusal_stop_details_do_not_create_refusal_result(): + response = SimpleNamespace( + content=[], + stop_reason="end_turn", + stop_details=SimpleNamespace( + type="max_tokens", + explanation="normal completion detail", + ), + ) + + result = AnthropicProvider._extract_result(response) + + assert result.refusal is None + assert result.finish_reason == "end_turn" + + +def test_anthropic_non_refusal_stream_stop_details_emit_finish(): + event = SimpleNamespace( + type="message_delta", + delta=SimpleNamespace( + stop_reason="end_turn", + stop_details=SimpleNamespace( + type="max_tokens", + explanation="normal completion detail", + ), + ), + usage=None, + ) + + converted = AnthropicProvider._stream_event(event, "msg-1") + + assert converted is not None + assert converted.type == "finish" + assert converted.finish_reason == "end_turn" + assert converted.refusal is None + + +def test_anthropic_invoke_retries_stream_context_before_first_event(monkeypatch): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + attempts = [] + + class StreamManager: + def __init__(self, fail_on_enter): + self.fail_on_enter = fail_on_enter + self.text_stream = iter(["答复"]) + + def __enter__(self): + if self.fail_on_enter: + raise RuntimeError("transient stream setup failure") + return self + + def __exit__(self, *_args): + return False + + class Messages: + def stream(self, **_kwargs): + attempts.append(True) + return StreamManager(len(attempts) == 1) + + def fake_retry(call): + last_error = None + for _ in range(2): + try: + return call() + except RuntimeError as error: + last_error = error + raise last_error + + monkeypatch.setattr(model_provider_module, "retry_sync_call", fake_retry) + provider._get_client = lambda: SimpleNamespace(messages=Messages()) + + assert list(provider.invoke("hello", stream=True)) == ["答复"] + assert len(attempts) == 2 + + +def test_anthropic_ainvoke_retries_async_stream_context_before_first_event(monkeypatch): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + attempts = [] + + class AsyncStreamManager: + def __init__(self, fail_on_enter): + self.fail_on_enter = fail_on_enter + self.text_stream = ["异步答复"] + + async def __aenter__(self): + if self.fail_on_enter: + raise RuntimeError("transient async stream setup failure") + return self + + async def __aexit__(self, *_args): + return False + + class Messages: + def stream(self, **_kwargs): + attempts.append(True) + return AsyncStreamManager(len(attempts) == 1) + + async def fake_retry(call): + last_error = None + for _ in range(2): + try: + result = call() + return await result if inspect.isawaitable(result) else result + except RuntimeError as error: + last_error = error + raise last_error + + monkeypatch.setattr(model_provider_module, "retry_async_call", fake_retry) + provider._get_aclient = lambda: SimpleNamespace(messages=Messages()) + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", stream=True)] + + assert asyncio.run(consume()) == ["异步答复"] + assert len(attempts) == 2 + + +def test_anthropic_invoke_non_stream_refusal_is_explicit_error(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + response = SimpleNamespace( + content=[SimpleNamespace(type="refusal", reason="不能回答")], + stop_reason="refusal", + ) + provider._get_client = lambda: SimpleNamespace( + messages=SimpleNamespace(create=lambda **_kwargs: response) + ) + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + list(provider.invoke("hello", stream=False)) + + +def test_anthropic_invoke_stream_refusal_is_explicit_error(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + class StreamManager: + def __enter__(self): + return self + + def __exit__(self, *_args): + return False + + def __iter__(self): + return iter( + [ + SimpleNamespace( + type="content_block_start", + content_block=SimpleNamespace(type="refusal", reason="不能回答"), + ), + SimpleNamespace(type="message_stop"), + ] + ) + + text_stream = iter(()) + + provider._get_client = lambda: SimpleNamespace( + messages=SimpleNamespace(stream=lambda **_kwargs: StreamManager()) + ) + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + list(provider.invoke("hello", stream=True)) + + +def test_anthropic_text_stream_detects_refusal_before_consuming_text_stream(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + class RealLikeStream: + current_message_snapshot = property( + lambda _self: SimpleNamespace( + content=[SimpleNamespace(type="refusal", reason="不能回答")], + stop_reason="refusal", + ) + ) + + def __iter__(self): + return iter( + [ + SimpleNamespace( + type="content_block_start", + content_block=SimpleNamespace(type="refusal", reason="不能回答"), + ) + ] + ) + + text_stream = iter(["这段文本不应先返回"]) + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + next(AnthropicProvider._iter_invoke_stream_text(RealLikeStream())) + + +def test_anthropic_text_stream_checks_snapshot_before_yielding_text(): + class SnapshotStream: + current_message_snapshot = property( + lambda _self: SimpleNamespace( + content=[SimpleNamespace(type="refusal", reason="拒答")], + stop_reason="refusal", + ) + ) + text_stream = iter(["不应输出"]) + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + next(AnthropicProvider._iter_invoke_stream_text(SnapshotStream())) + + +def test_anthropic_stream_error_preserves_nested_error_message(): + event = { + "type": "error", + "error": { + "type": "invalid_request_error", + "error": {"message": "nested request detail"}, + }, + } + + with pytest.raises(RuntimeError, match="nested request detail"): + AnthropicProvider._stream_event(event) + + +def test_openai_responses_non_stream_reasoning_summary_list_is_preserved(): + response = SimpleNamespace( + output=[ + SimpleNamespace( + type="reasoning", + summary=[SimpleNamespace(type="summary_text", text="先分析"), SimpleNamespace(text="后结论")], + ) + ] + ) + + assert OpenAICompatibleProvider._extract_reasoning(response) == "先分析后结论" + + +def test_ark_responses_non_stream_reasoning_summary_list_is_preserved(): + response = SimpleNamespace( + output=[ + SimpleNamespace( + type="reasoning", + summary=[SimpleNamespace(type="summary_text", text="Ark 思考")], + ) + ], + choices=[], + output_text="", + usage=None, + status="completed", + id="resp-1", + ) + + assert VolcengineProvider._extract_result(response).reasoning == "Ark 思考" + + +def test_anthropic_ainvoke_non_stream_refusal_is_explicit_error(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + response = SimpleNamespace( + content=[SimpleNamespace(type="refusal", reason="异步不能回答")], + stop_reason="refusal", + ) + + class Messages: + async def create(self, **_kwargs): + return response + + provider._get_aclient = lambda: SimpleNamespace(messages=Messages()) + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", stream=False)] + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + asyncio.run(consume()) + + +def test_anthropic_ainvoke_stream_refusal_is_explicit_error(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + class AsyncStreamManager: + async def __aenter__(self): + return self + + async def __aexit__(self, *_args): + return False + + def __aiter__(self): + async def events(): + yield SimpleNamespace( + type="content_block_start", + content_block=SimpleNamespace(type="refusal", reason="异步不能回答"), + ) + yield SimpleNamespace(type="message_stop") + + return events() + + text_stream = () + + provider._get_aclient = lambda: SimpleNamespace( + messages=SimpleNamespace(stream=lambda **_kwargs: AsyncStreamManager()) + ) + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", stream=True)] + + with pytest.raises(RuntimeError, match="拒绝|拒答"): + asyncio.run(consume()) + + +def test_responses_invoke_rejects_tools_for_text_only_interface(): + provider = _responses_provider() + provider._get_client = lambda: pytest.fail("Responses invoke 不应初始化客户端") + + with pytest.raises(ValueError, match="tools|工具"): + list(provider.invoke("hello", tools=TOOLS)) + + +def test_responses_ainvoke_rejects_tools_for_text_only_interface(): + provider = _responses_provider() + provider._get_aclient = lambda: pytest.fail("Responses ainvoke 不应初始化客户端") + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", tools=TOOLS)] + + with pytest.raises(ValueError, match="tools|工具"): + asyncio.run(consume()) + + +def test_chat_invoke_accepts_mapping_shaped_sdk_response(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = {"choices": [{"message": {"content": "字典答复"}}]} + provider._get_client = lambda: SimpleNamespace( + chat=SimpleNamespace( + completions=SimpleNamespace(create=lambda **_kwargs: response) + ) + ) + + assert list(provider.invoke("hello", stream=False)) == ["字典答复"] + + +def test_chat_ainvoke_accepts_mapping_shaped_sdk_response(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = {"choices": [{"message": {"content": "异步字典答复"}}]} + + class Completions: + async def create(self, **_kwargs): + return response + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", stream=False)] + + assert asyncio.run(consume()) == ["异步字典答复"] + + +def test_openai_chat_business_error_is_explicit_for_complete_and_invoke(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = SimpleNamespace(status="435", msg="Model not support", choices=None) + provider._get_client = lambda: SimpleNamespace( + chat=SimpleNamespace( + completions=SimpleNamespace(create=lambda **_kwargs: response) + ) + ) + + with pytest.raises(RuntimeError, match="435.*Model not support"): + provider.complete(CompletionRequest(prompt="hello")) + with pytest.raises(RuntimeError, match="435.*Model not support"): + list(provider.invoke("hello", stream=False)) + + +def test_openai_chat_business_error_is_explicit_for_async_complete_and_invoke(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = SimpleNamespace(status="435", msg="Model not support", choices=None) + + class Completions: + async def create(self, **_kwargs): + return response + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + with pytest.raises(RuntimeError, match="435.*Model not support"): + asyncio.run(provider.acomplete(CompletionRequest(prompt="hello"))) + + async def consume(): + return [item async for item in provider.ainvoke("hello", stream=False)] + + with pytest.raises(RuntimeError, match="435.*Model not support"): + asyncio.run(consume()) + + +def test_openai_chat_business_error_is_explicit_for_sync_and_async_streams(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + response = SimpleNamespace(status="435", msg="Model not support", choices=None) + provider._get_client = lambda: SimpleNamespace( + chat=SimpleNamespace( + completions=SimpleNamespace(create=lambda **_kwargs: iter([response])) + ) + ) + + with pytest.raises(RuntimeError, match="435.*Model not support"): + list(provider.stream_events(CompletionRequest(prompt="hello"))) + + class AsyncCompletions: + async def create(self, **_kwargs): + class Stream: + def __aiter__(self): + self._events = iter([response]) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=AsyncCompletions()) + ) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hello"))] + + with pytest.raises(RuntimeError, match="435.*Model not support"): + asyncio.run(collect()) + + +def test_ark_responses_invoke_rejects_tools_for_text_only_interface(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + provider._get_client = lambda: pytest.fail("Ark Responses invoke 不应初始化客户端") + + with pytest.raises(ValueError, match="tools|工具"): + list(provider.invoke("hello", tools=TOOLS)) + + +def test_ark_responses_ainvoke_rejects_tools_for_text_only_interface(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + provider._get_aclient = lambda: pytest.fail("Ark Responses ainvoke 不应初始化客户端") + + async def consume(): + return [chunk async for chunk in provider.ainvoke("hello", tools=TOOLS)] + + with pytest.raises(ValueError, match="tools|工具"): + asyncio.run(consume()) + + +@pytest.mark.parametrize( + "method_name", + [ + "retrieve_response", + "create_response", + "delete_response", + "list_response_input_items", + "list_input_items", + ], +) +def test_ark_response_resources_require_responses_protocol(method_name): + provider = object.__new__(VolcengineProvider) + provider._protocol = "chat_completions" + provider._get_client = lambda: pytest.fail("Ark Responses 资源不应初始化客户端") + method = getattr(provider, method_name) + + args = { + "retrieve_response": ("resp-1",), + "create_response": (), + "delete_response": ("resp-1",), + "list_response_input_items": ("resp-1",), + "list_input_items": ("resp-1",), + }[method_name] + kwargs = {"input": "hello"} if method_name == "create_response" else {} + + with pytest.raises(ValueError, match="仅适用于 responses 协议"): + method(*args, **kwargs) + + +@pytest.mark.parametrize( + "method_name", + [ + "async_retrieve_response", + "async_create_response", + "async_delete_response", + "async_list_response_input_items", + "async_list_input_items", + ], +) +def test_ark_async_response_resources_require_responses_protocol(method_name): + provider = object.__new__(VolcengineProvider) + provider._protocol = "chat_completions" + provider._get_aclient = lambda: pytest.fail("Ark Async Responses 资源不应初始化客户端") + method = getattr(provider, method_name) + + args = { + "async_retrieve_response": ("resp-1",), + "async_create_response": (), + "async_delete_response": ("resp-1",), + "async_list_response_input_items": ("resp-1",), + "async_list_input_items": ("resp-1",), + }[method_name] + kwargs = {"input": "hello"} if method_name == "async_create_response" else {} + + async def invoke(): + await method(*args, **kwargs) + + with pytest.raises(ValueError, match="仅适用于 responses 协议"): + asyncio.run(invoke()) + + +def test_openai_complete_uses_retry_call_wrapper(monkeypatch): + provider = _responses_provider() + provider._provider = "openai" + provider._base_url = None + calls = [] + + def fake_retry(call): + calls.append(call) + return call() + + monkeypatch.setattr(openai_compatible_module, "retry_sync_call", fake_retry) + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace( + create=lambda **_request: SimpleNamespace(status="completed", output_text="答复") + ) + ) + + assert provider.complete(CompletionRequest(prompt="hello")).text == "答复" + assert len(calls) == 1 + + +def test_openai_acomplete_uses_async_retry_call_wrapper(monkeypatch): + provider = _responses_provider() + provider._provider = "openai" + provider._base_url = None + calls = [] + + async def fake_retry(call): + calls.append(call) + result = call() + return await result if inspect.isawaitable(result) else result + + monkeypatch.setattr(openai_compatible_module, "retry_async_call", fake_retry) + + class Responses: + async def create(self, **_request): + return SimpleNamespace(status="completed", output_text="异步答复") + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + assert asyncio.run(provider.acomplete(CompletionRequest(prompt="hello"))).text == "异步答复" + assert len(calls) == 1 + + +def test_anthropic_complete_uses_retry_call_wrapper(monkeypatch): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + calls = [] + + def fake_retry(call): + calls.append(call) + return call() + + monkeypatch.setattr(anthropic_provider_module, "retry_sync_call", fake_retry) + provider._get_client = lambda: SimpleNamespace( + messages=SimpleNamespace( + create=lambda **_request: SimpleNamespace( + content=[SimpleNamespace(type="text", text="答复")] + ) + ) + ) + + assert provider.complete(CompletionRequest(prompt="hello")).text == "答复" + assert len(calls) == 1 + + +def test_anthropic_acomplete_uses_async_retry_call_wrapper(monkeypatch): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + calls = [] + + async def fake_retry(call): + calls.append(call) + result = call() + return await result if inspect.isawaitable(result) else result + + monkeypatch.setattr(anthropic_provider_module, "retry_async_call", fake_retry) + + class Messages: + async def create(self, **_request): + return SimpleNamespace(content=[SimpleNamespace(type="text", text="异步答复")]) + + provider._get_aclient = lambda: SimpleNamespace(messages=Messages()) + + assert asyncio.run(provider.acomplete(CompletionRequest(prompt="hello"))).text == "异步答复" + assert len(calls) == 1 + + +def test_ark_acomplete_uses_async_retry_call_wrapper(monkeypatch): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-test" + provider._protocol = "chat_completions" + provider._options = {} + calls = [] + + async def fake_retry(call): + calls.append(call) + result = call() + return await result if inspect.isawaitable(result) else result + + monkeypatch.setattr(volcengine_module, "retry_async_call", fake_retry) + + class Completions: + async def create(self, **_request): + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="异步答复", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_aclient = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + assert asyncio.run(provider.acomplete(CompletionRequest(prompt="hello"))).text == "异步答复" + assert len(calls) == 1 + + +def test_openai_and_ark_embedding_base64_is_decoded_to_float_vectors(): + encoded = base64.b64encode(struct.pack("<2f", 1.25, -2.5)).decode("ascii") + + openai_provider = object.__new__(OpenAICompatibleProvider) + openai_provider._model_name = "embedding" + openai_provider._provider = "openai" + openai_provider._options = {} + openai_provider._client = None + openai_provider._get_client = lambda: SimpleNamespace( + embeddings=SimpleNamespace( + create=lambda **_kwargs: SimpleNamespace( + data=[SimpleNamespace(embedding=encoded)] + ) + ) + ) + assert openai_provider.embed_documents(["text"], encoding_format="base64") == [[1.25, -2.5]] + + ark_provider = object.__new__(VolcengineProvider) + ark_provider._model_name = "embedding" + ark_provider._options = {} + ark_provider._get_client = lambda: SimpleNamespace( + embeddings=SimpleNamespace( + create=lambda **_kwargs: SimpleNamespace( + data=[SimpleNamespace(embedding=encoded)] + ) + ) + ) + assert ark_provider.embed_documents(["text"], encoding_format="base64") == [[1.25, -2.5]] + + +def test_responses_async_non_stream_extracts_output_text(): + provider = _responses_provider() + calls = [] + + class Responses: + async def create(self, **request): + calls.append(request) + return SimpleNamespace(status="completed", output_text="异步答复") + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [chunk async for chunk in provider._ainvoke_responses({"model": "responses-model", "input": "hello", "stream": False})] + + assert asyncio.run(collect()) == ["异步答复"] + assert calls == [{"model": "responses-model", "input": "hello", "stream": False}] + + +def test_responses_sync_stream_yields_text_and_refusal_deltas(): + provider = _responses_provider() + + class Responses: + def create(self, **request): + assert request["stream"] is True + return iter( + [ + SimpleNamespace(type="response.output_text.delta", delta="好"), + SimpleNamespace(type="response.refusal.delta", delta="的"), + SimpleNamespace(type="response.completed"), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="拒答"): + list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": True})) + + +def test_responses_sync_non_stream_refusal_is_explicit_error(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return SimpleNamespace( + status="completed", + output=[ + SimpleNamespace( + type="message", + content=[SimpleNamespace(type="refusal", refusal="不能回答")], + ) + ], + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="拒答"): + list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": False})) + + +def test_responses_async_stream_yields_text_deltas(): + provider = _responses_provider() + + class AsyncEvents: + def __aiter__(self): + return self + + async def __anext__(self): + if not hasattr(self, "events"): + self.events = iter( + [ + SimpleNamespace(type="response.output_text.delta", delta="异"), + SimpleNamespace(type="response.output_text.delta", delta="步"), + SimpleNamespace(type="response.completed"), + ] + ) + try: + return next(self.events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + class Responses: + async def create(self, **request): + assert request["stream"] is True + return AsyncEvents() + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [chunk async for chunk in provider._ainvoke_responses({"model": "responses-model", "input": "hello", "stream": True})] + + assert asyncio.run(collect()) == ["异", "步"] + + +@pytest.mark.parametrize( + ("status", "details", "expected"), + [ + ("failed", SimpleNamespace(), "Responses API 响应状态为 failed"), + ("incomplete", SimpleNamespace(reason="max_output_tokens"), "max_output_tokens"), + ], +) +def test_responses_non_stream_terminal_status_is_explicit_error(status, details, expected): + provider = _responses_provider() + response = SimpleNamespace(status=status, incomplete_details=details, error=None) + + class Responses: + def create(self, **request): + return response + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match=expected): + list( + provider._invoke_responses( + {"model": "responses-model", "input": "hello", "stream": False} + ) + ) + + +def test_responses_stream_failed_event_is_explicit_error(): + provider = _responses_provider() + + class Responses: + def create(self, **request): + return iter( + [ + SimpleNamespace( + type="response.failed", + response=SimpleNamespace( + status="failed", + error=SimpleNamespace(message="upstream failed"), + ), + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="upstream failed"): + list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": True})) + + +def test_responses_stream_incomplete_event_is_explicit_error(): + provider = _responses_provider() + + class Responses: + def create(self, **request): + return iter( + [ + SimpleNamespace( + type="response.incomplete", + response=SimpleNamespace( + status="incomplete", + incomplete_details=SimpleNamespace(reason="content_filter"), + ), + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="content_filter"): + list(provider._invoke_responses({"model": "responses-model", "input": "hello", "stream": True})) + + +def test_responses_stream_error_event_is_explicit_error(): + with pytest.raises(RuntimeError, match="rate limit"): + OpenAICompatibleProvider._stream_delta( + SimpleNamespace(type="error", message="rate limit") + ) + + +def test_responses_cancelled_events_are_explicit_errors(): + with pytest.raises(RuntimeError, match="cancelled|取消"): + OpenAICompatibleProvider._responses_stream_event( + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="cancelled"), + ) + ) + + with pytest.raises(RuntimeError, match="cancelled|取消"): + OpenAICompatibleProvider._stream_delta( + SimpleNamespace(type="response.cancelled") + ) + + +def test_openai_responses_non_stream_extracts_refusal_output(): + provider = _responses_provider() + + class Responses: + def create(self, **_request): + return SimpleNamespace( + status="completed", + output=[ + SimpleNamespace( + type="message", + content=[SimpleNamespace(type="refusal", refusal="不能回答")], + ) + ], + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + result = provider.complete(CompletionRequest(prompt="hello")) + + assert result.refusal == "不能回答" + + +def test_deepseek_chat_uses_max_tokens_for_explicit_and_configured_limits(): + provider = object.__new__(DeepSeekProvider) + provider._model_name = "deepseek-chat" + provider._protocol = "chat_completions" + + provider._options = {} + request = provider._build_chat_request(prompt="hi", stream=False, max_tokens=128) + assert request["max_tokens"] == 128 + assert "max_completion_tokens" not in request + + provider._options = {"max_tokens": 256} + request = provider._build_chat_request(prompt="hi", stream=False) + assert request["max_tokens"] == 256 + assert "max_completion_tokens" not in request diff --git a/tests/providers/test_rerank_contracts.py b/tests/providers/test_rerank_contracts.py new file mode 100644 index 0000000..654f611 --- /dev/null +++ b/tests/providers/test_rerank_contracts.py @@ -0,0 +1,125 @@ +import pytest + +from src.providers.jina import JinaProvider +from src.providers.siliconflow_rerank import SiliconflowRerankProvider + + +def test_jina_duplicate_documents_keep_distinct_indices(): + provider = object.__new__(JinaProvider) + indices, scores = provider._parse_response( + [ + {"document": {"text": "same"}, "relevance_score": 0.9}, + {"document": {"text": "same"}, "relevance_score": 0.8}, + ], + ["same", "same"], + ) + assert indices == [0, 1] + assert scores == [0.9, 0.8] + + +def test_siliconflow_duplicate_documents_keep_distinct_indices(): + provider = object.__new__(SiliconflowRerankProvider) + indices, scores = provider._parse_response( + [ + {"document": {"text": "same"}, "relevance_score": 0.9}, + {"document": {"text": "same"}, "relevance_score": 0.8}, + ], + ["same", "same"], + ) + assert indices == [0, 1] + assert scores == [0.9, 0.8] + + +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_accepts_top_n_larger_than_document_count(provider_type): + provider = object.__new__(provider_type) + + provider._validate_inputs("query", ["document"], 2) + + +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_invalid_index_is_explicit(provider_type): + provider = object.__new__(provider_type) + + with pytest.raises(RuntimeError, match="index 无效"): + provider._parse_response( + [{"index": 4, "relevance_score": 0.9}], + ["one", "two"], + ) + + +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_unknown_document_is_explicit(provider_type): + provider = object.__new__(provider_type) + + with pytest.raises(RuntimeError, match="无法映射"): + provider._parse_response( + [{"document": {"text": "unknown"}, "relevance_score": 0.9}], + ["one", "two"], + ) + + +@pytest.mark.parametrize("score", ["not-a-number", float("nan"), float("inf")]) +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_invalid_score_is_explicit(provider_type, score): + provider = object.__new__(provider_type) + + with pytest.raises(RuntimeError, match="relevance_score"): + provider._parse_response( + [{"index": 0, "relevance_score": score}], + ["one"], + ) + + +@pytest.mark.parametrize("timeout", [float("nan"), float("inf"), float("-inf"), 0, -1]) +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_non_finite_or_non_positive_timeout_is_explicit(provider_type, timeout): + provider = object.__new__(provider_type) + provider._options = {"timeout": timeout} + + with pytest.raises(ValueError, match="timeout.*正数"): + provider._request_timeout() + + +@pytest.mark.parametrize("provider_type", [JinaProvider, SiliconflowRerankProvider]) +def test_rerank_duplicate_index_is_explicit(provider_type): + provider = object.__new__(provider_type) + + with pytest.raises(RuntimeError, match="重复 index"): + provider._parse_response( + [ + {"index": 0, "relevance_score": 0.9}, + {"index": 0, "relevance_score": 0.8}, + ], + ["one"], + ) + + +def test_siliconflow_rerank_rejects_return_documents_override(): + provider = object.__new__(SiliconflowRerankProvider) + provider._model_name = "rerank-model" + provider._options = {"return_documents": False} + + with pytest.raises(ValueError, match="return_documents"): + provider._prepare_payload("query", ["document"], 1) + + +def test_jina_rerank_rejects_return_documents_override(): + """Jina 与 SiliconFlow 一样把 return_documents 固定为 True,不允许覆盖。""" + provider = object.__new__(JinaProvider) + provider._model_name = "rerank-model" + provider._options = {"return_documents": False} + + with pytest.raises(ValueError, match="return_documents"): + provider._prepare_payload("query", ["document"], 1) + + +def test_jina_rerank_requests_documents_by_default(): + """未显式配置时也要请求返回文档,与 SiliconFlow 行为一致。""" + provider = object.__new__(JinaProvider) + provider._model_name = "rerank-model" + provider._options = {} + + payload = provider._prepare_payload("query", ["document"], 1) + + assert payload["return_documents"] is True diff --git a/tests/providers/test_sdk_capabilities.py b/tests/providers/test_sdk_capabilities.py new file mode 100644 index 0000000..7ba57f9 --- /dev/null +++ b/tests/providers/test_sdk_capabilities.py @@ -0,0 +1,6297 @@ +import asyncio +import inspect +import json +from types import SimpleNamespace + +import pytest +from google.auth.credentials import AnonymousCredentials +from google.genai import types + +from src.providers.__base__.model_provider import ( + CompletionRequest, + LargeLanguageModel, + TextEmbeddingModel, + normalize_chat_messages, + normalize_messages, + normalize_responses_input, +) +from src.providers.anthropic import AnthropicProvider +from src.providers.factory import ModelProviderFactory +from src.providers.google import GoogleProvider +from src.providers.openai import OpenAIProvider +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.providers.resources import AsyncGoogleResources, GoogleResources +from src.providers.volcengine import VolcengineProvider +from src.utils.config import ModelDetail +from src.utils.security import ( + find_sensitive_option_paths, + is_sensitive_option_key, + redact_sensitive_text, + validate_secret_free_options, + validate_secret_free_payload, + validate_secret_free_resource_args, + validate_secret_free_resource_kwargs, +) + + +def test_model_detail_options_are_explicit_and_secret_free(): + detail = ModelDetail(provider="openai", model_name="demo", options={"top_p": 0.8}) + assert detail.options == {"top_p": 0.8} + with pytest.raises(ValueError, match="凭证"): + ModelDetail(provider="openai", model_name="demo", options={"api_key": "secret"}) + with pytest.raises(ValueError, match="凭证"): + ModelDetail(provider="openai", model_name="demo", options={"nested": {"headers": {"Authorization": "x"}}}) + with pytest.raises(ValueError): + ModelDetail(provider="openai", model_name="demo", unknown=True) + + +@pytest.mark.parametrize( + "field_name", + ["extra_headers", "extra_query"], +) +def test_completion_request_rejects_credentials_in_request_overrides(field_name): + with pytest.raises(ValueError, match="请求头/query"): + CompletionRequest(**{field_name: {"Authorization": "Bearer secret"}}) + + +def test_completion_request_keeps_safe_request_overrides_isolated(): + headers = {"X-Trace-ID": "trace-1"} + query = {"tenant": "demo"} + request = CompletionRequest(extra_headers=headers, extra_query=query) + + assert request.extra_headers == headers + assert request.extra_query == query + assert request.extra_headers is not headers + assert request.extra_query is not query + + +def test_completion_request_rejects_credentials_in_extra_body(): + with pytest.raises(ValueError, match="extra_body"): + CompletionRequest(extra_body={"vendor": {"access_token": "secret"}}) + + +def test_google_model_options_allow_safe_http_trace_headers(): + detail = ModelDetail( + provider="google", + model_name="gemini-test", + options={"http_options": {"headers": {"X-Trace-ID": "trace"}}}, + ) + assert detail.options["http_options"]["headers"] == {"X-Trace-ID": "trace"} + + +def test_google_model_options_reject_sdk_http_credentials(): + http_options = types.HttpOptions( + headers={"X-API-Key": "secret"}, + ) + with pytest.raises(ValueError, match="凭证"): + ModelDetail( + provider="google", + model_name="gemini-test", + options={"http_options": http_options}, + ) + + +@pytest.mark.parametrize("timeout", ["1", 0, -1, float("nan"), float("inf")]) +def test_google_request_timeout_must_be_finite_and_positive(timeout): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {} + with pytest.raises(ValueError, match="timeout|正数"): + provider._build_generation_config("system", None, 0.2, timeout=timeout) + + +def test_google_resource_config_rejects_nested_http_credentials(): + with pytest.raises(ValueError, match="凭证"): + GoogleProvider._resource_config( + lambda **values: values, + {"http_options": {"headers": {"Authorization": "Bearer secret"}}}, + {}, + ) + + +def test_google_options_extra_body_rejects_nested_credentials(): + with pytest.raises(ValueError, match="options.extra_body"): + GoogleProvider( + "gemini-test", + options={"extra_body": {"routing": {"access_token": "secret"}}}, + ) + + +@pytest.mark.parametrize("provider", ["openai", "qwen"]) +def test_direct_openai_compatible_provider_rejects_request_header_and_query_options(provider): + settings = SimpleNamespace( + openai_api_key="key", + qwen_api_key="key", + qwen_base_url="https://qwen.invalid/v1", + openai_api_base="https://api.openai.com/v1", + ) + with pytest.raises(ValueError, match="请求头/query"): + OpenAICompatibleProvider( + "demo", + provider, + settings=settings, + options={"default_headers": {"X-API-Key": "secret"}}, + ) + with pytest.raises(ValueError, match="请求头/query"): + OpenAICompatibleProvider( + "demo", + provider, + settings=settings, + options={"default_query": {"access_token": "secret"}}, + ) + + +def test_direct_openai_compatible_provider_rejects_base_url_and_http_client_options(): + settings = SimpleNamespace( + qwen_api_key="key", + qwen_base_url="https://qwen.invalid/v1", + ) + with pytest.raises(ValueError, match="请求头/query"): + OpenAICompatibleProvider( + "demo", + "qwen", + settings=settings, + options={"base_url": "https://proxy.invalid/v1"}, + ) + with pytest.raises(ValueError, match="请求头/query"): + OpenAICompatibleProvider( + "demo", + "qwen", + settings=settings, + options={"http_client": object()}, + ) + + +def test_direct_volcengine_provider_rejects_connection_override(monkeypatch): + settings = SimpleNamespace( + volc_access_key="ak", + volc_secret_key="sk", + ark_api_key=None, + volc_base_url="https://ark.invalid/v3", + ) + monkeypatch.setattr("src.providers.volcengine.get_settings", lambda: settings) + with pytest.raises(ValueError, match="请求头/query"): + VolcengineProvider( + "doubao", + options={"base_url": "https://proxy.invalid/v3"}, + ) + + +def test_direct_anthropic_provider_rejects_request_header_and_query_options(monkeypatch): + monkeypatch.setattr( + "src.providers.anthropic.get_settings", + lambda: SimpleNamespace(anthropic_api_key="key"), + ) + with pytest.raises(ValueError, match="请求头/query"): + AnthropicProvider( + "claude", + options={"default_headers": {"Authorization": "Bearer secret"}}, + ) + with pytest.raises(ValueError, match="请求头/query"): + AnthropicProvider( + "claude", + options={"default_query": {"access_token": "secret"}}, + ) + + +def test_direct_anthropic_provider_rejects_connection_override(monkeypatch): + monkeypatch.setattr( + "src.providers.anthropic.get_settings", + lambda: SimpleNamespace(anthropic_api_key="key"), + ) + with pytest.raises(ValueError, match="请求头/query"): + AnthropicProvider("claude", options={"base_url": "https://proxy.invalid"}) + + +def test_openai_chat_moderation_is_forwarded_only_for_official_provider(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + moderation = {"model": "omni-moderation-latest", "policy": {"input": {"mode": "block"}}} + request = provider._build_chat_request(prompt="hi", moderation=moderation, stream=False) + assert request["moderation"] == moderation + + provider._provider = "qwen" + with pytest.raises(ValueError, match="moderation"): + provider._build_chat_request(prompt="hi", moderation=moderation, stream=False) + + +def test_openai_compatibility_uses_endpoint_not_provider_name_for_official_fields(): + moderation = {"model": "omni-moderation-latest"} + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._options = {} + provider._provider = "qwen" + provider._base_url = "https://api.openai.com/v1" + request = provider._build_chat_request(prompt="hi", moderation=moderation, stream=False) + assert request["moderation"] == moderation + + provider._provider = "openai" + provider._base_url = "https://gateway.example.com/v1" + with pytest.raises(ValueError, match="moderation"): + provider._build_chat_request(prompt="hi", moderation=moderation, stream=False) + + +def test_openai_chat_request_supports_messages_and_extended_parameters(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._provider = "openai" + provider._options = {} + request = provider._build_chat_request( + messages=[{"role": "user", "content": [{"type": "text", "text": "hi"}]}], + temperature=0.2, + max_tokens=128, + top_p=0.9, + seed=7, + response_format={"type": "json_object"}, + stream=False, + ) + assert request["messages"][0]["content"][0]["type"] == "text" + assert request["max_completion_tokens"] == 128 + assert request["seed"] == 7 + assert request["response_format"]["type"] == "json_object" + + +def test_completion_request_omits_implicit_temperature_for_model_defaults(): + request = CompletionRequest(prompt="hi") + assert request.temperature == 0.7 + assert "temperature" not in request.to_invoke_kwargs() + + explicit = CompletionRequest(prompt="hi", temperature=0.7) + assert explicit.to_invoke_kwargs()["temperature"] == 0.7 + + +def test_completion_request_copy_preserves_implicit_temperature_marker(): + request = CompletionRequest(prompt="hi") + copied = request.copy_with(stream=False) + + assert copied.temperature == 0.7 + assert copied.temperature_is_explicit is False + assert "temperature" not in copied.to_invoke_kwargs() + + +def test_openai_model_temperature_remains_the_default_when_request_omits_it(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + + request = CompletionRequest(prompt="hi") + params = provider._build_chat_request(**request.to_invoke_kwargs()) + + assert params["temperature"] == 0.2 + + +def test_google_model_temperature_remains_the_default_when_request_omits_it(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {"temperature": 0.2} + + request = CompletionRequest(prompt="hi") + config = provider._build_generation_config( + None, + None, + request.temperature if request.temperature_is_explicit else None, + ) + + assert config.temperature == 0.2 + + +def test_legacy_openai_invoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + calls = [] + + class Completions: + def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + assert list(provider.invoke("hello", stream=False)) == ["ok"] + assert calls[0]["temperature"] == 0.2 + + +def test_legacy_openai_ainvoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + calls = [] + + class Completions: + async def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_aclient = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + async def consume(): + return [item async for item in provider.ainvoke("hello", stream=False)] + + assert asyncio.run(consume()) == ["ok"] + assert calls[0]["temperature"] == 0.2 + + +def test_legacy_google_invoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {"temperature": 0.2} + calls = [] + + class Models: + def generate_content(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(text="ok") + + client = SimpleNamespace(models=Models()) + provider._get_client = lambda: client + provider._effective_vertex_mode = lambda _client: False + + assert list(provider.invoke("hello", stream=False)) == ["ok"] + assert calls[0]["config"].temperature == 0.2 + + +def test_legacy_google_ainvoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {"temperature": 0.2} + calls = [] + + class Models: + async def generate_content(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(text="ok") + + client = SimpleNamespace(aio=SimpleNamespace(models=Models())) + provider._get_client = lambda: client + provider._effective_vertex_mode = lambda _client: False + + async def consume(): + return [item async for item in provider.ainvoke("hello", stream=False)] + + assert asyncio.run(consume()) == ["ok"] + assert calls[0]["config"].temperature == 0.2 + + +def test_legacy_volcengine_invoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + calls = [] + + class Completions: + def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + assert list(provider.invoke("hello", stream=False)) == ["ok"] + assert calls[0]["temperature"] == 0.2 + + +def test_legacy_volcengine_ainvoke_preserves_model_temperature_when_omitted(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "chat_completions" + provider._options = {"temperature": 0.2} + calls = [] + + class Completions: + async def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_aclient = lambda: SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + + async def consume(): + return [item async for item in provider.ainvoke("hello", stream=False)] + + assert asyncio.run(consume()) == ["ok"] + assert calls[0]["temperature"] == 0.2 + + +def test_openai_compatible_rejects_conflicting_model_length_aliases(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"max_tokens": 64, "max_completion_tokens": 128} + + with pytest.raises(ValueError, match="max_tokens.*max_completion_tokens"): + provider._build_chat_request(prompt="hi", stream=False) + + +def test_openai_chat_extra_body_cannot_override_request_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._options = {"extra_body": {"temperature": 0.1}} + + with pytest.raises(ValueError, match="extra_body.*temperature"): + provider._build_chat_request(prompt="hi", temperature=0.7, stream=False) + + +def test_openai_request_builder_rejects_sensitive_http_overrides(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {} + + with pytest.raises(ValueError, match="请求头/query"): + provider._build_chat_request( + prompt="hi", + extra_headers={"X-API-Key": "secret"}, + stream=False, + ) + with pytest.raises(ValueError, match="请求头/query"): + provider._build_responses_request( + prompt="hi", + extra_query={"access_token": "secret"}, + stream=False, + ) + + +def test_openai_resource_calls_fail_before_sdk_on_unknown_or_missing_arguments(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + + with pytest.raises(ValueError, match="SDK.*unknown_option"): + provider.create_upload( + bytes=10, + filename="demo.txt", + mime_type="text/plain", + purpose="assistants", + unknown_option=True, + ) + + with pytest.raises(ValueError, match="SDK.*part_ids"): + provider.complete_upload("upload-1") + + +def test_openai_direct_resource_calls_reject_sensitive_http_overrides(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + + with pytest.raises(ValueError, match="请求头/query 不允许包含凭证"): + provider.list_files(extra_headers={"X-API-Key": "secret"}) + with pytest.raises(ValueError, match="请求头/query 不允许包含凭证"): + provider.list_files(extra_query={"access_token": "secret"}) + + +def test_openai_vector_store_resource_calls_follow_installed_sdk_signature(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + + with pytest.raises(ValueError, match="SDK.*unknown_option"): + provider.create_vector_store(name="demo", unknown_option=True) + + +def test_openai_resource_creation_paths_reject_sensitive_overrides(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + cases = ( + (provider.create_vector_store, {"extra_headers": {"X-API-Key": "secret"}}), + (provider.create_upload, {"extra_query": {"access_token": "secret"}}), + (provider.complete_upload, {"upload_id": "upload-1", "extra_body": {"token": "secret"}}), + ) + for operation, kwargs in cases: + with pytest.raises(ValueError, match="凭证"): + operation(**kwargs) + + +def test_openai_async_resource_creation_paths_reject_sensitive_overrides(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_aclient = lambda: openai.AsyncOpenAI(api_key="local-only") + + async def run() -> None: + cases = ( + (provider.async_create_vector_store, {"extra_headers": {"X-API-Key": "secret"}}), + (provider.async_create_upload, {"extra_query": {"access_token": "secret"}}), + (provider.async_complete_upload, {"upload_id": "upload-1", "extra_body": {"token": "secret"}}), + ) + for operation, kwargs in cases: + with pytest.raises(ValueError, match="凭证"): + await operation(**kwargs) + + asyncio.run(run()) + + +def test_openai_resource_groups_validate_unknown_arguments_before_dispatch(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + cases = ( + (provider.upload_file, (b"data",), {"purpose": "assistants", "unknown_option": True}), + (provider.create_batch, ("file-1",), {"unknown_option": True}), + (provider.create_conversation, (), {"unknown_option": True}), + (provider.create_fine_tuning_job, (), {"unknown_option": True}), + (provider.create_eval, (), {"unknown_option": True}), + ) + for operation, args, kwargs in cases: + with pytest.raises(ValueError, match="SDK.*unknown_option"): + operation(*args, **kwargs) + + +def test_openai_resource_missing_required_arguments_are_project_errors(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + cases = ( + (provider.create_fine_tuning_job, (), {}, "model"), + (provider.create_eval, (), {}, "data_source_config"), + (provider.create_video, (), {}, "prompt"), + (provider.update_conversation, ("conversation-1",), {}, "metadata"), + ) + for operation, args, kwargs, missing in cases: + with pytest.raises(ValueError, match=f"SDK.*{missing}"): + operation(*args, **kwargs) + + +def test_openai_async_resource_groups_validate_unknown_arguments_before_dispatch(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_aclient = lambda: openai.AsyncOpenAI(api_key="local-only") + + async def run() -> None: + cases = ( + (provider.async_upload_file, (b"data",), {"purpose": "assistants", "unknown_option": True}), + (provider.async_create_batch, ("file-1",), {"unknown_option": True}), + (provider.async_create_fine_tuning_job, (), {"unknown_option": True}), + ) + for operation, args, kwargs in cases: + with pytest.raises(ValueError, match="SDK.*unknown_option"): + await operation(*args, **kwargs) + + asyncio.run(run()) + + +def test_openai_async_resource_missing_required_arguments_are_project_errors(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_aclient = lambda: openai.AsyncOpenAI(api_key="local-only") + + async def run() -> None: + cases = ( + (provider.async_create_fine_tuning_job, (), {}, "model"), + (provider.async_create_eval, (), {}, "data_source_config"), + (provider.async_create_video, (), {}, "prompt"), + (provider.async_update_conversation, ("conversation-1",), {}, "metadata"), + ) + for operation, args, kwargs, missing in cases: + with pytest.raises(ValueError, match=f"SDK.*{missing}"): + await operation(*args, **kwargs) + + asyncio.run(run()) + + +def test_openai_media_and_management_resources_validate_sdk_arguments(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + import openai + + provider._get_client = lambda: openai.OpenAI(api_key="local-only") + cases = ( + (provider.create_container, (), {"name": "demo", "unknown_option": True}), + ( + provider.create_fine_tuning_job, + (), + {"model": "demo", "training_file": "file-1", "unknown_option": True}, + ), + ( + provider.create_eval, + (), + { + "data_source_config": {}, + "testing_criteria": [], + "unknown_option": True, + }, + ), + (provider.generate_image, ("hello",), {"unknown_option": True}), + ( + provider.text_to_speech, + ("hello", "tts-1", "alloy"), + {"unknown_option": True}, + ), + (provider.create_video, (), {"prompt": "hello", "unknown_option": True}), + (provider.create_conversation, (), {"items": [], "unknown_option": True}), + ) + for operation, args, kwargs in cases: + with pytest.raises(ValueError, match="SDK.*unknown_option"): + operation(*args, **kwargs) + + +def test_openai_responses_extra_body_cannot_override_request_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"], "extra_body": {"input": "other"}} + + with pytest.raises(ValueError, match="extra_body.*input"): + provider._build_responses_request(prompt="hi", stream=False) + + +@pytest.mark.parametrize("field", ["top_k", "repetition_penalty", "reasoning"]) +def test_openai_chat_explicit_extra_body_extensions_cannot_be_silently_overridden(field): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {"extra_body": {field: {"effort": "low"} if field == "reasoning" else 1}} + + kwargs = {field: {"effort": "high"} if field == "reasoning" else 2} + with pytest.raises(ValueError, match=field): + provider._build_chat_request(prompt="hi", stream=False, **kwargs) + + +def test_ark_responses_rejects_instructions_with_enabled_caching(): + """官方规定 ``instructions`` 与 ``caching={"type": "enabled"}`` 互斥。 + + 配置 instructions 后本轮请求无法写入或使用缓存,caching 为 enabled 时 + 服务端会直接报错;SDK 不做本地校验,因此这里要求在构造阶段显式失败。 + """ + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + with pytest.raises(ValueError, match="instructions.*caching"): + provider._build_responses_request( + CompletionRequest( + prompt="hi", + system_prompt="你是助手", + caching={"type": "enabled"}, + stream=False, + ) + ) + + +def test_ark_responses_accepts_system_prompt_without_enabled_caching(): + """未启用 caching 时 instructions 正常透传;caching 非 enabled 也不拦截。""" + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + params = provider._build_responses_request( + CompletionRequest(prompt="hi", system_prompt="你是助手", stream=False) + ) + assert params["instructions"] == "你是助手" + assert "caching" not in params + + params = provider._build_responses_request( + CompletionRequest( + prompt="hi", system_prompt="你是助手", caching={"type": "disabled"}, stream=False + ) + ) + assert params["instructions"] == "你是助手" + assert params["caching"] == {"type": "disabled"} + + +def test_ark_responses_enabled_caching_ok_without_instructions(): + """把系统提示放进 messages 并显式关闭 system_prompt 时,caching 可正常启用。""" + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + params = provider._build_responses_request( + CompletionRequest( + messages=[ + {"role": "system", "content": "你是助手"}, + {"role": "user", "content": "hi"}, + ], + system_prompt=None, + caching={"type": "enabled"}, + stream=False, + ) + ) + assert "instructions" not in params + assert params["caching"] == {"type": "enabled"} + + +@pytest.mark.parametrize("field", ["top_k", "seed", "stop", "reasoning"]) +def test_openai_responses_explicit_extra_body_extensions_cannot_be_silently_overridden(field): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"], "extra_body": {field: 1}} + + kwargs = {field: {"effort": "high"} if field == "reasoning" else 2} + with pytest.raises(ValueError, match=field): + provider._build_responses_request(prompt="hi", stream=False, **kwargs) + + +@pytest.mark.parametrize("async_mode", [False, True]) +def test_openai_embedding_extra_body_conflicts_are_explicit(async_mode): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "embedding" + provider._provider = "openai" + provider._options = {"extra_body": {"tenant": "configured"}} + provider._client = SimpleNamespace(embeddings=SimpleNamespace(create=lambda **_: None)) + provider._aclient = SimpleNamespace(embeddings=SimpleNamespace(create=lambda **_: None)) + + if async_mode: + async def run(): + await provider.aembed_documents(["text"], extra_body={"tenant": "request"}) + + with pytest.raises(ValueError, match="tenant"): + asyncio.run(run()) + else: + with pytest.raises(ValueError, match="tenant"): + provider.embed_documents(["text"], extra_body={"tenant": "request"}) + + +@pytest.mark.parametrize("async_mode", [False, True]) +@pytest.mark.parametrize( + "field_name, value", + [ + ("extra_headers", {"Authorization": "Bearer secret"}), + ("extra_headers", {"X-API-Key": "secret"}), + ("extra_query", {"access_token": "secret"}), + ("extra_query", {"api_key": "secret"}), + ], +) +def test_openai_embedding_request_overrides_reject_credentials(async_mode, field_name, value): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "embedding" + provider._provider = "openai" + provider._options = {} + provider._client = SimpleNamespace( + embeddings=SimpleNamespace(create=lambda **_: SimpleNamespace(data=[])) + ) + + kwargs = {field_name: value} + if async_mode: + async def run(): + await provider.aembed_documents(["text"], **kwargs) + + with pytest.raises(ValueError, match="请求头/query|凭证"): + asyncio.run(run()) + else: + with pytest.raises(ValueError, match="请求头/query|凭证"): + provider.embed_documents(["text"], **kwargs) + + +def test_openai_embedding_rejects_unknown_model_options(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "openai" + provider._options = {"dimensons": 512} + + with pytest.raises(ValueError, match="不支持请求参数: dimensons"): + provider._embedding_options() + + +def test_openai_chat_rejects_unimplemented_request_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._options = {} + + with pytest.raises(ValueError, match="不支持请求参数: background"): + provider._build_chat_request(prompt="hi", background=True) + + +def test_non_openai_compatible_provider_requires_explicit_base_url(monkeypatch): + settings = SimpleNamespace(qwen_api_key="token", qwen_base_url=None) + monkeypatch.setattr("src.providers.openai_compatible.get_settings", lambda: settings) + + with pytest.raises(ValueError, match="qwen_base_url"): + OpenAICompatibleProvider("demo", "qwen") + + +def test_non_openai_chat_rejects_openai_only_request_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "chat_completions" + provider._options = {} + + with pytest.raises(ValueError, match="modalities"): + provider._build_chat_request(prompt="hi", modalities=["audio"], stream=False) + + +def test_google_rejects_unimplemented_request_fields(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + with pytest.raises(ValueError, match="不支持请求参数: background"): + provider.complete(CompletionRequest(prompt="hi", background=True)) + + +def test_openai_complete_preserves_chat_text_tool_calls_and_usage(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._options = {} + response = SimpleNamespace( + choices=[SimpleNamespace( + message=SimpleNamespace( + content="answer", + tool_calls=[SimpleNamespace(id="call-1", type="function", function=SimpleNamespace(name="lookup", arguments='{"id":1}'))], + ), + finish_reason="tool_calls", + )], + usage=SimpleNamespace(prompt_tokens=3, completion_tokens=4, total_tokens=7), + ) + provider._get_client = lambda: SimpleNamespace(chat=SimpleNamespace(completions=SimpleNamespace(create=lambda **_: response))) + result = provider.complete(CompletionRequest(prompt="hi", stream=False)) + assert result.text == "answer" + assert result.tool_calls[0]["name"] == "lookup" + assert result.usage["total_tokens"] == 7 + + +def test_openai_async_complete_preserves_responses_metadata(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = {} + + class Responses: + async def create(self, **_kwargs): + return SimpleNamespace( + id="resp-async", + status="completed", + output_text="异步答复", + usage=SimpleNamespace(input_tokens=4, output_tokens=5, total_tokens=9), + ) + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + result = asyncio.run(provider.acomplete(CompletionRequest(prompt="hi"))) + + assert result.text == "异步答复" + assert result.finish_reason == "completed" + assert result.usage["input_tokens"] == 4 + + +def test_openai_provider_responses_dispatch_uses_inherited_responses_adapter(): + provider = object.__new__(OpenAIProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://api.openai.com/v1" + provider._options = {} + calls = [] + + class Responses: + def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(status="completed", output_text="答复") + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + result = provider.complete(CompletionRequest(prompt="hello")) + + assert result.text == "答复" + assert calls == [ + { + "model": "demo", + "input": "hello", + "stream": False, + "instructions": "You are a helpful assistant.", + } + ] + + +def test_openai_responses_verbosity_uses_text_config(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = {} + + request = provider._build_responses_request( + prompt="hi", stream=False, verbosity="low" + ) + + assert request["text"] == {"verbosity": "low"} + assert "verbosity" not in request + + +def test_openai_responses_parse_does_not_forward_internal_stream_flag(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + calls = [] + + class Responses: + def parse(self, **kwargs): + calls.append(kwargs) + return "parsed" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + assert provider.parse(dict, CompletionRequest(prompt="hi")) == "parsed" + assert calls[0]["text_format"] is dict + assert "stream" not in calls[0] + + +def test_openai_async_responses_parse_does_not_forward_internal_stream_flag(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + calls = [] + + class Responses: + async def parse(self, **kwargs): + calls.append(kwargs) + return "parsed" + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + assert asyncio.run(provider.async_parse(dict, CompletionRequest(prompt="hi"))) == "parsed" + assert calls[0]["text_format"] is dict + assert "stream" not in calls[0] + + +@pytest.mark.parametrize("async_mode", [False, True]) +def test_openai_responses_parse_rejects_background_mode(async_mode): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {"background": True} + calls = [] + + class Responses: + def parse(self, **kwargs): + calls.append(kwargs) + return "parsed" + + class AsyncResponses: + async def parse(self, **kwargs): + calls.append(kwargs) + return "parsed" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + provider._get_aclient = lambda: SimpleNamespace(responses=AsyncResponses()) + + with pytest.raises(ValueError, match="background"): + if async_mode: + asyncio.run(provider.async_parse(dict, CompletionRequest(prompt="hi"))) + else: + provider.parse(dict, CompletionRequest(prompt="hi")) + + assert calls == [] + + +def test_openai_responses_normalizes_model_options_to_sdk_shape(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = { + "max_tokens": 256, + "verbosity": "low", + "response_format": {"type": "json_object"}, + } + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["max_output_tokens"] == 256 + assert request["text"] == { + "format": {"type": "json_object"}, + "verbosity": "low", + } + assert "max_tokens" not in request + assert "response_format" not in request + assert "verbosity" not in request + + +def test_openai_responses_normalizes_completion_and_reasoning_options(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = { + "max_completion_tokens": 256, + "reasoning_effort": "medium", + } + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["max_output_tokens"] == 256 + assert request["reasoning"] == {"effort": "medium"} + assert "max_completion_tokens" not in request + assert "reasoning_effort" not in request + + +@pytest.mark.parametrize("field", ["stop", "seed", "top_k"]) +def test_openai_responses_rejects_chat_only_sampling_fields(field): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + + with pytest.raises(ValueError, match=field): + provider._build_responses_request(prompt="hi", stream=False, **{field: 1}) + + +def test_compatible_responses_keeps_vendor_sampling_extensions_in_extra_body(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._options = {} + + request = provider._build_responses_request( + prompt="hi", stream=False, stop=["END"], seed=7, top_k=20 + ) + + assert request["extra_body"] == {"stop": ["END"], "seed": 7, "top_k": 20} + + +def test_compatible_responses_allows_supported_openai_response_extensions(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._base_url = "https://gateway.invalid/v1" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset({"responses"}) + provider._options = {} + + request = provider._build_responses_request( + prompt="hi", + stream=False, + store=True, + prompt_cache_key="cache-key", + prompt_cache_options={"scope": "user"}, + prompt_cache_retention="24h", + safety_identifier="user-1", + moderation={"mode": "strict"}, + verbosity="low", + ) + + assert request["store"] is True + assert request["prompt_cache_key"] == "cache-key" + assert request["prompt_cache_options"] == {"scope": "user"} + assert request["prompt_cache_retention"] == "24h" + assert request["safety_identifier"] == "user-1" + assert request["moderation"] == {"mode": "strict"} + assert request["text"]["verbosity"] == "low" + + +def test_factory_protocol_status_uses_runtime_verified_options(): + assert ModelProviderFactory.protocol_status( + "qwen", + "responses", + options={"server_verified_protocols": ["responses"]}, + ) == { + "adapter_supported": True, + "server_verified": True, + } + + report = ModelProviderFactory.capability_report( + "qwen", + options={"server_verified_protocols": ["responses"]}, + ) + assert report["server_verified_protocols"] == ["responses"] + assert report["protocol_status"]["responses"]["server_verified"] is True + + +@pytest.mark.parametrize( + ("provider", "alias", "canonical"), + [ + ("openai", "responses", "responses"), + ("openai", "response", "responses"), + ("anthropic", "anthropic", "messages"), + ("anthropic", "anthropic_messages", "messages"), + ("google", "gemini", "generate_content"), + ("google", "generate", "generate_content"), + ], +) +def test_factory_protocol_status_normalizes_protocol_aliases(provider, alias, canonical): + assert ModelProviderFactory.protocol_status(provider, alias) == ModelProviderFactory.protocol_status( + provider, canonical + ) + + +def test_factory_google_protocol_status_accepts_safe_http_options(): + status = ModelProviderFactory.protocol_status( + "google", + "generate_content", + options={"http_options": {"headers": {"X-Trace-ID": "trace"}}}, + ) + + assert status == {"adapter_supported": True, "server_verified": False} + report = ModelProviderFactory.capability_report( + "google", + options={"http_options": {"headers": {"X-Trace-ID": "trace"}}}, + ) + assert report["protocol_status"]["generate_content"]["server_verified"] is False + + +def test_factory_google_protocol_status_rejects_credentials_in_http_options(): + with pytest.raises(ValueError, match="http_options"): + ModelProviderFactory.protocol_status( + "google", + "generate_content", + options={"http_options": {"headers": {"Authorization": "Bearer secret"}}}, + ) + + +def test_openai_responses_request_values_override_model_options(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = { + "max_tokens": 256, + "verbosity": "low", + "response_format": {"type": "json_object"}, + } + + request = provider._build_responses_request( + prompt="hi", + stream=False, + max_tokens=512, + verbosity="high", + response_format={ + "type": "json_schema", + "json_schema": { + "name": "answer", + "schema": {"type": "object"}, + }, + }, + ) + + assert request["max_output_tokens"] == 512 + assert request["text"]["verbosity"] == "high" + assert request["text"]["format"]["name"] == "answer" + + +@pytest.mark.parametrize("field", ["thinking", "caching", "session", "expire_at"]) +def test_openai_responses_rejects_unsupported_model_options(field): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._options = {field: {"enabled": True} if field != "expire_at" else 123} + + with pytest.raises(ValueError, match=field): + provider._build_responses_request(prompt="hi", stream=False) + + +@pytest.mark.parametrize("protocol", ["chat_completions", "responses"]) +def test_ark_rejects_unknown_model_options(protocol): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = protocol + provider._options = {"unknown_option": True} + + with pytest.raises(ValueError, match="unknown_option"): + if protocol == "responses": + provider._build_responses_request(CompletionRequest(prompt="hi", stream=False)) + else: + provider._build_chat_request(prompt="hi", stream=False) + + +@pytest.mark.parametrize("protocol", ["chat_completions", "responses"]) +def test_openai_compatible_rejects_unknown_model_options(protocol): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = protocol + provider._server_verified_protocols = frozenset({"responses"}) + provider._options = {"unknown_option": True} + + with pytest.raises(ValueError, match="unknown_option"): + if protocol == "responses": + provider._build_responses_request(prompt="hi", stream=False) + else: + provider._build_chat_request(prompt="hi", stream=False) + + +def test_ark_responses_rejects_top_logprobs_at_request_and_model_boundaries(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + + provider._options = {} + with pytest.raises(ValueError, match="top_logprobs"): + provider._build_responses_request( + CompletionRequest(prompt="hi", stream=False, top_logprobs=2) + ) + + provider._options = {"top_logprobs": 2} + with pytest.raises(ValueError, match="top_logprobs"): + provider._build_responses_request(CompletionRequest(prompt="hi", stream=False)) + + +def test_ark_responses_validates_session_mapping_and_conversation_conflicts(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {} + + with pytest.raises(ValueError, match="conversation.*对象"): + provider._build_responses_request( + CompletionRequest(prompt="hi", stream=False, conversation="conversation-id") + ) + + with pytest.raises(ValueError, match="conversation.*session"): + provider._build_responses_request( + CompletionRequest( + prompt="hi", + stream=False, + conversation={"id": "conversation-id"}, + session={"id": "session-id"}, + ) + ) + + request = provider._build_responses_request( + CompletionRequest( + prompt="hi", stream=False, conversation={"id": "conversation-id"} + ) + ) + assert request["session"] == {"id": "conversation-id"} + + +def test_ark_native_response_resource_requires_explicit_model_and_rejects_unknown_kwargs(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "configured-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + def create(self, **kwargs): + calls.append(kwargs) + return "response" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(ValueError, match="model"): + provider.resources.create_response(input="raw") + with pytest.raises(ValueError, match="unknown_option"): + provider.resources.create_response( + input="raw", model="caller-model", unknown_option=True + ) + + assert provider.resources.create_response(input="raw", model="caller-model") == "response" + assert calls == [{"input": "raw", "model": "caller-model"}] + + +def test_secret_free_validation_isolates_nested_values(): + options = {"extra_body": {"routing": {"tenant": "demo"}}} + validated_options = validate_secret_free_options(options, "Provider") + validated_payload = validate_secret_free_payload(options, "Provider", "extra_body") + + assert validated_options is not options + assert validated_options["extra_body"] is not options["extra_body"] + assert validated_payload["extra_body"] is not options["extra_body"] + + options["extra_body"]["routing"]["tenant"] = "changed" + assert validated_options["extra_body"]["routing"]["tenant"] == "demo" + assert validated_payload["extra_body"]["routing"]["tenant"] == "demo" + + +def test_resource_security_validation_resolves_extension_key_variants(): + values = { + "Extra-Headers": {"X-Trace": "trace"}, + "EXTRA_QUERY": {"tenant": "demo"}, + "extra-body": {"routing": {"tenant": "demo"}}, + } + validated = validate_secret_free_resource_kwargs(values, "Provider") + + assert validated["Extra-Headers"] == {"X-Trace": "trace"} + assert validated["EXTRA_QUERY"] == {"tenant": "demo"} + assert validated["extra-body"] == {"routing": {"tenant": "demo"}} + assert validated["Extra-Headers"] is not values["Extra-Headers"] + assert validated["extra-body"] is not values["extra-body"] + + +@pytest.mark.parametrize( + "key", + [ + "openai_api_key", + "api-token", + "ApiToken", + "token", + "myClientSecret", + "myApiKey", + "myAPIKey", + "private_key", + "ssh_key", + "ssh-private-key", + "signing_key", + "encryption_key", + ], +) +def test_security_recognizes_separated_credential_key_variants(key): + assert is_sensitive_option_key(key) is True + + +def test_security_allows_non_credential_header_descriptors(): + assert is_sensitive_option_key("X-Authorization-Count") is False + assert is_sensitive_option_key("X-Credential-Provider") is False + + +def test_security_scans_string_values_for_embedded_credentials(): + found = find_sensitive_option_paths( + {"X-Trace": "Bearer secret-token", "tenant": "safe"} + ) + assert "X-Trace" in found + + +@pytest.mark.parametrize( + "value", + [ + '{"api_key": "secret-value"}', + "{'token': 'secret-value'}", + ], +) +def test_security_redacts_json_embedded_credentials(value): + redacted = redact_sensitive_text(value) + assert "secret-value" not in redacted + assert "REDACTED" in redacted + assert find_sensitive_option_paths({"message": value}) == ["message"] + + +@pytest.mark.parametrize( + "value", + [ + "token usage", + "token verification failed", + "what is the token budget", + "token bucket algorithm", + "renew authorization flow", + ], +) +def test_security_does_not_treat_ordinary_text_as_credentials(value): + assert redact_sensitive_text(value) == value + assert find_sensitive_option_paths({"note": value}) == [] + assert validate_secret_free_options({"note": value}, "Provider")["note"] == value + + +def test_security_still_redacts_whitespace_separated_credential_like_values(): + value = "token 0123456789abcdef" + assert "REDACTED" in redact_sensitive_text(value) + assert find_sensitive_option_paths({"value": value}) == ["value"] + + +def test_security_redacts_credentials_in_non_string_exception_values(): + class CredentialError: + def __str__(self): + return "Authorization: Bearer secret-token-123456" + + assert "REDACTED" in redact_sensitive_text(CredentialError()) + + +def test_secret_free_validation_deep_copies_sdk_models(): + http_options = types.HttpOptions(headers={"X-Trace-ID": "trace"}) + validated = validate_secret_free_options( + {"http_options": http_options}, + "Google", + allowed_containers={"http_options", "headers"}, + ) + + http_options.headers["X-Trace-ID"] = "changed" + assert validated["http_options"].headers["X-Trace-ID"] == "trace" + assert validated["http_options"] is not http_options + + +def test_openai_responses_merges_native_text_options(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._options = { + "text": {"verbosity": "low"}, + "response_format": {"type": "json_object"}, + } + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["text"] == { + "format": {"type": "json_object"}, + "verbosity": "low", + } + + +def test_openai_chat_model_max_tokens_uses_current_sdk_field(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._provider = "openai" + provider._options = {"max_tokens": 256} + + request = provider._build_chat_request(prompt="hi", stream=False, max_tokens=512) + + assert request["max_completion_tokens"] == 512 + assert "max_tokens" not in request + + +def test_openai_custom_gateway_keeps_legacy_chat_max_tokens_field(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "chat_completions" + provider._provider = "openai" + provider._base_url = "https://gateway.example/v1" + provider._options = {} + + request = provider._build_chat_request(prompt="hi", stream=False, max_tokens=512) + + assert request["max_tokens"] == 512 + assert "max_completion_tokens" not in request + + +@pytest.mark.parametrize("provider_name", ["qwen", "ollama", "lm-studio", "grok", "siliconflow"]) +def test_compatible_chat_models_keep_max_tokens_field(provider_name): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = provider_name + provider._protocol = "chat_completions" + provider._options = {} + + request = provider._build_chat_request(prompt="hi", stream=False, max_tokens=256) + + assert request["max_tokens"] == 256 + assert "max_completion_tokens" not in request + + +def test_google_async_invoke_stream_and_non_stream_issue_one_request_each(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + calls = {"stream": 0, "complete": 0} + + class AsyncModels: + def generate_content_stream(self, **_kwargs): + calls["stream"] += 1 + return iter([ + SimpleNamespace(text="流"), + SimpleNamespace(candidates=[SimpleNamespace(finish_reason="STOP")]), + ]) + + async def generate_content(self, **_kwargs): + calls["complete"] += 1 + return SimpleNamespace(text="非流") + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + + async def collect(stream): + return [chunk async for chunk in provider.ainvoke( + prompt="hi", stream=stream, temperature=None + )] + + assert asyncio.run(collect(True)) == ["流"] + assert calls == {"stream": 1, "complete": 0} + assert asyncio.run(collect(False)) == ["非流"] + assert calls == {"stream": 1, "complete": 1} + + +def test_google_query_embedding_uses_query_task(monkeypatch): + provider = object.__new__(GoogleProvider) + provider._model_name = "embedding" + provider._options = {} + response = SimpleNamespace(embeddings=[SimpleNamespace(values=[1.0, 2.0])]) + client = SimpleNamespace(models=SimpleNamespace(embed_content=lambda **kwargs: response)) + provider._get_client = lambda: client + assert provider.embed_documents(["question"], task_type="RETRIEVAL_QUERY") == [[1.0, 2.0]] + + +def test_google_http_extensions_map_to_sdk_http_options(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + config = provider._build_generation_config( + "system", None, 0.2, + extra_headers={"X-Trace": "trace-1"}, + extra_body={"tenant": "demo"}, + timeout=1.25, + ) + + assert config.http_options.headers == {"X-Trace": "trace-1"} + assert config.http_options.extra_body == {"tenant": "demo"} + assert config.http_options.timeout == 1250 + with pytest.raises(ValueError, match="extra_query"): + provider._build_generation_config("system", None, 0.2, extra_query={"tenant": "demo"}) + + +@pytest.mark.parametrize("field", ["timeout", "extra_body"]) +def test_google_http_options_conflict_with_model_level_http_fields_at_init(field): + options = {"http_options": {"headers": {"X-Trace": "trace"}}} + options[field] = 2 if field == "timeout" else {"tenant": "model"} + + with pytest.raises(ValueError, match="http_options.*模型级.*" + field): + GoogleProvider("gemini", options=options) + + +def test_google_model_http_options_apply_to_embedding_and_token_count(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {"timeout": 2, "extra_body": {"tenant": "model"}} + seen = {} + + class Models: + def count_tokens(self, **kwargs): + seen["count"] = kwargs + return SimpleNamespace(total_tokens=3) + + def embed_content(self, **kwargs): + seen["embed"] = kwargs + return SimpleNamespace(embeddings=[SimpleNamespace(values=[1.0, 2.0])]) + + provider._get_client = lambda: SimpleNamespace(models=Models()) + + assert provider.count_tokens(CompletionRequest(prompt="hi")) == 3 + assert provider.embed_documents(["doc"]) == [[1.0, 2.0]] + for call in (seen["count"], seen["embed"]): + assert call["config"].http_options.timeout == 2000 + assert call["config"].http_options.extra_body == {"tenant": "model"} + + +def test_google_request_headers_reject_sensitive_keys(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-test" + provider._options = {} + + with pytest.raises(ValueError, match="请求头/query"): + provider._build_generation_config( + "system", + None, + 0.2, + extra_headers={"X-API-Key": "secret"}, + ) + + +def test_google_async_token_count_and_embedding_forward_http_options(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + seen = {} + + class AsyncModels: + async def count_tokens(self, **kwargs): + seen["count"] = kwargs + return SimpleNamespace(total_tokens=7) + + async def embed_content(self, **kwargs): + seen["embed"] = kwargs + return SimpleNamespace(embeddings=[SimpleNamespace(values=[0.1, 0.2])]) + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + request = CompletionRequest( + prompt="hi", extra_headers={"X-Trace": "trace-2"}, + extra_body={"tenant": "demo"}, timeout=2, + ) + + assert asyncio.run(provider.async_count_tokens(request)) == 7 + count_http = seen["count"]["config"].http_options + assert count_http.headers == {"X-Trace": "trace-2"} + assert count_http.extra_body == {"tenant": "demo"} + assert count_http.timeout == 2000 + + assert asyncio.run(provider.aembed_documents( + ["doc"], extra_headers={"X-Trace": "trace-3"}, timeout=3, + )) == [[0.1, 0.2]] + embed_config = seen["embed"]["config"] + assert embed_config.http_options.headers == {"X-Trace": "trace-3"} + assert embed_config.http_options.timeout == 3000 + + +def test_google_compute_tokens_forwards_http_options_sync_and_async(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + seen = {} + + class Models: + def compute_tokens(self, **kwargs): + seen["sync"] = kwargs + return "sync" + + class AsyncModels: + async def compute_tokens(self, **kwargs): + seen["async"] = kwargs + return "async" + + provider._get_client = lambda: SimpleNamespace( + models=Models(), aio=SimpleNamespace(models=AsyncModels()) + ) + request = CompletionRequest( + prompt="hi", + extra_headers={"X-Trace": "trace"}, + extra_body={"tenant": "demo"}, + timeout=2, + ) + + assert provider.compute_tokens(request) == "sync" + assert asyncio.run(provider.async_compute_tokens(request)) == "async" + for call in (seen["sync"], seen["async"]): + assert call["model"] == "gemini" + assert call["config"].http_options.headers == {"X-Trace": "trace"} + assert call["config"].http_options.extra_body == {"tenant": "demo"} + assert call["config"].http_options.timeout == 2000 + + +def test_google_token_count_rejects_ignored_generation_fields(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + with pytest.raises(ValueError, match="max_tokens"): + provider.count_tokens(CompletionRequest(prompt="hi", max_tokens=10)) + with pytest.raises(ValueError, match="tools"): + provider.compute_tokens(CompletionRequest(prompt="hi", tools=[{"type": "function"}])) + + +def test_google_compute_tokens_rejects_gemini_developer_mode(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + provider._get_client = lambda: SimpleNamespace( + _api_client=SimpleNamespace(vertexai=False), + models=SimpleNamespace(compute_tokens=lambda **_: None), + ) + with pytest.raises(ValueError, match="仅支持 Vertex"): + provider.compute_tokens(CompletionRequest(prompt="hi")) + + +def test_openai_embedding_rejects_call_level_model_and_input_override(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "embedding" + provider._provider = "openai" + provider._options = {} + provider._client = None + + with pytest.raises(ValueError, match="不允许覆盖请求字段"): + provider.embed_documents(["text"], model="other") + + +def test_google_stream_events_cover_text_thought_tool_usage_and_finish(): + chunk = SimpleNamespace( + response_id="gemini-1", + candidates=[ + SimpleNamespace( + content=SimpleNamespace( + parts=[ + SimpleNamespace(text="思考", thought=True), + SimpleNamespace( + function_call=SimpleNamespace(name="lookup", args={"id": 1}) + ), + SimpleNamespace(text="答案", thought=False), + ] + ), + finish_reason="STOP", + ) + ], + usage_metadata=SimpleNamespace(prompt_token_count=2, candidates_token_count=3, total_token_count=5), + ) + + events = GoogleProvider._stream_events_from_chunk(chunk) + + assert [event.type for event in events] == [ + "reasoning_delta", "tool_call_delta", "text_delta", "finish", "usage" + ] + assert events[1].tool_call["arguments"] == {"id": 1} + assert events[-1].usage["total_tokens"] == 5 + + +def test_google_tool_call_merge_preserves_nested_partial_arguments(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "arguments": {"query": {"language": "zh"}}}, + ) + _, merged = GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "arguments": {"query": {"limit": 5}}}, + ) + + assert merged["arguments"] == { + "query": {"language": "zh", "limit": 5}, + } + + +def test_google_tool_call_merge_keeps_index_when_later_fragment_omits_id(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "index": 0, "name": "lookup", "arguments": {"first": 1}}, + ) + _, merged = GoogleProvider._merge_tool_call( + accumulator, + {"index": 0, "arguments": {"second": 2}}, + ) + + assert len(accumulator) == 1 + assert merged["id"] == "call-1" + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_keeps_id_when_later_fragment_has_no_identity_fields(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "name": "lookup", "arguments": {"first": 1}}, + ) + _, merged = GoogleProvider._merge_tool_call( + accumulator, + {"name": "lookup", "arguments": {"second": 2}}, + ) + + assert len(accumulator) == 1 + assert merged["id"] == "call-1" + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_migrates_index_to_late_id(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"index": 0, "name": "lookup", "arguments": {"first": 1}}, + ) + key, merged = GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "name": "lookup", "arguments": {"second": 2}}, + ) + + assert key == ("index", 0) + assert len(accumulator) == 1 + assert merged["id"] == "call-1" + assert merged["index"] == 0 + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_keeps_index_when_late_id_also_has_index(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"index": 0, "name": "lookup", "arguments": {"first": 1}}, + ) + key, merged = GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "index": 0, "name": "lookup", "arguments": {"second": 2}}, + ) + + assert key == ("index", 0) + assert list(accumulator) == [("index", 0)] + assert merged["id"] == "call-1" + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_prefers_index_when_later_fragment_has_index(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"id": "call-1", "name": "lookup", "arguments": {"first": 1}}, + ) + key, merged = GoogleProvider._merge_tool_call( + accumulator, + {"index": 0, "name": "lookup", "arguments": {"second": 2}}, + ) + + assert key == ("index", 0) + assert len(accumulator) == 1 + assert merged["id"] == "call-1" + assert merged["index"] == 0 + assert merged["arguments"] == {"first": 1, "second": 2} + + +def test_google_tool_call_merge_rejects_ambiguous_unidentified_parallel_calls(): + accumulator = {} + GoogleProvider._merge_tool_call( + accumulator, + {"name": "lookup", "arguments": {"first": 1}}, + ) + + with pytest.raises(RuntimeError, match="缺少稳定 id/index"): + GoogleProvider._merge_tool_call( + accumulator, + {"name": "lookup", "arguments": {"first": 2}}, + ) + + +def test_google_sync_stream_merges_tool_call_when_id_arrives_after_first_chunk(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + response = iter( + [ + chunk(SimpleNamespace(name="lookup", args={"first": 1})), + chunk(SimpleNamespace(id="call-1", args={"second": 2})), + chunk(finish_reason="STOP"), + ] + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: response) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert [event.type for event in events][-2:] == ["tool_call_completed", "finish"] + assert completed[0].response_id == "response-1" + assert completed[0].raw.response_id == "response-1" + assert completed[0].tool_call == { + "type": "function", + "arguments": {"first": 1, "second": 2}, + "name": "lookup", + "id": "call-1", + } + + +def test_google_sync_stream_defers_unidentified_tool_completion_until_finish(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + response = iter( + [ + chunk(SimpleNamespace(name="lookup", args={"id": 1}, will_continue=False)), + chunk(finish_reason="STOP"), + ] + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: response) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert [event.type for event in events][-2:] == ["tool_call_completed", "finish"] + + +def test_google_sync_stream_does_not_duplicate_string_tool_arguments_on_completion(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + function_call = SimpleNamespace( + id="call-1", + name="lookup", + args='{"id":1}', + will_continue=False, + ) + chunk = SimpleNamespace( + response_id="response-1", + candidates=[ + SimpleNamespace( + content=SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]), + finish_reason="STOP", + ) + ], + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: iter([chunk])) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert completed[0].tool_call["arguments"] == '{"id":1}' + + +def test_google_async_stream_merges_tool_call_when_id_arrives_after_first_chunk(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + async def chunks(): + yield chunk(SimpleNamespace(name="lookup", args={"first": 1})) + yield chunk(SimpleNamespace(id="call-1", args={"second": 2})) + yield chunk(finish_reason="STOP") + + class AsyncModels: + def generate_content_stream(self, **_kwargs): + return chunks() + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + + events = asyncio.run(_collect_google_async_events(provider)) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert [event.type for event in events][-2:] == ["tool_call_completed", "finish"] + assert completed[0].response_id == "response-1" + assert completed[0].raw is not None + assert completed[0].tool_call == { + "type": "function", + "arguments": {"first": 1, "second": 2}, + "name": "lookup", + "id": "call-1", + } + + +def test_google_async_stream_does_not_duplicate_string_tool_arguments_on_completion(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + function_call = SimpleNamespace( + id="call-1", + name="lookup", + args='{"id":1}', + will_continue=False, + ) + chunk = SimpleNamespace( + response_id="response-1", + candidates=[ + SimpleNamespace( + content=SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]), + finish_reason="STOP", + ) + ], + ) + + async def chunks(): + yield chunk + + class AsyncModels: + def generate_content_stream(self, **_kwargs): + return chunks() + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + + events = asyncio.run(_collect_google_async_events(provider)) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert len(completed) == 1 + assert completed[0].tool_call["arguments"] == '{"id":1}' + + +def test_google_sync_stream_keeps_parallel_tool_calls_separate_without_index(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + response = iter( + [ + chunk(SimpleNamespace(id="call-1", name="lookup", args={"first": 1})), + chunk(SimpleNamespace(id="call-2", name="search", args={"first": 2})), + chunk(SimpleNamespace(id="call-1", args={"second": 1})), + chunk(SimpleNamespace(id="call-2", args={"second": 2})), + chunk(finish_reason="STOP"), + ] + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: response) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert [event.type for event in events][-3:] == [ + "tool_call_completed", + "tool_call_completed", + "finish", + ] + assert {event.tool_call["id"] for event in completed} == {"call-1", "call-2"} + assert {event.tool_call["name"] for event in completed} == {"lookup", "search"} + assert {event.tool_call["arguments"]["second"] for event in completed} == {1, 2} + + +def test_google_async_stream_keeps_parallel_tool_calls_separate_without_index(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + async def chunks(): + yield chunk(SimpleNamespace(id="call-1", name="lookup", args={"first": 1})) + yield chunk(SimpleNamespace(id="call-2", name="search", args={"first": 2})) + yield chunk(SimpleNamespace(id="call-1", args={"second": 1})) + yield chunk(SimpleNamespace(id="call-2", args={"second": 2})) + yield chunk(finish_reason="STOP") + + class AsyncModels: + def generate_content_stream(self, **_kwargs): + return chunks() + + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace(models=AsyncModels())) + + events = asyncio.run(_collect_google_async_events(provider)) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert [event.type for event in events][-3:] == [ + "tool_call_completed", + "tool_call_completed", + "finish", + ] + assert {event.tool_call["id"] for event in completed} == {"call-1", "call-2"} + assert {event.tool_call["name"] for event in completed} == {"lookup", "search"} + assert {event.tool_call["arguments"]["second"] for event in completed} == {1, 2} + + +def test_google_sync_stream_migrates_parallel_unidentified_calls_by_name(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + def chunk(function_call=None, finish_reason=None): + content = SimpleNamespace(parts=[SimpleNamespace(function_call=function_call)]) if function_call else None + return SimpleNamespace( + response_id="response-1", + candidates=[SimpleNamespace(content=content, finish_reason=finish_reason)], + ) + + response = iter( + [ + chunk(SimpleNamespace(name="lookup", args={"first": 1})), + chunk(SimpleNamespace(name="search", args={"first": 2})), + chunk(SimpleNamespace(id="call-1", name="lookup", args={"second": 1})), + chunk(SimpleNamespace(id="call-2", name="search", args={"second": 2})), + chunk(finish_reason="STOP"), + ] + ) + provider._get_client = lambda: SimpleNamespace( + models=SimpleNamespace(generate_content_stream=lambda **_kwargs: response) + ) + + events = list(provider.stream_events(CompletionRequest(prompt="hi"))) + completed = [event for event in events if event.type == "tool_call_completed"] + + assert {event.tool_call["id"] for event in completed} == {"call-1", "call-2"} + assert {event.tool_call["name"] for event in completed} == {"lookup", "search"} + assert {event.tool_call["arguments"]["first"] for event in completed} == {1, 2} + assert {event.tool_call["arguments"]["second"] for event in completed} == {1, 2} + + +def test_google_contents_links_function_call_content_to_followup_tool_result(): + contents = GoogleProvider._contents( + [ + { + "role": "assistant", + "content": [ + { + "type": "function_call", + "id": "call-1", + "name": "lookup", + "arguments": {"id": 1}, + } + ], + }, + { + "role": "tool", + "tool_call_id": "call-1", + "content": '{"value": 2}', + }, + ], + None, + None, + ) + + function_call = contents[0].parts[0].function_call + function_response = contents[1].parts[0].function_response + assert function_call.id == "call-1" + assert function_call.name == "lookup" + assert function_response.id == "call-1" + assert function_response.name == "lookup" + assert function_response.response == {"value": 2} + + +@pytest.mark.parametrize("value", ["123abc", "-", "01", "-01"]) +def test_google_tool_result_numeric_prefix_that_is_not_json_stays_text(value): + contents = GoogleProvider._contents( + [ + {"role": "assistant", "content": [{"type": "function_call", "id": "call-1", "name": "lookup", "arguments": {}}]}, + {"role": "tool", "tool_call_id": "call-1", "content": value}, + ], + None, + None, + ) + + assert contents[1].parts[0].function_response.response == {"output": value} + + +@pytest.mark.parametrize("value", ["0", "-1", "1.25", "2e3"]) +def test_google_tool_result_complete_json_number_is_decoded(value): + contents = GoogleProvider._contents( + [ + {"role": "assistant", "content": [{"type": "function_call", "id": "call-1", "name": "lookup", "arguments": {}}]}, + {"role": "tool", "tool_call_id": "call-1", "content": value}, + ], + None, + None, + ) + + assert contents[1].parts[0].function_response.response == {"output": json.loads(value)} + + +def test_anthropic_stream_events_cover_text_thinking_tool_usage_and_finish(): + events = [ + AnthropicProvider._stream_event( + SimpleNamespace(type="content_block_delta", delta=SimpleNamespace(type="text_delta", text="答")), + "msg-1", + ), + AnthropicProvider._stream_event( + SimpleNamespace(type="content_block_delta", delta=SimpleNamespace(type="thinking_delta", thinking="想")), + "msg-1", + ), + AnthropicProvider._stream_event( + SimpleNamespace( + type="content_block_delta", + index=0, + delta=SimpleNamespace(type="input_json_delta", partial_json='{"id":1}'), + ), + "msg-1", + ), + AnthropicProvider._stream_event( + SimpleNamespace( + type="message_delta", + delta=SimpleNamespace(stop_reason="tool_use"), + usage=SimpleNamespace(input_tokens=2, output_tokens=3), + ), + "msg-1", + ), + ] + + assert [event.type for event in events if event] == [ + "text_delta", "reasoning_delta", "tool_call_delta", "finish" + ] + assert events[3].finish_reason == "tool_use" + assert events[3].usage["output_tokens"] == 3 + + +def test_ark_stream_events_cover_chat_and_responses_terminal_metadata(): + chat = VolcengineProvider._chat_stream_event( + SimpleNamespace( + id="chat-1", + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content="答", reasoning_content="想", tool_calls=None), + finish_reason=None, + ) + ], + ) + ) + provider = object.__new__(VolcengineProvider) + response = provider._responses_stream_event( + SimpleNamespace( + type="response.completed", + response=SimpleNamespace( + id="ark-1", status="completed", usage=SimpleNamespace(input_tokens=1, output_tokens=2) + ), + ) + ) + + assert chat.type == "text_delta" + assert chat.text == "答" + assert response.type == "finish" + assert response.finish_reason == "completed" + assert ( + response.usage["total_tokens"] + if "total_tokens" in response.usage + else response.usage["input_tokens"] + ) == 1 + + +def test_ark_invoke_non_stream_rejects_failed_response_status(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + def create(self, **_kwargs): + return SimpleNamespace( + status="failed", error=SimpleNamespace(message="ark failed") + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + with pytest.raises(RuntimeError, match="ark failed"): + list(provider.invoke("hi", stream=False)) + + +def test_ark_ainvoke_non_stream_rejects_failed_response_status(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + async def create(self, **_kwargs): + return SimpleNamespace( + status="failed", error=SimpleNamespace(message="ark async failed") + ) + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def collect(): + return [item async for item in provider.ainvoke("hi", stream=False)] + + with pytest.raises(RuntimeError, match="ark async failed"): + asyncio.run(collect()) + + +def test_ark_non_stream_result_preserves_chat_reasoning_content(): + response = SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace( + content="答复", reasoning_content="思考过程", tool_calls=[] + ), + finish_reason="stop", + ) + ], + usage=SimpleNamespace(input_tokens=1, output_tokens=2), + ) + result = VolcengineProvider._extract_result(response) + + assert result.text == "答复" + assert result.reasoning == "思考过程" + + +def test_ark_responses_result_does_not_replace_answer_with_reasoning_text(): + response = SimpleNamespace( + output_text="最终答案", + output=[ + SimpleNamespace( + type="reasoning", + summary=[SimpleNamespace(type="summary_text", text="思考过程")], + ) + ], + choices=[], + usage=None, + status="completed", + id="resp-1", + ) + + result = VolcengineProvider._extract_result(response) + + assert result.text == "最终答案" + assert result.reasoning == "思考过程" + + +def test_ark_responses_error_and_cancelled_events_are_explicit(): + provider = object.__new__(VolcengineProvider) + + with pytest.raises(RuntimeError, match="rate limit"): + provider._responses_stream_event( + SimpleNamespace(type="response.error", error=SimpleNamespace(message="rate limit")) + ) + + with pytest.raises(RuntimeError, match="取消|cancelled"): + provider._responses_stream_event( + SimpleNamespace(type="response.cancelled", response=SimpleNamespace(status="cancelled")) + ) + + +def test_ark_responses_custom_tool_stream_uses_output_item_metadata(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + + class Responses: + def create(self, **_request): + return iter( + [ + SimpleNamespace( + type="response.output_item.added", + output_index=0, + item=SimpleNamespace( + type="custom_tool_call", + id="item-1", + call_id="call-1", + name="run_command", + input="", + ), + ), + SimpleNamespace( + type="response.custom_tool_call_input.delta", + item_id="item-1", + output_index=0, + delta="echo hello", + ), + SimpleNamespace( + type="response.custom_tool_call_input.done", + item_id="item-1", + output_index=0, + input="echo hello", + ), + SimpleNamespace( + type="response.completed", + response=SimpleNamespace(status="completed"), + ), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + events = list(provider.stream_events(CompletionRequest(prompt="hello"))) + + assert [event.type for event in events] == [ + "tool_call_delta", + "tool_call_completed", + "finish", + ] + assert events[1].tool_call["type"] == "custom_tool_call" + assert events[1].tool_call["name"] == "run_command" + + +def test_anthropic_rejects_seed_in_complete_request(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-test" + provider._options = {} + + with pytest.raises(ValueError, match="seed"): + provider.complete(CompletionRequest(prompt="hi", seed=7)) + with pytest.raises(ValueError, match="seed"): + provider.parse(dict, CompletionRequest(prompt="hi", seed=7)) + with pytest.raises(ValueError, match="seed"): + asyncio.run(provider.async_parse(dict, CompletionRequest(prompt="hi", seed=7))) + + +def test_sdk_provider_close_and_aclose_release_clients(): + class SyncClient: + def __init__(self): + self.closed = False + + def close(self): + self.closed = True + + class AsyncClient: + def __init__(self): + self.closed = False + + async def close(self): + self.closed = True + + providers = [ + OpenAICompatibleProvider.__new__(OpenAICompatibleProvider), + VolcengineProvider.__new__(VolcengineProvider), + AnthropicProvider.__new__(AnthropicProvider), + ] + for provider in providers: + sync = SyncClient() + async_client = AsyncClient() + provider._client = sync + provider._aclient = async_client + provider.close() + assert sync.closed is True + assert provider._client is None + assert async_client.closed is True + assert provider._aclient is None + asyncio.run(provider.aclose()) + assert async_client.closed is True + assert provider._aclient is None + + +def test_anthropic_sync_close_requires_aclose_inside_running_event_loop(): + class AsyncClient: + def __init__(self): + self.closed = False + + async def close(self): + self.closed = True + + provider = AnthropicProvider.__new__(AnthropicProvider) + async_client = AsyncClient() + provider._client = None + provider._aclient = async_client + + async def run() -> None: + with pytest.raises(RuntimeError, match="需要异步关闭.*aclose"): + provider.close() + assert provider._aclient is async_client + await provider.aclose() + + asyncio.run(run()) + assert async_client.closed is True + assert provider._aclient is None + + +def test_sdk_provider_aclose_closes_shared_client_once(): + class SharedClient: + def __init__(self): + self.close_calls = 0 + + async def close(self): + self.close_calls += 1 + + for provider_type in ( + OpenAICompatibleProvider, + VolcengineProvider, + AnthropicProvider, + ): + provider = provider_type.__new__(provider_type) + shared = SharedClient() + provider._client = shared + provider._aclient = shared + + asyncio.run(provider.aclose()) + + assert shared.close_calls == 1 + assert provider._client is None + assert provider._aclient is None + + +def test_google_close_releases_sync_client_and_async_close_releases_aio_client(): + class SyncClient: + def __init__(self): + self.closed = False + self.aio = self + + def close(self): + self.closed = True + + async def aclose(self): + self.closed = True + + provider = GoogleProvider.__new__(GoogleProvider) + client = SyncClient() + provider._client = client + + provider.close() + assert client.closed is True + assert provider._client is None + + provider._client = client + asyncio.run(provider.aclose()) + assert client.closed is True + assert provider._client is None + + +def test_google_vertex_gemini_embedding_uses_single_content_path(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-embedding-001" + + assert provider._vertex_embed_content_only() is True + + +def test_anthropic_complete_extracts_tool_blocks_and_usage(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + response = SimpleNamespace( + content=[ + SimpleNamespace(type="text", text="answer"), + SimpleNamespace(type="tool_use", id="call-1", name="lookup", input={"id": 1}), + ], + usage=SimpleNamespace(input_tokens=4, output_tokens=5), + stop_reason="tool_use", + ) + provider._get_client = lambda: SimpleNamespace(messages=SimpleNamespace(create=lambda **_: response)) + result = provider.complete(CompletionRequest(prompt="hi", stream=False)) + assert result.text == "answer" + assert result.tool_calls[0]["arguments"] == {"id": 1} + assert result.usage == {"input_tokens": 4, "output_tokens": 5} + + +def test_anthropic_user_maps_to_sdk_user_profile_header(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + params = provider._build_message_params("hi", user="profile-1") + assert params["user_profile_id"] == "profile-1" + assert "user_id" not in params.get("metadata", {}) + + +def test_openai_parse_and_compact_facades_forward_native_methods(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + calls = {} + + class Completions: + def parse(self, **kwargs): + calls["parse"] = kwargs + return "parsed" + + class Responses: + def compact(self, **kwargs): + calls["compact"] = kwargs + return "compacted" + + provider._get_client = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()), responses=Responses() + ) + assert provider.parse(dict, CompletionRequest(prompt="hi")) == "parsed" + assert calls["parse"]["model"] == "demo" + assert "stream" not in calls["parse"] + provider._protocol = "responses" + assert provider.compact_responses( + input="hello", previous_response_id="resp-1", timeout=2 + ) == "compacted" + assert calls["compact"] == { + "model": "demo", + "input": "hello", + "previous_response_id": "resp-1", + "timeout": 2, + } + + +def test_openai_compatible_responses_parse_and_compact_require_verified_server_capability(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "qwen" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + provider._options = {} + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.parse(dict, CompletionRequest(prompt="hi")) + with pytest.raises(ValueError, match="未验证 Responses"): + provider.compact_responses(input="hello") + with pytest.raises(ValueError, match="未验证 Responses"): + asyncio.run(provider.async_parse(dict, CompletionRequest(prompt="hi"))) + with pytest.raises(ValueError, match="未验证 Responses"): + asyncio.run(provider.async_compact_responses(input="hello")) + + +@pytest.mark.parametrize("interval", [0, -1, "0"]) +def test_openai_responses_background_poll_interval_must_be_positive(interval): + provider = object.__new__(OpenAICompatibleProvider) + provider._options = {"background_poll_interval": interval} + with pytest.raises(ValueError, match="background_poll_interval.*正数"): + provider._background_poll_config() + + +def test_openai_custom_base_url_requires_verified_responses_resource_capability(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://gateway.example/v1" + provider._server_verified_protocols = frozenset() + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.retrieve_response("resp-1") + + +def test_openai_custom_base_url_blocks_regular_responses_requests(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://gateway.example/v1" + provider._server_verified_protocols = frozenset() + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.complete(CompletionRequest(prompt="hi")) + with pytest.raises(ValueError, match="未验证 Responses"): + list(provider.invoke("hi")) + with pytest.raises(ValueError, match="未验证 Responses"): + list(provider.stream_events(CompletionRequest(prompt="hi"))) + + +def test_openai_custom_base_url_blocks_responses_batch_requests(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://gateway.example/v1" + provider._server_verified_protocols = frozenset() + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.create_batch("file-1") + + +def test_openai_official_base_url_allows_responses_resource_contract(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "openai" + provider._protocol = "responses" + provider._base_url = "https://api.openai.com/v1/" + provider._server_verified_protocols = frozenset() + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace(retrieve=lambda response_id, **kwargs: (response_id, kwargs)) + ) + + assert provider.retrieve_response("resp-1", include=["output"]) == ( + "resp-1", {"include": ["output"]} + ) + + +def test_openai_eval_output_item_retrieve_forwards_sync_and_async_resources(): + provider = object.__new__(OpenAIProvider) + provider._model_name = "demo" + calls = {} + + class OutputItems: + def retrieve(self, output_item_id, **kwargs): + calls["sync"] = (output_item_id, kwargs) + return "retrieved" + + provider._get_client = lambda: SimpleNamespace( + evals=SimpleNamespace(runs=SimpleNamespace(output_items=OutputItems())) + ) + assert provider.retrieve_eval_run_output_item("eval-1", "run-1", "item-1", limit=2) == "retrieved" + assert calls["sync"] == ("item-1", {"eval_id": "eval-1", "run_id": "run-1", "limit": 2}) + assert provider.resources.retrieve_eval_run_output_item("eval-1", "run-1", "item-1") == "retrieved" + + class AsyncOutputItems: + async def retrieve(self, output_item_id, **kwargs): + calls["async"] = (output_item_id, kwargs) + return "async-retrieved" + + provider._get_aclient = lambda: SimpleNamespace( + evals=SimpleNamespace(runs=SimpleNamespace(output_items=AsyncOutputItems())) + ) + assert asyncio.run( + provider.async_retrieve_eval_run_output_item("eval-1", "run-1", "item-1", limit=3) + ) == "async-retrieved" + assert calls["async"] == ("item-1", {"eval_id": "eval-1", "run_id": "run-1", "limit": 3}) + assert asyncio.run( + provider.async_resources.retrieve_eval_run_output_item("eval-1", "run-1", "item-1") + ) == "async-retrieved" + + +def test_resource_facade_exposes_native_sdk_clients(): + provider = object.__new__(OpenAICompatibleProvider) + sync_client = object() + async_client = object() + provider._get_client = lambda: sync_client + provider._get_aclient = lambda: async_client + + assert provider.resources.native is sync_client + assert provider.async_resources.native is sync_client + assert provider.async_resources.async_native is async_client + + +def test_official_resource_facade_keeps_missing_dynamic_attributes_normal(): + provider = object.__new__(OpenAIProvider) + provider._get_client = lambda: SimpleNamespace(existing="value") + provider._get_aclient = lambda: SimpleNamespace(existing="async-value") + + resources = provider.resources + assert resources.existing == "value" + assert not hasattr(resources, "missing_resource") + assert not hasattr(resources, "__missing_resource__") + + +def test_openai_resources_expose_new_official_top_level_resources(): + provider = object.__new__(OpenAIProvider) + sync_resources = SimpleNamespace( + skills="skills", + realtime="realtime", + webhooks="webhooks", + admin="admin", + content_provenance_checks="provenance", + ) + async_resources = SimpleNamespace( + skills="async-skills", + realtime="async-realtime", + webhooks="async-webhooks", + admin="async-admin", + content_provenance_checks="async-provenance", + ) + provider._get_client = lambda: sync_resources + provider._get_aclient = lambda: async_resources + + assert provider.resources.skills == "skills" + assert provider.resources.realtime == "realtime" + assert provider.resources.webhooks == "webhooks" + assert provider.resources.admin == "admin" + assert provider.resources.content_provenance_checks == "provenance" + assert provider.async_resources.skills == "async-skills" + assert provider.async_resources.realtime == "async-realtime" + assert provider.async_resources.webhooks == "async-webhooks" + assert provider.async_resources.admin == "async-admin" + assert provider.async_resources.content_provenance_checks == "async-provenance" + + +def test_openai_capabilities_include_current_top_level_resource_families(): + capabilities = ModelProviderFactory.capabilities("openai") + assert { + "skills", + "realtime", + "webhooks", + "admin", + "content_provenance_checks", + }.issubset(capabilities) + + +def test_factory_capability_report_includes_sdk_and_protocol_evidence(): + report = ModelProviderFactory.capability_report("openai") + + assert report["provider"] == "openai" + assert report["resolved_provider"] == "openai" + assert report["role"] == "llm" + assert report["integration"] == "native_sdk" + assert report["sdk"] == {"package": "openai", "version": report["sdk"]["version"]} + assert report["sdk"]["version"] + assert report["protocol_status"]["responses"] == { + "adapter_supported": True, + "server_verified": False, + } + + +def test_factory_capabilities_are_queryable_by_role_and_alias(): + embedding = ModelProviderFactory.capability_report("qwen", "embedding") + assert embedding["role"] == "embedding" + assert embedding["integration"] == "openai_compatible_sdk" + assert embedding["sdk"]["package"] == "openai" + assert "responses" not in embedding["protocol_status"] + + rerank = ModelProviderFactory.capability_report("siliconflow", "rerank") + assert rerank["resolved_provider"] == "siliconflow_rerank" + assert rerank["integration"] == "http_json" + assert rerank["sdk"]["package"] == "httpx" + assert rerank["protocols"] == [] + + +def test_factory_capability_report_rejects_unknown_role(): + with pytest.raises(ValueError, match="不支持的模型角色"): + ModelProviderFactory.capability_report("openai", "moderation") + + +def test_official_protocol_status_does_not_claim_unverified_custom_endpoints(): + assert ModelProviderFactory.protocol_status("openai", "chat_completions") == { + "adapter_supported": True, + "server_verified": False, + } + assert ModelProviderFactory.protocol_status("openai", "responses") == { + "adapter_supported": True, + "server_verified": False, + } + assert ModelProviderFactory.protocol_status("volcengine", "ark") == { + "adapter_supported": True, + "server_verified": False, + } + assert ModelProviderFactory.protocol_status("volcengine", "responses") == { + "adapter_supported": True, + "server_verified": False, + } + + +def test_factory_rejects_server_verified_protocol_alias_not_supported_by_provider(): + with pytest.raises(ValueError, match="不适用于当前提供商"): + ModelProviderFactory.capability_report( + "qwen", options={"server_verified_protocols": ["gemini"]} + ) + with pytest.raises(ValueError, match="不适用于当前提供商"): + ModelProviderFactory.protocol_status( + "qwen", "generate_content", options={"server_verified_protocols": ["gemini"]} + ) + + +def test_google_auth_tokens_facade_forwards_sync_and_async_clients(): + provider = object.__new__(GoogleProvider) + calls = {} + + class Tokens: + def create(self, **kwargs): + calls["sync"] = kwargs + return "token" + + class AsyncTokens: + async def create(self, **kwargs): + calls["async"] = kwargs + return "async-token" + + sync_tokens = Tokens() + async_tokens = AsyncTokens() + provider._get_client = lambda: SimpleNamespace( + auth_tokens=sync_tokens, aio=SimpleNamespace(auth_tokens=async_tokens) + ) + + # Facade 资源经过安全代理;显式 native 出口仍保留 SDK 原生身份。 + assert provider.resources.auth_tokens._value is sync_tokens + assert provider.async_resources.auth_tokens._value is async_tokens + assert provider.resources.native.auth_tokens is sync_tokens + assert provider.async_resources.async_native.auth_tokens is async_tokens + assert provider.resources.create_auth_token(config={}) == "token" + assert asyncio.run(provider.async_resources.create_auth_token(config={})) == "async-token" + assert calls == {"sync": {"config": {}}, "async": {"config": {}}} + + with pytest.raises(ValueError, match="凭证"): + provider.resources.auth_tokens.create(extra_headers={"X-API-Key": "secret"}) + + +def test_google_register_files_facade_allows_auth_business_parameter_sync_and_async(): + provider = object.__new__(GoogleProvider) + auth = AnonymousCredentials() + calls = {} + + class Files: + def register_files(self, **kwargs): + calls["sync"] = kwargs + return "registered" + + class AsyncFiles: + async def register_files(self, **kwargs): + calls["async"] = kwargs + return "async-registered" + + provider._get_client = lambda: SimpleNamespace( + files=Files(), aio=SimpleNamespace(files=AsyncFiles()) + ) + + assert provider.resources.register_files(auth=auth, uris=["gs://bucket/file"]) == "registered" + assert asyncio.run( + provider.async_resources.register_files(auth=auth, uris=["gs://bucket/file"]) + ) == "async-registered" + assert calls == { + "sync": {"auth": auth, "uris": ["gs://bucket/file"]}, + "async": {"auth": auth, "uris": ["gs://bucket/file"]}, + } + + +def test_google_register_files_facade_rejects_non_credentials_auth_before_sdk_call(): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: pytest.fail("非法 auth 不应初始化 Google SDK 客户端") + + with pytest.raises(ValueError, match="Credentials"): + provider.resources.register_files( + auth={"api_key": "not-allowed"}, + uris=["gs://bucket/file"], + ) + + +def test_google_register_files_facade_still_rejects_credential_request_overrides(): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: pytest.fail("敏感请求覆盖不应初始化 Google SDK 客户端") + + with pytest.raises(ValueError, match="凭证"): + provider.resources.register_files( + auth=AnonymousCredentials(), + uris=["gs://bucket/file"], + extra_headers={"Authorization": "Bearer secret"}, + ) + + +def test_anthropic_beta_resource_facade_forwards_current_sdk_tree(): + provider = object.__new__(AnthropicProvider) + resources = { + name: object() + for name in ( + "agents", "deployments", "deployment_runs", "dreams", "environments", + "files", "memory_stores", "models", "sessions", "skills", "tunnels", + "user_profiles", "vaults", "webhooks", + ) + } + provider._get_client = lambda: SimpleNamespace(beta=SimpleNamespace(**resources)) + provider._get_aclient = lambda: SimpleNamespace(beta=SimpleNamespace(**resources)) + + for name, value in resources.items(): + assert getattr(provider.resources, f"beta_{name}")._value is value + assert getattr(provider.async_resources, f"beta_{name}")._value is value + + +def test_anthropic_direct_resource_calls_reject_sensitive_http_overrides(): + provider = object.__new__(AnthropicProvider) + + class Files: + def list(self, **_kwargs): + raise AssertionError("SDK should not be called") + + class Batches: + def list(self, **_kwargs): + raise AssertionError("SDK should not be called") + + provider._get_client = lambda: SimpleNamespace( + beta=SimpleNamespace(files=Files()), + messages=SimpleNamespace(batches=Batches()), + ) + + with pytest.raises(ValueError, match="请求头/query 不允许包含凭证"): + provider.list_files(extra_headers={"X-API-Key": "secret"}) + with pytest.raises(ValueError, match="请求头/query 不允许包含凭证"): + provider.list_batches(extra_query={"access_token": "secret"}) + + +def test_google_resource_model_methods_forward_to_sdk(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + calls = {} + + class Models: + def delete(self, **kwargs): + calls["delete"] = kwargs + return "deleted" + + def update(self, **kwargs): + calls["update"] = kwargs + return "updated" + + provider._get_client = lambda: SimpleNamespace(models=Models()) + assert provider.resources.delete_model("models/demo") == "deleted" + assert provider.resources.update_model("models/demo", config={"display_name": "demo"}) == "updated" + assert calls["delete"] == {"model": "models/demo"} + assert calls["update"] == {"model": "models/demo", "config": {"display_name": "demo"}} + + +def test_google_operation_and_token_count_facades_match_current_sdk_signature(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + calls = {} + + class Operations: + def get(self, operation, **kwargs): + calls["operation"] = (operation, kwargs) + return "operation" + + class Models: + def count_tokens(self, **kwargs): + calls["count"] = kwargs + return SimpleNamespace(total_tokens=6) + + provider._get_client = lambda: SimpleNamespace(operations=Operations(), models=Models()) + assert provider.resources.get_operation("operations/1", config={}) == "operation" + assert provider.resources.count_tokens(CompletionRequest(prompt="hi")) == 6 + assert calls["operation"] == ("operations/1", {"config": {}}) + assert calls["count"]["model"] == "gemini" + + +def test_google_async_operation_uses_sdk_config_keyword(): + provider = object.__new__(GoogleProvider) + calls = {} + + class Operations: + def get(self, operation, **kwargs): + calls["operation"] = (operation, kwargs) + return "operation" + + provider._get_client = lambda: SimpleNamespace( + aio=SimpleNamespace(operations=Operations()) + ) + + assert asyncio.run(provider.async_get_operation("operations/2", timeout=2)) == "operation" + operation, kwargs = calls["operation"] + assert operation == "operations/2" + assert kwargs["config"].http_options.timeout == 2000 + assert set(kwargs) == {"config"} + + +def test_google_chat_creation_wraps_generation_options_in_config(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = {} + + class Chats: + def create(self, **kwargs): + calls["sync"] = kwargs + return "chat" + + provider._get_client = lambda: SimpleNamespace(chats=Chats()) + + assert provider.create_chat( + history=[], temperature=0.2, max_output_tokens=64, extra_headers={"X-Trace": "1"} + ) == "chat" + assert calls["sync"]["model"] == "gemini-default" + assert calls["sync"]["history"] == [] + assert calls["sync"]["config"].temperature == 0.2 + assert calls["sync"]["config"].max_output_tokens == 64 + assert calls["sync"]["config"].http_options.headers == {"X-Trace": "1"} + assert set(calls["sync"]) == {"model", "history", "config"} + + +def test_google_async_chat_creation_wraps_generation_options_in_config(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = {} + + class Chats: + def create(self, **kwargs): + calls["async"] = kwargs + return ("chat", kwargs) + + provider._get_client = lambda: SimpleNamespace( + aio=SimpleNamespace(chats=Chats()) + ) + + result = asyncio.run( + provider.async_create_chat(history=[], top_p=0.8, response_mime_type="application/json") + ) + assert result[0] == "chat" + assert calls["async"]["config"].top_p == 0.8 + assert calls["async"]["config"].response_mime_type == "application/json" + assert set(calls["async"]) == {"model", "history", "config"} + + +def test_openai_compatible_capabilities_do_not_claim_official_resources(): + assert "files" not in OpenAICompatibleProvider.capabilities + assert "batches" not in OpenAICompatibleProvider.capabilities + assert "vector_stores" not in OpenAICompatibleProvider.capabilities + assert "files" in ModelProviderFactory.capabilities("openai") + assert "files" not in ModelProviderFactory.capabilities("qwen") + + +def test_openai_compatible_resources_only_expose_native_client(): + compatible = object.__new__(OpenAICompatibleProvider) + compatible._get_client = lambda: object() + compatible._get_aclient = lambda: object() + assert hasattr(compatible.resources, "native") + assert not hasattr(compatible.resources, "upload_file") + + official = object.__new__(OpenAIProvider) + official._get_client = lambda: object() + official._get_aclient = lambda: object() + assert hasattr(official.resources, "upload_file") + + +def test_openai_compatible_direct_official_resources_require_declared_capability(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "qwen" + provider._options = {} + + with pytest.raises(NotImplementedError, match="files"): + provider.list_files() + with pytest.raises(NotImplementedError, match="vector_stores"): + provider.create_vector_store(name="demo") + with pytest.raises(NotImplementedError, match="fine_tuning"): + provider.list_fine_tuning_jobs() + with pytest.raises(NotImplementedError, match="audio"): + provider.text_to_speech("hello", "tts-1", "alloy") + + +def test_google_provider_missing_experimental_resources_is_explicit(): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: SimpleNamespace() + + with pytest.raises(NotImplementedError, match="(?i)agents"): + provider.create_agent(name="demo") + with pytest.raises(NotImplementedError, match="(?i)auth token"): + provider.create_auth_token() + with pytest.raises(AttributeError, match="(?i)agents"): + _missing_agents = provider.resources.agents + + +def test_factory_capabilities_follow_role_specific_provider_contracts(): + assert "embedding" not in ModelProviderFactory.capabilities("deepseek", "llm") + assert ModelProviderFactory.capabilities("qwen", "embedding") == frozenset({"embedding"}) + assert ModelProviderFactory.capabilities("siliconflow", "rerank") == frozenset({"rerank"}) + + +def test_factory_protocol_status_infers_non_llm_role_for_diagnostics(): + assert ModelProviderFactory.protocol_status("local-hash", "responses") == { + "adapter_supported": False, + "server_verified": False, + } + assert ModelProviderFactory.protocol_status("siliconflow", "responses", role="rerank") == { + "adapter_supported": False, + "server_verified": False, + } + + +def test_protocol_status_separates_adapter_support_from_server_verification(): + assert ModelProviderFactory.protocol_status("qwen", "responses") == { + "adapter_supported": True, + "server_verified": False, + } + + +@pytest.mark.parametrize("configured", [1, {}, ["responses", 1], ["responses", ""]]) +def test_openai_verified_protocol_configuration_rejects_invalid_types(configured): + provider = object.__new__(OpenAICompatibleProvider) + with pytest.raises(ValueError, match="server_verified_protocols"): + provider._normalize_verified_protocols(configured) + + +def test_openai_responses_token_count_forwards_full_request_extensions(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + calls = [] + + class TokenCounter: + def count(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(input_tokens=11) + + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace(input_tokens=TokenCounter()) + ) + request = CompletionRequest( + prompt="hi", + response_format={"type": "json_object"}, + parallel_tool_calls=True, + reasoning={"effort": "low"}, + truncation="auto", + extra_body={"vendor_flag": True}, + extra_headers={"X-Trace": "trace-1"}, + extra_query={"tenant": "demo"}, + timeout=3, + ) + + assert provider.count_input_tokens(request) == 11 + assert calls == [{ + "model": "demo", + "input": "hi", + "instructions": "You are a helpful assistant.", + "parallel_tool_calls": True, + "reasoning": {"effort": "low"}, + "text": {"format": {"type": "json_object"}}, + "truncation": "auto", + "extra_headers": {"X-Trace": "trace-1"}, + "extra_query": {"tenant": "demo"}, + "extra_body": {"vendor_flag": True}, + "timeout": 3, + }] + + +def test_openai_responses_token_count_converts_chat_tool_choice(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + + calls = [] + + class TokenCounter: + def count(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace(input_tokens=3) + + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace(input_tokens=TokenCounter()) + ) + + request = CompletionRequest( + prompt="hi", + tools=[{"type": "function", "function": {"name": "lookup"}}], + tool_choice={"type": "function", "function": {"name": "lookup"}}, + ) + + assert provider.count_input_tokens(request) == 3 + assert calls[0]["tool_choice"] == {"type": "function", "name": "lookup"} + + +def test_openai_responses_token_count_rejects_generation_only_fields(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._options = {} + with pytest.raises(ValueError, match="max_tokens"): + provider.count_input_tokens(CompletionRequest(prompt="hi", max_tokens=16)) + + +def test_openai_compatible_responses_resources_require_verified_server_capability(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "qwen" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + + with pytest.raises(ValueError, match="未验证 Responses"): + provider.retrieve_response("resp-1") + + +def test_openai_compatible_verified_responses_resource_is_forwarded(): + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "qwen" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset({"responses"}) + provider._get_client = lambda: SimpleNamespace( + responses=SimpleNamespace(retrieve=lambda response_id, **kwargs: (response_id, kwargs)) + ) + + assert provider.retrieve_response("resp-1", include=["output"]) == ( + "resp-1", {"include": ["output"]} + ) + + +def test_anthropic_extra_body_and_request_extensions_keep_sdk_names(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + params = provider._build_message_params( + prompt="hi", + extra_body={"vendor_flag": True}, + extra_headers={"X-Trace": "trace-1"}, + extra_query={"tenant": "demo"}, + ) + + assert params["extra_body"] == {"vendor_flag": True} + assert params["extra_headers"] == {"X-Trace": "trace-1"} + assert params["extra_query"] == {"tenant": "demo"} + assert "vendor_flag" not in params + + +def test_anthropic_request_builder_rejects_sensitive_http_overrides(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="请求头/query"): + provider._build_message_params( + prompt="hi", + extra_headers={"Authorization": "Bearer secret"}, + ) + with pytest.raises(ValueError, match="请求头/query"): + provider._build_message_params( + prompt="hi", + extra_query={"access_token": "secret"}, + ) + + +def test_openai_compatible_server_capability_option_is_not_sent_as_body(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._provider = "qwen" + provider._server_verified_protocols = frozenset({"responses"}) + provider._options = {"server_verified_protocols": ["responses"], "top_k": 20} + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["extra_body"] == {"top_k": 20} + assert "server_verified_protocols" not in request + + +def test_openai_compatible_responses_model_stop_and_seed_use_extra_body(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._protocol = "responses" + provider._provider = "qwen" + provider._server_verified_protocols = frozenset({"responses"}) + provider._options = { + "server_verified_protocols": ["responses"], + "stop": ["END"], + "seed": 11, + } + + request = provider._build_responses_request(prompt="hi", stream=False) + + assert request["extra_body"] == {"stop": ["END"], "seed": 11} + assert "stop" not in request + assert "seed" not in request + + +def test_anthropic_client_options_are_not_forwarded_to_message_request(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = { + "base_url": "https://proxy.invalid", + "timeout": 3, + "max_tokens": 8192, + "top_p": 0.8, + } + params = provider._build_message_params(prompt="hi") + assert params["max_tokens"] == 8192 + assert params["extra_body"]["top_p"] == 0.8 + assert "top_p" not in params + assert "base_url" not in params + assert "timeout" not in params + assert provider._build_message_params(prompt="hi", timeout=4)["timeout"] == 4 + + +def test_anthropic_sampling_fields_follow_installed_sdk_request_shape(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + params = provider._build_message_params( + prompt="hi", + temperature=0.2, + top_p=0.8, + top_k=32, + ) + + assert params["extra_body"] == { + "temperature": 0.2, + "top_p": 0.8, + "top_k": 32, + } + assert not {"temperature", "top_p", "top_k"}.intersection(params) + + +def test_anthropic_extra_body_sampling_conflicts_are_explicit(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="temperature.*extra_body"): + provider._build_message_params( + prompt="hi", + temperature=0.2, + extra_body={"temperature": 0.4}, + ) + + +def test_anthropic_beta_tool_runner_rejects_unsupported_compaction_control(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="compaction_control"): + provider.beta_tool_runner([], prompt="hi", compaction_control={"enabled": True}) + + +def test_anthropic_output_format_follows_create_and_stream_sdk_shapes(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="create 不支持 output_format"): + provider._build_message_params(prompt="hi", output_format=dict, stream=False) + + params = provider._build_message_params(prompt="hi", output_format=dict, stream=True) + assert params["output_format"] is dict + + +def test_anthropic_beta_create_rejects_output_format(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="create 不支持 output_format"): + provider.beta_create(CompletionRequest(prompt="hi", output_format={"type": "json_schema"})) + + +def test_anthropic_async_beta_create_rejects_output_format_before_sdk_call(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + calls = [] + + class Messages: + async def create(self, **kwargs): + calls.append(kwargs) + return "beta" + + provider._get_aclient = lambda: SimpleNamespace( + beta=SimpleNamespace(messages=Messages()) + ) + + with pytest.raises(ValueError, match="create 不支持 output_format"): + asyncio.run( + provider.async_beta_create( + CompletionRequest(prompt="hi", output_format={"type": "json_schema"}) + ) + ) + assert calls == [] + + +def test_anthropic_beta_token_count_rejects_output_format(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + + with pytest.raises(ValueError, match="token count 不支持 output_format"): + provider.beta_count_tokens( + CompletionRequest(prompt="hi", output_format={"type": "json_schema"}) + ) + + +def test_anthropic_parse_ignores_configured_output_format(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {"output_format": {"type": "json_schema"}} + calls = {} + + class Messages: + def parse(self, **kwargs): + calls.update(kwargs) + return "parsed" + + provider._get_client = lambda: SimpleNamespace(messages=Messages()) + assert provider.parse(dict, CompletionRequest(prompt="hi")) == "parsed" + assert calls["output_format"] is dict + assert calls["output_format"] != provider._options["output_format"] + + +def test_anthropic_request_keys_match_installed_sdk_create_signatures(): + import anthropic + + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + params = provider._build_message_params(prompt="hi") + + create_keys = set(inspect.signature(anthropic.Anthropic(api_key="x").messages.create).parameters) + assert set(params).issubset(create_keys) + + beta_params = provider._build_message_params(prompt="hi", beta=True) + beta_keys = set(inspect.signature(anthropic.Anthropic(api_key="x").beta.messages.create).parameters) + assert set(beta_params).issubset(beta_keys) + + +def test_anthropic_modern_models_omit_deprecated_sampling_defaults(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-sonnet-4-6" + provider._options = {} + + params = provider._build_message_params(prompt="hi", temperature=0.7) + + assert "temperature" not in params.get("extra_body", {}) + with pytest.raises(ValueError, match="temperature"): + provider._build_message_params(prompt="hi", temperature=0.2) + with pytest.raises(ValueError, match="top_k"): + provider._build_message_params(prompt="hi", top_k=5) + + +def test_anthropic_modern_models_allow_explicit_legacy_sampling_for_proxies(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude-sonnet-4-6" + provider._options = {"allow_deprecated_sampling": True} + + params = provider._build_message_params( + prompt="hi", + temperature=0.2, + top_p=0.8, + top_k=32, + ) + + assert params["extra_body"] == { + "temperature": 0.2, + "top_p": 0.8, + "top_k": 32, + } + assert provider.supports_sampling_option("temperature") is True + assert provider.supports_sampling_option("top_p") is True + assert provider.supports_sampling_option("top_k") is True + + +def test_anthropic_default_max_tokens_scales_with_model_family(): + provider = object.__new__(AnthropicProvider) + provider._options = {} + + provider._model_name = "claude-3-opus-20240229" + legacy = AnthropicProvider._build_message_params(provider, prompt="hi") + provider._model_name = "claude-sonnet-4-6" + modern = AnthropicProvider._build_message_params(provider, prompt="hi") + + assert legacy["max_tokens"] == 4096 + assert modern["max_tokens"] > legacy["max_tokens"] + + +@pytest.mark.parametrize( + ("model_name", "deprecated", "max_tokens"), + [ + ("claude-sonnet-4-20250514", False, 8192), + ("claude-sonnet-4-5-20250929", True, 16384), + ("claude-sonnet-5", True, 16384), + ("claude-3-7-sonnet-20250219", False, 8192), + ], +) +def test_anthropic_model_version_parser_ignores_dates_and_supports_major_only( + model_name, deprecated, max_tokens +): + provider = object.__new__(AnthropicProvider) + provider._model_name = model_name + provider._options = {} + + assert provider._sampling_controls_deprecated(model_name) is deprecated + assert provider._default_max_tokens() == max_tokens + + +def test_anthropic_file_resources_prefer_stable_sdk_surface(): + provider = object.__new__(AnthropicProvider) + calls = [] + + class Files: + def upload(self, **kwargs): + calls.append(("stable", kwargs)) + return "stable" + + class BetaFiles: + def upload(self, **_kwargs): + raise AssertionError("beta files should not be used for stable file operations") + + provider._get_client = lambda: SimpleNamespace( + files=Files(), beta=SimpleNamespace(files=BetaFiles()) + ) + + assert provider.upload_file(b"data") == "stable" + assert calls == [("stable", {"file": b"data"})] + + +def test_google_cache_and_batch_resources_supply_default_model(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class Caches: + def create(self, **kwargs): + calls.append(("cache", kwargs)) + return "cache" + + class Batches: + def create(self, **kwargs): + calls.append(("batch", kwargs)) + return "batch" + + provider._get_client = lambda: SimpleNamespace(caches=Caches(), batches=Batches()) + assert provider.resources.create_cache(config={}) == "cache" + assert provider.resources.create_batch([]) == "batch" + assert calls == [ + ("cache", {"model": "gemini-default", "config": {}}), + ("batch", {"model": "gemini-default", "src": []}), + ] + + +def test_google_async_stream_and_chat_contracts_do_not_double_await(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + provider._options = {} + + class AsyncModels: + async def generate_content_stream(self, **_kwargs): + async def chunks(): + yield SimpleNamespace( + candidates=[SimpleNamespace(finish_reason="STOP")] + ) + + return chunks() + + class AsyncChats: + def create(self, **kwargs): + return ("chat", kwargs) + + client = SimpleNamespace( + aio=SimpleNamespace(models=AsyncModels(), chats=AsyncChats()) + ) + provider._get_client = lambda: client + events = asyncio.run(_collect_google_async_events(provider)) + assert [event.type for event in events] == ["finish"] + assert asyncio.run(provider.async_create_chat(history=[])) == ( + "chat", {"model": "gemini", "history": []} + ) + + +def test_google_async_cache_and_batch_resources_supply_default_model(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class AsyncCaches: + async def create(self, **kwargs): + calls.append(("cache", kwargs)) + return "cache" + + class AsyncBatches: + async def create(self, **kwargs): + calls.append(("batch", kwargs)) + return "batch" + + provider._get_client = lambda: SimpleNamespace( + aio=SimpleNamespace(caches=AsyncCaches(), batches=AsyncBatches()) + ) + assert asyncio.run(provider.async_resources.create_cache(config={})) == "cache" + assert asyncio.run(provider.async_resources.create_batch([])) == "batch" + assert calls == [ + ("cache", {"model": "gemini-default", "config": {}}), + ("batch", {"model": "gemini-default", "src": []}), + ] + + +def test_google_tuning_and_file_search_resources_forward_current_sdk_methods(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class Tunings: + def tune(self, **kwargs): + calls.append(("tune", kwargs)) + return "tuning" + + def validate_reward(self, **kwargs): + calls.append(("reward", kwargs)) + return "reward" + + class FileSearchStores: + def import_file(self, **kwargs): + calls.append(("import", kwargs)) + return "imported" + + provider._get_client = lambda: SimpleNamespace(tunings=Tunings(), file_search_stores=FileSearchStores()) + assert provider.resources.tune("gemini-base", {"examples": []}, config={}) == "tuning" + assert provider.resources.validate_tuning_reward("jobs/1", {"text": "ok"}, {"input": "x"}) == "reward" + assert provider.resources.import_file_to_file_search_store("stores/1", "files/1") == "imported" + assert calls == [ + ( + "tune", + { + "base_model": "gemini-base", + "training_dataset": {"examples": []}, + "config": {}, + }, + ), + ("reward", {"parent": "jobs/1", "sample_response": {"text": "ok"}, "example": {"input": "x"}}), + ("import", {"file_search_store_name": "stores/1", "file_name": "files/1"}), + ] + + +def test_google_tuning_resource_missing_is_explicit(): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: SimpleNamespace() + + with pytest.raises(NotImplementedError, match="调优资源"): + provider.resources.list_tunings() + + +def test_resource_facade_rejects_sensitive_request_extensions_sync_and_async(): + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider.list_files = lambda **_: "should-not-run" + provider.async_list_files = lambda **_: "should-not-run" + + with pytest.raises(ValueError, match="凭证"): + provider.resources.list_files(extra_headers={"X-API-Key": "secret"}) + + with pytest.raises(ValueError, match="凭证"): + asyncio.run(provider.async_resources.list_files(extra_body={"token": "secret"})) + + +def test_direct_provider_resource_methods_reject_sensitive_extensions(): + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_client = lambda: SimpleNamespace( + files=SimpleNamespace(list=lambda **kwargs: kwargs) + ) + + with pytest.raises(ValueError, match="凭证"): + provider.list_files(extra_headers={"X-API-Key": "secret"}) + + with pytest.raises(ValueError, match="凭证"): + provider.list_files(extra_body={"nested": {"access_token": "secret"}}) + + +def test_dynamic_native_resource_proxy_validates_nested_calls_and_keeps_results(): + calls = [] + + class Files: + def list(self, **kwargs): + calls.append(kwargs) + return "page" + + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + native = SimpleNamespace(experimental=SimpleNamespace(files=Files())) + provider._get_client = lambda: native + + assert provider.resources.experimental.files.list(limit=2) == "page" + assert calls == [{"limit": 2}] + with pytest.raises(ValueError, match="凭证"): + provider.resources.experimental.files.list( + extra_query={"access_token": "secret"} + ) + + +def test_dynamic_native_resource_proxy_rejects_sensitive_positional_arguments(): + calls = [] + + class Responses: + def connect(self, *args, **kwargs): + calls.append((args, kwargs)) + return "connection" + + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + + assert provider.resources.responses.connect({"tenant": "demo"}, {"X-Trace": "1"}) == "connection" + assert calls == [ + (({"tenant": "demo"}, {"X-Trace": "1"}), {}), + ] + for args in ( + ({"Authorization": "Bearer secret"},), + ({"nested": {"access_token": "secret"}},), + ("https://api.example.test/v1?api_key=secret",), + ): + with pytest.raises(ValueError, match="位置参数.*凭证"): + provider.resources.responses.connect(*args) + + +def test_direct_openai_resource_call_rejects_sensitive_positional_arguments(): + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + + def connect(*args, **kwargs): + return args, kwargs + + with pytest.raises(ValueError, match="位置参数.*凭证"): + provider._call_sdk_resource( + connect, + args=({"extra_headers": {"Authorization": "Bearer secret"}},), + operation="OpenAI Responses 连接", + ) + assert provider._call_sdk_resource( + connect, + args=("response-id",), + operation="OpenAI Responses 连接", + ) == (("response-id",), {}) + + +def test_provider_resource_guard_rejects_sensitive_positional_arguments(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini" + + class Models: + def update(self, **_kwargs): + raise AssertionError("SDK should not be called") + + provider._get_client = lambda: SimpleNamespace(models=Models()) + with pytest.raises(ValueError, match="位置参数.*凭证"): + provider.update_model("models/demo", {"Authorization": "Bearer secret"}) + + +def test_provider_resource_guard_normalizes_duplicate_sdk_arguments(): + provider = object.__new__(GoogleProvider) + + class Interactions: + def get(self, *, id): + return id + + provider._get_client = lambda: SimpleNamespace(interactions=Interactions()) + with pytest.raises(ValueError, match="资源 SDK 参数重复"): + provider.get_interaction("interaction-1", id="interaction-2") + + +def test_resource_argument_validator_keeps_nested_business_values_isolated(): + original = {"query": {"text": "hello"}} + validated = validate_secret_free_resource_args((original,), "Provider") + assert validated == (original,) + assert validated[0] is not original + original["query"]["text"] = "changed" + assert validated[0]["query"]["text"] == "hello" + + +def test_resource_kwargs_reject_duplicate_extension_spellings(): + with pytest.raises(ValueError, match="重复的扩展字段"): + validate_secret_free_resource_kwargs( + {"extra_headers": {"X-Trace": "1"}, "EXTRA_HEADERS": {"X-Trace": "2"}}, + "Provider", + ) + + +def test_dynamic_native_resource_proxy_preserves_native_client_identity(): + provider = object.__new__(OpenAIProvider) + native = SimpleNamespace(experimental=SimpleNamespace(value="ok")) + provider._get_client = lambda: native + + assert provider.resources.native is native + assert provider.resources.experimental.value == "ok" + + +def test_dynamic_native_vector_store_search_allows_business_query_but_scans_nested_credentials(): + calls = [] + + class VectorStores: + def search(self, **kwargs): + calls.append(kwargs) + return "page" + + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_client = lambda: SimpleNamespace(vector_stores=VectorStores()) + + assert provider.resources.vector_stores.search( + vector_store_id="vs_1", query="refund" + ) == "page" + assert calls == [{"vector_store_id": "vs_1", "query": "refund"}] + + with pytest.raises(ValueError, match="凭证"): + provider.resources.vector_stores.search( + vector_store_id="vs_1", query={"Authorization": "Bearer secret"} + ) + + +def test_dynamic_native_resource_proxy_wraps_with_options_results_and_guards_credentials(): + calls = [] + + class Responses: + def create(self, **kwargs): + calls.append(("create", kwargs)) + return "response" + + class Client: + def __init__(self): + self.responses = Responses() + + def with_options(self, **kwargs): + calls.append(("with_options", kwargs)) + return Client() + + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_client = Client + + configured = provider.resources.with_options(timeout=2) + assert configured.responses.create(model="demo") == "response" + assert calls == [ + ("with_options", {"timeout": 2}), + ("create", {"model": "demo"}), + ] + + for key in ("api_key", "admin_api_key", "webhook_secret", "default_headers", "base_url"): + with pytest.raises(ValueError, match="凭证或请求头/query"): + provider.resources.with_options(**{key: "secret"}) + + with pytest.raises(ValueError, match="凭证"): + configured.responses.create(extra_headers={"Authorization": "Bearer secret"}) + + +def test_dynamic_async_native_resource_proxy_guards_nested_raw_and_streaming_resources(): + calls = [] + + class RawResponses: + async def create(self, **kwargs): + calls.append(("raw", kwargs)) + return "raw-response" + + class StreamingResponses: + async def create(self, **kwargs): + calls.append(("streaming", kwargs)) + return "streaming-response" + + class Responses: + def __init__(self): + self._raw = RawResponses() + self._streaming = StreamingResponses() + + @property + def with_raw_response(self): + return self._raw + + @property + def with_streaming_response(self): + return self._streaming + + class Client: + def __init__(self): + self.responses = Responses() + + def with_options(self, **kwargs): + calls.append(("with_options", kwargs)) + return self + + def copy(self, **kwargs): + calls.append(("copy", kwargs)) + return self + + client = Client() + provider = object.__new__(OpenAIProvider) + provider._provider = "openai" + provider._get_aclient = lambda: client + + configured = provider.async_resources.with_options(timeout=2) + copied = configured.copy(max_retries=1) + assert asyncio.run(copied.responses.with_raw_response.create(model="demo")) == "raw-response" + assert asyncio.run( + copied.responses.with_streaming_response.create(model="demo") + ) == "streaming-response" + assert calls[:2] == [("with_options", {"timeout": 2}), ("copy", {"max_retries": 1})] + + with pytest.raises(ValueError, match="凭证"): + copied.responses.with_raw_response.create( + extra_headers={"Authorization": "Bearer secret"} + ) + with pytest.raises(ValueError, match="凭证"): + copied.responses.with_streaming_response.create( + extra_body={"nested": {"access_token": "secret"}} + ) + + for helper in ("with_options", "copy"): + with pytest.raises(ValueError, match="凭证或请求头/query"): + getattr(provider.async_resources, helper)(default_query={"token": "secret"}) + + +def test_google_tuning_facades_forward_additional_sdk_kwargs(): + calls = [] + + class Provider: + def tune(self, base_model, training_dataset, config=None, **kwargs): + calls.append(("sync", base_model, training_dataset, config, kwargs)) + return "tuning" + + async def async_tune(self, base_model, training_dataset, config=None, **kwargs): + calls.append(("async", base_model, training_dataset, config, kwargs)) + return "async-tuning" + + provider = Provider() + assert GoogleResources(provider).tune( + "gemini-base", {"examples": []}, labels={"suite": "test"} + ) == "tuning" + assert asyncio.run( + AsyncGoogleResources(provider).tune( + "gemini-base", {"examples": []}, labels={"suite": "test"} + ) + ) == "async-tuning" + assert calls == [ + ("sync", "gemini-base", {"examples": []}, None, {"labels": {"suite": "test"}}), + ("async", "gemini-base", {"examples": []}, None, {"labels": {"suite": "test"}}), + ] + + +def test_google_next_generation_resources_forward_sync_and_async_calls(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class Resource: + def __getattr__(self, name): + def method(*args, **kwargs): + calls.append((name, args, kwargs)) + return f"{name}-result" + + return method + + provider._get_client = lambda: SimpleNamespace( + interactions=Resource(), + agents=Resource(), + webhooks=Resource(), + environments=Resource(), + triggers=Resource(), + aio=SimpleNamespace( + interactions=Resource(), + agents=Resource(), + webhooks=Resource(), + environments=Resource(), + triggers=Resource(), + live=SimpleNamespace(connect=lambda **kwargs: ("live", kwargs)), + ), + ) + + assert provider.resources.create_interaction(input="hello") == "create-result" + assert provider.resources.get_webhook("wh-1") == "get-result" + assert provider.resources.run_trigger("tr-1", payload={"x": 1}) == "run-result" + assert provider.resources.connect_live(config={}) == ("live", {"model": "gemini-default", "config": {}}) + + async def collect(): + return ( + await provider.async_resources.create_interaction(input="hello"), + await provider.async_resources.get_agent("agent-1"), + await provider.async_resources.delete_environment("env-1"), + await provider.async_resources.list_trigger_executions("tr-1"), + ) + + assert asyncio.run(collect()) == ( + "create-result", + "get-result", + "delete_environment-result", + "list_executions-result", + ) + assert ("create", (), {"input": "hello"}) in calls + assert ("get", ("wh-1",), {}) in calls + + +def test_provider_resource_facades_expose_beta_trees_without_copying_sdk_methods(): + anthropic_provider = object.__new__(AnthropicProvider) + anthropic_client = SimpleNamespace(beta=SimpleNamespace(messages="beta-messages")) + anthropic_provider._get_client = lambda: anthropic_client + anthropic_provider._get_aclient = lambda: SimpleNamespace(beta="async-beta") + assert anthropic_provider.resources.beta.messages == "beta-messages" + assert anthropic_provider.async_resources.async_beta == "async-beta" + + compatible = object.__new__(OpenAICompatibleProvider) + compatible._get_client = lambda: SimpleNamespace(beta="should-not-be-visible") + compatible._get_aclient = lambda: SimpleNamespace(beta="should-not-be-visible") + assert not hasattr(compatible.resources, "beta") + assert not hasattr(compatible.resources, "async_beta") + + ark_provider = object.__new__(VolcengineProvider) + ark_calls = [] + + class Completions: + def parse(self, **kwargs): + ark_calls.append(("parse", kwargs)) + return "parsed" + + def stream(self, **kwargs): + ark_calls.append(("stream", kwargs)) + return "stream" + + ark_provider._model_name = "ark-model" + ark_provider._get_client = lambda: SimpleNamespace( + beta=SimpleNamespace(chat=SimpleNamespace(completions=Completions())), + bot_chat=SimpleNamespace(completions=SimpleNamespace(create=lambda **kwargs: ("bot", kwargs))), + ) + assert ark_provider.resources.beta_chat_parse(messages=[], response_format={}) == "parsed" + assert ark_provider.resources.beta_chat_stream(messages=[]) == "stream" + assert ark_provider.resources.bot_chat(messages=[]) == ("bot", {"messages": [], "model": "ark-model"}) + assert ark_calls == [ + ("parse", {"messages": [], "response_format": {}, "model": "ark-model"}), + ("stream", {"messages": [], "model": "ark-model"}), + ] + + +@pytest.mark.parametrize( + "method, args, kwargs", + [ + ("upload_file", (b"data", "user_data"), {"extra_headers": {"X-API-Key": "secret"}}), + ("list_files", (), {"extra_query": {"access_token": "secret"}}), + ("retrieve_response", ("resp-1",), {"extra_body": {"token": "secret"}}), + ("create_content_generation_task", (), {"content": [], "extra_body": {"password": "secret"}}), + ("beta_chat_parse", (), {"messages": [], "extra_query": {"api_key": "secret"}}), + ("bot_chat", (), {"messages": [], "extra_headers": {"Authorization": "Bearer secret"}}), + ("classify", ("query", ["label"]), {"extra_headers": {"X-Api-Key": "secret"}}), + ], +) +def test_ark_resource_methods_reject_sensitive_extensions(method, args, kwargs): + provider = object.__new__(VolcengineProvider) + provider._protocol = "responses" + provider._model_name = "ark-model" + + class Resource: + def __getattr__(self, _name): + return lambda **_kwargs: "should-not-run" + + provider._get_client = lambda: SimpleNamespace( + files=Resource(), + responses=Resource(), + content_generation=SimpleNamespace(tasks=Resource()), + beta=SimpleNamespace(chat=SimpleNamespace(completions=Resource())), + bot_chat=SimpleNamespace(completions=Resource()), + classification=Resource(), + ) + + with pytest.raises(ValueError, match="凭证"): + getattr(provider, method)(*args, **kwargs) + + +def test_ark_async_beta_chat_stream_returns_async_context_manager_directly(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + calls = [] + + class StreamManager: + def __init__(self): + self.entered = False + self.exited = False + + async def __aenter__(self): + self.entered = True + return "stream" + + async def __aexit__(self, exc_type, exc_value, traceback): + self.exited = True + + stream_manager = StreamManager() + + class Completions: + def stream(self, **kwargs): + calls.append(kwargs) + return stream_manager + + provider._get_aclient = lambda: SimpleNamespace( + beta=SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + ) + + result = provider.async_resources.beta_chat_stream(messages=[]) + + assert result is stream_manager + assert not inspect.isawaitable(result) + assert calls == [{"messages": [], "model": "ark-model"}] + + async def consume_stream(): + async with result as stream: + return stream + + assert asyncio.run(consume_stream()) == "stream" + assert stream_manager.entered is True + assert stream_manager.exited is True + + +async def _collect_google_async_events(provider): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hi"))] + + +def test_ark_async_input_items_use_top_level_resource(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = {} + + class InputItems: + def list(self, response_id, **kwargs): + calls["args"] = (response_id, kwargs) + return "items" + + provider._get_aclient = lambda: SimpleNamespace(input_items=InputItems()) + assert asyncio.run(provider.async_list_response_input_items("resp-1", limit=2)) == "items" + assert calls["args"] == ("resp-1", {"limit": 2}) + + +@pytest.mark.parametrize( + "method_name, args, kwargs", + [ + ("retrieve_response", ("resp-1",), {"include": ["output"]}), + ("delete_response", ("resp-1",), {"include": ["output"]}), + ("list_response_input_items", ("resp-1",), {"bogus": True}), + ("list_input_items", ("resp-1",), {"bogus": True}), + ], +) +def test_ark_response_resources_reject_unknown_kwargs_before_sdk_call( + method_name, args, kwargs +): + provider = object.__new__(VolcengineProvider) + provider._protocol = "responses" + provider._server_verified_protocols = frozenset({"responses"}) + provider._get_client = lambda: pytest.fail("不支持的 Ark Responses 参数不应调用 SDK") + + with pytest.raises(ValueError, match="不支持请求参数"): + getattr(provider, method_name)(*args, **kwargs) + + +@pytest.mark.parametrize( + "method_name, args, kwargs", + [ + ("async_retrieve_response", ("resp-1",), {"include": ["output"]}), + ("async_delete_response", ("resp-1",), {"include": ["output"]}), + ("async_list_response_input_items", ("resp-1",), {"bogus": True}), + ("async_list_input_items", ("resp-1",), {"bogus": True}), + ], +) +def test_ark_async_response_resources_reject_unknown_kwargs_before_sdk_call( + method_name, args, kwargs +): + provider = object.__new__(VolcengineProvider) + provider._protocol = "responses" + provider._server_verified_protocols = frozenset({"responses"}) + provider._get_aclient = lambda: pytest.fail("不支持的 Ark Responses 参数不应调用 SDK") + + async def invoke(): + await getattr(provider, method_name)(*args, **kwargs) + + with pytest.raises(ValueError, match="不支持请求参数"): + asyncio.run(invoke()) + + +def test_ark_async_chat_stream_rejects_incomplete_tool_call(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "chat_completions" + provider._options = {} + + class Completions: + async def create(self, **_request): + class Stream: + def __aiter__(self): + self._events = iter( + [ + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace( + content=None, + tool_calls=[ + SimpleNamespace( + id="call-1", + index=0, + type="function", + function=SimpleNamespace( + name="lookup", arguments="" + ), + ) + ], + ), + finish_reason=None, + ) + ] + ), + SimpleNamespace( + choices=[ + SimpleNamespace( + delta=SimpleNamespace(content=None, tool_calls=None), + finish_reason="tool_calls", + ) + ] + ), + ] + ) + return self + + async def __anext__(self): + try: + return next(self._events) + except StopIteration as exc: + raise StopAsyncIteration from exc + + return Stream() + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def collect(): + return [event async for event in provider.astream_events(CompletionRequest(prompt="hi"))] + + with pytest.raises(RuntimeError, match="完整 arguments"): + asyncio.run(collect()) + + +def test_ark_responses_request_and_stream(monkeypatch): + settings = SimpleNamespace( + volc_access_key=None, + volc_secret_key=None, + ark_api_key="ark-key", + volc_base_url="https://ark.example/v3", + ) + monkeypatch.setitem(VolcengineProvider.__init__.__globals__, "get_settings", lambda: settings) + provider = VolcengineProvider( + "doubao", + protocol="responses", + options={"server_verified_protocols": ["responses"]}, + ) + assert provider._build_responses_request(CompletionRequest(prompt="hi", stream=True))["input"] == "hi" + + class Responses: + def create(self, **_kwargs): + return iter( + [ + SimpleNamespace(type="response.output_text.delta", delta="ok"), + SimpleNamespace(type="response.completed", response=SimpleNamespace(status="completed")), + ] + ) + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + assert list(provider.invoke("hi")) == ["ok"] + + +def test_ark_responses_direct_invocation_requires_server_verification(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "responses" + provider._options = {} + provider._get_client = lambda: pytest.fail("未验证的 Ark Responses 不应初始化客户端") + + with pytest.raises(ValueError, match="server_verified_protocols"): + list(provider.invoke("hello", stream=False)) + + +def test_factory_ark_responses_requires_explicit_server_verification(monkeypatch): + settings = SimpleNamespace( + volc_access_key=None, + volc_secret_key=None, + ark_api_key="ark-key", + volc_base_url="https://ark.example/v3", + ) + monkeypatch.setitem(VolcengineProvider.__init__.__globals__, "get_settings", lambda: settings) + monkeypatch.setattr( + ModelProviderFactory, + "_get_provider_class", + staticmethod(lambda _provider_name: VolcengineProvider), + ) + + with pytest.raises(ValueError, match="server_verified_protocols"): + ModelProviderFactory._create_provider( + "volcengine", + "doubao-model", + "llm", + LargeLanguageModel, + protocol="responses", + options={}, + ) + + provider = ModelProviderFactory._create_provider( + "volcengine", + "doubao-model", + "llm", + LargeLanguageModel, + protocol="responses", + options={"server_verified_protocols": ["responses"]}, + ) + assert isinstance(provider, VolcengineProvider) + + +def test_ark_ainvoke_preserves_explicit_none_system_prompt(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "doubao-model" + provider._protocol = "chat_completions" + provider._options = {} + calls = [] + + class Completions: + async def create(self, **kwargs): + calls.append(kwargs) + return SimpleNamespace( + choices=[ + SimpleNamespace( + message=SimpleNamespace(content="ok", tool_calls=[]), + finish_reason="stop", + ) + ] + ) + + provider._get_aclient = lambda: SimpleNamespace( + chat=SimpleNamespace(completions=Completions()) + ) + + async def consume(): + return [item async for item in provider.ainvoke( + "hello", system_prompt=None, stream=False + )] + + assert asyncio.run(consume()) == ["ok"] + assert calls[0]["messages"] == [{"role": "user", "content": "hello"}] + + +def test_ark_responses_normalizes_model_options_to_native_shape(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "max_tokens": 1024, + "response_format": {"type": "json_object"}, + "reasoning_effort": "high", + } + + request = provider._build_responses_request( + CompletionRequest(prompt="hi", stream=False) + ) + + assert request["max_output_tokens"] == 1024 + assert request["text"] == {"format": {"type": "json_object"}} + assert request["reasoning"] == {"effort": "high"} + assert "max_tokens" not in request + assert "response_format" not in request + assert "reasoning_effort" not in request + + +def test_ark_responses_converts_configured_tool_choice_to_native_shape(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = { + "tool_choice": {"type": "function", "function": {"name": "lookup"}}, + } + + request = provider._build_responses_request( + CompletionRequest(prompt="hi", stream=False) + ) + + assert request["tool_choice"] == {"type": "function", "name": "lookup"} + + +def test_ark_chat_model_options_move_sampling_extensions_into_extra_body(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {"top_k": 20, "seed": 7} + + request = provider._build_chat_request(prompt="hi", stream=False) + + assert request["extra_body"] == {"top_k": 20, "seed": 7} + assert "top_k" not in request + assert "seed" not in request + + +@pytest.mark.parametrize( + "tool_choice", + [ + {"type": "function", "function": "lookup"}, + {"type": "function", "function": 1}, + ], +) +def test_ark_responses_tool_choice_rejects_non_object_function(tool_choice): + with pytest.raises(ValueError, match="function tool_choice"): + VolcengineProvider._convert_responses_tool_choice(tool_choice) + + +def test_ark_response_resource_create_uses_unified_request_and_native_defaults(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + def create(self, **kwargs): + calls.append(kwargs) + return "response" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + assert provider.resources.create_response(CompletionRequest(prompt="hi", stream=False)) == "response" + assert calls == [{ + "model": "ark-model", + "input": "hi", + "stream": False, + "instructions": "You are a helpful assistant.", + }] + with pytest.raises(ValueError, match="model"): + provider.resources.create_response(input="raw") + assert provider.resources.create_response(input="raw", model="caller-model") == "response" + assert calls[-1] == {"input": "raw", "model": "caller-model"} + + +def test_ark_async_native_response_resource_requires_explicit_model_and_preserves_kwargs(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "configured-model" + provider._protocol = "responses" + provider._options = {"server_verified_protocols": ["responses"]} + calls = [] + + class Responses: + async def create(self, **kwargs): + calls.append(kwargs) + return "async-response" + + provider._get_aclient = lambda: SimpleNamespace(responses=Responses()) + + async def invoke(): + with pytest.raises(ValueError, match="model"): + await provider.async_resources.create_response(input="raw") + result = await provider.async_resources.create_response( + input="raw", model="caller-model", stream=False + ) + return result + + assert asyncio.run(invoke()) == "async-response" + assert calls == [{"input": "raw", "model": "caller-model", "stream": False}] + + +def test_google_environment_resource_names_and_async_live_contracts(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + calls = [] + + class Files: + def list(self, environment, path, **kwargs): + calls.append(("files", (environment, path), kwargs)) + return "files" + + class AsyncFiles: + async def list(self, environment, path, **kwargs): + calls.append(("async-files", (environment, path), kwargs)) + return "async-files" + + class Environments: + def create_environment(self, **kwargs): + calls.append(("create", (), kwargs)) + return "created" + + def get_environment(self, **kwargs): + calls.append(("get", (), kwargs)) + return "environment" + + def list_environments(self, **kwargs): + calls.append(("list", (), kwargs)) + return "environments" + + def delete_environment(self, **kwargs): + calls.append(("delete", (), kwargs)) + return "deleted" + + files = Files() + + class AsyncEnvironments(Environments): + files = AsyncFiles() + + class Live: + def connect(self, **kwargs): + calls.append(("live", (), kwargs)) + return "live-manager" + + provider._get_client = lambda: SimpleNamespace( + environments=Environments(), + aio=SimpleNamespace( + environments=AsyncEnvironments(), + live=Live(), + ), + ) + assert provider.resources.create_environment(network={}) == "created" + assert provider.resources.get_environment("env-1") == "environment" + assert provider.resources.list_environments(page_size=10) == "environments" + assert provider.resources.delete_environment("env-1") == "deleted" + assert provider.resources.get_environment_files("env-1", "src", recursive=True) == "files" + assert provider.async_resources.connect_live(config={}) == "live-manager" + assert asyncio.run( + provider.async_resources.get_environment_files("env-1", "src", recursive=True) + ) == "async-files" + assert calls == [ + ("create", (), {"network": {}}), + ("get", (), {"id": "env-1"}), + ("list", (), {"page_size": 10}), + ("delete", (), {"id": "env-1"}), + ("files", ("env-1", "src"), {"recursive": True}), + ("live", (), {"model": "gemini-default", "config": {}}), + ("async-files", ("env-1", "src"), {"recursive": True}), + ] + + +@pytest.mark.parametrize( + "method_name", + [ + "create_auth_token", + "create_interaction", + "create_agent", + "create_webhook", + "create_environment", + "create_trigger", + ], +) +def test_google_experimental_resources_fail_with_capability_error_when_sdk_missing(method_name): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: SimpleNamespace() + + with pytest.raises(NotImplementedError, match="google-genai SDK"): + getattr(provider, method_name)() + + +@pytest.mark.parametrize( + "method_name", + [ + "async_create_auth_token", + "async_create_interaction", + "async_create_agent", + "async_create_webhook", + "async_create_environment", + "async_create_trigger", + ], +) +def test_google_async_experimental_resources_fail_with_capability_error_when_sdk_missing(method_name): + provider = object.__new__(GoogleProvider) + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace()) + + async def invoke(): + with pytest.raises(NotImplementedError, match="google-genai SDK"): + await getattr(provider, method_name)() + + asyncio.run(invoke()) + + +def test_google_live_connection_rejects_credentials_in_config(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + provider._get_client = lambda: SimpleNamespace( + aio=SimpleNamespace( + live=SimpleNamespace(connect=lambda **_: "should-not-run"), + ) + ) + config = {"http_options": {"headers": {"Authorization": "Bearer secret"}}} + with pytest.raises(ValueError, match="凭证"): + provider.connect_live(config=config) + with pytest.raises(ValueError, match="凭证"): + provider.async_connect_live(config=config) + + +def test_google_live_connection_reports_missing_sdk_resource(): + provider = object.__new__(GoogleProvider) + provider._model_name = "gemini-default" + provider._get_client = lambda: SimpleNamespace(aio=SimpleNamespace()) + with pytest.raises(NotImplementedError, match="Live"): + provider.connect_live() + + +def test_ark_content_generation_delete_uses_keyword_task_id_for_sync_and_async(): + provider = object.__new__(VolcengineProvider) + calls = [] + + class Tasks: + def delete(self, *, task_id, **kwargs): + calls.append(("sync", task_id, kwargs)) + return "deleted" + + class AsyncTasks: + async def delete(self, *, task_id, **kwargs): + calls.append(("async", task_id, kwargs)) + return "async-deleted" + + provider._get_client = lambda: SimpleNamespace(content_generation=SimpleNamespace(tasks=Tasks())) + provider._get_aclient = lambda: SimpleNamespace(content_generation=SimpleNamespace(tasks=AsyncTasks())) + + assert provider.resources.delete_content_generation_task("task-1", timeout=4) == "deleted" + assert asyncio.run(provider.async_resources.delete_content_generation_task("task-2", timeout=5)) == "async-deleted" + assert calls == [ + ("sync", "task-1", {"timeout": 4}), + ("async", "task-2", {"timeout": 5}), + ] + + +def test_ark_batch_chat_uses_current_nested_batch_resource(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + calls = {} + + class Completions: + def create(self, **kwargs): + calls.update(kwargs) + return "batch" + + provider._get_client = lambda: SimpleNamespace( + batch=SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + ) + assert provider.resources.create_batch_chat(messages=[]) == "batch" + assert calls == {"messages": [], "model": "ark-model"} + + +def test_ark_async_batch_chat_forwards_sdk_supported_user_argument(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + + calls = [] + + class Completions: + async def create(self, **kwargs): + calls.append(kwargs) + return "async-batch" + + provider._get_aclient = lambda: SimpleNamespace( + batch=SimpleNamespace(chat=SimpleNamespace(completions=Completions())) + ) + + assert asyncio.run( + provider.async_create_batch_chat(messages=[], user="user-1") + ) == "async-batch" + assert calls == [{"messages": [], "model": "ark-model", "user": "user-1"}] + + +def test_ark_context_resources_validate_sdk_parameter_names_and_defaults(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + calls = [] + + class Context: + def create(self, **kwargs): + calls.append(("create", kwargs)) + return "context" + + class Completions: + def create(self, **kwargs): + calls.append(("complete", kwargs)) + return "completion" + + completions = Completions() + + provider._get_client = lambda: SimpleNamespace(context=Context()) + + assert provider.create_context(messages=[], mode="session") == "context" + assert provider.context_complete(context_id="ctx-1", messages=[], stream=False) == "completion" + assert calls == [ + ("create", {"messages": [], "mode": "session", "model": "ark-model"}), + ("complete", {"context_id": "ctx-1", "messages": [], "stream": False, "model": "ark-model"}), + ] + + with pytest.raises(ValueError, match="Context.*temperature"): + provider.create_context(messages=[], temperature=0.2) + with pytest.raises(ValueError, match="Context Completion.*unknown"): + provider.context_complete(context_id="ctx-1", messages=[], unknown=None) + + +def test_ark_classification_validates_query_labels_model_and_kwargs(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._classification_resource = SimpleNamespace(create=lambda **kwargs: kwargs) + provider._get_client = lambda: SimpleNamespace() + + result = provider.classify(" refund ", [" billing ", "support"]) + assert result == { + "query": "refund", + "model": "ark-model", + "labels": ["billing", "support"], + } + with pytest.raises(ValueError, match="query"): + provider.classify(" ", ["support"]) + with pytest.raises(ValueError, match="labels"): + provider.classify("refund", []) + with pytest.raises(ValueError, match="Classification.*unknown"): + provider.classify("refund", ["support"], unknown=None) + + +def test_ark_batch_multimodal_embedding_rejects_sparse_embedding_before_sdk_call(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + + class FailingResource: + def create(self, **_kwargs): + raise AssertionError("不应调用不支持 sparse_embedding 的 SDK 方法") + + provider._get_client = lambda: SimpleNamespace( + batch=SimpleNamespace(multimodal_embeddings=FailingResource()) + ) + provider._get_aclient = lambda: SimpleNamespace( + batch=SimpleNamespace(multimodal_embeddings=FailingResource()) + ) + + with pytest.raises(ValueError, match="Ark Batch Multimodal Embedding 不支持请求参数: sparse_embedding"): + provider.create_batch_multimodal_embedding(input=[], sparse_embedding={"enabled": True}) + + with pytest.raises(ValueError, match="Ark Batch Multimodal Embedding 不支持请求参数: sparse_embedding"): + asyncio.run( + provider.async_create_batch_multimodal_embedding( + input=[], sparse_embedding={"enabled": True} + ) + ) + + +def test_ark_async_generate_batch_chat_uses_matching_provider_method(): + provider = object.__new__(VolcengineProvider) + calls = [] + + async def create_batch_chat(**kwargs): + calls.append(("create", kwargs)) + return "created" + + async def generate_batch_chat(**kwargs): + calls.append(("generate", kwargs)) + return "generated" + + provider.async_create_batch_chat = create_batch_chat + provider.async_generate_batch_chat = generate_batch_chat + + result = asyncio.run(provider.async_resources.generate_batch_chat(messages=[])) + + assert result == "generated" + assert calls == [("generate", {"messages": []})] + + +def test_ark_tokenization_and_multimodal_embedding_reject_unknown_sdk_kwargs(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._options = {} + provider._get_client = lambda: SimpleNamespace() + with pytest.raises(ValueError, match="Tokenization 不支持请求参数: temperature"): + provider.count_tokens("hello", temperature=0.2) + with pytest.raises(ValueError, match="Multimodal Embedding 不支持请求参数: temperature"): + provider.multimodal_embed([], temperature=0.2) + + +def test_openai_responses_stream_helper_matches_installed_sdk_contract_sync_and_async(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + calls = [] + + class Responses: + def stream(self, **kwargs): + calls.append(("sync", kwargs)) + return "sync-stream" + + class AsyncResponses: + def stream(self, **kwargs): + calls.append(("async", kwargs)) + return "async-stream" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + provider._get_aclient = lambda: SimpleNamespace(responses=AsyncResponses()) + + assert provider.stream_responses(CompletionRequest(prompt="hi", stream=True)) == "sync-stream" + assert provider.async_stream_responses(CompletionRequest(prompt="hi", stream=True)) == "async-stream" + assert calls == [ + ("sync", { + "model": "demo", + "input": "hi", + "instructions": "You are a helpful assistant.", + }), + ("async", { + "model": "demo", + "input": "hi", + "instructions": "You are a helpful assistant.", + }), + ] + + assert provider.stream_responses( + input="hi", safety_identifier="user-1" + ) == "sync-stream" + assert calls[-1] == ( + "sync", + {"input": "hi", "model": "demo", "safety_identifier": "user-1"}, + ) + assert provider.stream_responses(input="hi", verbosity="low") == "sync-stream" + assert calls[-1] == ( + "sync", + {"input": "hi", "model": "demo", "text": {"verbosity": "low"}}, + ) + + +def test_openai_responses_stream_continuation_omits_model_sync_and_async(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + calls = [] + + class Responses: + def stream(self, **kwargs): + if "response_id" in kwargs or "starting_after" in kwargs: + assert "model" not in kwargs + calls.append(("sync", kwargs)) + return "sync-stream" + + class AsyncResponses: + def stream(self, **kwargs): + if "response_id" in kwargs or "starting_after" in kwargs: + assert "model" not in kwargs + calls.append(("async", kwargs)) + return "async-stream" + + provider._get_client = lambda: SimpleNamespace(responses=Responses()) + provider._get_aclient = lambda: SimpleNamespace(responses=AsyncResponses()) + + assert provider.stream_responses(response_id="resp-1") == "sync-stream" + assert calls[-1] == ("sync", {"response_id": "resp-1"}) + assert ( + provider.stream_responses(response_id="resp-1", starting_after=2) + == "sync-stream" + ) + assert calls[-1] == ( + "sync", + {"response_id": "resp-1", "starting_after": 2}, + ) + + assert ( + provider.async_stream_responses(response_id="resp-1", starting_after=2) + == "async-stream" + ) + assert calls[-1] == ( + "async", + {"response_id": "resp-1", "starting_after": 2}, + ) + + + +def test_openai_polling_helpers_match_installed_sdk_parameters_sync(): + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + calls = [] + + class Files: + def wait_for_processing(self, **kwargs): + calls.append(("wait", kwargs)) + return "file" + + class VectorFiles: + def poll(self, *args, **kwargs): + calls.append(("vector_file", args, kwargs)) + return "vector-file" + + class VectorBatches: + def poll(self, *args, **kwargs): + calls.append(("vector_batch", args, kwargs)) + return "vector-batch" + + def upload_and_poll(self, **kwargs): + calls.append(("upload_batch", kwargs)) + return "uploaded-batch" + + class Videos: + def poll(self, *args, **kwargs): + calls.append(("video", args, kwargs)) + return "video" + + provider._get_client = lambda: SimpleNamespace( + files=Files(), + vector_stores=SimpleNamespace(files=VectorFiles(), file_batches=VectorBatches()), + videos=Videos(), + ) + + assert provider.wait_for_file("file-1", poll_interval=1.5, max_wait_seconds=20) == "file" + assert provider.poll_vector_store_file("store-1", "file-1", poll_interval_ms=250) == "vector-file" + assert provider.poll_vector_store_file_batch("store-1", "batch-1", poll_interval_ms=300) == "vector-batch" + assert provider.upload_vector_store_file_batch_and_poll( + "store-1", ["a"], max_concurrency=2, file_ids=["f-1"], + poll_interval_ms=400, chunking_strategy={"type": "auto"}, + ) == "uploaded-batch" + assert provider.poll_video("video-1", poll_interval_ms=500) == "video" + + assert calls == [ + ("wait", {"id": "file-1", "poll_interval": 1.5, "max_wait_seconds": 20}), + ("vector_file", ("file-1",), {"vector_store_id": "store-1", "poll_interval_ms": 250}), + ("vector_batch", ("batch-1",), {"vector_store_id": "store-1", "poll_interval_ms": 300}), + ("upload_batch", { + "vector_store_id": "store-1", "files": ["a"], "max_concurrency": 2, + "file_ids": ["f-1"], "poll_interval_ms": 400, + "chunking_strategy": {"type": "auto"}, + }), + ("video", ("video-1",), {"poll_interval_ms": 500}), + ] + + with pytest.raises(ValueError, match="文件等待.*timeout"): + provider.wait_for_file("file-1", timeout=1) + with pytest.raises(ValueError, match="向量库文件轮询.*extra_headers"): + provider.poll_vector_store_file("store-1", "file-1", extra_headers={"X-Test": "1"}) + with pytest.raises(ValueError, match="向量库文件批次上传轮询.*timeout"): + provider.upload_vector_store_file_batch_and_poll("store-1", [], timeout=1) + with pytest.raises(ValueError, match="视频轮询.*extra_query"): + provider.poll_video("video-1", extra_query={"trace": "1"}) + + +def test_openai_vector_store_upload_and_container_file_match_sdk_parameters(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + def upload(self, **kwargs): + calls.append(("upload", kwargs)) + return "uploaded" + + class ContainerFiles: + def create(self, *args, **kwargs): + calls.append(("container", args, kwargs)) + return "container-file" + + provider._get_client = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()), + containers=SimpleNamespace(files=ContainerFiles()), + ) + assert provider.upload_vector_store_file("store-1", "file", chunking_strategy={"type": "auto"}) == "uploaded" + assert provider.create_container_file("container-1", file_id="file-1") == "container-file" + assert calls == [ + ("upload", { + "vector_store_id": "store-1", "file": "file", + "chunking_strategy": {"type": "auto"}, + }), + ("container", ("container-1",), {"file_id": "file-1"}), + ] + with pytest.raises(ValueError, match="向量库文件上传.*timeout"): + provider.upload_vector_store_file("store-1", "file", timeout=1) + + +def test_openai_vector_store_upload_and_poll_matches_installed_sdk_parameters_sync(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + def upload_and_poll(self, **kwargs): + calls.append(kwargs) + return "uploaded-and-polled" + + provider._get_client = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()) + ) + + assert provider.upload_vector_store_file_and_poll( + "store-1", + "file", + attributes={"source": "docs", "rank": 1}, + poll_interval_ms=250, + chunking_strategy={"type": "auto"}, + ) == "uploaded-and-polled" + assert calls == [ + { + "vector_store_id": "store-1", + "file": "file", + "attributes": {"source": "docs", "rank": 1}, + "poll_interval_ms": 250, + "chunking_strategy": {"type": "auto"}, + } + ] + with pytest.raises(ValueError, match="向量库文件上传轮询.*timeout"): + provider.upload_vector_store_file_and_poll("store-1", "file", timeout=1) + + +def test_openai_vector_store_file_update_requires_sdk_attributes_parameter(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + def update(self, *args, **kwargs): + calls.append((args, kwargs)) + return "updated" + + provider._get_client = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()) + ) + + assert provider.update_vector_store_file( + "store-1", "file-1", attributes={"source": "docs", "rank": 1} + ) == "updated" + assert calls == [ + ( + ("file-1",), + { + "vector_store_id": "store-1", + "attributes": {"source": "docs", "rank": 1}, + }, + ) + ] + with pytest.raises(TypeError): + provider.update_vector_store_file("store-1", "file-1") + + +def test_openai_upload_file_chunked_matches_installed_sdk_contract_sync_and_async(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class Uploads: + def upload_file_chunked(self, **kwargs): + calls.append(("sync", kwargs)) + return "uploaded" + + class AsyncUploads: + async def upload_file_chunked(self, **kwargs): + calls.append(("async", kwargs)) + return "async-uploaded" + + provider._get_client = lambda: SimpleNamespace(uploads=Uploads()) + provider._get_aclient = lambda: SimpleNamespace(uploads=AsyncUploads()) + + assert provider.upload_file_chunked( + file=b"payload", + mime_type="text/plain", + purpose="assistants", + filename="notes.txt", + bytes=7, + part_size=4, + md5="md5-value", + ) == "uploaded" + + async def run(): + return await provider.async_upload_file_chunked( + file=b"payload", + mime_type="text/plain", + purpose="assistants", + filename="notes.txt", + bytes=7, + part_size=4, + md5="md5-value", + ) + + assert asyncio.run(run()) == "async-uploaded" + assert calls == [ + ( + "sync", + { + "file": b"payload", + "mime_type": "text/plain", + "purpose": "assistants", + "filename": "notes.txt", + "bytes": 7, + "part_size": 4, + "md5": "md5-value", + }, + ), + ( + "async", + { + "file": b"payload", + "mime_type": "text/plain", + "purpose": "assistants", + "filename": "notes.txt", + "bytes": 7, + "part_size": 4, + "md5": "md5-value", + }, + ), + ] + + with pytest.raises(ValueError, match="分片文件上传.*timeout"): + provider.upload_file_chunked( + file=b"payload", mime_type="text/plain", purpose="assistants", timeout=1 + ) + + async def run_invalid(): + with pytest.raises(ValueError, match="分片文件上传.*timeout"): + await provider.async_upload_file_chunked( + file=b"payload", mime_type="text/plain", purpose="assistants", timeout=1 + ) + + asyncio.run(run_invalid()) + + +def test_openai_container_file_content_reports_missing_sdk_resource(): + provider = object.__new__(OpenAICompatibleProvider) + provider._get_client = lambda: SimpleNamespace( + containers=SimpleNamespace(files=SimpleNamespace()) + ) + + with pytest.raises(NotImplementedError, match="容器文件内容读取资源"): + provider.container_file_content("container-1", "file-1") + + +def test_openai_vector_store_upload_and_container_file_match_sdk_parameters_async(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + async def upload(self, **kwargs): + calls.append(("upload", kwargs)) + return "uploaded" + + class ContainerFiles: + async def create(self, *args, **kwargs): + calls.append(("container", args, kwargs)) + return "container-file" + + provider._get_aclient = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()), + containers=SimpleNamespace(files=ContainerFiles()), + ) + + async def run(): + assert await provider.async_upload_vector_store_file( + "store-1", "file", chunking_strategy={"type": "auto"} + ) == "uploaded" + assert await provider.async_create_container_file("container-1", file_id="file-1") == "container-file" + with pytest.raises(ValueError, match="向量库文件上传.*timeout"): + await provider.async_upload_vector_store_file("store-1", "file", timeout=1) + + asyncio.run(run()) + assert calls == [ + ("upload", { + "vector_store_id": "store-1", "file": "file", + "chunking_strategy": {"type": "auto"}, + }), + ("container", ("container-1",), {"file_id": "file-1"}), + ] + + +def test_openai_vector_store_upload_and_poll_matches_installed_sdk_parameters_async(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class VectorFiles: + async def upload_and_poll(self, **kwargs): + calls.append(kwargs) + return "uploaded-and-polled" + + provider._get_aclient = lambda: SimpleNamespace( + vector_stores=SimpleNamespace(files=VectorFiles()) + ) + + async def run(): + assert await provider.async_upload_vector_store_file_and_poll( + "store-1", + "file", + attributes={"source": "docs", "rank": 1}, + poll_interval_ms=250, + chunking_strategy={"type": "auto"}, + ) == "uploaded-and-polled" + with pytest.raises(ValueError, match="向量库文件上传轮询.*timeout"): + await provider.async_upload_vector_store_file_and_poll( + "store-1", "file", timeout=1 + ) + + asyncio.run(run()) + assert calls == [ + { + "vector_store_id": "store-1", + "file": "file", + "attributes": {"source": "docs", "rank": 1}, + "poll_interval_ms": 250, + "chunking_strategy": {"type": "auto"}, + } + ] + + +def test_openai_async_container_file_content_reports_missing_sdk_resource(): + provider = object.__new__(OpenAICompatibleProvider) + provider._get_aclient = lambda: SimpleNamespace( + containers=SimpleNamespace(files=SimpleNamespace()) + ) + + async def run(): + with pytest.raises(NotImplementedError, match="容器文件内容读取资源"): + await provider.async_container_file_content("container-1", "file-1") + + asyncio.run(run()) + + +def test_openai_polling_helpers_match_installed_sdk_parameters_async(): + provider = object.__new__(OpenAICompatibleProvider) + calls = [] + + class Files: + async def wait_for_processing(self, **kwargs): + calls.append(("wait", kwargs)) + return "file" + + class VectorFiles: + async def poll(self, *args, **kwargs): + calls.append(("vector_file", args, kwargs)) + return "vector-file" + + class VectorBatches: + async def poll(self, *args, **kwargs): + calls.append(("vector_batch", args, kwargs)) + return "vector-batch" + + async def upload_and_poll(self, **kwargs): + calls.append(("upload_batch", kwargs)) + return "uploaded-batch" + + class Videos: + async def poll(self, *args, **kwargs): + calls.append(("video", args, kwargs)) + return "video" + + provider._get_aclient = lambda: SimpleNamespace( + files=Files(), + vector_stores=SimpleNamespace(files=VectorFiles(), file_batches=VectorBatches()), + videos=Videos(), + ) + + async def run(): + assert await provider.async_wait_for_file("file-1", poll_interval=1.5, max_wait_seconds=20) == "file" + assert await provider.async_poll_vector_store_file("store-1", "file-1", poll_interval_ms=250) == "vector-file" + assert await provider.async_poll_vector_store_file_batch("store-1", "batch-1", poll_interval_ms=300) == "vector-batch" + assert await provider.async_upload_vector_store_file_batch_and_poll( + "store-1", ["a"], max_concurrency=2, file_ids=["f-1"], + poll_interval_ms=400, chunking_strategy={"type": "auto"}, + ) == "uploaded-batch" + assert await provider.async_poll_video("video-1", poll_interval_ms=500) == "video" + with pytest.raises(ValueError, match="文件等待.*timeout"): + await provider.async_wait_for_file("file-1", timeout=1) + with pytest.raises(ValueError, match="向量库文件轮询.*extra_headers"): + await provider.async_poll_vector_store_file("store-1", "file-1", extra_headers={"X-Test": "1"}) + with pytest.raises(ValueError, match="向量库文件批次上传轮询.*timeout"): + await provider.async_upload_vector_store_file_batch_and_poll("store-1", [], timeout=1) + with pytest.raises(ValueError, match="视频轮询.*extra_query"): + await provider.async_poll_video("video-1", extra_query={"trace": "1"}) + + asyncio.run(run()) + assert calls == [ + ("wait", {"id": "file-1", "poll_interval": 1.5, "max_wait_seconds": 20}), + ("vector_file", ("file-1",), {"vector_store_id": "store-1", "poll_interval_ms": 250}), + ("vector_batch", ("batch-1",), {"vector_store_id": "store-1", "poll_interval_ms": 300}), + ("upload_batch", { + "vector_store_id": "store-1", "files": ["a"], "max_concurrency": 2, + "file_ids": ["f-1"], "poll_interval_ms": 400, + "chunking_strategy": {"type": "auto"}, + }), + ("video", ("video-1",), {"poll_interval_ms": 500}), + ] + + +def test_ark_file_wait_helpers_match_installed_sdk_parameters_sync_and_async(): + provider = object.__new__(VolcengineProvider) + calls = [] + + class Files: + def wait_for_processing(self, **kwargs): + calls.append(("sync", kwargs)) + return "file" + + class AsyncFiles: + async def wait_for_processing(self, **kwargs): + calls.append(("async", kwargs)) + return "async-file" + + provider._get_client = lambda: SimpleNamespace(files=Files()) + provider._get_aclient = lambda: SimpleNamespace(files=AsyncFiles()) + + assert provider.wait_for_file("file-1", poll_interval=2, max_wait_seconds=30) == "file" + assert asyncio.run( + provider.async_wait_for_file("file-2", poll_interval=1, max_wait_seconds=40) + ) == "async-file" + assert calls == [ + ("sync", {"id": "file-1", "poll_interval": 2, "max_wait_seconds": 30}), + ("async", {"id": "file-2", "poll_interval": 1, "max_wait_seconds": 40}), + ] + + with pytest.raises(ValueError, match="文件等待.*timeout"): + provider.wait_for_file("file-1", timeout=1) + with pytest.raises(ValueError, match="文件等待.*extra_headers"): + asyncio.run(provider.async_wait_for_file("file-2", extra_headers={"X-Test": "1"})) + + +@pytest.mark.parametrize( + ("file_id", "poll_interval", "max_wait_seconds", "match"), + [ + ("", 1, 10, "file_id"), + ("file-1", 0, 10, "poll_interval"), + ("file-1", float("inf"), 10, "poll_interval"), + ("file-1", 1, 0, "max_wait_seconds"), + ("file-1", 1, float("nan"), "max_wait_seconds"), + ], +) +def test_ark_file_wait_helpers_validate_identifiers_and_timeouts( + file_id, poll_interval, max_wait_seconds, match +): + provider = object.__new__(VolcengineProvider) + provider._get_client = lambda: pytest.fail("无效轮询参数不应调用 SDK") + provider._get_aclient = lambda: pytest.fail("无效轮询参数不应调用异步 SDK") + + with pytest.raises(ValueError, match=match): + provider.wait_for_file(file_id, poll_interval=poll_interval, max_wait_seconds=max_wait_seconds) + + with pytest.raises(ValueError, match=match): + asyncio.run( + provider.async_wait_for_file( + file_id, + poll_interval=poll_interval, + max_wait_seconds=max_wait_seconds, + ) + ) + + +def test_ark_file_wait_helpers_reject_unknown_none_arguments(): + provider = object.__new__(VolcengineProvider) + provider._get_client = lambda: pytest.fail("未知参数不应调用 SDK") + + with pytest.raises(ValueError, match="文件等待.*timeout"): + provider.wait_for_file("file-1", timeout=None) + + +def test_ark_classification_resource_forwards_sync_and_async_calls(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + calls = [] + + class Classification: + def create(self, **kwargs): + calls.append(("sync", kwargs)) + return "classified" + + class AsyncClassification: + async def create(self, **kwargs): + calls.append(("async", kwargs)) + return "async-classified" + + provider._get_client = lambda: SimpleNamespace() + provider._get_aclient = lambda: SimpleNamespace() + provider._classification_resource = Classification() + provider._async_classification_resource = AsyncClassification() + + assert provider.resources.classify("refund", ["billing", "support"]) == "classified" + assert asyncio.run( + provider.async_resources.classify("refund", ["billing", "support"]) + ) == "async-classified" + assert calls == [ + ("sync", {"query": "refund", "model": "ark-model", "labels": ["billing", "support"]}), + ("async", {"query": "refund", "model": "ark-model", "labels": ["billing", "support"]}), + ] + + +def test_ark_close_invalidates_cached_classification_resources(): + provider = object.__new__(VolcengineProvider) + provider._client = None + provider._aclient = None + provider._classification_resource = object() + provider._async_classification_resource = object() + + provider.close() + assert provider._classification_resource is None + assert provider._async_classification_resource is None + + provider._classification_resource = object() + provider._async_classification_resource = object() + asyncio.run(provider.aclose()) + assert provider._classification_resource is None + assert provider._async_classification_resource is None + + +def test_ark_classification_is_declared_as_provider_capability(): + assert "classification" in VolcengineProvider.capabilities + + +def test_ark_native_resource_wrappers_reject_credentials_before_sdk_dispatch(): + provider = object.__new__(VolcengineProvider) + provider._model_name = "ark-model" + provider._protocol = "responses" + + def fail_if_called(): + raise AssertionError("SDK client must not be constructed for rejected credentials") + + provider._get_client = fail_if_called + provider._get_aclient = fail_if_called + + with pytest.raises(ValueError, match="凭证"): + provider.list_files(extra_headers={"X-API-Key": "secret"}) + with pytest.raises(ValueError, match="凭证"): + provider.create_context(messages=[], extra_body={"token": "secret"}) + + async def run() -> None: + with pytest.raises(ValueError, match="凭证"): + await provider.async_list_files(extra_query={"access_token": "secret"}) + with pytest.raises(ValueError, match="凭证"): + await provider.async_create_context( + messages=[], extra_headers={"Authorization": "Bearer secret"} + ) + + asyncio.run(run()) + + +def test_anthropic_token_count_rejects_non_counting_fields(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + with pytest.raises(ValueError, match="不支持请求参数: background"): + provider.count_tokens(CompletionRequest(prompt="hi", background=True)) + + +def test_anthropic_token_count_rejects_output_format_before_sdk_call(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + calls = {} + + class Messages: + def count_tokens(self, **kwargs): + calls.update(kwargs) + return SimpleNamespace(input_tokens=5) + + provider._get_client = lambda: SimpleNamespace(messages=Messages()) + with pytest.raises(ValueError, match="token count 不支持 output_format"): + provider.count_tokens(CompletionRequest(prompt="hi", output_format=dict)) + assert calls == {} + + async def run() -> None: + with pytest.raises(ValueError, match="token count 不支持 output_format"): + await provider.async_count_tokens( + CompletionRequest(prompt="hi", output_format=dict) + ) + + asyncio.run(run()) + + +def test_anthropic_token_count_forwards_supported_extra_body_sync_and_async(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + calls = {} + + class SyncMessages: + def count_tokens(self, **kwargs): + calls["sync"] = kwargs + return SimpleNamespace(input_tokens=5) + + class AsyncMessages: + async def count_tokens(self, **kwargs): + calls["async"] = kwargs + return SimpleNamespace(input_tokens=6) + + provider._get_client = lambda: SimpleNamespace( + messages=SyncMessages(), + beta=SimpleNamespace(messages=SyncMessages()), + ) + provider._get_aclient = lambda: SimpleNamespace( + messages=AsyncMessages(), + beta=SimpleNamespace(messages=AsyncMessages()), + ) + + request = CompletionRequest(prompt="hi", extra_body={"tenant": "demo"}) + assert provider.count_tokens(request) == 5 + assert provider.beta_count_tokens(request) == 5 + assert asyncio.run(provider.async_count_tokens(request)) == 6 + assert asyncio.run(provider.async_beta_count_tokens(request)) == 6 + assert calls["sync"]["extra_body"] == {"tenant": "demo"} + assert calls["async"]["extra_body"] == {"tenant": "demo"} + + +def test_anthropic_beta_facade_does_not_leak_stream_flag(): + provider = object.__new__(AnthropicProvider) + provider._model_name = "claude" + provider._options = {} + calls = {} + + class Messages: + def create(self, **kwargs): + calls.update(kwargs) + return "beta" + + def count_tokens(self, **kwargs): + calls["count"] = kwargs + return SimpleNamespace(input_tokens=8) + + provider._get_client = lambda: SimpleNamespace(beta=SimpleNamespace(messages=Messages())) + assert provider.resources.beta_create(CompletionRequest(prompt="hi")) == "beta" + assert "stream" not in calls + assert provider.resources.beta_count_tokens(CompletionRequest(prompt="hi")) == 8 + + +def test_async_embedding_query_default_works_for_legacy_provider(): + class Embedding(TextEmbeddingModel): + def embed_documents(self, texts, **_kwargs): + return [[float(len(texts[0]))]] + + async def aembed_documents(self, texts, **_kwargs): + return [[float(len(texts[0]))]] + + assert asyncio.run(Embedding().aembed_query("abc")) == [3.0] + + +def test_chat_completions_normalizes_flat_tool_calls_to_openai_shape(): + """我们产出的扁平 tool_calls 必须能在回填时转换成 OpenAI 规范结构。 + + ``CompletionResult.tool_calls`` 使用扁平的 ``{"id","type","name", + "arguments"}`` 形状;OpenAI Chat Completions 要求 assistant 历史的 + ``tool_calls`` 使用嵌套的 ``{"id","type","function":{"name", + "arguments"}}``。适配器必须在请求边界完成转换,否则多轮工具调用会 + 被服务端以 400 拒绝。 + """ + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + flat = {"id": "call-1", "type": "function", "name": "lookup", + "arguments": '{"id":1}'} + request = provider._build_chat_request( + messages=[ + {"role": "user", "content": "查询"}, + {"role": "assistant", "content": "", "tool_calls": [flat]}, + {"role": "tool", "tool_call_id": "call-1", "content": "结果"}, + ], + stream=False, + ) + + tool_calls = request["messages"][1]["tool_calls"] + assert tool_calls == [{ + "id": "call-1", + "type": "function", + "function": {"name": "lookup", "arguments": '{"id":1}'}, + }], "扁平 tool_calls 必须转换为 OpenAI 嵌套结构" + + +def test_chat_completions_preserves_native_tool_calls(): + """已经是 OpenAI 原生嵌套结构的历史必须原样保留。""" + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + native = {"id": "call-2", "type": "function", + "function": {"name": "lookup", "arguments": '{"id":2}'}} + request = provider._build_chat_request( + messages=[ + {"role": "user", "content": "查询"}, + {"role": "assistant", "content": "", "tool_calls": [native]}, + {"role": "tool", "tool_call_id": "call-2", "content": "结果"}, + ], + stream=False, + ) + + assert request["messages"][1]["tool_calls"] == [native] + + +def test_chat_completions_rejects_tool_call_without_function_name(): + """缺少函数名的工具调用必须显式报错,不能静默发出无效请求。""" + provider = object.__new__(OpenAICompatibleProvider) + provider._model_name = "demo" + provider._provider = "openai" + provider._protocol = "chat_completions" + provider._options = {} + + with pytest.raises(ValueError, match="函数名"): + provider._build_chat_request( + messages=[ + {"role": "user", "content": "查询"}, + {"role": "assistant", "content": "", + "tool_calls": [{"id": "call-3", "type": "function", + "arguments": "{}"}]}, + ], + stream=False, + ) + + +def test_normalize_messages_accepts_id_less_tool_calls_for_non_chat_protocols(): + """Google 这类协议必须接受无 id 的历史工具调用。 + + Gemini 的 ``FunctionCall.id`` 是可选字段且 SDK 默认 ``None``,其 + ``_contents`` 明确允许无 id 的调用。公共的 ``normalize_messages`` 因此不能 + 强制要求 id,否则多轮工具调用会在到达 SDK 之前就被本地拒绝。 + """ + normalized = normalize_messages( + None, + None, + [ + {"role": "user", "content": "北京天气"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": None, "type": "function", "name": "get_weather", + "arguments": {"city": "BJ"}} + ], + }, + {"role": "tool", "tool_call_id": "get_weather", + "name": "get_weather", "content": '{"t":20}'}, + ], + ) + + tool_call = normalized[1]["tool_calls"][0] + assert "id" not in tool_call + assert tool_call["type"] == "function" + assert tool_call["function"] == { + "name": "get_weather", + "arguments": '{"city":"BJ"}', + } + + +def test_google_multi_turn_replay_accepts_id_less_tool_calls(): + """把上一轮无 id 的工具调用原样回填给 Google 必须成功。""" + contents = GoogleProvider._contents( + normalize_messages( + None, + None, + [ + {"role": "user", "content": "北京天气"}, + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": None, "type": "function", "name": "get_weather", + "arguments": {"city": "BJ"}} + ], + }, + {"role": "tool", "tool_call_id": "get_weather", + "name": "get_weather", "content": '{"t":20}'}, + ], + ), + None, + None, + ) + + function_call = next( + part.function_call + for content in contents + for part in content.parts + if part.function_call + ) + assert function_call.name == "get_weather" + + +def test_chat_tool_call_normalization_reads_input_alias(): + """Responses/Anthropic 用 ``input`` 承载参数,回填时不能静默变空。""" + normalized = normalize_chat_messages( + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": "c1", "type": "custom_tool_call", "name": "n", + "input": '{"a":1}'} + ], + } + ] + ) + + assert normalized[0]["tool_calls"] == [ + {"id": "c1", "type": "function", + "function": {"name": "n", "arguments": '{"a":1}'}} + ] + + +@pytest.mark.parametrize( + "tool_type", + ["function", "function_call", "custom_tool_call", "tool_call"], +) +def test_chat_tool_call_normalization_maps_type_to_function(tool_type): + """Chat Completions 只接受 ``type="function"``,其它来源取值都要映射。""" + normalized = normalize_chat_messages( + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": "x", "type": tool_type, "name": "n", "arguments": "{}"} + ], + } + ] + ) + + assert normalized[0]["tool_calls"][0]["type"] == "function" + + +def test_chat_completions_still_requires_tool_call_id(): + """Chat Completions 的严格校验不能被放宽,缺 id 必须显式报错。""" + with pytest.raises(ValueError, match="缺少 id/call_id"): + normalize_messages( + None, + None, + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": None, "type": "function", "name": "n", + "arguments": "{}"} + ], + } + ], + require_tool_call_ids=True, + ) + + +def test_responses_input_still_requires_tool_call_id(): + """Responses 输入项需要 call_id,缺 id 仍要在请求前显式报错。""" + with pytest.raises(ValueError, match="缺少 id/call_id"): + normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": None, "type": "function", "name": "n", + "arguments": "{}"} + ], + } + ], + ) + + + +@pytest.mark.parametrize( + "model_name", + [ + "claude-fable-5", + "claude-fable-5-1", + "claude-mythos-5", + "claude-mythos-5-1", + "claude-mythos-preview", + ], +) +def test_anthropic_sampling_deprecation_covers_new_family_names(model_name): + """5 代新增家族(fable/mythos)同样拒绝 legacy 采样控制字段。 + + Python SDK v1.0+ 已从请求签名移除 temperature/top_p/top_k,识别失败会让 + 请求直接抛 TypeError。家族名不能只匹配 opus/sonnet/haiku。 + """ + provider = object.__new__(AnthropicProvider) + provider._model_name = model_name + provider._options = {} + + assert provider._sampling_controls_deprecated(model_name) is True + + +@pytest.mark.parametrize( + "model_name", + ["claude-3-5-sonnet-20240620", "claude-3-haiku-20240307", "claude-2.1", + "claude-sonnet-4", "claude-opus-4"], +) +def test_anthropic_sampling_deprecation_keeps_legacy_models_permissive(model_name): + """4.5 之前的模型仍接受采样控制字段,不能被新家族规则误伤。""" + provider = object.__new__(AnthropicProvider) + provider._model_name = model_name + provider._options = {} + + assert provider._sampling_controls_deprecated(model_name) is False + + +@pytest.mark.parametrize( + "model_name", + ["notclaude-sonnet-4-6", "xclaude-opus-5", "myclaude-fable-5"], +) +def test_anthropic_version_parser_requires_a_word_boundary(model_name): + """``claude`` 必须是独立的名称段,不能被别的字符串前缀带出误判。""" + provider = object.__new__(AnthropicProvider) + provider._model_name = model_name + provider._options = {} + + assert provider._sampling_controls_deprecated(model_name) is False + + +# ── 回归:Ark Responses 非流式正文提取 ── + + +def _ark_response(output, *, status="completed"): + """用真实 Ark SDK 模型构造响应,避免伪造线上不存在的字段。 + + 此前测试普遍使用 ``SimpleNamespace(output_text=...)``,但 Ark 的 + ``Response`` 没有 ``output_text``(OpenAI SDK 才把它实现为聚合 property), + 正文位于 ``output[].content[].text``。伪造该字段会让测试永远通过,掩盖 + 真实环境下的空正文缺陷。 + """ + from volcenginesdkarkruntime.types.responses.response import Response + + return Response.model_validate( + { + "id": "resp_test", + "object": "response", + "created_at": 1, + "model": "ark-model", + "status": status, + "tools": [], + "output": output, + } + ) + + +def test_ark_responses_non_stream_extracts_text_from_output_content(): + """Ark 的 Response 没有 ``output_text`` 属性,正文必须从 output 内容块取。 + + 只读 ``output_text`` 会让所有非流式入口返回空文本且不报错,上层 + ``identify_intent`` 随后抛出「意图识别返回空结果」。 + """ + response = _ark_response( + [ + { + "type": "message", + "id": "msg_1", + "role": "assistant", + "status": "completed", + "content": [ + {"type": "output_text", "text": "这是真实回答。", "annotations": []} + ], + } + ] + ) + + assert VolcengineProvider._extract_result(response).text == "这是真实回答。" + + +def test_ark_response_model_has_no_output_text_attribute(): + """固定 SDK 事实:Ark 的 Response 不提供 ``output_text``。 + + 该断言失败说明 SDK 改变了响应形状,届时可以重新评估提取逻辑。 + """ + from volcenginesdkarkruntime.types.responses.response import Response + + assert "output_text" not in Response.model_fields + assert not hasattr(Response, "output_text") + + +def test_ark_responses_completed_response_without_any_content_fails_loudly(): + """标记 completed 却无正文、工具调用、拒答与推理时,必须显式失败。 + + 项目规则禁止用占位结果掩盖失败:静默返回空文本会让调用方拿到空串后 + 继续执行,最终表现为难以定位的「空结果」错误。 + """ + with pytest.raises(RuntimeError, match="未包含任何正文"): + VolcengineProvider._extract_result(_ark_response([])) + + +def test_ark_responses_reasoning_only_response_is_still_valid(): + """只有推理内容、没有正文的响应是合法的,不能被 fail-closed 误伤。""" + response = _ark_response( + [ + { + "type": "reasoning", + "id": "rs_1", + "summary": [{"type": "summary_text", "text": "Ark 思考"}], + } + ] + ) + + result = VolcengineProvider._extract_result(response) + + assert result.text == "" + assert result.reasoning == "Ark 思考" + + +# ── 回归:动态资源树必须与显式 Facade 共用能力门禁 ── + + +def _ark_resources_without_verified_protocols(calls): + """构造未登记 server_verified_protocols 的 Ark provider 与资源 Facade。""" + from src.providers.volcengine import ArkResources + + provider = object.__new__(VolcengineProvider) + provider._provider = "volcengine" + provider._protocol = "responses" + provider._server_verified_protocols = frozenset() + provider._options = {} + object.__setattr__( + provider, + "_client", + SimpleNamespace( + responses=SimpleNamespace( + create=lambda **kwargs: calls.append(kwargs) or "remote", + retrieve=lambda **kwargs: calls.append(kwargs) or "remote", + ), + files=SimpleNamespace(create=lambda **kwargs: "local-file"), + ), + ) + return provider, ArkResources(provider) + + +def test_ark_dynamic_responses_resource_enforces_server_verification(): + """``resources.responses.create(...)`` 必须与 ``create_response()`` 同样受检。 + + 动态资源树此前只做凭证校验,绕过了 server_verified_protocols 门禁,会把 + 本地 SDK 资源直接发往未验证的服务端。 + """ + calls: list[dict[str, object]] = [] + _, resources = _ark_resources_without_verified_protocols(calls) + + with pytest.raises(ValueError, match="未验证 Responses"): + resources.responses.create(model="ark-model", input="hi") + with pytest.raises(ValueError, match="未验证 Responses"): + resources.responses.retrieve(response_id="resp-1") + + assert calls == [] + + +def test_ark_dynamic_non_responses_resources_are_not_gated(): + """能力门禁只针对 responses 资源,files 等同名资源不能被误拦。""" + calls: list[dict[str, object]] = [] + _, resources = _ark_resources_without_verified_protocols(calls) + + assert resources.files.create(file="f") == "local-file" + + +def test_ark_dynamic_responses_resource_passes_once_protocol_is_verified(): + """登记 server_verified_protocols 后动态路径应正常放行。""" + calls: list[dict[str, object]] = [] + provider, resources = _ark_resources_without_verified_protocols(calls) + provider._server_verified_protocols = frozenset({"responses"}) + + assert resources.responses.create(model="ark-model", input="hi") == "remote" + assert len(calls) == 1 + + +def test_ark_resource_guard_keeps_credential_check_before_capability_check(): + """凭证问题必须优先报出,不能被能力错误掩盖成配置问题。""" + calls: list[dict[str, object]] = [] + _, resources = _ark_resources_without_verified_protocols(calls) + + with pytest.raises(ValueError, match="凭证"): + resources.responses.create(model="ark-model", input="hi", api_key="sk-secret") + + +# ── 回归:Ark instructions × caching 互斥必须覆盖所有构造路径 ── + + +def _ark_provider_for_request_build(options=None): + provider = object.__new__(VolcengineProvider) + provider._provider = "volcengine" + provider._model_name = "ark-model" + provider._protocol = "responses" + provider._options = options or {} + return provider + + +@pytest.mark.parametrize( + "request_kwargs", + [ + pytest.param({"prompt": "hi", "caching": {"type": "enabled"}}, id="top-level-caching"), + pytest.param( + {"prompt": "hi", "extra_body": {"caching": {"type": "enabled"}}}, + id="request-extra-body", + ), + ], +) +def test_ark_responses_rejects_instructions_with_enabled_caching_on_every_path(request_kwargs): + """``caching`` 也可从 ``extra_body`` 进来,Ark SDK 会把它合并进请求体, + 服务端看到的仍是 ``caching=enabled``。只查顶层参数会漏掉这条路径。""" + provider = _ark_provider_for_request_build() + + with pytest.raises(ValueError, match="互斥"): + provider._build_responses_request(CompletionRequest(**request_kwargs)) + + +def test_ark_responses_rejects_caching_enabled_via_model_options_extra_body(): + """模型级 options.extra_body 同样会被并入请求体。""" + provider = _ark_provider_for_request_build( + {"extra_body": {"caching": {"type": "enabled"}}} + ) + + with pytest.raises(ValueError, match="互斥"): + provider._build_responses_request(CompletionRequest(prompt="hi")) + + +def test_ark_native_response_kwargs_reject_instructions_with_enabled_caching(): + """原生 create_response/async_create_response 走 kwargs 校验,也必须受检。""" + with pytest.raises(ValueError, match="互斥"): + VolcengineProvider._validate_native_response_kwargs( + { + "model": "ark-model", + "input": "hi", + "instructions": "system", + "caching": {"type": "enabled"}, + } + ) + + +@pytest.mark.parametrize( + "kwargs", + [ + pytest.param( + {"model": "m", "input": "hi", "caching": {"type": "enabled"}}, id="no-instructions" + ), + pytest.param( + {"model": "m", "input": "hi", "instructions": "s", "caching": {"type": "disabled"}}, + id="caching-disabled", + ), + pytest.param({"model": "m", "input": "hi", "instructions": "s"}, id="no-caching"), + ], +) +def test_ark_native_response_kwargs_allow_non_conflicting_combinations(kwargs): + """合法组合不能被互斥检查误伤。""" + assert VolcengineProvider._validate_native_response_kwargs(kwargs)["model"] == "m" + + +# ── 回归:Ark 专属内容块不能经顶层 item 形态绕过 variant 守卫 ── + + +@pytest.mark.parametrize( + "item", + [ + {"type": "input_audio", "audio": "AAAA"}, + {"type": "input_video", "video": "BBBB"}, + {"type": "input_image", "image_url": "u", "image_pixel_limit": {"max_pixels": 100}}, + ], +) +def test_openai_responses_rejects_ark_only_blocks_as_top_level_items(item): + """同一个 Ark 专属块写成 content 会被拒、写成顶层 item 却直通请求体, + 等于绕过了 variant 守卫。两种形态必须一致拒绝。""" + with pytest.raises(ValueError): + normalize_responses_input(None, None, [dict(item)], provider="openai") + + +def test_ark_provider_allows_ark_only_blocks_as_top_level_items(): + """Ark 渠道本身要能发这些块,守卫不能把合法用法一并拦下。""" + converted = normalize_responses_input( + None, + None, + [{"type": "input_audio", "audio_url": "https://example.invalid/a.mp3"}], + provider="ark", + ) + + assert converted[0]["type"] == "input_audio" + + +# ── 回归:custom_tool_call 回填不能被强制 JSON 解析 ── + + +def test_responses_custom_tool_call_round_trip_preserves_free_form_input(): + """Responses custom tool 的 ``input`` 是任意文本(SDK 契约无 JSON 约束), + 强制 json.loads 会让多轮 custom tool 在请求边界直接失败。""" + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + { + "type": "custom_tool_call", + "id": "ctc_1", + "call_id": "call_abc", + "name": "my_custom_tool", + "arguments": "any free-form text, not JSON", + } + ], + } + ], + provider="openai", + ) + + assert converted[0]["type"] == "custom_tool_call" + assert converted[0]["input"] == "any free-form text, not JSON" + assert converted[0]["call_id"] == "call_abc" + + +def test_responses_custom_tool_call_accepts_empty_input(): + """空 input 是合法值(custom tool 无参数),不能被当成无效 JSON 拒绝。""" + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + {"type": "custom_tool_call", "call_id": "c", "name": "f", "arguments": ""} + ], + } + ], + provider="openai", + ) + + assert converted[0]["input"] == "" + + +def test_responses_function_call_still_normalizes_json_arguments(): + """function_call 必须继续做 JSON 规范化,不能被 custom 分支影响。""" + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + {"type": "function_call", "call_id": "c1", "name": "f", "arguments": '{"a":1}'} + ], + } + ], + provider="openai", + ) + + assert converted[0]["type"] == "function_call" + assert converted[0]["arguments"] == '{"a":1}' + + +def test_chat_messages_do_not_leak_responses_only_type_marker(): + """Chat Completions 路径不能带上 Responses 专属的内部字段,否则它会随 + 请求体发给 API。""" + normalized = normalize_chat_messages( + [ + { + "role": "assistant", + "content": "", + "tool_calls": [ + {"id": "c1", "type": "custom_tool_call", "name": "n", "input": '{"a":1}'} + ], + } + ] + ) + + assert normalized[0]["tool_calls"] == [ + {"id": "c1", "type": "function", "function": {"name": "n", "arguments": '{"a":1}'}} + ] + + +def test_responses_round_trip_keeps_call_id_for_streamed_tool_calls(): + """流式事件同时给出 ``id``(输出项 ID)与 ``call_id``(调用 ID),两者不同。 + + Responses 用 ``call_id`` 关联 ``function_call`` 与 ``function_call_output``, + 若 Chat 归一化只保留 ``id``,回填时输出项就配不上调用。 + """ + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "fc_0123", + "call_id": "call_abc", + "type": "function_call", + "name": "f", + "arguments": "{}", + } + ], + } + ], + provider="openai", + ) + + assert converted[0]["call_id"] == "call_abc" + assert converted[0]["id"] == "fc_0123" + + +def test_responses_function_call_output_pairs_with_preserved_call_id(): + """回填的 function_call 与 function_call_output 必须共享同一 call_id。""" + converted = normalize_responses_input( + None, + None, + [ + { + "role": "assistant", + "tool_calls": [ + { + "id": "fc_0123", + "call_id": "call_abc", + "type": "function_call", + "name": "f", + "arguments": "{}", + } + ], + }, + {"role": "tool", "tool_call_id": "call_abc", "content": "result"}, + ], + provider="openai", + ) + + assert converted[0]["call_id"] == converted[1]["call_id"] == "call_abc" diff --git a/tests/providers/test_security_boundaries.py b/tests/providers/test_security_boundaries.py new file mode 100644 index 0000000..9343d80 --- /dev/null +++ b/tests/providers/test_security_boundaries.py @@ -0,0 +1,189 @@ +"""凭证边界与脱敏的回归测试。""" + +import pytest + +from src.utils.security import ( + is_sensitive_option_key, + redact_sensitive_text, + validate_secret_free_options, +) + + +@pytest.mark.parametrize("key", ["ak", "sk", "AK", "SK", "account_key"]) +def test_security_recognizes_short_credential_keys(key): + """火山 ``ak``/``sk`` 与 Azure ``account_key`` 必须按凭证键处理。""" + assert is_sensitive_option_key(key) is True + + +@pytest.mark.parametrize("key", ["task", "risk", "ask", "mask", "disk", "sketch", "ak_region"]) +def test_security_does_not_flag_ordinary_keys_containing_ak_or_sk(key): + """短键必须精确匹配,普通业务字段不能被误判。""" + assert is_sensitive_option_key(key) is False + + +def test_security_rejects_volcengine_ak_sk_request_overrides(): + """把火山凭证塞进 extra_headers 必须被拒绝。""" + with pytest.raises(ValueError, match="凭证"): + validate_secret_free_options({"ak": "AKLTxxx", "sk": "SKxxx"}, "Volcengine") + + +@pytest.mark.parametrize( + "value", + [ + "ak=AKLTabcdefghijklmnop", + "sk=SKabcdefghijklmnop", + "account_key=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6I", + '{"ak": "AKLT1234567890", "sk": "SK1234567890"}', + ], +) +def test_security_redacts_short_credential_key_assignments(value): + redacted = redact_sensitive_text(value) + assert "[REDACTED]" in redacted + assert "AKLTabcdefghijklmnop" not in redacted + assert "SKabcdefghijklmnop" not in redacted + + +@pytest.mark.parametrize( + "value", + ["task = value", "risk=high", "ask me later", "disk=full", "mask=none"], +) +def test_security_does_not_redact_ordinary_text_with_ak_or_sk(value): + """普通文本里的 ``sk``/``ak`` 子串不能被当成凭证脱敏。""" + assert redact_sensitive_text(value) == value + + +@pytest.mark.parametrize( + "value", + [ + "Incorrect API key provided: sk-abc***...***xyz.", + "Invalid token: rk-abc*******************def", + "key sess-abcd****wxyz rejected", + ], +) +def test_security_redacts_masked_credential_shapes(value): + """服务端错误里「首尾可见、中间掩码」的凭证形态也要脱敏。 + + ``_OPENAI_KEY_RE`` 要求 ``sk-`` 后连续 8 个以上字母数字,星号会中断匹配, + 这类字符串此前会原样落进日志。 + """ + redacted = redact_sensitive_text(value) + assert "[REDACTED]" in redacted + assert "sk-abc" not in redacted + assert "rk-abc" not in redacted + assert "sess-abcd" not in redacted + + +# ── 回归:脱敏与配置边界必须使用同一套凭证键名 ── + +# 这些键在 is_sensitive_option_key 中返回 True,却曾因文本正则词表更小而在 +# 日志里明文输出(client_secret 是 Google OAuth 的常见形态)。 +_TEXT_REDACTION_KEYS = [ + "client_secret", + "clientsecret", + "private_key", + "privatekey", + "credentials", + "ssh_private_key", + "secret_access_key", + "encryption_key", + "webhook_secret", +] + + +@pytest.mark.parametrize("key", _TEXT_REDACTION_KEYS) +def test_security_redacts_every_sensitive_key_name_in_text(key): + """配置边界判为敏感的键名,在文本里也必须脱敏。""" + secret = "SUPERSECRETVALUE12345" + assert is_sensitive_option_key(key) is True + assert secret not in redact_sensitive_text(f"{key}={secret}") + + +@pytest.mark.parametrize("key", _TEXT_REDACTION_KEYS) +def test_security_redacts_sensitive_key_names_in_url_query(key): + """URL query 形态同样要覆盖,不能只处理 key=value。""" + secret = "SUPERSECRETVALUE12345" + assert secret not in redact_sensitive_text(f"https://example.invalid/x?{key}={secret}") + + +@pytest.mark.parametrize( + "text", + ["token usage is high", "task = value", "the secret sauce", "auth flow"], +) +def test_security_keeps_ordinary_text_intact(text): + """扩大键名词表后,普通文本不能被误脱敏。""" + assert redact_sensitive_text(text) == text + + +# ── 回归:掩码形态在 Bearer 前缀下也要完整脱敏 ── + + +@pytest.mark.parametrize( + "value", + [ + "Bearer sk-abc***...***xyz rejected", + "Authorization: Bearer rk-abcd****wxyz", + "Bearer sess-abcdef****tail", + "sk-abc***...***xyz", + "rk-abcd****wxyz", + ], +) +def test_security_redacts_masked_credentials_after_bearer_prefix(value): + """``_BEARER_TEXT_RE`` 会先吃掉 ``Bearer sk-abc`` 前缀,使掩码正则失配, + 导致尾部掩码片段残留。掩码规则必须优先于 Bearer 规则执行。""" + redacted = redact_sensitive_text(value) + assert "***" not in redacted + assert "****" not in redacted + + +# ── 回归:urlsplit 无法解析的值不能让凭证整体绕过边界 ── + + +def test_security_rejects_credential_hidden_behind_unparseable_url(): + """``urlsplit`` 对畸形 URL 抛 ValueError。若该分支直接 return,任何凭证 + 都能随一个畸形前缀整体绕过边界校验(这是叶子值的唯一检查入口)。""" + unparseable = "https://[::1 token sk-abcdefghijklmnopqrstuvwxyz0123456789" + with pytest.raises(ValueError, match="凭证"): + validate_secret_free_options({"note": unparseable}, "Provider") + + +def test_security_parseable_and_unparseable_urls_are_equally_strict(): + """同样藏凭证的两个值,只因 URL 可解析与否而一个被拒一个放行,即为漏洞。""" + secret = "token sk-abcdefghijklmnopqrstuvwxyz0123456789" + with pytest.raises(ValueError, match="凭证"): + validate_secret_free_options({"note": f"https://h/x {secret}"}, "Provider") + with pytest.raises(ValueError, match="凭证"): + validate_secret_free_options({"note": f"https://[::1 {secret}"}, "Provider") + + +# ── 回归:面向用户的异常输出必须脱敏 ── + + +def test_safe_exception_text_redacts_credentials(): + """SDK 异常常回显请求头或 URL,终端与日志是凭证最容易泄漏的出口。""" + from src.utils.security import safe_exception_text + + secret = "sk-abcdefghijklmnopqrstuvwxyz0123456789" + rendered = safe_exception_text(RuntimeError(f"auth failed for {secret}")) + + assert secret not in rendered + assert "RuntimeError" in rendered + + +def test_safe_exception_text_handles_empty_and_long_messages(): + from src.utils.security import safe_exception_text + + assert safe_exception_text(ValueError()) == "ValueError" + assert len(safe_exception_text(RuntimeError("x" * 500))) <= 260 + + +def test_user_facing_error_paths_do_not_print_raw_exceptions(): + """``retrieval_test`` 与顶层入口曾把裸异常交给 console/logger,凭证会 + 原样落到终端与日志;``chat`` 路径已做脱敏,三者必须一致。""" + import pathlib + + root = pathlib.Path(__file__).resolve().parents[2] + for relative in ("src/retrieval_test/core.py", "main.py", "src/chat/core.py"): + source = (root / relative).read_text(encoding="utf-8") + assert "safe_exception_text" in source, relative + assert 'console.print(f"[red]召回测试出错: {exc}' not in source, relative + assert '{e}")' not in source, relative diff --git a/tests/retrieval/test_parent_child_retrieval.py b/tests/retrieval/test_parent_child_retrieval.py index 899ef70..7652d89 100644 --- a/tests/retrieval/test_parent_child_retrieval.py +++ b/tests/retrieval/test_parent_child_retrieval.py @@ -1,6 +1,11 @@ +import asyncio +from types import SimpleNamespace from unittest.mock import MagicMock +import pytest + from src.retrieval.retriever import retrieve_documents +from src.services.retrieval_service import RetrievalService from src.utils.config import RetrievalMethod @@ -26,6 +31,213 @@ def resolve_parent_content(self, parent_id): return parent_document.get("content") +def test_hybrid_retrieval_starts_semantic_and_keyword_paths_in_parallel(): + service = object.__new__(RetrievalService) + started = [] + release = asyncio.Event() + + async def fake_semantic(_query, _top_k): + started.append("semantic") + await release.wait() + return ["semantic"] + + async def fake_keyword(_query, _top_k): + started.append("keyword") + await release.wait() + return ["keyword"] + + service._semantic_retrieve = fake_semantic + service._keyword_retrieve = fake_keyword + + async def run(): + task = asyncio.create_task(service._gather_hybrid_results("query", 3)) + async def wait_for_both_starts(): + while len(started) < 2: + await asyncio.sleep(0) + + await asyncio.wait_for(wait_for_both_starts(), timeout=5) + release.set() + return await asyncio.wait_for(task, timeout=5) + + assert asyncio.run(run()) == (["semantic"], ["keyword"]) + assert set(started) == {"semantic", "keyword"} + + +def test_rerank_provider_is_cached_per_configuration(monkeypatch): + service = RetrievalService(vector_store=object(), embedding_service=object()) + provider = MagicMock() + + async def fake_arerank(_query, _documents, top_n): + return [0], [float(top_n)] + + provider.arerank.side_effect = fake_arerank + factory = MagicMock(return_value=provider) + monkeypatch.setattr("src.services.retrieval_service.ModelProviderFactory.get_rerank_provider", factory) + session_config = SimpleNamespace( + rerank_enabled=True, + active_rerank_configuration="siliconflow", + top_k=1, + rerank_configurations={"siliconflow": SimpleNamespace(provider="siliconflow", model_name="rerank")}, + ) + documents = [{"page_content": "doc", "score": 0.5, "metadata": {}}] + + async def run(): + first = await service._rerank_if_needed("query", documents, session_config) + second = await service._rerank_if_needed("query", documents, session_config) + return first, second + + first, second = asyncio.run(run()) + + assert first[0]["score"] == 1.0 + assert second[0]["score"] == 1.0 + factory.assert_called_once_with("siliconflow", session_config.rerank_configurations) + assert provider.arerank.call_count == 2 + + +def test_rerank_provider_cache_refreshes_when_options_change(monkeypatch): + service = RetrievalService(vector_store=object(), embedding_service=object()) + first_provider = MagicMock() + second_provider = MagicMock() + factory = MagicMock(side_effect=[first_provider, second_provider]) + monkeypatch.setattr("src.services.retrieval_service.ModelProviderFactory.get_rerank_provider", factory) + first_config = SimpleNamespace( + provider="siliconflow", + model_name="rerank", + options={"timeout": 10}, + ) + second_config = SimpleNamespace( + provider="siliconflow", + model_name="rerank", + options={"timeout": 20}, + ) + + assert service._get_rerank_provider("siliconflow", {"siliconflow": first_config}) is first_provider + assert service._get_rerank_provider("siliconflow", {"siliconflow": second_config}) is second_provider + assert factory.call_count == 2 + + +def test_retrieval_service_close_disposes_cached_rerank_and_embedding_models(): + rerank = MagicMock() + embedding = MagicMock() + service = RetrievalService(vector_store=object(), embedding_service=embedding) + service._rerank_providers = {("key", "provider", "model"): rerank} + + service.close() + + rerank.close.assert_called_once_with() + embedding.close.assert_called_once_with() + assert service._rerank_providers == {} + + +def test_retrieval_service_aclose_falls_back_to_sync_embedding_close(): + class SyncOnlyEmbedding: + def __init__(self): + self.closed = False + + def close(self): + self.closed = True + + embedding = SyncOnlyEmbedding() + service = RetrievalService(vector_store=object(), embedding_service=embedding) + + asyncio.run(service.aclose()) + + assert embedding.closed + + +def test_rerank_top_n_is_clamped_after_candidate_reduction(monkeypatch): + service = RetrievalService(vector_store=object(), embedding_service=object()) + provider = MagicMock() + + async def fake_arerank(_query, _documents, top_n): + assert top_n == 1 + return [0], [0.9] + + provider.arerank.side_effect = fake_arerank + monkeypatch.setattr( + "src.services.retrieval_service.ModelProviderFactory.get_rerank_provider", + MagicMock(return_value=provider), + ) + session_config = SimpleNamespace( + rerank_enabled=True, + active_rerank_configuration="siliconflow", + top_k=5, + rerank_configurations={ + "siliconflow": SimpleNamespace(provider="siliconflow", model_name="rerank") + }, + ) + + result = asyncio.run( + service._rerank_if_needed( + "query", [{"page_content": "only", "score": 0.1, "metadata": {}}], session_config + ) + ) + + assert result[0]["score"] == 0.9 + + +def test_semantic_retrieve_uses_legacy_search_when_method_is_missing(): + class LegacyStore: + def search(self, query, top_k, search_type="semantic"): + return [{"page_content": query, "score": top_k, "metadata": {"search_type": search_type}}] + + class UnusedEmbedding: + async def embed_query(self, _query): + raise AssertionError("legacy semantic search should not require embeddings") + + embedding = UnusedEmbedding() + service = RetrievalService(LegacyStore(), embedding) + + result = asyncio.run(service._semantic_retrieve("query", 3)) + + assert result == [{"page_content": "query", "score": 3, "metadata": {"search_type": "semantic"}}] + + +def test_semantic_retrieve_does_not_hide_embedding_attribute_errors(): + class SemanticStore: + def semantic_search(self, _embedding, _top_k): + return [] + + class FailingEmbedding: + async def embed_query(self, _query): + raise AttributeError("embedding provider bug") + + service = RetrievalService(SemanticStore(), FailingEmbedding()) + + with pytest.raises(AttributeError, match="embedding provider bug"): + asyncio.run(service._semantic_retrieve("query", 3)) + + +def test_semantic_retrieve_does_not_hide_vector_store_attribute_errors(): + class BrokenStore: + def semantic_search(self, _embedding, _top_k): + raise AttributeError("vector store bug") + + class Embedding: + async def embed_query(self, _query): + return [0.1] + + service = RetrievalService(BrokenStore(), Embedding()) + + with pytest.raises(AttributeError, match="vector store bug"): + asyncio.run(service._semantic_retrieve("query", 3)) + + +@pytest.mark.parametrize( + ("indices", "scores", "message"), + [ + ([], [], "未返回可用结果"), + ([0], [], "数量不一致"), + ([2], [0.9], "index 无效"), + ([0, 0], [0.9, 0.8], "重复 index"), + ([0], [float("nan")], "必须是有限数值"), + ], +) +def test_rerank_output_contract_failures_are_explicit(indices, scores, message): + with pytest.raises(RuntimeError, match=message): + RetrievalService._validate_rerank_output(indices, scores, document_count=2) + + def test_retrieve_documents_uses_parent_sidecar_and_overfetches_candidates(monkeypatch): semantic_results = [ { @@ -101,7 +313,7 @@ def test_retrieve_documents_uses_parent_sidecar_and_overfetches_candidates(monke candidate_multiplier=3, ) - assert store.search_calls == [("semantic", 6), ("keyword", 6)] + assert sorted(store.search_calls) == [("keyword", 6), ("semantic", 6)] assert len(results) == 2 assert {doc["metadata"]["parent_id"] for doc in results} == {"parent-1", "parent-2"} assert all(doc["page_content"].startswith("parent content") for doc in results) diff --git a/tests/retrieval/vdb/test_factory.py b/tests/retrieval/vdb/test_factory.py index 6cc014b..f74c339 100644 --- a/tests/retrieval/vdb/test_factory.py +++ b/tests/retrieval/vdb/test_factory.py @@ -1,12 +1,13 @@ -import pytest -from unittest.mock import MagicMock, patch -from typing import Any, List import sys +from typing import Any +from unittest.mock import MagicMock, patch + +import pytest -from src.retrieval.vdb.factory import VectorStoreFactory from src.retrieval.vdb.base import VectorStoreBase -from src.utils.config import Settings, get_settings # 导入 Settings 和 get_settings -from src.retrieval.vdb.faiss_store import FaissStore # 导入 FaissStore +from src.retrieval.vdb.factory import VectorStoreFactory +from src.utils.config import Settings + # Mock FaissStore 类 class MockFaissStore(VectorStoreBase): @@ -15,25 +16,27 @@ def __init__(self, file_path: str | None): self.documents = [] self.embeddings = None - def add_documents(self, documents: List[dict[str, Any]]): + def add_documents(self, documents: list[dict[str, Any]]): self.documents.extend(documents) - def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[dict[str, Any]]: + def search(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: return [{"page_content": f"mock_doc_{i}", "metadata": {"source": "mock"}} for i in range(top_k)] - async def aadd_documents(self, documents: List[dict[str, Any]]): + async def aadd_documents(self, documents: list[dict[str, Any]]): self.documents.extend(documents) - async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> List[dict[str, Any]]: + async def asearch(self, query: str, top_k: int = 5, search_type: str = "semantic") -> list[dict[str, Any]]: return [{"page_content": f"mock_doc_{i}", "metadata": {"source": "mock"}} for i in range(top_k)] def save(self, path: str): """模拟保存操作""" - pass def load(self, path: str): """模拟加载操作""" - pass + + def load_snapshot(self, snapshot_dir: str): + """模拟从快照目录加载,记录路径供断言。""" + self.file_path = snapshot_dir def get_embedding_model(self) -> Any: """模拟获取嵌入模型""" @@ -80,7 +83,9 @@ def patch_settings(monkeypatch, tmp_path): # 重新导入 VectorStoreFactory,确保它加载的是最新的版本 if 'src.retrieval.vdb.factory' in sys.modules: del sys.modules['src.retrieval.vdb.factory'] - from src.retrieval.vdb.factory import VectorStoreFactory as ReloadedVectorStoreFactory + from src.retrieval.vdb.factory import ( + VectorStoreFactory as ReloadedVectorStoreFactory, + ) # 直接模拟 VectorStoreFactory.get_vector_store 方法 def mock_get_vector_store(store_type: str, file_path: str | None = None) -> VectorStoreBase: @@ -119,3 +124,66 @@ def test_get_default_vector_store_without_loading_existing(): store = VectorStoreFactory.get_default_vector_store(load_existing=False) assert isinstance(store, MockFaissStore) assert store.file_path is None + +def _write_snapshot(root, snapshot_id, provider, model): + """按真实快照目录结构写入一个活动快照,供兼容性检测使用。""" + from pathlib import Path + + from src.runtime.contracts import KnowledgeSnapshotManifest + + snapshot_dir = Path(root) / snapshot_id + snapshot_dir.mkdir(parents=True, exist_ok=True) + manifest = KnowledgeSnapshotManifest.create( + snapshot_id=snapshot_id, + store_type="faiss", + embedding_provider=provider, + embedding_model=model, + chunk_mode="standard", + source_digest="test-digest", + document_count=1, + chunk_count=1, + ) + (snapshot_dir / "manifest.toml").write_text(manifest.to_toml(), encoding="utf-8") + # 快照目录校验要求这一组文件同时存在;兼容性检测发生在 load_snapshot 之前, + # 所以这里只需占位内容,不参与断言。 + for name in ("chunks.pkl", "parents.pkl", "semantic.index", "embeddings.npy", "stats.json"): + (snapshot_dir / name).write_bytes(b"") + (Path(root) / "ACTIVE_SNAPSHOT").write_text(snapshot_id, encoding="utf-8") + return snapshot_dir + + +def test_default_vector_store_rejects_mismatched_embedding_provider(tmp_path): + """活动快照的 embedding provider 与当前运行配置不同时必须显式失败。 + + 向量空间不兼容时静默加载会产出错误的检索结果,因此这里要求抛出 + 带快照值与当前值的 RuntimeError,而不是继续使用旧索引。 + """ + from src.utils.config import get_settings + + settings = get_settings() + _write_snapshot(settings.snapshot_root, "kb-mismatch", "openai", "text-embedding-3-large") + + with pytest.raises(RuntimeError, match="embedding 配置与当前运行配置不一致"): + VectorStoreFactory.get_default_vector_store() + + +def test_default_vector_store_rejects_mismatched_embedding_model(tmp_path): + """provider 相同但模型名不同同样不兼容,也必须拦截。""" + from src.utils.config import get_settings + + settings = get_settings() + _write_snapshot(settings.snapshot_root, "kb-model-mismatch", "local-hash", "local-hash-512") + + with pytest.raises(RuntimeError, match="embedding 配置与当前运行配置不一致"): + VectorStoreFactory.get_default_vector_store() + + +def test_default_vector_store_accepts_matching_embedding(tmp_path): + """配置一致时正常加载,不应触发兼容性错误。""" + from src.utils.config import get_settings + + settings = get_settings() + _write_snapshot(settings.snapshot_root, "kb-match", "local-hash", "local-hash-256") + + store = VectorStoreFactory.get_default_vector_store() + assert isinstance(store, MockFaissStore) diff --git a/tests/retrieval/vdb/test_faiss_sidecar_roundtrip.py b/tests/retrieval/vdb/test_faiss_sidecar_roundtrip.py index 67e1486..329108c 100644 --- a/tests/retrieval/vdb/test_faiss_sidecar_roundtrip.py +++ b/tests/retrieval/vdb/test_faiss_sidecar_roundtrip.py @@ -1,8 +1,18 @@ import numpy as np +import pytest from src.retrieval.vdb.faiss_store import FaissStore +def test_faiss_store_rejects_snapshot_without_embeddings_before_writing_npy(tmp_path): + store = FaissStore(file_path=None) + + with pytest.raises(RuntimeError, match="语义索引为空"): + store.save_snapshot(str(tmp_path / "snapshot")) + + assert not (tmp_path / "snapshot" / "embeddings.npy").exists() + + def test_faiss_store_persists_parent_sidecar_snapshot_roundtrip(tmp_path): snapshot_dir = tmp_path / "snapshot" store = FaissStore(file_path=None) @@ -34,3 +44,13 @@ def test_faiss_store_persists_parent_sidecar_snapshot_roundtrip(tmp_path): assert reloaded.resolve_parent_content("parent-1") == "parent content" assert reloaded.parent_documents["parent-1"]["metadata"]["source"] == "kb.md" assert reloaded.documents[0]["metadata"]["parent_id"] == "parent-1" + + +def test_faiss_store_rejects_query_dimension_mismatch(): + store = FaissStore(file_path=None) + store.documents = [{"page_content": "doc", "metadata": {}}] + store.embeddings = np.array([[0.0, 0.0]], dtype=np.float32) + store._rebuild_indices() + + with pytest.raises(ValueError, match="维度"): + store.semantic_search([0.0, 0.0, 0.0]) diff --git a/tests/retrieval/vdb/test_snapshot_repository.py b/tests/retrieval/vdb/test_snapshot_repository.py index c70d062..32cad76 100644 --- a/tests/retrieval/vdb/test_snapshot_repository.py +++ b/tests/retrieval/vdb/test_snapshot_repository.py @@ -39,3 +39,19 @@ def test_snapshot_repository_writes_and_loads_manifest(tmp_path, monkeypatch): assert loaded_manifest.snapshot_id == snapshot_id assert loaded_manifest.embedding_model == "local-hash-256" get_settings.cache_clear() + + +def test_snapshot_repository_cleans_only_requested_temp_dir(tmp_path): + run_config = type("RunConfig", (), {"snapshot_root": tmp_path})() + repository = SnapshotRepository(run_config) + snapshot_id = "kb-cleanup" + temp_dir = repository.create_temp_snapshot_dir(snapshot_id) + (temp_dir / "partial.bin").write_bytes(b"partial") + final_dir = tmp_path / "kb-existing" + final_dir.mkdir() + (final_dir / "keep.bin").write_bytes(b"keep") + + repository.cleanup_temp_snapshot_dir(snapshot_id) + + assert not temp_dir.exists() + assert (final_dir / "keep.bin").exists() diff --git a/tests/services/test_chat_service.py b/tests/services/test_chat_service.py new file mode 100644 index 0000000..5852c59 --- /dev/null +++ b/tests/services/test_chat_service.py @@ -0,0 +1,127 @@ +import asyncio + +import pytest + +from src.services.chat_service import ChatService + + +class _FailingModel: + async def ainvoke(self, **_kwargs): + raise AttributeError("provider response bug") + yield "" + + +class _EmptyModel: + async def ainvoke(self, **_kwargs): + if False: + yield "" + + +class _ModernAnthropicModel: + def __init__(self): + self.calls = [] + + def supports_sampling_option(self, name): + assert name == "temperature" + return False + + async def ainvoke(self, **kwargs): + self.calls.append(kwargs) + yield "检索" + + +class _ResponsesReasoningModel: + def __init__(self): + self.calls = [] + + async def ainvoke(self, **kwargs): + self.calls.append(kwargs) + yield "检索" + + +class _SamplingModel(_ResponsesReasoningModel): + def supports_sampling_option(self, name): + assert name == "temperature" + return True + + +def test_identify_intent_propagates_provider_failure(): + service = ChatService(_FailingModel(), retrieval_service=object()) + + with pytest.raises(RuntimeError, match="意图识别失败"): + asyncio.run(service.identify_intent("问题")) + + +def test_identify_intent_rejects_empty_provider_response(): + service = ChatService(_EmptyModel(), retrieval_service=object()) + + with pytest.raises(RuntimeError, match="返回空结果"): + asyncio.run(service.identify_intent("问题")) + + +def test_identify_intent_omits_sampling_for_models_that_reject_temperature(): + model = _ModernAnthropicModel() + service = ChatService(model, retrieval_service=object()) + + assert asyncio.run(service.identify_intent("问题")) == "检索" + assert model.calls == [{"prompt": model.calls[0]["prompt"], "stream": False}] + + +def test_identify_intent_omits_sampling_without_capability_declaration(): + model = _ResponsesReasoningModel() + service = ChatService(model, retrieval_service=object()) + + assert asyncio.run(service.identify_intent("问题")) == "检索" + assert "temperature" not in model.calls[0] + + +def test_generate_reply_omits_sampling_without_capability_declaration(): + model = _ResponsesReasoningModel() + service = ChatService(model, retrieval_service=object()) + + async def consume(): + return [ + chunk + async for chunk in service.generate_reply( + "问题", + "意图", + [], + type("SessionConfig", (), {"chat_temperature": 0.7})(), + ) + ] + + assert asyncio.run(consume()) == ["检索"] + assert "temperature" not in model.calls[0] + + +def test_chat_service_passes_sampling_when_capability_is_declared(): + model = _SamplingModel() + service = ChatService(model, retrieval_service=object()) + + assert asyncio.run(service.identify_intent("问题")) == "检索" + assert model.calls[0]["temperature"] == 0.1 + + async def consume(): + return [ + chunk + async for chunk in service.generate_reply( + "问题", + "意图", + [], + type("SessionConfig", (), {"chat_temperature": 0.4})(), + ) + ] + + assert asyncio.run(consume()) == ["检索"] + assert model.calls[1]["temperature"] == 0.4 + + +def test_build_prompt_handles_documents_without_source_metadata(): + prompt = ChatService._build_prompt( + "问题", + "意图", + [{"page_content": "知识内容"}], + ) + + assert "来源: 未知来源" in prompt + assert "内容: 知识内容" in prompt diff --git a/tests/services/test_embedding_service.py b/tests/services/test_embedding_service.py new file mode 100644 index 0000000..4ecaf5b --- /dev/null +++ b/tests/services/test_embedding_service.py @@ -0,0 +1,181 @@ +import asyncio +from types import SimpleNamespace +from unittest.mock import AsyncMock, MagicMock + +import numpy as np +import pytest + +from src.providers.openai_compatible import OpenAICompatibleProvider +from src.services.embedding_service import EmbeddingService +from src.utils.config import ModelDetail + + +def test_embedding_service_passes_runtime_configurations_to_factory(monkeypatch): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + factory = MagicMock(return_value=provider) + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + factory, + ) + + service = EmbeddingService(run_config) + + assert service._get_model() is provider + factory.assert_called_once_with("custom", run_config.embedding_configurations) + + +def test_embedding_service_rejects_vectors_that_do_not_match_input_count(monkeypatch): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + provider.aembed_documents = AsyncMock(return_value=[[1.0, 2.0]]) + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + lambda *_args: provider, + ) + + service = EmbeddingService(run_config) + + with pytest.raises(ValueError, match="向量数量与输入文本数量不一致"): + asyncio.run(service.embed_texts(["one", "two"])) + + +@pytest.mark.parametrize("value", [[], [[1.0, 2.0], [3.0, 4.0]], 1.0]) +def test_embedding_service_rejects_malformed_query_vectors(monkeypatch, value): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + provider.aembed_query = AsyncMock(return_value=value) + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + lambda *_args: provider, + ) + + with pytest.raises((TypeError, ValueError), match="向量|float"): + asyncio.run(EmbeddingService(run_config).embed_query("one")) + + +def test_embedding_service_does_not_replay_model_options(monkeypatch): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail( + provider="openai", + model_name="text-embedding-3-small", + options={"dimensions": 512, "encoding_format": "float"}, + ) + }, + ) + provider = MagicMock() + provider.aembed_documents = AsyncMock(return_value=[[1.0, 2.0]]) + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + lambda *_args: provider, + ) + + service = EmbeddingService(run_config) + asyncio.run(service.embed_texts(["one"])) + + provider.aembed_documents.assert_awaited_once_with(["one"]) + + +@pytest.mark.parametrize("method", ["embed_texts", "embed_query", "embed_in_batches"]) +@pytest.mark.parametrize("options", [ + {"dimensions": 2}, + {"encoding_format": "float"}, + {"user": "embedding-test"}, + {"extra_body": {"vendor_flag": True}}, +]) +def test_embedding_service_model_options_reach_sdk_once(monkeypatch, method, options): + requests = [] + + class Embeddings: + async def create(self, **request): + requests.append(request) + return SimpleNamespace( + data=[SimpleNamespace(embedding=[1.0, 2.0]) for _ in request["input"]] + ) + + provider = object.__new__(OpenAICompatibleProvider) + provider._provider = "openai" + provider._model_name = "text-embedding-3-small" + provider._options = options + provider._get_aclient = lambda: SimpleNamespace(embeddings=Embeddings()) + + monkeypatch.setattr( + "src.services.embedding_service.ModelProviderFactory.get_embedding_provider", + lambda *_args: provider, + ) + service = EmbeddingService(SimpleNamespace( + default_embedding_provider="custom", kb_embedding_batch_size=1, + embedding_configurations={"custom": ModelDetail( + provider="openai", model_name="text-embedding-3-small", options=options, + )}, + )) + value = "one" if method == "embed_query" else ["one", "two"] + result = asyncio.run(getattr(service, method)(value)) + assert result.dtype == np.float32 + assert result.shape == ((2,) if method == "embed_query" else (2, 2)) + + assert len(requests) == (2 if method == "embed_in_batches" else 1) + for payload in requests: + for key, value in options.items(): + if key == "extra_body": + assert payload["extra_body"] == {"vendor_flag": True} + else: + assert payload[key] == value + + +def test_embedding_service_close_disposes_cached_model(): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + service = EmbeddingService(run_config) + service._embedding_model = provider + + service.close() + + provider.close.assert_called_once_with() + assert service._embedding_model is None + + +def test_embedding_service_aclose_disposes_cached_model_async(): + run_config = SimpleNamespace( + default_embedding_provider="custom", + embedding_configurations={ + "custom": ModelDetail(provider="local-hash", model_name="local-hash-32") + }, + ) + provider = MagicMock() + closed = False + + async def aclose(): + nonlocal closed + closed = True + + provider.aclose = aclose + service = EmbeddingService(run_config) + service._embedding_model = provider + + asyncio.run(service.aclose()) + + assert closed + assert service._embedding_model is None diff --git a/tests/services/test_knowledge_build_service.py b/tests/services/test_knowledge_build_service.py new file mode 100644 index 0000000..6a74550 --- /dev/null +++ b/tests/services/test_knowledge_build_service.py @@ -0,0 +1,72 @@ +import asyncio +from pathlib import Path +from types import SimpleNamespace + +import pytest + +from src.retrieval.snapshot_repository import SnapshotRepository +from src.services.knowledge_build_service import KnowledgeBuildService + + +class _RunConfig: + def __init__(self, knowledge_base_path: Path, snapshot_root: Path): + self.knowledge_base_path = knowledge_base_path + self.snapshot_root = snapshot_root + self.default_vector_store = "faiss" + + @property + def active_snapshot_marker(self) -> Path: + return self.snapshot_root / "ACTIVE_SNAPSHOT" + + +class _IncompleteVectorStore: + def save_snapshot(self, snapshot_dir: str) -> None: + (Path(snapshot_dir) / "chunks.pkl").write_bytes(b"partial") + + def upsert_embeddings(self, documents, embeddings) -> None: + raise AssertionError("空文档不应写入 embedding") + + +class _EmptyPipeline: + def process(self, document): + return [] + + +def test_build_validates_temp_snapshot_before_activation(tmp_path, monkeypatch): + knowledge_base = tmp_path / "knowledge_base" + knowledge_base.mkdir() + (knowledge_base / "article.md").write_text("内容", encoding="utf-8") + + snapshot_root = tmp_path / "snapshots" + snapshot_root.mkdir() + previous_snapshot = snapshot_root / "previous" + previous_snapshot.mkdir() + (previous_snapshot / "keep.txt").write_text("保留", encoding="utf-8") + (snapshot_root / "ACTIVE_SNAPSHOT").write_text("previous", encoding="utf-8") + + run_config = _RunConfig(knowledge_base, snapshot_root) + repository = SnapshotRepository(run_config) + monkeypatch.setattr(repository, "generate_snapshot_id", lambda: "kb-new") + monkeypatch.setattr( + "src.services.knowledge_build_service.Pipeline.from_file_path", + lambda *args, **kwargs: _EmptyPipeline(), + ) + + embedding_service = SimpleNamespace( + embedding_provider_key="local-hash", + embedding_model_detail=SimpleNamespace(model_name="local-hash-32"), + ) + service = KnowledgeBuildService( + run_config, + _IncompleteVectorStore(), + embedding_service, + repository, + ) + + with pytest.raises(FileNotFoundError, match="知识快照不完整"): + asyncio.run(service.build("standard")) + + assert (snapshot_root / "ACTIVE_SNAPSHOT").read_text(encoding="utf-8") == "previous" + assert (previous_snapshot / "keep.txt").read_text(encoding="utf-8") == "保留" + assert not (snapshot_root / "kb-new").exists() + assert not (snapshot_root / ".tmp-kb-new").exists() diff --git a/tests/test_config.py b/tests/test_config.py index bfdbef0..9635d4d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -1,11 +1,16 @@ -# -*- coding: utf-8 -*- import os import pytest from pydantic import ValidationError -from src.utils.config import RetrievalMethod, Settings, get_settings, resolve_app_root - +from src.utils.config import ( + ModelDetail, + ModelProtocol, + RetrievalMethod, + Settings, + get_settings, + resolve_app_root, +) MOCK_TOML_CONTENT = """ log_level = "INFO" @@ -16,6 +21,10 @@ retrieval_candidate_multiplier = 4 kb_child_chunk_size = 180 kb_child_chunk_overlap = 18 +google_genai_use_vertexai = true +gemini_api_key = "toml-gemini-key" +google_cloud_project = "toml-project" +google_cloud_location = "europe-west4" [embedding_configurations.google] provider = "google" @@ -24,6 +33,8 @@ [llm_configurations.demo] provider = "openai" model_name = "demo-model" +protocol = "responses" +options = { top_p = 0.8, response_format = { type = "json_object" } } """ @@ -59,6 +70,12 @@ def test_settings_model_validation(): with pytest.raises(ValidationError): Settings(chat_retrieval_method="UNKNOWN_METHOD") # type: ignore[arg-type] + with pytest.raises(ValidationError, match="chat_top_k"): + Settings(chat_top_k=0) + + with pytest.raises(ValidationError, match="chat_score_threshold"): + Settings(chat_score_threshold=1.1) + def test_settings_splitter_separators_empty_string_falls_back_to_default(): settings = Settings(kb_splitter_separators="") @@ -70,8 +87,11 @@ def test_settings_splitter_separators_blank_csv_falls_back_to_default(): assert settings.kb_splitter_separators == ["###"] -def test_settings_defaults(): - settings = get_settings() +def test_settings_defaults(monkeypatch, tmp_path): + # 直接构造,避免读取开发者本机的 config.toml:这些断言应验证模型内置默认值, + # 而不是某个本地文件恰好写了什么。 + monkeypatch.setattr("src.utils.config.CONFIG_TOML_PATH", tmp_path / "absent.toml") + settings = Settings(_env_file=None) assert settings.openai_api_key is None assert settings.chat_top_k == 5 @@ -84,7 +104,41 @@ def test_settings_defaults(): assert settings.kb_child_chunk_overlap == 30 assert settings.default_embedding_provider == "local-hash" assert settings.embedding_configurations["local-hash"].model_name == "local-hash-256" - assert settings.llm_configurations["iflow-qwen3-max"].model_name == "qwen3-max" + assert settings.qwen_base_url.endswith("/compatible-mode/v1") + assert settings.volc_base_url.endswith("/api/v3") + assert "jina" not in settings.embedding_configurations + + +def test_settings_builtin_model_ids_are_current(monkeypatch, tmp_path): + """兜底默认值必须是当前有效的官方 ID,避免新用户照抄到已失效模型。""" + monkeypatch.setattr("src.utils.config.CONFIG_TOML_PATH", tmp_path / "absent.toml") + settings = Settings(_env_file=None) + + # Google 的裸名 ``embedding-001`` 不是有效 ID;退役的 ``text-embedding-004`` + # 也不能作为兜底值。 + assert settings.embedding_configurations["google"].model_name == "gemini-embedding-2" + # SiliconFlow 官方模型广场使用 ``BAAI/`` 命名空间,不存在 ``alibaba/`` 前缀; + # 现行 rerank ID 是 ``bge-reranker-v2-m3``,没有 ``bge-reranker-large``。 + assert settings.embedding_configurations["siliconflow"].model_name == "BAAI/bge-large-zh-v1.5" + assert settings.rerank_configurations["siliconflow"].model_name == "BAAI/bge-reranker-v2-m3" + # Ollama 官方库的现行 tag 是带次版本号的 ``llama3.1``。 + assert settings.llm_configurations["ollama"].model_name == "llama3.1" + + +def test_settings_dump_hides_credentials_by_default(): + settings = Settings(openai_api_key="sk-test", lm_studio_api_key="local-secret") + + assert "openai_api_key" not in settings.model_dump() + assert "lm_studio_api_key" not in settings.model_dump_json() + assert settings.model_dump(include_secrets=True)["openai_api_key"] == "sk-test" + + +def test_settings_supports_gemini_api_key_alias_without_exposing_it_by_default(): + settings = Settings(gemini_api_key="gemini-test") + + assert settings.gemini_api_key == "gemini-test" + assert "gemini_api_key" not in settings.model_dump() + assert settings.model_dump(include_secrets=True)["gemini_api_key"] == "gemini-test" def test_settings_from_toml(tmp_path): @@ -101,19 +155,100 @@ def test_settings_from_toml(tmp_path): assert settings.retrieval_candidate_multiplier == 4 assert settings.kb_child_chunk_size == 180 assert settings.kb_child_chunk_overlap == 18 + assert settings.google_genai_use_vertexai is True + assert settings.gemini_api_key == "toml-gemini-key" + assert settings.google_cloud_project == "toml-project" + assert settings.google_cloud_location == "europe-west4" assert settings.embedding_configurations["google"].model_name == "toml-embedding-model" assert settings.llm_configurations["demo"].provider == "openai" assert settings.llm_configurations["demo"].model_name == "demo-model" + assert settings.llm_configurations["demo"].protocol == ModelProtocol.RESPONSES + assert settings.llm_configurations["demo"].options["top_p"] == 0.8 + + +def test_model_protocol_aliases_and_invalid_values(): + assert ModelDetail(provider="openai", model_name="gpt-test", protocol="response").protocol == ModelProtocol.RESPONSES + + with pytest.raises(ValidationError, match="不支持的模型协议"): + ModelDetail(provider="openai", model_name="gpt-test", protocol="unknown") + + +@pytest.mark.parametrize("field_name", ["embedding_configurations", "rerank_configurations"]) +def test_settings_rejects_protocol_on_non_llm_configuration(field_name): + with pytest.raises(ValidationError, match="配置不支持 protocol"): + Settings( + **{ + field_name: { + "demo": { + "provider": "openai", + "model_name": "demo-model", + "protocol": "responses", + } + } + } + ) + + +@pytest.mark.parametrize("sensitive_key", [ + "X-API-Key", "X-Auth-Token", "Authorization", "access_token", "api_key", "Bearer", +]) +def test_model_options_reject_credentials_nested_in_headers_or_query(sensitive_key): + with pytest.raises(ValueError, match="凭证"): + ModelDetail( + provider="openai", + model_name="gpt-test", + options={"default_headers": {sensitive_key: "secret"}}, + ) + + with pytest.raises(ValueError, match="凭证"): + ModelDetail( + provider="openai", + model_name="gpt-test", + options={"default_query": {sensitive_key: "secret"}}, + ) + + +@pytest.mark.parametrize("container", [ + "headers", "default_headers", "extra_headers", + "query", "default_query", "extra_query", "http_options", "header", "params", + "http_client", "httpx_client", "httpx_async_client", "aiohttp_client", +]) +def test_model_options_reject_request_header_and_query_containers(container): + with pytest.raises(ValueError, match="凭证或连接字段"): + ModelDetail( + provider="openai", + model_name="gpt-test", + options={container: {"X-Trace-ID": "trace"}}, + ) + + +@pytest.mark.parametrize("connection_key", ["base_url", "client_args", "async_client_args"]) +def test_model_options_reject_connection_overrides(connection_key): + with pytest.raises(ValueError, match="凭证或连接字段"): + ModelDetail( + provider="openai", + model_name="gpt-test", + options={connection_key: "https://proxy.invalid"}, + ) def test_settings_from_dotenv(tmp_path): dotenv_path = tmp_path / ".env" - dotenv_path.write_text('OPENAI_API_KEY="dotenv_key"\nCHAT_TOP_K=15', encoding="utf-8") + dotenv_path.write_text( + 'OPENAI_API_KEY="dotenv_key"\nCHAT_TOP_K=15\n' + 'GOOGLE_GENAI_USE_VERTEXAI=true\n' + 'GOOGLE_CLOUD_PROJECT="dotenv-project"\n' + 'GOOGLE_CLOUD_LOCATION="asia-east1"\n', + encoding="utf-8", + ) settings = get_settings() assert settings.openai_api_key == "dotenv_key" assert settings.chat_top_k == 15 + assert settings.google_genai_use_vertexai is True + assert settings.google_cloud_project == "dotenv-project" + assert settings.google_cloud_location == "asia-east1" def test_settings_from_env_vars(monkeypatch): @@ -203,3 +338,41 @@ def test_resolve_app_root_frozen_mode(monkeypatch, tmp_path): root = resolve_app_root() assert root == executable.parent + + +def test_settings_warns_on_retired_qwen_base_url(): + """旧 Qwen 端点会 404,加载配置时必须给出显式升级提示。""" + with pytest.warns(UserWarning, match="qwen_base_url"): + Settings.model_validate( + { + "qwen_base_url": "https://dashscope.aliyuncs.com/api/v1", + "volc_base_url": "https://ark.cn-beijing.volces.com/api/v3", + } + ) + + +def test_settings_warns_on_retired_volc_base_url(): + with pytest.warns(UserWarning, match="volc_base_url"): + Settings.model_validate( + { + "qwen_base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1", + "volc_base_url": "https://maas-api.ml-platform-cn-beijing.volces.com", + } + ) + + +def test_settings_accepts_current_and_custom_base_urls(): + """新默认值以及自建/代理端点都不应触发升级警告。""" + import warnings as warnings_module + + for qwen, volc in [ + ( + "https://dashscope.aliyuncs.com/compatible-mode/v1", + "https://ark.cn-beijing.volces.com/api/v3", + ), + ("https://my-proxy.internal/v1", "https://my-proxy.internal/v3"), + ]: + with warnings_module.catch_warnings(record=True) as caught: + warnings_module.simplefilter("always") + Settings.model_validate({"qwen_base_url": qwen, "volc_base_url": volc}) + assert not [w for w in caught if "base_url" in str(w.message)] diff --git a/tests/test_main_entry.py b/tests/test_main_entry.py index ca2d0aa..0411d9d 100644 --- a/tests/test_main_entry.py +++ b/tests/test_main_entry.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import main diff --git a/tests/test_release_scripts.py b/tests/test_release_scripts.py index 3d7deb8..d23d96d 100644 --- a/tests/test_release_scripts.py +++ b/tests/test_release_scripts.py @@ -1,8 +1,12 @@ -# -*- coding: utf-8 -*- import subprocess -from pathlib import Path -from scripts.build_binary_release import prepare_runtime_layout, validate_bundle +import pytest + +from scripts.build_binary_release import ( + prepare_runtime_layout, + validate_bundle, + validate_target_environment, +) from scripts.extract_release_notes import extract_section @@ -17,10 +21,10 @@ def test_prepare_runtime_layout_creates_placeholder_directories(tmp_path): def test_extract_section_returns_version_content(): - section = extract_section("1.3.0") + section = extract_section("1.4.0") - assert section.startswith("## [1.3.0]") - assert "运行与配置" in section + assert section.startswith("## [1.4.0]") + assert "Provider 与协议" in section def test_validate_bundle_uses_smoke_test(monkeypatch, tmp_path): @@ -35,7 +39,7 @@ def test_validate_bundle_uses_smoke_test(monkeypatch, tmp_path): def fake_run(command, **kwargs): recorded["command"] = command recorded["kwargs"] = kwargs - return subprocess.CompletedProcess(command, 0, stdout="PyRAG-Kit 1.3.0 smoke test ok\n", stderr="") + return subprocess.CompletedProcess(command, 0, stdout="PyRAG-Kit 1.4.0 smoke test ok\n", stderr="") monkeypatch.setattr("scripts.build_binary_release.subprocess.run", fake_run) @@ -43,3 +47,22 @@ def fake_run(command, **kwargs): assert recorded["command"][-1] == "--smoke-test" assert recorded["kwargs"]["check"] is True + + +@pytest.mark.parametrize( + ("target", "system", "machine"), + [ + ("macos-x64", "Darwin", "x86_64"), + ("macos-arm64", "Darwin", "arm64"), + ("linux-x64", "Linux", "amd64"), + ("linux-arm64", "Linux", "aarch64"), + ("windows-x64", "Windows", "AMD64"), + ], +) +def test_validate_target_environment_accepts_matching_host(target, system, machine): + validate_target_environment(target, system=system, machine=machine) + + +def test_validate_target_environment_rejects_cross_architecture(): + with pytest.raises(RuntimeError, match="不执行跨平台交叉编译"): + validate_target_environment("macos-arm64", system="Darwin", machine="x86_64") diff --git a/uv.lock b/uv.lock index 3f49626..971d95b 100644 --- a/uv.lock +++ b/uv.lock @@ -2,29 +2,25 @@ version = 1 revision = 3 requires-python = ">=3.11" resolution-markers = [ - "python_full_version >= '3.14' and sys_platform == 'win32'", - "python_full_version >= '3.14' and sys_platform == 'emscripten'", - "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'win32'", - "python_full_version == '3.13.*' and sys_platform == 'emscripten'", - "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", - "python_full_version < '3.13' and sys_platform == 'win32'", - "python_full_version < '3.13' and sys_platform == 'emscripten'", - "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", + "python_full_version < '3.12'", ] [[package]] name = "aiohappyeyeballs" -version = "2.6.1" +version = "2.7.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, upload-time = "2025-03-12T01:42:48.764Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/f4/eec0465c2f67b2664688d0240b3212d5196fd89e741df67ddb81f8d35658/aiohappyeyeballs-2.7.1.tar.gz", hash = "sha256:065665c041c42a5938ed220bdcd7230f22527fbec085e1853d2402c8a3615d9d", size = 24757, upload-time = "2026-07-01T17:11:55.501Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, + { url = "https://files.pythonhosted.org/packages/71/43/1947f06babed6b3f1d7f38b0c767f52df66bfb2bc10b468c4a7de9eceff2/aiohappyeyeballs-2.7.1-py3-none-any.whl", hash = "sha256:9243213661e29250eb41368e5daa826fc017156c3b8a11440826b2e3ed376472", size = 15038, upload-time = "2026-07-01T17:11:54.055Z" }, ] [[package]] name = "aiohttp" -version = "3.13.3" +version = "3.14.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -33,95 +29,111 @@ dependencies = [ { name = "frozenlist" }, { name = "multidict" }, { name = "propcache" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/50/42/32cf8e7704ceb4481406eb87161349abb46a57fee3f008ba9cb610968646/aiohttp-3.13.3.tar.gz", hash = "sha256:a949eee43d3782f2daae4f4a2819b2cb9b0c5d3b7f7a927067cc84dafdbb9f88", size = 7844556, upload-time = "2026-01-03T17:33:05.204Z" } -wheels = [ - { 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, upload-time = "2026-01-03T17:29:43.287Z" }, - { 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, upload-time = "2026-01-03T17:29:44.822Z" }, - { 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, upload-time = "2026-01-03T17:29:46.405Z" }, - { 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, upload-time = "2026-01-03T17:29:48.083Z" }, - { 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, upload-time = "2026-01-03T17:29:49.648Z" }, - { 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, upload-time = "2026-01-03T17:29:51.244Z" }, - { 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, upload-time = "2026-01-03T17:29:53.938Z" }, - { 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, upload-time = "2026-01-03T17:29:55.484Z" }, - { 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, upload-time = "2026-01-03T17:29:57.179Z" }, - { 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, upload-time = "2026-01-03T17:29:58.914Z" }, - { 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, upload-time = "2026-01-03T17:30:01.962Z" }, - { 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, upload-time = "2026-01-03T17:30:03.609Z" }, - { 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, upload-time = "2026-01-03T17:30:05.132Z" }, - { 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, upload-time = "2026-01-03T17:30:07.135Z" }, - { 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, upload-time = "2026-01-03T17:30:09.083Z" }, - { url = "https://files.pythonhosted.org/packages/3e/80/958f16de79ba0422d7c1e284b2abd0c84bc03394fbe631d0a39ffa10e1eb/aiohttp-3.13.3-cp311-cp311-win32.whl", hash = "sha256:a2212ad43c0833a873d0fb3c63fa1bacedd4cf6af2fee62bf4b739ceec3ab239", size = 433701, upload-time = "2026-01-03T17:30:10.869Z" }, - { url = "https://files.pythonhosted.org/packages/dc/f2/27cdf04c9851712d6c1b99df6821a6623c3c9e55956d4b1e318c337b5a48/aiohttp-3.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:642f752c3eb117b105acbd87e2c143de710987e09860d674e068c4c2c441034f", size = 457678, upload-time = "2026-01-03T17:30:12.719Z" }, - { 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, upload-time = "2026-01-03T17:30:14.23Z" }, - { 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, upload-time = "2026-01-03T17:30:15.96Z" }, - { 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, upload-time = "2026-01-03T17:30:17.431Z" }, - { 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, upload-time = "2026-01-03T17:30:19.422Z" }, - { 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, upload-time = "2026-01-03T17:30:21.756Z" }, - { 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, upload-time = "2026-01-03T17:30:23.932Z" }, - { 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, upload-time = "2026-01-03T17:30:26Z" }, - { 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, upload-time = "2026-01-03T17:30:27.554Z" }, - { 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, upload-time = "2026-01-03T17:30:29.254Z" }, - { 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, upload-time = "2026-01-03T17:30:31.033Z" }, - { 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, upload-time = "2026-01-03T17:30:32.703Z" }, - { 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, upload-time = "2026-01-03T17:30:34.695Z" }, - { 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, upload-time = "2026-01-03T17:30:36.864Z" }, - { 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, upload-time = "2026-01-03T17:30:39.433Z" }, - { 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, upload-time = "2026-01-03T17:30:41.081Z" }, - { url = "https://files.pythonhosted.org/packages/3c/4a/1a3fee7c21350cac78e5c5cef711bac1b94feca07399f3d406972e2d8fcd/aiohttp-3.13.3-cp312-cp312-win32.whl", hash = "sha256:b04be762396457bef43f3597c991e192ee7da460a4953d7e647ee4b1c28e7046", size = 428253, upload-time = "2026-01-03T17:30:42.644Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b7/76175c7cb4eb73d91ad63c34e29fc4f77c9386bba4a65b53ba8e05ee3c39/aiohttp-3.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:e3531d63d3bdfa7e3ac5e9b27b2dd7ec9df3206a98e0b3445fa906f233264c57", size = 455407, upload-time = "2026-01-03T17:30:44.195Z" }, - { 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, upload-time = "2026-01-03T17:30:45.832Z" }, - { 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, upload-time = "2026-01-03T17:30:47.466Z" }, - { 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, upload-time = "2026-01-03T17:30:49.373Z" }, - { 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, upload-time = "2026-01-03T17:30:50.974Z" }, - { 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, upload-time = "2026-01-03T17:30:52.729Z" }, - { 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, upload-time = "2026-01-03T17:30:54.537Z" }, - { 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, upload-time = "2026-01-03T17:30:56.512Z" }, - { 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, upload-time = "2026-01-03T17:30:58.256Z" }, - { 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, upload-time = "2026-01-03T17:31:00.445Z" }, - { 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, upload-time = "2026-01-03T17:31:03.024Z" }, - { 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, upload-time = "2026-01-03T17:31:04.842Z" }, - { 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, upload-time = "2026-01-03T17:31:06.868Z" }, - { 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, upload-time = "2026-01-03T17:31:08.958Z" }, - { 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, upload-time = "2026-01-03T17:31:10.676Z" }, - { 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, upload-time = "2026-01-03T17:31:12.575Z" }, - { url = "https://files.pythonhosted.org/packages/bc/9f/d643bb3c5fb99547323e635e251c609fbbc660d983144cfebec529e09264/aiohttp-3.13.3-cp313-cp313-win32.whl", hash = "sha256:1449ceddcdbcf2e0446957863af03ebaaa03f94c090f945411b61269e2cb5daf", size = 427383, upload-time = "2026-01-03T17:31:14.382Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" }, - { 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, upload-time = "2026-01-03T17:31:17.909Z" }, - { 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, upload-time = "2026-01-03T17:31:19.919Z" }, - { 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, upload-time = "2026-01-03T17:31:21.636Z" }, - { 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, upload-time = "2026-01-03T17:31:23.296Z" }, - { 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, upload-time = "2026-01-03T17:31:25.334Z" }, - { 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, upload-time = "2026-01-03T17:31:27.394Z" }, - { 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, upload-time = "2026-01-03T17:31:29.238Z" }, - { 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, upload-time = "2026-01-03T17:31:31.053Z" }, - { 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, upload-time = "2026-01-03T17:31:32.87Z" }, - { 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, upload-time = "2026-01-03T17:31:34.76Z" }, - { 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, upload-time = "2026-01-03T17:31:36.699Z" }, - { 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, upload-time = "2026-01-03T17:31:38.622Z" }, - { 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, upload-time = "2026-01-03T17:31:40.57Z" }, - { 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, upload-time = "2026-01-03T17:31:42.857Z" }, - { 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, upload-time = "2026-01-03T17:31:44.984Z" }, - { url = "https://files.pythonhosted.org/packages/29/a7/cbe6c9e8e136314fa1980da388a59d2f35f35395948a08b6747baebb6aa6/aiohttp-3.13.3-cp314-cp314-win32.whl", hash = "sha256:b556c85915d8efaed322bf1bdae9486aa0f3f764195a0fb6ee962e5c71ef5ce1", size = 433126, upload-time = "2026-01-03T17:31:47.463Z" }, - { url = "https://files.pythonhosted.org/packages/de/56/982704adea7d3b16614fc5936014e9af85c0e34b58f9046655817f04306e/aiohttp-3.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9bf9f7a65e7aa20dd764151fb3d616c81088f91f8df39c3893a536e279b4b984", size = 459128, upload-time = "2026-01-03T17:31:49.2Z" }, - { 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, upload-time = "2026-01-03T17:31:51.134Z" }, - { 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, upload-time = "2026-01-03T17:31:52.85Z" }, - { 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, upload-time = "2026-01-03T17:31:54.91Z" }, - { 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, upload-time = "2026-01-03T17:31:56.733Z" }, - { 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, upload-time = "2026-01-03T17:31:58.65Z" }, - { 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, upload-time = "2026-01-03T17:32:00.989Z" }, - { 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, upload-time = "2026-01-03T17:32:03.122Z" }, - { 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, upload-time = "2026-01-03T17:32:05.255Z" }, - { 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, upload-time = "2026-01-03T17:32:07.607Z" }, - { 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, upload-time = "2026-01-03T17:32:09.59Z" }, - { 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, upload-time = "2026-01-03T17:32:11.445Z" }, - { 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, upload-time = "2026-01-03T17:32:13.705Z" }, - { 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, upload-time = "2026-01-03T17:32:15.965Z" }, - { 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, upload-time = "2026-01-03T17:32:18.219Z" }, - { 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, upload-time = "2026-01-03T17:32:20.25Z" }, - { url = "https://files.pythonhosted.org/packages/eb/f8/7314031ff5c10e6ece114da79b338ec17eeff3a079e53151f7e9f43c4723/aiohttp-3.13.3-cp314-cp314t-win32.whl", hash = "sha256:2e41b18a58da1e474a057b3d35248d8320029f61d70a37629535b16a0c8f3767", size = 466523, upload-time = "2026-01-03T17:32:22.215Z" }, - { url = "https://files.pythonhosted.org/packages/b4/63/278a98c715ae467624eafe375542d8ba9b4383a016df8fdefe0ae28382a7/aiohttp-3.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:44531a36aa2264a1860089ffd4dce7baf875ee5a6079d5fb42e261c704ef7344", size = 499694, upload-time = "2026-01-03T17:32:24.546Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/58/d9/22ce5786ac0c1653ae8b6c23bded02c1686d11f0dbb45b31ce128e0df985/aiohttp-3.14.3.tar.gz", hash = "sha256:9491196535a88924a60afd5b5f434b5b203b6cc616250878dbdb223a8f7844bc", size = 7971213, upload-time = "2026-07-23T01:57:27.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/5c/b3e4ff8ad43a8afef9602c5e90285936da1beaea8b029016b793891f03c3/aiohttp-3.14.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e568e14940c09955aa51f4e645b6daa18a581c5dcfcd73744dcc86a856e3ced3", size = 764250, upload-time = "2026-07-23T01:52:48.525Z" }, + { url = "https://files.pythonhosted.org/packages/0e/da/f1b384465e51449d844056b75070461da03a9a23e6c1747003695bf4172a/aiohttp-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:54cfcdee2770dac994417cbb0ee1f3eb0e7cb6b30c79bf44f2c02ff79ec5124a", size = 516281, upload-time = "2026-07-23T01:52:51.047Z" }, + { url = "https://files.pythonhosted.org/packages/b9/3f/01264f820ee2e3712a827892b1cd6ff80f3300c1fcbffbb45714a915d47a/aiohttp-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:21c016079415ed3fd676963e9793700a566d85dbbd6bfc564b9b2d209147dcc8", size = 514742, upload-time = "2026-07-23T01:52:53.779Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8d/a71c6f2db52ac1ed142b133f7feddaa6b70539c3f4de24d7e226c95b794c/aiohttp-3.14.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d6088ec9894113802bddb3c09e974929aed2c7b3a8c456219b8aab4481f1a239", size = 1780613, upload-time = "2026-07-23T01:52:56.948Z" }, + { url = "https://files.pythonhosted.org/packages/a5/11/3dd9b3fb3a170f6ec9011b5291d876a6fab4086714c9e158600edf01b4fd/aiohttp-3.14.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:16ea7e24c309fb7c0bbd505d149abe4fe4dccfb8db911db7dbec0921bc889a6f", size = 1737688, upload-time = "2026-07-23T01:52:59.294Z" }, + { url = "https://files.pythonhosted.org/packages/6d/3e/834c26918be7d88068822b40e0db30fca50b5f4fe79104aa16a93f1d74e6/aiohttp-3.14.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56f355e79f71aef2a85c80305cc915f894b170dba76de5fe84f6351939b83c06", size = 1845742, upload-time = "2026-07-23T01:53:01.641Z" }, + { url = "https://files.pythonhosted.org/packages/cc/c9/49ab8572df7d66bc13d11e31f781292badb04180dd87ba98733066c6aed7/aiohttp-3.14.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:18c441d0a8fca6de8d1f546849b9f0ab20d435993e2c5b59562b2fae6be2f929", size = 1928412, upload-time = "2026-07-23T01:53:04.018Z" }, + { url = "https://files.pythonhosted.org/packages/a5/b9/2b8f0c0ce09c87a1daf80fd483431b56b1435d3f62789bc86f572e1245de/aiohttp-3.14.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53e7b4ce82b54a8bcc71b3b67a5cbd177ca1d7f592cbc92cd38b7349f73482db", size = 1786220, upload-time = "2026-07-23T01:53:06.481Z" }, + { url = "https://files.pythonhosted.org/packages/85/00/9c45f81de11710460edfa1dc81317b6e882703b160926c879a9d20da9fcc/aiohttp-3.14.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f55119f7bf25f49ed210f6096090715da24f2943c62102448915fde3c62877ce", size = 1637231, upload-time = "2026-07-23T01:53:10.258Z" }, + { url = "https://files.pythonhosted.org/packages/19/ce/967d628e910756f3539c6107cb7844a1b69440dcb3029a5ee7871b09ab63/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9aa6e61fdf20105c4144e755bd586008ff450791d67b1c8146fdc15959c4d51c", size = 1753161, upload-time = "2026-07-23T01:53:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/11/b2/0c3d4114f0aee4f580f5b3b4eb71b24d7a23b834ea506a4dfebe76513f35/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ccd4893707b3e2a13e39c90d43cf80edf2e4d0457935bcc103bf2346214c3f15", size = 1756356, upload-time = "2026-07-23T01:53:16.211Z" }, + { url = "https://files.pythonhosted.org/packages/63/5d/99e7d91c82f1399d1ae2a854e080bd1493fbc31e5e959dbc4ec33dac3bec/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b2466434105a4e03113c36ec775cc2ebe6676b62eae326fa670bb607ef788c1c", size = 1819846, upload-time = "2026-07-23T01:53:18.289Z" }, + { url = "https://files.pythonhosted.org/packages/ad/05/d5e1cb6480eeffd3f901d40a2c5e2d1e7effdc797837da3b490272699f13/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:ba59d59aba08ac02fc03b0c8983ccd5ee39a199d0552ce9e6d2b4845b34d59ae", size = 1628531, upload-time = "2026-07-23T01:53:23.86Z" }, + { url = "https://files.pythonhosted.org/packages/c9/90/b934682bcaefae18a9e04f3dff5b68522ba810906358ae5029b68110ea3b/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ed099d105449c4f9e84f24af203cd131349d4761d8813fa7e02c32e7128cd910", size = 1832712, upload-time = "2026-07-23T01:53:27.551Z" }, + { url = "https://files.pythonhosted.org/packages/21/df/6061679faaf81fac746e7307c7adb71e858071a5d34c27583afefc64f543/aiohttp-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:152516815ef926786a0b6ae2b8f1fd2e0c71582dee0b435636865316fd4891b7", size = 1775014, upload-time = "2026-07-23T01:53:30.223Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1d/f854878bbc69b88faefe924b619a34a6f59ec05fd387c77690667eaa75eb/aiohttp-3.14.3-cp311-cp311-win32.whl", hash = "sha256:a4af35c443e0b1a1bd6a8af3f3485d7fda15c142751a00f3ff8090f0b93346fa", size = 456006, upload-time = "2026-07-23T01:53:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/73/0c/2af9d1674baccd1dbd47282a93d660a22e57ef6167c856deb24b4214fbab/aiohttp-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:e1e74298bab6ee0d6e749ed4fd1901c7e604bdda32c03d787a2cc71c46d0433d", size = 481069, upload-time = "2026-07-23T01:53:39.673Z" }, + { url = "https://files.pythonhosted.org/packages/8e/76/88401ff3fc95e85c5fc38d588f36f55e61ecb64343b2bc8d69326f453cc0/aiohttp-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:03cd2bde3d7f085b64e549c985f4bb928cad7e8ecf5323bfca320db548d81b39", size = 453021, upload-time = "2026-07-23T01:53:43.749Z" }, + { url = "https://files.pythonhosted.org/packages/18/d4/eb96299230e20acf2efae207cb8d69051f1f68e357e5ea5e479bf6fb097a/aiohttp-3.14.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:39aded8c7f3b935b54aab1d8d73c70ec0ee2d3ec3b943e0e86611bc150ba47f5", size = 754690, upload-time = "2026-07-23T01:53:47.332Z" }, + { url = "https://files.pythonhosted.org/packages/88/11/e7a70a209eb9a067c0d3212b518a0134e3484f5178c7533878b6b514d469/aiohttp-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5bcb6ff3fdab1258a192679ff1a05d44f59626430aa05cd1a9d2447423599228", size = 509484, upload-time = "2026-07-23T01:53:51.159Z" }, + { url = "https://files.pythonhosted.org/packages/30/07/4bbc222cc8dbe31d4c3e8a5baad2286e4d42026ac0c570027b89afce6344/aiohttp-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:617105e2c3018ee38d0c8ce5ee3c84f621a6d8b9f723202aacaff28449ca91ee", size = 511949, upload-time = "2026-07-23T01:53:55.083Z" }, + { url = "https://files.pythonhosted.org/packages/54/b9/42e74c46b7b7c794b995bbc1f573fb48950c38b19d8600c62a6804ee2d67/aiohttp-3.14.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f631fe87a6f30df5fbe6d79640b25e4cffb38c31c7fb6f10871517b84b0f8c1a", size = 1765282, upload-time = "2026-07-23T01:53:59.662Z" }, + { url = "https://files.pythonhosted.org/packages/6b/ed/62bc4d74363ad346d518e0720363a949f63e2e23439a79eb5813d4d29bb3/aiohttp-3.14.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a94dbaae5ae27bd849c93570669bff91e0510f33a80805738e3de72a7be0447b", size = 1741511, upload-time = "2026-07-23T01:54:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9f/181e8a8bc79e47d13c7fc4540bd7a3b729d9505609c61f392a8dd2fbfe55/aiohttp-3.14.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8f2f1c4c032c7cedd7d8da6f54c97b70266c6570c3108d3fdffee7188bb70529", size = 1810680, upload-time = "2026-07-23T01:54:09.882Z" }, + { url = "https://files.pythonhosted.org/packages/5c/9a/dec94d6ad694552fe3424e3f1928d7a606a5d9d9433a04e7ecdd9d38ae7f/aiohttp-3.14.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea05e1f97ceea523942d9b2a7d7c0359d781d683d6b043f5943a602b14da4787", size = 1905646, upload-time = "2026-07-23T01:54:13.475Z" }, + { url = "https://files.pythonhosted.org/packages/52/b7/7cd31f29d6055bd711ae6e669367fba6f5ae9de463910a793e30556a8db7/aiohttp-3.14.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:543906c127fb1d929b95076db19b83fa2d46751006ff1e23b093aa5ac4d8db42", size = 1792122, upload-time = "2026-07-23T01:54:15.752Z" }, + { url = "https://files.pythonhosted.org/packages/66/73/10b1ef93afa61f4963c746257b70ced619cf31a4798671de5fdb2608501d/aiohttp-3.14.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0a5ff2dfbb9ce645fa5b8ef3e02c6c0b9cc3f6030ff863d0c51fffc50cb5541b", size = 1591127, upload-time = "2026-07-23T01:54:19.489Z" }, + { url = "https://files.pythonhosted.org/packages/49/ed/3b203fa6de1b338c14acdc06bf6ca9b043b7944f005966958c2ced932cde/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:041badb8f84396357c4d3ad26de6afd7a32b112f43d3c63045c0c8278cfd2043", size = 1725210, upload-time = "2026-07-23T01:54:24.129Z" }, + { url = "https://files.pythonhosted.org/packages/28/b7/1c2aab8c706436dcc28598452488ac9cd7c409da815237c28c27d58993e6/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:530125ee1163c4219af35dc3aa1206e541e7b31b6efc1a3f93b70a136f65d427", size = 1764848, upload-time = "2026-07-23T01:54:27.973Z" }, + { url = "https://files.pythonhosted.org/packages/54/50/94c28f08b131c4bf10984ea2c7a536c9920608bb2d6e7f95642c30cc87b7/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c8653fd547c93a61aadc612007790f5555cdd18946fa48cf45e26d8ea4ea473d", size = 1777102, upload-time = "2026-07-23T01:54:31.775Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/e7d09ba7d345fb2d74440fd2fa033c5e079fac05552927705986f41a364f/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:89176250f686cb9853c0fb7ead90e639e915b84a6f43eedc2a4e7ec21f1037f0", size = 1580205, upload-time = "2026-07-23T01:54:34.518Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/072a91d68e1e1eb587985b54baab94221277f877e8ef274fc213a0ceae28/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3a26434dafe408229ff3403458ca58de24fb51936504decac49ce6755f77e59d", size = 1797219, upload-time = "2026-07-23T01:54:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/e0/eb/aad34e897e668424d6e995da5dff8a4a09af93363d3392488772957a63aa/aiohttp-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d1558173930a5a8d3069cee5c92fc91c87c4dbcb099debbb3622053717145a19", size = 1768629, upload-time = "2026-07-23T01:54:40.103Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2b/6bb88ddba0fecd9122aa3ebcad25996cf6c083a4a7040dbb3a4f97972af6/aiohttp-3.14.3-cp312-cp312-win32.whl", hash = "sha256:16100ad3ab8d649fdfbee87602d9d2dcdca9df0b9eda8a1b5fdc0d41f96da559", size = 451481, upload-time = "2026-07-23T01:54:42.547Z" }, + { url = "https://files.pythonhosted.org/packages/76/9b/f2f8f108da17ecef2cc3efc424e8b7ad3782b1a8360f7b8eae8ced84f6ea/aiohttp-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:33a2d7c28d33797a2e99923dffa63f83d908a19b6bf26cfe80fa790aa5e1a75a", size = 476845, upload-time = "2026-07-23T01:54:44.853Z" }, + { url = "https://files.pythonhosted.org/packages/3e/44/28dac80a8941b604f4da10ce21097614ca1bf905ce93dca28d8d7de9c1e7/aiohttp-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:362a3fd481769cac1a824514bcd86fda51c65e8fe6e051099e008fddde6db17c", size = 448050, upload-time = "2026-07-23T01:54:47.087Z" }, + { url = "https://files.pythonhosted.org/packages/57/be/5afd201cc0ab139029aadb75392efe85a293403d9dd3a3226161c21ce00c/aiohttp-3.14.3-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:2e9878ae68e4a5f1c0abe4dd497dbc3d51946f5837b56759e2a02e78fa90ef86", size = 506269, upload-time = "2026-07-23T01:54:49.075Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/dec8189d62b45ade009f6792a2264b942a90cb88aeaf181239933cd72c3c/aiohttp-3.14.3-cp313-cp313-android_21_x86_64.whl", hash = "sha256:f3d2669fe7dec7fc359ecdb5984b29b50d85d5d00f8c1cb61de4f4a24ee42627", size = 515166, upload-time = "2026-07-23T01:54:51.894Z" }, + { url = "https://files.pythonhosted.org/packages/28/24/2854869d29ed8a8b19d74f9ec6629515f7e04d02dd329d9d179201e58e47/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:cc7cb243a68167172f48c1fd43cee91ec4b1d40cefd190edd43369d1a6bc9c82", size = 486263, upload-time = "2026-07-23T01:54:54.223Z" }, + { url = "https://files.pythonhosted.org/packages/d4/dd/57187c8be2a35aea65eaee3bd2c3dcbbcf0204f5106c89637e3610380cd1/aiohttp-3.14.3-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:78253b573e6ffab5028924fc98bc281aae05445969982a10864bc360dea2016c", size = 492299, upload-time = "2026-07-23T01:54:56.236Z" }, + { url = "https://files.pythonhosted.org/packages/b9/11/06ae6ed8f0d414edf4068861e233d8fe23ee699bfd4b3ceb8663db948a62/aiohttp-3.14.3-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7041d52c3a7fa20c9e8c182b534704abb19502c8bdcbde7ab23bfda6f642394f", size = 502235, upload-time = "2026-07-23T01:54:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a3/559639c34a345d2cf7c52dff6838119f2eaf29eb508227b5b83f573af813/aiohttp-3.14.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac74facc01463f138b0da5580329cfcc82818dea5656e83ddcd11268fc12ff80", size = 750883, upload-time = "2026-07-23T01:55:00.65Z" }, + { url = "https://files.pythonhosted.org/packages/91/cd/41e131f13afd1e7b0172a9d9eda085ef90eb8439f41f0d279db81ed3ae60/aiohttp-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d6218d92e450824e9b4881f44e8c09f1853b490f9a64130801024a4793b1b3b0", size = 508473, upload-time = "2026-07-23T01:55:02.945Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6b/e7f13410d391c6e55b4c007a8de024355389d7d459e3d64c42b2d33617e5/aiohttp-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:11fb37ef075669eee52ab1928fbf6e1741fada40409fa309ebde9607a962aebf", size = 509190, upload-time = "2026-07-23T01:55:05.173Z" }, + { url = "https://files.pythonhosted.org/packages/97/21/6464573e53d69672cc1eada3e5c5cb2d2efa82701e8305a0f2047a576967/aiohttp-3.14.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55bdcc472aafe2de4a253045cc128007a64f1e0264fb675791e132ea5edaa3bd", size = 1761478, upload-time = "2026-07-23T01:55:07.383Z" }, + { url = "https://files.pythonhosted.org/packages/1a/81/d217043a4c17fbce360905e3b2bdd20139ebc9a2de836d035d179c4da006/aiohttp-3.14.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c39846c3aad97a8530c89d7a3869a8f8e9e3762c6ac0504481e5c80948f7e807", size = 1735092, upload-time = "2026-07-23T01:55:09.803Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/e13a02d0eeb1a9a502402a977abb4e4abff9fe4051c26f80558c57a7c975/aiohttp-3.14.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5895ef58c4620afe02fa16044f023dc4dafec08158f9d08874a46a7dbc0341b8", size = 1800546, upload-time = "2026-07-23T01:55:12.012Z" }, + { url = "https://files.pythonhosted.org/packages/26/5e/57d42fca1d18cb5acc1cad945d017fabc5d6ae71d8a08ad66be8dc3ee544/aiohttp-3.14.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fa9467a8113aa69d3d7c55a70ef0b7c636010a40993f3df9d9d0d73b3eb7ef24", size = 1895250, upload-time = "2026-07-23T01:55:14.357Z" }, + { url = "https://files.pythonhosted.org/packages/ca/1c/7da8d08e74d56f00070822f9638ff3f1c563f8ad87d1efa996c87bfc8644/aiohttp-3.14.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7d2deec16eeedf55f2c7cf75b521ea3856a5177e123844f8fd0f114ce252cb5", size = 1789289, upload-time = "2026-07-23T01:55:16.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/0f/cf16bcf56896981c1a0319f5d5db9337994b5165730c48a8fa07e9b34be6/aiohttp-3.14.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dd54d0e8717de95939766febac482ac0474d8ac3b048115f9f2b1d23a16e7db4", size = 1586706, upload-time = "2026-07-23T01:55:18.913Z" }, + { url = "https://files.pythonhosted.org/packages/fe/6f/76eac12a7f2480e1e304f842efdb07db33256b0d9165b866b6ef0806c202/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:df82f3787c940c94986b34222d59c9e38843fba85139f36e85255a82ad5355a9", size = 1724652, upload-time = "2026-07-23T01:55:21.296Z" }, + { url = "https://files.pythonhosted.org/packages/39/b6/19c8c592baeeb94b75f966547d40c02ac7590902306ec5863d5c027cf506/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:42a67efc36300d052fb4508a53e8b6901b9284b599ae63945c377569c5fcc1e1", size = 1756239, upload-time = "2026-07-23T01:55:23.705Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c9/4e9383150296f97f873b680c4de8fb2cd88608fb9f48c79edcb111611abc/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7a75aa63cbf9b21cfaf60dc2657e19df2c2867d91707d653fee171ffeedd1371", size = 1769161, upload-time = "2026-07-23T01:55:26.082Z" }, + { url = "https://files.pythonhosted.org/packages/aa/1e/147bdc6cc5de5f3ab011be8bf5d6e786633249f22c20bae06f85e45f5387/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e92eb8acc45eb6a9f4935071a77edf5b85cc6f8dfad5cd99e97653c26593cdde", size = 1578759, upload-time = "2026-07-23T01:55:28.846Z" }, + { url = "https://files.pythonhosted.org/packages/fd/31/78388a9d6040ece2e11df62ea229a822cf5e52d238374b220ae9975b2623/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b014a6ed7cf912e787149fdc529166d3ceabac23f26efeea3158c9aba2354e7e", size = 1792025, upload-time = "2026-07-23T01:55:31.457Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/a3d29fdf2c25d796746af8ad6fe56a45d6256c38b0a8a2ed752e1160b3a2/aiohttp-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3d4f72af88ac2474bb5bca640030320e3d38a0163a1d7533500e87be458eef71", size = 1768477, upload-time = "2026-07-23T01:55:33.87Z" }, + { url = "https://files.pythonhosted.org/packages/29/a6/442e18b5afeade534d877a2dc3c3e392aff8d49787890b0cf84790410267/aiohttp-3.14.3-cp313-cp313-win32.whl", hash = "sha256:5f08ec777f35ee70720233b8b9811d3bb5d728137f30ac91b7457709c3261ac0", size = 451069, upload-time = "2026-07-23T01:55:36.121Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/3d876ac02659f271cf7f6769f14a8e3de5b6e888ed8b5a7e998086a4cec8/aiohttp-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:dff9461ec275f22135650d5ba4b4931a11f3958df7dfbb8db630000d4dee0883", size = 476518, upload-time = "2026-07-23T01:55:38.303Z" }, + { url = "https://files.pythonhosted.org/packages/b2/0e/50d6e6471cd31edce8b282bdec59375a3a69124d8a989a0b1313355cae52/aiohttp-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:ddcac3c6b382e81f1dd0499199d4136b877beb4cb5ef770bbbfba56c4b8f55d2", size = 447676, upload-time = "2026-07-23T01:55:40.451Z" }, + { url = "https://files.pythonhosted.org/packages/c8/20/887fdcf832326571b370ffc347b3e70abe101096f3720126aac161b1d872/aiohttp-3.14.3-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:49f7325beb0f85ef4aef5f48f490269575f83e6e2acad00a1d80b807eb027062", size = 509067, upload-time = "2026-07-23T01:55:42.618Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a3/92cec936f78cc4bf0fa5554ebe593b73459d94e3c62303e1902a4cccb6f7/aiohttp-3.14.3-cp314-cp314-android_24_x86_64.whl", hash = "sha256:e3be98a7c30b8c25d573dafba7171d66dfb05ee6a9070fc46535464ff97700a6", size = 514774, upload-time = "2026-07-23T01:55:44.937Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/2a0c38df3fc557620b6a5acd98364af050053b6285b4dc7ee74100c63c18/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:614c61d478b83953e261d02bb2df750f17227cd33ef8002945bf5aebbde21919", size = 488134, upload-time = "2026-07-23T01:55:47.135Z" }, + { url = "https://files.pythonhosted.org/packages/48/d6/d51b7d4bf309af3693940d8ffd2b9ed0b682434ef85959b7c9c137f60cf8/aiohttp-3.14.3-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:1caa7b0d05f3e3a36f87788c59e970a7ee1cefcfcbb924a9f138c4a6551c9cb7", size = 494201, upload-time = "2026-07-23T01:55:49.451Z" }, + { url = "https://files.pythonhosted.org/packages/3f/5a/8f624384e5f1efabb5229b94157eb966b021e97bdb188c62860c2ae243c2/aiohttp-3.14.3-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:dfa68deb2a443bdaa3ea5297b0699c1464f08aef3812b486d1348eee61b07dc0", size = 502766, upload-time = "2026-07-23T01:55:51.656Z" }, + { url = "https://files.pythonhosted.org/packages/a6/26/4ff0164370deec18fb19254ee4ab10b7a73304ac0c860b13f5f84663759b/aiohttp-3.14.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:e72ee89e28d907a18f46959b4eb0bb06701cc7f8cf4366e00029e2ccfaaf5924", size = 756557, upload-time = "2026-07-23T01:55:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/97/a3/7056b86dc0d9ec709ea9777eae3b0161428f943372f8b98c01c11593b682/aiohttp-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ad4c8b7488d745d2ca4838ebd8ae5ba9b56341d30b1da43640e4ce87f9f49646", size = 510168, upload-time = "2026-07-23T01:55:56.22Z" }, + { url = "https://files.pythonhosted.org/packages/85/ed/0357a015892fd68058bf2d39d3fd1958e459b997a7db30aaa6aaa434ae96/aiohttp-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:db332af25642007330fca8be5c4d194caf2bea7a7fc84415aff3497af5dfee6b", size = 512957, upload-time = "2026-07-23T01:55:58.437Z" }, + { url = "https://files.pythonhosted.org/packages/47/d1/8aba53f15ccb2238405f5e9d30e2a8ca44f93878c26e7165ade00d374b1c/aiohttp-3.14.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25bd2708db6bdf6a6630dd37bdcdfcb47c4434d22ac69c64665b802910140b30", size = 1750149, upload-time = "2026-07-23T01:56:00.856Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/40c3fee327529284375c6701cbb0fa4600cc2e8432af1378f897e2ef7d3a/aiohttp-3.14.3-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:cef89a58e628c4efcac3275c2d68083f82426dcdc89c1492a6f654f9f7ea6ab9", size = 1707685, upload-time = "2026-07-23T01:56:03.371Z" }, + { url = "https://files.pythonhosted.org/packages/2a/a3/ca0cc6724cca8114b05694abd916060758c79894c3aa5b012cdadc1bc28e/aiohttp-3.14.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c23ec8ee9d5ab2f5421f9c7fffce208435607af27fd46d4a44e031954352838f", size = 1803911, upload-time = "2026-07-23T01:56:05.817Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/85b099c299c3ffd38ad9b3e43694c8a346934e4a30c88c4fd5a841234f77/aiohttp-3.14.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e2667f0bbe7eb6c74eae5e9691441ad186e5845ca3cff63230fc09c4e7514f5d", size = 1876929, upload-time = "2026-07-23T01:56:08.413Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b7/1da684a04175473fa4cddbf9a2f572e79514c3fd27a74597f43057d4f3da/aiohttp-3.14.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18cb43369747b2ae007bd2655fb8e63a099c2ff1d207962943636dac989b3147", size = 1761112, upload-time = "2026-07-23T01:56:10.918Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/bc4b55e3e5cb175fd69c53c90d60d2f47797cb343da5106e23863dc4dba4/aiohttp-3.14.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d77640cc618c1d99fc4f8589c0f24a730adfa54eb1e57ef7bf0c8dfb78da898c", size = 1583500, upload-time = "2026-07-23T01:56:13.613Z" }, + { url = "https://files.pythonhosted.org/packages/2a/e8/13a9d957a1ee40837f46aa30f0f4c657e673ad86a2e6362a9f9be20d26d9/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:53e5179d8abb5710f8e83ba207c41c8d1261fcffd4616500e15ca2b7a33be10a", size = 1713940, upload-time = "2026-07-23T01:56:15.969Z" }, + { url = "https://files.pythonhosted.org/packages/38/05/d33c680c1bcf1c7e130f9cbfc1fc02fe8bb0c4af2a94a53dd5fb56131e5c/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:cd817772b2fcf2b8c0905795318485f9ec16eae60b29feb7f4c77085311637f0", size = 1724413, upload-time = "2026-07-23T01:56:18.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/1d/af798d306f7a74b6a632dbcabcf62a4c91391b7582d2a8c6d7712e2cc54e/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:4e3ac92d90e92773b2362d506068e9a948192bd553e743c5b2429e28527c8661", size = 1770748, upload-time = "2026-07-23T01:56:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/a8/92/ad720d472556a995049206867765e9410969684f86ee09423ff9969044c1/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3f42e9b78301f11c8f861746175d8b9c1ccef713fcad9eab396e2f6db8ed4a22", size = 1577564, upload-time = "2026-07-23T01:56:23.475Z" }, + { url = "https://files.pythonhosted.org/packages/60/ad/0ed7586cbef7a884e23a752fa2bb987a122e6a5dd50dab109258d0a95193/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:9d9edccfe496b476db5f398d97b865e9a6752bcf8aec4eef8390ce20fb64bb41", size = 1782080, upload-time = "2026-07-23T01:56:25.994Z" }, + { url = "https://files.pythonhosted.org/packages/97/ea/dbaed0d73e8a69aad653b045dab451c67c2454bb731a37b45a86593e9422/aiohttp-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c5ec8fb1bcc31a8466f74aaf26c345d5c386fa4bd08a3f0eb9c7a4a3fe8b5bf", size = 1745813, upload-time = "2026-07-23T01:56:28.604Z" }, + { url = "https://files.pythonhosted.org/packages/81/1b/6893d4bc57e434fc93a6c9217c637d967a0b651d989f6e3265179375754a/aiohttp-3.14.3-cp314-cp314-win32.whl", hash = "sha256:38901a84da3ce22249f6e860bf8f90d141bcab7da090cc398f8bb58c0e44b7da", size = 455872, upload-time = "2026-07-23T01:56:31.031Z" }, + { url = "https://files.pythonhosted.org/packages/f5/8b/c7baa1ba1eda4db6989baefe5de6d99834921b84ebd7918624febcb9f290/aiohttp-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:8b3b60de05f3dcb6f6a00f818bb2ec781cee4de0645f59ccaf99b1d1823b6100", size = 481030, upload-time = "2026-07-23T01:56:33.365Z" }, + { url = "https://files.pythonhosted.org/packages/22/8c/c29d067df825a2df88ca432db848aa2fe8199598359cc06c12b09320cac9/aiohttp-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:1576145bdceeb92382d899751e12743a3a5b8e460a841e3e50543859e54864dc", size = 453669, upload-time = "2026-07-23T01:56:35.731Z" }, + { url = "https://files.pythonhosted.org/packages/6a/a4/9c033beb355d39b6147980597ec9645e4729243f686ee4dc73945de72030/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:8800c996b01c2772a783e3e46f3e1abd5823029adca0df54231960de9bfefa5b", size = 791403, upload-time = "2026-07-23T01:56:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/80/ca/87c32a0a7704583cfc49660bd817889bae5b830bf53b5dcb4e92145ac2da/aiohttp-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebe8e504f058fe91223351cecd2d9d6946c9d241bb0250d898ffbdf584cc72b0", size = 526413, upload-time = "2026-07-23T01:56:40.523Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d8/8ec0e471248c500acdce2be3f46db8fb62b5eb60efef072529cc85ee1d26/aiohttp-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30402d03a7c0ff52bce290b57e564e9079fd9d0cb545c8aba73f86a103162d2e", size = 532135, upload-time = "2026-07-23T01:56:42.876Z" }, + { url = "https://files.pythonhosted.org/packages/fe/45/f8919fd936e8b79fcd9bda7b6d8e62613462a713f4f17987fd7c34399142/aiohttp-3.14.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9fc7b5bfec6573f3ae844f457fdde5adeb713f8b8e4a81ad64fc207b49383716", size = 1922742, upload-time = "2026-07-23T01:56:45.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ec/9ca76b28a27525b0cc53e20842e0228b022f301ce1f436b7d814b4aaf2df/aiohttp-3.14.3-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8a5fd34f7f7410d1730d5c2ba873cacb2eed3fede366feb268a70ba22581ed8f", size = 1787371, upload-time = "2026-07-23T01:56:48.045Z" }, + { url = "https://files.pythonhosted.org/packages/b1/04/6acdbf17315f7b55f1937e3387acb89a3cddeb4995689553d064af8e92ab/aiohttp-3.14.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:270d3dace9ca2f10f0da5d8ebe519b7a310fc6112ed916e32df5866df0888553", size = 1912623, upload-time = "2026-07-23T01:56:50.605Z" }, + { url = "https://files.pythonhosted.org/packages/86/e6/438b0c79ca6f45eb9fd9817dd4c01a91919a38c0de5ee9e05e2b4dc0ece7/aiohttp-3.14.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3ae5b3a59436d089b5395d910121a390feed4d00578eb95a0fd1a329fe963100", size = 2005515, upload-time = "2026-07-23T01:56:53.153Z" }, + { url = "https://files.pythonhosted.org/packages/bb/6b/62cbd6577758699525f5c712d1ddef57d9875fbab0ae8d5f5a202fd598f8/aiohttp-3.14.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2498f0fe69ead802f9675beca44a7c21c62fdaa4ec5145ea1c3ad6edbee29f85", size = 1879906, upload-time = "2026-07-23T01:56:55.818Z" }, + { url = "https://files.pythonhosted.org/packages/00/95/18bcbf830a21dc3aae24d8f6b6feaf3db1d2090242d00a7868db2ffb0b67/aiohttp-3.14.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0dc483c00da8b673abbb367eb6f8d8f4bcec30eb58529ea13cb42e7fd2dfa33", size = 1675849, upload-time = "2026-07-23T01:56:58.861Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/47f4968659c5e23606c3790c80fc624e691c153d036148449ee84d31b287/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c7d3a97c678d34fc5b59da671ee9cd630096ddc643e7b5a30d54a2a6f3574d3f", size = 1843496, upload-time = "2026-07-23T01:57:01.591Z" }, + { url = "https://files.pythonhosted.org/packages/64/af/38c33c4dd82fddcb4e56c4653b6f1072a8edbc6b7fa15809f14932c41e2d/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8fb78a83c9e5f741ca3a68cfb455c1f5bb83b4e7249a3848b3cd78d0a8563b0", size = 1827746, upload-time = "2026-07-23T01:57:05.131Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9d/0537cda4885ac8f5b7053d164dd06312f4c483a4edcb8ee5b8aaf2a989bf/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:74ab5b6a9fb13e873e5a90946588baecaf488745e1db1a4a5c433f971f035098", size = 1853810, upload-time = "2026-07-23T01:57:08.043Z" }, + { url = "https://files.pythonhosted.org/packages/19/fe/26f9c5e6458385aa86497836b0dea6fb2f027827d63f37c7856cce9286ee/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd52f811e65f6fb634b1047159657c98f52b407f8efec907bcfc09da9a4c0a25", size = 1668895, upload-time = "2026-07-23T01:57:10.837Z" }, + { url = "https://files.pythonhosted.org/packages/ec/4c/618b1db9b9ba079b8875d2cdf78e7c4a3bf72903bd5850fee7dd9544600a/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f0f177d1b195b9e06376cfd7d308d8a1b920909a609d03ac82a8c73bbb16d3b9", size = 1883833, upload-time = "2026-07-23T01:57:13.672Z" }, + { url = "https://files.pythonhosted.org/packages/94/c6/bd959bd1e4771f9fd944e9e436224c48c77b018b73b519b5aad346335bcc/aiohttp-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:498c6c623134f8e09a3c4e60bcd607a0b4590dd7dbf08dd40851b27cbb520ccb", size = 1844251, upload-time = "2026-07-23T01:57:16.593Z" }, + { url = "https://files.pythonhosted.org/packages/5e/19/08d41839658bdd44a0ed2480f3891705ecb487ce28c0dde62c9040c997e0/aiohttp-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:b304db572b4368edd8dda8a2274f73156fe15558fca4a917cb8a09fc47af5963", size = 474180, upload-time = "2026-07-23T01:57:19.306Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/3cd6ef0a2b2851f7ab913b5b079334781bd50ff56a323e4454063377a080/aiohttp-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:b20032766aedf6261c7a566585a40867d092ac03a0d81592d5370ef9b054f99b", size = 500528, upload-time = "2026-07-23T01:57:21.762Z" }, + { url = "https://files.pythonhosted.org/packages/a4/37/cfd1ed540a4d318da025590d96b728e63713c09e9377950fc655dadeb856/aiohttp-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:2e1161602f45a54de2ce0905243a95f58cb42dcd378402f3697f5e0b21e9d2e7", size = 469280, upload-time = "2026-07-23T01:57:24.241Z" }, ] [[package]] @@ -148,43 +160,106 @@ wheels = [ [[package]] name = "annotated-types" -version = "0.7.0" +version = "0.8.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, upload-time = "2024-05-20T21:33:25.928Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, + { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" }, ] [[package]] name = "anthropic" -version = "0.86.0" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "distro" }, { name = "docstring-parser" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/37/7a/8b390dc47945d3169875d342847431e5f7d5fa716b2e37494d57cfc1db10/anthropic-0.86.0.tar.gz", hash = "sha256:60023a7e879aa4fbb1fed99d487fe407b2ebf6569603e5047cfe304cebdaa0e5", size = 583820, upload-time = "2026-03-18T18:43:08.017Z" } +sdist = { url = "https://files.pythonhosted.org/packages/75/6d/793f5cfe2cd444c43b4eeb4cb7c3cc55ebcb38929fdfe81aa1f2fced7326/anthropic-1.4.0.tar.gz", hash = "sha256:f0d017e901e48b343520b5d458f8240c283c8d850bf6d119834c622207e0a74c", size = 1150831, upload-time = "2026-09-04T22:20:31.355Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/63/5f/67db29c6e5d16c8c9c4652d3efb934d89cb750cad201539141781d8eae14/anthropic-0.86.0-py3-none-any.whl", hash = "sha256:9d2bbd339446acce98858c5627d33056efe01f70435b22b63546fe7edae0cd57", size = 469400, upload-time = "2026-03-18T18:43:06.526Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e3/34a88f0e1e854022a352d67f72ca9baa8b952d4541315088411ba2bfbc2a/anthropic-1.4.0-py3-none-any.whl", hash = "sha256:590e85bff75b713a123b03f586d68f02266b5fdc49f70dd75f721ced93a4716c", size = 1300390, upload-time = "2026-09-04T22:20:33.078Z" }, ] [[package]] name = "anyio" -version = "4.12.1" +version = "4.15.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "idna" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.15'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/d2/f4d173e22df740bc37b1db102b386ba719b66e95b0f0d751f556b387e6d2/anyio-4.15.1.tar.gz", hash = "sha256:9f28306018cbd6d329e64a36d58256edff76dd996fe423bc957326e578b82a94", size = 276966, upload-time = "2026-09-05T10:42:39.44Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/12/b8/4bd346e22b28902df4d651910f5242c28d84e4a5c2435ca5c3f797ed7e2e/anyio-4.15.1-py3-none-any.whl", hash = "sha256:6152fdbbf9a77fdec97731721bebf7c4c44f7c29b424b0065826173efc7ed101", size = 132079, upload-time = "2026-09-05T10:42:37.923Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/c0/5bb6885a9608d86ee5712c0d88bc405d3a49f3e44231576e130ea2f53d34/ast_serialize-0.9.0.tar.gz", hash = "sha256:79fe8be1c934aa572940d1811d8dbe4d1b6f22291e3f16755c9b062e9ac92fb7", size = 951293, upload-time = "2026-09-02T15:50:45.078Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/76/497f19d9bdb3899a1efd82e2957f455d0c6e0cb9ebbc254735acb1f74235/ast_serialize-0.9.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ae1c46eb97865823f9843c4b80145e011874923e1a4a44b45738a5309d83e9f5", size = 889442, upload-time = "2026-09-02T15:49:21.144Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c5/9fb64b7106c5534739322c74be7b743c4f2e3b5fd05d5b8e677f05c54d5f/ast_serialize-0.9.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af082cb7e6c4fa3a428aa616c13d709a944076eba84a73184de15621cc1a915d", size = 1226721, upload-time = "2026-09-02T15:49:22.612Z" }, + { url = "https://files.pythonhosted.org/packages/27/67/b550fc81aa0133808410783c6d9a1b925e31610d226e836e21337850af55/ast_serialize-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7e9f2540741ad10657a209209f7e5cc6b530eb3ed145fd77258ab43542d96ad7", size = 1207369, upload-time = "2026-09-02T15:49:23.916Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/ed9e66deb7da63e44d0c0fd3a8feef698882ed56ea521a29494d4616eb46/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95485d5e8704af2ecc7f723757b88f992ae8122028d687ccf877cad2b4c3da4", size = 1273073, upload-time = "2026-09-02T15:49:25.336Z" }, + { url = "https://files.pythonhosted.org/packages/68/8f/cd337551d7a68c982425bbf91f183943d7ccc62394002c74807a7f0e60db/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b559cffac5a71a698d9194e4295765ff2132a10fd1284860f02b30f12c1f729e", size = 1279045, upload-time = "2026-09-02T15:49:26.75Z" }, + { url = "https://files.pythonhosted.org/packages/40/c6/98dc41eb4122d5da83241e805739838ed59e1e1b9006cbed89ded635a17f/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dad8a3f7106efcf252fc289c092ee0cee5c3512c0088bcf3fffa01458323092f", size = 1539300, upload-time = "2026-09-02T15:49:28.213Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d2/d94cede4b3f2a4e329d8ca92218f0846cfcc9257be91c5bf1168671f4ab5/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:24de2bc930b7e1ca86641136b9875a1e5f80f52b484deddc67893eeaf9077bd9", size = 1291957, upload-time = "2026-09-02T15:49:29.643Z" }, + { url = "https://files.pythonhosted.org/packages/8e/85/8ac18d754225cf13392786b23d4ba84273ceee562699362f22e61942ce64/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ac7b4cdf89ca8318aae157d824017596784982851c8a89a621973f261574696", size = 1291779, upload-time = "2026-09-02T15:49:31.199Z" }, + { url = "https://files.pythonhosted.org/packages/62/ed/cc757fec9e96e29f19a6f818e05147e4f2949356258a3243412756f22a2e/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:95dcb93f30258dcf09d9b6a302dac9323b70e9df8c18ee0bf4ea1fa7cc5f1875", size = 1299730, upload-time = "2026-09-02T15:49:32.774Z" }, + { url = "https://files.pythonhosted.org/packages/a7/8c/a575ae0ae954f21a187b4c1d8cec28d81a009693bd13a1388eec72d9b55a/ast_serialize-0.9.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1ed5824b4b2fa37ad93fa2db23d8243a3d315b3e2d7ca70f99b2727d8788af05", size = 1344671, upload-time = "2026-09-02T15:49:34.217Z" }, + { url = "https://files.pythonhosted.org/packages/80/41/0b2b15c0ae5f9a95f433016d1a59a3227eebbb378654d6354206c8ac8e8d/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3c69f2ce565c786bd3853ce42495e8a63610516f79651c7cb4f2cd0ddfaee52", size = 1448527, upload-time = "2026-09-02T15:49:35.68Z" }, + { url = "https://files.pythonhosted.org/packages/18/be/ee89cb6a5d3427946532f0611b514befdd69564803e9a9f9ce712f9d2654/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f8c824d822a2ac54ec4228b88c0d170ab0024cf285eeee57b9d4f994003fb553", size = 1554045, upload-time = "2026-09-02T15:49:37.15Z" }, + { url = "https://files.pythonhosted.org/packages/e0/4d/eaada807f98a2f0d370fec4b46c84f0e551a62911e751a76ecb32bef4dde/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c55010ea0fafc6bc8231809d328bda781bfe41a01c43522be5eb3713fd855cda", size = 1547578, upload-time = "2026-09-02T15:49:38.791Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/046c531f4af4e1bc3314077a84b3099f38b45e2d969faa24acedb3066d92/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:322282ac5337e5e776bc416f2b5201e680fa4dacf92ea739bd35e46a28a66c41", size = 1671896, upload-time = "2026-09-02T15:49:40.273Z" }, + { url = "https://files.pythonhosted.org/packages/6b/80/8d32aa0cf4e3e566399b2079a4c47f8ad3c62155f6ee1fe63631b6d3fdde/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:ded5ed06ec469407d6cd571ace7a7a25809cc388e4bac1dd35c7747469fc7fdf", size = 1472895, upload-time = "2026-09-02T15:49:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b9/8204c7e3d8abd4b0c7a56a8d5cce05fcacfd6a5d63ffbd812b8b94040d6d/ast_serialize-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3ee40752ddb4fb5c6a67161d13f3ce3df7987dcb9272260542a86b0be1519ae2", size = 1492731, upload-time = "2026-09-02T15:49:43.355Z" }, + { url = "https://files.pythonhosted.org/packages/f3/72/ff5c44c19409686798feeb1fbe209f2be78b4b64948d5aa2ddeec8901591/ast_serialize-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:d9c635eacfc02b91da6796d3b5ff9086e511b8a29b19f9b3f4f978b8d170f838", size = 1112847, upload-time = "2026-09-02T15:49:45.181Z" }, + { url = "https://files.pythonhosted.org/packages/20/75/fa5be1a94d189adafadf9c5f07fffd66af7a8061c4cff92f75285ef79d10/ast_serialize-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:62a96e327e2a178d6c295b10422e95c992a9286f0ec1b2bc7cd5b4873252a38c", size = 1146846, upload-time = "2026-09-02T15:49:46.63Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ef/b2bfb331b6e379d435543f6d3be0b590e7a2f441d31ccc17d75f2d2d7cb8/ast_serialize-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:41da4332492222d56345d5e436eed4fbec76caadee959f6ffa3cd2fc1bd51895", size = 1119605, upload-time = "2026-09-02T15:49:48.14Z" }, + { url = "https://files.pythonhosted.org/packages/e5/f9/a4af1bf8b35927814c09d90c3965dbfaa75c489ba34372bffafbc2209f40/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_10_12_x86_64.whl", hash = "sha256:383f56e3ae925f154632458f01b4bfcde3dd382f3ec04f5c7f6d72f76524ff48", size = 1226344, upload-time = "2026-09-02T15:49:49.79Z" }, + { url = "https://files.pythonhosted.org/packages/1d/6d/d3a95823a803c21f5c9df595a0bb93aada22e7aa22bf875fe00d89422d7f/ast_serialize-0.9.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:b9ef3d4173907bd19aa8f1683be9f06e7862e6cdf2ca6bca3633305c0df32063", size = 1207384, upload-time = "2026-09-02T15:49:51.22Z" }, + { url = "https://files.pythonhosted.org/packages/f0/97/6e7f46c8455b738609c29d1b7655307a168c4b40ce4c7a2c678c8ed9cf2e/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9482672ca8ec09f85cd050a053fb88c30c882c8e20ce7a140d8defe19c0ef2eb", size = 1273139, upload-time = "2026-09-02T15:49:52.679Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6e/25b70733f061766865cb04d913dc5332037c595796b871d52ab5b569abb8/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a6a568d1d489f0669a31aed90ca4845aa7f08e1b8cd5d05e1905dbdc3ae9b2b0", size = 1278242, upload-time = "2026-09-02T15:49:54.236Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f0/b7820399d9c5a0b7f07c239b6da93d2e21a1b3785137fa00e16528e414b3/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4dd005d4095a13eb312dc712c943c7730f262b000a87df963925328a38ffdb", size = 1541009, upload-time = "2026-09-02T15:49:56.149Z" }, + { url = "https://files.pythonhosted.org/packages/08/56/5146f1d2a77516e697f6f42825df79137e43560675cb4605c467775f8b4a/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:207ac73afa1f4654840593853c130eac2591dd942434176eea33f730afb3359b", size = 1290898, upload-time = "2026-09-02T15:49:57.502Z" }, + { url = "https://files.pythonhosted.org/packages/69/c4/87cd16228796d703de795a369b90b0f57f0f017f90f55c4c5876e3513a03/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ae01129e2cc5d57a3c434d8a990019de039350310a2e1dd3c9f61311964cf25", size = 1291742, upload-time = "2026-09-02T15:49:58.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/eb/13465c297268c5170b2bb746d75f37a8fad44a94a89b593071affc1071d0/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_31_riscv64.whl", hash = "sha256:4c522377f383670abfe21c94edc3032cb3bd34d8fcacd280fa9556907d4edd4b", size = 1300180, upload-time = "2026-09-02T15:50:00.454Z" }, + { url = "https://files.pythonhosted.org/packages/0a/a7/10b84c4274b2507b0ed9cc1654058ad64bcedb6ac574753d6a461bc6e204/ast_serialize-0.9.0-cp315-abi3.abi3t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4cd886f6e900f5e13f758cb8ef359652e690e4f3f9c257a7269e095940534167", size = 1345857, upload-time = "2026-09-02T15:50:01.875Z" }, + { url = "https://files.pythonhosted.org/packages/c7/dc/2702182c9773a15de9aabfaf66da7cb87548a56a6bac24f0c9176a4a13c3/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:f3f0f3359cf0f22bf096b07021ffe6bf0ec88ac8a2cf7ce5f4701af973112faa", size = 1448544, upload-time = "2026-09-02T15:50:03.496Z" }, + { url = "https://files.pythonhosted.org/packages/59/b5/eeef2124c9563b9861707ef4db91f153f3bb37b3e0cca9543bb88a4e9e53/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_armv7l.whl", hash = "sha256:6c428444656cffbd32c1e76626c6eec5237b58c8fcb0b5d3df75941cd50c4f3c", size = 1551572, upload-time = "2026-09-02T15:50:04.982Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8c/81d18349f1dffdcfeb80671bd737e342c86348e183692d9d0d8f573d1385/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_i686.whl", hash = "sha256:54f0babed5e2a4eb86a0716ac612aff33f933e7572e5bc067adcdbe672a26321", size = 1548118, upload-time = "2026-09-02T15:50:06.522Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1f/339131b60d1b0df13d9f3470cfac70f858b5649188ea01b2e7b39caeb720/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b779fdaee34d19900a5ba5fd6bd4cefe225650081f9de28de256eacee5113c2", size = 1674707, upload-time = "2026-09-02T15:50:07.919Z" }, + { url = "https://files.pythonhosted.org/packages/18/0a/ca77596fa229d88f96eca180d45dbe8efa11306f8b2b4f4ee301b3fe465f/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_riscv64.whl", hash = "sha256:4db7f524eaa857fbe650cac33b9cedb5ccda14393d40f640b75dfb06aa13c98c", size = 1473618, upload-time = "2026-09-02T15:50:09.312Z" }, + { url = "https://files.pythonhosted.org/packages/88/5c/6aebeb54dd226b480014ff4488e150aa23b1de3204e2bf3f87de27e6542a/ast_serialize-0.9.0-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:d2a37795a90809da6094825e7063118b4cf723b8701b134973b4566ed8b9ea09", size = 1492025, upload-time = "2026-09-02T15:50:10.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/c355d470a230f778311c28b80f6d934a497d094139f07230433eea18651b/ast_serialize-0.9.0-cp315-abi3.abi3t-win32.whl", hash = "sha256:4411d1cba9eeecb301365343a7e96813b4a44fcdb20181557867ff7e751804cf", size = 1113010, upload-time = "2026-09-02T15:50:12.337Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0d/66609ace58564727b68731293cc986c2ea1d5e6ef40e96571e7fb515f0af/ast_serialize-0.9.0-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:b5c3724faf780e25def89c369eb6340a15dff06ac348160c61781e2373a4cd10", size = 1146404, upload-time = "2026-09-02T15:50:13.761Z" }, + { url = "https://files.pythonhosted.org/packages/b1/fd/da28e1c85f05fb9976f247d2a3aefce68866cb2939abcbdbddd9a5e3b835/ast_serialize-0.9.0-cp315-abi3.abi3t-win_arm64.whl", hash = "sha256:1de0933a4c1d104d77d6e75f053f5e628b54cf8f9fea809b8250cb04cda07bd3", size = 1118328, upload-time = "2026-09-02T15:50:15.214Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8b/487a158a99e4564244e000ed18475255dfea53fd34a84d8ca73633710500/ast_serialize-0.9.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:5fc57f17fb4ce49b4eeccfcd2670e4a55659bd740bb8e8aedbe511ccab8b5f03", size = 889484, upload-time = "2026-09-02T15:50:16.643Z" }, + { url = "https://files.pythonhosted.org/packages/92/e4/175b0a64d6c96bc1b96598c6474ce8d1ef34e0b774bcf7183f4ce696fb10/ast_serialize-0.9.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dac690f99538d9df0d23ce0299e946add2744b007a36b480a292fe361c82553d", size = 1232635, upload-time = "2026-09-02T15:50:18.133Z" }, + { url = "https://files.pythonhosted.org/packages/28/0c/d51d8463aca43aaa833fdf1f25134d6cc1b483764896decca61306ad1f6e/ast_serialize-0.9.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:2223ead73b5a5399d39610cf9c4164ad0b2bf2025226626b87ae15226d93d3f7", size = 1219313, upload-time = "2026-09-02T15:50:19.497Z" }, + { url = "https://files.pythonhosted.org/packages/ef/19/c88bdc64f86095a9d6ab325ae422b2a5e1395cd63cd8aa539003d4d4ae1d/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b7c5f5838408fb000d76abd14e886836412b7ec7eccd028dbb5ed5819780008", size = 1279981, upload-time = "2026-09-02T15:50:20.811Z" }, + { url = "https://files.pythonhosted.org/packages/86/58/a492075826df1753896dc8e8f6ababae4016d8883b670ee3a1c34788b154/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1e05701fde79affa1cc53e391867f9da3eb03fa8501f87354292796b0f8398fd", size = 1286319, upload-time = "2026-09-02T15:50:22.203Z" }, + { url = "https://files.pythonhosted.org/packages/ae/79/3f6754eaa42fd2a6c36aac066890870cd44cbe0e25f75a67b1b99a2f4d82/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d013c36eb2f2ac0cb7d4d0e79918a92ab00fbce8f1542fe47f34a46e06168f82", size = 1551547, upload-time = "2026-09-02T15:50:23.528Z" }, + { url = "https://files.pythonhosted.org/packages/b1/05/8cfb7caadfaf28febaa6b61d31d778262f87f9366eda4dd9bd07ac940b75/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:19cde5c2110f7b90ab1210a599178524f6c9f34862b20ba2b9aa7832c67bb35d", size = 1302468, upload-time = "2026-09-02T15:50:24.99Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e2/750a0b136bb02ff8e4a17d65a3a78cd478ee50724704df8215797a226ba3/ast_serialize-0.9.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1514f4a39704e2e815f9fc675fc13f19f694f212086b520110840782cf3c5295", size = 1300563, upload-time = "2026-09-02T15:50:26.354Z" }, + { url = "https://files.pythonhosted.org/packages/ab/17/4c0aa852ff1e4f2d6723e8ce827136c1e1febf2845d7941ccc45426778de/ast_serialize-0.9.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:30651ccdec6d23c49ee4711b1a1096d8dbd3be38eecf2f09fdd98a608ce7ac24", size = 1308999, upload-time = "2026-09-02T15:50:27.901Z" }, + { url = "https://files.pythonhosted.org/packages/4d/1b/6e73d0a29aedb0db30cc68f2557acaac06cd24c9783ccb90f84f89e4ce87/ast_serialize-0.9.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d9a46caf5e3f2cd266e8638b2f4ea8bf54cf376f015f8418397b4633fdb38e9b", size = 1358191, upload-time = "2026-09-02T15:50:29.237Z" }, + { url = "https://files.pythonhosted.org/packages/dc/38/2cf5d552de99e0e9804a16fea73e54d0a7382498adddf57c0f6dc09cbc70/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:aed6e413c6c22a23c33c47a01dd2adce01d7a7ed408748e896903f47d0a1aa47", size = 1458944, upload-time = "2026-09-02T15:50:30.77Z" }, + { url = "https://files.pythonhosted.org/packages/4b/0b/5ef87adf955b6a027f616eb7b55f55a154c35ba600e9dd2d06ad2d30e5c2/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:871fb7c5b049897ee137b67efad7fe4545ad270f7eccd970da877833f8e63aa7", size = 1563421, upload-time = "2026-09-02T15:50:32.188Z" }, + { url = "https://files.pythonhosted.org/packages/81/dd/9ced05a17feeb0f83e84010d80f5a1b7b7aa19e75f0376f4d3780803654c/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:bb378efb5537b43f38660e2e6d6e138a40885cf191d43443bb3ff7ff47e9cd9b", size = 1558536, upload-time = "2026-09-02T15:50:33.861Z" }, + { url = "https://files.pythonhosted.org/packages/5f/8b/8ad486e44fc7081a2471055befc433dddc2e51c3a88dff141b3026f64602/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:b373beff65b01fcffca5aaad3269ae629f3a998b09efdd3635e48039008a5dec", size = 1682749, upload-time = "2026-09-02T15:50:35.257Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4c/7c282aba9cfb0b92d79fac45c04e4557d9a7f08d872e5a43577a50867e30/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:25c8d517c45cf2b1820fc2af6ac593783654818f79d05646d25d624360678a4e", size = 1482441, upload-time = "2026-09-02T15:50:37.319Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/e45914e8cad81b660915f3784d255460a6384183b76bfc2089fdd79ec7df/ast_serialize-0.9.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1675dc46578298ae00936164a160997801a6ca2385913150d8d16df634296cf3", size = 1499042, upload-time = "2026-09-02T15:50:38.76Z" }, + { url = "https://files.pythonhosted.org/packages/99/09/6988921dec19c810beef53539fec2e90ae551cd93853b3303a99fe45f772/ast_serialize-0.9.0-cp39-abi3-win32.whl", hash = "sha256:20fce3885eeff05a3d6afefa845c8168016e3ea1f6fc9cdc84c8db28b863a550", size = 1116391, upload-time = "2026-09-02T15:50:40.229Z" }, + { url = "https://files.pythonhosted.org/packages/fd/eb/839598a22a1f9af56d39e188451cad93dbcb0ce6539a45ac18fb8bf123fa/ast_serialize-0.9.0-cp39-abi3-win_amd64.whl", hash = "sha256:161914666a21d48b681982146ac0fa4086ef099d91c637cf595387f5f06aa099", size = 1156055, upload-time = "2026-09-02T15:50:42.05Z" }, + { url = "https://files.pythonhosted.org/packages/0d/45/c7cd8d36d3b506bbd02db5066fae3340284781168f0d08dac25deef5f69d/ast_serialize-0.9.0-cp39-abi3-win_arm64.whl", hash = "sha256:74473258a5c55855d5306c864a5c799fbff03a0f0ea1197346b2b5cc5b4ea48a", size = 1128237, upload-time = "2026-09-02T15:50:43.496Z" }, ] [[package]] @@ -196,172 +271,273 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, ] +[[package]] +name = "bandit" +version = "1.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "pyyaml" }, + { name = "rich" }, + { name = "stevedore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/c3/0cb80dfe0f3076e5da7e4c5ad8e57bac6ac357ff4a6406205501cade4965/bandit-1.9.4.tar.gz", hash = "sha256:b589e5de2afe70bd4d53fa0c1da6199f4085af666fde00e8a034f152a52cd628", size = 4242677, upload-time = "2026-02-25T06:44:15.503Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/a4/a26d5b25671d27e03afb5401a0be5899d94ff8fab6a698b1ac5be3ec29ef/bandit-1.9.4-py3-none-any.whl", hash = "sha256:f89ffa663767f5a0585ea075f01020207e966a9c0f2b9ef56a57c7963a3f6f8e", size = 134741, upload-time = "2026-02-25T06:44:13.694Z" }, +] + [[package]] name = "certifi" -version = "2026.2.25" +version = "2026.7.22" 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, upload-time = "2026-02-25T02:54:17.342Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" }, ] [[package]] name = "cffi" -version = "2.0.0" +version = "2.1.1" 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, upload-time = "2025-09-08T23:24:04.541Z" } -wheels = [ - { 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, upload-time = "2025-09-08T23:22:26.456Z" }, - { 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, upload-time = "2025-09-08T23:22:28.197Z" }, - { 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, upload-time = "2025-09-08T23:22:29.475Z" }, - { 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, upload-time = "2025-09-08T23:22:31.063Z" }, - { 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, upload-time = "2025-09-08T23:22:32.507Z" }, - { 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, upload-time = "2025-09-08T23:22:34.132Z" }, - { 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, upload-time = "2025-09-08T23:22:35.443Z" }, - { 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, upload-time = "2025-09-08T23:22:36.805Z" }, - { 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, upload-time = "2025-09-08T23:22:38.436Z" }, - { 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, upload-time = "2025-09-08T23:22:39.776Z" }, - { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, - { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, - { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, - { 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, upload-time = "2025-09-08T23:22:44.795Z" }, - { 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, upload-time = "2025-09-08T23:22:45.938Z" }, - { 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, upload-time = "2025-09-08T23:22:47.349Z" }, - { 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, upload-time = "2025-09-08T23:22:48.677Z" }, - { 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, upload-time = "2025-09-08T23:22:50.06Z" }, - { 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, upload-time = "2025-09-08T23:22:51.364Z" }, - { 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, upload-time = "2025-09-08T23:22:52.902Z" }, - { 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, upload-time = "2025-09-08T23:22:54.518Z" }, - { 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, upload-time = "2025-09-08T23:22:55.867Z" }, - { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, - { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, - { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, - { 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, upload-time = "2025-09-08T23:23:00.879Z" }, - { 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, upload-time = "2025-09-08T23:23:02.231Z" }, - { 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, upload-time = "2025-09-08T23:23:03.472Z" }, - { 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, upload-time = "2025-09-08T23:23:04.792Z" }, - { 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, upload-time = "2025-09-08T23:23:06.127Z" }, - { 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, upload-time = "2025-09-08T23:23:07.753Z" }, - { 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, upload-time = "2025-09-08T23:23:09.648Z" }, - { 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, upload-time = "2025-09-08T23:23:10.928Z" }, - { 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, upload-time = "2025-09-08T23:23:12.42Z" }, - { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, - { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, - { 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, upload-time = "2025-09-08T23:23:18.087Z" }, - { 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, upload-time = "2025-09-08T23:23:19.622Z" }, - { 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, upload-time = "2025-09-08T23:23:20.853Z" }, - { 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, upload-time = "2025-09-08T23:23:22.08Z" }, - { 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, upload-time = "2025-09-08T23:23:23.314Z" }, - { 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, upload-time = "2025-09-08T23:23:24.541Z" }, - { 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, upload-time = "2025-09-08T23:23:26.143Z" }, - { 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, upload-time = "2025-09-08T23:23:27.873Z" }, - { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, - { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, - { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, - { 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, upload-time = "2025-09-08T23:23:29.347Z" }, - { 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, upload-time = "2025-09-08T23:23:30.63Z" }, - { 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, upload-time = "2025-09-08T23:23:31.91Z" }, - { 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, upload-time = "2025-09-08T23:23:33.214Z" }, - { 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, upload-time = "2025-09-08T23:23:34.495Z" }, - { 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, upload-time = "2025-09-08T23:23:36.096Z" }, - { 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, upload-time = "2025-09-08T23:23:37.328Z" }, - { 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, upload-time = "2025-09-08T23:23:38.945Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, - { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, - { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/d2/16d99a0c4948febc0ebd133a13b2f688ff7f8cb04da971e1128872ce0c03/cffi-2.1.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:c8d2c9fd1f2d16f780d15127abb050d13d1a76c03a4bd87d7e4980e45e511e12", size = 183838, upload-time = "2026-08-03T21:19:29.637Z" }, + { url = "https://files.pythonhosted.org/packages/cd/95/31b535a9f0220ae9f357de4a08d57ce89cb417653c2fd9f075f50822a388/cffi-2.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:398aff33cee2767e3e781d2554c54bd0dff386bb437581e0d8011fde1a942ec1", size = 184168, upload-time = "2026-08-03T21:19:30.764Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5a/4707a0dc1f203f5dde5a907b0d4e3c25d71120241048bd5bc6f1bb9d4e71/cffi-2.1.1-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:154852545011f779917b11c78db2358d095da62a9a172b78ad0a583ee5adc0d0", size = 211805, upload-time = "2026-08-03T21:19:31.867Z" }, + { url = "https://files.pythonhosted.org/packages/ad/66/c19feabb28485b6e0bbaaafa90837a1ef5d302e90f2178bd33f17a49879b/cffi-2.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3311ed60d36f83378794e1009ac6258bafbf81f7888b4caa7b35a521e3f95813", size = 218716, upload-time = "2026-08-03T21:19:32.896Z" }, + { url = "https://files.pythonhosted.org/packages/a7/92/500760486c8baab49a7a8a58ba7fc3355ec3974b454b8a09e528efde9e1d/cffi-2.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6e192623c49c94421616a5778fba35cf0d5a8d000650c1967ef4448ee5cdd990", size = 205569, upload-time = "2026-08-03T21:19:34.142Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a7/a67c733254d6e7373f7822f8082d8d6beade791e0cf12a7611f376fa61c7/cffi-2.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a6e721d4b0e45d5b65e87534470e67b18dcd092c83f68fba09f152b9cbc061af", size = 204907, upload-time = "2026-08-03T21:19:35.174Z" }, + { url = "https://files.pythonhosted.org/packages/f7/a4/4399daaf8f7dfee9d7c3327fdb0426ee041cc63edc358b93911ceb2bfc7a/cffi-2.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:34e261f78cb6ceaaa36f42f2613f4380d94d9c759a9c73c769ee6e0247364632", size = 217807, upload-time = "2026-08-03T21:19:36.286Z" }, + { url = "https://files.pythonhosted.org/packages/28/f7/dabe6da2466ecbd82dc62e7342dc6b1065dad990c06f00f0ede9ebf2a0ed/cffi-2.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7225e4514edb64eb6740324353e0da0711954fd8d7da4576755b1c6e09b697cd", size = 221252, upload-time = "2026-08-03T21:19:37.416Z" }, + { url = "https://files.pythonhosted.org/packages/ce/87/616202d8e51342c07d2534c510111c4cc37201775ce8f60802c9335d1edd/cffi-2.1.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:df913725b79db7bcf03448f36b7bf8815363417d5b58deecf9305e3e30f0f21a", size = 214214, upload-time = "2026-08-03T21:19:38.507Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c6/ab025d75d2c26c19b087c0124e75ee31cb65032f4fe345d356d8c507ab97/cffi-2.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f5cfbc5fe74540d335175b656c725d74d90e3730c626d92575eea35029d9afaa", size = 219408, upload-time = "2026-08-03T21:19:39.809Z" }, + { url = "https://files.pythonhosted.org/packages/db/e2/7e8109f65445bdc673a7b54f02c677de462db75674220fd1335efc8eb598/cffi-2.1.1-cp311-cp311-win32.whl", hash = "sha256:f8ec5e643a9a937f64e1999eb9f75d072263751912dc5cd06d3c85f8f44be7c3", size = 174470, upload-time = "2026-08-03T21:19:41.246Z" }, + { url = "https://files.pythonhosted.org/packages/73/c0/77ba02423c2f7d7091143c45cd49e0e6575c4c1967394bb542bd923a9b74/cffi-2.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:42f6930c31dc7f50732c9ae793c2786c7b6b044195967bbdde40bb9be81c4cc0", size = 185096, upload-time = "2026-08-03T21:19:42.615Z" }, + { url = "https://files.pythonhosted.org/packages/7c/47/9f1f85f9672ceda4984dc6c4f8824e8558992a2972c3d3c81fb8eb28d4ba/cffi-2.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:c7659f22557c5a0bc4855cd635f55edec690cc008a40768527762cb9fb263455", size = 179941, upload-time = "2026-08-03T21:19:43.747Z" }, + { url = "https://files.pythonhosted.org/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" }, + { url = "https://files.pythonhosted.org/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" }, + { url = "https://files.pythonhosted.org/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" }, + { url = "https://files.pythonhosted.org/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" }, + { url = "https://files.pythonhosted.org/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" }, + { url = "https://files.pythonhosted.org/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" }, + { url = "https://files.pythonhosted.org/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" }, + { url = "https://files.pythonhosted.org/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" }, + { url = "https://files.pythonhosted.org/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" }, + { url = "https://files.pythonhosted.org/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" }, + { url = "https://files.pythonhosted.org/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" }, + { url = "https://files.pythonhosted.org/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" }, + { url = "https://files.pythonhosted.org/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" }, + { url = "https://files.pythonhosted.org/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" }, + { url = "https://files.pythonhosted.org/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" }, + { url = "https://files.pythonhosted.org/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" }, + { url = "https://files.pythonhosted.org/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" }, + { url = "https://files.pythonhosted.org/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" }, + { url = "https://files.pythonhosted.org/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" }, + { url = "https://files.pythonhosted.org/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" }, + { url = "https://files.pythonhosted.org/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" }, + { url = "https://files.pythonhosted.org/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" }, + { url = "https://files.pythonhosted.org/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" }, + { url = "https://files.pythonhosted.org/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" }, + { url = "https://files.pythonhosted.org/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" }, + { url = "https://files.pythonhosted.org/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" }, + { url = "https://files.pythonhosted.org/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" }, + { url = "https://files.pythonhosted.org/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" }, + { url = "https://files.pythonhosted.org/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" }, + { url = "https://files.pythonhosted.org/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" }, + { url = "https://files.pythonhosted.org/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" }, + { url = "https://files.pythonhosted.org/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" }, + { url = "https://files.pythonhosted.org/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" }, + { url = "https://files.pythonhosted.org/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" }, + { url = "https://files.pythonhosted.org/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" }, + { url = "https://files.pythonhosted.org/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" }, + { url = "https://files.pythonhosted.org/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" }, + { url = "https://files.pythonhosted.org/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" }, + { url = "https://files.pythonhosted.org/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" }, + { url = "https://files.pythonhosted.org/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" }, + { url = "https://files.pythonhosted.org/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" }, + { url = "https://files.pythonhosted.org/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" }, + { url = "https://files.pythonhosted.org/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" }, + { url = "https://files.pythonhosted.org/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" }, + { url = "https://files.pythonhosted.org/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" }, + { url = "https://files.pythonhosted.org/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" }, + { url = "https://files.pythonhosted.org/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" }, + { url = "https://files.pythonhosted.org/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" }, + { url = "https://files.pythonhosted.org/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" }, + { url = "https://files.pythonhosted.org/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" }, + { url = "https://files.pythonhosted.org/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" }, + { url = "https://files.pythonhosted.org/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" }, + { url = "https://files.pythonhosted.org/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" }, + { url = "https://files.pythonhosted.org/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" }, ] [[package]] name = "charset-normalizer" -version = "3.4.6" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/60/e3bec1881450851b087e301bedc3daa9377a4d45f1c26aa90b0b235e38aa/charset_normalizer-3.4.6.tar.gz", hash = "sha256:1ae6b62897110aa7c79ea2f5dd38d1abca6db663687c0b1ad9aed6f6bae3d9d6", size = 143363, upload-time = "2026-03-15T18:53:25.478Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/28/ff6f234e628a2de61c458be2779cb182bc03f6eec12200d4a525bbfc9741/charset_normalizer-3.4.6-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:82060f995ab5003a2d6e0f4ad29065b7672b6593c8c63559beefe5b443242c3e", size = 293582, upload-time = "2026-03-15T18:50:25.454Z" }, - { url = "https://files.pythonhosted.org/packages/1c/b7/b1a117e5385cbdb3205f6055403c2a2a220c5ea80b8716c324eaf75c5c95/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60c74963d8350241a79cb8feea80e54d518f72c26db618862a8f53e5023deaf9", size = 197240, upload-time = "2026-03-15T18:50:27.196Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5f/2574f0f09f3c3bc1b2f992e20bce6546cb1f17e111c5be07308dc5427956/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6e4333fb15c83f7d1482a76d45a0818897b3d33f00efd215528ff7c51b8e35d", size = 217363, upload-time = "2026-03-15T18:50:28.601Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d1/0ae20ad77bc949ddd39b51bf383b6ca932f2916074c95cad34ae465ab71f/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bc72863f4d9aba2e8fd9085e63548a324ba706d2ea2c83b260da08a59b9482de", size = 212994, upload-time = "2026-03-15T18:50:30.102Z" }, - { url = "https://files.pythonhosted.org/packages/60/ac/3233d262a310c1b12633536a07cde5ddd16985e6e7e238e9f3f9423d8eb9/charset_normalizer-3.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9cc4fc6c196d6a8b76629a70ddfcd4635a6898756e2d9cac5565cf0654605d73", size = 204697, upload-time = "2026-03-15T18:50:31.654Z" }, - { url = "https://files.pythonhosted.org/packages/25/3c/8a18fc411f085b82303cfb7154eed5bd49c77035eb7608d049468b53f87c/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0c173ce3a681f309f31b87125fecec7a5d1347261ea11ebbb856fa6006b23c8c", size = 191673, upload-time = "2026-03-15T18:50:33.433Z" }, - { url = "https://files.pythonhosted.org/packages/ff/a7/11cfe61d6c5c5c7438d6ba40919d0306ed83c9ab957f3d4da2277ff67836/charset_normalizer-3.4.6-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c907cdc8109f6c619e6254212e794d6548373cc40e1ec75e6e3823d9135d29cc", size = 201120, upload-time = "2026-03-15T18:50:35.105Z" }, - { url = "https://files.pythonhosted.org/packages/b5/10/cf491fa1abd47c02f69687046b896c950b92b6cd7337a27e6548adbec8e4/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:404a1e552cf5b675a87f0651f8b79f5f1e6fd100ee88dc612f89aa16abd4486f", size = 200911, upload-time = "2026-03-15T18:50:36.819Z" }, - { url = "https://files.pythonhosted.org/packages/28/70/039796160b48b18ed466fde0af84c1b090c4e288fae26cd674ad04a2d703/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e3c701e954abf6fc03a49f7c579cc80c2c6cc52525340ca3186c41d3f33482ef", size = 192516, upload-time = "2026-03-15T18:50:38.228Z" }, - { url = "https://files.pythonhosted.org/packages/ff/34/c56f3223393d6ff3124b9e78f7de738047c2d6bc40a4f16ac0c9d7a1cb3c/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:7a6967aaf043bceabab5412ed6bd6bd26603dae84d5cb75bf8d9a74a4959d398", size = 218795, upload-time = "2026-03-15T18:50:39.664Z" }, - { url = "https://files.pythonhosted.org/packages/e8/3b/ce2d4f86c5282191a041fdc5a4ce18f1c6bd40a5bd1f74cf8625f08d51c1/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:5feb91325bbceade6afab43eb3b508c63ee53579fe896c77137ded51c6b6958e", size = 201833, upload-time = "2026-03-15T18:50:41.552Z" }, - { url = "https://files.pythonhosted.org/packages/3b/9b/b6a9f76b0fd7c5b5ec58b228ff7e85095370282150f0bd50b3126f5506d6/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f820f24b09e3e779fe84c3c456cb4108a7aa639b0d1f02c28046e11bfcd088ed", size = 213920, upload-time = "2026-03-15T18:50:43.33Z" }, - { url = "https://files.pythonhosted.org/packages/ae/98/7bc23513a33d8172365ed30ee3a3b3fe1ece14a395e5fc94129541fc6003/charset_normalizer-3.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b35b200d6a71b9839a46b9b7fff66b6638bb52fc9658aa58796b0326595d3021", size = 206951, upload-time = "2026-03-15T18:50:44.789Z" }, - { url = "https://files.pythonhosted.org/packages/32/73/c0b86f3d1458468e11aec870e6b3feac931facbe105a894b552b0e518e79/charset_normalizer-3.4.6-cp311-cp311-win32.whl", hash = "sha256:9ca4c0b502ab399ef89248a2c84c54954f77a070f28e546a85e91da627d1301e", size = 143703, upload-time = "2026-03-15T18:50:46.103Z" }, - { url = "https://files.pythonhosted.org/packages/c6/e3/76f2facfe8eddee0bbd38d2594e709033338eae44ebf1738bcefe0a06185/charset_normalizer-3.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:a9e68c9d88823b274cf1e72f28cb5dc89c990edf430b0bfd3e2fb0785bfeabf4", size = 153857, upload-time = "2026-03-15T18:50:47.563Z" }, - { url = "https://files.pythonhosted.org/packages/e2/dc/9abe19c9b27e6cd3636036b9d1b387b78c40dedbf0b47f9366737684b4b0/charset_normalizer-3.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:97d0235baafca5f2b09cf332cc275f021e694e8362c6bb9c96fc9a0eb74fc316", size = 142751, upload-time = "2026-03-15T18:50:49.234Z" }, - { url = "https://files.pythonhosted.org/packages/e5/62/c0815c992c9545347aeea7859b50dc9044d147e2e7278329c6e02ac9a616/charset_normalizer-3.4.6-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:2ef7fedc7a6ecbe99969cd09632516738a97eeb8bd7258bf8a0f23114c057dab", size = 295154, upload-time = "2026-03-15T18:50:50.88Z" }, - { url = "https://files.pythonhosted.org/packages/a8/37/bdca6613c2e3c58c7421891d80cc3efa1d32e882f7c4a7ee6039c3fc951a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4ea868bc28109052790eb2b52a9ab33f3aa7adc02f96673526ff47419490e21", size = 199191, upload-time = "2026-03-15T18:50:52.658Z" }, - { url = "https://files.pythonhosted.org/packages/6c/92/9934d1bbd69f7f398b38c5dae1cbf9cc672e7c34a4adf7b17c0a9c17d15d/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:836ab36280f21fc1a03c99cd05c6b7af70d2697e374c7af0b61ed271401a72a2", size = 218674, upload-time = "2026-03-15T18:50:54.102Z" }, - { url = "https://files.pythonhosted.org/packages/af/90/25f6ab406659286be929fd89ab0e78e38aa183fc374e03aa3c12d730af8a/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f1ce721c8a7dfec21fcbdfe04e8f68174183cf4e8188e0645e92aa23985c57ff", size = 215259, upload-time = "2026-03-15T18:50:55.616Z" }, - { url = "https://files.pythonhosted.org/packages/4e/ef/79a463eb0fff7f96afa04c1d4c51f8fc85426f918db467854bfb6a569ce3/charset_normalizer-3.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e28d62a8fc7a1fa411c43bd65e346f3bce9716dc51b897fbe930c5987b402d5", size = 207276, upload-time = "2026-03-15T18:50:57.054Z" }, - { url = "https://files.pythonhosted.org/packages/f7/72/d0426afec4b71dc159fa6b4e68f868cd5a3ecd918fec5813a15d292a7d10/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:530d548084c4a9f7a16ed4a294d459b4f229db50df689bfe92027452452943a0", size = 195161, upload-time = "2026-03-15T18:50:58.686Z" }, - { url = "https://files.pythonhosted.org/packages/bf/18/c82b06a68bfcb6ce55e508225d210c7e6a4ea122bfc0748892f3dc4e8e11/charset_normalizer-3.4.6-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:30f445ae60aad5e1f8bdbb3108e39f6fbc09f4ea16c815c66578878325f8f15a", size = 203452, upload-time = "2026-03-15T18:51:00.196Z" }, - { url = "https://files.pythonhosted.org/packages/44/d6/0c25979b92f8adafdbb946160348d8d44aa60ce99afdc27df524379875cb/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ac2393c73378fea4e52aa56285a3d64be50f1a12395afef9cce47772f60334c2", size = 202272, upload-time = "2026-03-15T18:51:01.703Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3d/7fea3e8fe84136bebbac715dd1221cc25c173c57a699c030ab9b8900cbb7/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:90ca27cd8da8118b18a52d5f547859cc1f8354a00cd1e8e5120df3e30d6279e5", size = 195622, upload-time = "2026-03-15T18:51:03.526Z" }, - { url = "https://files.pythonhosted.org/packages/57/8a/d6f7fd5cb96c58ef2f681424fbca01264461336d2a7fc875e4446b1f1346/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e5a94886bedca0f9b78fecd6afb6629142fd2605aa70a125d49f4edc6037ee6", size = 220056, upload-time = "2026-03-15T18:51:05.269Z" }, - { url = "https://files.pythonhosted.org/packages/16/50/478cdda782c8c9c3fb5da3cc72dd7f331f031e7f1363a893cdd6ca0f8de0/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:695f5c2823691a25f17bc5d5ffe79fa90972cc34b002ac6c843bb8a1720e950d", size = 203751, upload-time = "2026-03-15T18:51:06.858Z" }, - { url = "https://files.pythonhosted.org/packages/75/fc/cc2fcac943939c8e4d8791abfa139f685e5150cae9f94b60f12520feaa9b/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:231d4da14bcd9301310faf492051bee27df11f2bc7549bc0bb41fef11b82daa2", size = 216563, upload-time = "2026-03-15T18:51:08.564Z" }, - { url = "https://files.pythonhosted.org/packages/a8/b7/a4add1d9a5f68f3d037261aecca83abdb0ab15960a3591d340e829b37298/charset_normalizer-3.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a056d1ad2633548ca18ffa2f85c202cfb48b68615129143915b8dc72a806a923", size = 209265, upload-time = "2026-03-15T18:51:10.312Z" }, - { url = "https://files.pythonhosted.org/packages/6c/18/c094561b5d64a24277707698e54b7f67bd17a4f857bbfbb1072bba07c8bf/charset_normalizer-3.4.6-cp312-cp312-win32.whl", hash = "sha256:c2274ca724536f173122f36c98ce188fd24ce3dad886ec2b7af859518ce008a4", size = 144229, upload-time = "2026-03-15T18:51:11.694Z" }, - { url = "https://files.pythonhosted.org/packages/ab/20/0567efb3a8fd481b8f34f739ebddc098ed062a59fed41a8d193a61939e8f/charset_normalizer-3.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:c8ae56368f8cc97c7e40a7ee18e1cedaf8e780cd8bc5ed5ac8b81f238614facb", size = 154277, upload-time = "2026-03-15T18:51:13.004Z" }, - { url = "https://files.pythonhosted.org/packages/15/57/28d79b44b51933119e21f65479d0864a8d5893e494cf5daab15df0247c17/charset_normalizer-3.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:899d28f422116b08be5118ef350c292b36fc15ec2daeb9ea987c89281c7bb5c4", size = 142817, upload-time = "2026-03-15T18:51:14.408Z" }, - { url = "https://files.pythonhosted.org/packages/1e/1d/4fdabeef4e231153b6ed7567602f3b68265ec4e5b76d6024cf647d43d981/charset_normalizer-3.4.6-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:11afb56037cbc4b1555a34dd69151e8e069bee82e613a73bef6e714ce733585f", size = 294823, upload-time = "2026-03-15T18:51:15.755Z" }, - { url = "https://files.pythonhosted.org/packages/47/7b/20e809b89c69d37be748d98e84dce6820bf663cf19cf6b942c951a3e8f41/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:423fb7e748a08f854a08a222b983f4df1912b1daedce51a72bd24fe8f26a1843", size = 198527, upload-time = "2026-03-15T18:51:17.177Z" }, - { url = "https://files.pythonhosted.org/packages/37/a6/4f8d27527d59c039dce6f7622593cdcd3d70a8504d87d09eb11e9fdc6062/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d73beaac5e90173ac3deb9928a74763a6d230f494e4bfb422c217a0ad8e629bf", size = 218388, upload-time = "2026-03-15T18:51:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/f6/9b/4770ccb3e491a9bacf1c46cc8b812214fe367c86a96353ccc6daf87b01ec/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d60377dce4511655582e300dc1e5a5f24ba0cb229005a1d5c8d0cb72bb758ab8", size = 214563, upload-time = "2026-03-15T18:51:20.374Z" }, - { url = "https://files.pythonhosted.org/packages/2b/58/a199d245894b12db0b957d627516c78e055adc3a0d978bc7f65ddaf7c399/charset_normalizer-3.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:530e8cebeea0d76bdcf93357aa5e41336f48c3dc709ac52da2bb167c5b8271d9", size = 206587, upload-time = "2026-03-15T18:51:21.807Z" }, - { url = "https://files.pythonhosted.org/packages/7e/70/3def227f1ec56f5c69dfc8392b8bd63b11a18ca8178d9211d7cc5e5e4f27/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:a26611d9987b230566f24a0a125f17fe0de6a6aff9f25c9f564aaa2721a5fb88", size = 194724, upload-time = "2026-03-15T18:51:23.508Z" }, - { url = "https://files.pythonhosted.org/packages/58/ab/9318352e220c05efd31c2779a23b50969dc94b985a2efa643ed9077bfca5/charset_normalizer-3.4.6-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:34315ff4fc374b285ad7f4a0bf7dcbfe769e1b104230d40f49f700d4ab6bbd84", size = 202956, upload-time = "2026-03-15T18:51:25.239Z" }, - { url = "https://files.pythonhosted.org/packages/75/13/f3550a3ac25b70f87ac98c40d3199a8503676c2f1620efbf8d42095cfc40/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5f8ddd609f9e1af8c7bd6e2aca279c931aefecd148a14402d4e368f3171769fd", size = 201923, upload-time = "2026-03-15T18:51:26.682Z" }, - { url = "https://files.pythonhosted.org/packages/1b/db/c5c643b912740b45e8eec21de1bbab8e7fc085944d37e1e709d3dcd9d72f/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:80d0a5615143c0b3225e5e3ef22c8d5d51f3f72ce0ea6fb84c943546c7b25b6c", size = 195366, upload-time = "2026-03-15T18:51:28.129Z" }, - { url = "https://files.pythonhosted.org/packages/5a/67/3b1c62744f9b2448443e0eb160d8b001c849ec3fef591e012eda6484787c/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:92734d4d8d187a354a556626c221cd1a892a4e0802ccb2af432a1d85ec012194", size = 219752, upload-time = "2026-03-15T18:51:29.556Z" }, - { url = "https://files.pythonhosted.org/packages/f6/98/32ffbaf7f0366ffb0445930b87d103f6b406bc2c271563644bde8a2b1093/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:613f19aa6e082cf96e17e3ffd89383343d0d589abda756b7764cf78361fd41dc", size = 203296, upload-time = "2026-03-15T18:51:30.921Z" }, - { url = "https://files.pythonhosted.org/packages/41/12/5d308c1bbe60cabb0c5ef511574a647067e2a1f631bc8634fcafaccd8293/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2b1a63e8224e401cafe7739f77efd3f9e7f5f2026bda4aead8e59afab537784f", size = 215956, upload-time = "2026-03-15T18:51:32.399Z" }, - { url = "https://files.pythonhosted.org/packages/53/e9/5f85f6c5e20669dbe56b165c67b0260547dea97dba7e187938833d791687/charset_normalizer-3.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6cceb5473417d28edd20c6c984ab6fee6c6267d38d906823ebfe20b03d607dc2", size = 208652, upload-time = "2026-03-15T18:51:34.214Z" }, - { url = "https://files.pythonhosted.org/packages/f1/11/897052ea6af56df3eef3ca94edafee410ca699ca0c7b87960ad19932c55e/charset_normalizer-3.4.6-cp313-cp313-win32.whl", hash = "sha256:d7de2637729c67d67cf87614b566626057e95c303bc0a55ffe391f5205e7003d", size = 143940, upload-time = "2026-03-15T18:51:36.15Z" }, - { url = "https://files.pythonhosted.org/packages/a1/5c/724b6b363603e419829f561c854b87ed7c7e31231a7908708ac086cdf3e2/charset_normalizer-3.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:572d7c822caf521f0525ba1bce1a622a0b85cf47ffbdae6c9c19e3b5ac3c4389", size = 154101, upload-time = "2026-03-15T18:51:37.876Z" }, - { url = "https://files.pythonhosted.org/packages/01/a5/7abf15b4c0968e47020f9ca0935fb3274deb87cb288cd187cad92e8cdffd/charset_normalizer-3.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a4474d924a47185a06411e0064b803c68be044be2d60e50e8bddcc2649957c1f", size = 143109, upload-time = "2026-03-15T18:51:39.565Z" }, - { url = "https://files.pythonhosted.org/packages/25/6f/ffe1e1259f384594063ea1869bfb6be5cdb8bc81020fc36c3636bc8302a1/charset_normalizer-3.4.6-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9cc6e6d9e571d2f863fa77700701dae73ed5f78881efc8b3f9a4398772ff53e8", size = 294458, upload-time = "2026-03-15T18:51:41.134Z" }, - { url = "https://files.pythonhosted.org/packages/56/60/09bb6c13a8c1016c2ed5c6a6488e4ffef506461aa5161662bd7636936fb1/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5960d965e67165d75b7c7ffc60a83ec5abfc5c11b764ec13ea54fbef8b4421", size = 199277, upload-time = "2026-03-15T18:51:42.953Z" }, - { url = "https://files.pythonhosted.org/packages/00/50/dcfbb72a5138bbefdc3332e8d81a23494bf67998b4b100703fd15fa52d81/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3694e3f87f8ac7ce279d4355645b3c878d24d1424581b46282f24b92f5a4ae2", size = 218758, upload-time = "2026-03-15T18:51:44.339Z" }, - { url = "https://files.pythonhosted.org/packages/03/b3/d79a9a191bb75f5aa81f3aaaa387ef29ce7cb7a9e5074ba8ea095cc073c2/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5d11595abf8dd942a77883a39d81433739b287b6aa71620f15164f8096221b30", size = 215299, upload-time = "2026-03-15T18:51:45.871Z" }, - { url = "https://files.pythonhosted.org/packages/76/7e/bc8911719f7084f72fd545f647601ea3532363927f807d296a8c88a62c0d/charset_normalizer-3.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7bda6eebafd42133efdca535b04ccb338ab29467b3f7bf79569883676fc628db", size = 206811, upload-time = "2026-03-15T18:51:47.308Z" }, - { url = "https://files.pythonhosted.org/packages/e2/40/c430b969d41dda0c465aa36cc7c2c068afb67177bef50905ac371b28ccc7/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:bbc8c8650c6e51041ad1be191742b8b421d05bbd3410f43fa2a00c8db87678e8", size = 193706, upload-time = "2026-03-15T18:51:48.849Z" }, - { url = "https://files.pythonhosted.org/packages/48/15/e35e0590af254f7df984de1323640ef375df5761f615b6225ba8deb9799a/charset_normalizer-3.4.6-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:22c6f0c2fbc31e76c3b8a86fba1a56eda6166e238c29cdd3d14befdb4a4e4815", size = 202706, upload-time = "2026-03-15T18:51:50.257Z" }, - { url = "https://files.pythonhosted.org/packages/5e/bd/f736f7b9cc5e93a18b794a50346bb16fbfd6b37f99e8f306f7951d27c17c/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7edbed096e4a4798710ed6bc75dcaa2a21b68b6c356553ac4823c3658d53743a", size = 202497, upload-time = "2026-03-15T18:51:52.012Z" }, - { url = "https://files.pythonhosted.org/packages/9d/ba/2cc9e3e7dfdf7760a6ed8da7446d22536f3d0ce114ac63dee2a5a3599e62/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7f9019c9cb613f084481bd6a100b12e1547cf2efe362d873c2e31e4035a6fa43", size = 193511, upload-time = "2026-03-15T18:51:53.723Z" }, - { url = "https://files.pythonhosted.org/packages/9e/cb/5be49b5f776e5613be07298c80e1b02a2d900f7a7de807230595c85a8b2e/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:58c948d0d086229efc484fe2f30c2d382c86720f55cd9bc33591774348ad44e0", size = 220133, upload-time = "2026-03-15T18:51:55.333Z" }, - { url = "https://files.pythonhosted.org/packages/83/43/99f1b5dad345accb322c80c7821071554f791a95ee50c1c90041c157ae99/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:419a9d91bd238052642a51938af8ac05da5b3343becde08d5cdeab9046df9ee1", size = 203035, upload-time = "2026-03-15T18:51:56.736Z" }, - { url = "https://files.pythonhosted.org/packages/87/9a/62c2cb6a531483b55dddff1a68b3d891a8b498f3ca555fbcf2978e804d9d/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5273b9f0b5835ff0350c0828faea623c68bfa65b792720c453e22b25cc72930f", size = 216321, upload-time = "2026-03-15T18:51:58.17Z" }, - { url = "https://files.pythonhosted.org/packages/6e/79/94a010ff81e3aec7c293eb82c28f930918e517bc144c9906a060844462eb/charset_normalizer-3.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0e901eb1049fdb80f5bd11ed5ea1e498ec423102f7a9b9e4645d5b8204ff2815", size = 208973, upload-time = "2026-03-15T18:51:59.998Z" }, - { url = "https://files.pythonhosted.org/packages/2a/57/4ecff6d4ec8585342f0c71bc03efaa99cb7468f7c91a57b105bcd561cea8/charset_normalizer-3.4.6-cp314-cp314-win32.whl", hash = "sha256:b4ff1d35e8c5bd078be89349b6f3a845128e685e751b6ea1169cf2160b344c4d", size = 144610, upload-time = "2026-03-15T18:52:02.213Z" }, - { url = "https://files.pythonhosted.org/packages/80/94/8434a02d9d7f168c25767c64671fead8d599744a05d6a6c877144c754246/charset_normalizer-3.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:74119174722c4349af9708993118581686f343adc1c8c9c007d59be90d077f3f", size = 154962, upload-time = "2026-03-15T18:52:03.658Z" }, - { url = "https://files.pythonhosted.org/packages/46/4c/48f2cdbfd923026503dfd67ccea45c94fd8fe988d9056b468579c66ed62b/charset_normalizer-3.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:e5bcc1a1ae744e0bb59641171ae53743760130600da8db48cbb6e4918e186e4e", size = 143595, upload-time = "2026-03-15T18:52:05.123Z" }, - { url = "https://files.pythonhosted.org/packages/31/93/8878be7569f87b14f1d52032946131bcb6ebbd8af3e20446bc04053dc3f1/charset_normalizer-3.4.6-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:ad8faf8df23f0378c6d527d8b0b15ea4a2e23c89376877c598c4870d1b2c7866", size = 314828, upload-time = "2026-03-15T18:52:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/06/b6/fae511ca98aac69ecc35cde828b0a3d146325dd03d99655ad38fc2cc3293/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5ea69428fa1b49573eef0cc44a1d43bebd45ad0c611eb7d7eac760c7ae771bc", size = 208138, upload-time = "2026-03-15T18:52:08.239Z" }, - { url = "https://files.pythonhosted.org/packages/54/57/64caf6e1bf07274a1e0b7c160a55ee9e8c9ec32c46846ce59b9c333f7008/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:06a7e86163334edfc5d20fe104db92fcd666e5a5df0977cb5680a506fe26cc8e", size = 224679, upload-time = "2026-03-15T18:52:10.043Z" }, - { url = "https://files.pythonhosted.org/packages/aa/cb/9ff5a25b9273ef160861b41f6937f86fae18b0792fe0a8e75e06acb08f1d/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e1f6e2f00a6b8edb562826e4632e26d063ac10307e80f7461f7de3ad8ef3f077", size = 223475, upload-time = "2026-03-15T18:52:11.854Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/440635fc093b8d7347502a377031f9605a1039c958f3cd18dcacffb37743/charset_normalizer-3.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b52c68d64c1878818687a473a10547b3292e82b6f6fe483808fb1468e2f52f", size = 215230, upload-time = "2026-03-15T18:52:13.325Z" }, - { url = "https://files.pythonhosted.org/packages/cd/24/afff630feb571a13f07c8539fbb502d2ab494019492aaffc78ef41f1d1d0/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:7504e9b7dc05f99a9bbb4525c67a2c155073b44d720470a148b34166a69c054e", size = 199045, upload-time = "2026-03-15T18:52:14.752Z" }, - { url = "https://files.pythonhosted.org/packages/e5/17/d1399ecdaf7e0498c327433e7eefdd862b41236a7e484355b8e0e5ebd64b/charset_normalizer-3.4.6-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:172985e4ff804a7ad08eebec0a1640ece87ba5041d565fff23c8f99c1f389484", size = 211658, upload-time = "2026-03-15T18:52:16.278Z" }, - { url = "https://files.pythonhosted.org/packages/b5/38/16baa0affb957b3d880e5ac2144caf3f9d7de7bc4a91842e447fbb5e8b67/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4be9f4830ba8741527693848403e2c457c16e499100963ec711b1c6f2049b7c7", size = 210769, upload-time = "2026-03-15T18:52:17.782Z" }, - { url = "https://files.pythonhosted.org/packages/05/34/c531bc6ac4c21da9ddfddb3107be2287188b3ea4b53b70fc58f2a77ac8d8/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:79090741d842f564b1b2827c0b82d846405b744d31e84f18d7a7b41c20e473ff", size = 201328, upload-time = "2026-03-15T18:52:19.553Z" }, - { url = "https://files.pythonhosted.org/packages/fa/73/a5a1e9ca5f234519c1953608a03fe109c306b97fdfb25f09182babad51a7/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:87725cfb1a4f1f8c2fc9890ae2f42094120f4b44db9360be5d99a4c6b0e03a9e", size = 225302, upload-time = "2026-03-15T18:52:21.043Z" }, - { url = "https://files.pythonhosted.org/packages/ba/f6/cd782923d112d296294dea4bcc7af5a7ae0f86ab79f8fefbda5526b6cfc0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:fcce033e4021347d80ed9c66dcf1e7b1546319834b74445f561d2e2221de5659", size = 211127, upload-time = "2026-03-15T18:52:22.491Z" }, - { url = "https://files.pythonhosted.org/packages/0e/c5/0b6898950627af7d6103a449b22320372c24c6feda91aa24e201a478d161/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ca0276464d148c72defa8bb4390cce01b4a0e425f3b50d1435aa6d7a18107602", size = 222840, upload-time = "2026-03-15T18:52:24.113Z" }, - { url = "https://files.pythonhosted.org/packages/7d/25/c4bba773bef442cbdc06111d40daa3de5050a676fa26e85090fc54dd12f0/charset_normalizer-3.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:197c1a244a274bb016dd8b79204850144ef77fe81c5b797dc389327adb552407", size = 216890, upload-time = "2026-03-15T18:52:25.541Z" }, - { url = "https://files.pythonhosted.org/packages/35/1a/05dacadb0978da72ee287b0143097db12f2e7e8d3ffc4647da07a383b0b7/charset_normalizer-3.4.6-cp314-cp314t-win32.whl", hash = "sha256:2a24157fa36980478dd1770b585c0f30d19e18f4fb0c47c13aa568f871718579", size = 155379, upload-time = "2026-03-15T18:52:27.05Z" }, - { url = "https://files.pythonhosted.org/packages/5d/7a/d269d834cb3a76291651256f3b9a5945e81d0a49ab9f4a498964e83c0416/charset_normalizer-3.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cd5e2801c89992ed8c0a3f0293ae83c159a60d9a5d685005383ef4caca77f2c4", size = 169043, upload-time = "2026-03-15T18:52:28.502Z" }, - { url = "https://files.pythonhosted.org/packages/23/06/28b29fba521a37a8932c6a84192175c34d49f84a6d4773fa63d05f9aff22/charset_normalizer-3.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:47955475ac79cc504ef2704b192364e51d0d473ad452caedd0002605f780101c", size = 148523, upload-time = "2026-03-15T18:52:29.956Z" }, - { url = "https://files.pythonhosted.org/packages/2a/68/687187c7e26cb24ccbd88e5069f5ef00eba804d36dde11d99aad0838ab45/charset_normalizer-3.4.6-py3-none-any.whl", hash = "sha256:947cf925bc916d90adba35a64c82aace04fa39b46b52d4630ece166655905a69", size = 61455, upload-time = "2026-03-15T18:53:23.833Z" }, +version = "3.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30", size = 363585, upload-time = "2026-08-15T08:16:46.646Z" }, + { url = "https://files.pythonhosted.org/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488", size = 251189, upload-time = "2026-08-15T08:16:48.287Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22", size = 239724, upload-time = "2026-08-15T08:16:49.67Z" }, + { url = "https://files.pythonhosted.org/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731", size = 280078, upload-time = "2026-08-15T08:16:51.228Z" }, + { url = "https://files.pythonhosted.org/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c", size = 276650, upload-time = "2026-08-15T08:16:52.625Z" }, + { url = "https://files.pythonhosted.org/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8", size = 262325, upload-time = "2026-08-15T08:16:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486", size = 261140, upload-time = "2026-08-15T08:16:55.604Z" }, + { url = "https://files.pythonhosted.org/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f", size = 252791, upload-time = "2026-08-15T08:16:57.103Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c", size = 240730, upload-time = "2026-08-15T08:16:58.418Z" }, + { url = "https://files.pythonhosted.org/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18", size = 280791, upload-time = "2026-08-15T08:16:59.918Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5", size = 259598, upload-time = "2026-08-15T08:17:01.421Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b", size = 278217, upload-time = "2026-08-15T08:17:02.782Z" }, + { url = "https://files.pythonhosted.org/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6", size = 263417, upload-time = "2026-08-15T08:17:04.216Z" }, + { url = "https://files.pythonhosted.org/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b", size = 181774, upload-time = "2026-08-15T08:17:05.58Z" }, + { url = "https://files.pythonhosted.org/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9", size = 206653, upload-time = "2026-08-15T08:17:07.075Z" }, + { url = "https://files.pythonhosted.org/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10", size = 185630, upload-time = "2026-08-15T08:17:08.502Z" }, + { url = "https://files.pythonhosted.org/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" }, + { url = "https://files.pythonhosted.org/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" }, + { url = "https://files.pythonhosted.org/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" }, + { url = "https://files.pythonhosted.org/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" }, + { url = "https://files.pythonhosted.org/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" }, + { url = "https://files.pythonhosted.org/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" }, + { url = "https://files.pythonhosted.org/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" }, + { url = "https://files.pythonhosted.org/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" }, + { url = "https://files.pythonhosted.org/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" }, + { url = "https://files.pythonhosted.org/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" }, + { url = "https://files.pythonhosted.org/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" }, + { url = "https://files.pythonhosted.org/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" }, + { url = "https://files.pythonhosted.org/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" }, + { url = "https://files.pythonhosted.org/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" }, + { url = "https://files.pythonhosted.org/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" }, + { url = "https://files.pythonhosted.org/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" }, + { url = "https://files.pythonhosted.org/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" }, + { url = "https://files.pythonhosted.org/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" }, + { url = "https://files.pythonhosted.org/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" }, + { url = "https://files.pythonhosted.org/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" }, + { url = "https://files.pythonhosted.org/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" }, + { url = "https://files.pythonhosted.org/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" }, + { url = "https://files.pythonhosted.org/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" }, + { url = "https://files.pythonhosted.org/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" }, + { url = "https://files.pythonhosted.org/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" }, + { url = "https://files.pythonhosted.org/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" }, + { url = "https://files.pythonhosted.org/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" }, + { url = "https://files.pythonhosted.org/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" }, + { url = "https://files.pythonhosted.org/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" }, + { url = "https://files.pythonhosted.org/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" }, + { url = "https://files.pythonhosted.org/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" }, + { url = "https://files.pythonhosted.org/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" }, + { url = "https://files.pythonhosted.org/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" }, + { url = "https://files.pythonhosted.org/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" }, + { url = "https://files.pythonhosted.org/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" }, + { url = "https://files.pythonhosted.org/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" }, + { url = "https://files.pythonhosted.org/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" }, + { url = "https://files.pythonhosted.org/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" }, + { url = "https://files.pythonhosted.org/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" }, + { url = "https://files.pythonhosted.org/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" }, + { url = "https://files.pythonhosted.org/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" }, + { url = "https://files.pythonhosted.org/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" }, + { url = "https://files.pythonhosted.org/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" }, + { url = "https://files.pythonhosted.org/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" }, + { url = "https://files.pythonhosted.org/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" }, + { url = "https://files.pythonhosted.org/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" }, + { url = "https://files.pythonhosted.org/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" }, + { url = "https://files.pythonhosted.org/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" }, + { url = "https://files.pythonhosted.org/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" }, + { url = "https://files.pythonhosted.org/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" }, + { url = "https://files.pythonhosted.org/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" }, + { url = "https://files.pythonhosted.org/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" }, + { url = "https://files.pythonhosted.org/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" }, + { url = "https://files.pythonhosted.org/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" }, + { url = "https://files.pythonhosted.org/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" }, + { url = "https://files.pythonhosted.org/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" }, + { url = "https://files.pythonhosted.org/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" }, + { url = "https://files.pythonhosted.org/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" }, + { url = "https://files.pythonhosted.org/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" }, + { url = "https://files.pythonhosted.org/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" }, + { url = "https://files.pythonhosted.org/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" }, + { url = "https://files.pythonhosted.org/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" }, + { url = "https://files.pythonhosted.org/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" }, + { url = "https://files.pythonhosted.org/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" }, + { url = "https://files.pythonhosted.org/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" }, + { url = "https://files.pythonhosted.org/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" }, + { url = "https://files.pythonhosted.org/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" }, + { url = "https://files.pythonhosted.org/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" }, + { url = "https://files.pythonhosted.org/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" }, + { url = "https://files.pythonhosted.org/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" }, + { url = "https://files.pythonhosted.org/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" }, + { url = "https://files.pythonhosted.org/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" }, + { url = "https://files.pythonhosted.org/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" }, + { url = "https://files.pythonhosted.org/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" }, + { url = "https://files.pythonhosted.org/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" }, + { url = "https://files.pythonhosted.org/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" }, + { url = "https://files.pythonhosted.org/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" }, + { url = "https://files.pythonhosted.org/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" }, + { url = "https://files.pythonhosted.org/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" }, + { url = "https://files.pythonhosted.org/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" }, + { url = "https://files.pythonhosted.org/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" }, + { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" }, ] [[package]] @@ -375,74 +551,58 @@ wheels = [ [[package]] name = "cryptography" -version = "46.0.5" +version = "50.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289, upload-time = "2026-02-10T19:17:08.274Z" }, - { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, - { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, - { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, - { url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993, upload-time = "2026-02-10T19:17:15.618Z" }, - { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, - { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, - { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, - { url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181, upload-time = "2026-02-10T19:17:21.825Z" }, - { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, - { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, - { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230, upload-time = "2026-02-10T19:17:30.518Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909, upload-time = "2026-02-10T19:17:32.083Z" }, - { url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287, upload-time = "2026-02-10T19:17:33.801Z" }, - { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, - { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, - { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, - { url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539, upload-time = "2026-02-10T19:17:40.241Z" }, - { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, - { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170, upload-time = "2026-02-10T19:17:46.997Z" }, - { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, - { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, - { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, - { url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637, upload-time = "2026-02-10T19:17:52.997Z" }, - { url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487, upload-time = "2026-02-10T19:17:54.549Z" }, - { url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514, upload-time = "2026-02-10T19:17:56.267Z" }, - { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, - { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, - { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, - { url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143, upload-time = "2026-02-10T19:18:03.964Z" }, - { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, - { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, - { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, - { url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539, upload-time = "2026-02-10T19:18:11.263Z" }, - { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, - { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, - { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, - { url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220, upload-time = "2026-02-10T19:18:17.361Z" }, - { url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050, upload-time = "2026-02-10T19:18:18.899Z" }, - { url = "https://files.pythonhosted.org/packages/eb/dd/2d9fdb07cebdf3d51179730afb7d5e576153c6744c3ff8fded23030c204e/cryptography-46.0.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3b4995dc971c9fb83c25aa44cf45f02ba86f71ee600d81091c2f0cbae116b06c", size = 3476964, upload-time = "2026-02-10T19:18:20.687Z" }, - { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, - { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, - { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, - { url = "https://files.pythonhosted.org/packages/bc/58/6b3d24e6b9bc474a2dcdee65dfd1f008867015408a271562e4b690561a4d/cryptography-46.0.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8456928655f856c6e1533ff59d5be76578a7157224dbd9ce6872f25055ab9ab7", size = 3407605, upload-time = "2026-02-10T19:18:29.233Z" }, -] - -[[package]] -name = "dataclasses-json" -version = "0.6.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "marshmallow" }, - { name = "typing-inspect" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/a4/f71d9cf3a5ac257c993b5ca3f93df5f7fb395c725e7f1e6479d2514173c3/dataclasses_json-0.6.7.tar.gz", hash = "sha256:b6b3e528266ea45b9535223bc53ca645f5208833c29229e847b3f26a1cc55fc0", size = 32227, upload-time = "2024-06-09T16:20:19.103Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/be/d0d44e092656fe7a06b55e6103cbce807cdbdee17884a5367c68c9860853/dataclasses_json-0.6.7-py3-none-any.whl", hash = "sha256:0dbf33f26c8d5305befd61b39d2b3414e8a407bedc2834dea9b8d642666fb40a", size = 28686, upload-time = "2024-06-09T16:20:16.715Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/bb/ad/5d6702db60b1e40b41ef513b6967ff5848f307d50f8449baf1634f5908f1/cryptography-50.0.1.tar.gz", hash = "sha256:5dd9bda1c12b4162f6ff568eeb5e0ff956c28d14406e875cfe8a63a2d414ff20", size = 880381, upload-time = "2026-08-25T19:45:45.499Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/19/797e2aaac9df6a66f1550f49979dc1b1e39ecd2077501c30efa81e8d5d67/cryptography-50.0.1-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:b8f852c65863251b9e3a1b8c150ce21e59b522dbb6a7d4bc80e680d38388e986", size = 4010153, upload-time = "2026-08-25T19:44:03.155Z" }, + { url = "https://files.pythonhosted.org/packages/90/34/9ce9a62ed9dc82ca9fd6a34445b6904af56e5f38b3eae2ed32e49c36053d/cryptography-50.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:53e279950892dc102c6b4e52af03ae5ea92fac572a1ddab78ca73a997f62b69f", size = 4723133, upload-time = "2026-08-25T19:44:05.461Z" }, + { url = "https://files.pythonhosted.org/packages/57/26/e6d4fc8512a51a5f9ee7bfdbfb853bce1197087df40c9ad993ad370b846f/cryptography-50.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ff838d62ec1bfce4f9ba7fa16f4a7b554cd8d0c299e6be37502161a660c84eef", size = 4712478, upload-time = "2026-08-25T19:44:07.375Z" }, + { url = "https://files.pythonhosted.org/packages/e6/de/d3cdc2815697aae84126cbd6a030ca7b6b452e28a88b501b836bd3aa7a86/cryptography-50.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e74591e283fe6eb956416c929eb58262a719fe0311fd9054c62c3350ed8760d8", size = 4730726, upload-time = "2026-08-25T19:44:09.294Z" }, + { url = "https://files.pythonhosted.org/packages/55/32/38c0d344b98c06d34b5df8946565a9c0d6dbf32c8e0730a7f05f0a3c6cab/cryptography-50.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5fe002589592ed749ce77fe0695fcbd3500dd61d7d6db5858a7544c612fa8e45", size = 5353524, upload-time = "2026-08-25T19:44:11.96Z" }, + { url = "https://files.pythonhosted.org/packages/e1/1b/82f0f0d8858d4432be1af790477edf62aef90324041aa07c57e57bef1af7/cryptography-50.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:51593d180cf6d179bde5c5d065bed81386b1f381656ae7d042b7ffc87a9895ad", size = 4746720, upload-time = "2026-08-25T19:44:14.051Z" }, + { url = "https://files.pythonhosted.org/packages/29/ba/042ca458b8c64348c768284b5d23e69b92ed53d057ab779fee628564676d/cryptography-50.0.1-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:359e62deae718bce96170e223fdcb6357e4fbd3bb7a3a75f4430763532560e49", size = 4361866, upload-time = "2026-08-25T19:44:16.167Z" }, + { url = "https://files.pythonhosted.org/packages/39/3b/e96c1ef71edef71057c7e3c3d982ce8fda554e0c52d0cc19c18845cde3eb/cryptography-50.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e2ca8fd1b6b4b82a1c4cb02841d0837e3c12336c2e24b520ab8ab3b969733d8f", size = 4730028, upload-time = "2026-08-25T19:44:18.085Z" }, + { url = "https://files.pythonhosted.org/packages/e3/38/45abd72ef63f2e7d0754a6cacf97bd8b69512ace7f6130d24c39ece65da2/cryptography-50.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:76de83fbd91ac49c0feaaa983d0748fd7a53176afac5fb3bf7478d244f0eb527", size = 5308405, upload-time = "2026-08-25T19:44:20.197Z" }, + { url = "https://files.pythonhosted.org/packages/85/66/6ccca4722987ddedaa7fc9c3f4708af7431f5535666c174350830888c6b7/cryptography-50.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:51afcfceb15597cf2635068e4ac9a56b2abde622edde17f37d85fd7b5306497a", size = 4746230, upload-time = "2026-08-25T19:44:22.376Z" }, + { url = "https://files.pythonhosted.org/packages/13/0e/b1f92e013228111413f2e6743948b80bc24dfd3c1b87ba98ceea16f5df89/cryptography-50.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:be224a65493ec5b74a158ff22a5522ce4a5ca1e543c647a3a4730d4a09e5f959", size = 4862596, upload-time = "2026-08-25T19:44:24.472Z" }, + { url = "https://files.pythonhosted.org/packages/7e/22/c3654cccc856e9d682817b04ac3ee79731cb09ca6f95996a95c904de2883/cryptography-50.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9ebcdd5519be9b652a46f507817a74591774fc3d6923ac364e4dfa64e36b291b", size = 5014082, upload-time = "2026-08-25T19:44:26.709Z" }, + { url = "https://files.pythonhosted.org/packages/42/8b/cb12b1b60c91b074ca6bf0fdd59aa8f10d8bc5f73af8faece86ef0421b37/cryptography-50.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:aed8db4f6d71c51efb89530e12d9464e7bf2923d46c3205dc794a2a93f8c0648", size = 3842826, upload-time = "2026-08-25T19:44:28.784Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f0/424cb557d99aa86ac55da5e2add02e2882e44047b6264f93ade1b975a993/cryptography-50.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:30a125032e5642a21ff816e021152bd4e7e94f03eff3f4b7fca41cd22bc3110f", size = 3973525, upload-time = "2026-08-25T19:44:30.7Z" }, + { url = "https://files.pythonhosted.org/packages/4d/72/3a2711d967977ab5fc80b782837c7e8d1ac7445e764c20c381a265c57ef3/cryptography-50.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a0b1a59e3a089064a0ec309e9428c8e3ae4e161419d20ac33600767e83fc658a", size = 4708817, upload-time = "2026-08-25T19:44:32.773Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f2/bb1f56e10815b789df0b409a69fa4992ff3d3fef9c72747f4a6b26fed38e/cryptography-50.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8921d58f426793c5f1b47f0b59575780de9a095214958d0eb37d909593db8367", size = 4697300, upload-time = "2026-08-25T19:44:35.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/bd/ed5396be499ffcf8807a585bfe38b71a1fbdd1c342b4f9b6d0ef5162a946/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a8f40ea47330e71b594a7e246898f93177c259490c63183dbaf9e571d71ed9a5", size = 4716039, upload-time = "2026-08-25T19:44:37.192Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/1cf405c5c8e8df7545378048e954792f00b7f2367af8863ce8b8f3e10607/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:a255449073358275b64b67d3f595f268bbef70e72b6edb65e0c70c735bf739c9", size = 5332388, upload-time = "2026-08-25T19:44:39.16Z" }, + { url = "https://files.pythonhosted.org/packages/47/92/b4317e8c32c4f47b062f5398bd79106b220a124546f42be83bf32b761e2a/cryptography-50.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:8df2de9102026855887e4587084f6eabd80ed0f345b8ad8a7ac27ab9bf4723e0", size = 4730293, upload-time = "2026-08-25T19:44:41.298Z" }, + { url = "https://files.pythonhosted.org/packages/39/0d/a1e7633e2c744d0f2983320a27e924ef2264c79c56e1a58d5fb0a1cfd413/cryptography-50.0.1-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:ac02b07824d4d1001bd4367599f839c19cb171924c796e52c23508ac14c2c0cc", size = 4346031, upload-time = "2026-08-25T19:44:43.245Z" }, + { url = "https://files.pythonhosted.org/packages/88/dd/b215616f9bab3fc18510c78a4e5c9f362d77838503c363dc747c7d4f5c6f/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:cbf74a81765ee67413503ca6e26dcc4f6f5a519822436cc0a1b97aab6c1b8a17", size = 4715344, upload-time = "2026-08-25T19:44:45.291Z" }, + { url = "https://files.pythonhosted.org/packages/b1/1b/ec3ebd31741d0e963612c4fe43caa39341b9b1e031e469820e42e4c83918/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:16c5ecd954b3330ebfb6605eca4fd952da8bef376551d5cc264534e3770a9ee6", size = 5287201, upload-time = "2026-08-25T19:44:47.297Z" }, + { url = "https://files.pythonhosted.org/packages/1a/01/0127d11a762b31a9ee0221894f540318761783f3fdc4bc5d057698caebd5/cryptography-50.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:79bf008d1f9af6071c797ad133e39915dfee7614f18f18f4db9072eb715064a3", size = 4730023, upload-time = "2026-08-25T19:44:49.435Z" }, + { url = "https://files.pythonhosted.org/packages/9e/b9/e7425ebfb599241a0c1d7000f1b466c3062da66c19d9525031315dff7213/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:330fbb252391c596f1ae42c5754449dc924e6ad012dca8efe0d703f9f2d12ec6", size = 4847362, upload-time = "2026-08-25T19:44:51.94Z" }, + { url = "https://files.pythonhosted.org/packages/2d/fd/60d0ddf4defa12e482c9d5e0f554384d6e8ab25341fd15f060028fd92e6a/cryptography-50.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:42be3bb70596b3abe4ac097b75be223e8b3ab614a0e5de068e3dcc54d71d6149", size = 4999247, upload-time = "2026-08-25T19:44:53.876Z" }, + { url = "https://files.pythonhosted.org/packages/4d/56/bc4f2b209e766c93372cfcd59b781a0b2b59700f62a969580415b699c2b2/cryptography-50.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f74455bb086a85d5e81246412602aaa97ed095e504cd40dd261ef50be42205bf", size = 3825806, upload-time = "2026-08-25T19:44:56.209Z" }, + { url = "https://files.pythonhosted.org/packages/84/a9/ee16a903f13755e914d1eecc482fe64d1f10761c3960e5d8fa6837377aff/cryptography-50.0.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ca83d00d9e69cd5eb63f2e69c3a5a59e0cecae5ae14c6ae0b35830fe3b37bad0", size = 4035307, upload-time = "2026-08-25T19:44:58.305Z" }, + { url = "https://files.pythonhosted.org/packages/5e/a5/9ec7e81e8526c0d7a387d73386b2daed3f39e10d81a85930bd1b6bfba65c/cryptography-50.0.1-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:05ba322c4da95b262a212c345af888ef2c37c88c0509756ea00a0e6d68850f23", size = 4751900, upload-time = "2026-08-25T19:45:00.401Z" }, + { url = "https://files.pythonhosted.org/packages/7e/3c/0e77bd5ffcf078e9dd27d3074aad6c030d9b10d0bf69329d573c927a188c/cryptography-50.0.1-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e22dfed744bd4002e909464cb23d2f0b05c6f3113a79ef2e9864a53db737c733", size = 4738357, upload-time = "2026-08-25T19:45:02.786Z" }, + { url = "https://files.pythonhosted.org/packages/27/3a/3c5f80daa4dcd47323c7af8a2fcb90de27a33564d4fcac69846c0972691a/cryptography-50.0.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4c4188f7c0cf655be5c06342b817ed0f9595b69ffa2b12026e5353eed29dea88", size = 4758474, upload-time = "2026-08-25T19:45:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/6e/2b/214cf0cf93db9628c3c20c896b229f327f6fb1b20e4b3743d8ad3f00af8b/cryptography-50.0.1-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2ebbfb0f1fed745e91796e3e1080a1440423fdae8ece1b995a1d80883a409054", size = 5375862, upload-time = "2026-08-25T19:45:07.163Z" }, + { url = "https://files.pythonhosted.org/packages/d6/51/3f9701867a46b6c1740c9b52fc4d3bed6cbdcfedcc9b6e64305c07f39cff/cryptography-50.0.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:407fe2b6db00939c05c0e945e9914238f2f0a430974839429dafc82b1ee6bee5", size = 4772942, upload-time = "2026-08-25T19:45:09.396Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5c/13ea642e08e2544d0f5396122055f4820cfacb3203562197b5967125ea97/cryptography-50.0.1-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:2b34d76a652ea2b6faf777c35df230c5637842cd904e04f16230c3f9f03e4361", size = 4383347, upload-time = "2026-08-25T19:45:11.659Z" }, + { url = "https://files.pythonhosted.org/packages/84/d5/7d1fe1cb93f91c428093ff234e128c89ba8ea61a6f26aab406081f9b996e/cryptography-50.0.1-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:01f41478cf33fc605a6a089cd56d28b45c6c0b45a1928b61797f2621a04bac71", size = 4758050, upload-time = "2026-08-25T19:45:13.745Z" }, + { url = "https://files.pythonhosted.org/packages/dd/04/557fc5ead96a829e0bc812a3b9dc4a52a2f27e4f7f5950da7ff27653a805/cryptography-50.0.1-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:fc3ed7ebd2a8c96f5b166de0ab9b624996bef3b07bbeb19364dfb78222c22c80", size = 5332955, upload-time = "2026-08-25T19:45:16.193Z" }, + { url = "https://files.pythonhosted.org/packages/8c/eb/5d7124083e8d8cda8f5b348f544b71ad6f707ad63193758ef4d8e569da02/cryptography-50.0.1-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dde0a357190eb3b1da1bb9ab750e9c85cba82ca5977aa0836cbb94e92611239", size = 4772694, upload-time = "2026-08-25T19:45:18.315Z" }, + { url = "https://files.pythonhosted.org/packages/63/8e/f1f955e0921dd2b6d22eae7e8d24a4c4b638d10735ffbf6a71f99eb0fcb8/cryptography-50.0.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd3718b960d0b5dd213cdf03f3bcb7000e69dda0de8b956061947ff6bcff5558", size = 4888413, upload-time = "2026-08-25T19:45:20.4Z" }, + { url = "https://files.pythonhosted.org/packages/1f/ab/89e2b798d2c3925f82e2bb72d5979f3d2f6da2dd22ef4a8cd8b70d920039/cryptography-50.0.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2a93d05e34d5f67fba6f891fe85d929999baa7195e853923ea6d7576c9e68c5e", size = 5044355, upload-time = "2026-08-25T19:45:22.353Z" }, + { url = "https://files.pythonhosted.org/packages/99/89/87ef49ffe383ef4e147d27b7bf2088fb0b54ea409dd87b5a89442e5828a5/cryptography-50.0.1-cp39-abi3-win_amd64.whl", hash = "sha256:55d16b1ef3ee0958d893a977b19777887e546c9954ea81b200c3301a864013f2", size = 3875429, upload-time = "2026-08-25T19:45:24.418Z" }, + { url = "https://files.pythonhosted.org/packages/c7/27/8d207af749c453ee17ea087340b3f2b4adef75aadd1d277b1b129bdda84e/cryptography-50.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9cb3cb952cf5a8abd50c782a98a89d71699715e802fe349704b47f2425b42a94", size = 3974350, upload-time = "2026-08-25T19:45:26.551Z" }, + { url = "https://files.pythonhosted.org/packages/14/9a/6d3a4d7852e22d657438b7bf51f66102c7d71c0e1fafeec652281d0403e5/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5fe939deeb161024a6be98229c953b6591fef1f41214497a78fe793a244c017f", size = 4698675, upload-time = "2026-08-25T19:45:28.658Z" }, + { url = "https://files.pythonhosted.org/packages/73/35/5c3717edf9e68a0550ce04e28eab493fe545eccd81742af03f6a75fe260b/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:fb4b9672d389c738b175c4166e78310f8a70358886aacd9173ee03a85ffdc671", size = 4707410, upload-time = "2026-08-25T19:45:30.816Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e0/e786934472e3ac4ecdecc7b129a0ca1a2a40dffdafcf2c3ea9d4397f8def/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d63ae8f6481fec907ac0f588eee8a90aefde112c633131fe540e5711ddbb5a4e", size = 4698378, upload-time = "2026-08-25T19:45:33.043Z" }, + { url = "https://files.pythonhosted.org/packages/51/cf/5b3f53a0b74d122f023476ede40ba5d3e70d5cf475f73b899740d26a4fb2/cryptography-50.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:804728ce710890870f3aaa344b2e161172d258d768ac139d02cfd9092d0d94e6", size = 4706889, upload-time = "2026-08-25T19:45:35.086Z" }, + { url = "https://files.pythonhosted.org/packages/71/44/711e61f7d014be825ef79b285b047292d1bf893732ac1bc030a351fb517f/cryptography-50.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:693c99b49bd37d0d096e4334c10232c77248c415b98d35236094cdf96d57258b", size = 3824006, upload-time = "2026-08-25T19:45:37.281Z" }, ] [[package]] @@ -456,11 +616,11 @@ wheels = [ [[package]] name = "docstring-parser" -version = "0.17.0" +version = "0.18.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/9d/c3b43da9515bd270df0f80548d9944e389870713cc1fe2b8fb35fe2bcefd/docstring_parser-0.17.0.tar.gz", hash = "sha256:583de4a309722b3315439bb31d64ba3eebada841f2e2cee23b99df001434c912", size = 27442, upload-time = "2025-07-21T07:35:01.868Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/4d/f332313098c1de1b2d2ff91cf2674415cc7cddab2ca1b01ae29774bd5fdf/docstring_parser-0.18.0.tar.gz", hash = "sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015", size = 29341, upload-time = "2026-04-14T04:09:19.867Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/55/e2/2537ebcff11c1ee1ff17d8d0b6f4db75873e3b0fb32c2d4a2ee31ecb310a/docstring_parser-0.17.0-py3-none-any.whl", hash = "sha256:cf2569abd23dce8099b300f9b4fa8191e9582dda731fd533daf54c4551658708", size = 36896, upload-time = "2025-07-21T07:35:00.684Z" }, + { url = "https://files.pythonhosted.org/packages/a7/5f/ed01f9a3cdffbd5a008556fc7b2a08ddb1cc6ace7effa7340604b1d16699/docstring_parser-0.18.0-py3-none-any.whl", hash = "sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b", size = 22484, upload-time = "2026-04-14T04:09:18.638Z" }, ] [[package]] @@ -474,27 +634,24 @@ wheels = [ [[package]] name = "faiss-cpu" -version = "1.13.2" +version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "packaging" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c9/671f66f6b31ec48e5825d36435f0cb91189fa8bb6b50724029dbff4ca83c/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:a9064eb34f8f64438dd5b95c8f03a780b1a3f0b99c46eeacb1f0b5d15fc02dc1", size = 3452776, upload-time = "2025-12-24T10:27:01.419Z" }, - { url = "https://files.pythonhosted.org/packages/5a/4a/97150aa1582fb9c2bca95bd8fc37f27d3b470acec6f0a6833844b21e4b40/faiss_cpu-1.13.2-cp310-abi3-macosx_14_0_x86_64.whl", hash = "sha256:c8d097884521e1ecaea6467aeebbf1aa56ee4a36350b48b2ca6b39366565c317", size = 7896434, upload-time = "2025-12-24T10:27:03.592Z" }, - { url = "https://files.pythonhosted.org/packages/0b/d0/0940575f059591ca31b63a881058adb16a387020af1709dcb7669460115c/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ee330a284042c2480f2e90450a10378fd95655d62220159b1408f59ee83ebf1", size = 11485825, upload-time = "2025-12-24T10:27:05.681Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e1/a5acac02aa593809f0123539afe7b4aff61d1db149e7093239888c9053e1/faiss_cpu-1.13.2-cp310-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ab88ee287c25a119213153d033f7dd64c3ccec466ace267395872f554b648cd7", size = 23845772, upload-time = "2025-12-24T10:27:08.194Z" }, - { url = "https://files.pythonhosted.org/packages/9c/7b/49dcaf354834ec457e85ca769d50bc9b5f3003fab7c94a9dcf08cf742793/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:85511129b34f890d19c98b82a0cd5ffb27d89d1cec2ee41d2621ee9f9ef8cf3f", size = 13477567, upload-time = "2025-12-24T10:27:10.822Z" }, - { url = "https://files.pythonhosted.org/packages/f7/6b/12bb4037921c38bb2c0b4cfc213ca7e04bbbebbfea89b0b5746248ce446e/faiss_cpu-1.13.2-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b32eb4065bac352b52a9f5ae07223567fab0a976c7d05017c01c45a1c24264f", size = 25102239, upload-time = "2025-12-24T10:27:13.476Z" }, - { url = "https://files.pythonhosted.org/packages/14/6d/40439a05e4e60a0e889aa68b08ec70f5c8e32901f75f2be25c593a2e050e/faiss_cpu-1.13.2-cp311-cp311-win_amd64.whl", hash = "sha256:7c5944d7807d58fe7244b6aba06be710ee7ed99343365ed92699349efe979f51", size = 18879906, upload-time = "2025-12-24T10:27:19.041Z" }, - { url = "https://files.pythonhosted.org/packages/9f/f9/b97eadbdd9e00f945d1566c7101382344f504596bfb19219465b0fc61e6e/faiss_cpu-1.13.2-cp311-cp311-win_arm64.whl", hash = "sha256:19508a1badfb36e456c1c8664eeb948349f604db5c7545f277a0126b4a84b080", size = 8548280, upload-time = "2025-12-24T10:27:22.114Z" }, - { url = "https://files.pythonhosted.org/packages/87/ff/35ed875423200c17bdd594ce921abfc1812ddd21e09355290b9a94e170ab/faiss_cpu-1.13.2-cp312-cp312-win_amd64.whl", hash = "sha256:b82c01d30430dd7b1fa442001b9099735d1a82f6bb72033acdc9206d5ac66a64", size = 18890300, upload-time = "2025-12-24T10:27:24.194Z" }, - { url = "https://files.pythonhosted.org/packages/c5/3a/bbdf5deaf6feb34b46b469c0a0acd40216c3d3c6ecf5aeb71d56b8a650e3/faiss_cpu-1.13.2-cp312-cp312-win_arm64.whl", hash = "sha256:2c4f696ae76e7c97cbc12311db83aaf1e7f4f7be06a3ffea7e5b0e8ec1fd805b", size = 8553157, upload-time = "2025-12-24T10:27:26.38Z" }, - { url = "https://files.pythonhosted.org/packages/60/4b/903d85bf3a8264d49964ec799e45c7ffc91098606b8bc9ef2c904c1a56cb/faiss_cpu-1.13.2-cp313-cp313-win_amd64.whl", hash = "sha256:cb4b5ee184816a4b099162ac93c0d7f0033d81a88e7c1291d0a9cc41ec348984", size = 18891330, upload-time = "2025-12-24T10:27:28.806Z" }, - { url = "https://files.pythonhosted.org/packages/b2/52/5d10642da628f63544aab27e48416be4a7ea25c6b81d8bd65016d8538b00/faiss_cpu-1.13.2-cp313-cp313-win_arm64.whl", hash = "sha256:1243967eeb2298791ff7f3683a4abd2100d7e6ec7542ca05c3b75d47a7f621e5", size = 8553088, upload-time = "2025-12-24T10:27:31.325Z" }, - { url = "https://files.pythonhosted.org/packages/b0/b1/daaab8046f56c60079648bd83774f61b283b59a9930a2f60790ee4cdedfe/faiss_cpu-1.13.2-cp314-cp314-win_amd64.whl", hash = "sha256:c8b645e7d56591aa35dc75415bb53a62e4a494dba010e16f4b67daeffd830bd7", size = 18892621, upload-time = "2025-12-24T10:27:33.923Z" }, - { url = "https://files.pythonhosted.org/packages/06/6f/5eaf3e249c636e616ebb52e369a4a2f1d32b1caf9a611b4f917b3dd21423/faiss_cpu-1.13.2-cp314-cp314-win_arm64.whl", hash = "sha256:8113a2a80b59fe5653cf66f5c0f18be0a691825601a52a614c30beb1fca9bc7c", size = 8556374, upload-time = "2025-12-24T10:27:36.653Z" }, + { url = "https://files.pythonhosted.org/packages/59/68/20e91694ad9a8b2bb48af956899e52b645cb1501e7e2ec31cb733da4d4c5/faiss_cpu-1.15.0-cp310-abi3-macosx_14_0_arm64.whl", hash = "sha256:50ea471ef1f4f3580eda8ab0ec9727d4bf65fd71c444bf306ce7cdbba8a42b21", size = 4904897, upload-time = "2026-08-03T17:49:37.003Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cd/ef4cf498977c4a84af7a8920bc97ca49fc19060c8464c63fab58847b4692/faiss_cpu-1.15.0-cp310-abi3-macosx_15_0_x86_64.whl", hash = "sha256:dd383bb1ce06fabcff5785f998f253aa88f88dcbe1fe36c922417cd6666dd896", size = 7087977, upload-time = "2026-08-03T17:49:38.947Z" }, + { url = "https://files.pythonhosted.org/packages/94/c8/88b072bf55714405d0d7e11c12349510f15a69ae56033b1cd894fb2be7d6/faiss_cpu-1.15.0-cp310-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d0a2d5d33fe023e263d0d355a837f20db67578e3be27fc5f4012a273274abf6", size = 9835009, upload-time = "2026-08-03T17:49:40.8Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3b/8878dbfc78a0084bbd408b34827a58b530be98132fcf620b7e15f9191614/faiss_cpu-1.15.0-cp310-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ec9b29aae29e428c085c2d49dbb02e4673cdea75db418d420f9e60e0b4184498", size = 18764625, upload-time = "2026-08-03T17:49:43.676Z" }, + { url = "https://files.pythonhosted.org/packages/db/2a/654116e6ee2808562a6b2a11c396bdb46d45689e3bf7206ee99400589cab/faiss_cpu-1.15.0-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:30da3029952f0de69f16ce31946fd63fc3e292c867749bbcd2c0a0f09fd06f65", size = 11413863, upload-time = "2026-08-03T17:49:46.471Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/0a0f09659c1972aa83b9820cd3dd7f68f6678cfcfebde542e1c23d7d8663/faiss_cpu-1.15.0-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:88fbe1acac6978869063cb2f9477f85718da596a6e0a17751618f9c756bce255", size = 19470092, upload-time = "2026-08-03T17:49:50.253Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b1/47967207659650ad74c1b06c42671e6beb4f7d798fe6eb2d53ba5e77ad90/faiss_cpu-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:90169515a95ea58a9a95d419e518907927a8ef54c46788396365ec5902c9c8df", size = 16247194, upload-time = "2026-08-03T17:49:56.38Z" }, + { url = "https://files.pythonhosted.org/packages/b2/76/021398ec5608314124b554bb025878a86f129bcf3576c293826352d9a783/faiss_cpu-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:5b940897b317febaa761088513a3db164fad3ac71a5e1ed7be9a052c9bf1a447", size = 16251530, upload-time = "2026-08-03T17:50:00.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/74/4a70395a6e07036628a1bd0b3f709101a6aecfa6a746db13b6e7921cf291/faiss_cpu-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:22dddb013e764aad66dac6cd15b49c7598d60339e0591b73b5e081629419c21b", size = 16251914, upload-time = "2026-08-03T17:50:03.293Z" }, + { url = "https://files.pythonhosted.org/packages/ec/13/0a021b9df16963f839a3f325657656b70f23b5a6dbeb422eaa187d0121b3/faiss_cpu-1.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:37170d5e9ead4b6bfd9c314afc39e17e92064068a0c5a4063dd3f39568c2667e", size = 16535739, upload-time = "2026-08-03T17:50:06.714Z" }, ] [[package]] @@ -604,15 +761,15 @@ wheels = [ [[package]] name = "google-auth" -version = "2.49.1" +version = "2.57.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyasn1-modules" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ea/80/6a696a07d3d3b0a92488933532f03dbefa4a24ab80fb231395b9a2a1be77/google_auth-2.49.1.tar.gz", hash = "sha256:16d40da1c3c5a0533f57d268fe72e0ebb0ae1cc3b567024122651c045d879b64", size = 333825, upload-time = "2026-03-12T19:30:58.135Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/3a/d3982b28d267880b5641f9a32c55d8062a9d2bad2d28d0274285391c89c2/google_auth-2.57.1.tar.gz", hash = "sha256:eb47b230fc6707eed4aee1c9cef55ec05bc1785eecba74ff8b572d531e921b1e", size = 372426, upload-time = "2026-09-04T00:50:27.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/eb/c6c2478d8a8d633460be40e2a8a6f8f429171997a35a96f81d3b680dec83/google_auth-2.49.1-py3-none-any.whl", hash = "sha256:195ebe3dca18eddd1b3db5edc5189b76c13e96f29e73043b923ebcf3f1a860f7", size = 240737, upload-time = "2026-03-12T19:30:53.159Z" }, + { url = "https://files.pythonhosted.org/packages/3b/27/0f7247b8002a1404fdb5412aeb15fb0fe70288eb8f88a5873d1c0d8262cf/google_auth-2.57.1-py3-none-any.whl", hash = "sha256:ab439dee60a6856412bc058f13a68eb6a59c5b81526bb89cfdcf89ce9c0a48c9", size = 259974, upload-time = "2026-09-04T00:50:21.693Z" }, ] [package.optional-dependencies] @@ -622,7 +779,7 @@ requests = [ [[package]] name = "google-genai" -version = "1.68.0" +version = "2.22.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, @@ -636,56 +793,72 @@ dependencies = [ { name = "typing-extensions" }, { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9c/2c/f059982dbcb658cc535c81bbcbe7e2c040d675f4b563b03cdb01018a4bc3/google_genai-1.68.0.tar.gz", hash = "sha256:ac30c0b8bc630f9372993a97e4a11dae0e36f2e10d7c55eacdca95a9fa14ca96", size = 511285, upload-time = "2026-03-18T01:03:18.243Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/f1/f2f31b2a6bd826bc2cb73068df880954a026474c03a4315637d20cc13965/google_genai-2.22.0.tar.gz", hash = "sha256:9fa3b5d9ddb635005d8ab2d6206fb2b3d7204b66965bbce7de13ecd1a866ebcd", size = 684719, upload-time = "2026-09-02T18:06:02.906Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/de/7d3ee9c94b74c3578ea4f88d45e8de9405902f857932334d81e89bce3dfa/google_genai-1.68.0-py3-none-any.whl", hash = "sha256:a1bc9919c0e2ea2907d1e319b65471d3d6d58c54822039a249fe1323e4178d15", size = 750912, upload-time = "2026-03-18T01:03:15.983Z" }, + { url = "https://files.pythonhosted.org/packages/de/96/0120d214958cb54f2b9c48da9f564a0e6eae269e9dd702415fb1d0551cc7/google_genai-2.22.0-py3-none-any.whl", hash = "sha256:c514001c45470cc0a942440ae1b8215445d12bfb6c373aac94637127e1f74ec6", size = 1088792, upload-time = "2026-09-02T18:06:00.629Z" }, ] [[package]] name = "greenlet" -version = "3.3.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a3/51/1664f6b78fc6ebbd98019a1fd730e83fa78f2db7058f72b1463d3612b8db/greenlet-3.3.2.tar.gz", hash = "sha256:2eaf067fc6d886931c7962e8c6bede15d2f01965560f3359b27c80bde2d151f2", size = 188267, upload-time = "2026-02-20T20:54:15.531Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/47/16400cb42d18d7a6bb46f0626852c1718612e35dcb0dffa16bbaffdf5dd2/greenlet-3.3.2-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:c56692189a7d1c7606cb794be0a8381470d95c57ce5be03fb3d0ef57c7853b86", size = 278890, upload-time = "2026-02-20T20:19:39.263Z" }, - { url = "https://files.pythonhosted.org/packages/a3/90/42762b77a5b6aa96cd8c0e80612663d39211e8ae8a6cd47c7f1249a66262/greenlet-3.3.2-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ebd458fa8285960f382841da585e02201b53a5ec2bac6b156fc623b5ce4499f", size = 581120, upload-time = "2026-02-20T20:47:30.161Z" }, - { url = "https://files.pythonhosted.org/packages/bf/6f/f3d64f4fa0a9c7b5c5b3c810ff1df614540d5aa7d519261b53fba55d4df9/greenlet-3.3.2-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a443358b33c4ec7b05b79a7c8b466f5d275025e750298be7340f8fc63dff2a55", size = 594363, upload-time = "2026-02-20T20:55:56.965Z" }, - { url = "https://files.pythonhosted.org/packages/72/83/3e06a52aca8128bdd4dcd67e932b809e76a96ab8c232a8b025b2850264c5/greenlet-3.3.2-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e2cd90d413acbf5e77ae41e5d3c9b3ac1d011a756d7284d7f3f2b806bbd6358", size = 594156, upload-time = "2026-02-20T20:20:59.955Z" }, - { url = "https://files.pythonhosted.org/packages/70/79/0de5e62b873e08fe3cef7dbe84e5c4bc0e8ed0c7ff131bccb8405cd107c8/greenlet-3.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:442b6057453c8cb29b4fb36a2ac689382fc71112273726e2423f7f17dc73bf99", size = 1554649, upload-time = "2026-02-20T20:49:32.293Z" }, - { url = "https://files.pythonhosted.org/packages/5a/00/32d30dee8389dc36d42170a9c66217757289e2afb0de59a3565260f38373/greenlet-3.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45abe8eb6339518180d5a7fa47fa01945414d7cca5ecb745346fc6a87d2750be", size = 1619472, upload-time = "2026-02-20T20:21:07.966Z" }, - { url = "https://files.pythonhosted.org/packages/f1/3a/efb2cf697fbccdf75b24e2c18025e7dfa54c4f31fab75c51d0fe79942cef/greenlet-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e692b2dae4cc7077cbb11b47d258533b48c8fde69a33d0d8a82e2fe8d8531d5", size = 230389, upload-time = "2026-02-20T20:17:18.772Z" }, - { url = "https://files.pythonhosted.org/packages/e1/a1/65bbc059a43a7e2143ec4fc1f9e3f673e04f9c7b371a494a101422ac4fd5/greenlet-3.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:02b0a8682aecd4d3c6c18edf52bc8e51eacdd75c8eac52a790a210b06aa295fd", size = 229645, upload-time = "2026-02-20T20:18:18.695Z" }, - { url = "https://files.pythonhosted.org/packages/ea/ab/1608e5a7578e62113506740b88066bf09888322a311cff602105e619bd87/greenlet-3.3.2-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:ac8d61d4343b799d1e526db579833d72f23759c71e07181c2d2944e429eb09cd", size = 280358, upload-time = "2026-02-20T20:17:43.971Z" }, - { url = "https://files.pythonhosted.org/packages/a5/23/0eae412a4ade4e6623ff7626e38998cb9b11e9ff1ebacaa021e4e108ec15/greenlet-3.3.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ceec72030dae6ac0c8ed7591b96b70410a8be370b6a477b1dbc072856ad02bd", size = 601217, upload-time = "2026-02-20T20:47:31.462Z" }, - { url = "https://files.pythonhosted.org/packages/f8/16/5b1678a9c07098ecb9ab2dd159fafaf12e963293e61ee8d10ecb55273e5e/greenlet-3.3.2-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a2a5be83a45ce6188c045bcc44b0ee037d6a518978de9a5d97438548b953a1ac", size = 611792, upload-time = "2026-02-20T20:55:58.423Z" }, - { url = "https://files.pythonhosted.org/packages/50/1f/5155f55bd71cabd03765a4aac9ac446be129895271f73872c36ebd4b04b6/greenlet-3.3.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e99d1749147ac21dde49b99c9abffcbc1e2d55c67501465ef0930d6e78e070", size = 613875, upload-time = "2026-02-20T20:21:01.102Z" }, - { url = "https://files.pythonhosted.org/packages/fc/dd/845f249c3fcd69e32df80cdab059b4be8b766ef5830a3d0aa9d6cad55beb/greenlet-3.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c956a19350e2c37f2c48b336a3afb4bff120b36076d9d7fb68cb44e05d95b79", size = 1571467, upload-time = "2026-02-20T20:49:33.495Z" }, - { url = "https://files.pythonhosted.org/packages/2a/50/2649fe21fcc2b56659a452868e695634722a6655ba245d9f77f5656010bf/greenlet-3.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6c6f8ba97d17a1e7d664151284cb3315fc5f8353e75221ed4324f84eb162b395", size = 1640001, upload-time = "2026-02-20T20:21:09.154Z" }, - { url = "https://files.pythonhosted.org/packages/9b/40/cc802e067d02af8b60b6771cea7d57e21ef5e6659912814babb42b864713/greenlet-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:34308836d8370bddadb41f5a7ce96879b72e2fdfb4e87729330c6ab52376409f", size = 231081, upload-time = "2026-02-20T20:17:28.121Z" }, - { url = "https://files.pythonhosted.org/packages/58/2e/fe7f36ff1982d6b10a60d5e0740c759259a7d6d2e1dc41da6d96de32fff6/greenlet-3.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:d3a62fa76a32b462a97198e4c9e99afb9ab375115e74e9a83ce180e7a496f643", size = 230331, upload-time = "2026-02-20T20:17:23.34Z" }, - { url = "https://files.pythonhosted.org/packages/ac/48/f8b875fa7dea7dd9b33245e37f065af59df6a25af2f9561efa8d822fde51/greenlet-3.3.2-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:aa6ac98bdfd716a749b84d4034486863fd81c3abde9aa3cf8eff9127981a4ae4", size = 279120, upload-time = "2026-02-20T20:19:01.9Z" }, - { url = "https://files.pythonhosted.org/packages/49/8d/9771d03e7a8b1ee456511961e1b97a6d77ae1dea4a34a5b98eee706689d3/greenlet-3.3.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab0c7e7901a00bc0a7284907273dc165b32e0d109a6713babd04471327ff7986", size = 603238, upload-time = "2026-02-20T20:47:32.873Z" }, - { url = "https://files.pythonhosted.org/packages/59/0e/4223c2bbb63cd5c97f28ffb2a8aee71bdfb30b323c35d409450f51b91e3e/greenlet-3.3.2-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d248d8c23c67d2291ffd47af766e2a3aa9fa1c6703155c099feb11f526c63a92", size = 614219, upload-time = "2026-02-20T20:55:59.817Z" }, - { url = "https://files.pythonhosted.org/packages/7a/34/259b28ea7a2a0c904b11cd36c79b8cef8019b26ee5dbe24e73b469dea347/greenlet-3.3.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6997d360a4e6a4e936c0f9625b1c20416b8a0ea18a8e19cabbefc712e7397ab", size = 616774, upload-time = "2026-02-20T20:21:02.454Z" }, - { url = "https://files.pythonhosted.org/packages/0a/03/996c2d1689d486a6e199cb0f1cf9e4aa940c500e01bdf201299d7d61fa69/greenlet-3.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:64970c33a50551c7c50491671265d8954046cb6e8e2999aacdd60e439b70418a", size = 1571277, upload-time = "2026-02-20T20:49:34.795Z" }, - { url = "https://files.pythonhosted.org/packages/d9/c4/2570fc07f34a39f2caf0bf9f24b0a1a0a47bc2e8e465b2c2424821389dfc/greenlet-3.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a9172f5bf6bd88e6ba5a84e0a68afeac9dc7b6b412b245dd64f52d83c81e55b", size = 1640455, upload-time = "2026-02-20T20:21:10.261Z" }, - { url = "https://files.pythonhosted.org/packages/91/39/5ef5aa23bc545aa0d31e1b9b55822b32c8da93ba657295840b6b34124009/greenlet-3.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:a7945dd0eab63ded0a48e4dcade82939783c172290a7903ebde9e184333ca124", size = 230961, upload-time = "2026-02-20T20:16:58.461Z" }, - { url = "https://files.pythonhosted.org/packages/62/6b/a89f8456dcb06becff288f563618e9f20deed8dd29beea14f9a168aef64b/greenlet-3.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:394ead29063ee3515b4e775216cb756b2e3b4a7e55ae8fd884f17fa579e6b327", size = 230221, upload-time = "2026-02-20T20:17:37.152Z" }, - { url = "https://files.pythonhosted.org/packages/3f/ae/8bffcbd373b57a5992cd077cbe8858fff39110480a9d50697091faea6f39/greenlet-3.3.2-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:8d1658d7291f9859beed69a776c10822a0a799bc4bfe1bd4272bb60e62507dab", size = 279650, upload-time = "2026-02-20T20:18:00.783Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c0/45f93f348fa49abf32ac8439938726c480bd96b2a3c6f4d949ec0124b69f/greenlet-3.3.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:18cb1b7337bca281915b3c5d5ae19f4e76d35e1df80f4ad3c1a7be91fadf1082", size = 650295, upload-time = "2026-02-20T20:47:34.036Z" }, - { url = "https://files.pythonhosted.org/packages/b3/de/dd7589b3f2b8372069ab3e4763ea5329940fc7ad9dcd3e272a37516d7c9b/greenlet-3.3.2-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2e47408e8ce1c6f1ceea0dffcdf6ebb85cc09e55c7af407c99f1112016e45e9", size = 662163, upload-time = "2026-02-20T20:56:01.295Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/09bfa816572a4d83bccd6750df1926f79158b1c36c5f73786e26dbe4ee38/greenlet-3.3.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63d10328839d1973e5ba35e98cccbca71b232b14051fd957b6f8b6e8e80d0506", size = 664160, upload-time = "2026-02-20T20:21:04.015Z" }, - { url = "https://files.pythonhosted.org/packages/48/cf/56832f0c8255d27f6c35d41b5ec91168d74ec721d85f01a12131eec6b93c/greenlet-3.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8e4ab3cfb02993c8cc248ea73d7dae6cec0253e9afa311c9b37e603ca9fad2ce", size = 1619181, upload-time = "2026-02-20T20:49:36.052Z" }, - { url = "https://files.pythonhosted.org/packages/0a/23/b90b60a4aabb4cec0796e55f25ffbfb579a907c3898cd2905c8918acaa16/greenlet-3.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94ad81f0fd3c0c0681a018a976e5c2bd2ca2d9d94895f23e7bb1af4e8af4e2d5", size = 1687713, upload-time = "2026-02-20T20:21:11.684Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ca/2101ca3d9223a1dc125140dbc063644dca76df6ff356531eb27bc267b446/greenlet-3.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:8c4dd0f3997cf2512f7601563cc90dfb8957c0cff1e3a1b23991d4ea1776c492", size = 232034, upload-time = "2026-02-20T20:20:08.186Z" }, - { url = "https://files.pythonhosted.org/packages/f6/4a/ecf894e962a59dea60f04877eea0fd5724618da89f1867b28ee8b91e811f/greenlet-3.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:cd6f9e2bbd46321ba3bbb4c8a15794d32960e3b0ae2cc4d49a1a53d314805d71", size = 231437, upload-time = "2026-02-20T20:18:59.722Z" }, - { url = "https://files.pythonhosted.org/packages/98/6d/8f2ef704e614bcf58ed43cfb8d87afa1c285e98194ab2cfad351bf04f81e/greenlet-3.3.2-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:e26e72bec7ab387ac80caa7496e0f908ff954f31065b0ffc1f8ecb1338b11b54", size = 286617, upload-time = "2026-02-20T20:19:29.856Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0d/93894161d307c6ea237a43988f27eba0947b360b99ac5239ad3fe09f0b47/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b466dff7a4ffda6ca975979bab80bdadde979e29fc947ac3be4451428d8b0e4", size = 655189, upload-time = "2026-02-20T20:47:35.742Z" }, - { url = "https://files.pythonhosted.org/packages/f5/2c/d2d506ebd8abcb57386ec4f7ba20f4030cbe56eae541bc6fd6ef399c0b41/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8bddc5b73c9720bea487b3bffdb1840fe4e3656fba3bd40aa1489e9f37877ff", size = 658225, upload-time = "2026-02-20T20:56:02.527Z" }, - { url = "https://files.pythonhosted.org/packages/8e/30/3a09155fbf728673a1dea713572d2d31159f824a37c22da82127056c44e4/greenlet-3.3.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b26b0f4428b871a751968285a1ac9648944cea09807177ac639b030bddebcea4", size = 657907, upload-time = "2026-02-20T20:21:05.259Z" }, - { url = "https://files.pythonhosted.org/packages/f3/fd/d05a4b7acd0154ed758797f0a43b4c0962a843bedfe980115e842c5b2d08/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1fb39a11ee2e4d94be9a76671482be9398560955c9e568550de0224e41104727", size = 1618857, upload-time = "2026-02-20T20:49:37.309Z" }, - { url = "https://files.pythonhosted.org/packages/6f/e1/50ee92a5db521de8f35075b5eff060dd43d39ebd46c2181a2042f7070385/greenlet-3.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:20154044d9085151bc309e7689d6f7ba10027f8f5a8c0676ad398b951913d89e", size = 1680010, upload-time = "2026-02-20T20:21:13.427Z" }, - { url = "https://files.pythonhosted.org/packages/29/4b/45d90626aef8e65336bed690106d1382f7a43665e2249017e9527df8823b/greenlet-3.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c04c5e06ec3e022cbfe2cd4a846e1d4e50087444f875ff6d2c2ad8445495cf1a", size = 237086, upload-time = "2026-02-20T20:20:45.786Z" }, +version = "3.5.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" }, + { url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/51/2d/f2c928218ac52f26d7a2c188c171d1b7e728b23782cb3347e7b4fce1493a/greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864", size = 624562, upload-time = "2026-08-10T13:40:48.064Z" }, + { url = "https://files.pythonhosted.org/packages/3e/4a/92fc51d5d35912f4f06eec037ba347985defd0be47463a010a325634d9d2/greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db", size = 1584909, upload-time = "2026-08-10T14:15:04.343Z" }, + { url = "https://files.pythonhosted.org/packages/ac/58/ed98b80ac5738c149a5258544843c45601ade1fd70f61740cdaead6351b3/greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39", size = 1651184, upload-time = "2026-08-10T13:40:28.879Z" }, + { url = "https://files.pythonhosted.org/packages/d8/be/b582ceb80cefdf9d8da34078714e4b12b3d16f509dee0f65e40a5cc8fc7d/greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53", size = 323280, upload-time = "2026-08-10T13:26:07.495Z" }, + { url = "https://files.pythonhosted.org/packages/4d/18/5313c4c58598c38b0373c013e4ff2b3e6d258aaaa338f373335ebecdaddd/greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5", size = 307785, upload-time = "2026-08-10T13:28:34.874Z" }, + { url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" }, + { url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" }, + { url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" }, + { url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" }, + { url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" }, + { url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" }, + { url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" }, + { url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" }, + { url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" }, + { url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" }, + { url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" }, + { url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" }, + { url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" }, + { url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" }, + { url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" }, + { url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" }, + { url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" }, + { url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" }, + { url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" }, + { url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" }, + { url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" }, + { url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" }, + { url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" }, + { url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" }, + { url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" }, + { url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" }, + { url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" }, + { url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" }, + { url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" }, + { url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" }, + { url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" }, + { url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" }, + { url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" }, + { url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" }, ] [[package]] @@ -710,6 +883,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -734,13 +920,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "httpx2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + [[package]] name = "idna" -version = "3.11" +version = "3.19" 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, upload-time = "2025-10-12T14:55:20.501Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" }, ] [[package]] @@ -760,87 +972,88 @@ sdist = { url = "https://files.pythonhosted.org/packages/c6/cb/18eeb235f833b7265 [[package]] name = "jiter" -version = "0.13.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/5e/4ec91646aee381d01cdb9974e30882c9cd3b8c5d1079d6b5ff4af522439a/jiter-0.13.0.tar.gz", hash = "sha256:f2839f9c2c7e2dffc1bc5929a510e14ce0a946be9365fd1219e7ef342dae14f4", size = 164847, upload-time = "2026-02-02T12:37:56.441Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/71/29/499f8c9eaa8a16751b1c0e45e6f5f1761d180da873d417996cc7bddc8eef/jiter-0.13.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ea026e70a9a28ebbdddcbcf0f1323128a8db66898a06eaad3a4e62d2f554d096", size = 311157, upload-time = "2026-02-02T12:35:37.758Z" }, - { url = "https://files.pythonhosted.org/packages/50/f6/566364c777d2ab450b92100bea11333c64c38d32caf8dc378b48e5b20c46/jiter-0.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66aa3e663840152d18cc8ff1e4faad3dd181373491b9cfdc6004b92198d67911", size = 319729, upload-time = "2026-02-02T12:35:39.246Z" }, - { url = "https://files.pythonhosted.org/packages/73/dd/560f13ec5e4f116d8ad2658781646cca91b617ae3b8758d4a5076b278f70/jiter-0.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3524798e70655ff19aec58c7d05adb1f074fecff62da857ea9be2b908b6d701", size = 354766, upload-time = "2026-02-02T12:35:40.662Z" }, - { url = "https://files.pythonhosted.org/packages/7c/0d/061faffcfe94608cbc28a0d42a77a74222bdf5055ccdbe5fd2292b94f510/jiter-0.13.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ec7e287d7fbd02cb6e22f9a00dd9c9cd504c40a61f2c61e7e1f9690a82726b4c", size = 362587, upload-time = "2026-02-02T12:35:42.025Z" }, - { url = "https://files.pythonhosted.org/packages/92/c9/c66a7864982fd38a9773ec6e932e0398d1262677b8c60faecd02ffb67bf3/jiter-0.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:47455245307e4debf2ce6c6e65a717550a0244231240dcf3b8f7d64e4c2f22f4", size = 487537, upload-time = "2026-02-02T12:35:43.459Z" }, - { url = "https://files.pythonhosted.org/packages/6c/86/84eb4352cd3668f16d1a88929b5888a3fe0418ea8c1dfc2ad4e7bf6e069a/jiter-0.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee9da221dca6e0429c2704c1b3655fe7b025204a71d4d9b73390c759d776d165", size = 373717, upload-time = "2026-02-02T12:35:44.928Z" }, - { url = "https://files.pythonhosted.org/packages/6e/09/9fe4c159358176f82d4390407a03f506a8659ed13ca3ac93a843402acecf/jiter-0.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24ab43126d5e05f3d53a36a8e11eb2f23304c6c1117844aaaf9a0aa5e40b5018", size = 362683, upload-time = "2026-02-02T12:35:46.636Z" }, - { url = "https://files.pythonhosted.org/packages/c9/5e/85f3ab9caca0c1d0897937d378b4a515cae9e119730563572361ea0c48ae/jiter-0.13.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9da38b4fedde4fb528c740c2564628fbab737166a0e73d6d46cb4bb5463ff411", size = 392345, upload-time = "2026-02-02T12:35:48.088Z" }, - { url = "https://files.pythonhosted.org/packages/12/4c/05b8629ad546191939e6f0c2f17e29f542a398f4a52fb987bc70b6d1eb8b/jiter-0.13.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0b34c519e17658ed88d5047999a93547f8889f3c1824120c26ad6be5f27b6cf5", size = 517775, upload-time = "2026-02-02T12:35:49.482Z" }, - { url = "https://files.pythonhosted.org/packages/4d/88/367ea2eb6bc582c7052e4baf5ddf57ebe5ab924a88e0e09830dfb585c02d/jiter-0.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2a6394e6af690d462310a86b53c47ad75ac8c21dc79f120714ea449979cb1d3", size = 551325, upload-time = "2026-02-02T12:35:51.104Z" }, - { url = "https://files.pythonhosted.org/packages/f3/12/fa377ffb94a2f28c41afaed093e0d70cfe512035d5ecb0cad0ae4792d35e/jiter-0.13.0-cp311-cp311-win32.whl", hash = "sha256:0f0c065695f616a27c920a56ad0d4fc46415ef8b806bf8fc1cacf25002bd24e1", size = 204709, upload-time = "2026-02-02T12:35:52.467Z" }, - { url = "https://files.pythonhosted.org/packages/cb/16/8e8203ce92f844dfcd3d9d6a5a7322c77077248dbb12da52d23193a839cd/jiter-0.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:0733312953b909688ae3c2d58d043aa040f9f1a6a75693defed7bc2cc4bf2654", size = 204560, upload-time = "2026-02-02T12:35:53.925Z" }, - { url = "https://files.pythonhosted.org/packages/44/26/97cc40663deb17b9e13c3a5cf29251788c271b18ee4d262c8f94798b8336/jiter-0.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:5d9b34ad56761b3bf0fbe8f7e55468704107608512350962d3317ffd7a4382d5", size = 189608, upload-time = "2026-02-02T12:35:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/2e/30/7687e4f87086829955013ca12a9233523349767f69653ebc27036313def9/jiter-0.13.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0a2bd69fc1d902e89925fc34d1da51b2128019423d7b339a45d9e99c894e0663", size = 307958, upload-time = "2026-02-02T12:35:57.165Z" }, - { url = "https://files.pythonhosted.org/packages/c3/27/e57f9a783246ed95481e6749cc5002a8a767a73177a83c63ea71f0528b90/jiter-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f917a04240ef31898182f76a332f508f2cc4b57d2b4d7ad2dbfebbfe167eb505", size = 318597, upload-time = "2026-02-02T12:35:58.591Z" }, - { url = "https://files.pythonhosted.org/packages/cf/52/e5719a60ac5d4d7c5995461a94ad5ef962a37c8bf5b088390e6fad59b2ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1e2b199f446d3e82246b4fd9236d7cb502dc2222b18698ba0d986d2fecc6152", size = 348821, upload-time = "2026-02-02T12:36:00.093Z" }, - { url = "https://files.pythonhosted.org/packages/61/db/c1efc32b8ba4c740ab3fc2d037d8753f67685f475e26b9d6536a4322bcdd/jiter-0.13.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04670992b576fa65bd056dbac0c39fe8bd67681c380cb2b48efa885711d9d726", size = 364163, upload-time = "2026-02-02T12:36:01.937Z" }, - { url = "https://files.pythonhosted.org/packages/55/8a/fb75556236047c8806995671a18e4a0ad646ed255276f51a20f32dceaeec/jiter-0.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a1aff1fbdb803a376d4d22a8f63f8e7ccbce0b4890c26cc7af9e501ab339ef0", size = 483709, upload-time = "2026-02-02T12:36:03.41Z" }, - { url = "https://files.pythonhosted.org/packages/7e/16/43512e6ee863875693a8e6f6d532e19d650779d6ba9a81593ae40a9088ff/jiter-0.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b3fb8c2053acaef8580809ac1d1f7481a0a0bdc012fd7f5d8b18fb696a5a089", size = 370480, upload-time = "2026-02-02T12:36:04.791Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/09b93e30e984a187bc8aaa3510e1ec8dcbdcd71ca05d2f56aac0492453aa/jiter-0.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bdaba7d87e66f26a2c45d8cbadcbfc4bf7884182317907baf39cfe9775bb4d93", size = 360735, upload-time = "2026-02-02T12:36:06.994Z" }, - { url = "https://files.pythonhosted.org/packages/1a/1b/46c5e349019874ec5dfa508c14c37e29864ea108d376ae26d90bee238cd7/jiter-0.13.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7b88d649135aca526da172e48083da915ec086b54e8e73a425ba50999468cc08", size = 391814, upload-time = "2026-02-02T12:36:08.368Z" }, - { url = "https://files.pythonhosted.org/packages/15/9e/26184760e85baee7162ad37b7912797d2077718476bf91517641c92b3639/jiter-0.13.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e404ea551d35438013c64b4f357b0474c7abf9f781c06d44fcaf7a14c69ff9e2", size = 513990, upload-time = "2026-02-02T12:36:09.993Z" }, - { url = "https://files.pythonhosted.org/packages/e9/34/2c9355247d6debad57a0a15e76ab1566ab799388042743656e566b3b7de1/jiter-0.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1f4748aad1b4a93c8bdd70f604d0f748cdc0e8744c5547798acfa52f10e79228", size = 548021, upload-time = "2026-02-02T12:36:11.376Z" }, - { url = "https://files.pythonhosted.org/packages/ac/4a/9f2c23255d04a834398b9c2e0e665382116911dc4d06b795710503cdad25/jiter-0.13.0-cp312-cp312-win32.whl", hash = "sha256:0bf670e3b1445fc4d31612199f1744f67f889ee1bbae703c4b54dc097e5dd394", size = 203024, upload-time = "2026-02-02T12:36:12.682Z" }, - { url = "https://files.pythonhosted.org/packages/09/ee/f0ae675a957ae5a8f160be3e87acea6b11dc7b89f6b7ab057e77b2d2b13a/jiter-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:15db60e121e11fe186c0b15236bd5d18381b9ddacdcf4e659feb96fc6c969c92", size = 205424, upload-time = "2026-02-02T12:36:13.93Z" }, - { url = "https://files.pythonhosted.org/packages/1b/02/ae611edf913d3cbf02c97cdb90374af2082c48d7190d74c1111dde08bcdd/jiter-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:41f92313d17989102f3cb5dd533a02787cdb99454d494344b0361355da52fcb9", size = 186818, upload-time = "2026-02-02T12:36:15.308Z" }, - { url = "https://files.pythonhosted.org/packages/91/9c/7ee5a6ff4b9991e1a45263bfc46731634c4a2bde27dfda6c8251df2d958c/jiter-0.13.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1f8a55b848cbabf97d861495cd65f1e5c590246fabca8b48e1747c4dfc8f85bf", size = 306897, upload-time = "2026-02-02T12:36:16.748Z" }, - { url = "https://files.pythonhosted.org/packages/7c/02/be5b870d1d2be5dd6a91bdfb90f248fbb7dcbd21338f092c6b89817c3dbf/jiter-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f556aa591c00f2c45eb1b89f68f52441a016034d18b65da60e2d2875bbbf344a", size = 317507, upload-time = "2026-02-02T12:36:18.351Z" }, - { url = "https://files.pythonhosted.org/packages/da/92/b25d2ec333615f5f284f3a4024f7ce68cfa0604c322c6808b2344c7f5d2b/jiter-0.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f7e1d61da332ec412350463891923f960c3073cf1aae93b538f0bb4c8cd46efb", size = 350560, upload-time = "2026-02-02T12:36:19.746Z" }, - { url = "https://files.pythonhosted.org/packages/be/ec/74dcb99fef0aca9fbe56b303bf79f6bd839010cb18ad41000bf6cc71eec0/jiter-0.13.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3097d665a27bc96fd9bbf7f86178037db139f319f785e4757ce7ccbf390db6c2", size = 363232, upload-time = "2026-02-02T12:36:21.243Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/f17375e0bb2f6a812d4dd92d7616e41917f740f3e71343627da9db2824ce/jiter-0.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d01ecc3a8cbdb6f25a37bd500510550b64ddf9f7d64a107d92f3ccb25035d0f", size = 483727, upload-time = "2026-02-02T12:36:22.688Z" }, - { url = "https://files.pythonhosted.org/packages/77/d2/a71160a5ae1a1e66c1395b37ef77da67513b0adba73b993a27fbe47eb048/jiter-0.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ed9bbc30f5d60a3bdf63ae76beb3f9db280d7f195dfcfa61af792d6ce912d159", size = 370799, upload-time = "2026-02-02T12:36:24.106Z" }, - { url = "https://files.pythonhosted.org/packages/01/99/ed5e478ff0eb4e8aa5fd998f9d69603c9fd3f32de3bd16c2b1194f68361c/jiter-0.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98fbafb6e88256f4454de33c1f40203d09fc33ed19162a68b3b257b29ca7f663", size = 359120, upload-time = "2026-02-02T12:36:25.519Z" }, - { url = "https://files.pythonhosted.org/packages/16/be/7ffd08203277a813f732ba897352797fa9493faf8dc7995b31f3d9cb9488/jiter-0.13.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5467696f6b827f1116556cb0db620440380434591e93ecee7fd14d1a491b6daa", size = 390664, upload-time = "2026-02-02T12:36:26.866Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/e0787856196d6d346264d6dcccb01f741e5f0bd014c1d9a2ebe149caf4f3/jiter-0.13.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:2d08c9475d48b92892583df9da592a0e2ac49bcd41fae1fec4f39ba6cf107820", size = 513543, upload-time = "2026-02-02T12:36:28.217Z" }, - { url = "https://files.pythonhosted.org/packages/65/50/ecbd258181c4313cf79bca6c88fb63207d04d5bf5e4f65174114d072aa55/jiter-0.13.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:aed40e099404721d7fcaf5b89bd3b4568a4666358bcac7b6b15c09fb6252ab68", size = 547262, upload-time = "2026-02-02T12:36:29.678Z" }, - { url = "https://files.pythonhosted.org/packages/27/da/68f38d12e7111d2016cd198161b36e1f042bd115c169255bcb7ec823a3bf/jiter-0.13.0-cp313-cp313-win32.whl", hash = "sha256:36ebfbcffafb146d0e6ffb3e74d51e03d9c35ce7c625c8066cdbfc7b953bdc72", size = 200630, upload-time = "2026-02-02T12:36:31.808Z" }, - { url = "https://files.pythonhosted.org/packages/25/65/3bd1a972c9a08ecd22eb3b08a95d1941ebe6938aea620c246cf426ae09c2/jiter-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:8d76029f077379374cf0dbc78dbe45b38dec4a2eb78b08b5194ce836b2517afc", size = 202602, upload-time = "2026-02-02T12:36:33.679Z" }, - { url = "https://files.pythonhosted.org/packages/15/fe/13bd3678a311aa67686bb303654792c48206a112068f8b0b21426eb6851e/jiter-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:bb7613e1a427cfcb6ea4544f9ac566b93d5bf67e0d48c787eca673ff9c9dff2b", size = 185939, upload-time = "2026-02-02T12:36:35.065Z" }, - { url = "https://files.pythonhosted.org/packages/49/19/a929ec002ad3228bc97ca01dbb14f7632fffdc84a95ec92ceaf4145688ae/jiter-0.13.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fa476ab5dd49f3bf3a168e05f89358c75a17608dbabb080ef65f96b27c19ab10", size = 316616, upload-time = "2026-02-02T12:36:36.579Z" }, - { url = "https://files.pythonhosted.org/packages/52/56/d19a9a194afa37c1728831e5fb81b7722c3de18a3109e8f282bfc23e587a/jiter-0.13.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ade8cb6ff5632a62b7dbd4757d8c5573f7a2e9ae285d6b5b841707d8363205ef", size = 346850, upload-time = "2026-02-02T12:36:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/36/4a/94e831c6bf287754a8a019cb966ed39ff8be6ab78cadecf08df3bb02d505/jiter-0.13.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9950290340acc1adaded363edd94baebcee7dabdfa8bee4790794cd5cfad2af6", size = 358551, upload-time = "2026-02-02T12:36:39.417Z" }, - { url = "https://files.pythonhosted.org/packages/a2/ec/a4c72c822695fa80e55d2b4142b73f0012035d9fcf90eccc56bc060db37c/jiter-0.13.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2b4972c6df33731aac0742b64fd0d18e0a69bc7d6e03108ce7d40c85fd9e3e6d", size = 201950, upload-time = "2026-02-02T12:36:40.791Z" }, - { url = "https://files.pythonhosted.org/packages/b6/00/393553ec27b824fbc29047e9c7cd4a3951d7fbe4a76743f17e44034fa4e4/jiter-0.13.0-cp313-cp313t-win_arm64.whl", hash = "sha256:701a1e77d1e593c1b435315ff625fd071f0998c5f02792038a5ca98899261b7d", size = 185852, upload-time = "2026-02-02T12:36:42.077Z" }, - { url = "https://files.pythonhosted.org/packages/6e/f5/f1997e987211f6f9bd71b8083047b316208b4aca0b529bb5f8c96c89ef3e/jiter-0.13.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:cc5223ab19fe25e2f0bf2643204ad7318896fe3729bf12fde41b77bfc4fafff0", size = 308804, upload-time = "2026-02-02T12:36:43.496Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8f/5482a7677731fd44881f0204981ce2d7175db271f82cba2085dd2212e095/jiter-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9776ebe51713acf438fd9b4405fcd86893ae5d03487546dae7f34993217f8a91", size = 318787, upload-time = "2026-02-02T12:36:45.071Z" }, - { url = "https://files.pythonhosted.org/packages/f3/b9/7257ac59778f1cd025b26a23c5520a36a424f7f1b068f2442a5b499b7464/jiter-0.13.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:879e768938e7b49b5e90b7e3fecc0dbec01b8cb89595861fb39a8967c5220d09", size = 353880, upload-time = "2026-02-02T12:36:47.365Z" }, - { url = "https://files.pythonhosted.org/packages/c3/87/719eec4a3f0841dad99e3d3604ee4cba36af4419a76f3cb0b8e2e691ad67/jiter-0.13.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:682161a67adea11e3aae9038c06c8b4a9a71023228767477d683f69903ebc607", size = 366702, upload-time = "2026-02-02T12:36:48.871Z" }, - { url = "https://files.pythonhosted.org/packages/d2/65/415f0a75cf6921e43365a1bc227c565cb949caca8b7532776e430cbaa530/jiter-0.13.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a13b68cd1cd8cc9de8f244ebae18ccb3e4067ad205220ef324c39181e23bbf66", size = 486319, upload-time = "2026-02-02T12:36:53.006Z" }, - { url = "https://files.pythonhosted.org/packages/54/a2/9e12b48e82c6bbc6081fd81abf915e1443add1b13d8fc586e1d90bb02bb8/jiter-0.13.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:87ce0f14c6c08892b610686ae8be350bf368467b6acd5085a5b65441e2bf36d2", size = 372289, upload-time = "2026-02-02T12:36:54.593Z" }, - { url = "https://files.pythonhosted.org/packages/4e/c1/e4693f107a1789a239c759a432e9afc592366f04e901470c2af89cfd28e1/jiter-0.13.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c365005b05505a90d1c47856420980d0237adf82f70c4aff7aebd3c1cc143ad", size = 360165, upload-time = "2026-02-02T12:36:56.112Z" }, - { url = "https://files.pythonhosted.org/packages/17/08/91b9ea976c1c758240614bd88442681a87672eebc3d9a6dde476874e706b/jiter-0.13.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1317fdffd16f5873e46ce27d0e0f7f4f90f0cdf1d86bf6abeaea9f63ca2c401d", size = 389634, upload-time = "2026-02-02T12:36:57.495Z" }, - { url = "https://files.pythonhosted.org/packages/18/23/58325ef99390d6d40427ed6005bf1ad54f2577866594bcf13ce55675f87d/jiter-0.13.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:c05b450d37ba0c9e21c77fef1f205f56bcee2330bddca68d344baebfc55ae0df", size = 514933, upload-time = "2026-02-02T12:36:58.909Z" }, - { url = "https://files.pythonhosted.org/packages/5b/25/69f1120c7c395fd276c3996bb8adefa9c6b84c12bb7111e5c6ccdcd8526d/jiter-0.13.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:775e10de3849d0631a97c603f996f518159272db00fdda0a780f81752255ee9d", size = 548842, upload-time = "2026-02-02T12:37:00.433Z" }, - { url = "https://files.pythonhosted.org/packages/18/05/981c9669d86850c5fbb0d9e62bba144787f9fba84546ba43d624ee27ef29/jiter-0.13.0-cp314-cp314-win32.whl", hash = "sha256:632bf7c1d28421c00dd8bbb8a3bac5663e1f57d5cd5ed962bce3c73bf62608e6", size = 202108, upload-time = "2026-02-02T12:37:01.718Z" }, - { url = "https://files.pythonhosted.org/packages/8d/96/cdcf54dd0b0341db7d25413229888a346c7130bd20820530905fdb65727b/jiter-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:f22ef501c3f87ede88f23f9b11e608581c14f04db59b6a801f354397ae13739f", size = 204027, upload-time = "2026-02-02T12:37:03.075Z" }, - { url = "https://files.pythonhosted.org/packages/fb/f9/724bcaaab7a3cd727031fe4f6995cb86c4bd344909177c186699c8dec51a/jiter-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:07b75fe09a4ee8e0c606200622e571e44943f47254f95e2436c8bdcaceb36d7d", size = 187199, upload-time = "2026-02-02T12:37:04.414Z" }, - { url = "https://files.pythonhosted.org/packages/62/92/1661d8b9fd6a3d7a2d89831db26fe3c1509a287d83ad7838831c7b7a5c7e/jiter-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:964538479359059a35fb400e769295d4b315ae61e4105396d355a12f7fef09f0", size = 318423, upload-time = "2026-02-02T12:37:05.806Z" }, - { url = "https://files.pythonhosted.org/packages/4f/3b/f77d342a54d4ebcd128e520fc58ec2f5b30a423b0fd26acdfc0c6fef8e26/jiter-0.13.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e104da1db1c0991b3eaed391ccd650ae8d947eab1480c733e5a3fb28d4313e40", size = 351438, upload-time = "2026-02-02T12:37:07.189Z" }, - { url = "https://files.pythonhosted.org/packages/76/b3/ba9a69f0e4209bd3331470c723c2f5509e6f0482e416b612431a5061ed71/jiter-0.13.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e3a5f0cde8ff433b8e88e41aa40131455420fb3649a3c7abdda6145f8cb7202", size = 364774, upload-time = "2026-02-02T12:37:08.579Z" }, - { url = "https://files.pythonhosted.org/packages/b3/16/6cdb31fa342932602458dbb631bfbd47f601e03d2e4950740e0b2100b570/jiter-0.13.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:57aab48f40be1db920a582b30b116fe2435d184f77f0e4226f546794cedd9cf0", size = 487238, upload-time = "2026-02-02T12:37:10.066Z" }, - { url = "https://files.pythonhosted.org/packages/ed/b1/956cc7abaca8d95c13aa8d6c9b3f3797241c246cd6e792934cc4c8b250d2/jiter-0.13.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7772115877c53f62beeb8fd853cab692dbc04374ef623b30f997959a4c0e7e95", size = 372892, upload-time = "2026-02-02T12:37:11.656Z" }, - { url = "https://files.pythonhosted.org/packages/26/c4/97ecde8b1e74f67b8598c57c6fccf6df86ea7861ed29da84629cdbba76c4/jiter-0.13.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1211427574b17b633cfceba5040de8081e5abf114f7a7602f73d2e16f9fdaa59", size = 360309, upload-time = "2026-02-02T12:37:13.244Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d7/eabe3cf46715854ccc80be2cd78dd4c36aedeb30751dbf85a1d08c14373c/jiter-0.13.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7beae3a3d3b5212d3a55d2961db3c292e02e302feb43fce6a3f7a31b90ea6dfe", size = 389607, upload-time = "2026-02-02T12:37:14.881Z" }, - { url = "https://files.pythonhosted.org/packages/df/2d/03963fc0804e6109b82decfb9974eb92df3797fe7222428cae12f8ccaa0c/jiter-0.13.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:e5562a0f0e90a6223b704163ea28e831bd3a9faa3512a711f031611e6b06c939", size = 514986, upload-time = "2026-02-02T12:37:16.326Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/8c83b45eb3eb1c1e18d841fe30b4b5bc5619d781267ca9bc03e005d8fd0a/jiter-0.13.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:6c26a424569a59140fb51160a56df13f438a2b0967365e987889186d5fc2f6f9", size = 548756, upload-time = "2026-02-02T12:37:17.736Z" }, - { url = "https://files.pythonhosted.org/packages/47/66/eea81dfff765ed66c68fd2ed8c96245109e13c896c2a5015c7839c92367e/jiter-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:24dc96eca9f84da4131cdf87a95e6ce36765c3b156fc9ae33280873b1c32d5f6", size = 201196, upload-time = "2026-02-02T12:37:19.101Z" }, - { url = "https://files.pythonhosted.org/packages/ff/32/4ac9c7a76402f8f00d00842a7f6b83b284d0cf7c1e9d4227bc95aa6d17fa/jiter-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0a8d76c7524087272c8ae913f5d9d608bd839154b62c4322ef65723d2e5bb0b8", size = 204215, upload-time = "2026-02-02T12:37:20.495Z" }, - { url = "https://files.pythonhosted.org/packages/f9/8e/7def204fea9f9be8b3c21a6f2dd6c020cf56c7d5ff753e0e23ed7f9ea57e/jiter-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2c26cf47e2cad140fa23b6d58d435a7c0161f5c514284802f25e87fddfe11024", size = 187152, upload-time = "2026-02-02T12:37:22.124Z" }, - { url = "https://files.pythonhosted.org/packages/79/b3/3c29819a27178d0e461a8571fb63c6ae38be6dc36b78b3ec2876bbd6a910/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b1cbfa133241d0e6bdab48dcdc2604e8ba81512f6bbd68ec3e8e1357dd3c316c", size = 307016, upload-time = "2026-02-02T12:37:42.755Z" }, - { url = "https://files.pythonhosted.org/packages/eb/ae/60993e4b07b1ac5ebe46da7aa99fdbb802eb986c38d26e3883ac0125c4e0/jiter-0.13.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:db367d8be9fad6e8ebbac4a7578b7af562e506211036cba2c06c3b998603c3d2", size = 305024, upload-time = "2026-02-02T12:37:44.774Z" }, - { url = "https://files.pythonhosted.org/packages/77/fa/2227e590e9cf98803db2811f172b2d6460a21539ab73006f251c66f44b14/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45f6f8efb2f3b0603092401dc2df79fa89ccbc027aaba4174d2d4133ed661434", size = 339337, upload-time = "2026-02-02T12:37:46.668Z" }, - { url = "https://files.pythonhosted.org/packages/2d/92/015173281f7eb96c0ef580c997da8ef50870d4f7f4c9e03c845a1d62ae04/jiter-0.13.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:597245258e6ad085d064780abfb23a284d418d3e61c57362d9449c6c7317ee2d", size = 346395, upload-time = "2026-02-02T12:37:48.09Z" }, - { url = "https://files.pythonhosted.org/packages/80/60/e50fa45dd7e2eae049f0ce964663849e897300433921198aef94b6ffa23a/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:3d744a6061afba08dd7ae375dcde870cffb14429b7477e10f67e9e6d68772a0a", size = 305169, upload-time = "2026-02-02T12:37:50.376Z" }, - { url = "https://files.pythonhosted.org/packages/d2/73/a009f41c5eed71c49bec53036c4b33555afcdee70682a18c6f66e396c039/jiter-0.13.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:ff732bd0a0e778f43d5009840f20b935e79087b4dc65bd36f1cd0f9b04b8ff7f", size = 303808, upload-time = "2026-02-02T12:37:52.092Z" }, - { url = "https://files.pythonhosted.org/packages/c4/10/528b439290763bff3d939268085d03382471b442f212dca4ff5f12802d43/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab44b178f7981fcaea7e0a5df20e773c663d06ffda0198f1a524e91b2fde7e59", size = 337384, upload-time = "2026-02-02T12:37:53.582Z" }, - { url = "https://files.pythonhosted.org/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/1f/10936e16d8860c70698a1aa939a46aa0224813b782bce4e000e637da0b2d/jiter-0.16.0.tar.gz", hash = "sha256:7b24c3492c5f4f84a37946ad9cf504910cf6a782d6a4e0689b6673c5894b4a1c", size = 176431, upload-time = "2026-06-29T13:05:13.657Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/3f/fae6cc967d120ec89e31c5418a51176d8278b3087fbb384a9176754f353c/jiter-0.16.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:67fddeda1688f0cce2d2ae83ccf8a80f79936f2d2997d6cc2261f82fdb54a4d3", size = 309289, upload-time = "2026-06-29T13:02:52.301Z" }, + { url = "https://files.pythonhosted.org/packages/c8/e3/97c6c3562c077f6247d6e6ce5c82562500b6316c0d928e97e106b7a1321a/jiter-0.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c90c0f63df322be920eda6ce622e3083d8906ba267f8220fe7873213b8b4430e", size = 315181, upload-time = "2026-06-29T13:02:53.964Z" }, + { url = "https://files.pythonhosted.org/packages/7b/89/d8d073f8aa2667e46c6c0873f86fe4a512bba4293cc730f626a076211a62/jiter-0.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64c0203212098470032aabcde9356fc168f377aade3e43def61dfe17e92f2037", size = 340939, upload-time = "2026-06-29T13:02:55.412Z" }, + { url = "https://files.pythonhosted.org/packages/87/c9/db4fda3ed73fb864139305e935e5b8b38a5a24692a5a9dd356c22f1b9c8d/jiter-0.16.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12288303c9844e61e1651d02a9a6f6633e47d39f897d6991d1427161ce6b746e", size = 364932, upload-time = "2026-06-29T13:02:57.28Z" }, + { url = "https://files.pythonhosted.org/packages/a2/74/52b5e86241057f52ddd7c9a580f90effb51f9d06239f6fc612279b91a838/jiter-0.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cf109d010b4b05a105afb3d43be36a21322d345ad3111e13d15f680afef0e5b", size = 461132, upload-time = "2026-06-29T13:02:58.994Z" }, + { url = "https://files.pythonhosted.org/packages/a9/87/544a700f7447c1f31c5d7833821a4daa5683165c2d5a094fbf5b5800c3dc/jiter-0.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62c1b7fe1f77925acf5af68b6140b8810fa87dfd4dc0a9c8568ec2fa2a10429c", size = 374857, upload-time = "2026-06-29T13:03:00.455Z" }, + { url = "https://files.pythonhosted.org/packages/40/cd/0fcc3f7d39183674d5bfa9ec640faaeb506c60be7c8f94625dfba366e37c/jiter-0.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8597d23c87f59294f83bcb6229b9ed1fccee13dbba967b46930d2f1759466fee", size = 347053, upload-time = "2026-06-29T13:03:02.045Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ae/c7e64e7932ad597fa395b61440b249ada6366716e25c6e08dd2afbd021e6/jiter-0.16.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:3126a5dbad56401989ac769aca0cb56005bfb3e2366eea0ca99d1a91c3c1ee03", size = 356153, upload-time = "2026-06-29T13:03:03.706Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/1c719044f14da814e1a060191ab19b96f3e99207bc5b4bfc6d6be34b3f80/jiter-0.16.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c4b4717bdb35ae456f831a6b08d01880fff399887a6bbc526a583a406e484eea", size = 393956, upload-time = "2026-06-29T13:03:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/3b/dc/7b2f303a2847207e265503853a2d964a55354cffd62a5f2936c155486798/jiter-0.16.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:adff21bc78edfe086c15eb495b900306076de378dc2337c132401fc39bd79c91", size = 521081, upload-time = "2026-06-29T13:03:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/c2/5f/501cf6e1e09caeb420195179ffc6f62aca603f1220ec53fd80d0d70b3e56/jiter-0.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dab907db06fc593645e73109acf4581ba5b548897d28b9348dc41ddc8343b2d3", size = 552085, upload-time = "2026-06-29T13:03:08.339Z" }, + { url = "https://files.pythonhosted.org/packages/79/54/aa5be86520113b79455c3877f3d1f07a348098df4083ba3688e9537e52dd/jiter-0.16.0-cp311-cp311-win32.whl", hash = "sha256:560b2cf3fb03240cd34f27409a238547488708f05b7c3924f571a60422251ec7", size = 206755, upload-time = "2026-06-29T13:03:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/64/ec/2feb893eb330bd69b413866f4d5daada33c3962f1c6f270c91ca2d87fdf9/jiter-0.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:e431cfc9caf44c1d5459ff77d4e64cbf85fddb6a35dad836a15c6a9ec23087c1", size = 199155, upload-time = "2026-06-29T13:03:10.979Z" }, + { url = "https://files.pythonhosted.org/packages/b9/9c/ca040d94415048a3666fc237774df8151c96f8d2b661cbe3b184acc95876/jiter-0.16.0-cp311-cp311-win_arm64.whl", hash = "sha256:2a8e9e39cf083016137aa5cadafe3188adc2ba6ba1fbf1e5d18889ad3e9ad056", size = 194403, upload-time = "2026-06-29T13:03:12.341Z" }, + { url = "https://files.pythonhosted.org/packages/83/2b/52ace16ed031354f0539749a49e4bf33797d82bea5137910835fa4b09793/jiter-0.16.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:67c3bc1760f8c99d805dcab4e644027142a53b1d5d861f18780ebdbd5d40b72a", size = 306943, upload-time = "2026-06-29T13:03:14.035Z" }, + { url = "https://files.pythonhosted.org/packages/94/2e/34957c2c1b661c252ba9bcc60ae0bddc27e0f7202c6073326a13c5390eec/jiter-0.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5af7780e4a26bd7d0d989592bf9ef12ebf806b74ab709223ecca37c749872ea9", size = 307779, upload-time = "2026-06-29T13:03:15.418Z" }, + { url = "https://files.pythonhosted.org/packages/88/6c/59bd309cab4460c54cf1079f3eb7fe7af6a4c895c5c957a53378693bad2b/jiter-0.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5bf78d0e05e45cfdd66558893938d59afe3d1b1a824a202039b20e607d25a72", size = 335826, upload-time = "2026-06-29T13:03:17.11Z" }, + { url = "https://files.pythonhosted.org/packages/3b/8c/f5ef7b65f0df47afa16596969defb281ebb86e96df346d62be6fd853d620/jiter-0.16.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f4444a83f946605990c98f625cdd3d2725bfb818158760c5748c653170a20e0e", size = 362573, upload-time = "2026-06-29T13:03:18.781Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0b/ace4354da061ee38844a0c27dc2c21eecd27aea119e8da324bea987522d0/jiter-0.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3a23f0e4f957e1be65752d2dfac9a5a06b1917af8dc85deb639c3b9d02e31290", size = 457979, upload-time = "2026-06-29T13:03:20.293Z" }, + { url = "https://files.pythonhosted.org/packages/55/40/c0253d3772eb9dcd8e6606ee9b2d53ec8e5b814589c47f140aa585f21eaa/jiter-0.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c22a488f7b9218e245a0025a9ba6b100e2e54700831cf4cf16833a27fba3ad01", size = 372302, upload-time = "2026-06-29T13:03:21.739Z" }, + { url = "https://files.pythonhosted.org/packages/a8/d2/4839422241aa12860ce597b20068727094ba0bc480723c74924ca5bad483/jiter-0.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46add52f4ad47a08bfb1219f3e673da972191489a33016edefdb5ea55bfa8c48", size = 343805, upload-time = "2026-06-29T13:03:23.384Z" }, + { url = "https://files.pythonhosted.org/packages/e2/59/e196888a05befdda7dbe299b722d56f2f6eec65402bc34c0a3306d595feb/jiter-0.16.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:9c8a956fd72c2cf1e730d01ea080341f13aa0a97a4a33b51abebe725b7ae9ca9", size = 351107, upload-time = "2026-06-29T13:03:24.815Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/4cd9e0fca65232136400354b630fbfcd2de634e22ccbb96567725981b548/jiter-0.16.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:561926e0573ffe4a32498420a76d64b16c513e1ab413b9d28158a8764ac701e5", size = 388441, upload-time = "2026-06-29T13:03:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/d9/8c/554691e48bc711299c0a293dd8a6179e24b2d66a54dc295421fcf64569c0/jiter-0.16.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:44d019fa8cdaf89bf29c71b39e3712143fdd0ac76725c6ef954f9957a5ea8730", size = 516354, upload-time = "2026-06-29T13:03:28.02Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cb/01e9d69dc2cc6759d4f91e230b34489c4fdb2518992650633f9e20bece89/jiter-0.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0df91907609837f33341b8e6fe73b95991fdaa57caf1a0fbd343dffe826f386f", size = 547880, upload-time = "2026-06-29T13:03:29.534Z" }, + { url = "https://files.pythonhosted.org/packages/79/70/2953195f1c6ad00f49fa67e13df7e60acb3dd4f387101bc15abccddd905e/jiter-0.16.0-cp312-cp312-win32.whl", hash = "sha256:51d7b836acb0108d7c77df1742332cac2a1fa04a74d6dacec46e7091f0e91274", size = 203473, upload-time = "2026-06-29T13:03:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/05/2909a8b10699a4d560f8c502b6b2c5f3991b682b1922c1eedda242b225bd/jiter-0.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:1878349266f8ee36ecb1375cc5ba2f115f35fd9f0a1a4119e725e379126647f7", size = 196905, upload-time = "2026-06-29T13:03:32.472Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a9/6b82bb1c8d7790d602489b967b982a909e5d092875a6c2ade96444c8dfc5/jiter-0.16.0-cp312-cp312-win_arm64.whl", hash = "sha256:2ed5738ae4af18271a51a528b8811b0cbfa4a1858de9d83359e4169855d6a331", size = 190618, upload-time = "2026-06-29T13:03:34.672Z" }, + { url = "https://files.pythonhosted.org/packages/91/c0/555fc60473d30d66894ba825e63615e3be7524fac23858356afa7a38906c/jiter-0.16.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:41977aa5654023948c2dae2a81cbf9c43343954bef1cd59a154dd15a4d84c195", size = 306203, upload-time = "2026-06-29T13:03:36.243Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/c3eaf16f5d7c9bad66ea32f40a95bd169b29a91217fcc7f081375157e99c/jiter-0.16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d28bb3c26762358dadf3e5bf0bccd29ae987d65e6988d2e6f49829c76b003c09", size = 306489, upload-time = "2026-06-29T13:03:37.846Z" }, + { url = "https://files.pythonhosted.org/packages/96/3f/02fdfc6705cad96127d883af5c34e4867f554f29ec7705ec1a46156400a9/jiter-0.16.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0542a7189c26920778658fc8fcf2af8bae05bae9924577f71804acef37996536", size = 335453, upload-time = "2026-06-29T13:03:39.221Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a6/e4bda5920d4b0d7c5dfb7174ce4a6b2e4d3e11c9162c452ef0eab4cdbdbd/jiter-0.16.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8fb8de1e23a0cb2a7f53c335049c7b72b6db41aa6227cdcc0972a1de5cb39450", size = 361625, upload-time = "2026-06-29T13:03:40.597Z" }, + { url = "https://files.pythonhosted.org/packages/b7/97/4e6b59b2c6e55cbb3e183595f81ad65dcfb21c915fee5e19e335df21bc55/jiter-0.16.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72d0b2990ca754a9102779ac98d8597b7cb31678958562214a007f909eab78e", size = 456958, upload-time = "2026-06-29T13:03:42.074Z" }, + { url = "https://files.pythonhosted.org/packages/15/e0/97e9557686d2f94f4b93786eccb7eed28e9228ad132ea8237f44727314a7/jiter-0.16.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f91b1c27fc22a57993d5a5cb8a627cb8ed4b10502716fac1ffbfe1d19d84e8", size = 372017, upload-time = "2026-06-29T13:03:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0f/94/db768b6938e0df35c86beeba3dfbbb025c9ee5c19e1aa271f2396e50864d/jiter-0.16.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c682bea068a90b764577bdb78a60a4c1d1606daf9cd4c893832a37c7cc9d9026", size = 343320, upload-time = "2026-06-29T13:03:45.226Z" }, + { url = "https://files.pythonhosted.org/packages/c1/d6/5a59d938244a30735fe62d9433fd325f9021ea29d89780ea4596ea93bc89/jiter-0.16.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:8d031aabecc4f1b6276adfb42e3aabb77c89d468bf616600e8d3a11328929053", size = 350520, upload-time = "2026-06-29T13:03:46.671Z" }, + { url = "https://files.pythonhosted.org/packages/67/f8/c4a857f49c9af125f6bbcac7e3eee7f7978ed89682833062e2dbf62576b1/jiter-0.16.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:eab2cd170150e70153de16896a1774e3a1dca80154c56b54d7a812c479a7165e", size = 387550, upload-time = "2026-06-29T13:03:48.361Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d6/5fbc2f7d6b67b754caa61a993a2e626e815dec47ffc2f9e35f01adfebec7/jiter-0.16.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:6edb63a46e65a82c26800a868e49b2cac30dd5a4218b88d74bc2c848c8ad60bb", size = 515424, upload-time = "2026-06-29T13:03:49.881Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/284f0164b64a5fed915fea6ba7e9ba9b3d8d37c67d59cf2e3bb99d45cdfe/jiter-0.16.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:659039cc50b5addcc35fcc87ae2c1833b7c0a8e5326ef631a75e4478447bcf84", size = 546981, upload-time = "2026-06-29T13:03:51.363Z" }, + { url = "https://files.pythonhosted.org/packages/13/c5/2a467585a576594384e1d2c43e1224deaafc085f24e243529cf98beef8e1/jiter-0.16.0-cp313-cp313-win32.whl", hash = "sha256:c9c53be232c2e206ef9cdbad81a48bfa74c3d3f08bcf8124630a8a748aad993e", size = 202853, upload-time = "2026-06-29T13:03:53.015Z" }, + { url = "https://files.pythonhosted.org/packages/88/6a/de61d04b9eec69c71719968d2f716532a3bc121170c44a39e14979c6be81/jiter-0.16.0-cp313-cp313-win_amd64.whl", hash = "sha256:baad945ed47f163ad833314f8e3288c396118934f94e7bbb9e243ce4b341a4fd", size = 196160, upload-time = "2026-06-29T13:03:54.447Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/b390ed59bafb3f31d008d1218578f10327714484b334439947f7e5b11e7f/jiter-0.16.0-cp313-cp313-win_arm64.whl", hash = "sha256:3c1fd2dbe1b0af19e987f03fe66c5f5bd105a2229c1aff4ab14890b24f41d21a", size = 189862, upload-time = "2026-06-29T13:03:55.754Z" }, + { url = "https://files.pythonhosted.org/packages/a7/89/bc4f1b57d5da938fd344a466396541e586d161320d70bffd929aaafcd8f4/jiter-0.16.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b2c61484666ad42726029af0c00ef4541f0f3b5cdc550221f56c2343208018ee", size = 308239, upload-time = "2026-06-29T13:03:57.205Z" }, + { url = "https://files.pythonhosted.org/packages/65/7a/c415453e5213001bf3b411ff65dec3d303b0e76a4a2cfea9768cd4960994/jiter-0.16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:63efadc657488f45db1c676d81e704cac2abf3fdb892def1faea61db053127e2", size = 308928, upload-time = "2026-06-29T13:03:58.643Z" }, + { url = "https://files.pythonhosted.org/packages/11/fc/1f4fb7ebf9a724c7741994f4aae18fba1e2f3133df14521a79194952c34a/jiter-0.16.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf0d73f50e7b6935677854f6e8e31d499ca7064dd24734f703e060f5b237d883", size = 336998, upload-time = "2026-06-29T13:04:00.071Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8d/72cadaac05ccfa7cc3a0a2232862e6c72443ca40cf300ba8b57f9f18b69b/jiter-0.16.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf3ea07d9bc8e7d03a9fbc051295462e6dbc295b894fd72457c3136e3e43d898", size = 362112, upload-time = "2026-06-29T13:04:01.52Z" }, + { url = "https://files.pythonhosted.org/packages/58/4a/c4b0d5f651fda90a24ffce9f8d56cde462a2e09d31ae3de3c68cef34c04e/jiter-0.16.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:26798522707abb47d767db536e4148ceac1b14446bf028ee85e579a2e043cfe5", size = 459807, upload-time = "2026-06-29T13:04:03.214Z" }, + { url = "https://files.pythonhosted.org/packages/80/58/ef77879ea9aa56b50824edc5a445e226422c7a8d211f3fd2a56bcb9493cf/jiter-0.16.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bc837c1b9631be10abfe0191537fe8009838204cec7e44827401ace390ddb567", size = 373181, upload-time = "2026-06-29T13:04:04.629Z" }, + { url = "https://files.pythonhosted.org/packages/49/2e/ffbc3f254e4d8a66da3062c624a7df4b7c2b2cf9e1fe43cf394b3e104041/jiter-0.16.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49060fd70737fad59d33ba9dcc0d83247dc9e77187de26053a19c16c9f32bd69", size = 344927, upload-time = "2026-06-29T13:04:06.067Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/0be5dc6d64a89f80aa8fec984f94dedb2973e251edcae55841d60786d578/jiter-0.16.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:adbb8edeadd431bc4477879d5d371ece7cb1334486584e0f252656dd7ffada29", size = 352754, upload-time = "2026-06-29T13:04:07.477Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/7d31243b3b91cd261dd19e9d3557fc3251a80883d3d8049c86174e7ab7af/jiter-0.16.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:31aaee5b80f672c1dc21272bcfb9cbdcfc1ea04ff50f00ed5af500b80c44fa93", size = 390553, upload-time = "2026-06-29T13:04:08.92Z" }, + { url = "https://files.pythonhosted.org/packages/25/33/51ae371fde3c88897520f62b4d5f8b27ad7103e2bb10812ff52195609853/jiter-0.16.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6722bcef4ffc86c835574b1b2fac6b33b9fb4a889c781e67950e891591f3c55a", size = 516900, upload-time = "2026-06-29T13:04:10.407Z" }, + { url = "https://files.pythonhosted.org/packages/a0/45/6449b3d123ea439ba79507c657288f461d55049e7bcbdc2cf8eb8210f491/jiter-0.16.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:5ab4f50ff971b611d656554ea10b75f80097392c827bc32923c6eeb6386c8b00", size = 548754, upload-time = "2026-06-29T13:04:12.046Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e7/fd2fb11ae3e2649333da3aa170d04d7b3000bbdc3b270f6513382fdf4e04/jiter-0.16.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:710cc51d4ebdcd3c1f70b232c1db1ea1344a075770422bbd4bede5708335acbe", size = 122381, upload-time = "2026-06-29T13:04:13.413Z" }, + { url = "https://files.pythonhosted.org/packages/26/80/f0b147a62c315a164ed2168908286ca302310824c218d3aae52b06c0c9a9/jiter-0.16.0-cp314-cp314-win32.whl", hash = "sha256:57b37fc887a32d44798e4d8ebfa7c9683ff3da1d5bf38f08d1bb3573ccb39106", size = 204578, upload-time = "2026-06-29T13:04:14.813Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e6/4758a14304b4523a6f5adb2419340086aa3593bd4327c2b25b5948a90548/jiter-0.16.0-cp314-cp314-win_amd64.whl", hash = "sha256:cbd18dd5e2df96b580487b5745adf57ef64ad89ba2d9662fc3c19386acce7db8", size = 198154, upload-time = "2026-06-29T13:04:16.272Z" }, + { url = "https://files.pythonhosted.org/packages/26/be/41fa54a2e7ea41d6c99f1dc5b1f0fd4cb474680304b5d268dd518e81da3a/jiter-0.16.0-cp314-cp314-win_arm64.whl", hash = "sha256:a32d2027a9fa67f109ff245a3252ece3ccc32cc56703e1deab6cc846a59e0585", size = 191458, upload-time = "2026-06-29T13:04:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/81/6b/59127338b86d9fe4d99418f5a15118bea778103ee0fe9d9dd7e0af174e95/jiter-0.16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2577196f4474ef3fc4779a088a23b0897bbf86f9ea3679c372d45b8383b43207", size = 316739, upload-time = "2026-06-29T13:04:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/2d/95/49461034d5388196d3dabf98748935f017b7785d8f3f5349f834bcc4ed0d/jiter-0.16.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:616e89e008a93c01104161c75b4988e58716b01d62307ebfe161e52a56d2a818", size = 340911, upload-time = "2026-06-29T13:04:21.257Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/a4369f2fb82cb3dda13b98622f31249b2e014b223fe64ee534413ad72294/jiter-0.16.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0e2e9efbe042210df657bade597f66d6d75723e3d8f45a12ea6d8167ff8bbce3", size = 361747, upload-time = "2026-06-29T13:04:22.677Z" }, + { url = "https://files.pythonhosted.org/packages/28/51/49b6ed456261646e1906016a6760367a28aacd3c24805e4e5fe64116c1db/jiter-0.16.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f4d9e473a5ce7d27fef8b848df4dc16e283893d3f53b4a585e72c9595f3c284", size = 460225, upload-time = "2026-06-29T13:04:24.441Z" }, + { url = "https://files.pythonhosted.org/packages/33/b5/5689aff4f66c5b60be63106e591dbfcba2190df97d2c9c7cf052361ddb98/jiter-0.16.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d30a4a1c87713060c8d1cc59a7b6c8fb6b8ef0a6900368014c76c87922a2929", size = 373169, upload-time = "2026-06-29T13:04:25.884Z" }, + { url = "https://files.pythonhosted.org/packages/a2/96/3ae1b85ee0d6d6cab254fb7f8da018272b932bbf2d69b07e98aa2a96c746/jiter-0.16.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bae96332410f866e5900d809298b1ed82735932986c672495f9701daacd80620", size = 350332, upload-time = "2026-06-29T13:04:27.302Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/c99d7bafd78986556c95bf60ce84c6cc98786eac56066c12d7f828bb6747/jiter-0.16.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:da3d7ec75dc83bb18bca888b5edfae0656a26849056c59e05a7728badd17e7af", size = 353377, upload-time = "2026-06-29T13:04:28.731Z" }, + { url = "https://files.pythonhosted.org/packages/0e/4b/f99a8e571287c3dec766bcc18528bbe8e8fb5365522ab5e6d64c93e87066/jiter-0.16.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ee6162b77d49a9939229df666dfa8af3e656b6701b54c4c84966d740e189264e", size = 387746, upload-time = "2026-06-29T13:04:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/75/69/c78a5b3f71040e34eb5917df26fb7ae9a2174cad1ccbf277512507c53a6e/jiter-0.16.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:63ffdbdae7d4499f4cda14eadc12ddcabef0fc0c081191bdc2247489cb698077", size = 517292, upload-time = "2026-06-29T13:04:31.709Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f7/095b38eda4c70d03651c403f29a5590f16d12ddc5d544aac9f9cddf72277/jiter-0.16.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a111256a7193bea0759267b10385e5870949c239ed7b6ddbaaf57573edb38734", size = 549259, upload-time = "2026-06-29T13:04:33.721Z" }, + { url = "https://files.pythonhosted.org/packages/2e/c5/6a0207d90e5f656d95af98ebd0934f382d37674416f215aeda2ff8063e51/jiter-0.16.0-cp314-cp314t-win32.whl", hash = "sha256:de5ba8763e56b793561f43bed197c9ea55776daa5e9a6b91eed68a909bc9cdbf", size = 206523, upload-time = "2026-06-29T13:04:35.068Z" }, + { url = "https://files.pythonhosted.org/packages/a5/31/c757d5f30a8980fd945ce7b98be10be9e4ff59c7c42f5fd86804c2e87db8/jiter-0.16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b8a3f9a6008048fe9def7bf465180564a6e458047d2ce499149cfbe73c3ae9db", size = 200366, upload-time = "2026-06-29T13:04:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/7c/a2/d88de6d313d734a544a7901353ad5db67cb38dcfcd91713b7979dafc345d/jiter-0.16.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0fa25b09b13075c46f5bc174f2690525a925a4fc2f7c82969a2bbabff22386ce", size = 190516, upload-time = "2026-06-29T13:04:38.004Z" }, + { url = "https://files.pythonhosted.org/packages/06/d3/8e278946d43eeca2585b4dd0834a887cd71136329b837f3a16ed86a8b4b0/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:850ccb1d7eedb4200f4014b1c0e8a577de114fc3cd88faad646dcc9bc4bb12ad", size = 304518, upload-time = "2026-06-29T13:05:00.172Z" }, + { url = "https://files.pythonhosted.org/packages/72/43/28d4ef495028bf0506a413d4db3f4eb3e7288a382e0f065f306a17bbeb5e/jiter-0.16.0-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:e34e97bda77eb63242a410243c071e28ac7e0d8c0948c5ee658498690a4b2f2f", size = 310207, upload-time = "2026-06-29T13:05:02.123Z" }, + { url = "https://files.pythonhosted.org/packages/e0/ca/c366b1012da1d640de975d9683acd44e4d150d9068845d0ca2610435253f/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7dc85ea77d4abbae8bad0d3538678aedee75bceec4e2f6c8dfb1c74772e5aa5", size = 342771, upload-time = "2026-06-29T13:05:03.55Z" }, + { url = "https://files.pythonhosted.org/packages/16/52/50cc4056fc1ae02e7154704e7ecc89df0afb8300222cfe8a52d3f67e4730/jiter-0.16.0-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17ca7fae79f6d99cd9a042b75f917eaada7b895cfc7dd2ee3a16089dcaec7a85", size = 346468, upload-time = "2026-06-29T13:05:05.452Z" }, + { url = "https://files.pythonhosted.org/packages/98/ab/664fd8c4be028b2bedd3d2ff08769c4ede23d0dbc87a77c62384a0515b5d/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:f17d61a28b4b3e0e3e2ba98490c70501403b4d196f78732439160e7fd3678127", size = 303106, upload-time = "2026-06-29T13:05:07.118Z" }, + { url = "https://files.pythonhosted.org/packages/1a/07/421f1d5b65493a76e16027b848aba6a7d28073ae75944fa4289cc914d39f/jiter-0.16.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:96e38eea538c8ddf853a35727c7be0741c76c13f04148ac5c116222f50ece3b3", size = 304658, upload-time = "2026-06-29T13:05:08.708Z" }, + { url = "https://files.pythonhosted.org/packages/0a/db/bba1155f01a01c3c37a89425d571da751bbedf5c54247b831a04cb971798/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d284fb8d94d5855d60c44fefcab4bf966f1da6fada73992b01f6f0c9bc0c6702", size = 339719, upload-time = "2026-06-29T13:05:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/18a1afcd64f35314b68c1f23afcd9994d0bc13e65cc77517afff4e83986d/jiter-0.16.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64d613743df53199b1aa256a7d328340da6d7078aac7705a7db9d7a791e9cfd2", size = 343885, upload-time = "2026-06-29T13:05:12.087Z" }, ] [[package]] @@ -857,30 +1070,30 @@ wheels = [ [[package]] name = "jsonpointer" -version = "3.0.0" +version = "3.1.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/c7/af399a2e7a67fd18d63c40c5e62d3af4e67b836a2107468b6a5ea24c4304/jsonpointer-3.1.1.tar.gz", hash = "sha256:0b801c7db33a904024f6004d526dcc53bbb8a4a0f4e32bfd10beadf60adf1900", size = 9068, upload-time = "2026-03-23T22:32:32.458Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6a/a83720e953b1682d2d109d3c2dbb0bc9bf28cc1cbc205be4ef4be5da709d/jsonpointer-3.1.1-py3-none-any.whl", hash = "sha256:8ff8b95779d071ba472cf5bc913028df06031797532f08a7d5b602d8b2a488ca", size = 7659, upload-time = "2026-03-23T22:32:31.568Z" }, ] [[package]] name = "langchain" -version = "1.2.13" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/e5/56fdeedaa0ef1be3c53721d382d9e21c63930179567361610ea6102c04ea/langchain-1.2.13.tar.gz", hash = "sha256:d566ef67c8287e7f2e2df3c99bf3953a6beefd2a75a97fe56ecce905e21f3ef4", size = 573819, upload-time = "2026-03-19T17:16:07.641Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/5d/0ef368fdcf5df08a394787196fb7a80f473a9c7b3fc4cd2e3b53a7a5853d/langchain-1.4.0.tar.gz", hash = "sha256:08da122a439f738f4c09a5158cfa3d2c1d8bea2d0bde7fbbf4aeb4d7f7fd9d80", size = 729354, upload-time = "2026-09-03T16:59:32.442Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9c/1d/a509af07535d8f4621d77f3ba5ec846ee6d52c59d2239e1385ec3b29bf92/langchain-1.2.13-py3-none-any.whl", hash = "sha256:37d4526ac4b0cdd3d7706a6366124c30dc0771bf5340865b37cdc99d5e5ad9b1", size = 112488, upload-time = "2026-03-19T17:16:06.134Z" }, + { url = "https://files.pythonhosted.org/packages/92/fa/7da07d9977a5e292ef602c1bb911514ff8206e85dc09a66961874345d57a/langchain-1.4.0-py3-none-any.whl", hash = "sha256:af1dc0161d30944a52ec7844d9bf890d0497b12ec0703069f3503b4003cbbac3", size = 161534, upload-time = "2026-09-03T16:59:31.154Z" }, ] [[package]] name = "langchain-classic" -version = "1.0.3" +version = "1.0.8" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -891,40 +1104,42 @@ dependencies = [ { name = "requests" }, { name = "sqlalchemy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/32/04/b01c09e37414bab9f209efa311502841a3c0de5bc6c35e729c8d8a9893c9/langchain_classic-1.0.3.tar.gz", hash = "sha256:168ef1dfbfb18cae5a9ff0accecc9413a5b5aa3464b53fa841561a3384b6324a", size = 10534933, upload-time = "2026-03-13T13:56:11.96Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8d/65/6b5e8a7ff2f2968652c88a67dcecb925b9d8f0a0ce9458c76cd5a0dbd138/langchain_classic-1.0.8.tar.gz", hash = "sha256:ada0cc341a8a5b80fb24d73bdfaaeb849056ee2d8a41cc468355163fd3667484", size = 10557071, upload-time = "2026-06-10T21:27:54.866Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ab/e6/cfdeedec0537ffbf5041773590d25beb7f2aa467cc6630e788c9c7c72c3e/langchain_classic-1.0.3-py3-none-any.whl", hash = "sha256:26df1ec9806b1fbff19d9085a747ea7d8d82d7e3fb1d25132859979de627ef79", size = 1041335, upload-time = "2026-03-13T13:56:09.677Z" }, + { url = "https://files.pythonhosted.org/packages/99/9a/b8f5cb7490fdbf233088031fc69c9c747439d4097f67f196c1eb4869916d/langchain_classic-1.0.8-py3-none-any.whl", hash = "sha256:1a11ea7fbe630c4f2af2f3873d27718ceac9488cf32d0821030be7cf039a6213", size = 1041536, upload-time = "2026-06-10T21:27:52.767Z" }, ] [[package]] name = "langchain-community" -version = "0.4.1" +version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohttp" }, - { name = "dataclasses-json" }, { name = "httpx-sse" }, { name = "langchain-classic" }, { name = "langchain-core" }, { name = "langsmith" }, - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "pydantic-settings" }, { name = "pyyaml" }, { name = "requests" }, { name = "sqlalchemy" }, { name = "tenacity" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/97/a03585d42b9bdb6fbd935282d6e3348b10322a24e6ce12d0c99eb461d9af/langchain_community-0.4.1.tar.gz", hash = "sha256:f3b211832728ee89f169ddce8579b80a085222ddb4f4ed445a46e977d17b1e85", size = 33241144, upload-time = "2025-10-27T15:20:32.504Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/0c/e3aca1f2b1c5b95f8b87cb2b6e81a6f20d538c07a128419dc01cef0617b6/langchain_community-0.4.2.tar.gz", hash = "sha256:a99308160d53d7e9b5965ee665e5173709914338210089fd5788ad724432c21e", size = 33268708, upload-time = "2026-05-22T19:42:59.374Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f0/a4/c4fde67f193401512337456cabc2148f2c43316e445f5decd9f8806e2992/langchain_community-0.4.1-py3-none-any.whl", hash = "sha256:2135abb2c7748a35c84613108f7ebf30f8505b18c3c18305ffaecfc7651f6c6a", size = 2533285, upload-time = "2025-10-27T15:20:30.767Z" }, + { url = "https://files.pythonhosted.org/packages/8f/39/5d97e42a3e95dc2a6d71b2f902a3fae71786131e11d01bddb604accb0ebe/langchain_community-0.4.2-py3-none-any.whl", hash = "sha256:84dd8c5122532394d5b6849a5fc9995ef28e4f77227daeb09f24b3d942e9e466", size = 2364406, upload-time = "2026-05-22T19:42:57.103Z" }, ] [[package]] name = "langchain-core" -version = "1.2.20" +version = "1.6.2" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "httpx" }, { name = "jsonpatch" }, + { name = "langchain-protocol" }, { name = "langsmith" }, { name = "packaging" }, { name = "pydantic" }, @@ -933,26 +1148,38 @@ dependencies = [ { name = "typing-extensions" }, { name = "uuid-utils" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/db/41/6552a419fe549a79601e5a698d1d5ee2ca7fe93bb87fd624a16a8c1bdee3/langchain_core-1.2.20.tar.gz", hash = "sha256:c7ac8b976039b5832abb989fef058b88c270594ba331efc79e835df046e7dc44", size = 838330, upload-time = "2026-03-18T17:34:45.522Z" } +sdist = { url = "https://files.pythonhosted.org/packages/89/7b/406b4a8dd43dd01f69e04aafdee206809a2097134b2f944e12802eae7948/langchain_core-1.6.2.tar.gz", hash = "sha256:1ec6d3a98f7c8cdbb5bb0deff86e7ca37e16bf4f8f49f022d10723656c62352d", size = 1004390, upload-time = "2026-09-04T21:29:22.769Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d9/06/08c88ddd4d6766de4e6c43111ae8f3025df383d2a4379cb938fc571b49d4/langchain_core-1.2.20-py3-none-any.whl", hash = "sha256:b65ff678f3c3dc1f1b4d03a3af5ee3b8d51f9be5181d74eb53c6c11cd9dd5e68", size = 504215, upload-time = "2026-03-18T17:34:44.087Z" }, + { url = "https://files.pythonhosted.org/packages/a8/62/d3fb7c215cb2f237c3fe84880bf347a38deafef6033b6d5f1339ba8ca401/langchain_core-1.6.2-py3-none-any.whl", hash = "sha256:21e6c7cf097c68b777fd69ea00864f09bd9ca76ac73e3e3fa14e1ee366eb4711", size = 571690, upload-time = "2026-09-04T21:29:21.175Z" }, +] + +[[package]] +name = "langchain-protocol" +version = "0.0.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/14/56/913599f2f9cec8524868929f12d72b2ede377a6056ca8a40a32bdadfa535/langchain_protocol-0.0.19.tar.gz", hash = "sha256:79d90a1425122ac87e8052e2ec054fbd09c3edbf341bdfb6397112a495c7bf8c", size = 6265, upload-time = "2026-08-26T21:12:00.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/c9/f6cbf357d48ccbd18bb394433b1fd7ad9be004eed9377ad08bb85777e5e6/langchain_protocol-0.0.19-py3-none-any.whl", hash = "sha256:4cdf879a492a35980fd859ae792d3c65458ccaae504e183c9a10d7eac1f0720f", size = 7327, upload-time = "2026-08-26T21:11:59.781Z" }, ] [[package]] name = "langchain-text-splitters" -version = "1.1.1" +version = "1.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/85/38/14121ead61e0e75f79c3a35e5148ac7c2fe754a55f76eab3eed573269524/langchain_text_splitters-1.1.1.tar.gz", hash = "sha256:34861abe7c07d9e49d4dc852d0129e26b32738b60a74486853ec9b6d6a8e01d2", size = 279352, upload-time = "2026-02-18T23:02:42.798Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/9f/6c545900fefb7b00ddfa3f16b80d61338a0ec68c31c5451eeeab99082760/langchain_text_splitters-1.1.2.tar.gz", hash = "sha256:782a723db0a4746ac91e251c7c1d57fd23636e4f38ed733074e28d7a86f41627", size = 293580, upload-time = "2026-04-16T14:20:39.162Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/66/d9e0c3b83b0ad75ee746c51ba347cacecb8d656b96e1d513f3e334d1ccab/langchain_text_splitters-1.1.1-py3-none-any.whl", hash = "sha256:5ed0d7bf314ba925041e7d7d17cd8b10f688300d5415fb26c29442f061e329dc", size = 35734, upload-time = "2026-02-18T23:02:41.913Z" }, + { url = "https://files.pythonhosted.org/packages/d3/26/1ef06f56198d631296d646a6223de35bcc6cf9795ceb2442816bc963b84c/langchain_text_splitters-1.1.2-py3-none-any.whl", hash = "sha256:a2de0d799ff31886429fd6e2e0032df275b60ec817c19059a7b46181cc1c2f10", size = 35903, upload-time = "2026-04-16T14:20:38.243Z" }, ] [[package]] name = "langgraph" -version = "1.1.3" +version = "1.2.11" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, @@ -962,68 +1189,192 @@ dependencies = [ { name = "pydantic" }, { name = "xxhash" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d2/b2/e7db624e8b0ee063ecfbf7acc09467c0836a05914a78e819dfb3744a0fac/langgraph-1.1.3.tar.gz", hash = "sha256:ee496c297a9c93b38d8560be15cbb918110f49077d83abd14976cb13ac3b3370", size = 545120, upload-time = "2026-03-18T23:42:58.24Z" } +sdist = { url = "https://files.pythonhosted.org/packages/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fb/f7/221cc479e95e03e260496616e5ce6fb50c1ea01472e3a5bc481a9b8a2f83/langgraph-1.1.3-py3-none-any.whl", hash = "sha256:57cd6964ebab41cbd211f222293a2352404e55f8b2312cecde05e8753739b546", size = 168149, upload-time = "2026-03-18T23:42:56.967Z" }, + { url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" }, ] [[package]] name = "langgraph-checkpoint" -version = "4.0.1" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "ormsgpack" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b1/44/a8df45d1e8b4637e29789fa8bae1db022c953cc7ac80093cfc52e923547e/langgraph_checkpoint-4.0.1.tar.gz", hash = "sha256:b433123735df11ade28829e40ce25b9be614930cd50245ff2af60629234befd9", size = 158135, upload-time = "2026-02-27T21:06:16.092Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/e1/089c4c9e0a2fec7f883f82ae8e6a727138d50074cfeb6644bc2d13b1019b/langgraph_checkpoint-4.2.0.tar.gz", hash = "sha256:51a593b6bee684b0818e5d6e58e28ab340c6db7794575056ce7bd1b746a84ed7", size = 180239, upload-time = "2026-08-07T20:05:03.756Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/65/4c/09a4a0c42f5d2fc38d6c4d67884788eff7fd2cfdf367fdf7033de908b4c0/langgraph_checkpoint-4.0.1-py3-none-any.whl", hash = "sha256:e3adcd7a0e0166f3b48b8cf508ce0ea366e7420b5a73aa81289888727769b034", size = 50453, upload-time = "2026-02-27T21:06:14.293Z" }, + { url = "https://files.pythonhosted.org/packages/05/71/3b475f09bd57d3a5649792c66353312b4432afd843f301739dfcebd157f0/langgraph_checkpoint-4.2.0-py3-none-any.whl", hash = "sha256:0547fd228935a0b758865de3a3d6d7a2537c308895d0f9ab092ce9151b5da942", size = 56833, upload-time = "2026-08-07T20:05:02.655Z" }, ] [[package]] name = "langgraph-prebuilt" -version = "1.0.8" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "langchain-core" }, { name = "langgraph-checkpoint" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/0d/06/dd61a5c2dce009d1b03b1d56f2a85b3127659fdddf5b3be5d8f1d60820fb/langgraph_prebuilt-1.0.8.tar.gz", hash = "sha256:0cd3cf5473ced8a6cd687cc5294e08d3de57529d8dd14fdc6ae4899549efcf69", size = 164442, upload-time = "2026-02-19T18:14:39.083Z" } +sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/41/ec966424ad3f2ed3996d24079d3342c8cd6c0bd0653c12b2a917a685ec6c/langgraph_prebuilt-1.0.8-py3-none-any.whl", hash = "sha256:d16a731e591ba4470f3e313a319c7eee7dbc40895bcf15c821f985a3522a7ce0", size = 35648, upload-time = "2026-02-19T18:14:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/e9/43/3fe1a700b8490ed02679cdbbc8c915eb23a092faf496c9c1118abcd10be3/langgraph_prebuilt-1.1.0-py3-none-any.whl", hash = "sha256:51e311747d755b751d5c6b39b0c1446124d3a7643d2515017e6714b323508fc9", size = 41043, upload-time = "2026-05-12T03:37:48.007Z" }, ] [[package]] name = "langgraph-sdk" -version = "0.3.12" +version = "0.4.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, + { name = "langchain-core" }, + { name = "langchain-protocol" }, { name = "orjson" }, + { name = "websockets" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/fd/a1/012f0e0f5c9fd26f92bdc9d244756ad673c428230156ef668e6ec7c18cee/langgraph_sdk-0.3.12.tar.gz", hash = "sha256:c9c9ec22b3c0fcd352e2b8f32a815164f69446b8648ca22606329f4ff4c59a71", size = 194932, upload-time = "2026-03-18T22:15:54.592Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/ae/91446c1fffa04a2dc1f81afbfb5bfff3590452891a72bd9098a656ab2657/langgraph_sdk-0.4.4.tar.gz", hash = "sha256:4e651ffa09de695681579396375377bdde23bedbc8e35b070e615cbd5af7da8b", size = 345789, upload-time = "2026-08-27T21:25:22.593Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/4d/4f796e86b03878ab20d9b30aaed1ad459eda71a5c5b67f7cfe712f3548f2/langgraph_sdk-0.3.12-py3-none-any.whl", hash = "sha256:44323804965d6ec2a07127b3cf08a0428ea6deaeb172c2d478d5cd25540e3327", size = 95834, upload-time = "2026-03-18T22:15:53.545Z" }, + { url = "https://files.pythonhosted.org/packages/43/d5/2ad7f3a835c6fcebf58b6669552536ecd52d2dfe4cf49c6c36648fd83572/langgraph_sdk-0.4.4-py3-none-any.whl", hash = "sha256:39afe416c91742925e6f8a93715f566d499b36e1b636b804a4ffe3190e4f4e64", size = 162270, upload-time = "2026-08-27T21:25:21.072Z" }, ] [[package]] name = "langsmith" -version = "0.7.22" +version = "0.12.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "httpx" }, + { name = "anyio" }, + { name = "distro" }, + { name = "httpx2" }, { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, { name = "packaging" }, { name = "pydantic" }, { name = "requests" }, { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, { name = "uuid-utils" }, + { name = "websockets" }, { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/be/2a/2d5e6c67396fd228670af278c4da7bd6db2b8d11deaf6f108490b6d3f561/langsmith-0.7.22.tar.gz", hash = "sha256:35bfe795d648b069958280760564632fd28ebc9921c04f3e209c0db6a6c7dc04", size = 1134923, upload-time = "2026-03-19T22:45:23.492Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/94/1f5d72655ab6534129540843776c40eff757387b88e798d8b3bf7e313fd4/langsmith-0.7.22-py3-none-any.whl", hash = "sha256:6e9d5148314d74e86748cb9d3898632cad0320c9323d95f70f969e5bc078eee4", size = 359927, upload-time = "2026-03-19T22:45:21.603Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/f4/b7/74d0992a461eacad8106e5dcfd7677d7640b5ff4e12ea545856467004f4f/langsmith-0.12.2.tar.gz", hash = "sha256:ff369ba4390e0969dbb109e838281404ae637e76ef73b347336bae05b4f87331", size = 4860106, upload-time = "2026-09-05T20:26:19.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/e1/5c751a0f1a14de49f19a2ebc0f5dfadfaf04a07f245c5f749ec31f263715/langsmith-0.12.2-py3-none-any.whl", hash = "sha256:ca2b6386cf452cf8edebf27c9c476503c18bad895e7ad566578393fa0ba7fea2", size = 760692, upload-time = "2026-09-05T20:26:18.166Z" }, +] + +[[package]] +name = "librt" +version = "0.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/9b/356320fbae2ac8467e21c5e73e1389c80468e4998c62cc7d3536cc51b614/librt-0.15.0.tar.gz", hash = "sha256:4e66cbe84437497d951b799d3e1551291b6fb3d643820a7014b3655d57a59162", size = 214338, upload-time = "2026-08-07T10:49:42.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/52/06790ced2ac7117f890c21bda43c39c958ec82aa665c0718e821d33ff939/librt-0.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:823b92cf3c18ecd08afc70c42473888b41b6e8ef5046f3b82c05c154a2fa3d22", size = 148039, upload-time = "2026-08-07T10:46:41.165Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1d/8e150b7fc449a1f33c8a760965cc1f43b14fc1577d9d0b50ab2701420e74/librt-0.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70bc1b602cf59917e8f0c7a2cbc8bcc6fbc14d5486136b00707a79619121d63", size = 153067, upload-time = "2026-08-07T10:46:42.418Z" }, + { url = "https://files.pythonhosted.org/packages/51/87/a162bc5a66a35599dc619ecb215145f4de7d68e886b479b6d12593139f7c/librt-0.15.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:814ff83a25b5fce8b9c80c4dd803153fb5c5599fc74db9e022466938368957ef", size = 493087, upload-time = "2026-08-07T10:46:43.657Z" }, + { url = "https://files.pythonhosted.org/packages/e5/3a/aeea1fc620cf48060d3065b37614edbf97043c099d0f50782bc8ca61d897/librt-0.15.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:57f5eeb6ad4c180de583b1038e61fe5fbd9796bb69a8a1c1a0c7ddbec4c8c60f", size = 485608, upload-time = "2026-08-07T10:46:45.038Z" }, + { url = "https://files.pythonhosted.org/packages/52/ff/fe571ad416f0856fd0d5578ffc2e6dc531891e586e36b647bcf50569cab8/librt-0.15.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:82909c8f7eb9952656b65d3147afde4cf8e6d5a991eebc86418b5e65843b0ab8", size = 498723, upload-time = "2026-08-07T10:46:46.35Z" }, + { url = "https://files.pythonhosted.org/packages/0f/e1/7a65eb5dedb1f00aebd948cdd8e17add48bf066cab3514e9daf84ab45a6c/librt-0.15.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f779070399f991400fc451719e0ea388eb7de313388bada2c127a35de05f798a", size = 516002, upload-time = "2026-08-07T10:46:47.599Z" }, + { url = "https://files.pythonhosted.org/packages/5f/45/59832b0ebfbd08c2742e6ece372ceb53f18bf1faef5d33c8daf3abebf749/librt-0.15.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bac89069bc496ebdf4f79ebb57bbd10d0b214c8454225deb672d91002bd17e18", size = 508607, upload-time = "2026-08-07T10:46:48.873Z" }, + { url = "https://files.pythonhosted.org/packages/ea/0d/37fa73f3b43ebd8259f91ae9102a15e5a54e65d581e48dea72df3e81d7a4/librt-0.15.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e0d00c708fb2f5822b152429b1ac80a58dbbbc3f6c232c4d13a3f7fcf2ea5b4c", size = 530422, upload-time = "2026-08-07T10:46:50.45Z" }, + { url = "https://files.pythonhosted.org/packages/26/02/e046c6fe7a5881ac34623242192f484426ba8a75595fd18f22c53a3f530f/librt-0.15.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6c6624fe268625869485553dd7cc1daf30d22558215bb2a4ff16f67a9801a31a", size = 534303, upload-time = "2026-08-07T10:46:51.693Z" }, + { url = "https://files.pythonhosted.org/packages/95/32/d5e6d861ab0366f3edf74f887ab0c9eb9f535aaf01d32b80b4f734daa179/librt-0.15.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f56b397858a23dacf35ede366ed2212fdc03a6a57a1ad36468ad6e9dc5fac091", size = 536084, upload-time = "2026-08-07T10:46:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/2a/de/d69d725513fe53fc90c6d7a1f86e4428939bad2fb905b17fe4c18d413dde/librt-0.15.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:4388184646efe2054911c5b00a1077d6d1ee86a95b7e8ba96dc7850a809f3f40", size = 514307, upload-time = "2026-08-07T10:46:54.194Z" }, + { url = "https://files.pythonhosted.org/packages/36/93/f8aded0d6682b4f25820fa86e0690f87f01df9fd7bd09ddb04d9167ad021/librt-0.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:97335f59082f9fe2ce6c2a9cc6433a0114bbb6cd4d5c09dd76c95c68b9f9a8b0", size = 557686, upload-time = "2026-08-07T10:46:55.443Z" }, + { url = "https://files.pythonhosted.org/packages/74/09/ffeb6bdeb6cd862b4272fddc8ad05f938dd25d020ed517e631813917d80a/librt-0.15.0-cp311-cp311-win32.whl", hash = "sha256:83380ffde38062a2e9bb55d83e74474f6614665528b98a6928720fc006dfffbb", size = 104917, upload-time = "2026-08-07T10:46:56.605Z" }, + { url = "https://files.pythonhosted.org/packages/96/28/7e2313a3ffbf0b4de7ba3da58a09e488507b4bd1ea2b5e69378354a23415/librt-0.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:f75720477ee05d509a310e856cacc8d909adc182f7b91193c207bcc26d7ee6db", size = 125886, upload-time = "2026-08-07T10:46:57.729Z" }, + { url = "https://files.pythonhosted.org/packages/39/9e/04b8c3cde014ef255ee785730425268354543acc38902093a40afa0dc164/librt-0.15.0-cp311-cp311-win_arm64.whl", hash = "sha256:256237037a3ab001ae8d9803b2d43562a4c3aa38739843694349e4d5ebb0fd56", size = 111885, upload-time = "2026-08-07T10:46:58.787Z" }, + { url = "https://files.pythonhosted.org/packages/ba/39/99c25030e782bdfb7a21be8c05254806a2e4bbb05c8d50c2a2130acbfa05/librt-0.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e87bc679f86a99aa3b26e3c78eeb821a247c9a28eae48eaafcc32c3bf4c3bb9e", size = 151021, upload-time = "2026-08-07T10:47:00.057Z" }, + { url = "https://files.pythonhosted.org/packages/14/43/f4b1bd1b2888798a1409808889a25ea1ba49eaabce7d681ed27734c2df9d/librt-0.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:71599e011ac880e8e45d46047d714871894c7d4ab6f25626f8d4f89da21f368d", size = 155267, upload-time = "2026-08-07T10:47:01.311Z" }, + { url = "https://files.pythonhosted.org/packages/0c/db/3ad9c965c72f1e1d6beeec44ec10a54e17be8ae042fbb4baade16cbadced/librt-0.15.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c802434092b769b1d613ed2e13fac15fbfce1934a74bd10283b03c0fae231cd1", size = 503136, upload-time = "2026-08-07T10:47:02.45Z" }, + { url = "https://files.pythonhosted.org/packages/4b/07/5888a6d76acd62ebce66c61b74d94e9370b9c32929f111e487bb6546f8ed/librt-0.15.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5500eeae393a184d14e1f35645962c27129d20c81afa4069e6ef826ebc2b3aaa", size = 496670, upload-time = "2026-08-07T10:47:03.675Z" }, + { url = "https://files.pythonhosted.org/packages/29/39/ab57cc2f5b276156da02bb7f5a8921bada1cb1993ffec99acf811c602c23/librt-0.15.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6ecfc32dfb46fb7b565bcd6abf9412acf978775a998273d22888a6d7953730dd", size = 513688, upload-time = "2026-08-07T10:47:04.981Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b9/bdbb0b648b5c2befb031f4c6f3b1dd857415e8fb492a25a3c764a6681e6c/librt-0.15.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cc46cfd15022e35084355478c9ac809d90b1152222706ac9a7655ec21df6fa", size = 531904, upload-time = "2026-08-07T10:47:06.211Z" }, + { url = "https://files.pythonhosted.org/packages/93/26/473c2e4b6c104e9e58e27ce95fc8005c8bd4fc36cae4f254371125a92db8/librt-0.15.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5f51401d102c885b9ca509e62c79b1dbff286e1b9b047fde6f763780789356d", size = 524427, upload-time = "2026-08-07T10:47:07.592Z" }, + { url = "https://files.pythonhosted.org/packages/26/60/03b3abb82b41714671b907bf6989b228e31e6a8af52dec82b5b0728dc250/librt-0.15.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cc30523e3f1a23fb7511cc659834a0d01a1042bb9de359bc1c131cc4ec6c9656", size = 543155, upload-time = "2026-08-07T10:47:08.866Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0e/9bb1f0a4affbd0a1888f4f79dc03ed2a299d9a2c26c59ab2a97dcbf11903/librt-0.15.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:59fe030d8ae4a57e3fb7756bf35a858de74e04066fc8555c53d0af979132af81", size = 546890, upload-time = "2026-08-07T10:47:10.327Z" }, + { url = "https://files.pythonhosted.org/packages/dc/84/6937a280d461f7de6e031ffb02edc2b7c3c90d49d630565ce8ff27cbc5f2/librt-0.15.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5a6526a2a956bbb1e4ae3568c82e650fc99119c66bb011ea60715744955a2b4d", size = 555163, upload-time = "2026-08-07T10:47:11.798Z" }, + { url = "https://files.pythonhosted.org/packages/bc/95/2a2853c1ee014bf102116e7f897a04beeaeb2461b45b79af98bdfb95f1ef/librt-0.15.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:85ea21ec6730194d67156b0e0b5430ccb1d61f8b8b907e39b37f9812b74a13f0", size = 535812, upload-time = "2026-08-07T10:47:13.279Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4c/cf9601c1b4c5f09280acd5d83abdb2e68527a2be8257136eb42304218622/librt-0.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e47b8ba865d7ede071a91a7163073bbaeb72541f1ef8a07d512c45c7b5007f2", size = 573688, upload-time = "2026-08-07T10:47:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/47/6d/9ac7cbec46189a7625af4b5acbd25f10d827f4141b2002181848c8418923/librt-0.15.0-cp312-cp312-win32.whl", hash = "sha256:a5207ec414d1c4a2a7231b2086970dc036f94293cdf338190984958a013a42f1", size = 106138, upload-time = "2026-08-07T10:47:15.973Z" }, + { url = "https://files.pythonhosted.org/packages/38/d0/2ae99c83be86ce23f925ac1aeeedc777e97f427c4a8d190c70d0a16e9a87/librt-0.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:73b30cfa976659b3917c8f6153bdb0591c6a9ec6583599fd24a689b690622022", size = 126974, upload-time = "2026-08-07T10:47:17.049Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ef/dd24f9635c730b86b87587967dda7516b1845e8b17684603d31607fed598/librt-0.15.0-cp312-cp312-win_arm64.whl", hash = "sha256:a54cf9e0ef47b96af580849db5471142200568ce1e02cbf416addab551369570", size = 112292, upload-time = "2026-08-07T10:47:18.222Z" }, + { url = "https://files.pythonhosted.org/packages/e7/42/467b53a601b406ccd7b97c1fd54b59cb34f9185ad5ce7e9d5c3c4e8961c8/librt-0.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:db13ca398005abcbe538deda87b686d9bd08b7001cf40c4c06b444960ae10a26", size = 151029, upload-time = "2026-08-07T10:47:19.312Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e6/36c2299b7a94b84fdd01220d8a777a71be5be0925bb0dbdf71c0a06a34d9/librt-0.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa1f1995789dca3698bc550aaceb09a51bd5df0a057ff84ff15296cd1975b801", size = 155194, upload-time = "2026-08-07T10:47:20.398Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/ed5071f9325845e670bd36012757419767fbf56af77ed483077b9e4db541/librt-0.15.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55456ea87d8df21808446d03817be2f65e20391c1c615d9187440dff28cd08dc", size = 502568, upload-time = "2026-08-07T10:47:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/7f/81/6450c67c3615d87704bcbc21323fafc69c799b06a044c447529f725d4b01/librt-0.15.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5a86a5a08c2235316bdb359d5dbb6ce0abfca7fac06363103e2c5af571d92f95", size = 496153, upload-time = "2026-08-07T10:47:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d6/5f52b722bc75076954b3bfd49be15ea362df4d580c6fb315d0f617100d30/librt-0.15.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e56b6a368529bed262da40ce13f8fef590db0479819cca84f16a1f01ac356d0b", size = 513336, upload-time = "2026-08-07T10:47:24.213Z" }, + { url = "https://files.pythonhosted.org/packages/8d/e2/c08fd1d36ce63ea5a12b85c5d37f4550b5f86a692167e41e5a74222607ae/librt-0.15.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:234d8d394721fa0d786af15ebf1f3fb7f3ed82fd1cd0cde45c2f247b5d4281d2", size = 531661, upload-time = "2026-08-07T10:47:25.507Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d8/d9482fcbeb177b9eb87bb3899eeb3b42be690313c652f9e146b1d0681fb2/librt-0.15.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d8363d7accb0286ac3a0e633f396e93800dafb8150494505daf9515bbda591f3", size = 524487, upload-time = "2026-08-07T10:47:26.79Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/075171517b41f861753034fbb151b42cfc83bcc853849f24f5e66fd60ccf/librt-0.15.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0f0ee3644d951f31055ad07d77d92520e84505dd7a432cc4cd501dd70ee06785", size = 543201, upload-time = "2026-08-07T10:47:27.999Z" }, + { url = "https://files.pythonhosted.org/packages/b0/03/42c2330f37eeb475b6affeedd06518f60035f323af3a839335e3fc9fef2d/librt-0.15.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2cfd1a81a648806e6a7717be4cc4d1bb392fa229752bf8444ba365e381e984d6", size = 546467, upload-time = "2026-08-07T10:47:29.396Z" }, + { url = "https://files.pythonhosted.org/packages/57/1e/1ad4c5638f7e64d8560328bd25c54b409a661bdb6ff254b38ff90744288d/librt-0.15.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:a6cd22c9da0d866558e46a041f1cc0c2bbb26b61b137b2347fa834c332e1d101", size = 555139, upload-time = "2026-08-07T10:47:30.815Z" }, + { url = "https://files.pythonhosted.org/packages/49/41/39fa7d15db1204cd1cbe6514680fbdc243adf754a0885061308f43afc013/librt-0.15.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6d5225ef8801e4ea5e482fa9b5dfb891dd9ef6f6d870f1f25d449ca2c70ac218", size = 536050, upload-time = "2026-08-07T10:47:32.222Z" }, + { url = "https://files.pythonhosted.org/packages/1e/88/c6dcf0dd8e26dc0c9a499a2abab8646c86dcaf9ecea9524cb46d3686331a/librt-0.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d28a05796b99f749bf8794f17ba9ba1612d0076b802e9cfc62c554634e9ce3b", size = 573700, upload-time = "2026-08-07T10:47:33.527Z" }, + { url = "https://files.pythonhosted.org/packages/1b/9b/ab54c71a7918a7c34fa5327fb61390a77446a07a146fbfb1165250a61035/librt-0.15.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:2067ff438048cead9d223ca5675bae2a25e520a7c3e6c1498bf9c6892d22caab", size = 82194, upload-time = "2026-08-07T10:47:34.835Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b2/4f9a243bb892395f3becb80789ade13771701091f9f07ab8230247953ba8/librt-0.15.0-cp313-cp313-win32.whl", hash = "sha256:1cd3b721f24c206398b9e26da3c3a9c011e6e89d06f318ba8ebefc30f1003890", size = 106231, upload-time = "2026-08-07T10:47:36.251Z" }, + { url = "https://files.pythonhosted.org/packages/bf/af/64aff4885a40b93132382f2c314647d722574605416504379184ef3045ea/librt-0.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:f395a4a9a03ac062dbe9a9f82e0c720502e590a38feee6a757bc82e9c63afbd8", size = 126996, upload-time = "2026-08-07T10:47:37.453Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/335bccf6c7cb9028cb0b54aead27d9ece3f01f83bc6baa2abace5da655c1/librt-0.15.0-cp313-cp313-win_arm64.whl", hash = "sha256:0a15cb554761247d84a3ec0cbdf4078d70725384f0e4662c0fa3b26266eb60ad", size = 112188, upload-time = "2026-08-07T10:47:38.729Z" }, + { url = "https://files.pythonhosted.org/packages/a8/93/949053fb462eecc4a9a5ee770a81f4b40be7b79538b245545d4aebc6b58b/librt-0.15.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f5de7feedc56337a088eb15cd9fafa9938367362221d8cc62c642b7f94821993", size = 149833, upload-time = "2026-08-07T10:47:39.86Z" }, + { url = "https://files.pythonhosted.org/packages/61/ca/8281aa6cd560a3420e4497729f6b704b53be3eeaaef82d5aeadddaf7441f/librt-0.15.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c0eb900c0e91f4aebe680845242e614f1864edfd44106380d0752ac29522bf8", size = 154088, upload-time = "2026-08-07T10:47:41.065Z" }, + { url = "https://files.pythonhosted.org/packages/dd/02/1a1662dceaba6a086360891448d5ce9a7d3555976cae59a31a39d744b9c7/librt-0.15.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e8c9a650a188e38bac005048cbe6342e81407782944d01934540ab75e417df21", size = 494215, upload-time = "2026-08-07T10:47:42.388Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/99211619dc656370a3740c33d2b0b6d5a3fb1e73689314f6ed477a397dc4/librt-0.15.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:92bfed8deec93df30286b9fe9e3b1dd17329cc076a192b4ee5ec223841d54953", size = 491173, upload-time = "2026-08-07T10:47:43.683Z" }, + { url = "https://files.pythonhosted.org/packages/d4/aa/5448d0b05f4579b635d3899176817ebf561af0e57bacd425b5b1887264c1/librt-0.15.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec4b19788f835711a2072f9dbe6b03b3bf32ed1f0fb30cf399bdd59d9f0c33fa", size = 505512, upload-time = "2026-08-07T10:47:45.314Z" }, + { url = "https://files.pythonhosted.org/packages/95/82/01940e40b83c43a546c4a3c896cf34ca272a9690899d55914e4827b3dcce/librt-0.15.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4c7bacb70930f3d0a56f4ecf1be474a1f0d941b01dd73b756f3c256d42cb879", size = 523073, upload-time = "2026-08-07T10:47:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/fa/759c0030f3ee371439eb26de34fc745807caf0abb878af7af4b8b7c3dd3d/librt-0.15.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3e79f05e4a08b4d880342673312bbc895b56df7765605796f15902eb5367d3ae", size = 515080, upload-time = "2026-08-07T10:47:48.319Z" }, + { url = "https://files.pythonhosted.org/packages/0b/27/894e072228fcb159703c655da69f8cd10dbed489c36e3df7dd032a2483be/librt-0.15.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a417149c0cba4d50b61e992e5a15e69eaf96746609b461cc4ed168aeef6b79dd", size = 534164, upload-time = "2026-08-07T10:47:49.875Z" }, + { url = "https://files.pythonhosted.org/packages/98/a3/0078e91c1f36f8815db17827de15650b9a3fe56c55fbf998c854b34e40d3/librt-0.15.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:da7a94d6a3411f579d72aa3e3bc5fbca7ed4549f3dbd7e5de3aa567333374285", size = 540616, upload-time = "2026-08-07T10:47:51.408Z" }, + { url = "https://files.pythonhosted.org/packages/86/33/81a29b796dd52a45e9ef7974c7732926e8f10f15b8d2be505665979f896d/librt-0.15.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:856f743ae607f2c1380eccb566c0038a9fb3eabf0fc2be2704d76d9f73557239", size = 545890, upload-time = "2026-08-07T10:47:52.818Z" }, + { url = "https://files.pythonhosted.org/packages/05/82/8be1baa1350e5d30cfd70ae79d0a6f4dc5862ef47f7bb2808aabc9bb86e5/librt-0.15.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:779a6e7c894737e5983e7790a9c78c4000c30e23c9aada08081bdbea53b0fa60", size = 523287, upload-time = "2026-08-07T10:47:54.165Z" }, + { url = "https://files.pythonhosted.org/packages/c6/4f/d1be6a01a35c20ef734e0e44113f87d4af756a9354a89dcfbe3b4f8af5e1/librt-0.15.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:96bb17dbe8bab3c0954fbebfc69ed395599de75b6bbc35e3270a878e15d4dd65", size = 565868, upload-time = "2026-08-07T10:47:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/67/88/649cfa33f5825927b160610f670bdab012a64d627eddb94fa795ea4292fd/librt-0.15.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:7220697efaa6e5348fc3d18ee7f8563d4bfecd9872b37ffb915bfc1d08840622", size = 81619, upload-time = "2026-08-07T10:47:56.886Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/8e88a8d5e48fc8d1a817787fb6811dfff6499acd6c8683dd83934aa6ede0/librt-0.15.0-cp314-cp314-win32.whl", hash = "sha256:f54598964d357b1c5ab77cf5d92f21e598fe0e23cdbe9618480807f81b4eba15", size = 100138, upload-time = "2026-08-07T10:47:58.093Z" }, + { url = "https://files.pythonhosted.org/packages/80/92/20fd6c4b6a1b1a564b076d55cd3d427d8428217d7638dc25a654cc4791d4/librt-0.15.0-cp314-cp314-win_amd64.whl", hash = "sha256:3ff5893a2c23d886aa9ce786de5ac6ddc74aeeaf90743682b74d920e117d2e28", size = 121258, upload-time = "2026-08-07T10:47:59.564Z" }, + { url = "https://files.pythonhosted.org/packages/fc/28/6af430b44d9ebb897b865a3c363b6dcace51357be2347cc0f8f869656a86/librt-0.15.0-cp314-cp314-win_arm64.whl", hash = "sha256:3722a099730704c9a3d70c879fc0f51daec25fe5f1555672d97bc595abeafb95", size = 106467, upload-time = "2026-08-07T10:48:01.097Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/b42bb798942ced219f6d63b27e07f91237887a8d0bd0921666db79a13790/librt-0.15.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:38c0c7d4b6fc06c3324b3f9162c8391bfc4fd9dde53afe1033ce7edb48d5a714", size = 159523, upload-time = "2026-08-07T10:48:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/75/03/1b53cd4ef904e73b1d828a5f90143bf94a2967d7cfff0b9ccf93e12aa9b4/librt-0.15.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b2fdd7ead3c995c37940a790690660d0ca006c302db26cc51933f6766866fc3", size = 161638, upload-time = "2026-08-07T10:48:03.725Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/9f9c9fba097d49e9e694c2b4dc331df31884645ecbc58a93b4b5fc69d2c5/librt-0.15.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fde98cf1fc4bac144ce23c2c4c017b924ba714509ea9334977b0b27050c837d", size = 701795, upload-time = "2026-08-07T10:48:05.135Z" }, + { url = "https://files.pythonhosted.org/packages/4c/05/0966840bda0380c8ae167b9043c6230202941cc90ea29c48e096964c765e/librt-0.15.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:e3b461183c5fa7681b48560f91515f53a953122fb30c71e07abc67d7ddf58c38", size = 682147, upload-time = "2026-08-07T10:48:06.555Z" }, + { url = "https://files.pythonhosted.org/packages/18/af/1c47ca573c30ea47d195aec26133af522fea1104afaace028d7b32247ea8/librt-0.15.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4bbcc257e3babea20a91715c361b24554ec4e8f51aa578568afc230799fe1a19", size = 696397, upload-time = "2026-08-07T10:48:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/2e/0f/1aed6223d4f9f9d1171a8596ff100ea4c3f7699fea7a4ba657c3e60daa6c/librt-0.15.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b845b8d48088fad0cadc84be4b8fda63203be7e9237b71015b3925443c1f35ab", size = 722542, upload-time = "2026-08-07T10:48:09.569Z" }, + { url = "https://files.pythonhosted.org/packages/c6/22/9e3a929aea456c97d69e6ef3884efea56d4807f97399471cc946baebd8af/librt-0.15.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b30e600e8f337b9bd7f39b86d9fdfedc73cc46e3d0f745931a23a234220bb7e2", size = 729709, upload-time = "2026-08-07T10:48:11.129Z" }, + { url = "https://files.pythonhosted.org/packages/e9/1b/c327ef6018e3a9ca0b8e7c5eddeeb331ba8f9b76c24e126d37d0f6d62faf/librt-0.15.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:64b0c8c35aa4c4ed79896359f3e0b285cbe4e610042106500da4811c322cc108", size = 752891, upload-time = "2026-08-07T10:48:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/d7/d1/d5f1ea02c56930087009e39db9b70660a663e76c730b27b925d786718457/librt-0.15.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0da0d94cb802f32a0524653e7201f2cef72d5f700a5407678f5290483d4fcd08", size = 745301, upload-time = "2026-08-07T10:48:14.55Z" }, + { url = "https://files.pythonhosted.org/packages/d9/3c/5f7c585d15ebb2250c73e7c0ee4e9e47be72c65d520c07ddbcdc62037674/librt-0.15.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4a6369168d371207339b1e50d4532b06a7121586141f82599505a3f315751d47", size = 747921, upload-time = "2026-08-07T10:48:16.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/52/1443a446486eba966bcbca1696b472e4f210320ec42f490a47f48fbf0fdc/librt-0.15.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c434e072557ade9cbc642d052c89d031efe47d5c9614523619d0d74a02378e81", size = 727561, upload-time = "2026-08-07T10:48:18.089Z" }, + { url = "https://files.pythonhosted.org/packages/79/91/2270a9380f11725cf83ce1925a5e32dd1dde2be9bba597f25c10a38644e7/librt-0.15.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7eec6a42018bc1d45763b1c162d3d2bf7c3b9a1b0ed30d3e91dcba390efefcc", size = 774417, upload-time = "2026-08-07T10:48:19.611Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/f4b1548d4f5b99186737fe27aec238e9823e8d5d23bf4df007c030689dc5/librt-0.15.0-cp314-cp314t-win32.whl", hash = "sha256:6912fa5e635d74529ac7cdb1bdf6ca3af4453da8d1edbe0110ee1cb4ad407ebf", size = 104381, upload-time = "2026-08-07T10:48:21.048Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/134afad262def1de04c0843c376d02135f1168af43f22e09a52bd8394727/librt-0.15.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8e11699ed745931c395acd3621b07062e0f840efa6935aad87a64ed0995f0915", size = 127034, upload-time = "2026-08-07T10:48:22.561Z" }, + { url = "https://files.pythonhosted.org/packages/99/5f/1b6846b20572bd699c9e9ec321a5f781845bee477df2aa2a43b28bc40119/librt-0.15.0-cp314-cp314t-win_arm64.whl", hash = "sha256:5d2a91724463bfed4f573cd7a9fdc856d2e230d0c0e5a61416a93481dccd8605", size = 110827, upload-time = "2026-08-07T10:48:23.804Z" }, + { url = "https://files.pythonhosted.org/packages/c6/44/4de9f4ddadb009a55c7758eb5736d62534a7daaf27bd71bc50e64b606b06/librt-0.15.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:8443e38dcfcfdbcf5add5118c623efd788d65ac2e25756d6251a54a06a4d0aca", size = 149843, upload-time = "2026-08-07T10:48:25.148Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/5d9ab71e30119c44094e0275f38b47dd327aea0f843a080396677029d508/librt-0.15.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6d15a29033c57490cfe2069097c6fc4049e4e65ffbb749be7dc453b7c4c68965", size = 154510, upload-time = "2026-08-07T10:48:26.485Z" }, + { url = "https://files.pythonhosted.org/packages/d0/9c/8505d1b8f5e8c19587bd03f7429993b3e9ce5c06819d856bfb11d919374c/librt-0.15.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2c05c729b589e734c09578bf5964be48a911765484840d017bbc84f49d4c4ad", size = 497543, upload-time = "2026-08-07T10:48:28.045Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9a/3a8390775cb095765aded027ac9c63e7c8ea74e731498607544c6505de0e/librt-0.15.0-cp315-cp315-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:fa60887537e1d0cd2d9982269d33a709bf54b195cd2b9364fc0a758022af5bd9", size = 480452, upload-time = "2026-08-07T10:48:29.531Z" }, + { url = "https://files.pythonhosted.org/packages/e7/40/258a4a7117ee915d66de5cd9b8ade65a440993161107ce3a686f1859955c/librt-0.15.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d8bc24219b24c0af375718942ab75e3544b2763085f40f965be4326734ae8328", size = 507768, upload-time = "2026-08-07T10:48:31.007Z" }, + { url = "https://files.pythonhosted.org/packages/6b/c6/2f4dd296c97a0b85b98894519b279408ec9dd602d4f692b1ea0e25dee670/librt-0.15.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:86a21a7bd3fe3a419512ef424cc1c020f6771d0b29cfddff36d1635a855e63f0", size = 525122, upload-time = "2026-08-07T10:48:32.7Z" }, + { url = "https://files.pythonhosted.org/packages/49/dd/29eab42be13b2bf0ea8cb227135a45d44693e30a7e8b92871981ff56b82b/librt-0.15.0-cp315-cp315-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dbab647e88d90b3167b91efe7091e248653688ed4337e4f90907a722c7361bb9", size = 520371, upload-time = "2026-08-07T10:48:34.294Z" }, + { url = "https://files.pythonhosted.org/packages/91/ed/4bad71adeca8fe208b775c2a35417fa5a2584c8f4791daaf89a89450fea1/librt-0.15.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d8edcf6f550e918dca779c069b9e156385c60b406f99fc7641f32c52f7193659", size = 537258, upload-time = "2026-08-07T10:48:35.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/63/59dba6143fdcc7240c54458b629f3250000a61b8945890fc9efd451b19c5/librt-0.15.0-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:8b62076030baa2d8b1501a46bf0e19c27a489aa90671c55665bff7887f7660b0", size = 527432, upload-time = "2026-08-07T10:48:37.466Z" }, + { url = "https://files.pythonhosted.org/packages/ec/21/21a24c6a2327d8362580efebe77286bf47b0f4062ec5ea41766e609d3c7d/librt-0.15.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:d00d20d1818e82a07a0ee0aa89a98b17ed7916b92441090b683719cb20a59b6d", size = 548108, upload-time = "2026-08-07T10:48:39.384Z" }, + { url = "https://files.pythonhosted.org/packages/5a/6d/fc68c89a7971418b41f9a873623ff935cb864097544c6a2f8ce491c8ef5d/librt-0.15.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:4e6ee93fc3cf848dcbf0cce2eca73d8e7dcd0cc2b6df3a529d57750b30a4c55c", size = 529681, upload-time = "2026-08-07T10:48:41.392Z" }, + { url = "https://files.pythonhosted.org/packages/65/7e/c2d98766124400d722063a630b0fde38a9fc768705d37eecca15c47dc192/librt-0.15.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:32896a0af72508ea979e0acb4e4c04cbeeae04938167950d535c83c45597167d", size = 567736, upload-time = "2026-08-07T10:48:43.124Z" }, + { url = "https://files.pythonhosted.org/packages/55/6c/f8c34a95e3a515c6e1c192b89511e7253c89a7760c6b500d57ffdb8d2dc8/librt-0.15.0-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:ec3ba415afaf951f6951b1dd16d3c8e4f540065fc382d7e70b823a79567ca374", size = 81673, upload-time = "2026-08-07T10:48:44.645Z" }, + { url = "https://files.pythonhosted.org/packages/c9/9e/e23fa8e78679ec45728188650b39e8ff476c83b691c96f749217df3b1b7c/librt-0.15.0-cp315-cp315-win32.whl", hash = "sha256:d2813ba2503764f0450680c533d13df7cff9b49df1411062eded5f67db4195b9", size = 100081, upload-time = "2026-08-07T10:48:46.171Z" }, + { url = "https://files.pythonhosted.org/packages/e1/dc/3eb4c5e297343f0620a55532cd7c8d764d3001fa2159212dadf480464827/librt-0.15.0-cp315-cp315-win_amd64.whl", hash = "sha256:b87d67e33afaf265262f2a66db578284b88ee2e6fcd224579cb5c15518677ad8", size = 121228, upload-time = "2026-08-07T10:48:47.631Z" }, + { url = "https://files.pythonhosted.org/packages/97/70/43abce19f04e49762f8ec834c8fafee13cc40fd6b94a72a24e534febfcd0/librt-0.15.0-cp315-cp315-win_arm64.whl", hash = "sha256:713bd7df21170b982e729e46870f31d6b437bd1a9b4648cffb529bd3c2ec5c4b", size = 106487, upload-time = "2026-08-07T10:48:49.095Z" }, + { url = "https://files.pythonhosted.org/packages/de/15/83f2deddb9368b8951ec8c9477269b5b9b8bd9bbf15e57402d0f38817dca/librt-0.15.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:3de789c82752730f94782a5ee518baf9c05edf85733aeaf73bb6e518755cdf54", size = 159448, upload-time = "2026-08-07T10:48:50.649Z" }, + { url = "https://files.pythonhosted.org/packages/06/bf/043097353f9b3c73b583d07f6b8e552795463f4bfc8caf85e42eee50c26a/librt-0.15.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:e0b5deec9a8664eb722c797241970fd4aa1894d25fda36a1ddac0f7407606bd6", size = 161686, upload-time = "2026-08-07T10:48:52.174Z" }, + { url = "https://files.pythonhosted.org/packages/f4/2a/8ae77f9719d42ce71cd708560a3557b38ac3c17a0383e57f87084de45bbe/librt-0.15.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5563302a8359bc2295bb7084d1a8ed1519df96afb30eb2aa4e0bff7b54228988", size = 710668, upload-time = "2026-08-07T10:48:53.782Z" }, + { url = "https://files.pythonhosted.org/packages/61/34/c0436ea134deb9a0d6da80a396a2739a81cb31e0418f7227239e23140898/librt-0.15.0-cp315-cp315t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:22d6263b9d39d7bbb286fa791945646e3218f1be2d693e36fb630f1d0e59cd13", size = 679396, upload-time = "2026-08-07T10:48:55.645Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/001e0d99aa9250d5cd5715a9081291a20656083459f9019cda15255329e1/librt-0.15.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:39ffd14646190c454f0d86e0d256b33f00a87a26ab410e619773b841d0e41416", size = 704313, upload-time = "2026-08-07T10:48:57.46Z" }, + { url = "https://files.pythonhosted.org/packages/2d/53/b34fa9d0ff00f136f4d58ebb4c411ff634baed1eb412bb602a2bc8dcafcb/librt-0.15.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c47318cd3a61401452de11282242937e3e057c4fd3dbaf601e269d0928a06c0a", size = 729847, upload-time = "2026-08-07T10:48:59.231Z" }, + { url = "https://files.pythonhosted.org/packages/86/ac/fa4d7a424665040e95baf480a6d523446057684b6758624c85338e8a23b2/librt-0.15.0-cp315-cp315t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a56a1d4f859a82ca5b99fc4b82c9b027b15e3c455c5cd99e7d0719f27bb20b6c", size = 742736, upload-time = "2026-08-07T10:49:01.151Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f1/e17a9bb5de6fb8c3186ed1a7d68d21618b027ac2d3633e03d3b6109c67ae/librt-0.15.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:077471b3182db4e17c36ae91555f36a4d2c00080b267f749bcad34a478a9a302", size = 763454, upload-time = "2026-08-07T10:49:03.039Z" }, + { url = "https://files.pythonhosted.org/packages/1d/ec/ecd02cd30935b931b9cdbfed6ab5a099c51b280b4e7baa274da80978ed27/librt-0.15.0-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:411ca4d1b905b860ceba7570dd6717a71dedaddcc4b0f77ece710aa41ee11f8d", size = 743296, upload-time = "2026-08-07T10:49:04.941Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b5/b3c2b8353ce820a4854f78d19321344242f89fa71c975b71132ba9bf242a/librt-0.15.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1256589e0b0adb31751d685a68bce29d73407ddf4ef05d4188f49d5dcf9566d9", size = 756217, upload-time = "2026-08-07T10:49:06.825Z" }, + { url = "https://files.pythonhosted.org/packages/3c/52/6cc22542ba59146b05cca2a656f9ff8bb67e38e63d12c3b0cc183d837bf1/librt-0.15.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:f42b74a53e5f26a0ba0007411a7455b66c67ce4022a39cc1f56fc4efd65bcbab", size = 741934, upload-time = "2026-08-07T10:49:08.839Z" }, + { url = "https://files.pythonhosted.org/packages/40/32/a04b72b1aa86e3be23b2ecff8c1aad2dcc955bd3956d6d26e7e34267e57a/librt-0.15.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:291bf73caf78b9e88d6fae9bfd693207ff7d832e2fdbe2cf8e746bc13f5f892b", size = 783763, upload-time = "2026-08-07T10:49:10.661Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/89eb11dffbe9279ff37144dec786927314502ae0b114f1449dc78c458aab/librt-0.15.0-cp315-cp315t-win32.whl", hash = "sha256:c16d15ee371643ab48dc8248a3e680ebbeca573a13af2c3dd0c985b142d77162", size = 104313, upload-time = "2026-08-07T10:49:12.305Z" }, + { url = "https://files.pythonhosted.org/packages/6d/4a/1f1978c200f563beda63c36adff2d65bbecb81e365e8e69e572f5f70fbc6/librt-0.15.0-cp315-cp315t-win_amd64.whl", hash = "sha256:dbd605739f228912dc49027cb764456b9757750bdc2b6b7773164db7096c6fd1", size = 126889, upload-time = "2026-08-07T10:49:13.881Z" }, + { url = "https://files.pythonhosted.org/packages/38/a6/800800bfed7b1fb10fc3f3d557785c3854e80d3f7a9800d784b176a1fc2d/librt-0.15.0-cp315-cp315t-win_arm64.whl", hash = "sha256:84d244b00604d17df3fc7736c327892d6bba66181254aa4087be807b6c342bdc", size = 110700, upload-time = "2026-08-07T10:49:15.499Z" }, ] [[package]] @@ -1031,7 +1382,7 @@ name = "macholib" version = "1.16.4" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "altgraph", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "altgraph" }, ] sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" } wheels = [ @@ -1040,26 +1391,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.0.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, -] - -[[package]] -name = "marshmallow" -version = "3.26.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/55/79/de6c16cc902f4fc372236926b0ce2ab7845268dcc30fb2fbb7f71b418631/marshmallow-3.26.2.tar.gz", hash = "sha256:bbe2adb5a03e6e3571b573f42527c6fe926e17467833660bebd11593ab8dfd57", size = 222095, upload-time = "2025-12-22T06:53:53.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/be/2f/5108cb3ee4ba6501748c4908b908e55f42a5b66245b4cfe0c99326e1ef6e/marshmallow-3.26.2-py3-none-any.whl", hash = "sha256:013fa8a3c4c276c24d26d84ce934dc964e2aa794345a0f8c7e5a7191482c8a73", size = 50964, upload-time = "2025-12-22T06:53:51.801Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -1188,6 +1527,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "mypy" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/82/6a/878cc1097d4035f82bd516658d0c528d2a9955bc7b363afcbd0b07fea11b/mypy-2.3.1.tar.gz", hash = "sha256:47c1b1207258513a9d93495f69c8be9de73916186f0e52703e8c461b7a623419", size = 3992554, upload-time = "2026-08-15T03:03:38.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/be/c624d4241484f37dc62839e177ab607a9b8b3e96f0866544ca99e8e41d51/mypy-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:94f04929f1c44c35fb0061e912087edaf504acede963a4a7d00680bd089d8531", size = 13936739, upload-time = "2026-08-15T03:03:26.475Z" }, + { url = "https://files.pythonhosted.org/packages/53/84/e3cf72f90dce5960871c82551c8fba6da05fc1018f79be41c047bd126bdd/mypy-2.3.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f5d716048611e85ca9eefb2e1baa5d73ede389b5820ded260ea27c757d667af8", size = 14166460, upload-time = "2026-08-15T03:01:50.565Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ff/6b97d58aa0f79a5ab9b472db1f6d6df1b11a51d74d0c08ab3760d3a613ba/mypy-2.3.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b091a455111214cb5c9d54a57b9618e9a49f9fe2a42e4e1ac86e9d104ed96ce8", size = 15100476, upload-time = "2026-08-15T03:03:12.079Z" }, + { url = "https://files.pythonhosted.org/packages/da/f0/cbb4b7d2ae3ac635f6b4f2d9b04070b8a92edf50da599d3b39e5ed109001/mypy-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df12e20c9efd614738c71b390007ecd0181125afc4ccafca04d78a1d2eed2c01", size = 15347826, upload-time = "2026-08-15T03:03:02.856Z" }, + { url = "https://files.pythonhosted.org/packages/5f/10/91dcdc6f8d43fc08e6a06ab1f9732f3abaaf835ac1b2e67b9dff56910855/mypy-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:52eaf3a155f35cf80b40220288c861eb45f14a2340c1f6cbfbdb0feff32879d1", size = 11142615, upload-time = "2026-08-15T03:03:36.316Z" }, + { url = "https://files.pythonhosted.org/packages/3d/8a/28d54535bf4b9aa43b2d8918c2ef660378b9f66b23d78dcee052744ae622/mypy-2.3.1-cp311-cp311-win_arm64.whl", hash = "sha256:9b4eacbee8a69836c06eff6d0dd4e134a07c2b047755b30c08625fe214f322c6", size = 10141145, upload-time = "2026-08-15T03:03:07.406Z" }, + { url = "https://files.pythonhosted.org/packages/85/da/d6effc4f808a842d91edc22535dc9e799d2ff6e91449168b7f47a0771f54/mypy-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a32bbbb940af990d3be0b8af321c7b6815bb1b3b48142fe7459b9cc5f58959ff", size = 14047547, upload-time = "2026-08-15T03:02:57.707Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e6/478229701dab76f26485fc8ff5d6f241f393da22447400bbc56f6946aebe/mypy-2.3.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff715e45b2231a8e85de1d163d1b42791e4d7aab8f5145f85fee1b710b735aff", size = 14216515, upload-time = "2026-08-15T03:01:26.496Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/7c42327a3b21e84681f691982cbfe43f334a3685f3b683b72c376476c4fa/mypy-2.3.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:858fc57d3d91fa728e33e7ad71def60fc6272694607b306cd3292db53ae39080", size = 15307789, upload-time = "2026-08-15T03:03:31.62Z" }, + { url = "https://files.pythonhosted.org/packages/59/f4/7e597edbe01b5a56fa958ce541302dcaabfed979966f1dffedbea0ea0fc2/mypy-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:851833db876e7b650f93719c74b7879a08e338979c96054fdfc3bfd90a486355", size = 15548831, upload-time = "2026-08-15T03:03:15.55Z" }, + { url = "https://files.pythonhosted.org/packages/a3/52/cb31e084bc0314a1e384bdd677a4b80e55af04ccac077545e2238b9d320a/mypy-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:4c5095a327483591c94e0c8d3ef9e50d4ab1369b541eae007c1f23bc2a41f6bb", size = 11226359, upload-time = "2026-08-15T03:03:29.002Z" }, + { url = "https://files.pythonhosted.org/packages/7a/47/88fcf6217b43fa2da81a8c2611370af18141536a4f0294bbf98b457d456d/mypy-2.3.1-cp312-cp312-win_arm64.whl", hash = "sha256:bbfe022634a2a195406bd469e888d2eaf193b02ba7e607391cd7640374aaae3b", size = 10214707, upload-time = "2026-08-15T03:02:48.807Z" }, + { url = "https://files.pythonhosted.org/packages/de/cf/862010ee800ca9c2bd0c4c0dacf0f092e5411824a09b8f97ad4be8fe250e/mypy-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:114dff494000f18bd10d5d95d84b8567b26da60279ecbe838131841df20e635d", size = 13964542, upload-time = "2026-08-15T03:02:21.43Z" }, + { url = "https://files.pythonhosted.org/packages/75/5a/3f3a2107b41e3e92e617e25daaee121413b91e9784bea733131ed4fecc5d/mypy-2.3.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8637731bb5eee3671eb2c3200827aa3564ed8a9309ecee4d1afe77e6d031bdb", size = 14168922, upload-time = "2026-08-15T03:03:00.351Z" }, + { url = "https://files.pythonhosted.org/packages/8b/41/04dc4fe7e63d7820fa4eff272e95157d30cbea921388f3ab3fe77794cd0b/mypy-2.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c80fbc405ed8020f5ff3802dc18cf060197bcdd3fbdd6a26ef2fd34dfdd5226", size = 15244791, upload-time = "2026-08-15T03:02:31.089Z" }, + { url = "https://files.pythonhosted.org/packages/96/fc/c3053b26b9054949285aa868cb6af8c10e7591541cacd79c5dcc06a1fcf9/mypy-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:84081f538ce27375045c02e3d7f81bd11d853400621ae245d87ce7b6c420ec74", size = 15501627, upload-time = "2026-08-15T03:03:34.128Z" }, + { url = "https://files.pythonhosted.org/packages/70/4e/d77daab008bbc4e5001374d7928f4a260d28f0e6747af444fc4763f7a310/mypy-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:e9144ac16fde007096f9563eb2041b4433c2d705c4218edeb79e7e9d01035ee6", size = 11243961, upload-time = "2026-08-15T03:02:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f8/7eb68c136e4abd30569fe31ef2bfcb7eceae9952cab80017c04cd09f5d0c/mypy-2.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:77ad9529e67dca28e511f5cd5671436584ce91f6d3bac159a353158187b986ac", size = 10213219, upload-time = "2026-08-15T03:02:26.361Z" }, + { url = "https://files.pythonhosted.org/packages/be/c4/42a49d44aeff804edf1b19acce0b49e8bd1a9c57dee9605dd8d980aa43d7/mypy-2.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:192abaedf75da1bc0b1cef104927e70ec49c1ef0031cc4825c7ee10a438ed24d", size = 13986778, upload-time = "2026-08-15T03:01:33.69Z" }, + { url = "https://files.pythonhosted.org/packages/45/13/9331fd2dfed7194d66c5304072894a8be3e51e9deda6863c1eceaa35a43d/mypy-2.3.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf678dffd16efcda2c15cbd30e9ecc0081388e29ea23687a88e686ed92638dc3", size = 14188467, upload-time = "2026-08-15T03:02:40.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/f7/f4a34edab45667c5465855dc585a20e87978ffa8aee711445b7239d120c6/mypy-2.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e036f06b41630f4c8a1d48f9ac6aa26acc65f8be089973f5519da643318f03f", size = 15225538, upload-time = "2026-08-15T03:03:09.761Z" }, + { url = "https://files.pythonhosted.org/packages/40/05/534b3590757bd05794f73e07f6666c2a77b8597ffed795c94ce570096aa0/mypy-2.3.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:71af9c8a894e862b58e92abb08e53b05a384a1e5e5d6dc7cda59126211a53d82", size = 15480805, upload-time = "2026-08-15T03:01:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/55/da/bdfba852e2562f599624af5bb7d29e36b0b4f526f2b8bac85efe0dd1803d/mypy-2.3.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:3c80cd23d85368bdd9f37d5231dfd97d35bcbf5bf41af96ef3a9b078ad1957f9", size = 7761712, upload-time = "2026-08-15T03:02:36.008Z" }, + { url = "https://files.pythonhosted.org/packages/98/31/60fc64a74cdba4f2a5d642d32317993e479163e1ac7d91b695e5d15e2264/mypy-2.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:4956f34d145e145562a0a0bf367f642bbc85c04ec2baf47ae015947c3169a85d", size = 11423968, upload-time = "2026-08-15T03:02:06.931Z" }, + { url = "https://files.pythonhosted.org/packages/a9/23/eb5950b24cd26ba3b78f87707a275568d633c77dae8e61c9661be6055ca6/mypy-2.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:cfb12e360242d23d91f5e978d94f58ea66acf5804c4fb6f2f794a20d4cb1b595", size = 10399323, upload-time = "2026-08-15T03:02:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/82/c7/f80f4e46c0b9a00eb5f78a79d49dda8bdf56a5230f7257fb33e76be04da7/mypy-2.3.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5f1c50bb05b64e2026b52867e8d21106f01313c744a2c4ecc34c90d12e8d6e2", size = 15121308, upload-time = "2026-08-15T03:01:46.053Z" }, + { url = "https://files.pythonhosted.org/packages/5d/74/9b04f17c7074cc5188f02fb63a2ca1d43fedf479e84fe3091c39061a1d7f/mypy-2.3.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:667196b352f4cf304ded4c10f90cfc179263a1acfb3cdcfa984bdfd340d498bc", size = 15536590, upload-time = "2026-08-15T03:01:35.941Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/c837ef6208e567774e2ed1f863f8ba6ec4817b1b6dd426315e5d559b6ec9/mypy-2.3.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9c53e395c12cad2c6d4b67d5da7c6057638a132d85c08b73646b18f802a0045", size = 16791074, upload-time = "2026-08-15T03:01:31.073Z" }, + { url = "https://files.pythonhosted.org/packages/37/68/48730230afa45192d5bd429a6a2ff24a6f8dedda90fdf2b221792b54518f/mypy-2.3.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:18162b128c3f9c703cd35f5537446900b0d21a2549aa7a95d21380d2ef643fb0", size = 17069183, upload-time = "2026-08-15T03:02:28.566Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/ca23fc9c20eeda09a15c9cbcf50015d0e73f409f6ead059e42aa69a608ff/mypy-2.3.1-cp314-cp314t-win_amd64.whl", hash = "sha256:30c0477d4aab7b7f39c8397dc877f2c96b9fe5588ec379f372c56eb63d599f63", size = 12154679, upload-time = "2026-08-15T03:02:04.809Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/8d982126034990869466f73b8db80dcb2234a7ac39b4dad093e047a79835/mypy-2.3.1-cp314-cp314t-win_arm64.whl", hash = "sha256:6941ab3619377bc3f32ca02876b07d27f216f5201604b664d3937ea0fdd23bb4", size = 10969159, upload-time = "2026-08-15T03:02:38.152Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f7/41e7f2d8117fbc7a7587286162ffe2f688984b69c46ed63cf5f2e4fc3bae/mypy-2.3.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:6f041a6de52c9217ca125e78ba0a335cb7fd98a1c0580978e49ab2b126f70b57", size = 13990694, upload-time = "2026-08-15T03:03:21.919Z" }, + { url = "https://files.pythonhosted.org/packages/06/85/8f665811a0c8f3bf6fa1d9acd665ec2d97a2bcc453ae68dcd92340941cd6/mypy-2.3.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5159ae60f5dbc3a498af5ba8365505808ac8031bc63f9e00304ad545d40bdd9b", size = 14203518, upload-time = "2026-08-15T03:01:48.455Z" }, + { url = "https://files.pythonhosted.org/packages/2d/82/91b866c8546b120bff83b73a439d90d2d63ef3aff113599e6b8e4d566848/mypy-2.3.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47a8a7a0a7f6f6e63995c0ac36fa0c07b127413fdc81f0439b7f3dccafd33561", size = 15220224, upload-time = "2026-08-15T03:01:23.577Z" }, + { url = "https://files.pythonhosted.org/packages/c8/78/c226c99208ee40de7c768369fa533f933afa003dfdc606ff021450724e91/mypy-2.3.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2329c0501293d4e1f33bc15d04d6304d65a1cdda967ee93a05c1e681a3923133", size = 15501512, upload-time = "2026-08-15T03:02:09.453Z" }, + { url = "https://files.pythonhosted.org/packages/a9/e7/7cfb3f106c393979f4cc37ad6c0586044d50401e3c35b0c003e4f3ba6bc9/mypy-2.3.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:bb26deed807bdb0457cf3e3f1cd7c4a1cf9d66864eaf1b4a61e06805d4c6b1f9", size = 7761913, upload-time = "2026-08-15T03:01:55.65Z" }, + { url = "https://files.pythonhosted.org/packages/99/3c/52affefa273b97939a1f474ae4a349c8718635c15b941112dfab4291b0c1/mypy-2.3.1-cp315-cp315-win_amd64.whl", hash = "sha256:375d7013876a8233b2d05be185bfa09f689696cd999ce8b1cfe6acac5c80e8a3", size = 11422533, upload-time = "2026-08-15T03:03:24.101Z" }, + { url = "https://files.pythonhosted.org/packages/2a/b7/75643e70c72a5b346d8a9b1543c967ea8824df2ee3fb7ccba652c272b7bb/mypy-2.3.1-cp315-cp315-win_arm64.whl", hash = "sha256:586b3612214cceabb3c0f588c97e7d1e535393f06a60e912e994f6b3ace97523", size = 10397931, upload-time = "2026-08-15T03:02:55.265Z" }, + { url = "https://files.pythonhosted.org/packages/10/ce/53be21f2d4adfcd26f63f1184a13ed797015ab463853f117e2e11e4d726f/mypy-2.3.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:ef0c6335cda9d807f8193d8ff6204a72bc909fa9882aacbca14f43cdb7188306", size = 15118669, upload-time = "2026-08-15T03:02:51.479Z" }, + { url = "https://files.pythonhosted.org/packages/62/43/20de757cd42989d291a17fad607742c4c74e875ce5cea00e5a5225020ac1/mypy-2.3.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e598c8c66401d26b150872154a286e6d484cf2789c3bb28a7556806298423021", size = 15545627, upload-time = "2026-08-15T03:03:05.132Z" }, + { url = "https://files.pythonhosted.org/packages/7e/fc/092bdf77ad280eaf501422f0f3b966012b528076cc13e41a774861c907d1/mypy-2.3.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eda22fd4efa9dcd39331d1dede9b5b8b8a7fd69af07592e778433da98610d29e", size = 16764157, upload-time = "2026-08-15T03:02:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/94/5c/c94c4d62d909b07f552d0d9356d7acc943825558e602a64822ffa2231536/mypy-2.3.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:2a0ba2e57847849fb0d1fcdabb32786d223095ed8bc121dfe322bcdb3d9c46bc", size = 17073258, upload-time = "2026-08-15T03:02:14.573Z" }, + { url = "https://files.pythonhosted.org/packages/c0/f7/511a88b89e478053c02d22039bb8f3ce4183efe8fd7a4f0a5910a8bb0a32/mypy-2.3.1-cp315-cp315t-win_amd64.whl", hash = "sha256:3f7e865dd51f235f60a2dbcd8728a1c095f5ca28f095d48a725b84cd935735c4", size = 12135505, upload-time = "2026-08-15T03:02:16.714Z" }, + { url = "https://files.pythonhosted.org/packages/71/bf/02573b56964ecb0f7c644f915f53c325ae15c3faec521c5adf11599a32df/mypy-2.3.1-cp315-cp315t-win_arm64.whl", hash = "sha256:8ad80807dc3ab8ea978b1b2b6e4a657194ace1d4ef03e0e731aff1abd517da29", size = 10962647, upload-time = "2026-08-15T03:01:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/8e/41/9675c7a1e78edecfba0b79e587a52594c56e189368261dc7b3a7fffb9527/mypy-2.3.1-py3-none-any.whl", hash = "sha256:6ed5c7e3419083268e5c9258bd1c1ef91af44a9e89374dbcaf37b775716e72eb", size = 2754338, upload-time = "2026-08-15T03:02:53.4Z" }, +] + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -1199,100 +1598,180 @@ wheels = [ [[package]] name = "numpy" -version = "2.4.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/10/8b/c265f4823726ab832de836cdd184d0986dcf94480f81e8739692a7ac7af2/numpy-2.4.3.tar.gz", hash = "sha256:483a201202b73495f00dbc83796c6ae63137a9bdade074f7648b3e32613412dd", size = 20727743, upload-time = "2026-03-09T07:58:53.426Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/51/5093a2df15c4dc19da3f79d1021e891f5dcf1d9d1db6ba38891d5590f3fe/numpy-2.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:33b3bf58ee84b172c067f56aeadc7ee9ab6de69c5e800ab5b10295d54c581adb", size = 16957183, upload-time = "2026-03-09T07:55:57.774Z" }, - { url = "https://files.pythonhosted.org/packages/b5/7c/c061f3de0630941073d2598dc271ac2f6cbcf5c83c74a5870fea07488333/numpy-2.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8ba7b51e71c05aa1f9bc3641463cd82308eab40ce0d5c7e1fd4038cbf9938147", size = 14968734, upload-time = "2026-03-09T07:56:00.494Z" }, - { url = "https://files.pythonhosted.org/packages/ef/27/d26c85cbcd86b26e4f125b0668e7a7c0542d19dd7d23ee12e87b550e95b5/numpy-2.4.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1988292870c7cb9d0ebb4cc96b4d447513a9644801de54606dc7aabf2b7d920", size = 5475288, upload-time = "2026-03-09T07:56:02.857Z" }, - { url = "https://files.pythonhosted.org/packages/2b/09/3c4abbc1dcd8010bf1a611d174c7aa689fc505585ec806111b4406f6f1b1/numpy-2.4.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:23b46bb6d8ecb68b58c09944483c135ae5f0e9b8d8858ece5e4ead783771d2a9", size = 6805253, upload-time = "2026-03-09T07:56:04.53Z" }, - { url = "https://files.pythonhosted.org/packages/21/bc/e7aa3f6817e40c3f517d407742337cbb8e6fc4b83ce0b55ab780c829243b/numpy-2.4.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a016db5c5dba78fa8fe9f5d80d6708f9c42ab087a739803c0ac83a43d686a470", size = 15969479, upload-time = "2026-03-09T07:56:06.638Z" }, - { url = "https://files.pythonhosted.org/packages/78/51/9f5d7a41f0b51649ddf2f2320595e15e122a40610b233d51928dd6c92353/numpy-2.4.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:715de7f82e192e8cae5a507a347d97ad17598f8e026152ca97233e3666daaa71", size = 16901035, upload-time = "2026-03-09T07:56:09.405Z" }, - { url = "https://files.pythonhosted.org/packages/64/6e/b221dd847d7181bc5ee4857bfb026182ef69499f9305eb1371cbb1aea626/numpy-2.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2ddb7919366ee468342b91dea2352824c25b55814a987847b6c52003a7c97f15", size = 17325657, upload-time = "2026-03-09T07:56:12.067Z" }, - { url = "https://files.pythonhosted.org/packages/eb/b8/8f3fd2da596e1063964b758b5e3c970aed1949a05200d7e3d46a9d46d643/numpy-2.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a315e5234d88067f2d97e1f2ef670a7569df445d55400f1e33d117418d008d52", size = 18635512, upload-time = "2026-03-09T07:56:14.629Z" }, - { url = "https://files.pythonhosted.org/packages/5c/24/2993b775c37e39d2f8ab4125b44337ab0b2ba106c100980b7c274a22bee7/numpy-2.4.3-cp311-cp311-win32.whl", hash = "sha256:2b3f8d2c4589b1a2028d2a770b0fc4d1f332fb5e01521f4de3199a896d158ddd", size = 6238100, upload-time = "2026-03-09T07:56:17.243Z" }, - { url = "https://files.pythonhosted.org/packages/76/1d/edccf27adedb754db7c4511d5eac8b83f004ae948fe2d3509e8b78097d4c/numpy-2.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:77e76d932c49a75617c6d13464e41203cd410956614d0a0e999b25e9e8d27eec", size = 12609816, upload-time = "2026-03-09T07:56:19.089Z" }, - { url = "https://files.pythonhosted.org/packages/92/82/190b99153480076c8dce85f4cfe7d53ea84444145ffa54cb58dcd460d66b/numpy-2.4.3-cp311-cp311-win_arm64.whl", hash = "sha256:eb610595dd91560905c132c709412b512135a60f1851ccbd2c959e136431ff67", size = 10485757, upload-time = "2026-03-09T07:56:21.753Z" }, - { url = "https://files.pythonhosted.org/packages/a9/ed/6388632536f9788cea23a3a1b629f25b43eaacd7d7377e5d6bc7b9deb69b/numpy-2.4.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61b0cbabbb6126c8df63b9a3a0c4b1f44ebca5e12ff6997b80fcf267fb3150ef", size = 16669628, upload-time = "2026-03-09T07:56:24.252Z" }, - { url = "https://files.pythonhosted.org/packages/74/1b/ee2abfc68e1ce728b2958b6ba831d65c62e1b13ce3017c13943f8f9b5b2e/numpy-2.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7395e69ff32526710748f92cd8c9849b361830968ea3e24a676f272653e8983e", size = 14696872, upload-time = "2026-03-09T07:56:26.991Z" }, - { url = "https://files.pythonhosted.org/packages/ba/d1/780400e915ff5638166f11ca9dc2c5815189f3d7cf6f8759a1685e586413/numpy-2.4.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:abdce0f71dcb4a00e4e77f3faf05e4616ceccfe72ccaa07f47ee79cda3b7b0f4", size = 5203489, upload-time = "2026-03-09T07:56:29.414Z" }, - { url = "https://files.pythonhosted.org/packages/0b/bb/baffa907e9da4cc34a6e556d6d90e032f6d7a75ea47968ea92b4858826c4/numpy-2.4.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:48da3a4ee1336454b07497ff7ec83903efa5505792c4e6d9bf83d99dc07a1e18", size = 6550814, upload-time = "2026-03-09T07:56:32.225Z" }, - { url = "https://files.pythonhosted.org/packages/7b/12/8c9f0c6c95f76aeb20fc4a699c33e9f827fa0d0f857747c73bb7b17af945/numpy-2.4.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32e3bef222ad6b052280311d1d60db8e259e4947052c3ae7dd6817451fc8a4c5", size = 15666601, upload-time = "2026-03-09T07:56:34.461Z" }, - { url = "https://files.pythonhosted.org/packages/bd/79/cc665495e4d57d0aa6fbcc0aa57aa82671dfc78fbf95fe733ed86d98f52a/numpy-2.4.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7dd01a46700b1967487141a66ac1a3cf0dd8ebf1f08db37d46389401512ca97", size = 16621358, upload-time = "2026-03-09T07:56:36.852Z" }, - { url = "https://files.pythonhosted.org/packages/a8/40/b4ecb7224af1065c3539f5ecfff879d090de09608ad1008f02c05c770cb3/numpy-2.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:76f0f283506c28b12bba319c0fab98217e9f9b54e6160e9c79e9f7348ba32e9c", size = 17016135, upload-time = "2026-03-09T07:56:39.337Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b1/6a88e888052eed951afed7a142dcdf3b149a030ca59b4c71eef085858e43/numpy-2.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:737f630a337364665aba3b5a77e56a68cc42d350edd010c345d65a3efa3addcc", size = 18345816, upload-time = "2026-03-09T07:56:42.31Z" }, - { url = "https://files.pythonhosted.org/packages/f3/8f/103a60c5f8c3d7fc678c19cd7b2476110da689ccb80bc18050efbaeae183/numpy-2.4.3-cp312-cp312-win32.whl", hash = "sha256:26952e18d82a1dbbc2f008d402021baa8d6fc8e84347a2072a25e08b46d698b9", size = 5960132, upload-time = "2026-03-09T07:56:44.851Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7c/f5ee1bf6ed888494978046a809df2882aad35d414b622893322df7286879/numpy-2.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:65f3c2455188f09678355f5cae1f959a06b778bc66d535da07bf2ef20cd319d5", size = 12316144, upload-time = "2026-03-09T07:56:47.057Z" }, - { url = "https://files.pythonhosted.org/packages/71/46/8d1cb3f7a00f2fb6394140e7e6623696e54c6318a9d9691bb4904672cf42/numpy-2.4.3-cp312-cp312-win_arm64.whl", hash = "sha256:2abad5c7fef172b3377502bde47892439bae394a71bc329f31df0fd829b41a9e", size = 10220364, upload-time = "2026-03-09T07:56:49.849Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d0/1fe47a98ce0df229238b77611340aff92d52691bcbc10583303181abf7fc/numpy-2.4.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b346845443716c8e542d54112966383b448f4a3ba5c66409771b8c0889485dd3", size = 16665297, upload-time = "2026-03-09T07:56:52.296Z" }, - { url = "https://files.pythonhosted.org/packages/27/d9/4e7c3f0e68dfa91f21c6fb6cf839bc829ec920688b1ce7ec722b1a6202fb/numpy-2.4.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2629289168f4897a3c4e23dc98d6f1731f0fc0fe52fb9db19f974041e4cc12b9", size = 14691853, upload-time = "2026-03-09T07:56:54.992Z" }, - { url = "https://files.pythonhosted.org/packages/3a/66/bd096b13a87549683812b53ab211e6d413497f84e794fb3c39191948da97/numpy-2.4.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:bb2e3cf95854233799013779216c57e153c1ee67a0bf92138acca0e429aefaee", size = 5198435, upload-time = "2026-03-09T07:56:57.184Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2f/687722910b5a5601de2135c891108f51dfc873d8e43c8ed9f4ebb440b4a2/numpy-2.4.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:7f3408ff897f8ab07a07fbe2823d7aee6ff644c097cc1f90382511fe982f647f", size = 6546347, upload-time = "2026-03-09T07:56:59.531Z" }, - { url = "https://files.pythonhosted.org/packages/bf/ec/7971c4e98d86c564750393fab8d7d83d0a9432a9d78bb8a163a6dc59967a/numpy-2.4.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:decb0eb8a53c3b009b0962378065589685d66b23467ef5dac16cbe818afde27f", size = 15664626, upload-time = "2026-03-09T07:57:01.385Z" }, - { url = "https://files.pythonhosted.org/packages/7e/eb/7daecbea84ec935b7fc732e18f532073064a3816f0932a40a17f3349185f/numpy-2.4.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5f51900414fc9204a0e0da158ba2ac52b75656e7dce7e77fb9f84bfa343b4cc", size = 16608916, upload-time = "2026-03-09T07:57:04.008Z" }, - { url = "https://files.pythonhosted.org/packages/df/58/2a2b4a817ffd7472dca4421d9f0776898b364154e30c95f42195041dc03b/numpy-2.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6bd06731541f89cdc01b261ba2c9e037f1543df7472517836b78dfb15bd6e476", size = 17015824, upload-time = "2026-03-09T07:57:06.347Z" }, - { url = "https://files.pythonhosted.org/packages/4a/ca/627a828d44e78a418c55f82dd4caea8ea4a8ef24e5144d9e71016e52fb40/numpy-2.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:22654fe6be0e5206f553a9250762c653d3698e46686eee53b399ab90da59bd92", size = 18334581, upload-time = "2026-03-09T07:57:09.114Z" }, - { url = "https://files.pythonhosted.org/packages/cd/c0/76f93962fc79955fcba30a429b62304332345f22d4daec1cb33653425643/numpy-2.4.3-cp313-cp313-win32.whl", hash = "sha256:d71e379452a2f670ccb689ec801b1218cd3983e253105d6e83780967e899d687", size = 5958618, upload-time = "2026-03-09T07:57:11.432Z" }, - { url = "https://files.pythonhosted.org/packages/b1/3c/88af0040119209b9b5cb59485fa48b76f372c73068dbf9254784b975ac53/numpy-2.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:0a60e17a14d640f49146cb38e3f105f571318db7826d9b6fef7e4dce758faecd", size = 12312824, upload-time = "2026-03-09T07:57:13.586Z" }, - { url = "https://files.pythonhosted.org/packages/58/ce/3d07743aced3d173f877c3ef6a454c2174ba42b584ab0b7e6d99374f51ed/numpy-2.4.3-cp313-cp313-win_arm64.whl", hash = "sha256:c9619741e9da2059cd9c3f206110b97583c7152c1dc9f8aafd4beb450ac1c89d", size = 10221218, upload-time = "2026-03-09T07:57:16.183Z" }, - { url = "https://files.pythonhosted.org/packages/62/09/d96b02a91d09e9d97862f4fc8bfebf5400f567d8eb1fe4b0cc4795679c15/numpy-2.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7aa4e54f6469300ebca1d9eb80acd5253cdfa36f2c03d79a35883687da430875", size = 14819570, upload-time = "2026-03-09T07:57:18.564Z" }, - { url = "https://files.pythonhosted.org/packages/b5/ca/0b1aba3905fdfa3373d523b2b15b19029f4f3031c87f4066bd9d20ef6c6b/numpy-2.4.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:d1b90d840b25874cf5cd20c219af10bac3667db3876d9a495609273ebe679070", size = 5326113, upload-time = "2026-03-09T07:57:21.052Z" }, - { url = "https://files.pythonhosted.org/packages/c0/63/406e0fd32fcaeb94180fd6a4c41e55736d676c54346b7efbce548b94a914/numpy-2.4.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a749547700de0a20a6718293396ec237bb38218049cfce788e08fcb716e8cf73", size = 6646370, upload-time = "2026-03-09T07:57:22.804Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d0/10f7dc157d4b37af92720a196be6f54f889e90dcd30dce9dc657ed92c257/numpy-2.4.3-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94f3c4a151a2e529adf49c1d54f0f57ff8f9b233ee4d44af623a81553ab86368", size = 15723499, upload-time = "2026-03-09T07:57:24.693Z" }, - { url = "https://files.pythonhosted.org/packages/66/f1/d1c2bf1161396629701bc284d958dc1efa3a5a542aab83cf11ee6eb4cba5/numpy-2.4.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22c31dc07025123aedf7f2db9e91783df13f1776dc52c6b22c620870dc0fab22", size = 16657164, upload-time = "2026-03-09T07:57:27.676Z" }, - { url = "https://files.pythonhosted.org/packages/1a/be/cca19230b740af199ac47331a21c71e7a3d0ba59661350483c1600d28c37/numpy-2.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:148d59127ac95979d6f07e4d460f934ebdd6eed641db9c0db6c73026f2b2101a", size = 17081544, upload-time = "2026-03-09T07:57:30.664Z" }, - { url = "https://files.pythonhosted.org/packages/b9/c5/9602b0cbb703a0936fb40f8a95407e8171935b15846de2f0776e08af04c7/numpy-2.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a97cbf7e905c435865c2d939af3d93f99d18eaaa3cabe4256f4304fb51604349", size = 18380290, upload-time = "2026-03-09T07:57:33.763Z" }, - { url = "https://files.pythonhosted.org/packages/ed/81/9f24708953cd30be9ee36ec4778f4b112b45165812f2ada4cc5ea1c1f254/numpy-2.4.3-cp313-cp313t-win32.whl", hash = "sha256:be3b8487d725a77acccc9924f65fd8bce9af7fac8c9820df1049424a2115af6c", size = 6082814, upload-time = "2026-03-09T07:57:36.491Z" }, - { url = "https://files.pythonhosted.org/packages/e2/9e/52f6eaa13e1a799f0ab79066c17f7016a4a8ae0c1aefa58c82b4dab690b4/numpy-2.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1ec84fd7c8e652b0f4aaaf2e6e9cc8eaa9b1b80a537e06b2e3a2fb176eedcb26", size = 12452673, upload-time = "2026-03-09T07:57:38.281Z" }, - { url = "https://files.pythonhosted.org/packages/c4/04/b8cece6ead0b30c9fbd99bb835ad7ea0112ac5f39f069788c5558e3b1ab2/numpy-2.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:120df8c0a81ebbf5b9020c91439fccd85f5e018a927a39f624845be194a2be02", size = 10290907, upload-time = "2026-03-09T07:57:40.747Z" }, - { url = "https://files.pythonhosted.org/packages/70/ae/3936f79adebf8caf81bd7a599b90a561334a658be4dcc7b6329ebf4ee8de/numpy-2.4.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5884ce5c7acfae1e4e1b6fde43797d10aa506074d25b531b4f54bde33c0c31d4", size = 16664563, upload-time = "2026-03-09T07:57:43.817Z" }, - { url = "https://files.pythonhosted.org/packages/9b/62/760f2b55866b496bb1fa7da2a6db076bef908110e568b02fcfc1422e2a3a/numpy-2.4.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:297837823f5bc572c5f9379b0c9f3a3365f08492cbdc33bcc3af174372ebb168", size = 14702161, upload-time = "2026-03-09T07:57:46.169Z" }, - { url = "https://files.pythonhosted.org/packages/32/af/a7a39464e2c0a21526fb4fb76e346fb172ebc92f6d1c7a07c2c139cc17b1/numpy-2.4.3-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:a111698b4a3f8dcbe54c64a7708f049355abd603e619013c346553c1fd4ca90b", size = 5208738, upload-time = "2026-03-09T07:57:48.506Z" }, - { url = "https://files.pythonhosted.org/packages/29/8c/2a0cf86a59558fa078d83805589c2de490f29ed4fb336c14313a161d358a/numpy-2.4.3-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:4bd4741a6a676770e0e97fe9ab2e51de01183df3dcbcec591d26d331a40de950", size = 6543618, upload-time = "2026-03-09T07:57:50.591Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b8/612ce010c0728b1c363fa4ea3aa4c22fe1c5da1de008486f8c2f5cb92fae/numpy-2.4.3-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:54f29b877279d51e210e0c80709ee14ccbbad647810e8f3d375561c45ef613dd", size = 15680676, upload-time = "2026-03-09T07:57:52.34Z" }, - { url = "https://files.pythonhosted.org/packages/a9/7e/4f120ecc54ba26ddf3dc348eeb9eb063f421de65c05fc961941798feea18/numpy-2.4.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:679f2a834bae9020f81534671c56fd0cc76dd7e5182f57131478e23d0dc59e24", size = 16613492, upload-time = "2026-03-09T07:57:54.91Z" }, - { url = "https://files.pythonhosted.org/packages/2c/86/1b6020db73be330c4b45d5c6ee4295d59cfeef0e3ea323959d053e5a6909/numpy-2.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d84f0f881cb2225c2dfd7f78a10a5645d487a496c6668d6cc39f0f114164f3d0", size = 17031789, upload-time = "2026-03-09T07:57:57.641Z" }, - { url = "https://files.pythonhosted.org/packages/07/3a/3b90463bf41ebc21d1b7e06079f03070334374208c0f9a1f05e4ae8455e7/numpy-2.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d213c7e6e8d211888cc359bab7199670a00f5b82c0978b9d1c75baf1eddbeac0", size = 18339941, upload-time = "2026-03-09T07:58:00.577Z" }, - { url = "https://files.pythonhosted.org/packages/a8/74/6d736c4cd962259fd8bae9be27363eb4883a2f9069763747347544c2a487/numpy-2.4.3-cp314-cp314-win32.whl", hash = "sha256:52077feedeff7c76ed7c9f1a0428558e50825347b7545bbb8523da2cd55c547a", size = 6007503, upload-time = "2026-03-09T07:58:03.331Z" }, - { url = "https://files.pythonhosted.org/packages/48/39/c56ef87af669364356bb011922ef0734fc49dad51964568634c72a009488/numpy-2.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:0448e7f9caefb34b4b7dd2b77f21e8906e5d6f0365ad525f9f4f530b13df2afc", size = 12444915, upload-time = "2026-03-09T07:58:06.353Z" }, - { url = "https://files.pythonhosted.org/packages/9d/1f/ab8528e38d295fd349310807496fabb7cf9fe2e1f70b97bc20a483ea9d4a/numpy-2.4.3-cp314-cp314-win_arm64.whl", hash = "sha256:b44fd60341c4d9783039598efadd03617fa28d041fc37d22b62d08f2027fa0e7", size = 10494875, upload-time = "2026-03-09T07:58:08.734Z" }, - { url = "https://files.pythonhosted.org/packages/e6/ef/b7c35e4d5ef141b836658ab21a66d1a573e15b335b1d111d31f26c8ef80f/numpy-2.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0a195f4216be9305a73c0e91c9b026a35f2161237cf1c6de9b681637772ea657", size = 14822225, upload-time = "2026-03-09T07:58:11.034Z" }, - { url = "https://files.pythonhosted.org/packages/cd/8d/7730fa9278cf6648639946cc816e7cc89f0d891602584697923375f801ed/numpy-2.4.3-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:cd32fbacb9fd1bf041bf8e89e4576b6f00b895f06d00914820ae06a616bdfef7", size = 5328769, upload-time = "2026-03-09T07:58:13.67Z" }, - { url = "https://files.pythonhosted.org/packages/47/01/d2a137317c958b074d338807c1b6a383406cdf8b8e53b075d804cc3d211d/numpy-2.4.3-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:2e03c05abaee1f672e9d67bc858f300b5ccba1c21397211e8d77d98350972093", size = 6649461, upload-time = "2026-03-09T07:58:15.912Z" }, - { url = "https://files.pythonhosted.org/packages/5c/34/812ce12bc0f00272a4b0ec0d713cd237cb390666eb6206323d1cc9cedbb2/numpy-2.4.3-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d1ce23cce91fcea443320a9d0ece9b9305d4368875bab09538f7a5b4131938a", size = 15725809, upload-time = "2026-03-09T07:58:17.787Z" }, - { url = "https://files.pythonhosted.org/packages/25/c0/2aed473a4823e905e765fee3dc2cbf504bd3e68ccb1150fbdabd5c39f527/numpy-2.4.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c59020932feb24ed49ffd03704fbab89f22aa9c0d4b180ff45542fe8918f5611", size = 16655242, upload-time = "2026-03-09T07:58:20.476Z" }, - { url = "https://files.pythonhosted.org/packages/f2/c8/7e052b2fc87aa0e86de23f20e2c42bd261c624748aa8efd2c78f7bb8d8c6/numpy-2.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9684823a78a6cd6ad7511fc5e25b07947d1d5b5e2812c93fe99d7d4195130720", size = 17080660, upload-time = "2026-03-09T07:58:23.067Z" }, - { url = "https://files.pythonhosted.org/packages/f3/3d/0876746044db2adcb11549f214d104f2e1be00f07a67edbb4e2812094847/numpy-2.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0200b25c687033316fb39f0ff4e3e690e8957a2c3c8d22499891ec58c37a3eb5", size = 18380384, upload-time = "2026-03-09T07:58:25.839Z" }, - { url = "https://files.pythonhosted.org/packages/07/12/8160bea39da3335737b10308df4f484235fd297f556745f13092aa039d3b/numpy-2.4.3-cp314-cp314t-win32.whl", hash = "sha256:5e10da9e93247e554bb1d22f8edc51847ddd7dde52d85ce31024c1b4312bfba0", size = 6154547, upload-time = "2026-03-09T07:58:28.289Z" }, - { url = "https://files.pythonhosted.org/packages/42/f3/76534f61f80d74cc9cdf2e570d3d4eeb92c2280a27c39b0aaf471eda7b48/numpy-2.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:45f003dbdffb997a03da2d1d0cb41fbd24a87507fb41605c0420a3db5bd4667b", size = 12633645, upload-time = "2026-03-09T07:58:30.384Z" }, - { url = "https://files.pythonhosted.org/packages/1f/b6/7c0d4334c15983cec7f92a69e8ce9b1e6f31857e5ee3a413ac424e6bd63d/numpy-2.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:4d382735cecd7bcf090172489a525cd7d4087bc331f7df9f60ddc9a296cf208e", size = 10565454, upload-time = "2026-03-09T07:58:33.031Z" }, - { url = "https://files.pythonhosted.org/packages/64/e4/4dab9fb43c83719c29241c535d9e07be73bea4bc0c6686c5816d8e1b6689/numpy-2.4.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c6b124bfcafb9e8d3ed09130dbee44848c20b3e758b6bbf006e641778927c028", size = 16834892, upload-time = "2026-03-09T07:58:35.334Z" }, - { url = "https://files.pythonhosted.org/packages/c9/29/f8b6d4af90fed3dfda84ebc0df06c9833d38880c79ce954e5b661758aa31/numpy-2.4.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:76dbb9d4e43c16cf9aa711fcd8de1e2eeb27539dcefb60a1d5e9f12fae1d1ed8", size = 14893070, upload-time = "2026-03-09T07:58:37.7Z" }, - { url = "https://files.pythonhosted.org/packages/9a/04/a19b3c91dbec0a49269407f15d5753673a09832daed40c45e8150e6fa558/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:29363fbfa6f8ee855d7569c96ce524845e3d726d6c19b29eceec7dd555dab152", size = 5399609, upload-time = "2026-03-09T07:58:39.853Z" }, - { url = "https://files.pythonhosted.org/packages/79/34/4d73603f5420eab89ea8a67097b31364bf7c30f811d4dd84b1659c7476d9/numpy-2.4.3-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:bc71942c789ef415a37f0d4eab90341425a00d538cd0642445d30b41023d3395", size = 6714355, upload-time = "2026-03-09T07:58:42.365Z" }, - { url = "https://files.pythonhosted.org/packages/58/ad/1100d7229bb248394939a12a8074d485b655e8ed44207d328fdd7fcebc7b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e58765ad74dcebd3ef0208a5078fba32dc8ec3578fe84a604432950cd043d79", size = 15800434, upload-time = "2026-03-09T07:58:44.837Z" }, - { url = "https://files.pythonhosted.org/packages/0c/fd/16d710c085d28ba4feaf29ac60c936c9d662e390344f94a6beaa2ac9899b/numpy-2.4.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e236dbda4e1d319d681afcbb136c0c4a8e0f1a5c58ceec2adebb547357fe857", size = 16729409, upload-time = "2026-03-09T07:58:47.972Z" }, - { url = "https://files.pythonhosted.org/packages/57/a7/b35835e278c18b85206834b3aa3abe68e77a98769c59233d1f6300284781/numpy-2.4.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:4b42639cdde6d24e732ff823a3fa5b701d8acad89c4142bc1d0bd6dc85200ba5", size = 12504685, upload-time = "2026-03-09T07:58:50.525Z" }, +version = "2.4.6" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" }, + { url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" }, + { url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" }, + { url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" }, + { url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" }, + { url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" }, + { url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" }, + { url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" }, + { url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" }, + { url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" }, + { url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" }, + { url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" }, + { url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" }, + { url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" }, + { url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" }, + { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" }, + { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" }, + { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" }, + { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" }, + { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" }, + { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" }, + { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" }, + { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" }, + { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" }, + { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" }, + { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" }, + { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" }, + { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" }, + { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" }, + { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" }, + { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" }, + { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" }, + { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" }, + { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" }, + { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" }, + { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" }, + { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" }, + { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" }, + { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" }, + { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" }, + { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" }, + { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" }, + { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" }, + { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" }, + { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" }, + { url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" }, + { url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" }, + { url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" }, + { url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" }, +] + +[[package]] +name = "numpy" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version == '3.14.*'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*'", +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" }, + { url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" }, + { url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" }, + { url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" }, + { url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" }, + { url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" }, + { url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" }, + { url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" }, + { url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" }, + { url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" }, + { url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" }, + { url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" }, + { url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" }, + { url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" }, + { url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" }, + { url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" }, + { url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" }, + { url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" }, + { url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" }, + { url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" }, + { url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" }, + { url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" }, + { url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" }, + { url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" }, + { url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" }, + { url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" }, + { url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" }, + { url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" }, + { url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" }, + { url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" }, + { url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" }, + { url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" }, + { url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" }, + { url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" }, + { url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" }, + { url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" }, + { url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" }, + { url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" }, + { url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" }, + { url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" }, + { url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" }, + { url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" }, + { url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" }, + { url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" }, + { url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" }, + { url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" }, + { url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" }, ] [[package]] name = "openai" -version = "2.29.0" +version = "3.10.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "anyio" }, - { name = "distro" }, - { name = "httpx" }, + { name = "httpx2" }, { name = "jiter" }, { name = "pydantic" }, { name = "sniffio" }, - { name = "tqdm" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b4/15/203d537e58986b5673e7f232453a2a2f110f22757b15921cbdeea392e520/openai-2.29.0.tar.gz", hash = "sha256:32d09eb2f661b38d3edd7d7e1a2943d1633f572596febe64c0cd370c86d52bec", size = 671128, upload-time = "2026-03-17T17:53:49.599Z" } +sdist = { url = "https://files.pythonhosted.org/packages/24/4e/45c33ece533f9e4252de537c802e2efb68b066befc7b7bbbbc59c4865341/openai-3.10.0.tar.gz", hash = "sha256:c2dc841b944c4720a222f9a92f66a9a122e46968d87ed7857104655054c687ba", size = 1486030, upload-time = "2026-09-09T00:18:25.046Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/b1/35b6f9c8cf9318e3dbb7146cc82dab4cf61182a8d5406fc9b50864362895/openai-2.29.0-py3-none-any.whl", hash = "sha256:b7c5de513c3286d17c5e29b92c4c98ceaf0d775244ac8159aeb1bddf840eb42a", size = 1141533, upload-time = "2026-03-17T17:53:47.348Z" }, + { url = "https://files.pythonhosted.org/packages/dd/50/0d796ada2d1222cfe609cf435ce7a688450735cee401b24737882c9b9612/openai-3.10.0-py3-none-any.whl", hash = "sha256:d1f307d866c7d12872809aeb68b7d1a085b49c2656787eae7c39ba381a41ad0d", size = 1746707, upload-time = "2026-09-09T00:18:22.498Z" }, ] [[package]] @@ -1309,70 +1788,65 @@ wheels = [ [[package]] name = "orjson" -version = "3.11.7" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/53/45/b268004f745ede84e5798b48ee12b05129d19235d0e15267aa57dcdb400b/orjson-3.11.7.tar.gz", hash = "sha256:9b1a67243945819ce55d24a30b59d6a168e86220452d2c96f4d1f093e71c0c49", size = 6144992, upload-time = "2026-02-02T15:38:49.29Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/02/da6cb01fc6087048d7f61522c327edf4250f1683a58a839fdcc435746dd5/orjson-3.11.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9487abc2c2086e7c8eb9a211d2ce8855bae0e92586279d0d27b341d5ad76c85c", size = 228664, upload-time = "2026-02-02T15:37:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/c1/c2/5885e7a5881dba9a9af51bc564e8967225a642b3e03d089289a35054e749/orjson-3.11.7-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:79cacb0b52f6004caf92405a7e1f11e6e2de8bdf9019e4f76b44ba045125cd6b", size = 125344, upload-time = "2026-02-02T15:37:26.92Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1d/4e7688de0a92d1caf600dfd5fb70b4c5bfff51dfa61ac555072ef2d0d32a/orjson-3.11.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c2e85fe4698b6a56d5e2ebf7ae87544d668eb6bde1ad1226c13f44663f20ec9e", size = 128404, upload-time = "2026-02-02T15:37:28.108Z" }, - { url = "https://files.pythonhosted.org/packages/2f/b2/ec04b74ae03a125db7bd69cffd014b227b7f341e3261bf75b5eb88a1aa92/orjson-3.11.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b8d14b71c0b12963fe8a62aac87119f1afdf4cb88a400f61ca5ae581449efcb5", size = 123677, upload-time = "2026-02-02T15:37:30.287Z" }, - { url = "https://files.pythonhosted.org/packages/4c/69/f95bdf960605f08f827f6e3291fe243d8aa9c5c9ff017a8d7232209184c3/orjson-3.11.7-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:91c81ef070c8f3220054115e1ef468b1c9ce8497b4e526cb9f68ab4dc0a7ac62", size = 128950, upload-time = "2026-02-02T15:37:31.595Z" }, - { url = "https://files.pythonhosted.org/packages/a4/1b/de59c57bae1d148ef298852abd31909ac3089cff370dfd4cd84cc99cbc42/orjson-3.11.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:411ebaf34d735e25e358a6d9e7978954a9c9d58cfb47bc6683cdc3964cd2f910", size = 141756, upload-time = "2026-02-02T15:37:32.985Z" }, - { url = "https://files.pythonhosted.org/packages/ee/9e/9decc59f4499f695f65c650f6cfa6cd4c37a3fbe8fa235a0a3614cb54386/orjson-3.11.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a16bcd08ab0bcdfc7e8801d9c4a9cc17e58418e4d48ddc6ded4e9e4b1a94062b", size = 130812, upload-time = "2026-02-02T15:37:34.204Z" }, - { url = "https://files.pythonhosted.org/packages/28/e6/59f932bcabd1eac44e334fe8e3281a92eacfcb450586e1f4bde0423728d8/orjson-3.11.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c0b51672e466fd7e56230ffbae7f1639e18d0ce023351fb75da21b71bc2c960", size = 133444, upload-time = "2026-02-02T15:37:35.446Z" }, - { url = "https://files.pythonhosted.org/packages/f1/36/b0f05c0eaa7ca30bc965e37e6a2956b0d67adb87a9872942d3568da846ae/orjson-3.11.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:136dcd6a2e796dfd9ffca9fc027d778567b0b7c9968d092842d3c323cef88aa8", size = 138609, upload-time = "2026-02-02T15:37:36.657Z" }, - { url = "https://files.pythonhosted.org/packages/b8/03/58ec7d302b8d86944c60c7b4b82975d5161fcce4c9bc8c6cb1d6741b6115/orjson-3.11.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:7ba61079379b0ae29e117db13bda5f28d939766e410d321ec1624afc6a0b0504", size = 408918, upload-time = "2026-02-02T15:37:38.076Z" }, - { url = "https://files.pythonhosted.org/packages/06/3a/868d65ef9a8b99be723bd510de491349618abd9f62c826cf206d962db295/orjson-3.11.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:0527a4510c300e3b406591b0ba69b5dc50031895b0a93743526a3fc45f59d26e", size = 143998, upload-time = "2026-02-02T15:37:39.706Z" }, - { url = "https://files.pythonhosted.org/packages/5b/c7/1e18e1c83afe3349f4f6dc9e14910f0ae5f82eac756d1412ea4018938535/orjson-3.11.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a709e881723c9b18acddcfb8ba357322491ad553e277cf467e1e7e20e2d90561", size = 134802, upload-time = "2026-02-02T15:37:41.002Z" }, - { url = "https://files.pythonhosted.org/packages/d4/0b/ccb7ee1a65b37e8eeb8b267dc953561d72370e85185e459616d4345bab34/orjson-3.11.7-cp311-cp311-win32.whl", hash = "sha256:c43b8b5bab288b6b90dac410cca7e986a4fa747a2e8f94615aea407da706980d", size = 127828, upload-time = "2026-02-02T15:37:42.241Z" }, - { url = "https://files.pythonhosted.org/packages/af/9e/55c776dffda3f381e0f07d010a4f5f3902bf48eaba1bb7684d301acd4924/orjson-3.11.7-cp311-cp311-win_amd64.whl", hash = "sha256:6543001328aa857187f905308a028935864aefe9968af3848401b6fe80dbb471", size = 124941, upload-time = "2026-02-02T15:37:43.444Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8e/424a620fa7d263b880162505fb107ef5e0afaa765b5b06a88312ac291560/orjson-3.11.7-cp311-cp311-win_arm64.whl", hash = "sha256:1ee5cc7160a821dfe14f130bc8e63e7611051f964b463d9e2a3a573204446a4d", size = 126245, upload-time = "2026-02-02T15:37:45.18Z" }, - { url = "https://files.pythonhosted.org/packages/80/bf/76f4f1665f6983385938f0e2a5d7efa12a58171b8456c252f3bae8a4cf75/orjson-3.11.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:bd03ea7606833655048dab1a00734a2875e3e86c276e1d772b2a02556f0d895f", size = 228545, upload-time = "2026-02-02T15:37:46.376Z" }, - { url = "https://files.pythonhosted.org/packages/79/53/6c72c002cb13b5a978a068add59b25a8bdf2800ac1c9c8ecdb26d6d97064/orjson-3.11.7-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:89e440ebc74ce8ab5c7bc4ce6757b4a6b1041becb127df818f6997b5c71aa60b", size = 125224, upload-time = "2026-02-02T15:37:47.697Z" }, - { url = "https://files.pythonhosted.org/packages/2c/83/10e48852865e5dd151bdfe652c06f7da484578ed02c5fca938e3632cb0b8/orjson-3.11.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ede977b5fe5ac91b1dffc0a517ca4542d2ec8a6a4ff7b2652d94f640796342a", size = 128154, upload-time = "2026-02-02T15:37:48.954Z" }, - { url = "https://files.pythonhosted.org/packages/6e/52/a66e22a2b9abaa374b4a081d410edab6d1e30024707b87eab7c734afe28d/orjson-3.11.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b7b1dae39230a393df353827c855a5f176271c23434cfd2db74e0e424e693e10", size = 123548, upload-time = "2026-02-02T15:37:50.187Z" }, - { url = "https://files.pythonhosted.org/packages/de/38/605d371417021359f4910c496f764c48ceb8997605f8c25bf1dfe58c0ebe/orjson-3.11.7-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed46f17096e28fb28d2975834836a639af7278aa87c84f68ab08fbe5b8bd75fa", size = 129000, upload-time = "2026-02-02T15:37:51.426Z" }, - { url = "https://files.pythonhosted.org/packages/44/98/af32e842b0ffd2335c89714d48ca4e3917b42f5d6ee5537832e069a4b3ac/orjson-3.11.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3726be79e36e526e3d9c1aceaadbfb4a04ee80a72ab47b3f3c17fefb9812e7b8", size = 141686, upload-time = "2026-02-02T15:37:52.607Z" }, - { url = "https://files.pythonhosted.org/packages/96/0b/fc793858dfa54be6feee940c1463370ece34b3c39c1ca0aa3845f5ba9892/orjson-3.11.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0724e265bc548af1dedebd9cb3d24b4e1c1e685a343be43e87ba922a5c5fff2f", size = 130812, upload-time = "2026-02-02T15:37:53.944Z" }, - { url = "https://files.pythonhosted.org/packages/dc/91/98a52415059db3f374757d0b7f0f16e3b5cd5976c90d1c2b56acaea039e6/orjson-3.11.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e7745312efa9e11c17fbd3cb3097262d079da26930ae9ae7ba28fb738367cbad", size = 133440, upload-time = "2026-02-02T15:37:55.615Z" }, - { url = "https://files.pythonhosted.org/packages/dc/b6/cb540117bda61791f46381f8c26c8f93e802892830a6055748d3bb1925ab/orjson-3.11.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f904c24bdeabd4298f7a977ef14ca2a022ca921ed670b92ecd16ab6f3d01f867", size = 138386, upload-time = "2026-02-02T15:37:56.814Z" }, - { url = "https://files.pythonhosted.org/packages/63/1a/50a3201c334a7f17c231eee5f841342190723794e3b06293f26e7cf87d31/orjson-3.11.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b9fc4d0f81f394689e0814617aadc4f2ea0e8025f38c226cbf22d3b5ddbf025d", size = 408853, upload-time = "2026-02-02T15:37:58.291Z" }, - { url = "https://files.pythonhosted.org/packages/87/cd/8de1c67d0be44fdc22701e5989c0d015a2adf391498ad42c4dc589cd3013/orjson-3.11.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:849e38203e5be40b776ed2718e587faf204d184fc9a008ae441f9442320c0cab", size = 144130, upload-time = "2026-02-02T15:38:00.163Z" }, - { url = "https://files.pythonhosted.org/packages/0f/fe/d605d700c35dd55f51710d159fc54516a280923cd1b7e47508982fbb387d/orjson-3.11.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4682d1db3bcebd2b64757e0ddf9e87ae5f00d29d16c5cdf3a62f561d08cc3dd2", size = 134818, upload-time = "2026-02-02T15:38:01.507Z" }, - { url = "https://files.pythonhosted.org/packages/e4/e4/15ecc67edb3ddb3e2f46ae04475f2d294e8b60c1825fbe28a428b93b3fbd/orjson-3.11.7-cp312-cp312-win32.whl", hash = "sha256:f4f7c956b5215d949a1f65334cf9d7612dde38f20a95f2315deef167def91a6f", size = 127923, upload-time = "2026-02-02T15:38:02.75Z" }, - { url = "https://files.pythonhosted.org/packages/34/70/2e0855361f76198a3965273048c8e50a9695d88cd75811a5b46444895845/orjson-3.11.7-cp312-cp312-win_amd64.whl", hash = "sha256:bf742e149121dc5648ba0a08ea0871e87b660467ef168a3a5e53bc1fbd64bb74", size = 125007, upload-time = "2026-02-02T15:38:04.032Z" }, - { url = "https://files.pythonhosted.org/packages/68/40/c2051bd19fc467610fed469dc29e43ac65891571138f476834ca192bc290/orjson-3.11.7-cp312-cp312-win_arm64.whl", hash = "sha256:26c3b9132f783b7d7903bf1efb095fed8d4a3a85ec0d334ee8beff3d7a4749d5", size = 126089, upload-time = "2026-02-02T15:38:05.297Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/6e0e52cac5aab51d7b6dcd257e855e1dec1c2060f6b28566c509b4665f62/orjson-3.11.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:1d98b30cc1313d52d4af17d9c3d307b08389752ec5f2e5febdfada70b0f8c733", size = 228390, upload-time = "2026-02-02T15:38:06.8Z" }, - { url = "https://files.pythonhosted.org/packages/a5/29/a77f48d2fc8a05bbc529e5ff481fb43d914f9e383ea2469d4f3d51df3d00/orjson-3.11.7-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:d897e81f8d0cbd2abb82226d1860ad2e1ab3ff16d7b08c96ca00df9d45409ef4", size = 125189, upload-time = "2026-02-02T15:38:08.181Z" }, - { url = "https://files.pythonhosted.org/packages/89/25/0a16e0729a0e6a1504f9d1a13cdd365f030068aab64cec6958396b9969d7/orjson-3.11.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:814be4b49b228cfc0b3c565acf642dd7d13538f966e3ccde61f4f55be3e20785", size = 128106, upload-time = "2026-02-02T15:38:09.41Z" }, - { url = "https://files.pythonhosted.org/packages/66/da/a2e505469d60666a05ab373f1a6322eb671cb2ba3a0ccfc7d4bc97196787/orjson-3.11.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d06e5c5fed5caedd2e540d62e5b1c25e8c82431b9e577c33537e5fa4aa909539", size = 123363, upload-time = "2026-02-02T15:38:10.73Z" }, - { url = "https://files.pythonhosted.org/packages/23/bf/ed73f88396ea35c71b38961734ea4a4746f7ca0768bf28fd551d37e48dd0/orjson-3.11.7-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:31c80ce534ac4ea3739c5ee751270646cbc46e45aea7576a38ffec040b4029a1", size = 129007, upload-time = "2026-02-02T15:38:12.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/3c/b05d80716f0225fc9008fbf8ab22841dcc268a626aa550561743714ce3bf/orjson-3.11.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f50979824bde13d32b4320eedd513431c921102796d86be3eee0b58e58a3ecd1", size = 141667, upload-time = "2026-02-02T15:38:13.398Z" }, - { url = "https://files.pythonhosted.org/packages/61/e8/0be9b0addd9bf86abfc938e97441dcd0375d494594b1c8ad10fe57479617/orjson-3.11.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e54f3808e2b6b945078c41aa8d9b5834b28c50843846e97807e5adb75fa9705", size = 130832, upload-time = "2026-02-02T15:38:14.698Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ec/c68e3b9021a31d9ec15a94931db1410136af862955854ed5dd7e7e4f5bff/orjson-3.11.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a12b80df61aab7b98b490fe9e4879925ba666fccdfcd175252ce4d9035865ace", size = 133373, upload-time = "2026-02-02T15:38:16.109Z" }, - { url = "https://files.pythonhosted.org/packages/d2/45/f3466739aaafa570cc8e77c6dbb853c48bf56e3b43738020e2661e08b0ac/orjson-3.11.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:996b65230271f1a97026fd0e6a753f51fbc0c335d2ad0c6201f711b0da32693b", size = 138307, upload-time = "2026-02-02T15:38:17.453Z" }, - { url = "https://files.pythonhosted.org/packages/e1/84/9f7f02288da1ffb31405c1be07657afd1eecbcb4b64ee2817b6fe0f785fa/orjson-3.11.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ab49d4b2a6a1d415ddb9f37a21e02e0d5dbfe10b7870b21bf779fc21e9156157", size = 408695, upload-time = "2026-02-02T15:38:18.831Z" }, - { url = "https://files.pythonhosted.org/packages/18/07/9dd2f0c0104f1a0295ffbe912bc8d63307a539b900dd9e2c48ef7810d971/orjson-3.11.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:390a1dce0c055ddf8adb6aa94a73b45a4a7d7177b5c584b8d1c1947f2ba60fb3", size = 144099, upload-time = "2026-02-02T15:38:20.28Z" }, - { url = "https://files.pythonhosted.org/packages/a5/66/857a8e4a3292e1f7b1b202883bcdeb43a91566cf59a93f97c53b44bd6801/orjson-3.11.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1eb80451a9c351a71dfaf5b7ccc13ad065405217726b59fdbeadbcc544f9d223", size = 134806, upload-time = "2026-02-02T15:38:22.186Z" }, - { url = "https://files.pythonhosted.org/packages/0a/5b/6ebcf3defc1aab3a338ca777214966851e92efb1f30dc7fc8285216e6d1b/orjson-3.11.7-cp313-cp313-win32.whl", hash = "sha256:7477aa6a6ec6139c5cb1cc7b214643592169a5494d200397c7fc95d740d5fcf3", size = 127914, upload-time = "2026-02-02T15:38:23.511Z" }, - { url = "https://files.pythonhosted.org/packages/00/04/c6f72daca5092e3117840a1b1e88dfc809cc1470cf0734890d0366b684a1/orjson-3.11.7-cp313-cp313-win_amd64.whl", hash = "sha256:b9f95dcdea9d4f805daa9ddf02617a89e484c6985fa03055459f90e87d7a0757", size = 124986, upload-time = "2026-02-02T15:38:24.836Z" }, - { url = "https://files.pythonhosted.org/packages/03/ba/077a0f6f1085d6b806937246860fafbd5b17f3919c70ee3f3d8d9c713f38/orjson-3.11.7-cp313-cp313-win_arm64.whl", hash = "sha256:800988273a014a0541483dc81021247d7eacb0c845a9d1a34a422bc718f41539", size = 126045, upload-time = "2026-02-02T15:38:26.216Z" }, - { url = "https://files.pythonhosted.org/packages/e9/1e/745565dca749813db9a093c5ebc4bac1a9475c64d54b95654336ac3ed961/orjson-3.11.7-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:de0a37f21d0d364954ad5de1970491d7fbd0fb1ef7417d4d56a36dc01ba0c0a0", size = 228391, upload-time = "2026-02-02T15:38:27.757Z" }, - { url = "https://files.pythonhosted.org/packages/46/19/e40f6225da4d3aa0c8dc6e5219c5e87c2063a560fe0d72a88deb59776794/orjson-3.11.7-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:c2428d358d85e8da9d37cba18b8c4047c55222007a84f97156a5b22028dfbfc0", size = 125188, upload-time = "2026-02-02T15:38:29.241Z" }, - { url = "https://files.pythonhosted.org/packages/9d/7e/c4de2babef2c0817fd1f048fd176aa48c37bec8aef53d2fa932983032cce/orjson-3.11.7-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c4bc6c6ac52cdaa267552544c73e486fecbd710b7ac09bc024d5a78555a22f6", size = 128097, upload-time = "2026-02-02T15:38:30.618Z" }, - { url = "https://files.pythonhosted.org/packages/eb/74/233d360632bafd2197f217eee7fb9c9d0229eac0c18128aee5b35b0014fe/orjson-3.11.7-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd0d68edd7dfca1b2eca9361a44ac9f24b078de3481003159929a0573f21a6bf", size = 123364, upload-time = "2026-02-02T15:38:32.363Z" }, - { url = "https://files.pythonhosted.org/packages/79/51/af79504981dd31efe20a9e360eb49c15f06df2b40e7f25a0a52d9ae888e8/orjson-3.11.7-cp314-cp314-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:623ad1b9548ef63886319c16fa317848e465a21513b31a6ad7b57443c3e0dcf5", size = 129076, upload-time = "2026-02-02T15:38:33.68Z" }, - { url = "https://files.pythonhosted.org/packages/67/e2/da898eb68b72304f8de05ca6715870d09d603ee98d30a27e8a9629abc64b/orjson-3.11.7-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6e776b998ac37c0396093d10290e60283f59cfe0fc3fccbd0ccc4bd04dd19892", size = 141705, upload-time = "2026-02-02T15:38:34.989Z" }, - { url = "https://files.pythonhosted.org/packages/c5/89/15364d92acb3d903b029e28d834edb8780c2b97404cbf7929aa6b9abdb24/orjson-3.11.7-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:652c6c3af76716f4a9c290371ba2e390ede06f6603edb277b481daf37f6f464e", size = 130855, upload-time = "2026-02-02T15:38:36.379Z" }, - { url = "https://files.pythonhosted.org/packages/c2/8b/ecdad52d0b38d4b8f514be603e69ccd5eacf4e7241f972e37e79792212ec/orjson-3.11.7-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a56df3239294ea5964adf074c54bcc4f0ccd21636049a2cf3ca9cf03b5d03cf1", size = 133386, upload-time = "2026-02-02T15:38:37.704Z" }, - { url = "https://files.pythonhosted.org/packages/b9/0e/45e1dcf10e17d0924b7c9162f87ec7b4ca79e28a0548acf6a71788d3e108/orjson-3.11.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bda117c4148e81f746655d5a3239ae9bd00cb7bc3ca178b5fc5a5997e9744183", size = 138295, upload-time = "2026-02-02T15:38:39.096Z" }, - { url = "https://files.pythonhosted.org/packages/63/d7/4d2e8b03561257af0450f2845b91fbd111d7e526ccdf737267108075e0ba/orjson-3.11.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:23d6c20517a97a9daf1d48b580fcdc6f0516c6f4b5038823426033690b4d2650", size = 408720, upload-time = "2026-02-02T15:38:40.634Z" }, - { url = "https://files.pythonhosted.org/packages/78/cf/d45343518282108b29c12a65892445fc51f9319dc3c552ceb51bb5905ed2/orjson-3.11.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:8ff206156006da5b847c9304b6308a01e8cdbc8cce824e2779a5ba71c3def141", size = 144152, upload-time = "2026-02-02T15:38:42.262Z" }, - { url = "https://files.pythonhosted.org/packages/a9/3a/d6001f51a7275aacd342e77b735c71fa04125a3f93c36fee4526bc8c654e/orjson-3.11.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:962d046ee1765f74a1da723f4b33e3b228fe3a48bd307acce5021dfefe0e29b2", size = 134814, upload-time = "2026-02-02T15:38:43.627Z" }, - { url = "https://files.pythonhosted.org/packages/1d/d3/f19b47ce16820cc2c480f7f1723e17f6d411b3a295c60c8ad3aa9ff1c96a/orjson-3.11.7-cp314-cp314-win32.whl", hash = "sha256:89e13dd3f89f1c38a9c9eba5fbf7cdc2d1feca82f5f290864b4b7a6aac704576", size = 127997, upload-time = "2026-02-02T15:38:45.06Z" }, - { url = "https://files.pythonhosted.org/packages/12/df/172771902943af54bf661a8d102bdf2e7f932127968080632bda6054b62c/orjson-3.11.7-cp314-cp314-win_amd64.whl", hash = "sha256:845c3e0d8ded9c9271cd79596b9b552448b885b97110f628fb687aee2eed11c1", size = 124985, upload-time = "2026-02-02T15:38:46.388Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1c/f2a8d8a1b17514660a614ce5f7aac74b934e69f5abc2700cc7ced882a009/orjson-3.11.7-cp314-cp314-win_arm64.whl", hash = "sha256:4a2e9c5be347b937a2e0203866f12bba36082e89b402ddb9e927d5822e43088d", size = 126038, upload-time = "2026-02-02T15:38:47.703Z" }, +version = "3.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/742fb1f62b825f2c010697eaf4e828004bc2a81e7e806666989c132c7c42/orjson-3.12.0.tar.gz", hash = "sha256:d14203fb1aae2ad9b3d52f8a0e82aeb10197ef1c9bc61da7f358bd70b00123d5", size = 4142915, upload-time = "2026-08-14T16:13:30.607Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/1a/a7075a8e8b0d3f5097d17ac3099017104b6b7b42012041147995d5b2da05/orjson-3.12.0-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a94f0f0c6fcbb2b5bd9734c57a489c7584a732bbdf04a39e8c83b861e9d03e92", size = 223409, upload-time = "2026-08-14T16:12:12.654Z" }, + { url = "https://files.pythonhosted.org/packages/05/34/c2eb3b2900e5597db7841a4c6416ac2d90081bd956b02d4dd1833fa2b96b/orjson-3.12.0-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:a696529ec96a90d9a5f9570207efe403c8b08f8e4aa2783ee3403511e2fdfa10", size = 124015, upload-time = "2026-08-14T16:12:14.025Z" }, + { url = "https://files.pythonhosted.org/packages/1c/df/b49081766a75b6a37b3d33bdc0a39e492abab8441dd25e3e1998e7b83fcb/orjson-3.12.0-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:e4ac5059baab4b3acbd99485de019ff8cda0fdf34b61fa74f7197a53db78bfe8", size = 113471, upload-time = "2026-08-14T16:12:15.81Z" }, + { url = "https://files.pythonhosted.org/packages/48/d4/58ea28eeef95c2a27358ed927380a621162cf20bd740bbccf9c3f09a200a/orjson-3.12.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8e29957429c35bbb5a185a119c523aa2428b7bbf1a293724c7b9375ed8f892a3", size = 129998, upload-time = "2026-08-14T16:12:17.503Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f4/1e82aa2efc9916422d804697876ce433c907a1abd7c7e5c6d3d48565e5f9/orjson-3.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dce0166feb0a737ab84f598c9a338cbc0b764a036617aa686194f53c7eba0c3e", size = 130891, upload-time = "2026-08-14T16:12:18.762Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e1/15169e9d22b59a406264f99d6db387c0b0b12b6357a8a0169917c2a713eb/orjson-3.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9caf3d09f47c3c70c4451ada20ef9bc4a4cdffa26f49862cf0a253b329aae2d5", size = 131285, upload-time = "2026-08-14T16:12:20.251Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3a/763dbd426290d044ec3e615a05e70adb6d8b6f95bf17dc355c0081a5e8b6/orjson-3.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b9dca132b1fda5565088e65a6b6e742285e0aeceb6fae549fa8863e16c7d3998", size = 135707, upload-time = "2026-08-14T16:12:21.652Z" }, + { url = "https://files.pythonhosted.org/packages/04/d1/3b2038ed168d22e14182ed715d6963f9c073a83a2ba43cfe918a4fc43c64/orjson-3.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a791f793b287bbc135b8e87c34e35c8bfc693e2a8a620fab1ae682b925f9a32e", size = 127669, upload-time = "2026-08-14T16:12:22.926Z" }, + { url = "https://files.pythonhosted.org/packages/88/ae/b84b3d3e65f5629ada0edcb1d2bccc55d7c5f89d8b981537ecdc3d6f31ec/orjson-3.12.0-cp311-cp311-win32.whl", hash = "sha256:31ed278a36304390adc3eec5d7f6fd593a7c3e99e5a06cd07866396c4b1b4710", size = 128043, upload-time = "2026-08-14T16:12:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/35/24/2ed0e6f51ea3d0af45d807233a851175af75bec83ef5fd0d6a2601904ec0/orjson-3.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:fb2539159dfe8d371914f354360fa50e4a577cc89222a3828b9650a5e5040252", size = 122084, upload-time = "2026-08-14T16:12:25.813Z" }, + { url = "https://files.pythonhosted.org/packages/21/dd/95d25fcfbc9471799ef6bb01c552d64ee5cde93ee40ba2f423dd3442c708/orjson-3.12.0-cp311-cp311-win_arm64.whl", hash = "sha256:61318b6de893c7a9d9f3e5ecbadccbfc26a7eb417ccc7bbf0771de3b4d72f868", size = 127035, upload-time = "2026-08-14T16:12:27.201Z" }, + { url = "https://files.pythonhosted.org/packages/be/4a/295da39c651c2faac8bd351a2a346f0fdedd9d50b847ee9dfc27d2207ef6/orjson-3.12.0-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:aa3e43a6846e91d7bde3d5a9c66090fcd8744f569a9b6cffc5e1ca38f6a461c0", size = 223427, upload-time = "2026-08-14T16:12:28.525Z" }, + { url = "https://files.pythonhosted.org/packages/29/98/758cf90fbeaaafb7f8141bfac75a432099959f3a2f5db93a412e876415d8/orjson-3.12.0-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:11edb4660a6680abee9788a3a9072208a2c96538cc1322bd79542065229d8e54", size = 123725, upload-time = "2026-08-14T16:12:30.013Z" }, + { url = "https://files.pythonhosted.org/packages/32/b5/5b934d251f8651f7e41df180ad0c57a6e1cabe15c7bd331638413a50ebc9/orjson-3.12.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2d3a9da945a4d96ae758fdaaca56742e6b73b6fd554c5d8876f252a6dad70b83", size = 113375, upload-time = "2026-08-14T16:12:31.209Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d2/37efb5b12a176ce3ced29f4144f20da57d02757f78ce549637dc1b4e1fc8/orjson-3.12.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:92ffc09e07233a6ab6d4e067f7841edcbcc134cb4812155cf171ea5255a421d7", size = 129983, upload-time = "2026-08-14T16:12:32.721Z" }, + { url = "https://files.pythonhosted.org/packages/50/22/0644b87c73f13e0092df8f35a1fe280d991e5e90072087411e0dd7e44e0c/orjson-3.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf44e374aadde77b1f6109f1030be51433eb61984379852766b6f4e187db7b1e", size = 130629, upload-time = "2026-08-14T16:12:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/8c/57/80b986ebfecd9c6a177ddf1c2319717f0cd8feffb2b78946595a18a2fc88/orjson-3.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1192a7021b6d071aaf909864f6e924d6a2675ca360485b972b8401749311750b", size = 131245, upload-time = "2026-08-14T16:12:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/80/3d/75c5ac5a69161f44492a68fbdde66f4cc4ce48cd5e1fb05918e46f0c8848/orjson-3.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:53c0c474a9d9aff9aebfc0c88de1f28f843d940e6e3a80729abdf6a20274356f", size = 135397, upload-time = "2026-08-14T16:12:37.128Z" }, + { url = "https://files.pythonhosted.org/packages/71/93/4d71f2df314a97ff0d27a4559bf5888fc8406e3c6dec90e92291e3511215/orjson-3.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:532ff8cd4bd59a327a953a7dcde922c7fc25b85e29721bb8633265430d3a3873", size = 127693, upload-time = "2026-08-14T16:12:38.627Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/0dbc6be5adfd1730491072fb60beb6bcdf5d7b2596ee41b7fc2e298bfc09/orjson-3.12.0-cp312-cp312-win32.whl", hash = "sha256:a6cf4b18e7de173f209f2084ffbd736dd72389a396326ee80a7022168be232e5", size = 128000, upload-time = "2026-08-14T16:12:39.954Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c9/97b1ce0112ebf5e949c775ed5b1755e562233179f3584579673cc24d6378/orjson-3.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:010811c1b69773450a01cef97727a67b223242f350b77d4ca000e59a9ef2155a", size = 122106, upload-time = "2026-08-14T16:12:41.324Z" }, + { url = "https://files.pythonhosted.org/packages/a8/6a/facd8b312e4a0d3a7fa978c7e15821f74a336adf1d65529faec33b48e18b/orjson-3.12.0-cp312-cp312-win_arm64.whl", hash = "sha256:ad29eece0c601737f2a60edc2752a84e7a0785df3efb62e3012834700a5afe0d", size = 126869, upload-time = "2026-08-14T16:12:42.651Z" }, + { url = "https://files.pythonhosted.org/packages/54/cb/d7b78218a987eb8a8ce4eeae0286b1bb679333eb631ea0eeaf6371680bfc/orjson-3.12.0-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9a36ec60f1796f9a3f13e3b98390295e17a1c7c10155b448d264098bf9ee5900", size = 223397, upload-time = "2026-08-14T16:12:44.003Z" }, + { url = "https://files.pythonhosted.org/packages/f8/4a/bc87c45e7ec639d35ebefd62618e01939531ac8e171426606a01bda05914/orjson-3.12.0-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:ad0422b92d5195443a39f80c3bcf731cc2e00f153bd32063a47b73b057bd0f03", size = 123662, upload-time = "2026-08-14T16:12:45.433Z" }, + { url = "https://files.pythonhosted.org/packages/94/ee/c9a4ff3f2dbedbbe9e635d0fa72c8866adede09b6335ef9644f53752f0d8/orjson-3.12.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:5a0fdbc216388f653d3752ff310e710f59253bd4ed6a2bfb3f4f06b84714bbd8", size = 113374, upload-time = "2026-08-14T16:12:46.755Z" }, + { url = "https://files.pythonhosted.org/packages/75/09/3f330a026a796c8b4c97a6f429652a5e912e7065039bf96ed25e42aa7b25/orjson-3.12.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:2eb5c56e534127b2b8fa38d2363c8b1b8190367ee0d1d16c041517d880843b94", size = 130029, upload-time = "2026-08-14T16:12:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/7d/40/094cc53126a3d22f76cdf83b6ea67338bed01d774037621a785aa8e6e5ea/orjson-3.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:784106539f4b9d4b930e0b4eb8d45168507dae001945e71b4675a367f1e5e806", size = 130528, upload-time = "2026-08-14T16:12:49.362Z" }, + { url = "https://files.pythonhosted.org/packages/bc/74/89bb236deb9565f99434b13052bb40ddfcce4adf3afbfa3132ee7e421468/orjson-3.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c680706fc8396d95e7c4c1f9482563f552137aef91b57237a3ad5aaf64629df", size = 131075, upload-time = "2026-08-14T16:12:50.692Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ac/1176360d762c01b5bd34acd56fc098e936c491363d8b6b397ad4aa475547/orjson-3.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:83445adc40cba26d6d621185a45128ce455b766af368cad2ab64b970603a7978", size = 135321, upload-time = "2026-08-14T16:12:52.114Z" }, + { url = "https://files.pythonhosted.org/packages/7a/02/bbd881c8b9276d50b998de38b4e97de8ace1aac940b0ee545aedbf65ed00/orjson-3.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:644d005bc82f917337a95ce270c9f6f92f9834c2bed7b1477572f8db00784222", size = 127472, upload-time = "2026-08-14T16:12:53.517Z" }, + { url = "https://files.pythonhosted.org/packages/8e/02/a0934d7503e6dcbedd6afac3e7f3f8597fd09389949ad94d0f7540e9dbca/orjson-3.12.0-cp313-cp313-win32.whl", hash = "sha256:d8e78d3d93705e3d27cc17cdb209e44d7a8ea203010cac6ce9c7ffc1ae1996f1", size = 128000, upload-time = "2026-08-14T16:12:55.14Z" }, + { url = "https://files.pythonhosted.org/packages/52/87/69f98f8d40faff103a965a5fbb83f08241b01beaf92badb5413fbc9358cc/orjson-3.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b85931be5b6763c31283805c9bdaae1ca03ad9f6f12a15f1cbf6745b907932c2", size = 121841, upload-time = "2026-08-14T16:12:56.507Z" }, + { url = "https://files.pythonhosted.org/packages/e6/07/b83046a4e3cadcc0987d0f160696107c4af706a619b56e4ad01940cadadf/orjson-3.12.0-cp313-cp313-win_arm64.whl", hash = "sha256:6a31348d7dfa64cd9c78bd1f510ff44c48fe64d71094e6b90e364dba3b55949e", size = 126765, upload-time = "2026-08-14T16:12:57.806Z" }, + { url = "https://files.pythonhosted.org/packages/12/9d/3931253e6f3148abf2cbe14830367042a4806b362ea520df2303db188fb9/orjson-3.12.0-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9e6fee342a48760e854d743e7a81534d8e2925a6f46e09f750cf56b50fd1de5d", size = 223391, upload-time = "2026-08-14T16:12:59.184Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0e/b4a4f1e305367245877b967a0bad70fcf001d77c54ac4339a120b66fdae4/orjson-3.12.0-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:8c3bb86dd10f39b3fbf434b7d5dc7cac77d6fc8ac572ae30a10731ede2c4b647", size = 123659, upload-time = "2026-08-14T16:13:00.548Z" }, + { url = "https://files.pythonhosted.org/packages/96/f3/6782c6fa85e2702bc66be183c3b421486167dcf266ee4dc1403fe3824870/orjson-3.12.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.whl", hash = "sha256:2bb3ce43203936072dd8b4917b01d3aecfc02329bfb42510cb7cfb24708adc9c", size = 113337, upload-time = "2026-08-14T16:13:02.009Z" }, + { url = "https://files.pythonhosted.org/packages/bf/79/b32ab64bacda9d0fa4942ef483bd03cabf0eaf2be819ca9fb7ff610c559d/orjson-3.12.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:6a2a79c89984dc719817d388c8709e0efc2a2795a934eaa746b4882eb6045adc", size = 130112, upload-time = "2026-08-14T16:13:03.404Z" }, + { url = "https://files.pythonhosted.org/packages/ee/49/6e6142999ca01509219be5e5a9c338a3e5ea011f63e91ff473fbbf3734ed/orjson-3.12.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f06dd838d1e07d9b1de0932ec0485ec92c4d5f5d1ad4817a656268c3e88be1e1", size = 130520, upload-time = "2026-08-14T16:13:04.798Z" }, + { url = "https://files.pythonhosted.org/packages/49/d0/3745af0a4cc9867784f29722929cec4d10bd1c877cd754b01ba6d96eb21a/orjson-3.12.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c6b11be792c3d2c6a4be2af4ebf97a68d0bf5f580aca6e86a418a354f6cc846a", size = 131053, upload-time = "2026-08-14T16:13:06.14Z" }, + { url = "https://files.pythonhosted.org/packages/c3/f4/6fe5a22fa478fffb190e65c338c84df5c311ef597b363150a17cc57063c0/orjson-3.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:477ecaf6b9f88f873341b91fcc736119ca81b5e002a9f7f308ff5b4f2ce2a70e", size = 135321, upload-time = "2026-08-14T16:13:07.544Z" }, + { url = "https://files.pythonhosted.org/packages/ff/41/b1b0ec30289646a81a76e2dbaae2686b96fcccb7cb0323dc1dd78cbc7875/orjson-3.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f3c0683136acdc29afdf88a5bc2f7d3d0e34087788d1d63c0144b805a87a196f", size = 127485, upload-time = "2026-08-14T16:13:08.88Z" }, + { url = "https://files.pythonhosted.org/packages/bf/2b/277404bdcc21c93b112b963655b76443ebfe828f8a3ff1de7d90f8850eb3/orjson-3.12.0-cp314-cp314-win32.whl", hash = "sha256:d39f3f5c3927e2dc0913fe5bbc1a2f6b1b9d1bba1de6358340d0ad0d0c00ca92", size = 128048, upload-time = "2026-08-14T16:13:10.305Z" }, + { url = "https://files.pythonhosted.org/packages/41/2b/395b36fa2b4ce7af70b651d715e88f80d884b2c2b14a6b53e84d554fb5f0/orjson-3.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:0b1ac5bf6609b2716c7954011c5fef6254922df029f45d032ee4ebf5d363cbed", size = 121858, upload-time = "2026-08-14T16:13:11.634Z" }, + { url = "https://files.pythonhosted.org/packages/ea/a3/833e895ff452859eebe75093d26691fe9108f1a7a6a08435d7a5780ea652/orjson-3.12.0-cp314-cp314-win_arm64.whl", hash = "sha256:50fae885cb073eac7556353ff3df93312b0d5137b0a5056b2bb63f97ed9a93c7", size = 126749, upload-time = "2026-08-14T16:13:13.117Z" }, + { url = "https://files.pythonhosted.org/packages/58/64/99c8947ece10c17176af9aae85c4948f1d109da77440ec14d87239efaf73/orjson-3.12.0-cp315-cp315-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:01efac2074fffb4cb1ea3fab7861e9d0f2a26913854a972f5ac760525dbdaf6e", size = 223398, upload-time = "2026-08-14T16:13:14.694Z" }, + { url = "https://files.pythonhosted.org/packages/3e/30/cf983fe09f2731420fda097a9f7ef4343f47fa216c228961ad8f6da44f3d/orjson-3.12.0-cp315-cp315-macosx_15_0_arm64.whl", hash = "sha256:ed4ca42bd55955aa34deedcfdfd0e0c31abf51143aae158ae2bc3520b626e517", size = 123655, upload-time = "2026-08-14T16:13:16.221Z" }, + { url = "https://files.pythonhosted.org/packages/11/50/9cb8ae73fa4749dbbc20f617004213b5ff01c20aaeec34c3f31124f2c1d8/orjson-3.12.0-cp315-cp315-manylinux_2_39_aarch64.whl", hash = "sha256:40f92192227505acca4e2533ce565f8e6b9535f7d0d09b0968452f18b7376b38", size = 130515, upload-time = "2026-08-14T16:13:17.601Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0a/adb6ce1a5b5fbf9cb1790f9961bb668a0dd5429aadaf6cee044724681795/orjson-3.12.0-cp315-cp315-manylinux_2_39_armv7l.whl", hash = "sha256:33efefcf5d88eaf400b47e2eba02f91f319bb9951be61ca500b7d536d3f2079d", size = 113327, upload-time = "2026-08-14T16:13:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/51/5c/d17f61581d8dbdde7048f87a330fa24915edec38db4d72b381fec14fbb56/orjson-3.12.0-cp315-cp315-manylinux_2_39_i686.whl", hash = "sha256:8e386b0bc0ddd7cd2056f884b5a0af33592bd01ac66a7ca4b42a65a7e7774a13", size = 130105, upload-time = "2026-08-14T16:13:20.317Z" }, + { url = "https://files.pythonhosted.org/packages/9f/b7/938befcf33bee4704a92ecec6a2731224c539d939bf9429fd39396d28931/orjson-3.12.0-cp315-cp315-manylinux_2_39_x86_64.whl", hash = "sha256:58c58e1de0006ffb580368d6793c36c7b0b021db066479cf281bf5061e732328", size = 131049, upload-time = "2026-08-14T16:13:21.719Z" }, + { url = "https://files.pythonhosted.org/packages/b0/15/cfa2021d64d5aa8bb5c9f604ef375e00ec8b657651b5dd650b1b7ad13df1/orjson-3.12.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:08231552159be266a7269555bd9f7c016aee7d9ad6dab06eb58796c5ccb7101c", size = 135320, upload-time = "2026-08-14T16:13:23.415Z" }, + { url = "https://files.pythonhosted.org/packages/1a/50/3e75dfe357c1e8f9e287c7a5740260ef15bd23a5299eae8d0835dcad5375/orjson-3.12.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:a15f9a891bce5f5cc5d210e3ad8614d4d1b489a56448c099d6d2a7168b2d954a", size = 127488, upload-time = "2026-08-14T16:13:24.791Z" }, + { url = "https://files.pythonhosted.org/packages/11/a6/79aed402eb3ab284dc5b4791a7ad62c5875127de01b8e3f04bd92d551298/orjson-3.12.0-cp315-cp315-win32.whl", hash = "sha256:03091c8a64db4be38746597ceea68f33c238e27acd9bfe99fb59420224ae7a55", size = 128048, upload-time = "2026-08-14T16:13:26.217Z" }, + { url = "https://files.pythonhosted.org/packages/64/f7/2723e264aab7248c1ed6ecaad8e5d0cb866c0cffde75442102ffa7491aba/orjson-3.12.0-cp315-cp315-win_amd64.whl", hash = "sha256:2b7bcefb9f40fa242fa6b06377232c048e655747790829609168c01162f60578", size = 121860, upload-time = "2026-08-14T16:13:27.577Z" }, + { url = "https://files.pythonhosted.org/packages/82/56/630c9113ec8996778f1f0304b364b091b9a9db5fef5fdc17cca622f5ea24/orjson-3.12.0-cp315-cp315-win_arm64.whl", hash = "sha256:859fc4196855890150bb08e649b30d2c93b249b3e3edd0d3bb2231abf8aa8adc", size = 126754, upload-time = "2026-08-14T16:13:28.962Z" }, ] [[package]] @@ -1425,11 +1899,20 @@ wheels = [ [[package]] name = "packaging" -version = "26.0" +version = "26.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, ] [[package]] @@ -1452,122 +1935,134 @@ wheels = [ [[package]] name = "prompt-toolkit" -version = "3.0.52" +version = "3.0.53" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "wcwidth" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/96/06e01a7b38dce6fe1db213e061a4602dd6032a8a97ef6c1a862537732421/prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855", size = 434198, upload-time = "2025-08-27T15:24:02.057Z" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/03/0d3ce49e2505ae70cf43bc5bb3033955d2fc9f932163e84dc0779cc47f48/prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955", size = 391431, upload-time = "2025-08-27T15:23:59.498Z" }, + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, ] [[package]] name = "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, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { 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, upload-time = "2025-10-08T19:46:24.597Z" }, - { 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, upload-time = "2025-10-08T19:46:25.733Z" }, - { 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, upload-time = "2025-10-08T19:46:27.304Z" }, - { 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, upload-time = "2025-10-08T19:46:28.62Z" }, - { 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, upload-time = "2025-10-08T19:46:30.358Z" }, - { 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, upload-time = "2025-10-08T19:46:32.607Z" }, - { 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, upload-time = "2025-10-08T19:46:33.969Z" }, - { 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, upload-time = "2025-10-08T19:46:35.309Z" }, - { 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, upload-time = "2025-10-08T19:46:36.993Z" }, - { 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, upload-time = "2025-10-08T19:46:38.398Z" }, - { 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, upload-time = "2025-10-08T19:46:39.732Z" }, - { 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, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { 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, upload-time = "2025-10-08T19:46:46.075Z" }, - { 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, upload-time = "2025-10-08T19:46:47.23Z" }, - { 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, upload-time = "2025-10-08T19:46:48.384Z" }, - { 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, upload-time = "2025-10-08T19:46:50.055Z" }, - { 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, upload-time = "2025-10-08T19:46:51.815Z" }, - { 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, upload-time = "2025-10-08T19:46:53.208Z" }, - { 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, upload-time = "2025-10-08T19:46:54.511Z" }, - { 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, upload-time = "2025-10-08T19:46:56.212Z" }, - { 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, upload-time = "2025-10-08T19:46:57.595Z" }, - { 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, upload-time = "2025-10-08T19:46:59.067Z" }, - { 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, upload-time = "2025-10-08T19:47:00.544Z" }, - { 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, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { 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, upload-time = "2025-10-08T19:47:07.648Z" }, - { 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, upload-time = "2025-10-08T19:47:08.851Z" }, - { 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, upload-time = "2025-10-08T19:47:09.982Z" }, - { 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, upload-time = "2025-10-08T19:47:11.319Z" }, - { 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, upload-time = "2025-10-08T19:47:13.146Z" }, - { 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, upload-time = "2025-10-08T19:47:14.913Z" }, - { 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, upload-time = "2025-10-08T19:47:16.277Z" }, - { 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, upload-time = "2025-10-08T19:47:17.962Z" }, - { 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, upload-time = "2025-10-08T19:47:19.355Z" }, - { 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, upload-time = "2025-10-08T19:47:21.338Z" }, - { 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, upload-time = "2025-10-08T19:47:23.059Z" }, - { 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, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { 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, upload-time = "2025-10-08T19:47:29.445Z" }, - { 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, upload-time = "2025-10-08T19:47:30.579Z" }, - { 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, upload-time = "2025-10-08T19:47:31.79Z" }, - { 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, upload-time = "2025-10-08T19:47:33.481Z" }, - { 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, upload-time = "2025-10-08T19:47:34.906Z" }, - { 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, upload-time = "2025-10-08T19:47:36.338Z" }, - { 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, upload-time = "2025-10-08T19:47:37.692Z" }, - { 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, upload-time = "2025-10-08T19:47:39.659Z" }, - { 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, upload-time = "2025-10-08T19:47:41.084Z" }, - { 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, upload-time = "2025-10-08T19:47:42.51Z" }, - { 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, upload-time = "2025-10-08T19:47:43.927Z" }, - { 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, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { 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, upload-time = "2025-10-08T19:47:51.051Z" }, - { 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, upload-time = "2025-10-08T19:47:52.594Z" }, - { 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, upload-time = "2025-10-08T19:47:54.073Z" }, - { 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, upload-time = "2025-10-08T19:47:55.715Z" }, - { 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, upload-time = "2025-10-08T19:47:57.499Z" }, - { 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, upload-time = "2025-10-08T19:47:59.317Z" }, - { 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, upload-time = "2025-10-08T19:48:00.67Z" }, - { 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, upload-time = "2025-10-08T19:48:02.604Z" }, - { 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, upload-time = "2025-10-08T19:48:04.499Z" }, - { 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, upload-time = "2025-10-08T19:48:06.213Z" }, - { 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, upload-time = "2025-10-08T19:48:08.432Z" }, - { 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, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { 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, upload-time = "2025-10-08T19:48:15.16Z" }, - { 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, upload-time = "2025-10-08T19:48:16.424Z" }, - { 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, upload-time = "2025-10-08T19:48:17.577Z" }, - { 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, upload-time = "2025-10-08T19:48:18.901Z" }, - { 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, upload-time = "2025-10-08T19:48:20.762Z" }, - { 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, upload-time = "2025-10-08T19:48:22.592Z" }, - { 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, upload-time = "2025-10-08T19:48:23.947Z" }, - { 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, upload-time = "2025-10-08T19:48:25.656Z" }, - { 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, upload-time = "2025-10-08T19:48:27.207Z" }, - { 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, upload-time = "2025-10-08T19:48:28.65Z" }, - { 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, upload-time = "2025-10-08T19:48:30.133Z" }, - { 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, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] name = "pyasn1" -version = "0.6.3" +version = "0.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/5c/5f/6583902b6f79b399c9c40674ac384fd9cd77805f9e6205075f828ef11fb2/pyasn1-0.6.3.tar.gz", hash = "sha256:697a8ecd6d98891189184ca1fa05d1bb00e2f84b5977c481452050549c8a72cf", size = 148685, upload-time = "2026-03-17T01:06:53.382Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/a0/7d793dce3fa811fe047d6ae2431c672364b462850c6235ae306c0efd025f/pyasn1-0.6.3-py3-none-any.whl", hash = "sha256:a80184d120f0864a52a073acc6fc642847d0be408e7c7252f31390c0f4eadcde", size = 83997, upload-time = "2026-03-17T01:06:52.036Z" }, + { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, ] [[package]] @@ -1593,7 +2088,7 @@ wheels = [ [[package]] name = "pydantic" -version = "2.12.5" +version = "2.13.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "annotated-types" }, @@ -1601,120 +2096,125 @@ dependencies = [ { 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, upload-time = "2025-11-26T15:11:46.471Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/ef/fc4f868f4e2cee79f863883abffceff107875f569b848507319842d2a681/pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08", size = 845750, upload-time = "2026-08-28T14:04:00.916Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, + { url = "https://files.pythonhosted.org/packages/eb/47/c95ffc2009878c7aac0c5e08528022dcb885933252a88b5f170058014464/pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73", size = 472589, upload-time = "2026-08-28T14:03:59.136Z" }, ] [[package]] name = "pydantic-core" -version = "2.41.5" +version = "2.46.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, upload-time = "2025-11-04T13:43:49.098Z" } -wheels = [ - { 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, upload-time = "2025-11-04T13:39:31.373Z" }, - { 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, upload-time = "2025-11-04T13:39:32.897Z" }, - { 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, upload-time = "2025-11-04T13:39:34.469Z" }, - { 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, upload-time = "2025-11-04T13:39:36.053Z" }, - { 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, upload-time = "2025-11-04T13:39:37.753Z" }, - { 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, upload-time = "2025-11-04T13:39:40.94Z" }, - { 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, upload-time = "2025-11-04T13:39:42.523Z" }, - { 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, upload-time = "2025-11-04T13:39:44.553Z" }, - { 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, upload-time = "2025-11-04T13:39:46.238Z" }, - { 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, upload-time = "2025-11-04T13:39:48.002Z" }, - { 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, upload-time = "2025-11-04T13:39:49.705Z" }, - { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, - { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, - { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, - { 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, upload-time = "2025-11-04T13:39:58.079Z" }, - { 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, upload-time = "2025-11-04T13:39:59.956Z" }, - { 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, upload-time = "2025-11-04T13:40:02.241Z" }, - { 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, upload-time = "2025-11-04T13:40:04.401Z" }, - { 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, upload-time = "2025-11-04T13:40:06.072Z" }, - { 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, upload-time = "2025-11-04T13:40:07.835Z" }, - { 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, upload-time = "2025-11-04T13:40:09.804Z" }, - { 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, upload-time = "2025-11-04T13:40:12.004Z" }, - { 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, upload-time = "2025-11-04T13:40:13.868Z" }, - { 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, upload-time = "2025-11-04T13:40:15.672Z" }, - { 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, upload-time = "2025-11-04T13:40:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, - { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, - { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, - { 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, upload-time = "2025-11-04T13:40:25.248Z" }, - { 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, upload-time = "2025-11-04T13:40:27.099Z" }, - { 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, upload-time = "2025-11-04T13:40:29.806Z" }, - { 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, upload-time = "2025-11-04T13:40:33.544Z" }, - { 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, upload-time = "2025-11-04T13:40:35.479Z" }, - { 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, upload-time = "2025-11-04T13:40:37.436Z" }, - { 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, upload-time = "2025-11-04T13:40:40.289Z" }, - { 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, upload-time = "2025-11-04T13:40:42.809Z" }, - { 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, upload-time = "2025-11-04T13:40:44.752Z" }, - { 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, upload-time = "2025-11-04T13:40:46.66Z" }, - { 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, upload-time = "2025-11-04T13:40:48.575Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, - { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, - { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, - { 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, upload-time = "2025-11-04T13:40:56.68Z" }, - { 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, upload-time = "2025-11-04T13:40:58.807Z" }, - { 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, upload-time = "2025-11-04T13:41:00.853Z" }, - { 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, upload-time = "2025-11-04T13:41:03.504Z" }, - { 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, upload-time = "2025-11-04T13:41:05.804Z" }, - { 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, upload-time = "2025-11-04T13:41:07.809Z" }, - { 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, upload-time = "2025-11-04T13:41:09.827Z" }, - { 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, upload-time = "2025-11-04T13:41:12.379Z" }, - { 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, upload-time = "2025-11-04T13:41:14.627Z" }, - { 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, upload-time = "2025-11-04T13:41:16.868Z" }, - { 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, upload-time = "2025-11-04T13:41:18.934Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, - { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, - { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, - { 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, upload-time = "2025-11-04T13:41:28.569Z" }, - { 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, upload-time = "2025-11-04T13:41:31.055Z" }, - { 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, upload-time = "2025-11-04T13:41:33.21Z" }, - { 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, upload-time = "2025-11-04T13:41:35.508Z" }, - { 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, upload-time = "2025-11-04T13:41:37.732Z" }, - { 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, upload-time = "2025-11-04T13:41:40Z" }, - { 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, upload-time = "2025-11-04T13:41:42.323Z" }, - { 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, upload-time = "2025-11-04T13:41:45.221Z" }, - { 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, upload-time = "2025-11-04T13:41:47.474Z" }, - { 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, upload-time = "2025-11-04T13:41:49.992Z" }, - { 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, upload-time = "2025-11-04T13:41:54.079Z" }, - { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, - { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, - { 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, upload-time = "2025-11-04T13:42:39.557Z" }, - { 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, upload-time = "2025-11-04T13:42:42.169Z" }, - { 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, upload-time = "2025-11-04T13:42:44.564Z" }, - { 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, upload-time = "2025-11-04T13:42:47.156Z" }, - { 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, upload-time = "2025-11-04T13:42:49.689Z" }, - { 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, upload-time = "2025-11-04T13:42:52.215Z" }, - { 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, upload-time = "2025-11-04T13:42:56.483Z" }, - { 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, upload-time = "2025-11-04T13:42:59.471Z" }, - { 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, upload-time = "2025-11-04T13:43:25.97Z" }, - { 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, upload-time = "2025-11-04T13:43:28.763Z" }, - { 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, upload-time = "2025-11-04T13:43:31.71Z" }, - { 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, upload-time = "2025-11-04T13:43:34.744Z" }, - { 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, upload-time = "2025-11-04T13:43:37.701Z" }, - { 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, upload-time = "2025-11-04T13:43:40.406Z" }, - { 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, upload-time = "2025-11-04T13:43:43.602Z" }, - { 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, upload-time = "2025-11-04T13:43:46.64Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/af/f9/8a06bea35ef8daf588f707784c973a7046e0034c8d8cfb08828eeffb8b75/pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc", size = 472262, upload-time = "2026-08-28T10:01:31.677Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/b6/81d2d19ea0be2c03664381b59f65fa72fc7969decedae00bc2c4ad835708/pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f", size = 2074737, upload-time = "2026-08-28T09:57:57.711Z" }, + { url = "https://files.pythonhosted.org/packages/0c/18/b70da8300e292df4099684ea11b1958043580d2f50d2dc8bf7e542bdd84a/pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f", size = 1921751, upload-time = "2026-08-28T09:57:59.265Z" }, + { url = "https://files.pythonhosted.org/packages/e7/1a/0d590341b6ffa4b4aca83508e6b8db4761aaeacfc15a25ca3815876d4797/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061", size = 1948231, upload-time = "2026-08-28T09:58:00.678Z" }, + { url = "https://files.pythonhosted.org/packages/7d/1d/02eb35761c51f2f7b1b042d6ab4cda6600f0c8c88a2243b3f734376201e5/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be", size = 2020708, upload-time = "2026-08-28T09:58:02.267Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ea/f86073830e35d508cc8ddf9c3d9e6e6840fcb88d34bf726b0b4710186f27/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a", size = 2194914, upload-time = "2026-08-28T09:58:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/bb/d7/fc36240d7791ce90939e51608568c33bfdae26202016f9770c229a487d86/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b", size = 2235622, upload-time = "2026-08-28T09:58:05.516Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bc/3fa2d76b83162820a17da7f645b28d1cba99fc8e1e5fc6517067ec450fa1/pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c", size = 2062091, upload-time = "2026-08-28T09:58:07.135Z" }, + { url = "https://files.pythonhosted.org/packages/ab/9a/095d557bb492c90cd8a70a6dd048bf793d433d03d86c81c11e912e4cd049/pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee", size = 2089904, upload-time = "2026-08-28T09:58:08.814Z" }, + { url = "https://files.pythonhosted.org/packages/24/98/7b76b1ad10a19a617a52aaa1d80e159115af939b095e86f8e756fd52e0df/pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e", size = 2132244, upload-time = "2026-08-28T09:58:10.435Z" }, + { url = "https://files.pythonhosted.org/packages/20/32/7d6ca365fadba186a0c8f85de1a701663bce81efd309d9479be58687622f/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2", size = 2143901, upload-time = "2026-08-28T09:58:12.033Z" }, + { url = "https://files.pythonhosted.org/packages/f8/09/eb9a6aa57f22fd1541a9c0aa2a1f3aeef3ec65347d33e10a6da2f43e0ee9/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689", size = 2299425, upload-time = "2026-08-28T09:58:13.614Z" }, + { url = "https://files.pythonhosted.org/packages/8a/f9/548a5bb9d4ba8cd26e26daf48052236f6b38bb61e7b7241fbc3c995719eb/pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec", size = 2318566, upload-time = "2026-08-28T09:58:15.199Z" }, + { url = "https://files.pythonhosted.org/packages/4a/20/06454d18834c02c406c9133f1a3b485305fd9ee984f9636c2f730bef6a9d/pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129", size = 1954258, upload-time = "2026-08-28T09:58:16.813Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/718b9deb4b72453b5d8c7447a3b14cb77bef36917ef5f514e0948a4096a0/pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c", size = 2041030, upload-time = "2026-08-28T09:58:18.288Z" }, + { url = "https://files.pythonhosted.org/packages/67/ea/c1d1a5b72d6e1ff7f377a4d9199f6591f095beb5b409a8a5d89f7238d939/pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8", size = 2009234, upload-time = "2026-08-28T09:58:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/82/3f/76358795aa7a8c6d4f36e2cb828ad1c90ee118e1393a9281664f5aade9d4/pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d", size = 2076516, upload-time = "2026-08-28T09:58:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/db/50/26b091836076ce4cb2fac264186936acc069e0595772cfd02a563bc4761a/pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e", size = 1922874, upload-time = "2026-08-28T09:58:23.766Z" }, + { url = "https://files.pythonhosted.org/packages/09/f0/2a8ce3849e299d44e2d2c196b6082643a3235565a735cb51db7a6261f614/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29", size = 1951772, upload-time = "2026-08-28T09:58:25.435Z" }, + { url = "https://files.pythonhosted.org/packages/87/46/ac0dc8bdd9e6048183a14eb127764e7ad9240021c17513074a4711b0e31e/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4", size = 2031832, upload-time = "2026-08-28T09:58:27.102Z" }, + { url = "https://files.pythonhosted.org/packages/c4/c2/339de5bef7be36301a2231eaa52e62163742c2281f11b5f4892bc79785cd/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a", size = 2208645, upload-time = "2026-08-28T09:58:28.948Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a0/9ff22b797724262da14427abaed4dd1d864a139693fc5e7809114376a716/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62", size = 2265935, upload-time = "2026-08-28T09:58:30.625Z" }, + { url = "https://files.pythonhosted.org/packages/c0/a4/eb9409ec0736e50aa70a412f16c204ed149516846912f7e6724d4c73ee53/pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2", size = 2066284, upload-time = "2026-08-28T09:58:32.289Z" }, + { url = "https://files.pythonhosted.org/packages/c0/02/7f6156ffc926857f1c37c07d9a388682865a81830ab6a1b637082c25e399/pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869", size = 2105889, upload-time = "2026-08-28T09:58:33.986Z" }, + { url = "https://files.pythonhosted.org/packages/92/b1/e781d357ebe09fc929f995700f1b3503e8897f1cece183ecb1300d4d67e9/pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5", size = 2158006, upload-time = "2026-08-28T09:58:35.647Z" }, + { url = "https://files.pythonhosted.org/packages/70/0a/644597d84ab400e50609c192120b85c9681c22d3a20461b9060a79be0a7a/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3", size = 2158408, upload-time = "2026-08-28T09:58:37.38Z" }, + { url = "https://files.pythonhosted.org/packages/1e/ee/ca3b7b3a4b3769ffe9ce9432a7c9be755de9593a46d3b0d54d0409323e44/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b", size = 2309609, upload-time = "2026-08-28T09:58:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ce/52/39fa1f451486019524ca685020390e7ca351832fd874530ba30c8628e6dc/pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0", size = 2342618, upload-time = "2026-08-28T09:58:40.89Z" }, + { url = "https://files.pythonhosted.org/packages/81/5e/468fc630568c61dcef3cd47ad32ffbeed9af643f49208d1ea86ab4f890c4/pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b", size = 1939475, upload-time = "2026-08-28T09:58:42.591Z" }, + { url = "https://files.pythonhosted.org/packages/cf/c9/4c19f41b84cf6b622a72fbeed7665b25d47a187d68d47d0d430c07f23268/pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8", size = 2043140, upload-time = "2026-08-28T09:58:44.272Z" }, + { url = "https://files.pythonhosted.org/packages/af/dd/0c1a050299147c746e5256db16d645ab5efd4f78c59937d581a0524e74a2/pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084", size = 1997729, upload-time = "2026-08-28T09:58:46.13Z" }, + { url = "https://files.pythonhosted.org/packages/f5/37/5abe39a8372a61d3dc3c1338fc504281c01b32fdb3169cd7187153b56d3e/pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0", size = 2075885, upload-time = "2026-08-28T09:58:47.856Z" }, + { url = "https://files.pythonhosted.org/packages/21/43/6323b1f8b217780454c61304bcd2b38ae4762f50754414124603ccc90bb2/pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff", size = 1922768, upload-time = "2026-08-28T09:58:49.58Z" }, + { url = "https://files.pythonhosted.org/packages/0f/a3/c05ca796e1197618a774b01e596aeedfefc2f7d8c01ae3054e910b120e8a/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931", size = 1951241, upload-time = "2026-08-28T09:58:51.511Z" }, + { url = "https://files.pythonhosted.org/packages/68/32/33bc39ac705c52cffc908e8389f9754fdb208aea5c69cceddf4eb3ce99af/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f", size = 2031975, upload-time = "2026-08-28T09:58:53.166Z" }, + { url = "https://files.pythonhosted.org/packages/b0/70/2333e885c0f6a67bc105c5916965dac9b57f2718ee20d81d1a06a4ebdc13/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038", size = 2208542, upload-time = "2026-08-28T09:58:55.017Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ea/296debfb4264207bbda5936133892e027c0a58875ad53ebd512fba8ec3a2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f", size = 2264692, upload-time = "2026-08-28T09:58:56.767Z" }, + { url = "https://files.pythonhosted.org/packages/d3/f2/9e4de77a6271e07a76d2d58b11c091a979c191ed2939bf80067568b369d2/pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1", size = 2066633, upload-time = "2026-08-28T09:58:58.531Z" }, + { url = "https://files.pythonhosted.org/packages/8d/db/f9e9d0c97445987b2084823d5c240de88087338f04fc2cfaa2df186b8049/pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761", size = 2105235, upload-time = "2026-08-28T09:59:00.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/c5/79169b047b3b2c3e99e04bc76372af9637e0bf6db638274fa927df96369e/pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5", size = 2157367, upload-time = "2026-08-28T09:59:02.442Z" }, + { url = "https://files.pythonhosted.org/packages/26/b5/ba6057afb7c291bd449f51b867f95aef2072941c4ce4e5c31d6ffd132d3b/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e", size = 2158420, upload-time = "2026-08-28T09:59:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/6e/28/2057abecaafdc22912afa819603a51f0a62d40643b7c4871c51721fea9be/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed", size = 2309588, upload-time = "2026-08-28T09:59:06.048Z" }, + { url = "https://files.pythonhosted.org/packages/71/9d/881156dc404e27479c4246128d73538464cab4a239bec61995e227644c30/pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519", size = 2341866, upload-time = "2026-08-28T09:59:08.539Z" }, + { url = "https://files.pythonhosted.org/packages/5a/38/d66f443a259f84d13babdceae568e572b0ed26da17ca5d0a649ebb110a67/pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea", size = 1938580, upload-time = "2026-08-28T09:59:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/1e/1d5371213f4cc9a7ed70c0bfcc7911de22311ee99a662a56077d7292d2ac/pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5", size = 2041980, upload-time = "2026-08-28T09:59:12.396Z" }, + { url = "https://files.pythonhosted.org/packages/5a/48/4222d90b1c67568bace4dec6dca6271449c66de3595d72b6d098f5fde597/pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575", size = 1997213, upload-time = "2026-08-28T09:59:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/8e/8a/14596f2a8367da50cf7cbac48169ee5d9c8e11d486a3b527082384630c72/pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355", size = 2074081, upload-time = "2026-08-28T09:59:16.141Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d5/d8a4eb6d6c7f66b91dd37c576d76e9e60fba900caf5372c17bcf949febc2/pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e", size = 1920497, upload-time = "2026-08-28T09:59:18.065Z" }, + { url = "https://files.pythonhosted.org/packages/8e/26/092079428f86e927e030b2c0ced87df69dbb1c875cdeaa67bf42ea2be746/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3", size = 1952130, upload-time = "2026-08-28T09:59:20.476Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/8ec0e290a9ebaebd64047bf5fda94be835c6b1551b02437e4b76778fbcd7/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c", size = 2026371, upload-time = "2026-08-28T09:59:22.227Z" }, + { url = "https://files.pythonhosted.org/packages/01/72/4fd20ad520fb8da0157f95b27a7eb05a72790ef08138e7701ac972c342ea/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21", size = 2202822, upload-time = "2026-08-28T09:59:24.277Z" }, + { url = "https://files.pythonhosted.org/packages/31/b0/d16e0771206b29314f0d52198b720be21e8a99ab2bf11e3bc0d7c9cebdff/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f", size = 2262756, upload-time = "2026-08-28T09:59:26.608Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9b/59634b7ac631c63b2a37760eb6943af3e29573d6b59a4abc5e7f019d4cee/pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f", size = 2068352, upload-time = "2026-08-28T09:59:29.044Z" }, + { url = "https://files.pythonhosted.org/packages/08/7c/570abb1ad2155348dc754ea91be22e5aaa18eb6d69a6068f7c6f2679a6ed/pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a", size = 2104777, upload-time = "2026-08-28T09:59:30.95Z" }, + { url = "https://files.pythonhosted.org/packages/8e/25/5bf74adc65a1ac5b7be3f6cb0bcb5433615c1598a801c19d830d84c98ded/pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821", size = 2156312, upload-time = "2026-08-28T09:59:32.604Z" }, + { url = "https://files.pythonhosted.org/packages/90/6a/2ef38830675e050121040618135564ed56b860b45433b02d9b4ebece46f3/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2", size = 2150067, upload-time = "2026-08-28T09:59:34.453Z" }, + { url = "https://files.pythonhosted.org/packages/90/ef/a7dbb03a14a64c2a4621f989c615ed9a892535a6cad938fc27079f919d80/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47", size = 2304516, upload-time = "2026-08-28T09:59:36.194Z" }, + { url = "https://files.pythonhosted.org/packages/68/f8/6bb4c4b80e8a6fde1904c64a51c62a1d04fcdfa3ea521a66b2ddefa1d885/pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a", size = 2335223, upload-time = "2026-08-28T09:59:37.931Z" }, + { url = "https://files.pythonhosted.org/packages/2a/80/f46b8c681195190b2c1f1c7c0a81abce60663e987613e09ef64d433dd96b/pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074", size = 1934827, upload-time = "2026-08-28T09:59:39.836Z" }, + { url = "https://files.pythonhosted.org/packages/f7/3c/60674207246bc0a4009d2391b7c7251c7159f279c8d2ab8aae8ef46f3dee/pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0", size = 2042648, upload-time = "2026-08-28T09:59:41.792Z" }, + { url = "https://files.pythonhosted.org/packages/69/0c/117c562c7c1babdf44576b72a5e496906506c93690387ecfbca7c729ae2e/pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5", size = 1989652, upload-time = "2026-08-28T09:59:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/e8/66/9336ae58f9eb68c41d121894e52c4c89eccb07eb8f602a04ee9c3f37736a/pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7", size = 2065829, upload-time = "2026-08-28T09:59:45.364Z" }, + { url = "https://files.pythonhosted.org/packages/c5/02/bc19b47a96c2d3109760711acf22369e56bd7e405ca52f7ade164d2ead57/pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3", size = 1905716, upload-time = "2026-08-28T09:59:47.18Z" }, + { url = "https://files.pythonhosted.org/packages/52/a4/70b47c0509923dd98ccfed04fb3e32ea3849c82a0ff2205bb41009b43c00/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f", size = 1934216, upload-time = "2026-08-28T09:59:49.241Z" }, + { url = "https://files.pythonhosted.org/packages/52/ab/aa03b65f7bb198585edf806b906c3223ecf1795543e39e23aec4cce27ad2/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7", size = 2010635, upload-time = "2026-08-28T09:59:51.692Z" }, + { url = "https://files.pythonhosted.org/packages/3c/8b/0da06343f30b84ec549aafd309c6456223d5dc8bd36af504c573faad561d/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c", size = 2209369, upload-time = "2026-08-28T09:59:53.582Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5b/844c4defaa34a3df66eb9257087d121d70c201298b96abdf9f492fc2f1bf/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111", size = 2253238, upload-time = "2026-08-28T09:59:55.484Z" }, + { url = "https://files.pythonhosted.org/packages/f4/64/a4e536cb16d7f61a7fd3120b46c577fc7fa7325992f69c4f52bc786d77d8/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829", size = 2065740, upload-time = "2026-08-28T09:59:58.038Z" }, + { url = "https://files.pythonhosted.org/packages/5f/75/aaa38c6bc2d085f6605b34eabdc6a8a4e0b2e61fc9c8e6e52b28e97b3125/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa", size = 2087425, upload-time = "2026-08-28T09:59:59.898Z" }, + { url = "https://files.pythonhosted.org/packages/55/ae/fcab4cfc39aba3689e1d20c8b5250ad280957022c09af2ed9cd585602a5e/pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034", size = 2139306, upload-time = "2026-08-28T10:00:03.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/f4/f1d03a4bc9d9acbc62f4d742b8a319af52f71885079868b2ff8e48a651ee/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184", size = 2144589, upload-time = "2026-08-28T10:00:05.645Z" }, + { url = "https://files.pythonhosted.org/packages/83/f3/7a53bb1356de514a4cd295f25b6ac39237895620c0462d2592b76c16e114/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38", size = 2288882, upload-time = "2026-08-28T10:00:07.931Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/5a81583660c175c59d49ffb09f4b3a44debeaf86a19fca664ae1cdd9ee32/pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9", size = 2335210, upload-time = "2026-08-28T10:00:10.177Z" }, + { url = "https://files.pythonhosted.org/packages/5a/9f/5d685c2693b972d1a59c998586e8823712b66603aeff47ee60a4bdaafd37/pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9", size = 1921180, upload-time = "2026-08-28T10:00:12.35Z" }, + { url = "https://files.pythonhosted.org/packages/70/12/5c94ee16d65a37a15f9e869f5e6256df111154491173801a4c5e800ab548/pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290", size = 2020515, upload-time = "2026-08-28T10:00:14.774Z" }, + { url = "https://files.pythonhosted.org/packages/63/19/67830dda664e6bdf9285ee2e40f355d0d7d6b92aa0c42e8d217bb8d33d36/pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f", size = 1989276, upload-time = "2026-08-28T10:00:16.984Z" }, + { url = "https://files.pythonhosted.org/packages/af/1e/ecca01fce348f7e8afa9572441ff6f7d1cc70d21e4859f33944d10877e1e/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2", size = 2075342, upload-time = "2026-08-28T10:00:51.353Z" }, + { url = "https://files.pythonhosted.org/packages/1f/4c/af80c7a8032dfc897040ad5cb772bebde529a381186499e6e29987f23f8c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c", size = 1907219, upload-time = "2026-08-28T10:00:53.438Z" }, + { url = "https://files.pythonhosted.org/packages/be/3e/54d89e2b092e778716bf6153634ef479e955f48c261090be23aa1e0fb0b5/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47", size = 1953393, upload-time = "2026-08-28T10:00:55.58Z" }, + { url = "https://files.pythonhosted.org/packages/ea/89/828ee90cda28ce17bdefaa3a6eaf74fe430e113295a10e6126beca559d6c/pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a", size = 2099024, upload-time = "2026-08-28T10:00:57.794Z" }, + { url = "https://files.pythonhosted.org/packages/df/dd/053c2e4303f791f3b8f8a14ab0b22008e8eb21d868c0c90b4f9be705b76a/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942", size = 2062540, upload-time = "2026-08-28T10:01:00.318Z" }, + { url = "https://files.pythonhosted.org/packages/d7/dd/a18df751a5e37dd51bfad7f68e766999125bebe68c9e1d10a493ad01bd63/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f", size = 1902040, upload-time = "2026-08-28T10:01:02.529Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/01d40f9d07ce8a779fd6e0bd8ad4fba91309500dd67b869e2e219d261a6d/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433", size = 1967479, upload-time = "2026-08-28T10:01:05.004Z" }, + { url = "https://files.pythonhosted.org/packages/fa/04/c81d4841331c2178b6fb09ae225425e110ed72d990c9fe556c4ec03d1013/pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c", size = 2111034, upload-time = "2026-08-28T10:01:07.345Z" }, + { url = "https://files.pythonhosted.org/packages/20/21/22102e9950b3049526d20e811b95396508377d87651edd2b80d2b3d28659/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f", size = 2071333, upload-time = "2026-08-28T10:01:09.636Z" }, + { url = "https://files.pythonhosted.org/packages/d8/18/87aefa427d191e6d3ab1447f1efc1cdcac86af1069239b133e8a0fd7f7c9/pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0", size = 1912713, upload-time = "2026-08-28T10:01:12.285Z" }, + { url = "https://files.pythonhosted.org/packages/1f/93/fd89e9ad49b1805ca94d24ce1088b7d305f05c35ffafcedb9819d03588a0/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4", size = 2090926, upload-time = "2026-08-28T10:01:15.19Z" }, + { url = "https://files.pythonhosted.org/packages/6f/45/8e59dab6acf8d35f02f0a958980074f31038968bdb2c983fcae9d1efee03/pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25", size = 2131303, upload-time = "2026-08-28T10:01:17.937Z" }, + { url = "https://files.pythonhosted.org/packages/d5/a5/e1d4dc5180dd887a9522efc1f8716b8692b7606b1d3273d7862eaf66be44/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6", size = 2145128, upload-time = "2026-08-28T10:01:20.694Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d7/ad493864a7fb21c0c4df98f965e2db430cb25a9d7369b5778d5016c09fd9/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e", size = 2294560, upload-time = "2026-08-28T10:01:23.495Z" }, + { url = "https://files.pythonhosted.org/packages/02/8e/b41c84c913f29973a268e6c2b5bbf13c95adb9956c126d10da11ba3b2bef/pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda", size = 2317531, upload-time = "2026-08-28T10:01:26.334Z" }, + { url = "https://files.pythonhosted.org/packages/db/1d/068464f23075f66a8f1b806935e9cd9363ee446636ea70d2c22ee8659dbf/pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266", size = 2140686, upload-time = "2026-08-28T10:01:28.947Z" }, ] [[package]] name = "pydantic-settings" -version = "2.13.1" +version = "2.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/52/6d/fffca34caecc4a3f97bda81b2098da5e8ab7efc9a66e819074a11955d87e/pydantic_settings-2.13.1.tar.gz", hash = "sha256:b4c11847b15237fb0171e1462bf540e294affb9b86db4d9aa5c01730bdbe4025", size = 223826, upload-time = "2026-02-19T13:45:08.055Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/ca/31c57507b13119d7d3cfa1576dad2911a4861e3be07b579395f4e9d393f9/pydantic_settings-2.15.0.tar.gz", hash = "sha256:694b793e84f766ba76a90ebdefc01d0a9a045dab0382bee70393da93712ad117", size = 261253, upload-time = "2026-08-07T09:24:57.419Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/00/4b/ccc026168948fec4f7555b9164c724cf4125eac006e176541483d2c959be/pydantic_settings-2.13.1-py3-none-any.whl", hash = "sha256:d56fd801823dbeae7f0975e1f8c8e25c258eb75d278ea7abb5d9cebb01b56237", size = 58929, upload-time = "2026-02-19T13:45:06.034Z" }, + { url = "https://files.pythonhosted.org/packages/30/a4/2bffa9f8e804325a09867f0e9d30795c80ea9f8d62560bd1b6ad6220eb2f/pydantic_settings-2.15.0-py3-none-any.whl", hash = "sha256:0ba092c291c94baceb5eff768aa0d56400a457585bc0175925a5a5510303da42", size = 69413, upload-time = "2026-08-07T09:24:55.839Z" }, ] [[package]] @@ -1728,16 +2228,16 @@ wheels = [ [[package]] name = "pygments" -version = "2.19.2" +version = "2.21.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, ] [[package]] name = "pyinstaller" -version = "6.19.0" +version = "6.22.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "altgraph" }, @@ -1748,37 +2248,36 @@ dependencies = [ { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c8/63/fd62472b6371d89dc138d40c36d87a50dc2de18a035803bbdc376b4ffac4/pyinstaller-6.19.0.tar.gz", hash = "sha256:ec73aeb8bd9b7f2f1240d328a4542e90b3c6e6fbc106014778431c616592a865", size = 4036072, upload-time = "2026-02-14T18:06:28.718Z" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/2b/836d9def811c02522e0921d8b8cdf0c16b0545a216e97e71041758057859/pyinstaller-6.22.2.tar.gz", hash = "sha256:89b65a3ad07d9dd5832253e37bc45f31872d10d7f9d5c9fd0fdd6088a83829dd", size = 4092631, upload-time = "2026-08-17T20:53:22.231Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/e3/eb/23374721fecfa72677e79800921cb6aceefa6ba48574dc404f3f6c6c3be7/pyinstaller-6.19.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:4190e76b74f0c4b5c5f11ac360928cd2e36ec8e3194d437bf6b8648c7bc0c134", size = 1040563, upload-time = "2026-02-14T18:05:22.436Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7e/dfd724b0b533f5aaec0ee5df406fe2319987ed6964480a706f85478b12ea/pyinstaller-6.19.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8bd68abd812d8a6ba33b9f1810e91fee0f325969733721b78151f0065319ca11", size = 735477, upload-time = "2026-02-14T18:05:27.143Z" }, - { url = "https://files.pythonhosted.org/packages/88/c9/ee3a4101c31f26344e66896c73c1fd6ed8282bf871473365b7f8674af406/pyinstaller-6.19.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1ec54ef967996ca61dacba676227e2b23219878ccce5ee9d6f3aada7b8ed8abf", size = 747143, upload-time = "2026-02-14T18:05:31.488Z" }, - { url = "https://files.pythonhosted.org/packages/da/0a/fc77e9f861be8cf300ac37155f59cc92aff99b29f2ddd78546f563a5b5a6/pyinstaller-6.19.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:4ab2bb52e58448e14ddf9450601bdedd66800465043501c1d8f1cab87b60b122", size = 744849, upload-time = "2026-02-14T18:05:35.492Z" }, - { url = "https://files.pythonhosted.org/packages/6d/e3/6872e020ee758afe0b821663858492c10745608b07150e5e2c824a5b3e1c/pyinstaller-6.19.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:da6d5c6391ccefe73554b9fa29b86001c8e378e0f20c2a4004f836ba537eff63", size = 741590, upload-time = "2026-02-14T18:05:39.59Z" }, - { url = "https://files.pythonhosted.org/packages/53/60/b8db5f1a4b0fb228175f2ea0aa33f949adcc097fbe981cc524f9faf85777/pyinstaller-6.19.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a0fc5f6b3c55aa54353f0c74ffa59b1115433c1850c6f655d62b461a2ed6cbbe", size = 741448, upload-time = "2026-02-14T18:05:45.636Z" }, - { url = "https://files.pythonhosted.org/packages/6f/4d/63b0600f2694e9141b83129fbc1c488ec84d5a0770b1448ec154dcd0fee9/pyinstaller-6.19.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:e649ba6bd1b0b89b210ad92adb5fbdc8a42dd2c5ca4f72ef3a0bfec83a424b83", size = 740613, upload-time = "2026-02-14T18:05:49.726Z" }, - { url = "https://files.pythonhosted.org/packages/01/d4/e812ad36178093a0e9fd4b8127577748dd85b0cb71de912229dca21fd741/pyinstaller-6.19.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:481a909c8e60c8692fc60fcb1344d984b44b943f8bc9682f2fcdae305ad297e6", size = 740350, upload-time = "2026-02-14T18:05:54.093Z" }, - { url = "https://files.pythonhosted.org/packages/52/03/b2c2ee41fb8e10fd2a45d21f5ec2ef25852cfb978dbf762972eed59e3d63/pyinstaller-6.19.0-py3-none-win32.whl", hash = "sha256:3c5c251054fe4cfaa04c34a363dcfbf811545438cb7198304cd444756bc2edd2", size = 1324317, upload-time = "2026-02-14T18:06:00.085Z" }, - { url = "https://files.pythonhosted.org/packages/9c/d3/6d5e62b8270e2b53a6065e281b3a7785079b00e9019c8019952828dd1669/pyinstaller-6.19.0-py3-none-win_amd64.whl", hash = "sha256:b5bb6536c6560330d364d91522250f254b107cf69129d9cbcd0e6727c570be33", size = 1384894, upload-time = "2026-02-14T18:06:06.425Z" }, - { url = "https://files.pythonhosted.org/packages/81/65/458cd523308a101a22fd2742893405030cc24994cc74b1b767cecf137160/pyinstaller-6.19.0-py3-none-win_arm64.whl", hash = "sha256:c2d5a539b0bfe6159d5522c8c70e1c0e487f22c2badae0f97d45246223b798ea", size = 1325374, upload-time = "2026-02-14T18:06:12.804Z" }, + { url = "https://files.pythonhosted.org/packages/57/39/08cd53632276de70426e7c273820277a48253fee397b4048301ec03c3566/pyinstaller-6.22.2-py3-none-macosx_10_13_universal2.whl", hash = "sha256:ebd1b1ca932d7cf25d7366ce691aaf79a5ff9425811ed7328b5116e4471b6d6d", size = 1062734, upload-time = "2026-08-17T20:52:17.635Z" }, + { url = "https://files.pythonhosted.org/packages/74/22/2d865896782cbb41e2388c7314207c17a98acdcc1b8e5eef668873505c9f/pyinstaller-6.22.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:f5ccb847451df4207bce18bf53a57b124c9bb4e7e4bad08c5ecc627bcf00b28c", size = 755697, upload-time = "2026-08-17T20:52:21.676Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/6c11e4d5a76e716ea68da1946ab706a4bdf6d8e68b0e1dea314366d046a0/pyinstaller-6.22.2-py3-none-manylinux2014_i686.whl", hash = "sha256:becb47ad78272bede87acf2ed830d7545a8f65ab11d06a68fe2b99ba1afaac6d", size = 768870, upload-time = "2026-08-17T20:52:25.511Z" }, + { url = "https://files.pythonhosted.org/packages/cf/e2/dbfea6a58b68acf644f7193b11d570476d6ffaed57381b6d1dd9e898a971/pyinstaller-6.22.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:06d7b3827a8049db4a2d47e3ec4ae2f69a1041577d8833b8f169745cde573ab4", size = 767445, upload-time = "2026-08-17T20:52:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/5e/db/f24a21af2f87ce1df4e07fdad95eec65ef9f284267a7c1e5eeea40f49aa4/pyinstaller-6.22.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:7bee432404eca5dc3ef37c36811c266561b03988f6d3e70ab0fa5352dd5de9e4", size = 762113, upload-time = "2026-08-17T20:52:34.025Z" }, + { url = "https://files.pythonhosted.org/packages/2b/07/b304ff3f5f8333778065e3658b604b0e108cd934b920f3fbab825a0dd5b7/pyinstaller-6.22.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:9622686ecc5d5fa492fe6cde29d47df9dd41138cff8177be9f901ca3260f2096", size = 762173, upload-time = "2026-08-17T20:52:38.477Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9b/0af69d93dfad2e3d590a5de5828d5bcd903747c0224bd9560ab476904dfe/pyinstaller-6.22.2-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:0260eaad6be3f6fbc1affffe6dc7b8e5b636dbca51b463224daba971610b6fc1", size = 761674, upload-time = "2026-08-17T20:52:42.646Z" }, + { url = "https://files.pythonhosted.org/packages/d3/4e/28b6094dcbd1e1bbb868daf4f8752999a20c1020ab64569232be42af2be8/pyinstaller-6.22.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:8c2c4b14caad38c1f3df8e9bf5276fc265e27fe8b4180cab4a37864288427bc0", size = 761053, upload-time = "2026-08-17T20:52:46.594Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2c/f91b63fd01422111ac04e3b9bf61c039da5a3292acb4fe5248905f0be688/pyinstaller-6.22.2-py3-none-win32.whl", hash = "sha256:9a078877caa3920558a3242d0a7019fe5b825cff4b65ed380c7d1e1bd200ddd9", size = 1344474, upload-time = "2026-08-17T20:52:53.108Z" }, + { url = "https://files.pythonhosted.org/packages/3f/53/8ba1d0f6159b490f700eac6161a4be5f0d4672608a6dae9fd73679f183ee/pyinstaller-6.22.2-py3-none-win_amd64.whl", hash = "sha256:9b990fa6bbe143572f06644a984ad0d7aa2e2ccc6929d4916031343a5888e9a7", size = 1405725, upload-time = "2026-08-17T20:52:59.667Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1b/9a3062cc34c939f694d4262c4754681f6ffe853c21c124209ad2d08b8e84/pyinstaller-6.22.2-py3-none-win_arm64.whl", hash = "sha256:afb6f9a95d19b6dcd3a7decc40d9adb6ba9c4f8802ddd6c972dfb552953f384e", size = 1353806, upload-time = "2026-08-17T20:53:06.222Z" }, ] [[package]] name = "pyinstaller-hooks-contrib" -version = "2026.3" +version = "2026.7" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "packaging" }, - { name = "setuptools" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/80/17/716326f6ba18d0663f7995ae369c23e50efebc22fbb054e9710a45688f61/pyinstaller_hooks_contrib-2026.3.tar.gz", hash = "sha256:800d3a198a49a6cd0de2d7fb795005fdca7a0222ed9cb47c0691abd1c27b9310", size = 172323, upload-time = "2026-03-09T22:44:06.345Z" } +sdist = { url = "https://files.pythonhosted.org/packages/26/60/d881fa1ba8c160c18d8e6f782bb16ec4640c08bc08fc50f704c368ad4f9e/pyinstaller_hooks_contrib-2026.7.tar.gz", hash = "sha256:5fbcaacb22c4f4aac869a127dce283f67a4b4cfcc37d496f2446603e6d68aefa", size = 175992, upload-time = "2026-08-24T21:26:58.713Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/19/781352446af28755f16ce52b2d97f7a6f2d7974ac34c00ca5cd8c40c9098/pyinstaller_hooks_contrib-2026.3-py3-none-any.whl", hash = "sha256:5ecd1068ad262afecadf07556279d2be52ca93a88b049fae17f1a2eb2969254a", size = 454625, upload-time = "2026-03-09T22:44:04.717Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/350377af7b50416344ab8792756d414eef7629c618a73e9a0b13bb1552d9/pyinstaller_hooks_contrib-2026.7-py3-none-any.whl", hash = "sha256:24257a04c7a5a7a034cf28e39dcee20fbeeb9f043076729480f2e1b69904408a", size = 459445, upload-time = "2026-08-24T21:26:57.274Z" }, ] [[package]] name = "pyrag-kit" -version = "1.3.0" +version = "1.4.0" source = { virtual = "." } dependencies = [ { name = "anthropic" }, @@ -1790,7 +2289,8 @@ dependencies = [ { name = "langchain-community" }, { name = "langchain-core" }, { name = "langchain-text-splitters" }, - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "openai" }, { name = "openpyxl" }, { name = "prompt-toolkit" }, @@ -1808,46 +2308,52 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "bandit" }, + { name = "mypy" }, { name = "pyinstaller" }, { name = "pytest" }, + { name = "ruff" }, ] [package.metadata] requires-dist = [ - { name = "anthropic", specifier = ">=0.86.0" }, - { name = "faiss-cpu", specifier = ">=1.13.2" }, - { name = "google-genai", specifier = ">=1.68.0" }, + { name = "anthropic", specifier = ">=1.4.0" }, + { name = "faiss-cpu", specifier = ">=1.15.0" }, + { name = "google-genai", specifier = ">=2.22.0" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "jieba", specifier = ">=0.42.1" }, - { name = "langchain", specifier = ">=1.2.13" }, - { name = "langchain-community", specifier = ">=0.4.1" }, - { name = "langchain-core", specifier = ">=1.2.20" }, - { name = "langchain-text-splitters", specifier = ">=1.1.1" }, - { name = "numpy", specifier = ">=2.4.3" }, - { name = "openai", specifier = ">=2.29.0" }, + { name = "langchain", specifier = ">=1.4.0" }, + { name = "langchain-community", specifier = ">=0.4.2" }, + { name = "langchain-core", specifier = ">=1.6.2" }, + { name = "langchain-text-splitters", specifier = ">=1.1.2" }, + { name = "numpy", specifier = ">=2.4.6" }, + { name = "openai", specifier = ">=3.10.0" }, { name = "openpyxl", specifier = ">=3.1.5" }, - { name = "prompt-toolkit", specifier = ">=3.0.52" }, - { name = "pydantic", specifier = ">=2.12.5" }, - { name = "pydantic-settings", specifier = ">=2.13.1" }, + { name = "prompt-toolkit", specifier = ">=3.0.53" }, + { name = "pydantic", specifier = ">=2.13.5" }, + { name = "pydantic-settings", specifier = ">=2.15.0" }, { name = "pyfiglet", specifier = ">=1.0.4" }, { name = "questionary", specifier = ">=2.1.1" }, { name = "rank-bm25", specifier = ">=0.2.2" }, - { name = "requests", specifier = ">=2.32.5" }, - { name = "rich", specifier = ">=14.3.3" }, + { name = "requests", specifier = ">=2.34.2" }, + { name = "rich", specifier = ">=15.0.0" }, { name = "tenacity", specifier = ">=9.1.4" }, - { name = "tiktoken", specifier = ">=0.12.0" }, - { name = "volcengine-python-sdk", extras = ["ark"], specifier = ">=5.0.18" }, + { name = "tiktoken", specifier = ">=0.14.0" }, + { name = "volcengine-python-sdk", extras = ["ark"], specifier = ">=5.0.48" }, ] [package.metadata.requires-dev] dev = [ - { name = "pyinstaller", specifier = ">=6.16.0" }, - { name = "pytest", specifier = ">=9.0.2" }, + { name = "bandit", specifier = ">=1.9.4" }, + { name = "mypy", specifier = ">=2.3.1" }, + { name = "pyinstaller", specifier = ">=6.22.2" }, + { name = "pytest", specifier = ">=9.1.1" }, + { name = "ruff", specifier = ">=0.16.6" }, ] [[package]] name = "pytest" -version = "9.0.2" +version = "9.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, @@ -1856,9 +2362,9 @@ dependencies = [ { name = "pluggy" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, ] [[package]] @@ -1875,11 +2381,11 @@ wheels = [ [[package]] name = "python-dotenv" -version = "1.2.2" +version = "1.2.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, + { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" }, ] [[package]] @@ -1963,7 +2469,8 @@ name = "rank-bm25" version = "0.2.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fc/0a/f9579384aa017d8b4c15613f86954b92a95a93d641cc849182467cf0bb3b/rank_bm25-0.2.2.tar.gz", hash = "sha256:096ccef76f8188563419aaf384a02f0ea459503fdf77901378d4fd9d87e5e51d", size = 8347, upload-time = "2022-02-16T12:10:52.196Z" } wheels = [ @@ -1972,111 +2479,127 @@ wheels = [ [[package]] name = "regex" -version = "2026.2.28" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8b/71/41455aa99a5a5ac1eaf311f5d8efd9ce6433c03ac1e0962de163350d0d97/regex-2026.2.28.tar.gz", hash = "sha256:a729e47d418ea11d03469f321aaf67cdee8954cde3ff2cf8403ab87951ad10f2", size = 415184, upload-time = "2026-02-28T02:19:42.792Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/db/8cbfd0ba3f302f2d09dd0019a9fcab74b63fee77a76c937d0e33161fb8c1/regex-2026.2.28-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e621fb7c8dc147419b28e1702f58a0177ff8308a76fa295c71f3e7827849f5d9", size = 488462, upload-time = "2026-02-28T02:16:22.616Z" }, - { url = "https://files.pythonhosted.org/packages/5d/10/ccc22c52802223f2368731964ddd117799e1390ffc39dbb31634a83022ee/regex-2026.2.28-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d5bef2031cbf38757a0b0bc4298bb4824b6332d28edc16b39247228fbdbad97", size = 290774, upload-time = "2026-02-28T02:16:23.993Z" }, - { url = "https://files.pythonhosted.org/packages/62/b9/6796b3bf3101e64117201aaa3a5a030ec677ecf34b3cd6141b5d5c6c67d5/regex-2026.2.28-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bcb399ed84eabf4282587ba151f2732ad8168e66f1d3f85b1d038868fe547703", size = 288724, upload-time = "2026-02-28T02:16:25.403Z" }, - { url = "https://files.pythonhosted.org/packages/9c/02/291c0ae3f3a10cea941d0f5366da1843d8d1fa8a25b0671e20a0e454bb38/regex-2026.2.28-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7c1b34dfa72f826f535b20712afa9bb3ba580020e834f3c69866c5bddbf10098", size = 791924, upload-time = "2026-02-28T02:16:26.863Z" }, - { url = "https://files.pythonhosted.org/packages/0f/57/f0235cc520d9672742196c5c15098f8f703f2758d48d5a7465a56333e496/regex-2026.2.28-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:851fa70df44325e1e4cdb79c5e676e91a78147b1b543db2aec8734d2add30ec2", size = 860095, upload-time = "2026-02-28T02:16:28.772Z" }, - { url = "https://files.pythonhosted.org/packages/b3/7c/393c94cbedda79a0f5f2435ebd01644aba0b338d327eb24b4aa5b8d6c07f/regex-2026.2.28-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:516604edd17b1c2c3e579cf4e9b25a53bf8fa6e7cedddf1127804d3e0140ca64", size = 906583, upload-time = "2026-02-28T02:16:30.977Z" }, - { url = "https://files.pythonhosted.org/packages/2c/73/a72820f47ca5abf2b5d911d0407ba5178fc52cf9780191ed3a54f5f419a2/regex-2026.2.28-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7ce83654d1ab701cb619285a18a8e5a889c1216d746ddc710c914ca5fd71022", size = 800234, upload-time = "2026-02-28T02:16:32.55Z" }, - { url = "https://files.pythonhosted.org/packages/34/b3/6e6a4b7b31fa998c4cf159a12cbeaf356386fbd1a8be743b1e80a3da51e4/regex-2026.2.28-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f2791948f7c70bb9335a9102df45e93d428f4b8128020d85920223925d73b9e1", size = 772803, upload-time = "2026-02-28T02:16:34.029Z" }, - { url = "https://files.pythonhosted.org/packages/10/e7/5da0280c765d5a92af5e1cd324b3fe8464303189cbaa449de9a71910e273/regex-2026.2.28-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03a83cc26aa2acda6b8b9dfe748cf9e84cbd390c424a1de34fdcef58961a297a", size = 781117, upload-time = "2026-02-28T02:16:36.253Z" }, - { url = "https://files.pythonhosted.org/packages/76/39/0b8d7efb256ae34e1b8157acc1afd8758048a1cf0196e1aec2e71fd99f4b/regex-2026.2.28-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ec6f5674c5dc836994f50f1186dd1fafde4be0666aae201ae2fcc3d29d8adf27", size = 854224, upload-time = "2026-02-28T02:16:38.119Z" }, - { url = "https://files.pythonhosted.org/packages/21/ff/a96d483ebe8fe6d1c67907729202313895d8de8495569ec319c6f29d0438/regex-2026.2.28-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:50c2fc924749543e0eacc93ada6aeeb3ea5f6715825624baa0dccaec771668ae", size = 761898, upload-time = "2026-02-28T02:16:40.333Z" }, - { url = "https://files.pythonhosted.org/packages/89/bd/d4f2e75cb4a54b484e796017e37c0d09d8a0a837de43d17e238adf163f4e/regex-2026.2.28-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ba55c50f408fb5c346a3a02d2ce0ebc839784e24f7c9684fde328ff063c3cdea", size = 844832, upload-time = "2026-02-28T02:16:41.875Z" }, - { url = "https://files.pythonhosted.org/packages/8a/a7/428a135cf5e15e4e11d1e696eb2bf968362f8ea8a5f237122e96bc2ae950/regex-2026.2.28-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:edb1b1b3a5576c56f08ac46f108c40333f222ebfd5cf63afdfa3aab0791ebe5b", size = 788347, upload-time = "2026-02-28T02:16:43.472Z" }, - { url = "https://files.pythonhosted.org/packages/a9/59/68691428851cf9c9c3707217ab1d9b47cfeec9d153a49919e6c368b9e926/regex-2026.2.28-cp311-cp311-win32.whl", hash = "sha256:948c12ef30ecedb128903c2c2678b339746eb7c689c5c21957c4a23950c96d15", size = 266033, upload-time = "2026-02-28T02:16:45.094Z" }, - { url = "https://files.pythonhosted.org/packages/42/8b/1483de1c57024e89296cbcceb9cccb3f625d416ddb46e570be185c9b05a9/regex-2026.2.28-cp311-cp311-win_amd64.whl", hash = "sha256:fd63453f10d29097cc3dc62d070746523973fb5aa1c66d25f8558bebd47fed61", size = 277978, upload-time = "2026-02-28T02:16:46.75Z" }, - { url = "https://files.pythonhosted.org/packages/a4/36/abec45dc6e7252e3dbc797120496e43bb5730a7abf0d9cb69340696a2f2d/regex-2026.2.28-cp311-cp311-win_arm64.whl", hash = "sha256:00f2b8d9615aa165fdff0a13f1a92049bfad555ee91e20d246a51aa0b556c60a", size = 270340, upload-time = "2026-02-28T02:16:48.626Z" }, - { url = "https://files.pythonhosted.org/packages/07/42/9061b03cf0fc4b5fa2c3984cbbaed54324377e440a5c5a29d29a72518d62/regex-2026.2.28-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fcf26c3c6d0da98fada8ae4ef0aa1c3405a431c0a77eb17306d38a89b02adcd7", size = 489574, upload-time = "2026-02-28T02:16:50.455Z" }, - { url = "https://files.pythonhosted.org/packages/77/83/0c8a5623a233015595e3da499c5a1c13720ac63c107897a6037bb97af248/regex-2026.2.28-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02473c954af35dd2defeb07e44182f5705b30ea3f351a7cbffa9177beb14da5d", size = 291426, upload-time = "2026-02-28T02:16:52.52Z" }, - { url = "https://files.pythonhosted.org/packages/9e/06/3ef1ac6910dc3295ebd71b1f9bfa737e82cfead211a18b319d45f85ddd09/regex-2026.2.28-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9b65d33a17101569f86d9c5966a8b1d7fbf8afdda5a8aa219301b0a80f58cf7d", size = 289200, upload-time = "2026-02-28T02:16:54.08Z" }, - { url = "https://files.pythonhosted.org/packages/dd/c9/8cc8d850b35ab5650ff6756a1cb85286e2000b66c97520b29c1587455344/regex-2026.2.28-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71dcecaa113eebcc96622c17692672c2d104b1d71ddf7adeda90da7ddeb26fc", size = 796765, upload-time = "2026-02-28T02:16:55.905Z" }, - { url = "https://files.pythonhosted.org/packages/e9/5d/57702597627fc23278ebf36fbb497ac91c0ce7fec89ac6c81e420ca3e38c/regex-2026.2.28-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:481df4623fa4969c8b11f3433ed7d5e3dc9cec0f008356c3212b3933fb77e3d8", size = 863093, upload-time = "2026-02-28T02:16:58.094Z" }, - { url = "https://files.pythonhosted.org/packages/02/6d/f3ecad537ca2811b4d26b54ca848cf70e04fcfc138667c146a9f3157779c/regex-2026.2.28-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:64e7c6ad614573e0640f271e811a408d79a9e1fe62a46adb602f598df42a818d", size = 909455, upload-time = "2026-02-28T02:17:00.918Z" }, - { url = "https://files.pythonhosted.org/packages/9e/40/bb226f203caa22c1043c1ca79b36340156eca0f6a6742b46c3bb222a3a57/regex-2026.2.28-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6b08a06976ff4fb0d83077022fde3eca06c55432bb997d8c0495b9a4e9872f4", size = 802037, upload-time = "2026-02-28T02:17:02.842Z" }, - { url = "https://files.pythonhosted.org/packages/44/7c/c6d91d8911ac6803b45ca968e8e500c46934e58c0903cbc6d760ee817a0a/regex-2026.2.28-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:864cdd1a2ef5716b0ab468af40139e62ede1b3a53386b375ec0786bb6783fc05", size = 775113, upload-time = "2026-02-28T02:17:04.506Z" }, - { url = "https://files.pythonhosted.org/packages/dc/8d/4a9368d168d47abd4158580b8c848709667b1cd293ff0c0c277279543bd0/regex-2026.2.28-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:511f7419f7afab475fd4d639d4aedfc54205bcb0800066753ef68a59f0f330b5", size = 784194, upload-time = "2026-02-28T02:17:06.888Z" }, - { url = "https://files.pythonhosted.org/packages/cc/bf/2c72ab5d8b7be462cb1651b5cc333da1d0068740342f350fcca3bca31947/regex-2026.2.28-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:b42f7466e32bf15a961cf09f35fa6323cc72e64d3d2c990b10de1274a5da0a59", size = 856846, upload-time = "2026-02-28T02:17:09.11Z" }, - { url = "https://files.pythonhosted.org/packages/7c/f4/6b65c979bb6d09f51bb2d2a7bc85de73c01ec73335d7ddd202dcb8cd1c8f/regex-2026.2.28-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8710d61737b0c0ce6836b1da7109f20d495e49b3809f30e27e9560be67a257bf", size = 763516, upload-time = "2026-02-28T02:17:11.004Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/29ea5e27400ee86d2cc2b4e80aa059df04eaf78b4f0c18576ae077aeff68/regex-2026.2.28-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4390c365fd2d45278f45afd4673cb90f7285f5701607e3ad4274df08e36140ae", size = 849278, upload-time = "2026-02-28T02:17:12.693Z" }, - { url = "https://files.pythonhosted.org/packages/1d/91/3233d03b5f865111cd517e1c95ee8b43e8b428d61fa73764a80c9bb6f537/regex-2026.2.28-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb3b1db8ff6c7b8bf838ab05583ea15230cb2f678e569ab0e3a24d1e8320940b", size = 790068, upload-time = "2026-02-28T02:17:14.9Z" }, - { url = "https://files.pythonhosted.org/packages/76/92/abc706c1fb03b4580a09645b206a3fc032f5a9f457bc1a8038ac555658ab/regex-2026.2.28-cp312-cp312-win32.whl", hash = "sha256:f8ed9a5d4612df9d4de15878f0bc6aa7a268afbe5af21a3fdd97fa19516e978c", size = 266416, upload-time = "2026-02-28T02:17:17.15Z" }, - { url = "https://files.pythonhosted.org/packages/fa/06/2a6f7dff190e5fa9df9fb4acf2fdf17a1aa0f7f54596cba8de608db56b3a/regex-2026.2.28-cp312-cp312-win_amd64.whl", hash = "sha256:01d65fd24206c8e1e97e2e31b286c59009636c022eb5d003f52760b0f42155d4", size = 277297, upload-time = "2026-02-28T02:17:18.723Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f0/58a2484851fadf284458fdbd728f580d55c1abac059ae9f048c63b92f427/regex-2026.2.28-cp312-cp312-win_arm64.whl", hash = "sha256:c0b5ccbb8ffb433939d248707d4a8b31993cb76ab1a0187ca886bf50e96df952", size = 270408, upload-time = "2026-02-28T02:17:20.328Z" }, - { url = "https://files.pythonhosted.org/packages/87/f6/dc9ef48c61b79c8201585bf37fa70cd781977da86e466cd94e8e95d2443b/regex-2026.2.28-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6d63a07e5ec8ce7184452cb00c41c37b49e67dc4f73b2955b5b8e782ea970784", size = 489311, upload-time = "2026-02-28T02:17:22.591Z" }, - { url = "https://files.pythonhosted.org/packages/95/c8/c20390f2232d3f7956f420f4ef1852608ad57aa26c3dd78516cb9f3dc913/regex-2026.2.28-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e59bc8f30414d283ae8ee1617b13d8112e7135cb92830f0ec3688cb29152585a", size = 291285, upload-time = "2026-02-28T02:17:24.355Z" }, - { url = "https://files.pythonhosted.org/packages/d2/a6/ba1068a631ebd71a230e7d8013fcd284b7c89c35f46f34a7da02082141b1/regex-2026.2.28-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:de0cf053139f96219ccfabb4a8dd2d217c8c82cb206c91d9f109f3f552d6b43d", size = 289051, upload-time = "2026-02-28T02:17:26.722Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1b/7cc3b7af4c244c204b7a80924bd3d85aecd9ba5bc82b485c5806ee8cda9e/regex-2026.2.28-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb4db2f17e6484904f986c5a657cec85574c76b5c5e61c7aae9ffa1bc6224f95", size = 796842, upload-time = "2026-02-28T02:17:29.064Z" }, - { url = "https://files.pythonhosted.org/packages/24/87/26bd03efc60e0d772ac1e7b60a2e6325af98d974e2358f659c507d3c76db/regex-2026.2.28-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52b017b35ac2214d0db5f4f90e303634dc44e4aba4bd6235a27f97ecbe5b0472", size = 863083, upload-time = "2026-02-28T02:17:31.363Z" }, - { url = "https://files.pythonhosted.org/packages/ae/54/aeaf4afb1aa0a65e40de52a61dc2ac5b00a83c6cb081c8a1d0dda74f3010/regex-2026.2.28-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:69fc560ccbf08a09dc9b52ab69cacfae51e0ed80dc5693078bdc97db2f91ae96", size = 909412, upload-time = "2026-02-28T02:17:33.248Z" }, - { url = "https://files.pythonhosted.org/packages/12/2f/049901def913954e640d199bbc6a7ca2902b6aeda0e5da9d17f114100ec2/regex-2026.2.28-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e61eea47230eba62a31f3e8a0e3164d0f37ef9f40529fb2c79361bc6b53d2a92", size = 802101, upload-time = "2026-02-28T02:17:35.053Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/512fb9ff7f5b15ea204bb1967ebb649059446decacccb201381f9fa6aad4/regex-2026.2.28-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4f5c0b182ad4269e7381b7c27fdb0408399881f7a92a4624fd5487f2971dfc11", size = 775260, upload-time = "2026-02-28T02:17:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/d1/a8/9a92935878aba19bd72706b9db5646a6f993d99b3f6ed42c02ec8beb1d61/regex-2026.2.28-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:96f6269a2882fbb0ee76967116b83679dc628e68eaea44e90884b8d53d833881", size = 784311, upload-time = "2026-02-28T02:17:39.855Z" }, - { url = "https://files.pythonhosted.org/packages/09/d3/fc51a8a738a49a6b6499626580554c9466d3ea561f2b72cfdc72e4149773/regex-2026.2.28-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b5acd4b6a95f37c3c3828e5d053a7d4edaedb85de551db0153754924cb7c83e3", size = 856876, upload-time = "2026-02-28T02:17:42.317Z" }, - { url = "https://files.pythonhosted.org/packages/08/b7/2e641f3d084b120ca4c52e8c762a78da0b32bf03ef546330db3e2635dc5f/regex-2026.2.28-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2234059cfe33d9813a3677ef7667999caea9eeaa83fef98eb6ce15c6cf9e0215", size = 763632, upload-time = "2026-02-28T02:17:45.073Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6d/0009021d97e79ee99f3d8641f0a8d001eed23479ade4c3125a5480bf3e2d/regex-2026.2.28-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:c15af43c72a7fb0c97cbc66fa36a43546eddc5c06a662b64a0cbf30d6ac40944", size = 849320, upload-time = "2026-02-28T02:17:47.192Z" }, - { url = "https://files.pythonhosted.org/packages/05/7a/51cfbad5758f8edae430cb21961a9c8d04bce1dae4d2d18d4186eec7cfa1/regex-2026.2.28-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9185cc63359862a6e80fe97f696e04b0ad9a11c4ac0a4a927f979f611bfe3768", size = 790152, upload-time = "2026-02-28T02:17:49.067Z" }, - { url = "https://files.pythonhosted.org/packages/90/3d/a83e2b6b3daa142acb8c41d51de3876186307d5cb7490087031747662500/regex-2026.2.28-cp313-cp313-win32.whl", hash = "sha256:fb66e5245db9652abd7196ace599b04d9c0e4aa7c8f0e2803938377835780081", size = 266398, upload-time = "2026-02-28T02:17:50.744Z" }, - { url = "https://files.pythonhosted.org/packages/85/4f/16e9ebb1fe5425e11b9596c8d57bf8877dcb32391da0bfd33742e3290637/regex-2026.2.28-cp313-cp313-win_amd64.whl", hash = "sha256:71a911098be38c859ceb3f9a9ce43f4ed9f4c6720ad8684a066ea246b76ad9ff", size = 277282, upload-time = "2026-02-28T02:17:53.074Z" }, - { url = "https://files.pythonhosted.org/packages/07/b4/92851335332810c5a89723bf7a7e35c7209f90b7d4160024501717b28cc9/regex-2026.2.28-cp313-cp313-win_arm64.whl", hash = "sha256:39bb5727650b9a0275c6a6690f9bb3fe693a7e6cc5c3155b1240aedf8926423e", size = 270382, upload-time = "2026-02-28T02:17:54.888Z" }, - { url = "https://files.pythonhosted.org/packages/24/07/6c7e4cec1e585959e96cbc24299d97e4437a81173217af54f1804994e911/regex-2026.2.28-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:97054c55db06ab020342cc0d35d6f62a465fa7662871190175f1ad6c655c028f", size = 492541, upload-time = "2026-02-28T02:17:56.813Z" }, - { url = "https://files.pythonhosted.org/packages/7c/13/55eb22ada7f43d4f4bb3815b6132183ebc331c81bd496e2d1f3b8d862e0d/regex-2026.2.28-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0d25a10811de831c2baa6aef3c0be91622f44dd8d31dd12e69f6398efb15e48b", size = 292984, upload-time = "2026-02-28T02:17:58.538Z" }, - { url = "https://files.pythonhosted.org/packages/5b/11/c301f8cb29ce9644a5ef85104c59244e6e7e90994a0f458da4d39baa8e17/regex-2026.2.28-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d6cfe798d8da41bb1862ed6e0cba14003d387c3c0c4a5d45591076ae9f0ce2f8", size = 291509, upload-time = "2026-02-28T02:18:00.208Z" }, - { url = "https://files.pythonhosted.org/packages/b5/43/aabe384ec1994b91796e903582427bc2ffaed9c4103819ed3c16d8e749f3/regex-2026.2.28-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd0ce43e71d825b7c0661f9c54d4d74bd97c56c3fd102a8985bcfea48236bacb", size = 809429, upload-time = "2026-02-28T02:18:02.328Z" }, - { url = "https://files.pythonhosted.org/packages/04/b8/8d2d987a816720c4f3109cee7c06a4b24ad0e02d4fc74919ab619e543737/regex-2026.2.28-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00945d007fd74a9084d2ab79b695b595c6b7ba3698972fadd43e23230c6979c1", size = 869422, upload-time = "2026-02-28T02:18:04.23Z" }, - { url = "https://files.pythonhosted.org/packages/fc/ad/2c004509e763c0c3719f97c03eca26473bffb3868d54c5f280b8cd4f9e3d/regex-2026.2.28-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bec23c11cbbf09a4df32fe50d57cbdd777bc442269b6e39a1775654f1c95dee2", size = 915175, upload-time = "2026-02-28T02:18:06.791Z" }, - { url = "https://files.pythonhosted.org/packages/55/c2/fd429066da487ef555a9da73bf214894aec77fc8c66a261ee355a69871a8/regex-2026.2.28-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cdcc17d935c8f9d3f4db5c2ebe2640c332e3822ad5d23c2f8e0228e6947943a", size = 812044, upload-time = "2026-02-28T02:18:08.736Z" }, - { url = "https://files.pythonhosted.org/packages/5b/ca/feedb7055c62a3f7f659971bf45f0e0a87544b6b0cf462884761453f97c5/regex-2026.2.28-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a448af01e3d8031c89c5d902040b124a5e921a25c4e5e07a861ca591ce429341", size = 782056, upload-time = "2026-02-28T02:18:10.777Z" }, - { url = "https://files.pythonhosted.org/packages/95/30/1aa959ed0d25c1dd7dd5047ea8ba482ceaef38ce363c401fd32a6b923e60/regex-2026.2.28-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:10d28e19bd4888e4abf43bd3925f3c134c52fdf7259219003588a42e24c2aa25", size = 798743, upload-time = "2026-02-28T02:18:13.025Z" }, - { url = "https://files.pythonhosted.org/packages/3b/1f/dadb9cf359004784051c897dcf4d5d79895f73a1bbb7b827abaa4814ae80/regex-2026.2.28-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:99985a2c277dcb9ccb63f937451af5d65177af1efdeb8173ac55b61095a0a05c", size = 864633, upload-time = "2026-02-28T02:18:16.84Z" }, - { url = "https://files.pythonhosted.org/packages/a7/f1/b9a25eb24e1cf79890f09e6ec971ee5b511519f1851de3453bc04f6c902b/regex-2026.2.28-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:e1e7b24cb3ae9953a560c563045d1ba56ee4749fbd05cf21ba571069bd7be81b", size = 770862, upload-time = "2026-02-28T02:18:18.892Z" }, - { url = "https://files.pythonhosted.org/packages/02/9a/c5cb10b7aa6f182f9247a30cc9527e326601f46f4df864ac6db588d11fcd/regex-2026.2.28-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d8511a01d0e4ee1992eb3ba19e09bc1866fe03f05129c3aec3fdc4cbc77aad3f", size = 854788, upload-time = "2026-02-28T02:18:21.475Z" }, - { url = "https://files.pythonhosted.org/packages/0a/50/414ba0731c4bd40b011fa4703b2cc86879ec060c64f2a906e65a56452589/regex-2026.2.28-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:aaffaecffcd2479ce87aa1e74076c221700b7c804e48e98e62500ee748f0f550", size = 800184, upload-time = "2026-02-28T02:18:23.492Z" }, - { url = "https://files.pythonhosted.org/packages/69/50/0c7290987f97e7e6830b0d853f69dc4dc5852c934aae63e7fdcd76b4c383/regex-2026.2.28-cp313-cp313t-win32.whl", hash = "sha256:ef77bdde9c9eba3f7fa5b58084b29bbcc74bcf55fdbeaa67c102a35b5bd7e7cc", size = 269137, upload-time = "2026-02-28T02:18:25.375Z" }, - { url = "https://files.pythonhosted.org/packages/68/80/ef26ff90e74ceb4051ad6efcbbb8a4be965184a57e879ebcbdef327d18fa/regex-2026.2.28-cp313-cp313t-win_amd64.whl", hash = "sha256:98adf340100cbe6fbaf8e6dc75e28f2c191b1be50ffefe292fb0e6f6eefdb0d8", size = 280682, upload-time = "2026-02-28T02:18:27.205Z" }, - { url = "https://files.pythonhosted.org/packages/69/8b/fbad9c52e83ffe8f97e3ed1aa0516e6dff6bb633a41da9e64645bc7efdc5/regex-2026.2.28-cp313-cp313t-win_arm64.whl", hash = "sha256:2fb950ac1d88e6b6a9414381f403797b236f9fa17e1eee07683af72b1634207b", size = 271735, upload-time = "2026-02-28T02:18:29.015Z" }, - { url = "https://files.pythonhosted.org/packages/cf/03/691015f7a7cb1ed6dacb2ea5de5682e4858e05a4c5506b2839cd533bbcd6/regex-2026.2.28-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:78454178c7df31372ea737996fb7f36b3c2c92cccc641d251e072478afb4babc", size = 489497, upload-time = "2026-02-28T02:18:30.889Z" }, - { url = "https://files.pythonhosted.org/packages/c6/ba/8db8fd19afcbfa0e1036eaa70c05f20ca8405817d4ad7a38a6b4c2f031ac/regex-2026.2.28-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:5d10303dd18cedfd4d095543998404df656088240bcfd3cd20a8f95b861f74bd", size = 291295, upload-time = "2026-02-28T02:18:33.426Z" }, - { url = "https://files.pythonhosted.org/packages/5a/79/9aa0caf089e8defef9b857b52fc53801f62ff868e19e5c83d4a96612eba1/regex-2026.2.28-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:19a9c9e0a8f24f39d575a6a854d516b48ffe4cbdcb9de55cb0570a032556ecff", size = 289275, upload-time = "2026-02-28T02:18:35.247Z" }, - { url = "https://files.pythonhosted.org/packages/eb/26/ee53117066a30ef9c883bf1127eece08308ccf8ccd45c45a966e7a665385/regex-2026.2.28-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:09500be324f49b470d907b3ef8af9afe857f5cca486f853853f7945ddbf75911", size = 797176, upload-time = "2026-02-28T02:18:37.15Z" }, - { url = "https://files.pythonhosted.org/packages/05/1b/67fb0495a97259925f343ae78b5d24d4a6624356ae138b57f18bd43006e4/regex-2026.2.28-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb1c4ff62277d87a7335f2c1ea4e0387b8f2b3ad88a64efd9943906aafad4f33", size = 863813, upload-time = "2026-02-28T02:18:39.478Z" }, - { url = "https://files.pythonhosted.org/packages/a0/1d/93ac9bbafc53618091c685c7ed40239a90bf9f2a82c983f0baa97cb7ae07/regex-2026.2.28-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b8b3f1be1738feadc69f62daa250c933e85c6f34fa378f54a7ff43807c1b9117", size = 908678, upload-time = "2026-02-28T02:18:41.619Z" }, - { url = "https://files.pythonhosted.org/packages/c7/7a/a8f5e0561702b25239846a16349feece59712ae20598ebb205580332a471/regex-2026.2.28-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc8ed8c3f41c27acb83f7b6a9eb727a73fc6663441890c5cb3426a5f6a91ce7d", size = 801528, upload-time = "2026-02-28T02:18:43.624Z" }, - { url = "https://files.pythonhosted.org/packages/96/5d/ed6d4cbde80309854b1b9f42d9062fee38ade15f7eb4909f6ef2440403b5/regex-2026.2.28-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa539be029844c0ce1114762d2952ab6cfdd7c7c9bd72e0db26b94c3c36dcc5a", size = 775373, upload-time = "2026-02-28T02:18:46.102Z" }, - { url = "https://files.pythonhosted.org/packages/6a/e9/6e53c34e8068b9deec3e87210086ecb5b9efebdefca6b0d3fa43d66dcecb/regex-2026.2.28-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7900157786428a79615a8264dac1f12c9b02957c473c8110c6b1f972dcecaddf", size = 784859, upload-time = "2026-02-28T02:18:48.269Z" }, - { url = "https://files.pythonhosted.org/packages/48/3c/736e1c7ca7f0dcd2ae33819888fdc69058a349b7e5e84bc3e2f296bbf794/regex-2026.2.28-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0b1d2b07614d95fa2bf8a63fd1e98bd8fa2b4848dc91b1efbc8ba219fdd73952", size = 857813, upload-time = "2026-02-28T02:18:50.576Z" }, - { url = "https://files.pythonhosted.org/packages/6e/7c/48c4659ad9da61f58e79dbe8c05223e0006696b603c16eb6b5cbfbb52c27/regex-2026.2.28-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:b389c61aa28a79c2e0527ac36da579869c2e235a5b208a12c5b5318cda2501d8", size = 763705, upload-time = "2026-02-28T02:18:52.59Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a1/bc1c261789283128165f71b71b4b221dd1b79c77023752a6074c102f18d8/regex-2026.2.28-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f467cb602f03fbd1ab1908f68b53c649ce393fde056628dc8c7e634dab6bfc07", size = 848734, upload-time = "2026-02-28T02:18:54.595Z" }, - { url = "https://files.pythonhosted.org/packages/10/d8/979407faf1397036e25a5ae778157366a911c0f382c62501009f4957cf86/regex-2026.2.28-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e8c8cb2deba42f5ec1ede46374e990f8adc5e6456a57ac1a261b19be6f28e4e6", size = 789871, upload-time = "2026-02-28T02:18:57.34Z" }, - { url = "https://files.pythonhosted.org/packages/03/23/da716821277115fcb1f4e3de1e5dc5023a1e6533598c486abf5448612579/regex-2026.2.28-cp314-cp314-win32.whl", hash = "sha256:9036b400b20e4858d56d117108d7813ed07bb7803e3eed766675862131135ca6", size = 271825, upload-time = "2026-02-28T02:18:59.202Z" }, - { url = "https://files.pythonhosted.org/packages/91/ff/90696f535d978d5f16a52a419be2770a8d8a0e7e0cfecdbfc31313df7fab/regex-2026.2.28-cp314-cp314-win_amd64.whl", hash = "sha256:1d367257cd86c1cbb97ea94e77b373a0bbc2224976e247f173d19e8f18b4afa7", size = 280548, upload-time = "2026-02-28T02:19:01.049Z" }, - { url = "https://files.pythonhosted.org/packages/69/f9/5e1b5652fc0af3fcdf7677e7df3ad2a0d47d669b34ac29a63bb177bb731b/regex-2026.2.28-cp314-cp314-win_arm64.whl", hash = "sha256:5e68192bb3a1d6fb2836da24aa494e413ea65853a21505e142e5b1064a595f3d", size = 273444, upload-time = "2026-02-28T02:19:03.255Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/8389f9e940ac89bcf58d185e230a677b4fd07c5f9b917603ad5c0f8fa8fe/regex-2026.2.28-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a5dac14d0872eeb35260a8e30bac07ddf22adc1e3a0635b52b02e180d17c9c7e", size = 492546, upload-time = "2026-02-28T02:19:05.378Z" }, - { url = "https://files.pythonhosted.org/packages/7b/c7/09441d27ce2a6fa6a61ea3150ea4639c1dcda9b31b2ea07b80d6937b24dd/regex-2026.2.28-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ec0c608b7a7465ffadb344ed7c987ff2f11ee03f6a130b569aa74d8a70e8333c", size = 292986, upload-time = "2026-02-28T02:19:07.24Z" }, - { url = "https://files.pythonhosted.org/packages/fb/69/4144b60ed7760a6bd235e4087041f487aa4aa62b45618ce018b0c14833ea/regex-2026.2.28-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c7815afb0ca45456613fdaf60ea9c993715511c8d53a83bc468305cbc0ee23c7", size = 291518, upload-time = "2026-02-28T02:19:09.698Z" }, - { url = "https://files.pythonhosted.org/packages/2d/be/77e5426cf5948c82f98c53582009ca9e94938c71f73a8918474f2e2990bb/regex-2026.2.28-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b059e71ec363968671693a78c5053bd9cb2fe410f9b8e4657e88377ebd603a2e", size = 809464, upload-time = "2026-02-28T02:19:12.494Z" }, - { url = "https://files.pythonhosted.org/packages/45/99/2c8c5ac90dc7d05c6e7d8e72c6a3599dc08cd577ac476898e91ca787d7f1/regex-2026.2.28-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b8cf76f1a29f0e99dcfd7aef1551a9827588aae5a737fe31442021165f1920dc", size = 869553, upload-time = "2026-02-28T02:19:15.151Z" }, - { url = "https://files.pythonhosted.org/packages/53/34/daa66a342f0271e7737003abf6c3097aa0498d58c668dbd88362ef94eb5d/regex-2026.2.28-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:180e08a435a0319e6a4821c3468da18dc7001987e1c17ae1335488dfe7518dd8", size = 915289, upload-time = "2026-02-28T02:19:17.331Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c7/e22c2aaf0a12e7e22ab19b004bb78d32ca1ecc7ef245949935463c5567de/regex-2026.2.28-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e496956106fd59ba6322a8ea17141a27c5040e5ee8f9433ae92d4e5204462a0", size = 812156, upload-time = "2026-02-28T02:19:20.011Z" }, - { url = "https://files.pythonhosted.org/packages/7f/bb/2dc18c1efd9051cf389cd0d7a3a4d90f6804b9fff3a51b5dc3c85b935f71/regex-2026.2.28-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bba2b18d70eeb7b79950f12f633beeecd923f7c9ad6f6bae28e59b4cb3ab046b", size = 782215, upload-time = "2026-02-28T02:19:22.047Z" }, - { url = "https://files.pythonhosted.org/packages/17/1e/9e4ec9b9013931faa32226ec4aa3c71fe664a6d8a2b91ac56442128b332f/regex-2026.2.28-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6db7bfae0f8a2793ff1f7021468ea55e2699d0790eb58ee6ab36ae43aa00bc5b", size = 798925, upload-time = "2026-02-28T02:19:24.173Z" }, - { url = "https://files.pythonhosted.org/packages/71/57/a505927e449a9ccb41e2cc8d735e2abe3444b0213d1cf9cb364a8c1f2524/regex-2026.2.28-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d0b02e8b7e5874b48ae0f077ecca61c1a6a9f9895e9c6dfb191b55b242862033", size = 864701, upload-time = "2026-02-28T02:19:26.376Z" }, - { url = "https://files.pythonhosted.org/packages/a6/ad/c62cb60cdd93e13eac5b3d9d6bd5d284225ed0e3329426f94d2552dd7cca/regex-2026.2.28-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:25b6eb660c5cf4b8c3407a1ed462abba26a926cc9965e164268a3267bcc06a43", size = 770899, upload-time = "2026-02-28T02:19:29.38Z" }, - { url = "https://files.pythonhosted.org/packages/3c/5a/874f861f5c3d5ab99633e8030dee1bc113db8e0be299d1f4b07f5b5ec349/regex-2026.2.28-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:5a932ea8ad5d0430351ff9c76c8db34db0d9f53c1d78f06022a21f4e290c5c18", size = 854727, upload-time = "2026-02-28T02:19:31.494Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ca/d2c03b0efde47e13db895b975b2be6a73ed90b8ba963677927283d43bf74/regex-2026.2.28-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1c2c95e1a2b0f89d01e821ff4de1be4b5d73d1f4b0bf679fa27c1ad8d2327f1a", size = 800366, upload-time = "2026-02-28T02:19:34.248Z" }, - { url = "https://files.pythonhosted.org/packages/14/bd/ee13b20b763b8989f7c75d592bfd5de37dc1181814a2a2747fedcf97e3ba/regex-2026.2.28-cp314-cp314t-win32.whl", hash = "sha256:bbb882061f742eb5d46f2f1bd5304055be0a66b783576de3d7eef1bed4778a6e", size = 274936, upload-time = "2026-02-28T02:19:36.313Z" }, - { url = "https://files.pythonhosted.org/packages/cb/e7/d8020e39414c93af7f0d8688eabcecece44abfd5ce314b21dfda0eebd3d8/regex-2026.2.28-cp314-cp314t-win_amd64.whl", hash = "sha256:6591f281cb44dc13de9585b552cec6fc6cf47fb2fe7a48892295ee9bc4a612f9", size = 284779, upload-time = "2026-02-28T02:19:38.625Z" }, - { url = "https://files.pythonhosted.org/packages/13/c0/ad225f4a405827486f1955283407cf758b6d2fb966712644c5f5aef33d1b/regex-2026.2.28-cp314-cp314t-win_arm64.whl", hash = "sha256:dee50f1be42222f89767b64b283283ef963189da0dda4a515aa54a5563c62dec", size = 275010, upload-time = "2026-02-28T02:19:40.65Z" }, +version = "2026.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/c1/6b30b775c7bcc6cf6506a4d4741c2123e8d99cd50f3fe8cbd731f5fef526/regex-2026.9.3.tar.gz", hash = "sha256:aabd43208e335f4c3f0b56de3464b066dd425983a58f6eeb5738bcd7465403db", size = 416720, upload-time = "2026-09-01T00:53:43.821Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/fc/b4ff5d9550796f9f955b6c2470f37e2b582ec944638f3004499929a69c96/regex-2026.9.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6fe39780de6916ecb1c664eda81802e6310fd9d4a07dccb13a86b63918e00e65", size = 493967, upload-time = "2026-09-01T00:50:00.23Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/ab60a24883b810a4b29065540f56e6f72590bb97d704d7e0ee7bcd2ba98c/regex-2026.9.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d75065d9f6ed1afb2a41588def408cf442cee65b3651cbd7e86650146127bb4", size = 295227, upload-time = "2026-09-01T00:50:01.804Z" }, + { url = "https://files.pythonhosted.org/packages/6c/e6/e3bf61522cd82eda85a90dd8f69c839f0ce1da64dc6e87f803bb79267c59/regex-2026.9.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5dd356a646fe549d42b766cb9075b54eccd3f20604d87a3eff25f1430bba5b2e", size = 290639, upload-time = "2026-09-01T00:50:03.244Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b2/e8492b054c5c9fe09bc0aefdc308b8f845ec2094b1ae779accec8f14dedb/regex-2026.9.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90aa2a7f9cc1cd2e8082db61618a2ed0f4197eef52266a6420e88277f184e328", size = 791712, upload-time = "2026-09-01T00:50:05.539Z" }, + { url = "https://files.pythonhosted.org/packages/48/03/429ee3479bb438f7b2b41147fa68d63a7ef9c7486fcd3960e04e61133cf3/regex-2026.9.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f975a75dae06e88665e4a709873d936a7e8f9445e3d354b75de0e735d28cf71", size = 861711, upload-time = "2026-09-01T00:50:06.988Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9e/6ebf3cec538ed49d0a4cf5729129fe39580ddfa80c1c59c4274651762202/regex-2026.9.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1d52739de118acf82bfbaf7046955ead4fb613d24ba4187be565cd91ff9a64e", size = 905894, upload-time = "2026-09-01T00:50:08.437Z" }, + { url = "https://files.pythonhosted.org/packages/19/e4/a7bb1e4038a170714c824dc7c739f8b911cb1172f87ab950be39e0691f95/regex-2026.9.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:99034ec353c973e2c89555866083491b9a2dbe81f2fbe15fb0f2b68506232f01", size = 801135, upload-time = "2026-09-01T00:50:10.308Z" }, + { url = "https://files.pythonhosted.org/packages/bd/47/1b318fb249d5fa3dcb0375ad284d34155f0da4a2fe92c8f01edef0cb0258/regex-2026.9.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3cc1a82779315f7b2a4642d5028b39057838cd4c0415744d4e53c8b512352141", size = 773578, upload-time = "2026-09-01T00:50:12.125Z" }, + { url = "https://files.pythonhosted.org/packages/a6/16/63f2644a0a3e92623f83ccadd04873916f5b650e2028bc44ee9df104a839/regex-2026.9.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c0ea77435b1d5a27cccf27f8762f50e73fd2d94e8e412a8e5cdedb650b36d5fc", size = 783915, upload-time = "2026-09-01T00:50:14.081Z" }, + { url = "https://files.pythonhosted.org/packages/ca/f2/f078963d1dbd3632f6e25f7d972cc9a9332649ed239995bfeb403cf88a3b/regex-2026.9.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6c997a1703401089bc02d731e360127428fb4ecdf6524268e8975882ff02528b", size = 854924, upload-time = "2026-09-01T00:50:15.826Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/457ac94a2a2aaa2ab5c076324dfd7e38e76f36d4d78923c4ddb8c3d93d75/regex-2026.9.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:70082b2a8f099b8bf660b553b22a4b8ffd34fcc09ef154fc6b9c7108518fc124", size = 763172, upload-time = "2026-09-01T00:50:17.682Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/db145a6ad935cdc6c694381543d53e6e7d7212287d4f8885f965e91ea4a4/regex-2026.9.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f5e8a0ce681ddabf6a35d7b817d74a94ab237ae7233a5b97118db0b4e524473f", size = 844983, upload-time = "2026-09-01T00:50:19.572Z" }, + { url = "https://files.pythonhosted.org/packages/76/00/100346c5cc2384a7ce0adfb7dbb11f4a865c76e3a920fe68542042da66ea/regex-2026.9.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3367d5eefae493ac2a1586ec11cc8213c3305528ed3f8e19d5c3871cc01da6ce", size = 789532, upload-time = "2026-09-01T00:50:21.142Z" }, + { url = "https://files.pythonhosted.org/packages/39/62/d878c1d3eed2768f19e8b6fa4aa81b0a0e6bc23fa34e64173ef48816844a/regex-2026.9.3-cp311-cp311-win32.whl", hash = "sha256:33d3a772ff62c882a5d1402045e17c0199f33b9b173139071c4203e7e0292420", size = 266766, upload-time = "2026-09-01T00:50:22.863Z" }, + { url = "https://files.pythonhosted.org/packages/e5/da/6a0ac27bacb39aca3452cdd7d1bfc361608ad000cea3e1b0d7dbaa1e2b15/regex-2026.9.3-cp311-cp311-win_amd64.whl", hash = "sha256:8c8a63d55cdf3c716225a2f8741a1df7a52b5fa98ac7530165c9cc9b32feabcc", size = 278007, upload-time = "2026-09-01T00:50:24.597Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a2/b23997814f48f823bcda615764b4fb00d72a5a06bdbb41cafa895ec62428/regex-2026.9.3-cp311-cp311-win_arm64.whl", hash = "sha256:cb6374a84f11a6b25e63aa69ee2d015286048c1efee93c4a3b5b8df62da79ff8", size = 276960, upload-time = "2026-09-01T00:50:26.101Z" }, + { url = "https://files.pythonhosted.org/packages/da/cb/cba530bc3b068fc337f8f455c63ef5ee91a4eb4c76ecf5998e5cef5aaa6b/regex-2026.9.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5db80d0b1c8238940b5957dd66b5c818ea40a221f6652fb717c027a562d09c77", size = 496699, upload-time = "2026-09-01T00:50:27.98Z" }, + { url = "https://files.pythonhosted.org/packages/81/39/f2e9fb6bbbc80f8bf67ad79d7e2e8866f7837d7c24c692f7faf8f1272e7e/regex-2026.9.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:35d48ce3dee087b63b15cd0a7a3110d0a76c29edbe1f2ad0520b8c4adb7cb596", size = 297018, upload-time = "2026-09-01T00:50:29.487Z" }, + { url = "https://files.pythonhosted.org/packages/a1/b6/c16ee58840baf7659def27ef6f62f3d9a9909670d3c1b4b98bb8b8ee47e2/regex-2026.9.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1f22e0d21ae7016c77175c139a7fca465b988efc1280df4816c79752068d9e2e", size = 292008, upload-time = "2026-09-01T00:50:30.929Z" }, + { url = "https://files.pythonhosted.org/packages/cb/b4/4987bf0f17604669b4ea5aef219886d0a73188c4716ff3a7d275d4d15c15/regex-2026.9.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:233662cf8cfdfe3c0e58aa8f7bbefc579b5be0ac34546f123c159804179e8687", size = 796101, upload-time = "2026-09-01T00:50:32.486Z" }, + { url = "https://files.pythonhosted.org/packages/0b/95/2a9ab02a68c8a61dc0b4882ed643b1a95740d9dc291dc26c77d19af79691/regex-2026.9.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7d2eed2e4d231278a2ccab3f4bfa2c1e39855f336475f7756a281d767d2b1753", size = 865435, upload-time = "2026-09-01T00:50:34.171Z" }, + { url = "https://files.pythonhosted.org/packages/04/92/0570d41559b446c97c1148cb9ebc1df09f2949b03c7c9bfee09976b3465f/regex-2026.9.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e674cecb61cb160be392da07fd8a71509ef927f437fbf3215432692ed385151", size = 911828, upload-time = "2026-09-01T00:50:35.72Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8b/9cc6d4123033f7cb82df6cd8ce19eb0fc18a964afe060a03c9b26757c9f3/regex-2026.9.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:665207e41bacd435db001099eeab44103197c2c1a729d73ade74688a905ed4ce", size = 801965, upload-time = "2026-09-01T00:50:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/c9/98/39262e91aa87a67c82cbe90a0df4c3d382c7a44811fe80067904085211b4/regex-2026.9.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7a7ddc9a8ca1795166a1ca80364b8ce74187fc210e112d3fb048b711b934f36c", size = 776192, upload-time = "2026-09-01T00:50:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/24/e9/3bb93fe4ee4b6f8ce7ba69b527c4a63cfa3393fc425ab26486041fe441c8/regex-2026.9.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3037d02425863ce9501afbaa04ba967162810004bacde39a53ea9a5b740eb32", size = 785053, upload-time = "2026-09-01T00:50:41.156Z" }, + { url = "https://files.pythonhosted.org/packages/f9/05/31d5bc2553a700c0dfc6b5b6a13c61cdcd1210fde1e304cfa18a33f138b2/regex-2026.9.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3de4eab8c763393b75bbb26f81934ab2cc8794f48f79e90622e3ab7ea57f3d14", size = 860546, upload-time = "2026-09-01T00:50:42.746Z" }, + { url = "https://files.pythonhosted.org/packages/65/a3/2e1e854d80becda0f061093805bbfc037a5849448f46d0a2b71a070d45e2/regex-2026.9.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:98620c9c4c22568ad70f57b80527c780b6f8fd26e36507bf8e2273262a228275", size = 765841, upload-time = "2026-09-01T00:50:44.5Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d6/43d02948cedde2e8476ac893ea02755ee5ee1b21c531fda92d80e114f0bc/regex-2026.9.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0b1ba3aaaf5776de473ee16625ac60ac195abb0343afb273575a8201d99be089", size = 852147, upload-time = "2026-09-01T00:50:46.474Z" }, + { url = "https://files.pythonhosted.org/packages/21/ff/adb4e2d08afe8f4c6df004d94604257e1f72af7ba328af7715601585aba4/regex-2026.9.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56d8659c65166641d8f1b5efccc391c62c8a899eff4d528b981cc62b7b402a4b", size = 789761, upload-time = "2026-09-01T00:50:48.749Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e1/1490d1351758e87f6e702cf2025036bdc7bc59182e2ff5c7bec004b19aed/regex-2026.9.3-cp312-cp312-win32.whl", hash = "sha256:837c1859913798d8bebcd98d4a037e113f8d79e81733009bf590e449769eecb3", size = 267150, upload-time = "2026-09-01T00:50:50.414Z" }, + { url = "https://files.pythonhosted.org/packages/d5/49/4c40cf722d84d60e807a08ef4c3f579216bf97df60c4a1b10be49655d302/regex-2026.9.3-cp312-cp312-win_amd64.whl", hash = "sha256:1ba1dbbb93c5c5629c1861763aec5bfa9f05ad24ef450694130e25029ce7bc36", size = 277773, upload-time = "2026-09-01T00:50:51.963Z" }, + { url = "https://files.pythonhosted.org/packages/aa/af/c48b3b2b4244b4b090554c78d3387e9ae7b859f3dbf7148a27d427e9e5b8/regex-2026.9.3-cp312-cp312-win_arm64.whl", hash = "sha256:d7b3a8a4bbd83ad8b29758f5d24bab10a3f2de87970db36f1e3651c733353136", size = 277122, upload-time = "2026-09-01T00:50:53.778Z" }, + { url = "https://files.pythonhosted.org/packages/9b/d8/1fb6053247efc5b5a1d7b3b7881dcf42861f8ba46bd72a2ff126469d4053/regex-2026.9.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1d9148e47cfa1a067138867996b1d5d825de0132fed8dac3c92eebaaf312d280", size = 496442, upload-time = "2026-09-01T00:50:55.351Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b7/0b9c1c0385365ad12deba0bdf93a70ad9f97d1a919cc5699c33b449ad662/regex-2026.9.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:33c2860b73ea342c0a42bee9ebe3b3a0de3d68c580c4dcb52241cf4b6663731b", size = 296913, upload-time = "2026-09-01T00:50:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/5750ebabdb010ffb0d31fceae68c7a8f3876c06140c3bd6a7feff6240d6d/regex-2026.9.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e33dfc13c02d9c4e55bcf3f3b2eb448537823a6f6f30bf737b2974b63a530bc9", size = 291783, upload-time = "2026-09-01T00:50:58.892Z" }, + { url = "https://files.pythonhosted.org/packages/96/12/3ee1542b6428a0955ac5a31562d87966a417c0e0a4c1e5c62ec822fe78de/regex-2026.9.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:48e84252a16234ee860206a738f9a5084f830a5d0a1370a418d3af4e917f5e08", size = 796114, upload-time = "2026-09-01T00:51:00.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/24/5ad415b80958d79f2da66a1997743ea8c38a1e9e2f63660f7cbf9303eda4/regex-2026.9.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3077ace9bf59f8513c8471a817a5af63699987dc024f535c1eac3447a4d70211", size = 865524, upload-time = "2026-09-01T00:51:02.25Z" }, + { url = "https://files.pythonhosted.org/packages/2b/8a/c298d469f5dd12b11d4e7b8c7714c808b9edace73118eaf753fc327c9429/regex-2026.9.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:635482cd183a1856da75a39c473a2e222697b7927f1955f586db83fc8a5da17c", size = 911948, upload-time = "2026-09-01T00:51:04.099Z" }, + { url = "https://files.pythonhosted.org/packages/3f/a6/e9a59b507cdf7a9735df4bab92b4ea9d2ca2e665c6de14c112db7e3c0926/regex-2026.9.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27f0809798071f56fb1bc536bb93714a95e8ed2ec0dfd869f095deebb30fd11a", size = 801945, upload-time = "2026-09-01T00:51:05.878Z" }, + { url = "https://files.pythonhosted.org/packages/56/1d/443e5541fd23d97c841ccddf90dbc9a964bd00e0ec9219b45a3eb0cde552/regex-2026.9.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d514026ca1c473cc14440e4d7bdf6721c642455b647a74c8143c0f22da358c28", size = 776223, upload-time = "2026-09-01T00:51:07.58Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ae/78d48b14582f5733facaa2e001499b98707d8b8b1b31ab7559053c18f168/regex-2026.9.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b9190d4901d7786af9ab0ec46172e27cf7d72cdba2b82ee38eb40aadd3239a6e", size = 785072, upload-time = "2026-09-01T00:51:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/55/42/2f4ac830de12d264c1486f749988003a7f5a0366d7557a75de2418461466/regex-2026.9.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6c0f60b05cc708e6cdf68dbca86b7a36d99695962db0189bb1b3884ca3b28e90", size = 860644, upload-time = "2026-09-01T00:51:11.558Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/6b44c6cf56f353744e6066a1310177b7b195afb11d33dff4d6cf3ec52720/regex-2026.9.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:b7b7e6be82fd6d5256adabb82253c5c307de981cc20c0ce4cff0cbe6de88529b", size = 765862, upload-time = "2026-09-01T00:51:13.371Z" }, + { url = "https://files.pythonhosted.org/packages/64/0f/c5a8023dfc988cddd2c614b1afbe547aedab1ffebd5b5d9cd58f6ab28908/regex-2026.9.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b5f85bffdfe17da7dfff78eb32b261c27f3cd64c060033645079493f4cebc8e9", size = 852081, upload-time = "2026-09-01T00:51:15.324Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f4/ab0ba467ebcaecfddf72ea7b3527a6088863094377c1303a3379284ef621/regex-2026.9.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d10a442c6450ebd35aa89392b8e4b0459ba474df63fc5e6828573d0a31a627ec", size = 789774, upload-time = "2026-09-01T00:51:17.057Z" }, + { url = "https://files.pythonhosted.org/packages/4e/fa/8e2f3021ff5ee3aafc80eed8f16663be5e16c7b663d72e4bd2c9e71e1433/regex-2026.9.3-cp313-cp313-win32.whl", hash = "sha256:63fa79eab192623acb169de1dfe8e733598c4047d06f7712347d1bb810a5ad20", size = 267125, upload-time = "2026-09-01T00:51:18.837Z" }, + { url = "https://files.pythonhosted.org/packages/6d/25/6d20a309c2e4b554cc33579dd55b0bb50d0c2ace7ce6f084be2327c330fc/regex-2026.9.3-cp313-cp313-win_amd64.whl", hash = "sha256:185c1ae881856208dda05708b6c908aff76878e59c998c8548d365c1bbcaf1bd", size = 277741, upload-time = "2026-09-01T00:51:20.583Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b3/0282ae58fc167953809a968850622f89955e0fd70a65df882e5d488138b0/regex-2026.9.3-cp313-cp313-win_arm64.whl", hash = "sha256:db6538d733047f9ce4b74ee29c77643a1f99e4ca36e273495da93fbeedd2f03f", size = 277123, upload-time = "2026-09-01T00:51:22.332Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a0/322f050f77289a1acf99b5c400fe201780bdcfa8f8876fbe250ae3de37af/regex-2026.9.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:99896cc18fb421be93e337d6bf2c1686ba330bc2d5c0ef581c842f0639a5e886", size = 496701, upload-time = "2026-09-01T00:51:24.146Z" }, + { url = "https://files.pythonhosted.org/packages/91/30/85df182aec58aaa34b02a3f0a99068bf34a7c9b3f9bd7ff06d58aa866e1d/regex-2026.9.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0540de6e7917f89acaf9771bdcd6fa7e505c67416d3b283e52ad4ab25399c6d6", size = 297055, upload-time = "2026-09-01T00:51:25.812Z" }, + { url = "https://files.pythonhosted.org/packages/27/8f/64b8bbd0316baaede047dbdbf2bcc96f10a4eeddec2e9cbf289e855e636f/regex-2026.9.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:647983d2609be6155c748249e770ab7e75e15e386cbf15469569f3eaf165bbb7", size = 292008, upload-time = "2026-09-01T00:51:27.415Z" }, + { url = "https://files.pythonhosted.org/packages/b8/87/224614ef16bc8336269a84b578fb0dbab1433b584aaefc5f0680cacf9227/regex-2026.9.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7bb75921a4885d30d9e881d7a595ce50407a8a82933e15451ad7e0d89d1a5944", size = 796341, upload-time = "2026-09-01T00:51:29.344Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e1/10ad988d3b673db86353666c56cb32e1b6246ca1ab0c81018a58ebec889c/regex-2026.9.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f93c60d8c522b4ecea35dd6c58cc42f251ecb12882a5d67d4bd8d12137fbd05b", size = 866311, upload-time = "2026-09-01T00:51:31.592Z" }, + { url = "https://files.pythonhosted.org/packages/08/16/8d597f97bb6215876512cd7b3c47179c0eb79d1820ab2ed8412d3ba5599a/regex-2026.9.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9e61478a8a06e6456ff2e66b9ac18f7f28d76a75fa5fda0c5198a4de07b8dcb8", size = 911869, upload-time = "2026-09-01T00:51:33.926Z" }, + { url = "https://files.pythonhosted.org/packages/89/e3/f6bcb26472873b9308df329f507d9c6cb526e3f9aed847498f153f2b7539/regex-2026.9.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f64c66b3b13758b4f8f56f17972cd0ce5d0033d19d7332ed32e2dbdbce94dec", size = 801375, upload-time = "2026-09-01T00:51:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/bc/a5/38c7cd736bfd58b190412dbf5a81aae1c38456c538d5c4987f5980cbfa3d/regex-2026.9.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6b5dc780377e35be6b0cf6fec7fb4a45cadb1e834bc0ef2ce596cc015290ef69", size = 776522, upload-time = "2026-09-01T00:51:37.81Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ac/8ecefa1c9b57b29fcbe963797435e69a3ba6a9773d802b3d4fca4c75ecf6/regex-2026.9.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f02091b425bbcc2d8481913855c744baa4dd73e334814337b201d837e9040ef7", size = 785727, upload-time = "2026-09-01T00:51:39.995Z" }, + { url = "https://files.pythonhosted.org/packages/5a/4c/af402c5aa95615ab66620d775de07ea09b0fea206cd79e3c8a5716d5fc96/regex-2026.9.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ad2027883344e70ddddff02259411f80faf47d5e779eb0e39cb95ee546fa628c", size = 861375, upload-time = "2026-09-01T00:51:41.829Z" }, + { url = "https://files.pythonhosted.org/packages/dd/7b/b83dd26470fa4cc6d7beee9b3c52ff0e988ba636c1367a04d4b2829d8d95/regex-2026.9.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0edd12c8201222f58817689dc61fe44893f3f2f2aee530b211dfa92af84df9cc", size = 766221, upload-time = "2026-09-01T00:51:44.7Z" }, + { url = "https://files.pythonhosted.org/packages/94/5b/3b91b19d8fb11a353738cec56b19440bf181f3f9a0298d91140fb4061778/regex-2026.9.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4a9cd8485a6729387c889c88c24d5eace39dc0c0c9ddb9003f9c81751f654b69", size = 851737, upload-time = "2026-09-01T00:51:46.708Z" }, + { url = "https://files.pythonhosted.org/packages/2e/be/ea0762fd8a12fa711f1a626283f6d79a3d8a81df4eaada4fbe82009e46f6/regex-2026.9.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:994fa00a9b0d14c6e6926ff5ade98d4d83676a5eeab6f87718170e481396d380", size = 789455, upload-time = "2026-09-01T00:51:49.2Z" }, + { url = "https://files.pythonhosted.org/packages/d1/dd/b9af9d65de78831fb76cb8ef420db4094295b09f93f8abd17c867261c4fb/regex-2026.9.3-cp314-cp314-win32.whl", hash = "sha256:4f39485bb02dae23e14cdbad086ab0e468756775bc5c64bb69e7cb756ebc1dcd", size = 272528, upload-time = "2026-09-01T00:51:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/d0/fe/ecb15616ae7aa4892299b9ca7c20ef0dd6e5c833643b7ed46e27ff5fcccd/regex-2026.9.3-cp314-cp314-win_amd64.whl", hash = "sha256:445623b1337e971ccc571d3642aeb3f2fec77e60b6ee193dd7688168471d1846", size = 280812, upload-time = "2026-09-01T00:51:53.211Z" }, + { url = "https://files.pythonhosted.org/packages/18/4f/60efc748152c82b967bd186c356dd1fc21c4f6c225b8026b52547ae60c9f/regex-2026.9.3-cp314-cp314-win_arm64.whl", hash = "sha256:9887e9455398a1517294dec14e23ed9a178c8dd909f2788e971b66623d3f7c16", size = 281095, upload-time = "2026-09-01T00:51:55.052Z" }, + { url = "https://files.pythonhosted.org/packages/d2/cc/aa269583e8986be9604ef5fb9e2164f33108f6220c896efff2dfb0d4e4c4/regex-2026.9.3-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:c1fa3f84cee5211a3e574ba76ac9596df8c8d16a855f31a25681d23eadcc6c16", size = 501014, upload-time = "2026-09-01T00:51:56.902Z" }, + { url = "https://files.pythonhosted.org/packages/22/51/bb8a3f3a47beccbf92c3bcda67c2c0336c13c2e10f9744306275f428ebc9/regex-2026.9.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d539a51be176e874ed66029b2df8cb8e1123a3e6420d52a690de6effded4f20d", size = 299376, upload-time = "2026-09-01T00:51:59.121Z" }, + { url = "https://files.pythonhosted.org/packages/48/ec/c646df2b62c1b1334e28581715ed776814c8ac80c0c9106175f0c61ce12a/regex-2026.9.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5133884ec10c9d6bcf7fed4ceb98fb3a6acbb6c2ba1bab6c4b6700d6c39c7595", size = 294424, upload-time = "2026-09-01T00:52:01.113Z" }, + { url = "https://files.pythonhosted.org/packages/d2/18/3dd8c49cc4af440a9a1a0ebb2b4a939398398c81fb7fecbc3c7d534924a1/regex-2026.9.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fecba510f6b8f9cf1dfd103d785a61da47221cf09d4cec10946d1950daf1a17", size = 811421, upload-time = "2026-09-01T00:52:03.289Z" }, + { url = "https://files.pythonhosted.org/packages/71/32/c35f031aec040b9136189e3f403681e3168f1f9862e31d9df92ea599f2c1/regex-2026.9.3-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:72df83ee0eb89b070e28d1260786d13485b98a8b80228c7439d303dd9aac9970", size = 870110, upload-time = "2026-09-01T00:52:05.746Z" }, + { url = "https://files.pythonhosted.org/packages/a7/7b/931d962f9fbca3326f282a01fa5bed4d6734dab5215d1c20bac0ab85df58/regex-2026.9.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:610371fe95c7e8e824ad8762248836cabbb5a4ab8befb982cb7dc8df773075d3", size = 917489, upload-time = "2026-09-01T00:52:07.703Z" }, + { url = "https://files.pythonhosted.org/packages/46/1a/da0c28f2b1e65d46a24abc1dd33e0dc9ddbcfeeee7c5625103b3e09e548c/regex-2026.9.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:af06c9099df15ee44fda3fdfa002bfe37de02901c2b3a5ef350853861ef3b4b5", size = 817470, upload-time = "2026-09-01T00:52:09.686Z" }, + { url = "https://files.pythonhosted.org/packages/d4/a1/6a540ec40c3b8eeefec445362cf08bbb686b22d4088f97d3c1ea38323434/regex-2026.9.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e27003c0a93a5aa541c260bd8ba8b917a2af4c378eb684daa9f1565f5e363181", size = 784742, upload-time = "2026-09-01T00:52:11.778Z" }, + { url = "https://files.pythonhosted.org/packages/fc/88/75500ca27ba85256eb46119ebd99ec10ead4c3ffacd47fe7dfe034662960/regex-2026.9.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9ce34fc5a6f6c9b2ba4a8060009d989e4f55630e3f9c3bf5962f42dcc31355ef", size = 800609, upload-time = "2026-09-01T00:52:13.817Z" }, + { url = "https://files.pythonhosted.org/packages/67/33/18541aaad8b88977e2c3fbbb53c1a75f6f6c999800a82d2f7306581cb6b6/regex-2026.9.3-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2cbc83154c8b0201ada07bc5d6e37106df6325f2fda60d73228e2e8da7433cae", size = 864798, upload-time = "2026-09-01T00:52:16.137Z" }, + { url = "https://files.pythonhosted.org/packages/16/62/ffbfbeb5a3f2cca701afb9f8aebbfcbf37a4aae2f1584c96680142aab445/regex-2026.9.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f568bdc17b7ebb3a323ee8920468d2ed74af84da911a00d9585ac887bac73b88", size = 772892, upload-time = "2026-09-01T00:52:18.567Z" }, + { url = "https://files.pythonhosted.org/packages/23/14/04e2db28f490b1fcc684428bd2de21e1ec46f7d8d421b7f2cc74d120affd/regex-2026.9.3-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:ae9f7055e7357b2873866a3d77d0136a251ca2ae2dde3d8cdb819425d717c177", size = 858065, upload-time = "2026-09-01T00:52:20.674Z" }, + { url = "https://files.pythonhosted.org/packages/ac/14/c0cac930d1dc0b86c5d8ff10602f15df4fb15fa3f33e04a98be197f33618/regex-2026.9.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d963442186918577ad83e3a8c5564eeeba90e2da233ce59f6eeccbb7b5cf771e", size = 804739, upload-time = "2026-09-01T00:52:22.866Z" }, + { url = "https://files.pythonhosted.org/packages/01/12/69e6f4d02e11a5cb45033abede6e04b0da3716b1b6c311d4609be14a01a7/regex-2026.9.3-cp314-cp314t-win32.whl", hash = "sha256:bda6af6fb4d5fe9532620e4f72d3c7efcdf7472ead9037b184c0a060f0e7c71f", size = 274514, upload-time = "2026-09-01T00:52:25.224Z" }, + { url = "https://files.pythonhosted.org/packages/28/64/064b78028b52c0c21b9c0b362b1bdcee7179f009891ddf959d9b6390b30c/regex-2026.9.3-cp314-cp314t-win_amd64.whl", hash = "sha256:2d25e41851e41539898116ac760fcc856e2c1047a843a336b19e2c8e4a43ad16", size = 283797, upload-time = "2026-09-01T00:52:27.358Z" }, + { url = "https://files.pythonhosted.org/packages/f2/46/fbb41eb6cc92403ba9f896dfd56a1d5d31549c4033dfb7418da7ecd5802a/regex-2026.9.3-cp314-cp314t-win_arm64.whl", hash = "sha256:356fc21b4c313decb3214a4306bb5bd0623dce4a8d03d0d5ada6fb0b6a5b93b5", size = 283444, upload-time = "2026-09-01T00:52:29.632Z" }, + { url = "https://files.pythonhosted.org/packages/1b/1a/825beb1c803d579d1119723235ede2211949a8d320bcc79e3f6e5dd6090f/regex-2026.9.3-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:e337dceb936f333775cf51d49f6badb8cd3d2a6b27e8cb443a6c5861fbf3c1f9", size = 496929, upload-time = "2026-09-01T00:52:31.593Z" }, + { url = "https://files.pythonhosted.org/packages/ff/54/9b8255521680d54e1d157ed6532e3082998f47925d2ab3861550e0e89455/regex-2026.9.3-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bb2d4ad7f9bac398a7a19f07bd09fd5b2c1e4eeab316065aa84a305f62fc5361", size = 297050, upload-time = "2026-09-01T00:52:33.459Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8b/0e4df28fd94304050efc2373dff1803e5d62056cf744a694eaa376df2fe8/regex-2026.9.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:01bdce6a372efd5ae3d8560cedbc691be259a50206564bbaf04008b2937721ab", size = 292296, upload-time = "2026-09-01T00:52:35.309Z" }, + { url = "https://files.pythonhosted.org/packages/97/a0/689a8403f309a7a3f3f3e74b3e81b144602481d122c9e4070a601367f8e0/regex-2026.9.3-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55493b5b6cb6c4ec9a3c310d4ef947dbd34326ee4eecd4249e18e00d84f14be5", size = 798376, upload-time = "2026-09-01T00:52:37.235Z" }, + { url = "https://files.pythonhosted.org/packages/ad/a9/3d22278a38bb8adb254d345e60935316222091caf00a4233aaef388d636e/regex-2026.9.3-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:690ab9d06cd689b79aab84afa0984a1bc85ea4b93b0a42b015f3ff5e2f5b08be", size = 866607, upload-time = "2026-09-01T00:52:39.461Z" }, + { url = "https://files.pythonhosted.org/packages/69/ef/ecf8c5fdcec73ac0526fbd074be3bd1059b11cbda739ac0ab9001f311aed/regex-2026.9.3-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:24b4bbb65ff2c4e8c552c93c342bf5ffa0ad162d963d96bac7c1883c20e1b34e", size = 912160, upload-time = "2026-09-01T00:52:41.603Z" }, + { url = "https://files.pythonhosted.org/packages/68/e8/c07c3d2b44c9154672a33911cfcfba626c78d939d7e31103864d81ad319a/regex-2026.9.3-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fbaf76379bf2a72e534bbb1276d45e63a80d8cade17953ed79a350d6426262fb", size = 804589, upload-time = "2026-09-01T00:52:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/91/16/d42957c78b5934efd03ca5713d99952cbe71e2d0b3b24d9e78256f208587/regex-2026.9.3-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:57f405fcb82e2b78df88f04f8aa49ce513ce23bfea073b581597bd52545e9b3f", size = 784446, upload-time = "2026-09-01T00:52:45.866Z" }, + { url = "https://files.pythonhosted.org/packages/00/1e/4252dff33a1a5afb3271fb3859e992d34c47c040ecfc27bbfe6a7eaf99a8/regex-2026.9.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:a87ab35e92d40b53c5373af36625794e422f83ec56122f0a63871892856d721c", size = 787996, upload-time = "2026-09-01T00:52:48.345Z" }, + { url = "https://files.pythonhosted.org/packages/ee/19/90d31048bb402fd746a95922d7dfdb9de9f902c39c8d838f2fef05c9c92c/regex-2026.9.3-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:14bf4a88833c12a990dbf5cca77eb293401d60878be7d566a2d5096fb0216c27", size = 861752, upload-time = "2026-09-01T00:52:50.597Z" }, + { url = "https://files.pythonhosted.org/packages/88/77/42ad133550cbd64d8fb33ec5ff55e1f524b06c6328e9f3bf9c3e8652b525/regex-2026.9.3-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:862c29f7e7927e71391df6700079b1dd7c2aeb75115dc46e37004a092f8f16b7", size = 773488, upload-time = "2026-09-01T00:52:53.142Z" }, + { url = "https://files.pythonhosted.org/packages/78/8f/0c90e8fa607c36a65c360f6b02980aa7aa5885afa3d737daba4229bef086/regex-2026.9.3-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:61a9da95a836e7d300945891265bbeb6510a860863f6b01ed6397e6239a31253", size = 852603, upload-time = "2026-09-01T00:52:55.471Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bc/e1be309afb6be94916d8f4f000dbc66040fc0da2008b3f714dc4893259fd/regex-2026.9.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e7663a6803a47255c32cc7e17ae3ccfe02dba5b0849f87729651c0263a129d20", size = 794635, upload-time = "2026-09-01T00:52:57.611Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f7/15f6d0ea137316531c992e7468a4c6f6e39159a6780c2c16ec3ab56f2149/regex-2026.9.3-cp315-cp315-win32.whl", hash = "sha256:0ee4721472e00e96b3cceec545c9867f91815f628f6ea304ae6cea93a7e4e7ae", size = 272530, upload-time = "2026-09-01T00:52:59.644Z" }, + { url = "https://files.pythonhosted.org/packages/ba/8e/739168818790278dd4f1ba60c582fed41323508d7a8e850323851de1aa1f/regex-2026.9.3-cp315-cp315-win_amd64.whl", hash = "sha256:cc5d0f82cf05beb6c0d463398173a09357ed4f4a631f7641cef6f6480a05bc57", size = 280822, upload-time = "2026-09-01T00:53:02.57Z" }, + { url = "https://files.pythonhosted.org/packages/12/6a/55f5f6e92a8c7d6511e069d3e8a4b45e3506941c1a97dddf5a329b132be4/regex-2026.9.3-cp315-cp315-win_arm64.whl", hash = "sha256:6f198b622a3ccf02eeab00de71f6b2f45b1b50b1979aa56b25178c963e475950", size = 281102, upload-time = "2026-09-01T00:53:04.62Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6a/adaf9ed10ffdef60684bf8757c257400500342503f1ce5c7cffcdb2e8aaf/regex-2026.9.3-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:294ef8fb58a45f912513692380f366ca191940de5b3b3de5308ce2e8500d8ea4", size = 501333, upload-time = "2026-09-01T00:53:06.681Z" }, + { url = "https://files.pythonhosted.org/packages/64/dd/2cbc3b253a88fdb0c8ca641367dc2f280541fdf1722f201fc0be62797577/regex-2026.9.3-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:edfd2b0cad175780f8668fd6f66486354b8770e4057e44038daaa4a066f8ddff", size = 299273, upload-time = "2026-09-01T00:53:09.002Z" }, + { url = "https://files.pythonhosted.org/packages/2a/29/4818d31ec2555309b3935d3cd006a7f6076e11bb21eec9e72afee3db87b1/regex-2026.9.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:cda393bb35828e3993fcaf39c8ede78b16614954eb15fe77336d5d141be40f76", size = 294888, upload-time = "2026-09-01T00:53:11.009Z" }, + { url = "https://files.pythonhosted.org/packages/94/37/8970a4102a067e0a28e40a232bf536752c373da8a4f932755679f77fa9ac/regex-2026.9.3-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce8e5243d95068f595155663e3e18b1938b16cf716dce6cf2491ebc08b98d96", size = 813072, upload-time = "2026-09-01T00:53:13.566Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/2e52f5e9e92b79e324c9807e977fa6f9ab210b41ab5b7356307d5ffb2f4c/regex-2026.9.3-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2a1269856278ae8bc78342bf20191c1f10638d2c22df72364d2fa02d70d49f35", size = 869947, upload-time = "2026-09-01T00:53:15.773Z" }, + { url = "https://files.pythonhosted.org/packages/f6/42/c676e3f2dcb47ec941fa678019044cbac51db9441e09cec47392e4986013/regex-2026.9.3-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ac52b95a938789fcfcfbf6faf647b45d7be940f0c51f06991c1353db54c67aa", size = 915678, upload-time = "2026-09-01T00:53:18.384Z" }, + { url = "https://files.pythonhosted.org/packages/01/6a/73c971bcaf1d24e73aa3c8e0f767caccfdc9fcda021e59f28f6dcd12728b/regex-2026.9.3-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:490a81770599b17b8594227674872dfb215af05f2be0065a32a7c70175fd9e23", size = 817714, upload-time = "2026-09-01T00:53:20.723Z" }, + { url = "https://files.pythonhosted.org/packages/b2/7c/0e9bffb6cfa8a00e765eaea0d1e6d207bb98e620d5658c537c8d87df671c/regex-2026.9.3-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f584dd93ef6ddb10ba028e247fe1fc0ba52ed70ca4d596bb8d52bf4304c147ec", size = 792411, upload-time = "2026-09-01T00:53:22.925Z" }, + { url = "https://files.pythonhosted.org/packages/61/d4/b1d226e8a3d0c8aac577a517384ffaea7b9ff98b194ddbd0bef58b9d8738/regex-2026.9.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:c07eaf30bc072b179acc8ac50519a2a81f03f88a220750c6d83dadbdc33fb1de", size = 802521, upload-time = "2026-09-01T00:53:25.281Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ad/dc47f94a4ace898561dfafeed6052dd3551915f36aa1971f4387d6daf0d4/regex-2026.9.3-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:9e1c602c55dc8ec05cc7e0a8e31f3ad3d4644dbcb7ac39114105c9bd0384371f", size = 865266, upload-time = "2026-09-01T00:53:27.667Z" }, + { url = "https://files.pythonhosted.org/packages/41/7b/743565fc25cb36981186b4e9b42b3d093a7226cca328380cfd223c1b5a03/regex-2026.9.3-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:2ef9a284ec658d48ec57edfba0944d1582532601472f695b799ba08d37fd6544", size = 780298, upload-time = "2026-09-01T00:53:30.017Z" }, + { url = "https://files.pythonhosted.org/packages/c6/ad/d62e48324632eb0bae2c336da5810b51a14a9e23ba964f2828cc1ac23011/regex-2026.9.3-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:c90f34f1b1905d7d6c42b25b6ffb4e5066e6c95c7715169e3332b1760614d092", size = 858045, upload-time = "2026-09-01T00:53:32.397Z" }, + { url = "https://files.pythonhosted.org/packages/2e/41/8eff42a2516494bb6b36d33b15bcd861764dcc836ad7c0b746e46c513af9/regex-2026.9.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:307dbd844f678de48ea74cdc909b007729c50e6be7f182bdf6a1df6732374c69", size = 805406, upload-time = "2026-09-01T00:53:34.601Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1d/98198fd6dc185a172f5de48b8a4c7ca03c0d4285b1c1c32300672e7cf85a/regex-2026.9.3-cp315-cp315t-win32.whl", hash = "sha256:ce6505846d29f860966e0e9cfadaad1041a7d8ce7dc4af39940fcb1877dec29a", size = 274674, upload-time = "2026-09-01T00:53:36.77Z" }, + { url = "https://files.pythonhosted.org/packages/d2/82/dc0e76842ccddc8756471adff38c140d130c6b601b9ae5724274f1881e7a/regex-2026.9.3-cp315-cp315t-win_amd64.whl", hash = "sha256:2c71da070224baf426850d9eab23ac797d9859b1841dbda43012c01b69697803", size = 283692, upload-time = "2026-09-01T00:53:38.951Z" }, + { url = "https://files.pythonhosted.org/packages/da/3d/e0677f590f3473a3defb63ef6e3469154382a2a15ebafb364482d7ed3b01/regex-2026.9.3-cp315-cp315t-win_arm64.whl", hash = "sha256:ecc27adda0d1e1bc39793b41fdd562d2f4bc4dcee6ee0e3c733d519c332183b2", size = 283421, upload-time = "2026-09-01T00:53:41.393Z" }, ] [[package]] name = "requests" -version = "2.32.5" +version = "2.34.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2084,9 +2607,9 @@ dependencies = [ { 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, upload-time = "2025-08-18T20:46:02.573Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" }, ] [[package]] @@ -2103,24 +2626,49 @@ wheels = [ [[package]] name = "rich" -version = "14.3.3" +version = "15.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "markdown-it-py" }, { name = "pygments" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "ruff" +version = "0.16.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a4/7c/6adb35d70e7c027e308274557901c7e00fb3407750faf3620c184ae058cb/ruff-0.16.6.tar.gz", hash = "sha256:dcf8a73d2ff77e99dde91244b4da16feba7f14e6beeb4015dee7c5a909e99050", size = 4921251, upload-time = "2026-09-03T16:57:29.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/28/9cc1b79639e284ec103f43c88c644db4eb58cbd0ea1ca11f1193435369ac/ruff-0.16.6-py3-none-linux_armv6l.whl", hash = "sha256:61c368c26bf8e973e5ab14a2772de587bc068ea3f9a277f673380749b4898fb8", size = 10015638, upload-time = "2026-09-03T16:56:40.986Z" }, + { url = "https://files.pythonhosted.org/packages/71/11/627d342ef727ea7794edf74fe23d60a074b02c3acc2e9436684e782286ca/ruff-0.16.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ecf4f068e2e123e43a26e9db4e19524cc56563912404e83bbfca375757e45a32", size = 10220762, upload-time = "2026-09-03T16:56:44.681Z" }, + { url = "https://files.pythonhosted.org/packages/43/d9/b75668ce41e4c8d073d18d6d08672ba6906ce45d5c06ea4fdb2e84ce3853/ruff-0.16.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:99b62ea33baf130f50368798d841f0d95527b6d817bf31817b65dd058f1d314c", size = 9835082, upload-time = "2026-09-03T16:56:47.142Z" }, + { url = "https://files.pythonhosted.org/packages/99/97/123ab10b05cde889c107c20f5a9774955104b5552796a2a8584b089ae8eb/ruff-0.16.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7fbf89013f2bb3f6835a6038ff658dc8a1b38c98dc8e724b964168ad4e881876", size = 9949304, upload-time = "2026-09-03T16:56:49.813Z" }, + { url = "https://files.pythonhosted.org/packages/3e/58/a4a2c59dd2e5b85929c912d9cac3056eb9ee8c7e75e9b9fe3e109174966b/ruff-0.16.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56a67065e22efa6bc4d498299d3bb06c0c90aace8fac2068b5a12f9dc4d8d51d", size = 9840612, upload-time = "2026-09-03T16:56:52.368Z" }, + { url = "https://files.pythonhosted.org/packages/61/6a/ff8c8626a786c4f49d48ced4a752dadbca65f5263005f9c2416578194694/ruff-0.16.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e25cc89174874b176a157e4428d66761c2c0c006654419bf384f967f361ff1b1", size = 10543465, upload-time = "2026-09-03T16:56:55.089Z" }, + { url = "https://files.pythonhosted.org/packages/ad/bb/c47535923365f337b82e28192e4e9eef2176511007cfd99a62fc22df5dad/ruff-0.16.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0700580ed5303723cb3c11c2f1d2a8913ce77b7ea86646dddb887f5417a9ba70", size = 11267576, upload-time = "2026-09-03T16:56:57.791Z" }, + { url = "https://files.pythonhosted.org/packages/ba/50/e5119a5212b5cd63b51e1f4b25e7bd636a6668fc069a3160b108ad7e3c16/ruff-0.16.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15f1d0b6e165a6e56567befb6629f8209271311d990bae0f37e6d065035ef5f3", size = 10781993, upload-time = "2026-09-03T16:57:00.666Z" }, + { url = "https://files.pythonhosted.org/packages/8b/98/083d8b4ef3c51a0d19db84367791cbe9f44e4b53343d19dfa83556e1cd9a/ruff-0.16.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d72c591a96986ee4268860e2b7235082129ca5e4cb9cbba653a4b57c11893757", size = 10317748, upload-time = "2026-09-03T16:57:03.428Z" }, + { url = "https://files.pythonhosted.org/packages/9a/29/68f7ff2c5ad95f19f00627ac2de95644e25fe47371ea60b2db1fd952315e/ruff-0.16.6-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:65a006baa18f33324325814c864daef03541d51564b98c517610ea756ab7003e", size = 10540096, upload-time = "2026-09-03T16:57:06.182Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f9/79a8f6de85968641d68a7863aeec577551924ef066a990a48ff93167beab/ruff-0.16.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cd02a7bf1a21a8735228a3e8c95a9dc5cf86bd2a52194f4aaae2a5755b4de0f4", size = 10100494, upload-time = "2026-09-03T16:57:09.194Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e8/b81a22d9b90c00b892ccf2fa2ac36fa95de4c13ab85aea3e73795cfe4651/ruff-0.16.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:31b36f1e5ad85e0737f09d2be4e512e2e283583c14015da3b9dc07359ac0fc88", size = 9843663, upload-time = "2026-09-03T16:57:12.168Z" }, + { url = "https://files.pythonhosted.org/packages/39/aa/54f516ec5e5a11c4afdceb1c454ebb054ffb96e4f4a1705580b4346abd35/ruff-0.16.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:61029b4ab4aa723fd3064fab96b1d814492596bf0c792679fffcbde1e1679953", size = 10282461, upload-time = "2026-09-03T16:57:15.077Z" }, + { url = "https://files.pythonhosted.org/packages/52/0b/38d0aa8aa32372b96dc44f97b22e576c4147808271aab7b2cb1e353d4445/ruff-0.16.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:9ac8998457832c2061709d900856b7ad271dace0cb41f346588d540162bfa718", size = 10728808, upload-time = "2026-09-03T16:57:17.797Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e5/9e274e24eeb027640ffc7442f21239f16d17f47acec15ae34f32e03a5c79/ruff-0.16.6-py3-none-win32.whl", hash = "sha256:0b87d9d16fcb63e8018423ca1d50b7260f15cb2da33e30db4baad4183a948c25", size = 10049212, upload-time = "2026-09-03T16:57:20.55Z" }, + { url = "https://files.pythonhosted.org/packages/22/31/72472449414223ed1a2da236b992adbb1a2ae59e34794574810f60ce068e/ruff-0.16.6-py3-none-win_amd64.whl", hash = "sha256:10d21c51c3495d8eaea7b703a16592117ea6eb1d649e36335aa965ff1173eb39", size = 10556402, upload-time = "2026-09-03T16:57:23.501Z" }, + { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" }, ] [[package]] name = "setuptools" -version = "82.0.1" +version = "84.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" }, + { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" }, ] [[package]] @@ -2143,55 +2691,53 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.48" +version = "2.0.52" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1f/73/b4a9737255583b5fa858e0bb8e116eb94b88c910164ed2ed719147bde3de/sqlalchemy-2.0.48.tar.gz", hash = "sha256:5ca74f37f3369b45e1f6b7b06afb182af1fd5dde009e4ffd831830d98cbe5fe7", size = 9886075, upload-time = "2026-03-02T15:28:51.474Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d7/6d/b8b78b5b80f3c3ab3f7fa90faa195ec3401f6d884b60221260fd4d51864c/sqlalchemy-2.0.48-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b4c575df7368b3b13e0cebf01d4679f9a28ed2ae6c1cd0b1d5beffb6b2007dc", size = 2157184, upload-time = "2026-03-02T15:38:28.161Z" }, - { url = "https://files.pythonhosted.org/packages/21/4b/4f3d4a43743ab58b95b9ddf5580a265b593d017693df9e08bd55780af5bb/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e83e3f959aaa1c9df95c22c528096d94848a1bc819f5d0ebf7ee3df0ca63db6c", size = 3313555, upload-time = "2026-03-02T15:58:57.21Z" }, - { url = "https://files.pythonhosted.org/packages/21/dd/3b7c53f1dbbf736fd27041aee68f8ac52226b610f914085b1652c2323442/sqlalchemy-2.0.48-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f7b7243850edd0b8b97043f04748f31de50cf426e939def5c16bedb540698f7", size = 3313057, upload-time = "2026-03-02T15:52:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cc/3e600a90ae64047f33313d7d32e5ad025417f09d2ded487e8284b5e21a15/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:82745b03b4043e04600a6b665cb98697c4339b24e34d74b0a2ac0a2488b6f94d", size = 3265431, upload-time = "2026-03-02T15:58:59.096Z" }, - { url = "https://files.pythonhosted.org/packages/8b/19/780138dacfe3f5024f4cf96e4005e91edf6653d53d3673be4844578faf1d/sqlalchemy-2.0.48-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e5e088bf43f6ee6fec7dbf1ef7ff7774a616c236b5c0cb3e00662dd71a56b571", size = 3287646, upload-time = "2026-03-02T15:52:31.569Z" }, - { url = "https://files.pythonhosted.org/packages/40/fd/f32ced124f01a23151f4777e4c705f3a470adc7bd241d9f36a7c941a33bf/sqlalchemy-2.0.48-cp311-cp311-win32.whl", hash = "sha256:9c7d0a77e36b5f4b01ca398482230ab792061d243d715299b44a0b55c89fe617", size = 2116956, upload-time = "2026-03-02T15:46:54.535Z" }, - { url = "https://files.pythonhosted.org/packages/58/d5/dd767277f6feef12d05651538f280277e661698f617fa4d086cce6055416/sqlalchemy-2.0.48-cp311-cp311-win_amd64.whl", hash = "sha256:583849c743e0e3c9bb7446f5b5addeacedc168d657a69b418063dfdb2d90081c", size = 2141627, upload-time = "2026-03-02T15:46:55.849Z" }, - { url = "https://files.pythonhosted.org/packages/ef/91/a42ae716f8925e9659df2da21ba941f158686856107a61cc97a95e7647a3/sqlalchemy-2.0.48-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:348174f228b99f33ca1f773e85510e08927620caa59ffe7803b37170df30332b", size = 2155737, upload-time = "2026-03-02T15:49:13.207Z" }, - { url = "https://files.pythonhosted.org/packages/b9/52/f75f516a1f3888f027c1cfb5d22d4376f4b46236f2e8669dcb0cddc60275/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53667b5f668991e279d21f94ccfa6e45b4e3f4500e7591ae59a8012d0f010dcb", size = 3337020, upload-time = "2026-03-02T15:50:34.547Z" }, - { url = "https://files.pythonhosted.org/packages/37/9a/0c28b6371e0cdcb14f8f1930778cb3123acfcbd2c95bb9cf6b4a2ba0cce3/sqlalchemy-2.0.48-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34634e196f620c7a61d18d5cf7dc841ca6daa7961aed75d532b7e58b309ac894", size = 3349983, upload-time = "2026-03-02T15:53:25.542Z" }, - { url = "https://files.pythonhosted.org/packages/1c/46/0aee8f3ff20b1dcbceb46ca2d87fcc3d48b407925a383ff668218509d132/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:546572a1793cc35857a2ffa1fe0e58571af1779bcc1ffa7c9fb0839885ed69a9", size = 3279690, upload-time = "2026-03-02T15:50:36.277Z" }, - { url = "https://files.pythonhosted.org/packages/ce/8c/a957bc91293b49181350bfd55e6dfc6e30b7f7d83dc6792d72043274a390/sqlalchemy-2.0.48-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:07edba08061bc277bfdc772dd2a1a43978f5a45994dd3ede26391b405c15221e", size = 3314738, upload-time = "2026-03-02T15:53:27.519Z" }, - { url = "https://files.pythonhosted.org/packages/4b/44/1d257d9f9556661e7bdc83667cc414ba210acfc110c82938cb3611eea58f/sqlalchemy-2.0.48-cp312-cp312-win32.whl", hash = "sha256:908a3fa6908716f803b86896a09a2c4dde5f5ce2bb07aacc71ffebb57986ce99", size = 2115546, upload-time = "2026-03-02T15:54:31.591Z" }, - { url = "https://files.pythonhosted.org/packages/f2/af/c3c7e1f3a2b383155a16454df62ae8c62a30dd238e42e68c24cebebbfae6/sqlalchemy-2.0.48-cp312-cp312-win_amd64.whl", hash = "sha256:68549c403f79a8e25984376480959975212a670405e3913830614432b5daa07a", size = 2142484, upload-time = "2026-03-02T15:54:34.072Z" }, - { url = "https://files.pythonhosted.org/packages/d1/c6/569dc8bf3cd375abc5907e82235923e986799f301cd79a903f784b996fca/sqlalchemy-2.0.48-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e3070c03701037aa418b55d36532ecb8f8446ed0135acb71c678dbdf12f5b6e4", size = 2152599, upload-time = "2026-03-02T15:49:14.41Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ff/f4e04a4bd5a24304f38cb0d4aa2ad4c0fb34999f8b884c656535e1b2b74c/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2645b7d8a738763b664a12a1542c89c940daa55196e8d73e55b169cc5c99f65f", size = 3278825, upload-time = "2026-03-02T15:50:38.269Z" }, - { url = "https://files.pythonhosted.org/packages/fe/88/cb59509e4668d8001818d7355d9995be90c321313078c912420603a7cb95/sqlalchemy-2.0.48-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b19151e76620a412c2ac1c6f977ab1b9fa7ad43140178345136456d5265b32ed", size = 3295200, upload-time = "2026-03-02T15:53:29.366Z" }, - { url = "https://files.pythonhosted.org/packages/87/dc/1609a4442aefd750ea2f32629559394ec92e89ac1d621a7f462b70f736ff/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5b193a7e29fd9fa56e502920dca47dffe60f97c863494946bd698c6058a55658", size = 3226876, upload-time = "2026-03-02T15:50:39.802Z" }, - { url = "https://files.pythonhosted.org/packages/37/c3/6ae2ab5ea2fa989fbac4e674de01224b7a9d744becaf59bb967d62e99bed/sqlalchemy-2.0.48-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:36ac4ddc3d33e852da9cb00ffb08cea62ca05c39711dc67062ca2bb1fae35fd8", size = 3265045, upload-time = "2026-03-02T15:53:31.421Z" }, - { url = "https://files.pythonhosted.org/packages/6f/82/ea4665d1bb98c50c19666e672f21b81356bd6077c4574e3d2bbb84541f53/sqlalchemy-2.0.48-cp313-cp313-win32.whl", hash = "sha256:389b984139278f97757ea9b08993e7b9d1142912e046ab7d82b3fbaeb0209131", size = 2113700, upload-time = "2026-03-02T15:54:35.825Z" }, - { url = "https://files.pythonhosted.org/packages/b7/2b/b9040bec58c58225f073f5b0c1870defe1940835549dafec680cbd58c3c3/sqlalchemy-2.0.48-cp313-cp313-win_amd64.whl", hash = "sha256:d612c976cbc2d17edfcc4c006874b764e85e990c29ce9bd411f926bbfb02b9a2", size = 2139487, upload-time = "2026-03-02T15:54:37.079Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f4/7b17bd50244b78a49d22cc63c969d71dc4de54567dc152a9b46f6fae40ce/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69f5bc24904d3bc3640961cddd2523e361257ef68585d6e364166dfbe8c78fae", size = 3558851, upload-time = "2026-03-02T15:57:48.607Z" }, - { url = "https://files.pythonhosted.org/packages/20/0d/213668e9aca61d370f7d2a6449ea4ec699747fac67d4bda1bb3d129025be/sqlalchemy-2.0.48-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd08b90d211c086181caed76931ecfa2bdfc83eea3cfccdb0f82abc6c4b876cb", size = 3525525, upload-time = "2026-03-02T16:04:38.058Z" }, - { url = "https://files.pythonhosted.org/packages/85/d7/a84edf412979e7d59c69b89a5871f90a49228360594680e667cb2c46a828/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1ccd42229aaac2df431562117ac7e667d702e8e44afdb6cf0e50fa3f18160f0b", size = 3466611, upload-time = "2026-03-02T15:57:50.759Z" }, - { url = "https://files.pythonhosted.org/packages/86/55/42404ce5770f6be26a2b0607e7866c31b9a4176c819e9a7a5e0a055770be/sqlalchemy-2.0.48-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f0dcbc588cd5b725162c076eb9119342f6579c7f7f55057bb7e3c6ff27e13121", size = 3475812, upload-time = "2026-03-02T16:04:40.092Z" }, - { url = "https://files.pythonhosted.org/packages/ae/ae/29b87775fadc43e627cf582fe3bda4d02e300f6b8f2747c764950d13784c/sqlalchemy-2.0.48-cp313-cp313t-win32.whl", hash = "sha256:9764014ef5e58aab76220c5664abb5d47d5bc858d9debf821e55cfdd0f128485", size = 2141335, upload-time = "2026-03-02T15:52:51.518Z" }, - { url = "https://files.pythonhosted.org/packages/91/44/f39d063c90f2443e5b46ec4819abd3d8de653893aae92df42a5c4f5843de/sqlalchemy-2.0.48-cp313-cp313t-win_amd64.whl", hash = "sha256:e2f35b4cccd9ed286ad62e0a3c3ac21e06c02abc60e20aa51a3e305a30f5fa79", size = 2173095, upload-time = "2026-03-02T15:52:52.79Z" }, - { url = "https://files.pythonhosted.org/packages/f7/b3/f437eaa1cf028bb3c927172c7272366393e73ccd104dcf5b6963f4ab5318/sqlalchemy-2.0.48-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e2d0d88686e3d35a76f3e15a34e8c12d73fc94c1dea1cd55782e695cc14086dd", size = 2154401, upload-time = "2026-03-02T15:49:17.24Z" }, - { url = "https://files.pythonhosted.org/packages/6c/1c/b3abdf0f402aa3f60f0df6ea53d92a162b458fca2321d8f1f00278506402/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49b7bddc1eebf011ea5ab722fdbe67a401caa34a350d278cc7733c0e88fecb1f", size = 3274528, upload-time = "2026-03-02T15:50:41.489Z" }, - { url = "https://files.pythonhosted.org/packages/f2/5e/327428a034407651a048f5e624361adf3f9fbac9d0fa98e981e9c6ff2f5e/sqlalchemy-2.0.48-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:426c5ca86415d9b8945c7073597e10de9644802e2ff502b8e1f11a7a2642856b", size = 3279523, upload-time = "2026-03-02T15:53:32.962Z" }, - { url = "https://files.pythonhosted.org/packages/2a/ca/ece73c81a918add0965b76b868b7b5359e068380b90ef1656ee995940c02/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:288937433bd44e3990e7da2402fabc44a3c6c25d3704da066b85b89a85474ae0", size = 3224312, upload-time = "2026-03-02T15:50:42.996Z" }, - { url = "https://files.pythonhosted.org/packages/88/11/fbaf1ae91fa4ee43f4fe79661cead6358644824419c26adb004941bdce7c/sqlalchemy-2.0.48-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8183dc57ae7d9edc1346e007e840a9f3d6aa7b7f165203a99e16f447150140d2", size = 3246304, upload-time = "2026-03-02T15:53:34.937Z" }, - { url = "https://files.pythonhosted.org/packages/fa/a8/5fb0deb13930b4f2f698c5541ae076c18981173e27dd00376dbaea7a9c82/sqlalchemy-2.0.48-cp314-cp314-win32.whl", hash = "sha256:1182437cb2d97988cfea04cf6cdc0b0bb9c74f4d56ec3d08b81e23d621a28cc6", size = 2116565, upload-time = "2026-03-02T15:54:38.321Z" }, - { url = "https://files.pythonhosted.org/packages/95/7e/e83615cb63f80047f18e61e31e8e32257d39458426c23006deeaf48f463b/sqlalchemy-2.0.48-cp314-cp314-win_amd64.whl", hash = "sha256:144921da96c08feb9e2b052c5c5c1d0d151a292c6135623c6b2c041f2a45f9e0", size = 2142205, upload-time = "2026-03-02T15:54:39.831Z" }, - { url = "https://files.pythonhosted.org/packages/83/e3/69d8711b3f2c5135e9cde5f063bc1605860f0b2c53086d40c04017eb1f77/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5aee45fd2c6c0f2b9cdddf48c48535e7471e42d6fb81adfde801da0bd5b93241", size = 3563519, upload-time = "2026-03-02T15:57:52.387Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4f/a7cce98facca73c149ea4578981594aaa5fd841e956834931de503359336/sqlalchemy-2.0.48-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7cddca31edf8b0653090cbb54562ca027c421c58ddde2c0685f49ff56a1690e0", size = 3528611, upload-time = "2026-03-02T16:04:42.097Z" }, - { url = "https://files.pythonhosted.org/packages/cd/7d/5936c7a03a0b0cb0fa0cc425998821c6029756b0855a8f7ee70fba1de955/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7a936f1bb23d370b7c8cc079d5fce4c7d18da87a33c6744e51a93b0f9e97e9b3", size = 3472326, upload-time = "2026-03-02T15:57:54.423Z" }, - { url = "https://files.pythonhosted.org/packages/f4/33/cea7dfc31b52904efe3dcdc169eb4514078887dff1f5ae28a7f4c5d54b3c/sqlalchemy-2.0.48-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e004aa9248e8cb0a5f9b96d003ca7c1c0a5da8decd1066e7b53f59eb8ce7c62b", size = 3478453, upload-time = "2026-03-02T16:04:44.584Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/32107c4d13be077a9cae61e9ae49966a35dc4bf442a8852dd871db31f62e/sqlalchemy-2.0.48-cp314-cp314t-win32.whl", hash = "sha256:b8438ec5594980d405251451c5b7ea9aa58dda38eb7ac35fb7e4c696712ee24f", size = 2147209, upload-time = "2026-03-02T15:52:54.274Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d7/1e073da7a4bc645eb83c76067284a0374e643bc4be57f14cc6414656f92c/sqlalchemy-2.0.48-cp314-cp314t-win_amd64.whl", hash = "sha256:d854b3970067297f3a7fbd7a4683587134aa9b3877ee15aa29eea478dc68f933", size = 2182198, upload-time = "2026-03-02T15:52:55.606Z" }, - { url = "https://files.pythonhosted.org/packages/46/2c/9664130905f03db57961b8980b05cab624afd114bf2be2576628a9f22da4/sqlalchemy-2.0.48-py3-none-any.whl", hash = "sha256:a66fe406437dd65cacd96a72689a3aaaecaebbcd62d81c5ac1c0fdbeac835096", size = 1940202, upload-time = "2026-03-02T15:52:43.285Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/3b/21/77b4c147963073040dc3c3a5cb7a8c3001a1893c0209432cb77f9df836aa/sqlalchemy-2.0.52.tar.gz", hash = "sha256:5e2d46356ac2ccb7d268ab6c2319ac6a2b42f1b8d5fd8bd3d46855cd82abee97", size = 9945637, upload-time = "2026-08-11T19:07:09.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/08/cc5f7627b92f1456bc0b5fb7e98af4600248abe422a44da0d17a3fe6a448/sqlalchemy-2.0.52-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0c3ce43907374889f3352bdcc6195c970148a2cb71574cd0237a5071a37fb6c", size = 2172460, upload-time = "2026-08-11T20:58:22.429Z" }, + { url = "https://files.pythonhosted.org/packages/ed/dc/9a2abad8bfc8fdcd38c64adc056aeefab7aaa96ecd32f5e8c140e6375f17/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a0d48c4b80717c61385b4e966e087c839a66cfd7b780641dcb428f4dba65608", size = 3355720, upload-time = "2026-08-11T21:00:06.746Z" }, + { url = "https://files.pythonhosted.org/packages/a8/73/e75597b5841043e3c74055d00d4feb53d9a49a5c89ba2450d2d9aab53597/sqlalchemy-2.0.52-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938325a5373267afc53bfbe72983b20fbd64ca47842aac62433c3da1137ecff1", size = 3354394, upload-time = "2026-08-11T21:05:51.454Z" }, + { url = "https://files.pythonhosted.org/packages/12/25/410fbc6c2f1fa8310f4ef1b6847d47d0ac1c042c7b4e81eaaca063d030a9/sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f8438a98d49424acf69d0d53c0a522951dfe49a6f2d86417fbb37ad3066ab43", size = 3306991, upload-time = "2026-08-11T21:00:08.603Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ba/25ffd5c24681ea4b46e62c80ceca8200ce204de1773366321306cf3f608a/sqlalchemy-2.0.52-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4699dbb8d396d199e7e78fd4d525e3ad3d6008a9c8c0160b87e74c606c2c3736", size = 3327454, upload-time = "2026-08-11T21:05:53.368Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f1/0f1b1d4800e51218e736a06ed55a3b2a59c257600bbaca7673bf13d2dbec/sqlalchemy-2.0.52-cp311-cp311-win32.whl", hash = "sha256:cef328349452ae152637df4d11ce5a0919ecdf0a363e16c830c3518ee33bde72", size = 2131248, upload-time = "2026-08-11T21:09:50.765Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/04d2ac5ad66f3d31278f37064ed5f5ef3fe653f7bdaa67036663f223d186/sqlalchemy-2.0.52-cp311-cp311-win_amd64.whl", hash = "sha256:f1c850792a3b25a3ad74dade3f05e4f402cdebfea27438bcadafaa1617f77bcc", size = 2156943, upload-time = "2026-08-11T21:09:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d5/1b77a026d161f98a08f11af1a5f6c47b98ee7c7e2648af525a1004826c78/sqlalchemy-2.0.52-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be8c49131665dfe2cc74c498aa1240ffb548d0fd901325dd11c2c7a18956f727", size = 2170940, upload-time = "2026-08-11T20:58:11.25Z" }, + { url = "https://files.pythonhosted.org/packages/54/bd/f444444adb37b5d53753fb1730ee7a421628e2e3b756c4da461af7e6394a/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b2d9e507a458832adcfbd8af6e2036ddf069b7710b799448542ebccae2dceee", size = 3383415, upload-time = "2026-08-11T21:02:38.534Z" }, + { url = "https://files.pythonhosted.org/packages/be/57/2eadf93a552568c57e8680b7e58bb5e9770d80942a1bdbaf4f2f63f0d7c8/sqlalchemy-2.0.52-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8738008376d22f30f411ea3efecf39b51110b6996d80bb73786f30bcfdd5fd3b", size = 3398577, upload-time = "2026-08-11T21:16:59.092Z" }, + { url = "https://files.pythonhosted.org/packages/15/c3/2887cf9dd111d1fbf05d22165b404c221ef43e029f7a2695e7302f27a7cc/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37a4d548327b6cab9c7d8cdb4e0e82feabee0110c4d150059068e2d1cfbd99ee", size = 3328225, upload-time = "2026-08-11T21:02:40.183Z" }, + { url = "https://files.pythonhosted.org/packages/02/0f/466bdf9e1feeeef5587f868c187d8687e21ff8c85b1775e9041130181132/sqlalchemy-2.0.52-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e49f51a5d59857a7a0dcaf9469febf7197d9394bd88f00d69c2c4e848112cdbf", size = 3357374, upload-time = "2026-08-11T21:17:01.076Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/5c2b4583904af4173076dda1c9e53c9e2ffc7a702d2efde0216bbacbf7cb/sqlalchemy-2.0.52-cp312-cp312-win32.whl", hash = "sha256:afda3ec521d0517d0de783fc70030775841900896d832de5bbd066549290470e", size = 2129366, upload-time = "2026-08-11T21:14:50.991Z" }, + { url = "https://files.pythonhosted.org/packages/ed/06/543dab8ef62d4e9fb96fb31a30c2b8b14a8763bccf48d428294d6b3041c0/sqlalchemy-2.0.52-cp312-cp312-win_amd64.whl", hash = "sha256:2d5e53e36e37129fe0be8b9d08b6e4052c10a963ee6cda56c8c10dcc194b99ca", size = 2157344, upload-time = "2026-08-11T21:14:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/7f/18/e30c6fe1eca1bf34a39fbdd6066121cc9974c850faf6f349eac563697a26/sqlalchemy-2.0.52-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2eb3c6a64b1bfe6704777cfd504e7b8ad093a5f3e03ce67663a5e6742f294e43", size = 2167724, upload-time = "2026-08-11T20:58:12.679Z" }, + { url = "https://files.pythonhosted.org/packages/d0/56/2e17d161a4f7ecc1c2ffb93e607b4e1898bb551b451b283235acb8f6ce47/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:923bb183c1dc64fdf7b717965e3d59938ec4f8b8710b419a21ce403e5da9a9e1", size = 3321189, upload-time = "2026-08-11T21:02:41.932Z" }, + { url = "https://files.pythonhosted.org/packages/cf/b8/8490916e893f3f8d74dc9cc54c078619364999dee37047a188e73abbc852/sqlalchemy-2.0.52-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:651d6d8782e80679e6151707c7b490834d46ada526328895abf567f25e63d29c", size = 3338185, upload-time = "2026-08-11T21:17:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f7/752cc8ee453da222829b3f5c4613614bf750d97429363b70414fa10478e4/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b08cddb8989775e3c88799d86704bdfc3ee6e9846118201aa5997f16f27e3a15", size = 3271698, upload-time = "2026-08-11T21:02:43.963Z" }, + { url = "https://files.pythonhosted.org/packages/51/e6/074ade0c07b9e4c8e8bca46820320ed94df9702afdb6f2af06623068d2e6/sqlalchemy-2.0.52-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ab66fa9618269390d4dfa222f2f2f88f7bc4bf5da13905131b818217db7e8057", size = 3308936, upload-time = "2026-08-11T21:17:04.172Z" }, + { url = "https://files.pythonhosted.org/packages/66/07/557c0d04716705599227945ac14e0a17ad0338e899f37d8c2ddff4dcc663/sqlalchemy-2.0.52-cp313-cp313-win32.whl", hash = "sha256:c63bda077685c85ca513286547a531ba57e7a68cf0a7ed3bafcc2bbd18896f4d", size = 2127308, upload-time = "2026-08-11T21:14:53.879Z" }, + { url = "https://files.pythonhosted.org/packages/96/4e/226eda27654318ce525d043025221f689abef883da2c7126f9065121618c/sqlalchemy-2.0.52-cp313-cp313-win_amd64.whl", hash = "sha256:9876b09b9f1ce7398b0ffece585c0a911244c53191187341f6bcae640e133751", size = 2153876, upload-time = "2026-08-11T21:14:55.527Z" }, + { url = "https://files.pythonhosted.org/packages/d5/f5/71cb30af58c9b80a4e1fac0b73bb48f86d497a774a6a2eb6d2f1e657bb73/sqlalchemy-2.0.52-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:410d52be41d17f1a236d19520fbe776257dc16516ed06bd16d433311842aefd9", size = 2169537, upload-time = "2026-08-11T20:58:13.855Z" }, + { url = "https://files.pythonhosted.org/packages/4c/93/d07ebd645d1b07b6b5ed63450a70f063a346a7e0f2c8810daf2e532400cb/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfe9ce533dbe4d0a2ae1486546619bd30b76bcd670539a44d910361376175f5e", size = 3319606, upload-time = "2026-08-11T21:02:45.829Z" }, + { url = "https://files.pythonhosted.org/packages/ae/5c/290c84c7c2566ecd3b65baaae0fddec9bc33b033b398a06123bb86fbfc6e/sqlalchemy-2.0.52-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:812bae5138bfc0aa46fb0686da0fc7f581f68e2bbb05bc24c3713bebaedd1437", size = 3323642, upload-time = "2026-08-11T21:17:05.675Z" }, + { url = "https://files.pythonhosted.org/packages/13/f5/2cc160590ca49173359557880b92a0572293ccb899e8f6cedf150c5a3ddf/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:50bff43b632a56fbf5ed9afdd76307e1512b62051bcd5afb341ae67205bbb6c8", size = 3268125, upload-time = "2026-08-11T21:02:47.649Z" }, + { url = "https://files.pythonhosted.org/packages/35/f3/ea8933fc9f7d1353e9c2ff9965eae687c4cef181120574591ed2fa0633e1/sqlalchemy-2.0.52-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:49565daf5af554f538e23aef1fc81a95a4e49658f152285e45c02f5fc44f04cd", size = 3289516, upload-time = "2026-08-11T21:17:07.267Z" }, + { url = "https://files.pythonhosted.org/packages/45/67/05cf86541c1e1716fca1e4a996954a439cd74501707cda607fb7cb02ef50/sqlalchemy-2.0.52-cp314-cp314-win32.whl", hash = "sha256:ab9da41e61b9979b910499d633b241df20c51ee5037e5405b11c2faac3cbe1a2", size = 2130249, upload-time = "2026-08-11T21:14:57.273Z" }, + { url = "https://files.pythonhosted.org/packages/96/d7/8ac6ffa1e36169e762ef65bd835046abb2251b1bc17f8f6708e14ed8d31f/sqlalchemy-2.0.52-cp314-cp314-win_amd64.whl", hash = "sha256:a593db51b3bae75db17a5738ad5f992244b3a03863f83c28117ee482c6a3f76d", size = 2156718, upload-time = "2026-08-11T21:14:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/dc/4b/e01a737eef378e734cc6394a82248a6ce13b167dfa36c731075ce9fc9c64/sqlalchemy-2.0.52-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1e61d08bdf4ee2f41024569e3400de7d6734ba498144766b11260936ccfa582", size = 2190344, upload-time = "2026-08-11T19:53:21.393Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/3582293d1e185e71d19d7c731c3e2ee20ba21981c4a1115c0806c1f62120/sqlalchemy-2.0.52-py3-none-any.whl", hash = "sha256:3b81b8363a919ce53453591cdb93702e6bd54ade6c4fa2f468fc053baee5ed89", size = 1950700, upload-time = "2026-08-11T20:47:21.603Z" }, +] + +[[package]] +name = "stevedore" +version = "5.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/a1/3b8ed9c1fc3aa6eebb57732d924ddaa0500ecc3b638d0454816320994383/stevedore-5.9.1.tar.gz", hash = "sha256:e97a2667923efda926e8713fde6a73616df68210a3cbc6f02b48967b676fd8bf", size = 518111, upload-time = "2026-08-20T15:25:14.754Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/97/bba6e7ec2f5498b9dcb7b1b6400086b80ae5a8ebaff4b25e8c8add75f439/stevedore-5.9.1-py3-none-any.whl", hash = "sha256:5c8ff3a9f336cc1a06ac0f597bc79d11a2f950bfd32e290ca56b5a301fafafbf", size = 54931, upload-time = "2026-08-20T15:25:13.602Z" }, ] [[package]] @@ -2205,145 +2751,195 @@ wheels = [ [[package]] name = "tiktoken" -version = "0.12.0" +version = "0.14.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "regex" }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/7d/ab/4d017d0f76ec3171d469d80fc03dfbb4e48a4bcaddaa831b31d526f05edc/tiktoken-0.12.0.tar.gz", hash = "sha256:b18ba7ee2b093863978fcb14f74b3707cdc8d4d4d3836853ce7ec60772139931", size = 37806, upload-time = "2025-10-06T20:22:45.419Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/46/21ea696b21f1d6d1efec8639c204bdf20fde8bafb351e1355c72c5d7de52/tiktoken-0.12.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6e227c7f96925003487c33b1b32265fad2fbcec2b7cf4817afb76d416f40f6bb", size = 1051565, upload-time = "2025-10-06T20:21:44.566Z" }, - { url = "https://files.pythonhosted.org/packages/c9/d9/35c5d2d9e22bb2a5f74ba48266fb56c63d76ae6f66e02feb628671c0283e/tiktoken-0.12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c06cf0fcc24c2cb2adb5e185c7082a82cba29c17575e828518c2f11a01f445aa", size = 995284, upload-time = "2025-10-06T20:21:45.622Z" }, - { url = "https://files.pythonhosted.org/packages/01/84/961106c37b8e49b9fdcf33fe007bb3a8fdcc380c528b20cc7fbba80578b8/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f18f249b041851954217e9fd8e5c00b024ab2315ffda5ed77665a05fa91f42dc", size = 1129201, upload-time = "2025-10-06T20:21:47.074Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d0/3d9275198e067f8b65076a68894bb52fd253875f3644f0a321a720277b8a/tiktoken-0.12.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:47a5bc270b8c3db00bb46ece01ef34ad050e364b51d406b6f9730b64ac28eded", size = 1152444, upload-time = "2025-10-06T20:21:48.139Z" }, - { url = "https://files.pythonhosted.org/packages/78/db/a58e09687c1698a7c592e1038e01c206569b86a0377828d51635561f8ebf/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:508fa71810c0efdcd1b898fda574889ee62852989f7c1667414736bcb2b9a4bd", size = 1195080, upload-time = "2025-10-06T20:21:49.246Z" }, - { url = "https://files.pythonhosted.org/packages/9e/1b/a9e4d2bf91d515c0f74afc526fd773a812232dd6cda33ebea7f531202325/tiktoken-0.12.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a1af81a6c44f008cba48494089dd98cccb8b313f55e961a52f5b222d1e507967", size = 1255240, upload-time = "2025-10-06T20:21:50.274Z" }, - { url = "https://files.pythonhosted.org/packages/9d/15/963819345f1b1fb0809070a79e9dd96938d4ca41297367d471733e79c76c/tiktoken-0.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:3e68e3e593637b53e56f7237be560f7a394451cb8c11079755e80ae64b9e6def", size = 879422, upload-time = "2025-10-06T20:21:51.734Z" }, - { url = "https://files.pythonhosted.org/packages/a4/85/be65d39d6b647c79800fd9d29241d081d4eeb06271f383bb87200d74cf76/tiktoken-0.12.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b97f74aca0d78a1ff21b8cd9e9925714c15a9236d6ceacf5c7327c117e6e21e8", size = 1050728, upload-time = "2025-10-06T20:21:52.756Z" }, - { url = "https://files.pythonhosted.org/packages/4a/42/6573e9129bc55c9bf7300b3a35bef2c6b9117018acca0dc760ac2d93dffe/tiktoken-0.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b90f5ad190a4bb7c3eb30c5fa32e1e182ca1ca79f05e49b448438c3e225a49b", size = 994049, upload-time = "2025-10-06T20:21:53.782Z" }, - { url = "https://files.pythonhosted.org/packages/66/c5/ed88504d2f4a5fd6856990b230b56d85a777feab84e6129af0822f5d0f70/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:65b26c7a780e2139e73acc193e5c63ac754021f160df919add909c1492c0fb37", size = 1129008, upload-time = "2025-10-06T20:21:54.832Z" }, - { url = "https://files.pythonhosted.org/packages/f4/90/3dae6cc5436137ebd38944d396b5849e167896fc2073da643a49f372dc4f/tiktoken-0.12.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:edde1ec917dfd21c1f2f8046b86348b0f54a2c0547f68149d8600859598769ad", size = 1152665, upload-time = "2025-10-06T20:21:56.129Z" }, - { url = "https://files.pythonhosted.org/packages/a3/fe/26df24ce53ffde419a42f5f53d755b995c9318908288c17ec3f3448313a3/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:35a2f8ddd3824608b3d650a000c1ef71f730d0c56486845705a8248da00f9fe5", size = 1194230, upload-time = "2025-10-06T20:21:57.546Z" }, - { url = "https://files.pythonhosted.org/packages/20/cc/b064cae1a0e9fac84b0d2c46b89f4e57051a5f41324e385d10225a984c24/tiktoken-0.12.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83d16643edb7fa2c99eff2ab7733508aae1eebb03d5dfc46f5565862810f24e3", size = 1254688, upload-time = "2025-10-06T20:21:58.619Z" }, - { url = "https://files.pythonhosted.org/packages/81/10/b8523105c590c5b8349f2587e2fdfe51a69544bd5a76295fc20f2374f470/tiktoken-0.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ffc5288f34a8bc02e1ea7047b8d041104791d2ddbf42d1e5fa07822cbffe16bd", size = 878694, upload-time = "2025-10-06T20:21:59.876Z" }, - { url = "https://files.pythonhosted.org/packages/00/61/441588ee21e6b5cdf59d6870f86beb9789e532ee9718c251b391b70c68d6/tiktoken-0.12.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:775c2c55de2310cc1bc9a3ad8826761cbdc87770e586fd7b6da7d4589e13dab3", size = 1050802, upload-time = "2025-10-06T20:22:00.96Z" }, - { url = "https://files.pythonhosted.org/packages/1f/05/dcf94486d5c5c8d34496abe271ac76c5b785507c8eae71b3708f1ad9b45a/tiktoken-0.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a01b12f69052fbe4b080a2cfb867c4de12c704b56178edf1d1d7b273561db160", size = 993995, upload-time = "2025-10-06T20:22:02.788Z" }, - { url = "https://files.pythonhosted.org/packages/a0/70/5163fe5359b943f8db9946b62f19be2305de8c3d78a16f629d4165e2f40e/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:01d99484dc93b129cd0964f9d34eee953f2737301f18b3c7257bf368d7615baa", size = 1128948, upload-time = "2025-10-06T20:22:03.814Z" }, - { url = "https://files.pythonhosted.org/packages/0c/da/c028aa0babf77315e1cef357d4d768800c5f8a6de04d0eac0f377cb619fa/tiktoken-0.12.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:4a1a4fcd021f022bfc81904a911d3df0f6543b9e7627b51411da75ff2fe7a1be", size = 1151986, upload-time = "2025-10-06T20:22:05.173Z" }, - { url = "https://files.pythonhosted.org/packages/a0/5a/886b108b766aa53e295f7216b509be95eb7d60b166049ce2c58416b25f2a/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:981a81e39812d57031efdc9ec59fa32b2a5a5524d20d4776574c4b4bd2e9014a", size = 1194222, upload-time = "2025-10-06T20:22:06.265Z" }, - { url = "https://files.pythonhosted.org/packages/f4/f8/4db272048397636ac7a078d22773dd2795b1becee7bc4922fe6207288d57/tiktoken-0.12.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9baf52f84a3f42eef3ff4e754a0db79a13a27921b457ca9832cf944c6be4f8f3", size = 1255097, upload-time = "2025-10-06T20:22:07.403Z" }, - { url = "https://files.pythonhosted.org/packages/8e/32/45d02e2e0ea2be3a9ed22afc47d93741247e75018aac967b713b2941f8ea/tiktoken-0.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:b8a0cd0c789a61f31bf44851defbd609e8dd1e2c8589c614cc1060940ef1f697", size = 879117, upload-time = "2025-10-06T20:22:08.418Z" }, - { url = "https://files.pythonhosted.org/packages/ce/76/994fc868f88e016e6d05b0da5ac24582a14c47893f4474c3e9744283f1d5/tiktoken-0.12.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d5f89ea5680066b68bcb797ae85219c72916c922ef0fcdd3480c7d2315ffff16", size = 1050309, upload-time = "2025-10-06T20:22:10.939Z" }, - { url = "https://files.pythonhosted.org/packages/f6/b8/57ef1456504c43a849821920d582a738a461b76a047f352f18c0b26c6516/tiktoken-0.12.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b4e7ed1c6a7a8a60a3230965bdedba8cc58f68926b835e519341413370e0399a", size = 993712, upload-time = "2025-10-06T20:22:12.115Z" }, - { url = "https://files.pythonhosted.org/packages/72/90/13da56f664286ffbae9dbcfadcc625439142675845baa62715e49b87b68b/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:fc530a28591a2d74bce821d10b418b26a094bf33839e69042a6e86ddb7a7fb27", size = 1128725, upload-time = "2025-10-06T20:22:13.541Z" }, - { url = "https://files.pythonhosted.org/packages/05/df/4f80030d44682235bdaecd7346c90f67ae87ec8f3df4a3442cb53834f7e4/tiktoken-0.12.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:06a9f4f49884139013b138920a4c393aa6556b2f8f536345f11819389c703ebb", size = 1151875, upload-time = "2025-10-06T20:22:14.559Z" }, - { url = "https://files.pythonhosted.org/packages/22/1f/ae535223a8c4ef4c0c1192e3f9b82da660be9eb66b9279e95c99288e9dab/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:04f0e6a985d95913cabc96a741c5ffec525a2c72e9df086ff17ebe35985c800e", size = 1194451, upload-time = "2025-10-06T20:22:15.545Z" }, - { url = "https://files.pythonhosted.org/packages/78/a7/f8ead382fce0243cb625c4f266e66c27f65ae65ee9e77f59ea1653b6d730/tiktoken-0.12.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:0ee8f9ae00c41770b5f9b0bb1235474768884ae157de3beb5439ca0fd70f3e25", size = 1253794, upload-time = "2025-10-06T20:22:16.624Z" }, - { url = "https://files.pythonhosted.org/packages/93/e0/6cc82a562bc6365785a3ff0af27a2a092d57c47d7a81d9e2295d8c36f011/tiktoken-0.12.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dc2dd125a62cb2b3d858484d6c614d136b5b848976794edfb63688d539b8b93f", size = 878777, upload-time = "2025-10-06T20:22:18.036Z" }, - { url = "https://files.pythonhosted.org/packages/72/05/3abc1db5d2c9aadc4d2c76fa5640134e475e58d9fbb82b5c535dc0de9b01/tiktoken-0.12.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a90388128df3b3abeb2bfd1895b0681412a8d7dc644142519e6f0a97c2111646", size = 1050188, upload-time = "2025-10-06T20:22:19.563Z" }, - { url = "https://files.pythonhosted.org/packages/e3/7b/50c2f060412202d6c95f32b20755c7a6273543b125c0985d6fa9465105af/tiktoken-0.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:da900aa0ad52247d8794e307d6446bd3cdea8e192769b56276695d34d2c9aa88", size = 993978, upload-time = "2025-10-06T20:22:20.702Z" }, - { url = "https://files.pythonhosted.org/packages/14/27/bf795595a2b897e271771cd31cb847d479073497344c637966bdf2853da1/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:285ba9d73ea0d6171e7f9407039a290ca77efcdb026be7769dccc01d2c8d7fff", size = 1129271, upload-time = "2025-10-06T20:22:22.06Z" }, - { url = "https://files.pythonhosted.org/packages/f5/de/9341a6d7a8f1b448573bbf3425fa57669ac58258a667eb48a25dfe916d70/tiktoken-0.12.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:d186a5c60c6a0213f04a7a802264083dea1bbde92a2d4c7069e1a56630aef830", size = 1151216, upload-time = "2025-10-06T20:22:23.085Z" }, - { url = "https://files.pythonhosted.org/packages/75/0d/881866647b8d1be4d67cb24e50d0c26f9f807f994aa1510cb9ba2fe5f612/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:604831189bd05480f2b885ecd2d1986dc7686f609de48208ebbbddeea071fc0b", size = 1194860, upload-time = "2025-10-06T20:22:24.602Z" }, - { url = "https://files.pythonhosted.org/packages/b3/1e/b651ec3059474dab649b8d5b69f5c65cd8fcd8918568c1935bd4136c9392/tiktoken-0.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:8f317e8530bb3a222547b85a58583238c8f74fd7a7408305f9f63246d1a0958b", size = 1254567, upload-time = "2025-10-06T20:22:25.671Z" }, - { url = "https://files.pythonhosted.org/packages/80/57/ce64fd16ac390fafde001268c364d559447ba09b509181b2808622420eec/tiktoken-0.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:399c3dd672a6406719d84442299a490420b458c44d3ae65516302a99675888f3", size = 921067, upload-time = "2025-10-06T20:22:26.753Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a4/72eed53e8976a099539cdd5eb36f241987212c29629d0a52c305173e0a68/tiktoken-0.12.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2c714c72bc00a38ca969dae79e8266ddec999c7ceccd603cc4f0d04ccd76365", size = 1050473, upload-time = "2025-10-06T20:22:27.775Z" }, - { url = "https://files.pythonhosted.org/packages/e6/d7/0110b8f54c008466b19672c615f2168896b83706a6611ba6e47313dbc6e9/tiktoken-0.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:cbb9a3ba275165a2cb0f9a83f5d7025afe6b9d0ab01a22b50f0e74fee2ad253e", size = 993855, upload-time = "2025-10-06T20:22:28.799Z" }, - { url = "https://files.pythonhosted.org/packages/5f/77/4f268c41a3957c418b084dd576ea2fad2e95da0d8e1ab705372892c2ca22/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:dfdfaa5ffff8993a3af94d1125870b1d27aed7cb97aa7eb8c1cefdbc87dbee63", size = 1129022, upload-time = "2025-10-06T20:22:29.981Z" }, - { url = "https://files.pythonhosted.org/packages/4e/2b/fc46c90fe5028bd094cd6ee25a7db321cb91d45dc87531e2bdbb26b4867a/tiktoken-0.12.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:584c3ad3d0c74f5269906eb8a659c8bfc6144a52895d9261cdaf90a0ae5f4de0", size = 1150736, upload-time = "2025-10-06T20:22:30.996Z" }, - { url = "https://files.pythonhosted.org/packages/28/c0/3c7a39ff68022ddfd7d93f3337ad90389a342f761c4d71de99a3ccc57857/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:54c891b416a0e36b8e2045b12b33dd66fb34a4fe7965565f1b482da50da3e86a", size = 1194908, upload-time = "2025-10-06T20:22:32.073Z" }, - { url = "https://files.pythonhosted.org/packages/ab/0d/c1ad6f4016a3968c048545f5d9b8ffebf577774b2ede3e2e352553b685fe/tiktoken-0.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5edb8743b88d5be814b1a8a8854494719080c28faaa1ccbef02e87354fe71ef0", size = 1253706, upload-time = "2025-10-06T20:22:33.385Z" }, - { url = "https://files.pythonhosted.org/packages/af/df/c7891ef9d2712ad774777271d39fdef63941ffba0a9d59b7ad1fd2765e57/tiktoken-0.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f61c0aea5565ac82e2ec50a05e02a6c44734e91b51c10510b084ea1b8e633a71", size = 920667, upload-time = "2025-10-06T20:22:34.444Z" }, -] - -[[package]] -name = "tqdm" -version = "4.67.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "colorama", marker = "sys_platform == 'win32'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/66/62/167a842aa0429d45f5e797354fd4343a96f6043d67d0513c675c7b8d36e6/tiktoken-0.14.0.tar.gz", hash = "sha256:231dec90efcdccf1b565a1416107736f1e09b1a08fe736ef9d6363e626d03874", size = 38898, upload-time = "2026-08-17T19:49:49.514Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/c5/9d848b7f408241171e1f843deb8bfa626086452bc9c78beee500829583e3/tiktoken-0.14.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:c2edf09b381fafbc014ae8e018ed25087abb9a3dafa8465a0ea63c6558c47a79", size = 1094971, upload-time = "2026-08-17T19:48:40.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/a9/d94302340304328961d6f0c35ca4e60617fbb57a5cf667e2ed1692cb9e57/tiktoken-0.14.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cd8ca1305c1c902fe42c486165f2e4808d9997625c98ffb05b9e0366d99d3948", size = 1042916, upload-time = "2026-08-17T19:48:41.541Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b6/31da98ee871383509cae2ba96a9ddef1965e3c4f8cb6dc7bcda3379398db/tiktoken-0.14.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:1f83081065ee5833d35b49e9180f3d8d15622a603dd1c435da0da6cc12b3662f", size = 1188650, upload-time = "2026-08-17T19:48:42.729Z" }, + { url = "https://files.pythonhosted.org/packages/24/65/8c5dddd7cb67f6571d154a58d7c6e2f07da54bf84c49b6a1839965b7c35e/tiktoken-0.14.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:f5e7665f6624e052e5e7f6a36919ab69279decdc976d7b16b4fa15e1897d0513", size = 1206378, upload-time = "2026-08-17T19:48:44.013Z" }, + { url = "https://files.pythonhosted.org/packages/d1/04/522ec59d30dd9a2f3ab837011cd4fc5d1178dc4a2fa07c9fa4b90af6ba9d/tiktoken-0.14.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:144a3fc369f92b7d548995217c5d6e84038d3572157a0f6f34080d65291d0f78", size = 1253694, upload-time = "2026-08-17T19:48:45.597Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/9019e272bad188a1c61ecf44f25a9ba2368744644e3ac1f3d6516f3c9e80/tiktoken-0.14.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:151d37a150c8f3dfc5f4345597b10e101876bd1bd13494e0185af6b508758d2e", size = 1317873, upload-time = "2026-08-17T19:48:46.792Z" }, + { url = "https://files.pythonhosted.org/packages/24/7f/fff1217240343c0c11b5938b98aeae0e3a266cacfac25f86f91cdcd748f0/tiktoken-0.14.0-cp311-cp311-win_amd64.whl", hash = "sha256:c77d4a3e1deb2707819df92046b89aad1ac81d27e07616b797cbff3f62c037da", size = 944395, upload-time = "2026-08-17T19:48:48.028Z" }, + { url = "https://files.pythonhosted.org/packages/8c/da/e273746b9d24a63c776bc60fba914351573ad9c575b52601eb5e60632564/tiktoken-0.14.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8e947aefe98ef74cce94923f90e48c98fe34eb1ec0a6bfdfadfc5a96359bfc36", size = 1094408, upload-time = "2026-08-17T19:48:49.269Z" }, + { url = "https://files.pythonhosted.org/packages/69/9f/fe6b1aca23331aa5271df5a4bd07bf68a7059254d47faee1b8272592a777/tiktoken-0.14.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cebe67765569df3dafac8474e4eccf5c19d24140492567a5e58a11445732a4", size = 1038499, upload-time = "2026-08-17T19:48:50.666Z" }, + { url = "https://files.pythonhosted.org/packages/0b/35/e9f47647c9e163bd1de30fe1a491669b7248cfc67b7404c35c009a701e1a/tiktoken-0.14.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:7db45b98e94adf4173a5cd7422b150999a7ee11ff847783a14f6e1b80cc38cb6", size = 1186355, upload-time = "2026-08-17T19:48:51.93Z" }, + { url = "https://files.pythonhosted.org/packages/51/11/9976ad86980a00cdef05e730a0127a2578a1bc6d11644d8d47246de2eb26/tiktoken-0.14.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:7896eea257fe497a2b7134474d909156c6744ce8da35bce88011a960e008aa0d", size = 1204197, upload-time = "2026-08-17T19:48:53.18Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9c/7035b0bcfaa68d1ee4803fc5be5214ad865669b05bd20e7105ae8a18afc6/tiktoken-0.14.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b950248272f1b303dc32986396e2dccfa10cf6d1e83ec8f0bba1776660305482", size = 1250635, upload-time = "2026-08-17T19:48:54.392Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1d/69cabf18bed7f4366da076735816abce0d4db3fae491ae338a6612128777/tiktoken-0.14.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3de75343041a1c57333b1e707ac8a9769738241d7d6a55d39e12cf84548337c6", size = 1316085, upload-time = "2026-08-17T19:48:55.525Z" }, + { url = "https://files.pythonhosted.org/packages/bd/bd/a2e884fb1402cba5be08836590320012b2d8ada0e2eef9911a64df4bcd2d/tiktoken-0.14.0-cp312-cp312-win_amd64.whl", hash = "sha256:087538c080e5ff421abd3a0785ed63c5111d06af98e6cd0d374dbe5969147ca3", size = 941208, upload-time = "2026-08-17T19:48:56.938Z" }, + { url = "https://files.pythonhosted.org/packages/50/53/ee1453623bf65f019328721ccb6587846d2c5b7b82f34e73ca09101f072e/tiktoken-0.14.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9c5fe393aab56469f04e432ff851216d3def3436cf5f07e442a240164bf500f", size = 1094198, upload-time = "2026-08-17T19:48:57.955Z" }, + { url = "https://files.pythonhosted.org/packages/ad/5f/6448cfe278c3664ba9ec5b5ac08344341f7dc3d42888476e215a14eda2be/tiktoken-0.14.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbe2cc3bba939bcdaf103e03df9d5039d33887080b315624be28ec69059e5f94", size = 1038820, upload-time = "2026-08-17T19:48:59.015Z" }, + { url = "https://files.pythonhosted.org/packages/69/3b/d67eac1bcce9dee3abe23aff5e3ded3116bbebaf67b80a0811c06d3806fc/tiktoken-0.14.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2157f52e4b4d7ac5ecc7457b3716834706e7ef9a46f5144029bfeb7cf71f4e06", size = 1186175, upload-time = "2026-08-17T19:49:00.068Z" }, + { url = "https://files.pythonhosted.org/packages/37/62/cae690d9783146b0f81f564ada0f8f611de68178c0c9c7e1e969f0516b48/tiktoken-0.14.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:26e60f6a956ee171ab728b37b8439905d7ea1db435c30f9822f291e9861c861d", size = 1203884, upload-time = "2026-08-17T19:49:01.163Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1e/633e30237b94e383cf814145499079f3bb9cdd4aeafc1bc42e01b0f810a6/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:380873f330b741c4435574f37edb20813d04603ace2d53e0a63560e1fec83010", size = 1250980, upload-time = "2026-08-17T19:49:02.274Z" }, + { url = "https://files.pythonhosted.org/packages/cb/56/4c12f07b812f84206f38d723eb1ebfdd34bad9309b5dbc0bee6bbcff4cbf/tiktoken-0.14.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3fd7c14b1cb45b486c39fc9b3443bb341f3e2fc7e6f31247f3435a5836651632", size = 1315434, upload-time = "2026-08-17T19:49:03.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e0/c65603f0c44811def666d3fbf611bf2af3b5e1ef613e06c19411419830b3/tiktoken-0.14.0-cp313-cp313-win_amd64.whl", hash = "sha256:90a762670c7f968184723769a06ed51f5cf5ce5dcd1e30164f25c72d85c2d1f1", size = 940883, upload-time = "2026-08-17T19:49:04.583Z" }, + { url = "https://files.pythonhosted.org/packages/59/b0/1cf129f4af8fc513931f931023def596b7c4bfc77026513cd9d851da9e88/tiktoken-0.14.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e067f4cbcc5d036e8aff7fe7a6b530a8f4de2e4616ad9005a24a1879e24e6450", size = 1096273, upload-time = "2026-08-17T19:49:05.807Z" }, + { url = "https://files.pythonhosted.org/packages/62/85/2ae74575e321148484147e10b53c3b1717c59ebaa9edb4fe18b1f5c055f8/tiktoken-0.14.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2af4a336ea56d6c14f27741a0e1d8294a35dd0b038bcf990d232ebb54eb994b", size = 1040269, upload-time = "2026-08-17T19:49:06.943Z" }, + { url = "https://files.pythonhosted.org/packages/89/29/92a1120a12e4bcf2d5464350d1a91b68a433d63ce656bb7f806c27aec09c/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f702e0aeeb6506e57687e881c59e844ebe8f0a6a097ddafe20e3ab25f387be4e", size = 1186101, upload-time = "2026-08-17T19:49:08.102Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7d/144af98dc5ad68108451a82e2f5a17f80e2663f5115058b8dfd215c1ad02/tiktoken-0.14.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:e3442bbb2f0c588cec876061e37ae67b455b9df9978b003c8fe30e45f2ef5b42", size = 1204457, upload-time = "2026-08-17T19:49:09.28Z" }, + { url = "https://files.pythonhosted.org/packages/e6/1f/be7cb06ab2108f612f3e92e7b76cf391e192db0db37a984616f0cc32aafc/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:979c1524f753b662b0f3cd261b135afe6659cce33caaa7a5ea00dd1756b3055c", size = 1251716, upload-time = "2026-08-17T19:49:10.509Z" }, + { url = "https://files.pythonhosted.org/packages/ab/6b/81f158d0f90adb826cd704069c2129a046cb784a2a09861009519fc41cf4/tiktoken-0.14.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2cc19ac87b41c9493c9778ff5847f0c8bbcf5bd0ec6b87ce06c1c802adc8a771", size = 1315432, upload-time = "2026-08-17T19:49:11.844Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ec/f5fa35ec13f07279fdcaf3cc9c04bbb154ea591d23978651f2b672593e8a/tiktoken-0.14.0-cp314-cp314-win_amd64.whl", hash = "sha256:eceeff0c62419bc78d4b6e70a4762a4d25df3ae8f2d5946e3853ce93e7a57098", size = 988046, upload-time = "2026-08-17T19:49:13.282Z" }, + { url = "https://files.pythonhosted.org/packages/68/c9/7756717408d3d0dfea3f046c9466144b28afde39ff69d5808f2475dcd7f5/tiktoken-0.14.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:6eb94895c45f26bb8f5546e5fd8a069efcf6e3f108ea9d5cbe3bf6f7f3983438", size = 1096261, upload-time = "2026-08-17T19:49:14.351Z" }, + { url = "https://files.pythonhosted.org/packages/79/29/46ad8061f57bd9f8b2ea0aa82bf574e0f2aa040b0857a1582adba9957899/tiktoken-0.14.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:86951a971c53979ec857bd8c4a32dc227ab0fd33f6c12a3bd62d3fbf5f0bfcaa", size = 1040183, upload-time = "2026-08-17T19:49:15.707Z" }, + { url = "https://files.pythonhosted.org/packages/5a/7c/3184d17b868456f17b60b1a75f5ec0405618a43aa753336df341d8f11781/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:e2eca764c53490f8930dbce329e0769f11108d87d908282a80c5c130e26e7037", size = 1186719, upload-time = "2026-08-17T19:49:16.84Z" }, + { url = "https://files.pythonhosted.org/packages/0b/e8/46de4400d5bf859f640feee85bd7e32235f68ddf25db53c63be78e581e3a/tiktoken-0.14.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:26cc4b4840fa0e9f4b72ed489883e12f57e00d1021ca794720e3c29a12f0edef", size = 1204660, upload-time = "2026-08-17T19:49:17.987Z" }, + { url = "https://files.pythonhosted.org/packages/29/ce/af8964c38bc8226dd8950305b7a255fa33345d5572f78af7275a313d28e0/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2fc834fbe3f6a0736905c36ab709537e6840dbd63b982dc9e0216ae7d305ba1a", size = 1250932, upload-time = "2026-08-17T19:49:19.28Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4b/323631116fc986d9cc5bbeb2b8223c7c85e61a8bb94ea5ab4951023b149b/tiktoken-0.14.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ca4db6ff5c5bf600f9b7761a0070ed44dfe5797a76bd432fb978bc480ef40c58", size = 1315190, upload-time = "2026-08-17T19:49:20.467Z" }, + { url = "https://files.pythonhosted.org/packages/18/8b/ba48a73729c9270989b36f37ab2ed5525e52690d715097c9fa791aaa5d05/tiktoken-0.14.0-cp314-cp314t-win_amd64.whl", hash = "sha256:7aab286a020660a039097912a088236b985d18a3090d73f136c4413d29d37ca0", size = 987717, upload-time = "2026-08-17T19:49:21.704Z" }, + { url = "https://files.pythonhosted.org/packages/1d/10/b73b7e319179e0f60b32475f783b044f9cece872c53b6662664e9084b0d0/tiktoken-0.14.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:14b47e3674f2624803a8acc8fb367b7e24fc53055f9df3296482fe9a3a34a232", size = 1096280, upload-time = "2026-08-17T19:49:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/c2/6b/09999a9bf1d559670d1680e8f8e419ac0e2c5f6aac82e9bfdf70f260b30a/tiktoken-0.14.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:19d643d701fdaa70e5b9c7f8f96abcaffe77ca5e482a3a1a7dde46feb4284695", size = 1040433, upload-time = "2026-08-17T19:49:23.998Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7b/8537be0836f3df99b2a636b44399bfa43cd757f2b8b4097dacb794cf24a7/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_aarch64.whl", hash = "sha256:e4ddf863b59347deaa92302dcd90e5eb003cdc9be06ec2b692c38d1bdd9efd49", size = 1186989, upload-time = "2026-08-17T19:49:25.021Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9d/f9c56d7a943a4468abf9ef37661bb9b8e0cd3aa8aa87368c7146cc3f3222/tiktoken-0.14.0-cp315-cp315-manylinux_2_28_x86_64.whl", hash = "sha256:60c47ca69ddda0dea8256fffd12e1b86f4b59734a20e4a70c61f63cc5f021df4", size = 1204615, upload-time = "2026-08-17T19:49:26.37Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d2/98a38579db25c4a8a84e31dd95d9072ec5f21f7e70de591da0412e29b25b/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:728303a072163130c5b477b1f20d6211895569c1d5302c24ffc93a3009160871", size = 1251828, upload-time = "2026-08-17T19:49:27.423Z" }, + { url = "https://files.pythonhosted.org/packages/0c/83/467be424746c039c5493c0f4102feab16b9b48eb6f5c089b2a2438e3cde2/tiktoken-0.14.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:3c5349c9f916283bba32bec8af69b763e4faa304dc004d0eaaea66a3cf004c1f", size = 1316260, upload-time = "2026-08-17T19:49:29.101Z" }, + { url = "https://files.pythonhosted.org/packages/02/ee/ddf46ca78e371f5890e96b6e7d089a85b3536432be219851eb0481786ca8/tiktoken-0.14.0-cp315-cp315-win_amd64.whl", hash = "sha256:1b6e4adcfd285c44502aed51df98aaaca4f0fea028165dbf8a9e857b9f98d8ea", size = 988230, upload-time = "2026-08-17T19:49:30.246Z" }, + { url = "https://files.pythonhosted.org/packages/2a/00/5162e90c851a28da18ed382d34898b79a8022548e5619a64e14c03ce7c3d/tiktoken-0.14.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:11d8211b290855d2721334ff17dd9b3a17bfb26872be01f25d73612ef7ece890", size = 1096186, upload-time = "2026-08-17T19:49:31.656Z" }, + { url = "https://files.pythonhosted.org/packages/65/97/a5a7bfccf25b1bb65e82bae8edff11ac3c9c041c374b7b4a823d60c38133/tiktoken-0.14.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d0781223705199b289faa59601bb9c2441712d4c600dd13c43d8fd6a33d22cd5", size = 1039947, upload-time = "2026-08-17T19:49:32.848Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ba/ef427fc638f1439181c5e12dd26b70e881861f89c007aa7e5b36300f8342/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_aarch64.whl", hash = "sha256:2ea70afba6b9eddbf22c165142e5f0a2ad7aa36a452873c48b57bb2aeb8492ae", size = 1186997, upload-time = "2026-08-17T19:49:34.121Z" }, + { url = "https://files.pythonhosted.org/packages/3e/88/2f3f85a968cdc514152129af0a060ebcccb067005a2f29b0d5ef3c838514/tiktoken-0.14.0-cp315-cp315t-manylinux_2_28_x86_64.whl", hash = "sha256:78571efc311c30b73f31eb949a921d6dac39a5d9dc42d1cfa8f8db157b3447b1", size = 1205211, upload-time = "2026-08-17T19:49:35.284Z" }, + { url = "https://files.pythonhosted.org/packages/4e/f6/80760e98a08e6649d2d68afb6035af713121dfb615acce8c4f73810ec438/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86f66c85e796f5d05d5c4a60ec1d40cbfebc47a32464053528c797163fa9ab89", size = 1251479, upload-time = "2026-08-17T19:49:36.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/84/50966fb6918a0fb9b32721277e5342bf729a2d74350074d662fbedf9772e/tiktoken-0.14.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:149d97453c4c98c04b081d64a85e635921269b532710d6faf81e9e82b790e7d3", size = 1316673, upload-time = "2026-08-17T19:49:37.756Z" }, + { url = "https://files.pythonhosted.org/packages/35/5e/9b01afd037bfa22a0033963fa091e0f75b6fb15cd85bffb42ff86e697323/tiktoken-0.14.0-cp315-cp315t-win_amd64.whl", hash = "sha256:561e7580f84a79859af1ef6f676968e9030fcc3fe195700b15235bca64f009c9", size = 987929, upload-time = "2026-08-17T19:49:38.947Z" }, +] + +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, ] [[package]] name = "typing-extensions" -version = "4.15.0" +version = "4.16.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, upload-time = "2025-08-25T13:49:26.313Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, -] - -[[package]] -name = "typing-inspect" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mypy-extensions" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/dc/74/1789779d91f1961fa9438e9a8710cdae6bd138c80d7303996933d117264a/typing_inspect-0.9.0.tar.gz", hash = "sha256:b23fc42ff6f6ef6954e4852c1fb512cdd18dbea03134f91f856a95ccc9461f78", size = 13825, upload-time = "2023-05-24T20:25:47.612Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/65/f3/107a22063bf27bdccf2024833d3445f4eea42b2e598abfbd46f6a63b6cb0/typing_inspect-0.9.0-py3-none-any.whl", hash = "sha256:9ee6fc59062311ef8547596ab6b955e1b8aa46242d854bfc78f4f6b0eff35f9f", size = 8827, upload-time = "2023-05-24T20:25:45.287Z" }, + { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, ] [[package]] name = "typing-inspection" -version = "0.4.2" +version = "0.4.4" 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, upload-time = "2025-10-01T02:14:41.687Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, + { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" }, ] [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" 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, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] name = "uuid-utils" -version = "0.14.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/7b/d1/38a573f0c631c062cf42fa1f5d021d4dd3c31fb23e4376e4b56b0c9fbbed/uuid_utils-0.14.1.tar.gz", hash = "sha256:9bfc95f64af80ccf129c604fb6b8ca66c6f256451e32bc4570f760e4309c9b69", size = 22195, upload-time = "2026-02-20T22:50:38.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/b7/add4363039a34506a58457d96d4aa2126061df3a143eb4d042aedd6a2e76/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:93a3b5dc798a54a1feb693f2d1cb4cf08258c32ff05ae4929b5f0a2ca624a4f0", size = 604679, upload-time = "2026-02-20T22:50:27.469Z" }, - { url = "https://files.pythonhosted.org/packages/dd/84/d1d0bef50d9e66d31b2019997c741b42274d53dde2e001b7a83e9511c339/uuid_utils-0.14.1-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:ccd65a4b8e83af23eae5e56d88034b2fe7264f465d3e830845f10d1591b81741", size = 309346, upload-time = "2026-02-20T22:50:31.857Z" }, - { url = "https://files.pythonhosted.org/packages/ef/ed/b6d6fd52a6636d7c3eddf97d68da50910bf17cd5ac221992506fb56cf12e/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b56b0cacd81583834820588378e432b0696186683b813058b707aedc1e16c4b1", size = 344714, upload-time = "2026-02-20T22:50:42.642Z" }, - { url = "https://files.pythonhosted.org/packages/a8/a7/a19a1719fb626fe0b31882db36056d44fe904dc0cf15b06fdf56b2679cf7/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb3cf14de789097320a3c56bfdfdd51b1225d11d67298afbedee7e84e3837c96", size = 350914, upload-time = "2026-02-20T22:50:36.487Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fc/f6690e667fdc3bb1a73f57951f97497771c56fe23e3d302d7404be394d4f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:60e0854a90d67f4b0cc6e54773deb8be618f4c9bad98d3326f081423b5d14fae", size = 482609, upload-time = "2026-02-20T22:50:37.511Z" }, - { url = "https://files.pythonhosted.org/packages/54/6e/dcd3fa031320921a12ec7b4672dea3bd1dd90ddffa363a91831ba834d559/uuid_utils-0.14.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce6743ba194de3910b5feb1a62590cd2587e33a73ab6af8a01b642ceb5055862", size = 345699, upload-time = "2026-02-20T22:50:46.87Z" }, - { url = "https://files.pythonhosted.org/packages/04/28/e5220204b58b44ac0047226a9d016a113fde039280cc8732d9e6da43b39f/uuid_utils-0.14.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:043fb58fde6cf1620a6c066382f04f87a8e74feb0f95a585e4ed46f5d44af57b", size = 372205, upload-time = "2026-02-20T22:50:28.438Z" }, - { url = "https://files.pythonhosted.org/packages/c7/d9/3d2eb98af94b8dfffc82b6a33b4dfc87b0a5de2c68a28f6dde0db1f8681b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c915d53f22945e55fe0d3d3b0b87fd965a57f5fd15666fd92d6593a73b1dd297", size = 521836, upload-time = "2026-02-20T22:50:23.057Z" }, - { url = "https://files.pythonhosted.org/packages/a8/15/0eb106cc6fe182f7577bc0ab6e2f0a40be247f35c5e297dbf7bbc460bd02/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:0972488e3f9b449e83f006ead5a0e0a33ad4a13e4462e865b7c286ab7d7566a3", size = 625260, upload-time = "2026-02-20T22:50:25.949Z" }, - { url = "https://files.pythonhosted.org/packages/3c/17/f539507091334b109e7496830af2f093d9fc8082411eafd3ece58af1f8ba/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:1c238812ae0c8ffe77d8d447a32c6dfd058ea4631246b08b5a71df586ff08531", size = 587824, upload-time = "2026-02-20T22:50:35.225Z" }, - { url = "https://files.pythonhosted.org/packages/2e/c2/d37a7b2e41f153519367d4db01f0526e0d4b06f1a4a87f1c5dfca5d70a8b/uuid_utils-0.14.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:bec8f8ef627af86abf8298e7ec50926627e29b34fa907fcfbedb45aaa72bca43", size = 551407, upload-time = "2026-02-20T22:50:44.915Z" }, - { url = "https://files.pythonhosted.org/packages/65/36/2d24b2cbe78547c6532da33fb8613debd3126eccc33a6374ab788f5e46e9/uuid_utils-0.14.1-cp39-abi3-win32.whl", hash = "sha256:b54d6aa6252d96bac1fdbc80d26ba71bad9f220b2724d692ad2f2310c22ef523", size = 183476, upload-time = "2026-02-20T22:50:32.745Z" }, - { url = "https://files.pythonhosted.org/packages/83/92/2d7e90df8b1a69ec4cff33243ce02b7a62f926ef9e2f0eca5a026889cd73/uuid_utils-0.14.1-cp39-abi3-win_amd64.whl", hash = "sha256:fc27638c2ce267a0ce3e06828aff786f91367f093c80625ee21dad0208e0f5ba", size = 187147, upload-time = "2026-02-20T22:50:45.807Z" }, - { url = "https://files.pythonhosted.org/packages/d9/26/529f4beee17e5248e37e0bc17a2761d34c0fa3b1e5729c88adb2065bae6e/uuid_utils-0.14.1-cp39-abi3-win_arm64.whl", hash = "sha256:b04cb49b42afbc4ff8dbc60cf054930afc479d6f4dd7f1ec3bbe5dbfdde06b7a", size = 188132, upload-time = "2026-02-20T22:50:41.718Z" }, - { url = "https://files.pythonhosted.org/packages/91/f9/6c64bdbf71f58ccde7919e00491812556f446a5291573af92c49a5e9aaef/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:b197cd5424cf89fb019ca7f53641d05bfe34b1879614bed111c9c313b5574cd8", size = 591617, upload-time = "2026-02-20T22:50:24.532Z" }, - { url = "https://files.pythonhosted.org/packages/d0/f0/758c3b0fb0c4871c7704fef26a5bc861de4f8a68e4831669883bebe07b0f/uuid_utils-0.14.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:12c65020ba6cb6abe1d57fcbfc2d0ea0506c67049ee031714057f5caf0f9bc9c", size = 303702, upload-time = "2026-02-20T22:50:40.687Z" }, - { url = "https://files.pythonhosted.org/packages/85/89/d91862b544c695cd58855efe3201f83894ed82fffe34500774238ab8eba7/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b5d2ad28063d422ccc2c28d46471d47b61a58de885d35113a8f18cb547e25bf", size = 337678, upload-time = "2026-02-20T22:50:39.768Z" }, - { url = "https://files.pythonhosted.org/packages/ee/6b/cf342ba8a898f1de024be0243fac67c025cad530c79ea7f89c4ce718891a/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:da2234387b45fde40b0fedfee64a0ba591caeea9c48c7698ab6e2d85c7991533", size = 343711, upload-time = "2026-02-20T22:50:43.965Z" }, - { url = "https://files.pythonhosted.org/packages/b3/20/049418d094d396dfa6606b30af925cc68a6670c3b9103b23e6990f84b589/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50fffc2827348c1e48972eed3d1c698959e63f9d030aa5dd82ba451113158a62", size = 476731, upload-time = "2026-02-20T22:50:30.589Z" }, - { url = "https://files.pythonhosted.org/packages/77/a1/0857f64d53a90321e6a46a3d4cc394f50e1366132dcd2ae147f9326ca98b/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1dbe718765f70f5b7f9b7f66b6a937802941b1cc56bcf642ce0274169741e01", size = 338902, upload-time = "2026-02-20T22:50:33.927Z" }, - { url = "https://files.pythonhosted.org/packages/ed/d0/5bf7cbf1ac138c92b9ac21066d18faf4d7e7f651047b700eb192ca4b9fdb/uuid_utils-0.14.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:258186964039a8e36db10810c1ece879d229b01331e09e9030bc5dcabe231bd2", size = 364700, upload-time = "2026-02-20T22:50:21.732Z" }, +version = "0.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/91/63938e0e7e7876658e5e40178e7c0735b53527886fe11797a11699c55edd/uuid_utils-0.17.0.tar.gz", hash = "sha256:abb5667a36119019b3fa320c4d10c21ebccfcc87c8a739e6a0056cee7f48dde2", size = 43220, upload-time = "2026-07-09T13:49:58.433Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d7/b2/8f03b61f0aa4afc687855c4f00db35f4d3e58c480cd885abc46f6e41308f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:f9b093cb3b6c9d6233ef45a05cab064d2aa0a8cb3c5777084c9e20fcb77c2371", size = 563901, upload-time = "2026-07-09T13:48:08.961Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cb/88b909ffb9ac11f88d2e6ceabc592ccc660b5830b06dbcbd290ab8981f1f/uuid_utils-0.17.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0bc4c431ccd59c764080ceb43b126043325fe17861b87759d026a0cdd8423bb2", size = 286383, upload-time = "2026-07-09T13:48:10.2Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b8/bc5b64e9898867227c535cd0366c571c580a736748e81329437c1773e442/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c00d182e31034250690f417b9068b78eab423c10d76766664e82d9860c340479", size = 323244, upload-time = "2026-07-09T13:48:11.477Z" }, + { url = "https://files.pythonhosted.org/packages/13/d9/8a17462ce066fbf89670fb737a3f0c93a77816736d2a4d134787e759d8ea/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:570db214f6d8507587a8faa968a3fe65e957daeb7bc48b27dc7f69bc3ecdd6f1", size = 330466, upload-time = "2026-07-09T13:48:13.092Z" }, + { url = "https://files.pythonhosted.org/packages/43/37/0c65d0db3bae45183419756d938f1791a82c835fd92bf234eb4f008d2e02/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:351462debd866f1f25e4d4f5c7fac89525b52151f0102a1bdfe94a999b046f5f", size = 443806, upload-time = "2026-07-09T13:48:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/32/d5/7e698466d1f5254620b5ee0d711fdd20a0e9c2acd7040740c37193a8f673/uuid_utils-0.17.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:622cdde768300591ac79bfcd7bb3468e4b191b1105d5dbfe8d87c39d8f63dd46", size = 324261, upload-time = "2026-07-09T13:48:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/5d/48/3a5b242d7f0b8e3ca77dcd7177f3cf73e0280cee32e2349d9796ca27f183/uuid_utils-0.17.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:75d7411e8eb9259764dd60310738540649057cda4509b4af14b36b7f663bfeb0", size = 350657, upload-time = "2026-07-09T13:48:17.273Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/f32ea82a89efed2eafee2f1d925d64687a81e550a9951933fb1b75c95ca6/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1019476b6bdc047216ef7414be5babe0fa5ccfde977c0cac4fd6c75ddec66ff7", size = 500613, upload-time = "2026-07-09T13:48:18.459Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5c/c7b73ec4bbe28db162a4841d352c6eda582801e0dd9fe72f6ad5cc584ee4/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:04452640d8b6920c480c16e5afe91ff896d236e0c972830f9247e0898d38c803", size = 606306, upload-time = "2026-07-09T13:48:19.726Z" }, + { url = "https://files.pythonhosted.org/packages/63/95/8a2777204e8691b4961e6aa619001c3e5175aa430ab43da3079142e8d310/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:793229621e1ad6cac55f015cfa9f4eff102accbc3da25d607b91c6b0bec167fb", size = 567231, upload-time = "2026-07-09T13:48:21.024Z" }, + { url = "https://files.pythonhosted.org/packages/1a/6f/1d778ca3ed6d2cf35f22088e2de714675416747ab41be510f22c141043a7/uuid_utils-0.17.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03815cea572c8a693cab5475b9d750cc161470961c7defa27e9286cad62f38f5", size = 529373, upload-time = "2026-07-09T13:48:22.312Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d3/9ad1ab64b3bed0a0237d1db89dc6f5001d6116a82766753da4ac4496f979/uuid_utils-0.17.0-cp311-cp311-win32.whl", hash = "sha256:c4f845166b09acc65c5213a35551a7f81c17fa010ab467229b5813f79d17fe13", size = 169930, upload-time = "2026-07-09T13:48:23.504Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1a/e01417f52eae6e2cb412260bb332b4ee4b37af2982d9c38cff4b68b2e899/uuid_utils-0.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:14dc2f46abb1091260c0d203fcbdf4e045042cc07e49183fd3b255904b95eb70", size = 177242, upload-time = "2026-07-09T13:48:24.723Z" }, + { url = "https://files.pythonhosted.org/packages/35/20/396c27f996add19f8ac31e49cc4570824e51a97719087dabf94694d25bc4/uuid_utils-0.17.0-cp311-cp311-win_arm64.whl", hash = "sha256:29179ffb7b317239b6d6afb100d14c439c728770460718280b9c0a42d2561ec2", size = 177023, upload-time = "2026-07-09T13:48:25.834Z" }, + { url = "https://files.pythonhosted.org/packages/20/80/a7e685968e3cec99d6fe2fb25d0f5726310e1bba356da68c13dfd8b7d140/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:9205068badf453d2f0821fd5d340389b4679992d7ff79d4f3e5608996dd1b287", size = 556403, upload-time = "2026-07-09T13:48:27.022Z" }, + { url = "https://files.pythonhosted.org/packages/56/47/3102d93bcb7b0bfe6bede63ff8f221a7f91348e10a37f682773be27c56d9/uuid_utils-0.17.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0fcca4e838af9ac9243b3358d7c14afa4dca286a87781124c272d6c4cad9c968", size = 285608, upload-time = "2026-07-09T13:48:28.769Z" }, + { url = "https://files.pythonhosted.org/packages/55/fb/d59695f0f8db065b93c63316eaafa05a22d75a0486978a33736c52c646d5/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f3729e839209f3457d0d8b6a35a376fdf65577a5aecaf4cc3587d3305759ba6", size = 319926, upload-time = "2026-07-09T13:48:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/5a/03/62fabcd1e990e07a0e220e8d552af45bc16f107fa8e55c2014a706bb1a1e/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3dac0ad0cd9a2818d1775215365a4e8c2f8ada215529dd26f3f8cceeb67a6988", size = 327172, upload-time = "2026-07-09T13:48:31.187Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/a5081391338b459e2f8d8b12581f00f8caa6317fab510e0e85c18c59e938/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e671b2322ef09106ecb1ca0f4c398b134d5e2c1f80d7a4f3336847a3072c0e94", size = 439075, upload-time = "2026-07-09T13:48:32.295Z" }, + { url = "https://files.pythonhosted.org/packages/59/30/91795bd01e17a13661280d4899fbf38fb05e3f38e873f9aaec106ec30aa0/uuid_utils-0.17.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8eb3e5caca8d3a6f72ea4cce024583f989f6f2e9186f98800213fff0176e8bcc", size = 320247, upload-time = "2026-07-09T13:48:33.64Z" }, + { url = "https://files.pythonhosted.org/packages/e5/11/09102b78303e4eb62069d6d88ef9fd661dc523e8f429e1fd67eaa78a6f44/uuid_utils-0.17.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8b72c2002202038666bf647f9a790906214c7c11cd0d6efef77b7d07bef3034a", size = 344738, upload-time = "2026-07-09T13:48:34.786Z" }, + { url = "https://files.pythonhosted.org/packages/74/f9/be95bad6954b60328878c3800258f01a6accd24fd75112d13f023462d53f/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e2ac1c0b56f2c91b6f158e29ed96b1503223fe8aa6e79b1be1dc55bd8a5131c", size = 496845, upload-time = "2026-07-09T13:48:36.057Z" }, + { url = "https://files.pythonhosted.org/packages/2d/02/8a19a34e0530d987488a068a71576a236f5c8c746630b870b57f71eb24ef/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6c142bd0cb4dba31c10babe00d59f7ef6460f0ef55eaa9c1a9da270684af996a", size = 603233, upload-time = "2026-07-09T13:48:37.512Z" }, + { url = "https://files.pythonhosted.org/packages/f4/a8/b1abab36ff73b0248d82179816467f6d39a2e80fd64329a895ca94f3508e/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e252db239eb41c32248e096e0d170bce5896a4fd3405556362bc3dd83d912206", size = 561401, upload-time = "2026-07-09T13:48:38.977Z" }, + { url = "https://files.pythonhosted.org/packages/61/91/70e7b528b351cc03a9ca43e6116371cdde31bb12bcead7ca2ca1367366cc/uuid_utils-0.17.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:237722b6581bb5b4eb4cefbcbe5c6e2980a440aabe781fbe50ebf1cb71eee4cc", size = 525314, upload-time = "2026-07-09T13:48:40.599Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f6/9167e90cf9937d6558f92d022ff3024a69d938a514d9c8faa4080f73b001/uuid_utils-0.17.0-cp312-cp312-win32.whl", hash = "sha256:46a73cacdf512f473a81f65dbf84186e08cfe6e9118fa582b6c6b33a8288a30d", size = 166831, upload-time = "2026-07-09T13:48:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/5c/7d/0b889654d9ee3413f810cf4685e241285f650d98a4103ac9f3c6bcc95f29/uuid_utils-0.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:e59b60a0a4cb7541480e02090d37dc2df3b72df4c2e776fff64ce3a4e3dd4637", size = 172944, upload-time = "2026-07-09T13:48:42.992Z" }, + { url = "https://files.pythonhosted.org/packages/be/35/8c6e1bf65e4d400352885dadc656ad6d0af96e89231e3f04686bc2197128/uuid_utils-0.17.0-cp312-cp312-win_arm64.whl", hash = "sha256:d561a4c5747a1e6c7fa7c49a0292e78b4e8c456332caa084fc7abad8de828652", size = 172459, upload-time = "2026-07-09T13:48:44.271Z" }, + { url = "https://files.pythonhosted.org/packages/d2/dd/614fb9912157ac0128e6050859ccf06d9f13df9a944a803e8f80f6157e38/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d11a7bc1e02da8984d32e6de9e0826c6edac00eac17de270f372bf32f9a0af63", size = 557259, upload-time = "2026-07-09T13:48:45.664Z" }, + { url = "https://files.pythonhosted.org/packages/3e/11/d072711704de3d21bec08b6c2f36a215200ca1d5e01a390ea1ac434080a0/uuid_utils-0.17.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7a49f47ac26df3e431c56b825c1bae8e6d3d591fdbb7438c227cc9845a7e3d73", size = 286271, upload-time = "2026-07-09T13:48:47.018Z" }, + { url = "https://files.pythonhosted.org/packages/18/6d/8a63e5eb2d5a6ba69a6c2036e305075bd6f5a022e7ea25fc6ce0eb7c51d2/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32df1944808877702ceea398c103881c09a679bb672a215e01c2a84231266bf9", size = 320025, upload-time = "2026-07-09T13:48:48.208Z" }, + { url = "https://files.pythonhosted.org/packages/f7/2d/bdc2caf9719d9090d7c46043242ae6136cba4f7a7ee384992ab905ad9aa1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:98c88d3edd08e7245562e9815996dbc6f0bd4745e1c76462f24af5ae4e187dd1", size = 327931, upload-time = "2026-07-09T13:48:49.673Z" }, + { url = "https://files.pythonhosted.org/packages/b6/33/9219d09d51ead282b578b2a4e0a515c2cce3ec52076cada8bfb7e35727d5/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a4370089c8b2e42f1db51d76408c7fa8eaa2934bf854d17983d16179c07c098", size = 438537, upload-time = "2026-07-09T13:48:50.842Z" }, + { url = "https://files.pythonhosted.org/packages/d8/79/e8e0f8b3955f2081c116157119d87659937893242eb834aa170da04d660b/uuid_utils-0.17.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:09a55b7a5ae764985cb46467496a1787678d0a1400356157a080ad95b1a36869", size = 320656, upload-time = "2026-07-09T13:48:52.164Z" }, + { url = "https://files.pythonhosted.org/packages/d5/5e/d1ceddc430ff04b6e21704b2030d4438074a2f478b265dab43da957791c1/uuid_utils-0.17.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:56aa6488b931246fae11924e4bd0e2b32677e63945eecb71c29e3c2ca0dc3131", size = 345310, upload-time = "2026-07-09T13:48:54.076Z" }, + { url = "https://files.pythonhosted.org/packages/d5/62/89438e12f389a843e626b7e37691319a057b3d6b80914609106891faadda/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:309a35f12d99dde19032bc2259cda6431c85eeac0879134dc777cc3087d7e1cb", size = 496771, upload-time = "2026-07-09T13:48:55.365Z" }, + { url = "https://files.pythonhosted.org/packages/87/d2/eedcd99f522d60e238ead03844f0d51743ba84d33044959e230b756bf212/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:21c79b61ff750abcf057163dd764ccb6196cde7a26cda1b31b45cd97769e03b3", size = 603631, upload-time = "2026-07-09T13:48:56.746Z" }, + { url = "https://files.pythonhosted.org/packages/0e/a8/bb1b38aaddd7243b6e562c6694f499bf094800918316192fd8cb2cdc2620/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4134353bfe3026ddab8e886002dc52bc5a0ab04611aabb0eaae23c32e6e57f64", size = 562008, upload-time = "2026-07-09T13:48:58.241Z" }, + { url = "https://files.pythonhosted.org/packages/b4/77/5f7ed930dc105e293845c09e4d5bd84076318a12f45a46783e1af64906d7/uuid_utils-0.17.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7c89359affecebe2e39e6a116d069b363c936511a9572b308402489a26957d89", size = 525527, upload-time = "2026-07-09T13:48:59.784Z" }, + { url = "https://files.pythonhosted.org/packages/fd/25/1b55697adf6811a6f92cff6340e6b03e31fd6bc51066a5c10698c29b3679/uuid_utils-0.17.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:6a019a31bc4db89a0903a3e4f6b218571f3a6ff0ad4b3d3fe1c8f91a05ff6e3e", size = 97965, upload-time = "2026-07-09T13:49:01.217Z" }, + { url = "https://files.pythonhosted.org/packages/26/bf/cd729343de4684230be8a966bad7bfc2cf10ce3e643b1189a8b5370dbe35/uuid_utils-0.17.0-cp313-cp313-win32.whl", hash = "sha256:b3131a82d0c7611f0aa480a6d36929e001a3f54ba0fc029a8118a5863cce513c", size = 167316, upload-time = "2026-07-09T13:49:02.354Z" }, + { url = "https://files.pythonhosted.org/packages/76/f0/e602ae0a1b139a7826e5189b93d91902564def06d5006324fd2faf82c8fc/uuid_utils-0.17.0-cp313-cp313-win_amd64.whl", hash = "sha256:9e311f908d2f842fca4c7dcebc4f10306b8089b204ef04cf6704b4332c9ff6ff", size = 173630, upload-time = "2026-07-09T13:49:03.529Z" }, + { url = "https://files.pythonhosted.org/packages/1a/52/024ebece265b387154115dc4f1d9727174ef82623069f4bec8b7ed7e73f7/uuid_utils-0.17.0-cp313-cp313-win_arm64.whl", hash = "sha256:c351737e2e65497c7200ab4ffb8af97e9f48be6488309abdd265fe08d66ee92f", size = 173214, upload-time = "2026-07-09T13:49:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/56/44/e2fd3fdf356e1b55d2acf1b956b4f3f29ffb215a99c387eba04b1c5fba66/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:673d89cc434cc9b97a0b4cf61272f6fca70a81f64eb0afbface2a0d9f77f06cd", size = 562232, upload-time = "2026-07-09T13:49:06.201Z" }, + { url = "https://files.pythonhosted.org/packages/19/28/65e0980d668a6d44e699f59d1acf43d6b5d4893592c115ce7c680bb4dfa1/uuid_utils-0.17.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:387cf7437c94ddec08651a0f1081381299c7075bc48a6251d8922bf39973378a", size = 287858, upload-time = "2026-07-09T13:49:07.45Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8d/5e97bcebc90fb6a10f98af3dc1ba552e04183aba59e2edc0b9cf486dd998/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:220b52746d99e11964badac3c0869016e0c24bafb70a7dd5c2c072a6be3da9cc", size = 321587, upload-time = "2026-07-09T13:49:09.489Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d7/88b2a2370cc3d455ba0515fb6f5c8f7ac0c0f55a86801b6e56a432f22c17/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0ab4a66e7a035ad6625cfc1fbdb34f5c2d25a80ae1ef4bfee458ea2036333c6d", size = 328964, upload-time = "2026-07-09T13:49:11.292Z" }, + { url = "https://files.pythonhosted.org/packages/bd/0f/181c5da673953dfc0958cb4fb3a4984a9098673ddb05cac68e994bc8511b/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5641071337eb11d61a001ea08793bf72216f3241f0a433ed2764804b2a3e3cc7", size = 442909, upload-time = "2026-07-09T13:49:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/ec/38/5c5e665af542884a8fd3c61725c38453239e13940326b5b70f3ef8881a97/uuid_utils-0.17.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9082e709014946b1f6e96ae6ecd93652efca2d2a6a3ab67dbe151c8b4bf193a4", size = 323076, upload-time = "2026-07-09T13:49:13.897Z" }, + { url = "https://files.pythonhosted.org/packages/f5/35/7de97de18cbf226c2a4f2104ad15e56ca4491717c81c0b71795c0c585b4e/uuid_utils-0.17.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1fd6f0e8a162dc0e9255b6aebe3cd175e76c33202f1bf39da9e6294b93db0099", size = 347360, upload-time = "2026-07-09T13:49:15.237Z" }, + { url = "https://files.pythonhosted.org/packages/26/a1/9915d5dd59fdd1957ded5d188c0ea0b9db5a1d84d42c8d8828a7b83b366e/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d63010803d7c368963bbe6f7ec379593e76dd581d7db0f29118d88713c9e0354", size = 499267, upload-time = "2026-07-09T13:49:16.774Z" }, + { url = "https://files.pythonhosted.org/packages/c0/05/88108405262ec850cea0f95733445d6873e5772af3292baabd9ef8457740/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a46bedc273b6f58f11dee816ff74999625ef8d007890f411b7a4975bf1c89330", size = 604940, upload-time = "2026-07-09T13:49:18.147Z" }, + { url = "https://files.pythonhosted.org/packages/89/d5/6dbcd300de47cc443cff2656cd5327a385751213dcb2101cfee7388170b2/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:405233a5f625b3d995648f4647fa6befa4567cf3f74e1f6b9837e16f7310f0e0", size = 564172, upload-time = "2026-07-09T13:49:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ab/94/e8057f2288a415fba8a978bca4b589f5cb6b91a028a5dc07a1775938b33f/uuid_utils-0.17.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b6c5d2d71e1f17329150ad9427d27f4a3f29a01792e7ecdc64a98ac5368fc4d5", size = 528533, upload-time = "2026-07-09T13:49:21.075Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6b/31713148c77e48e62f51aa042a98a54a8be0396912ea5130f83f52ae722d/uuid_utils-0.17.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7e9b8728ba07a3cb2f29d5aa1a266c2664eb8ef0fd43afa34627c92f7fac8f0", size = 99197, upload-time = "2026-07-09T13:49:22.351Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f3/ca6f6ac5428312df8ed632f6dd9f9e6aba23090471fcdeae53eab027e8b3/uuid_utils-0.17.0-cp314-cp314-win32.whl", hash = "sha256:58838921e377791ef22c64cc92141bfae030f43651ff9272f0f28a208a9e6a5a", size = 169540, upload-time = "2026-07-09T13:49:23.563Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cd/7ede0db66411fa09817d79b680f7454ea9bee2d374e1922e4efd065760a3/uuid_utils-0.17.0-cp314-cp314-win_amd64.whl", hash = "sha256:42275ebd0e8e74e32cdbfb8bd88fc99576567d51d54a508020611fd8f4f463a0", size = 175984, upload-time = "2026-07-09T13:49:24.703Z" }, + { url = "https://files.pythonhosted.org/packages/f0/81/533b5f80cd4918c0693f4e1b7b90ceb1caa45f4266ae8b528135d7ecca5d/uuid_utils-0.17.0-cp314-cp314-win_arm64.whl", hash = "sha256:b5d11cccba076a32321ef1380dea956821f0b51794ef59df64e58fb1cd543aae", size = 174749, upload-time = "2026-07-09T13:49:25.886Z" }, + { url = "https://files.pythonhosted.org/packages/a0/13/f400ac39d06fd8be5b099c09e41bb975205926722a3e8d53348817cb7ff9/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fae8b282f0cb22a5de222999f7723f4e5ec04f6fcdf4aaef879b5b36625ae2b0", size = 562610, upload-time = "2026-07-09T13:49:27.374Z" }, + { url = "https://files.pythonhosted.org/packages/03/8c/c71c8312304c56f6d0bcba87cd402fa79bec35d18ffc8c41954196ca68e5/uuid_utils-0.17.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:967955620df45e6cffe2e9950cb9903cb455649396f896b26b04363a91a5054b", size = 289473, upload-time = "2026-07-09T13:49:28.989Z" }, + { url = "https://files.pythonhosted.org/packages/bb/cd/522117e2e5184ca1d4f0f85ee833e9e21bd8c6b99eff8a4d1a8e5a194e33/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:375cde148430d60a4a07c03abaa0774c4fddfdd90de99b4ba02f24088bc9d750", size = 321600, upload-time = "2026-07-09T13:49:30.4Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f4/0d81f9bd346fc717bc561c08fa6457e0328966eb76e536b938fe77d56459/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:975c17da26c5b9d46c336b03c52a057ac28378d6f9d98b58d32a038589bb3912", size = 329569, upload-time = "2026-07-09T13:49:31.732Z" }, + { url = "https://files.pythonhosted.org/packages/5e/41/26e1363f36a94c9e8ec2dd21d5f63088d3e7c723adbb12dcc8fdc77be417/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3150d836290c88f1d26eb59c4db280d87417dd3bfaadd2889c77416c8f0ff6fa", size = 442051, upload-time = "2026-07-09T13:49:33.024Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a7/2c1ed1b34d7df7fdcc11c28fd26d94d44843b37d9af2435ff9fd8abdbc08/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9472a8de37faf8bd216c628e0e68c8f6bef730d3ba0a5060f3b0fa460c992ac2", size = 324372, upload-time = "2026-07-09T13:49:34.554Z" }, + { url = "https://files.pythonhosted.org/packages/78/bf/328d3c6bb22c496944a1b3b732207d71aa6964eb604e5e3b9dcb91ed0a00/uuid_utils-0.17.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d27c531edb8d1f38ca2eddaa1fa24913a460aeb721f2efd4ef42a124ce94e354", size = 348548, upload-time = "2026-07-09T13:49:35.898Z" }, + { url = "https://files.pythonhosted.org/packages/3e/76/a07de5cb7b90582fdbbc830fd19be129cbbb9897cfe239fef469d7bd2d09/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5670c52a438e21483ce715776144914a4e2a2a5c62d9dee15f8a3e90cf128ae6", size = 498985, upload-time = "2026-07-09T13:49:37.142Z" }, + { url = "https://files.pythonhosted.org/packages/f4/62/9966e46ae34fcec6b06119631fb3c09705ea78835035ce3a82d3348eb61a/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:6f29689a76fe7a49cbd629a794d0ec1eab48814e323a00a146a741b0195bde68", size = 605183, upload-time = "2026-07-09T13:49:38.648Z" }, + { url = "https://files.pythonhosted.org/packages/d7/4e/bb962ba0fe31e903b199f22cf4c1a6cba35a8987aef526d287277ab8ca8b/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:4441600447d340ae103a353f01dbcd22ff680e5ee1a22988efe8d7b791d8fdb3", size = 565412, upload-time = "2026-07-09T13:49:40.115Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/122adfeeeae8a84ccfd43bce627b104d12a2180a93bffd2c0e1b54dad7a6/uuid_utils-0.17.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7b04935a79c03c41ad08d0a5f390aac968bfb561f1268897bc5b0f077971efd", size = 529885, upload-time = "2026-07-09T13:49:41.513Z" }, + { url = "https://files.pythonhosted.org/packages/b3/4f/257304dded339dc35fc9bf35722ac68fd4fdb930f255b8f7bccdf74ebba9/uuid_utils-0.17.0-cp314-cp314t-win32.whl", hash = "sha256:239d8a281fe10bae33205b5d43185834d556b18434e0a113b5dc1dfb2fd97e91", size = 169472, upload-time = "2026-07-09T13:49:42.871Z" }, + { url = "https://files.pythonhosted.org/packages/35/c8/e78c06db7e9ce317ce7b8759ff2058333eac75caa8c22b75f0059589c9be/uuid_utils-0.17.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e288a06cbbbcd01b44386e767985c9e21d2ad9bf59829aa7058d9a2a494804ab", size = 176271, upload-time = "2026-07-09T13:49:44.105Z" }, + { url = "https://files.pythonhosted.org/packages/a7/11/bd1c70e1ad3301163cebe66c8d26de26e6814d52f642a849448bd2833626/uuid_utils-0.17.0-cp314-cp314t-win_arm64.whl", hash = "sha256:1776a80d16369999b21627028cc5dbce819be83e1e079fdd7a51b587d2916db9", size = 175004, upload-time = "2026-07-09T13:49:45.591Z" }, + { url = "https://files.pythonhosted.org/packages/ee/14/4ae708968b15cac7b68d5b854bfce724b21faa1c7a5147fb96d87f468a45/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7b9044ce4acbf392d4b3a503fe377641f4deff82e6c341c36ef27af0dea76cdf", size = 567823, upload-time = "2026-07-09T13:49:46.902Z" }, + { url = "https://files.pythonhosted.org/packages/4c/e2/d3af9c3d1dc6efb9ee1cffab30f3f2aacacc3892b21b495d78d34c6696bc/uuid_utils-0.17.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9a91c4814c7150a4d798da691b7804eacd78c4b84fb392a60fa0de21341861eb", size = 288763, upload-time = "2026-07-09T13:49:48.491Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c2/f1b183e412387529893015a94a8447633c665f6d0392de20e245680e636a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2dd4a21baaac9a88486f0dd166c5793feb101a0bb9f006f2c401657fff5a1343", size = 324919, upload-time = "2026-07-09T13:49:49.972Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3c/d32c799bdd51f3b08b6ee95f9de921b59c69075a96767f937fab55014813/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:32abaafc8e91928b3d9f4d82e42d2094041e38ad6bb964066faadff28e4162f1", size = 332689, upload-time = "2026-07-09T13:49:51.402Z" }, + { url = "https://files.pythonhosted.org/packages/6f/90/b4cd455619ff276dc3c3262a7420ead63aa1e531362f00df4cdb07d90e0a/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd741c73440b328f937dc53b344ecadc46bc4f0cec0333a8f42b55f3468ce7ec", size = 445726, upload-time = "2026-07-09T13:49:52.757Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f1/5cc042a37932aa9a66eb8ab4a9a5b31d80261ae4565ff0193d8cc1fb9392/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:89a0980d49683c00539c59cd9f46b1908c538e6b5b0a48ad12187bb856d0f391", size = 325610, upload-time = "2026-07-09T13:49:54.191Z" }, + { url = "https://files.pythonhosted.org/packages/5e/72/9e800c41d766484484e97845a7a7f677ba94462df86c97183e0290229d16/uuid_utils-0.17.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:de1064663aa7c839286488a319d2b3b478ca5ab5b2091ade888ed0eeca11a98a", size = 352672, upload-time = "2026-07-09T13:49:55.748Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8e/86ce2c03a1d9674530f6649e49067f7c69929600127077731de590d12132/uuid_utils-0.17.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2db386941cfdecdd0b5a8ceeed5cf7479c83d1730dcf64a48d43cfa018cc3310", size = 178681, upload-time = "2026-07-09T13:49:57.096Z" }, ] [[package]] name = "volcengine-python-sdk" -version = "5.0.18" +version = "5.0.48" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -2351,9 +2947,9 @@ dependencies = [ { name = "six" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/61/bc/15bc52f304c4d9762fa03db17527636d1b9b5d31617f371df6420e17a8d0/volcengine_python_sdk-5.0.18.tar.gz", hash = "sha256:b86ea55855aa8f1ca5baa92da7f4d1050b8c4b96762808f8e1aab49a2c5b0621", size = 8276904, upload-time = "2026-03-19T12:09:09.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/26/33282a2954cfd0cac2f596846953cb04ccbc6fe6e439499fe993c86c19fc/volcengine_python_sdk-5.0.48.tar.gz", hash = "sha256:bb78995e35c34ad7a24e535826a1cb197e1209783c8a7dc11b001a09cb4bc48c", size = 10754352, upload-time = "2026-09-03T12:18:26.556Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/df/3a/51a8b9a58854585b963a2f6a8c2f04e7ff875f4904de339554f2a28a1d96/volcengine_python_sdk-5.0.18-py2.py3-none-any.whl", hash = "sha256:ea36abcd786ebb10d21ebd1df239f8384feede952cd98f1642461562d7c8e926", size = 32542954, upload-time = "2026-03-19T12:09:02.951Z" }, + { url = "https://files.pythonhosted.org/packages/24/be/29eefbbdae397908c8a05eee73208229065481f2d797f5f2c720a6a02193/volcengine_python_sdk-5.0.48-py2.py3-none-any.whl", hash = "sha256:05c15f9138e016e479cdf29971d1e5fedbb9e8f4378930c68e2a9622d087995c", size = 43152725, upload-time = "2026-09-03T12:18:20.217Z" }, ] [package.optional-dependencies] @@ -2366,295 +2962,380 @@ ark = [ [[package]] name = "wcwidth" -version = "0.6.0" +version = "0.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/35/a2/8e3becb46433538a38726c948d3399905a4c7cabd0df578ede5dc51f0ec2/wcwidth-0.6.0.tar.gz", hash = "sha256:cdc4e4262d6ef9a1a57e018384cbeb1208d8abbc64176027e2c2455c81313159", size = 159684, upload-time = "2026-02-06T19:19:40.919Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/57/ed58088fafdf4c55a0ad6bde846502567645424d7ebf325230b9237f4085/wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb", size = 1458450, upload-time = "2026-08-28T18:10:06.875Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/5a/199c59e0a824a3db2b89c5d2dade7ab5f9624dbf6448dc291b46d5ec94d3/wcwidth-0.6.0-py3-none-any.whl", hash = "sha256:1a3a1e510b553315f8e146c54764f4fb6264ffad731b3d78088cdb1478ffbdad", size = 94189, upload-time = "2026-02-06T19:19:39.646Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0e/57f6bb3024a597b2e8ec4aee710ffe62ddc95af2e2bb1ee7a7abdc22c68c/wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4", size = 331669, upload-time = "2026-08-28T18:10:04.909Z" }, ] [[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, upload-time = "2026-01-10T09:23:47.181Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f2/db/de907251b4ff46ae804ad0409809504153b3f30984daf82a1d84a9875830/websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8", size = 177340, upload-time = "2026-01-10T09:22:34.539Z" }, - { 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, upload-time = "2026-01-10T09:22:36.332Z" }, - { url = "https://files.pythonhosted.org/packages/58/5d/88ea17ed1ded2079358b40d31d48abe90a73c9e5819dbcde1606e991e2ad/websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d", size = 175319, upload-time = "2026-01-10T09:22:37.602Z" }, - { 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, upload-time = "2026-01-10T09:22:38.789Z" }, - { 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, upload-time = "2026-01-10T09:22:39.893Z" }, - { url = "https://files.pythonhosted.org/packages/87/05/536652aa84ddc1c018dbb7e2c4cbcd0db884580bf8e95aece7593fde526f/websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5", size = 185361, upload-time = "2026-01-10T09:22:41.016Z" }, - { 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, upload-time = "2026-01-10T09:22:42.442Z" }, - { url = "https://files.pythonhosted.org/packages/77/fb/d3f9576691cae9253b51555f841bc6600bf0a983a461c79500ace5a5b364/websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6", size = 178246, upload-time = "2026-01-10T09:22:43.654Z" }, - { url = "https://files.pythonhosted.org/packages/54/67/eaff76b3dbaf18dcddabc3b8c1dba50b483761cccff67793897945b37408/websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac", size = 178684, upload-time = "2026-01-10T09:22:44.941Z" }, - { url = "https://files.pythonhosted.org/packages/84/7b/bac442e6b96c9d25092695578dda82403c77936104b5682307bd4deb1ad4/websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00", size = 177365, upload-time = "2026-01-10T09:22:46.787Z" }, - { 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, upload-time = "2026-01-10T09:22:47.999Z" }, - { url = "https://files.pythonhosted.org/packages/40/1e/9771421ac2286eaab95b8575b0cb701ae3663abf8b5e1f64f1fd90d0a673/websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39", size = 175328, upload-time = "2026-01-10T09:22:49.809Z" }, - { 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, upload-time = "2026-01-10T09:22:51.071Z" }, - { 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, upload-time = "2026-01-10T09:22:52.224Z" }, - { url = "https://files.pythonhosted.org/packages/4a/34/9bf8df0c0cf88fa7bfe36678dc7b02970c9a7d5e065a3099292db87b1be2/websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1", size = 185583, upload-time = "2026-01-10T09:22:53.443Z" }, - { 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, upload-time = "2026-01-10T09:22:55.033Z" }, - { url = "https://files.pythonhosted.org/packages/91/d6/7d4553ad4bf1c0421e1ebd4b18de5d9098383b5caa1d937b63df8d04b565/websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89", size = 178261, upload-time = "2026-01-10T09:22:56.251Z" }, - { url = "https://files.pythonhosted.org/packages/c3/f0/f3a17365441ed1c27f850a80b2bc680a0fa9505d733fe152fdf5e98c1c0b/websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea", size = 178693, upload-time = "2026-01-10T09:22:57.478Z" }, - { url = "https://files.pythonhosted.org/packages/cc/9c/baa8456050d1c1b08dd0ec7346026668cbc6f145ab4e314d707bb845bf0d/websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9", size = 177364, upload-time = "2026-01-10T09:22:59.333Z" }, - { 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, upload-time = "2026-01-10T09:23:01.171Z" }, - { url = "https://files.pythonhosted.org/packages/aa/82/39a5f910cb99ec0b59e482971238c845af9220d3ab9fa76dd9162cda9d62/websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c", size = 175323, upload-time = "2026-01-10T09:23:02.341Z" }, - { 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, upload-time = "2026-01-10T09:23:03.756Z" }, - { 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, upload-time = "2026-01-10T09:23:05.01Z" }, - { url = "https://files.pythonhosted.org/packages/37/e5/8e32857371406a757816a2b471939d51c463509be73fa538216ea52b792a/websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8", size = 185653, upload-time = "2026-01-10T09:23:06.301Z" }, - { 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, upload-time = "2026-01-10T09:23:07.492Z" }, - { url = "https://files.pythonhosted.org/packages/3c/a1/3d6ccdcd125b0a42a311bcd15a7f705d688f73b2a22d8cf1c0875d35d34a/websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a", size = 178255, upload-time = "2026-01-10T09:23:09.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ae/90366304d7c2ce80f9b826096a9e9048b4bb760e44d3b873bb272cba696b/websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156", size = 178689, upload-time = "2026-01-10T09:23:10.483Z" }, - { url = "https://files.pythonhosted.org/packages/f3/1d/e88022630271f5bd349ed82417136281931e558d628dd52c4d8621b4a0b2/websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0", size = 177406, upload-time = "2026-01-10T09:23:12.178Z" }, - { 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, upload-time = "2026-01-10T09:23:13.511Z" }, - { url = "https://files.pythonhosted.org/packages/bb/f4/d3c9220d818ee955ae390cf319a7c7a467beceb24f05ee7aaaa2414345ba/websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4", size = 175328, upload-time = "2026-01-10T09:23:14.727Z" }, - { 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, upload-time = "2026-01-10T09:23:15.939Z" }, - { 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, upload-time = "2026-01-10T09:23:17.148Z" }, - { url = "https://files.pythonhosted.org/packages/b9/ca/bf1c68440d7a868180e11be653c85959502efd3a709323230314fda6e0b3/websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1", size = 185711, upload-time = "2026-01-10T09:23:18.372Z" }, - { 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, upload-time = "2026-01-10T09:23:19.652Z" }, - { url = "https://files.pythonhosted.org/packages/dd/d1/574fa27e233764dbac9c52730d63fcf2823b16f0856b3329fc6268d6ae4f/websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8", size = 177915, upload-time = "2026-01-10T09:23:21.458Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f1/ae6b937bf3126b5134ce1f482365fde31a357c784ac51852978768b5eff4/websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d", size = 178381, upload-time = "2026-01-10T09:23:22.715Z" }, - { url = "https://files.pythonhosted.org/packages/06/9b/f791d1db48403e1f0a27577a6beb37afae94254a8c6f08be4a23e4930bc0/websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244", size = 177737, upload-time = "2026-01-10T09:23:24.523Z" }, - { 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, upload-time = "2026-01-10T09:23:25.781Z" }, - { url = "https://files.pythonhosted.org/packages/74/9b/6158d4e459b984f949dcbbb0c5d270154c7618e11c01029b9bbd1bb4c4f9/websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641", size = 175486, upload-time = "2026-01-10T09:23:27.033Z" }, - { 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, upload-time = "2026-01-10T09:23:28.259Z" }, - { 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, upload-time = "2026-01-10T09:23:29.449Z" }, - { url = "https://files.pythonhosted.org/packages/19/60/b8ebe4c7e89fb5f6cdf080623c9d92789a53636950f7abacfc33fe2b3135/websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944", size = 186062, upload-time = "2026-01-10T09:23:31.368Z" }, - { 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, upload-time = "2026-01-10T09:23:32.627Z" }, - { url = "https://files.pythonhosted.org/packages/c2/b6/b9afed2afadddaf5ebb2afa801abf4b0868f42f8539bfe4b071b5266c9fe/websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6", size = 178085, upload-time = "2026-01-10T09:23:33.816Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3e/28135a24e384493fa804216b79a6a6759a38cc4ff59118787b9fb693df93/websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd", size = 178531, upload-time = "2026-01-10T09:23:35.016Z" }, - { 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, upload-time = "2026-01-10T09:23:36.166Z" }, - { 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, upload-time = "2026-01-10T09:23:37.409Z" }, - { 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, upload-time = "2026-01-10T09:23:39.158Z" }, - { 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, upload-time = "2026-01-10T09:23:41.031Z" }, - { url = "https://files.pythonhosted.org/packages/9a/3f/f70e03f40ffc9a30d817eef7da1be72ee4956ba8d7255c399a01b135902a/websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767", size = 178735, upload-time = "2026-01-10T09:23:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/6f/28/258ebab549c2bf3e64d2b0217b973467394a9cea8c42f70418ca2c5d0d2e/websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec", size = 171598, upload-time = "2026-01-10T09:23:45.395Z" }, +version = "16.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/f7/bc3a25c5ec26ce62ce487690becc2f3710bbc7b33338f005ad390db0b986/websockets-16.1.1.tar.gz", hash = "sha256:db234eda965dcce15df96bb9709f587cd87d4d52aaf0e80e2f34ec04c7670c57", size = 182204, upload-time = "2026-07-17T22:51:05.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/03/47debfe28e9d6d354be5d777b67fd44c359b9eb299a5d103500bd7cc3e37/websockets-16.1.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d0fcf657e9f13ff4b177960ab2200237b12994232dfb6df16f1cfe1d4339f93c", size = 179566, upload-time = "2026-07-17T22:48:49.596Z" }, + { url = "https://files.pythonhosted.org/packages/72/93/31efa1ed78c17e5cfc229fd449e3966e1b9cc15753204cd585cc8dd01f4a/websockets-16.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b852788aa51764e2d8e4cf5493d559326bcae5e38d16ba25ffa322b034df272a", size = 177250, upload-time = "2026-07-17T22:48:50.942Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/542378ab3972b0c1cf1df3df3eff9591cea0d30c58c3aa3c4ddbc244e787/websockets-16.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1427fb4cf0d72f66333e2cacc3ff5f575bf2d7008166ce991a4a470b21d51a22", size = 177528, upload-time = "2026-07-17T22:48:52.59Z" }, + { url = "https://files.pythonhosted.org/packages/33/d9/162321f63c7eed558e9e1798ed7a1e34a4f6dab51f35419e4ed7a4907979/websockets-16.1.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:da4ca1a9d72f9030b3146b8d7022719a9f3d478f61efe6f7dd51d243f61c51b2", size = 186859, upload-time = "2026-07-17T22:48:53.915Z" }, + { url = "https://files.pythonhosted.org/packages/de/09/87df740f7430ce564bd52402e9c9458d4d0459cc7d2ee29e530c8204851b/websockets-16.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:86d7f0f8bdb25d2c632b72527325e4776430fd5bc61b9118de4e2b8ddb5f5b01", size = 188095, upload-time = "2026-07-17T22:48:55.384Z" }, + { url = "https://files.pythonhosted.org/packages/d2/12/3d2703af7cc095f3c81904c92208cc1ae79affbc67376944b50ee9301f73/websockets-16.1.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7dfcad78ea1492ee3a9ec765cb7f51bbc17d477107aaf6b22abf7b2558d1c5a0", size = 191385, upload-time = "2026-07-17T22:48:56.742Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/986aa0234a964a00f5149cfc46e136e96c8faad1c783474550f40d31aef4/websockets-16.1.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fb9a0a6dc3d1b3986cb88091b6899f0396651e0f74e2c9766ab8d6ffc3842e29", size = 188653, upload-time = "2026-07-17T22:48:58.134Z" }, + { url = "https://files.pythonhosted.org/packages/35/6b/10f9d03e3970a69ba67bd3b46b87a929b586d0300fadbfe14f57c1f85490/websockets-16.1.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29dfa8114c4a620c69591c5973860f768eac29d3fd6904f37f34266cb219c512", size = 187426, upload-time = "2026-07-17T22:48:59.515Z" }, + { url = "https://files.pythonhosted.org/packages/56/db/bb3aad62bf63d8bb3f0634b2eabffcfb3677a34bd19492110ff6869cf703/websockets-16.1.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ff9417c0ada4d0f7d212f928303e5579bdf3ace4c802fa4afabb30995da58c3", size = 184882, upload-time = "2026-07-17T22:49:00.916Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4c/c09a2ea9bfbeccce52fdc383e5f28af4bc8843338aabac28c81489af6120/websockets-16.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fe0b50da2d84535fb4f7b4bfa951280f97ce3d558a0443b541166d609e67b57", size = 187584, upload-time = "2026-07-17T22:49:02.283Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8b/31bb4eb4d9eaacf1fdd39d115772a8aeaedfc19b5dc262e57ffbc8a9d42c/websockets-16.1.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:34420aaa64440ebd51ac72ca8a45ef4626429438c9b02e633ae412ed43f925d3", size = 186174, upload-time = "2026-07-17T22:49:03.973Z" }, + { url = "https://files.pythonhosted.org/packages/2f/e4/dc02d725610a1ad49e193ef91a548194d71bdc6cdf27da83067dd1f73995/websockets-16.1.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a6a61aff018180c9c50b7b0da33bfd29d378af3497429c95006c589a23a11648", size = 187986, upload-time = "2026-07-17T22:49:05.553Z" }, + { url = "https://files.pythonhosted.org/packages/e0/73/30ed84c8bfd14c73d4af29d5ed9323c3073b48e0b7b23b67070f4e7fd59b/websockets-16.1.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:04fd29a0e2fe9414a95b00e92c67ae51bf900c50c0f8a4b2dafdad621f49ea1d", size = 185565, upload-time = "2026-07-17T22:49:06.959Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d3/4be8d4959f51e31b4f8fc0ece12b45bd3b6c0d15ea23b9990d9c11fc805f/websockets-16.1.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5c31aa7e39ee3e8a358573257f1c0bb5c52430d1b637030dd9c8cc2c282926be", size = 186598, upload-time = "2026-07-17T22:49:08.293Z" }, + { url = "https://files.pythonhosted.org/packages/26/fa/abb38597a52d84ed9cfacadc7a0c6f2db282c0ab23cdf72b58a666a21227/websockets-16.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d14bfb217eb4701e850f1525c9d29d79c44794cdf1c299ead25f39f8c78dea81", size = 186834, upload-time = "2026-07-17T22:49:09.766Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/1119ad08a228b90c4eb77fbe48df7836731a605f5f881ba701ca826a4a65/websockets-16.1.1-cp311-cp311-win32.whl", hash = "sha256:2e28e602bb13da44fbe518c1781a88e3b9d4c3d48d02c9bad83e546164336f57", size = 179940, upload-time = "2026-07-17T22:49:11.196Z" }, + { url = "https://files.pythonhosted.org/packages/71/b2/e511c1c6f64a95c2f3fc54bffda0e14eaa7e9442be605c29270f7589b918/websockets-16.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:7421fad442de870a8cbf2287d1cad7e706ece0dbfeba5e911df132cbdc1cb56a", size = 180239, upload-time = "2026-07-17T22:49:12.519Z" }, + { url = "https://files.pythonhosted.org/packages/17/9d/681cda21c9eee743203a6cb79b9d3d05adad9aa60ec660c6c9bf4dd619ca/websockets-16.1.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:cc97814dfb786a83b6e2dc2e79351e1b83e6d715647d6887fcabd83026417a00", size = 179600, upload-time = "2026-07-17T22:49:13.92Z" }, + { url = "https://files.pythonhosted.org/packages/fb/8d/6195a88b45e8d2a8f745fc2046e36f885a3c9763e6767d2c46229bf9510c/websockets-16.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e047dc87ef7ca50f4d309bf775ad4a71711c58556d75d7bd0604b2317f43e94b", size = 177272, upload-time = "2026-07-17T22:49:15.453Z" }, + { url = "https://files.pythonhosted.org/packages/73/e3/fe2d498c64dea0095c9a9f9a351af4cd6eef31b618395582bc1f38ba45ff/websockets-16.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:01fbdcbac298efe19360b94bc0039c8f746f0220ba570f327577bfee81059175", size = 177542, upload-time = "2026-07-17T22:49:16.875Z" }, + { url = "https://files.pythonhosted.org/packages/fe/ed/f1831681fce0e3242346e5458486003c5f124ed69e5e0b847fd029db4973/websockets-16.1.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f62863e8a00a6d33c3d6566ec0b89f23787b747ffe0c3bc71ec0e76b82c94b1", size = 187137, upload-time = "2026-07-17T22:49:18.323Z" }, + { url = "https://files.pythonhosted.org/packages/6f/79/4ff9dcc1bb46f6b4c536936dde1fd60f9b564f3304307274db97f4c9496d/websockets-16.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8087e82f842609734c9b5a1330464f8e94e346ba0e18c832c08bafa4b0d63c15", size = 188374, upload-time = "2026-07-17T22:49:19.65Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/5c49b6efb36cab733d23773f6de575e1dba65736ead17d5d2b2a1daef779/websockets-16.1.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bb5d041a8307d2e18782e7ce777f6fdb1e8c2f5d09291484b18c294b789d9aa", size = 191155, upload-time = "2026-07-17T22:49:21.331Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f6/56ccceda3a4838d18f1d40821480da4775397e8b1eecf4031e20c50e2e90/websockets-16.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1db4de4a0e95673f7545d393c49eeb0c2f18ac1ef93073218c79d5cdb2ee75ab", size = 189011, upload-time = "2026-07-17T22:49:22.889Z" }, + { url = "https://files.pythonhosted.org/packages/86/d6/ad5286241a2bce1107e2798d3bfbd62cf79aee167bdb654f8cb1e9dbf949/websockets-16.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f17dbe07eb3ea7f99e4df9b7e0efefe80fbf30d37a8cc4d561a0aed310bc8847", size = 187766, upload-time = "2026-07-17T22:49:24.339Z" }, + { url = "https://files.pythonhosted.org/packages/bc/67/d65c970b7e347fdca69479beb7811c2060529956730a7a4e3ae7c66b0e31/websockets-16.1.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4b57693728576d84ede0a77987ab16881b783d2cd9f1dc180a8fbbc3f79c4428", size = 185173, upload-time = "2026-07-17T22:49:25.743Z" }, + { url = "https://files.pythonhosted.org/packages/1d/5b/14af3cd4ee69d8ea9baca58f3dc3cfb1ba78332a347fd478cb096549d60e/websockets-16.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a636ff1e7a5c4edf71ef0e79adae7f25dba93b4fcbe3dc958733477ffeb0eaf", size = 187809, upload-time = "2026-07-17T22:49:27.147Z" }, + { url = "https://files.pythonhosted.org/packages/7b/11/be301710d70de97e3e7b3586e6d492c9c06d6a61bf1c2202c36cf0c75607/websockets-16.1.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d6bec75c290fe484a8ba4cacdf838501e17c06ecfbbf31eede81a9e431bd7751", size = 186412, upload-time = "2026-07-17T22:49:28.611Z" }, + { url = "https://files.pythonhosted.org/packages/db/07/fe1435bf6fe738a3d3b54dbe0c18dabf12cba4d909ac8b58b539ce27c1f4/websockets-16.1.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:54509b8e92fee4453e152b7558ddef37ce9705a044922f2095a6105e3f80c96f", size = 188290, upload-time = "2026-07-17T22:49:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/8a/0a/81f394aff8efcbb01208c1ced77df0a3c7fcce584a88c7273663697946c2/websockets-16.1.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f0aa4aad3b1b69ad3fd85a0fd0952ec64331c762bd77ec51cc814170873890b2", size = 185844, upload-time = "2026-07-17T22:49:31.447Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/dd485b995473f415510251fe9bd708f2d24458f439fce958daf8d66dc7c6/websockets-16.1.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:42290eb6db4ccaca7012656738214f8514082fb6fa40cdeb61bb9a471b52e383", size = 186823, upload-time = "2026-07-17T22:49:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0b/f78de76ff446f1e66af12b43c48a35f31744de93cfdec2f4ea67d5d7bbf1/websockets-16.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:53260c8930da5771cec89439bff99c20c8cb03ddb9588b980697355a83cd4bd3", size = 187102, upload-time = "2026-07-17T22:49:34.616Z" }, + { url = "https://files.pythonhosted.org/packages/37/a1/4cf892007778eaf84ad162bfc98046e0ed89b63ac55949e3236626b2a23f/websockets-16.1.1-cp312-cp312-win32.whl", hash = "sha256:1d27fa8462ad6a1cb36206a3d0640b2333340def181fae11ed7f9adeaa5c0747", size = 179943, upload-time = "2026-07-17T22:49:36.213Z" }, + { url = "https://files.pythonhosted.org/packages/d9/de/6abe251d28c3a3f217096575400b27750b18e0b1d2fff3a2a239960fea07/websockets-16.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:b436f6ec4fc3a6b4237c84d3f83170ed2b40bb584222f0ac47a0c8a5921980c7", size = 180243, upload-time = "2026-07-17T22:49:37.626Z" }, + { url = "https://files.pythonhosted.org/packages/ce/fd/6ec6c6d2850aea25b1b2aa9901a016980bb87d01e89b3eb00470b1b5d471/websockets-16.1.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ab59169ace05dcb49a1d4118f0bde139557adf45091bd85747e36bf5de984dd1", size = 179587, upload-time = "2026-07-17T22:49:38.959Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d8/1d299d2dd34087db39831a34cc645ef8a6f89d78efada6983093513cd81c/websockets-16.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5e3b7d601f6f84156b08cc4a5e541c2b50ad7b36cfc302b657a12477c904a5df", size = 177272, upload-time = "2026-07-17T22:49:40.293Z" }, + { url = "https://files.pythonhosted.org/packages/3d/86/0a70d3ae2f0f2256bb41302d9804dbca65d4360281e7feb3e1f94102ac46/websockets-16.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd2ca96a082a36964aca83e992f72abeb61b7306c1a6cba4c7d06a7b93750cac", size = 177530, upload-time = "2026-07-17T22:49:41.786Z" }, + { url = "https://files.pythonhosted.org/packages/b5/c2/c676c69444d9db448b3f0a55a98dcc534affce0bce961d9d2f0b8499b10a/websockets-16.1.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f5d497865f05bb222cab7016c6034542e84e5f29f49c6fd3f4939cda7197b5b8", size = 187197, upload-time = "2026-07-17T22:49:43.658Z" }, + { url = "https://files.pythonhosted.org/packages/0b/13/88137fbaf726ebe29d62c1117fa11fa2bbb6209dc79d4ad738efbe36a2aa/websockets-16.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bae954c382e013d5ea5b190d2830526bfa45ad121c326da0049b8c769f185db6", size = 188433, upload-time = "2026-07-17T22:49:45.147Z" }, + { url = "https://files.pythonhosted.org/packages/01/6d/46c2f2ce6751cb26f39293e1ecbf8544cb01321397cd476c2756b98c216d/websockets-16.1.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e09f753a169951eb4f28c2c774f71069304f66e7277e0f5a2892423599cfa854", size = 189868, upload-time = "2026-07-17T22:49:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/29/2b/170a9e8097636cfde4dc3c592b6e00b18a44a2f5407606d96ca542dd5838/websockets-16.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:024193f8551a2b0eafbdd160911012c4e6c228c28430c84433253299a9e42d6a", size = 189059, upload-time = "2026-07-17T22:49:47.972Z" }, + { url = "https://files.pythonhosted.org/packages/a7/48/f0d4ebc9ab4b473b8861b9e20fdb663d515d42f7befdf62cdb60fee7a1ec/websockets-16.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:aabe464bfd13bd25f4821faf111da6fefdc389f870265a53105580e45b0a2e49", size = 187814, upload-time = "2026-07-17T22:49:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ba/39a41d3ae8e72696a9492581900611c5a91e2b07563b0bcd2523adea9854/websockets-16.1.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a28fcbc9b6baf54a2e23f8655f308e4ccc6afdd7266f8fe7954f320dcda0f785", size = 185229, upload-time = "2026-07-17T22:49:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/3c/36/ac15b604f850d1907f0a85ed721cefe47cd45034b3620069b829746cccbe/websockets-16.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79eace538c6a97e96d0d03d4f9d314f9677f5ed85a8a984992ffd90b13cb8a56", size = 187874, upload-time = "2026-07-17T22:49:52.228Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f3/3fbd5d71d59299c3770faa5884d4f45070236ca5a35ab3a61830812c409a/websockets-16.1.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:496af849a472b531f758dbd4d61338f5000538cb1a7b3d20d9d32a264517f509", size = 186469, upload-time = "2026-07-17T22:49:53.776Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/dd90349bba58af2a53ef2ddd9c32716c81eb6d59a0687939fff561860878/websockets-16.1.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5283810d2646741a0d8da2aa733d6aefa0545809afccb2a5d105a26bc45125f1", size = 188347, upload-time = "2026-07-17T22:49:55.202Z" }, + { url = "https://files.pythonhosted.org/packages/4c/f3/f73ba86427682da59b78c11d77ba56d5b801c32e84afe79b274bbd6a9bb2/websockets-16.1.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4e3b680b1e0a27457e727a0d572fd81dffa87b6dbf8b228ab57da64f7d85aead", size = 185903, upload-time = "2026-07-17T22:49:56.75Z" }, + { url = "https://files.pythonhosted.org/packages/34/7c/f95eb20e80104173b3a0a092291f89ea4047ef6e608e0a57ca06eb14eecb/websockets-16.1.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:69159730a823dde3ea8d08783e8d47ef135a6d7e8d44eb127e32b321c9db8e3e", size = 186855, upload-time = "2026-07-17T22:49:58.467Z" }, + { url = "https://files.pythonhosted.org/packages/b0/35/dd875b3e050ff232d60fa377707f890e369f74d134f1be32e8f68879747c/websockets-16.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ed5bb271084b46530ee2ddc0410537a9961152c5ccba2fc98c5276d992ccba87", size = 187140, upload-time = "2026-07-17T22:50:00.016Z" }, + { url = "https://files.pythonhosted.org/packages/e8/dc/5cbfcb41824502f6af93b8f3943a4d06c67c23c7d2e31eb18748c4a5b2a7/websockets-16.1.1-cp313-cp313-win32.whl", hash = "sha256:cfb70b4eb56cac4da0a83588f3ad50d46beb0690391082f3d4e2d488c70b68ea", size = 179928, upload-time = "2026-07-17T22:50:01.685Z" }, + { url = "https://files.pythonhosted.org/packages/b0/c1/71e5deb5b7f8f226997ab64908c184ac3105c0155ce2d486f318e5dd08a8/websockets-16.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:d9531d9cbeac99af6f038fb1bc351403531f7d634a2c2e10e2f7c854c6ed5b68", size = 180242, upload-time = "2026-07-17T22:50:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/73/a2/ba78a164eeea4620df4a4df4bd2ed6017438c4655cc0f36f2c0bc0432355/websockets-16.1.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:443aefe96b7fdb132e2a70806cca1f2af49bb3f28e47abcd7c2e9dcf4d8fa1b8", size = 179635, upload-time = "2026-07-17T22:50:05.001Z" }, + { url = "https://files.pythonhosted.org/packages/b9/08/d26d7a7628cd4ac34cbbdb63ac80914ca842ed8e42938c40a53567806df3/websockets-16.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6456ff333092d509127d75a638cb411afae8ff17f092635015d1902efec8a293", size = 177320, upload-time = "2026-07-17T22:50:06.427Z" }, + { url = "https://files.pythonhosted.org/packages/0f/45/ebec83e6269536aa5932533c67b0af5c781f3e73fdbcd68672dcf43f4f44/websockets-16.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fce6c48559c86d1ac3632ccb1bebc7d5442fbe79bd9bb0e40379ee54be2a4051", size = 177544, upload-time = "2026-07-17T22:50:07.834Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d5/abc614d2297f6c1c3e01e61260364457a47c25cc1cf6a879038902bc6aa8/websockets-16.1.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:92b820d345f7a3fc7b8163949ee92df910f290c3fc517b3d5301c78065adafe1", size = 187270, upload-time = "2026-07-17T22:50:09.275Z" }, + { url = "https://files.pythonhosted.org/packages/52/71/4c99af3b87dff1b2927981f6876607d4acb45338c665242168d3982f7758/websockets-16.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a606d9c24035242a3e256e9d5b77ed9cd6bccfcb7cf993e5ca3c0f6f68fb6a7", size = 188509, upload-time = "2026-07-17T22:50:10.722Z" }, + { url = "https://files.pythonhosted.org/packages/9b/b4/5c8ca14b0df7eb84ed0524165c5359150210140817a3312aee57bf62a1cf/websockets-16.1.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:414e596c75f74e0994084694189d7dc9229fb278e33064d6784b73ffbba3ca31", size = 189882, upload-time = "2026-07-17T22:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/25/c1/bedfba9e70557129cb8083748d167bdcc01483dedf0f0df143676df05cbe/websockets-16.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:536676848fc5961aca9d20389951f59169508f765637a172403dc5434d722fa0", size = 189114, upload-time = "2026-07-17T22:50:13.789Z" }, + { url = "https://files.pythonhosted.org/packages/df/09/aa835b2787835aebd839114be5de51b797cb480b63ba42b26d34dfe147cb/websockets-16.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:97fd3a0e8b53efa41970ac1dff3d8cf0d2884cadeb4caaf95db7ad1526926ee3", size = 187861, upload-time = "2026-07-17T22:50:15.179Z" }, + { url = "https://files.pythonhosted.org/packages/20/26/f6408330694dbc9830857d9d23bc14ac4f6875127a480cfdda8d5ca21198/websockets-16.1.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7b1b19636af86a3c7995d4d028dbe376f39b4bf31541146f9c123582a6c94562", size = 185286, upload-time = "2026-07-17T22:50:16.741Z" }, + { url = "https://files.pythonhosted.org/packages/17/9a/e0675e70dd8a80762cf35bb18799d3f290a4890ffe6439bc51d222796083/websockets-16.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:41c8e77f17294c0ac18008a7309b99b34ee72247ef10b6dff4c3f8b5ac29896b", size = 187935, upload-time = "2026-07-17T22:50:18.213Z" }, + { url = "https://files.pythonhosted.org/packages/33/c1/3234cfb86afde01b81e9bddcc6e534c440975d60a13991259e833069ab3e/websockets-16.1.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:9f63bcef7f4b02b06b35fc01c93b96c43b5e88e1e8868676caacf493d5a31f3a", size = 186444, upload-time = "2026-07-17T22:50:19.67Z" }, + { url = "https://files.pythonhosted.org/packages/89/87/9c15206e1d778923d8daa9657de07aa62ea815e13448319c98458c37b281/websockets-16.1.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dab9eb87869da2d6ed3af3f3adf28414baae6ec9d4df355ffc18889132f3436c", size = 188409, upload-time = "2026-07-17T22:50:21.28Z" }, + { url = "https://files.pythonhosted.org/packages/f2/00/cf5de5c67676de2d3eef8b2a518f168f6796595447a5b7161ba0d012915c/websockets-16.1.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:43e3a9fdd7cbf7ba6040c31fae0faf84ca1474fef777c4e37912f1540f854499", size = 185958, upload-time = "2026-07-17T22:50:22.719Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/731b6ddede2e4136912ec4cff2cffbda35af73546be4762c3d7bd3bd79af/websockets-16.1.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:056ae37939ed7e9974f364f5864e76e49182622d8f9751ac1903c0d09b013985", size = 186911, upload-time = "2026-07-17T22:50:24.108Z" }, + { url = "https://files.pythonhosted.org/packages/8c/7f/39c634472c4469a24a7c09cecddffb08fac6d0e74f73881a94ee8a40a196/websockets-16.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0eadbbf2c30f01efa58e1f110eb6fa293261f6b0b1aa38f7f48707107690af9", size = 187204, upload-time = "2026-07-17T22:50:25.548Z" }, + { url = "https://files.pythonhosted.org/packages/26/89/9667c256c256dafcc62d21328ce7a40067da857969b68ee9af375b0aaf72/websockets-16.1.1-cp314-cp314-win32.whl", hash = "sha256:195c978b065fa40910582464f99d6b15c8b314c68e0546549a55ed83f4735328", size = 179603, upload-time = "2026-07-17T22:50:27.086Z" }, + { url = "https://files.pythonhosted.org/packages/bd/dd/1c099d6c0fc5deb6b46ccdbb6981fdb4b12c917869cb3952408409dc18db/websockets-16.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:4e8d01cc3bcae7bbf8167f944aeafefed590fae5693552bba9794a9df68371cc", size = 179948, upload-time = "2026-07-17T22:50:28.521Z" }, + { url = "https://files.pythonhosted.org/packages/35/25/9956b2d5e0529d5d23924f21bba1440d4c5c88a562e4f08550871ffa97a7/websockets-16.1.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0ffd3031ea8bda8d61762e84220186105ba3b748b3c8da2ae4f7816fac03e573", size = 179963, upload-time = "2026-07-17T22:50:29.982Z" }, + { url = "https://files.pythonhosted.org/packages/17/06/55ffc976c488b6aee9ea05761ff7c4e88e7c1fd82818c8ca7b556ad2f90c/websockets-16.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:84a2cef8deffbd9ab8ee0ea546a2a6a7030c28f44e6cdd4547dbfeb489eb8999", size = 177497, upload-time = "2026-07-17T22:50:31.396Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/f7dac2e980bacc92bdc26cebae4ae4d50cae5380732c50980598fc0bbae4/websockets-16.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3df13f73af9b3b38ab1195eb299ecb67a4330c911c97ae04043ff74085728abe", size = 177698, upload-time = "2026-07-17T22:50:32.829Z" }, + { url = "https://files.pythonhosted.org/packages/b2/39/26762f734113e22da2b942c3aca85798e0c0405d64c256549540ff31e5a1/websockets-16.1.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:23253dd5bcae3f9aaee0a1d30967a8dbd52e5d3cff93a2e5b84df57b77d4750d", size = 187561, upload-time = "2026-07-17T22:50:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/11/94/c3f330851806b9b02138b774d593478323e73c99238681b4b93efe64e02d/websockets-16.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c1c5705e314449e3308872fe084b8571ce078ee4fc55a98a769bdefe5917392", size = 188732, upload-time = "2026-07-17T22:50:36.088Z" }, + { url = "https://files.pythonhosted.org/packages/d1/f2/eb2c450f052de334ae33cf200ece6e87b0e14d186807074e4eb1cd2cdea2/websockets-16.1.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69e52d175a0a7d1e13b4b67ad41c560b7d98e8c6f6126eb0bda496c784faf8c7", size = 190872, upload-time = "2026-07-17T22:50:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/70/31/2ac8cecf3a74f7fed9132129fc3d90b3998a1554570c11a69b2a8c20332d/websockets-16.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1f79c89b5eb034d1722938a891916582f8f7f503f58ca22518a63c3f2cd18499", size = 189305, upload-time = "2026-07-17T22:50:39.53Z" }, + { url = "https://files.pythonhosted.org/packages/6a/cf/8ab19650d3c0d4562c92e70ab47c257c4aa5c6a713ed87fe63766b31fefc/websockets-16.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:39f2a024af5c345ffe8fcf1ee18c049c024c94df393bb09b044a6917c77bde43", size = 188033, upload-time = "2026-07-17T22:50:40.912Z" }, + { url = "https://files.pythonhosted.org/packages/66/d7/a49a38a6127a4acb134fb1912b215d900cc657605cff32445bf519f3acc4/websockets-16.1.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:952303a7318d4cbe1011400839bb2051c9f84fa0a35923267f5daba34b15d458", size = 185748, upload-time = "2026-07-17T22:50:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/95/3e/ad1fa40388c7f2e0bb2c7930d0090b6c5498594bd1cdaec18864df3d9e97/websockets-16.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:249116b4a76063d930a46391ad56e135c286e4562a18309029fc2c73f4ed4c62", size = 188285, upload-time = "2026-07-17T22:50:43.974Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/d5db28ca264b9104f82196f92dc8843e35fd391f763d42e4ad358f5bc97e/websockets-16.1.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:61922544a0587a13fd3f53e4c0e5e606510c7b0d9d22c8444e5fae22a06b38cb", size = 186777, upload-time = "2026-07-17T22:50:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/42/9c/726cb39d0cc43ae848dce4aa2acb04eecc6738b1264ec6d700bf6bcfb9f8/websockets-16.1.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:46dcaa042cd1de6c59e7d9269fa63ff7572b6df40510600b678f0826b3c7af51", size = 188682, upload-time = "2026-07-17T22:50:46.973Z" }, + { url = "https://files.pythonhosted.org/packages/be/c7/1168704de8c2dd483edabe4a22cbe4465dd8be8dd95561d214f9fe092871/websockets-16.1.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:38565aca3e01ea8734e578fb2118dade0ecb0250533f29e22b8d1a7a196cf4d0", size = 186377, upload-time = "2026-07-17T22:50:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/ca/40/f9ff2d630ffce4e7dfea0b2288e1caf9ebbf9ff8a9ec9396136ce8b94935/websockets-16.1.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:42f599f4d48c7e1a3338fdaac3acd075be3b3cf02d4b274f3bf2767aedd3d217", size = 187148, upload-time = "2026-07-17T22:50:49.845Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/e177c8299f78d7cbe2d14df228643c10c70c0e86e108e092056bbcc16e46/websockets-16.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:dcc04fedf83effaeb9cce98abc9469bb1b42ef85f03e01c8c1f4438ef7555737", size = 187578, upload-time = "2026-07-17T22:50:51.619Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/b6987faf330f5af5c787a2610124c2e8403d51724f9001ec4fff6311fe7a/websockets-16.1.1-cp314-cp314t-win32.whl", hash = "sha256:8483c2096363120eea8b07c06ae7304d520f686665fffd4811fad423930a65d7", size = 179729, upload-time = "2026-07-17T22:50:53.269Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6e/fbac6ed878dd362fbad7d415fa4f84d38e3e33fed8cde45c64e783acf826/websockets-16.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:bcce07e23e5769375158f5efdcdafa8d5cd014b93c6683865b840ed65b96f231", size = 180072, upload-time = "2026-07-17T22:50:54.969Z" }, + { url = "https://files.pythonhosted.org/packages/e1/ed/71fea6e141590cafc40b14dc5943b0845606bee87bdb52a21b6a73eb4311/websockets-16.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:820fb8450edddae3812fd58cbc08e2bf22812cb248ecb5f06dbb82119a56e869", size = 177185, upload-time = "2026-07-17T22:50:56.665Z" }, + { url = "https://files.pythonhosted.org/packages/01/ec/00e7eeca200facf9266a83e4cbbf1bed0e67fba1d4d45031d3e5b3d81b5c/websockets-16.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:125f22dbefaf1554fea66fc83851490edb284ce4f501d37ffed2752f418332d9", size = 177459, upload-time = "2026-07-17T22:50:58.197Z" }, + { url = "https://files.pythonhosted.org/packages/75/fd/5774c4b33f7c0d8f0c51809c8b3a93456c48e3543579262cfa64eb5f522e/websockets-16.1.1-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:30bbe120437b5648a77d3519b7024ea09530e0b5b18d3698c5a0ae536fe0cc2e", size = 178294, upload-time = "2026-07-17T22:50:59.641Z" }, + { url = "https://files.pythonhosted.org/packages/37/c3/48e2c03d2bd79bb45948841c592d24156312dd5f58cdf8f549febe652fb6/websockets-16.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6b9dadbef0cccd9f4c4ee96b08898afa73e26803bbe0f6aeb5bb12b0074206d", size = 179190, upload-time = "2026-07-17T22:51:01.129Z" }, + { url = "https://files.pythonhosted.org/packages/2d/3f/73e511ecf2496ceac57dd4ed8388efe2bcf0769338a2dbf242c8366ae87e/websockets-16.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56cd5fc4f10a9ea8aa0804bddb7b42506cf9e136046f3b4c27de8fec9e2ecba5", size = 180330, upload-time = "2026-07-17T22:51:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/be/4d/2d0d67834092e354d2b0498f014a41249a89556bc406cf86f3e1557bb463/websockets-16.1.1-py3-none-any.whl", hash = "sha256:6abbd3e82c731c8e531714466acd5d87b5e88ac3243465337ba71d68e23ae7e3", size = 173814, upload-time = "2026-07-17T22:51:04.184Z" }, ] [[package]] name = "xxhash" -version = "3.6.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/84/30869e01909fb37a6cc7e18688ee8bf1e42d57e7e0777636bd47524c43c7/xxhash-3.6.0.tar.gz", hash = "sha256:f0162a78b13a0d7617b2845b90c763339d1f1d82bb04a4b07f4ab535cc5e05d6", size = 85160, upload-time = "2025-10-02T14:37:08.097Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/17/d4/cc2f0400e9154df4b9964249da78ebd72f318e35ccc425e9f403c392f22a/xxhash-3.6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b47bbd8cf2d72797f3c2772eaaac0ded3d3af26481a26d7d7d41dc2d3c46b04a", size = 32844, upload-time = "2025-10-02T14:34:14.037Z" }, - { url = "https://files.pythonhosted.org/packages/5e/ec/1cc11cd13e26ea8bc3cb4af4eaadd8d46d5014aebb67be3f71fb0b68802a/xxhash-3.6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2b6821e94346f96db75abaa6e255706fb06ebd530899ed76d32cd99f20dc52fa", size = 30809, upload-time = "2025-10-02T14:34:15.484Z" }, - { url = "https://files.pythonhosted.org/packages/04/5f/19fe357ea348d98ca22f456f75a30ac0916b51c753e1f8b2e0e6fb884cce/xxhash-3.6.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d0a9751f71a1a65ce3584e9cae4467651c7e70c9d31017fa57574583a4540248", size = 194665, upload-time = "2025-10-02T14:34:16.541Z" }, - { url = "https://files.pythonhosted.org/packages/90/3b/d1f1a8f5442a5fd8beedae110c5af7604dc37349a8e16519c13c19a9a2de/xxhash-3.6.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b29ee68625ab37b04c0b40c3fafdf24d2f75ccd778333cfb698f65f6c463f62", size = 213550, upload-time = "2025-10-02T14:34:17.878Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ef/3a9b05eb527457d5db13a135a2ae1a26c80fecd624d20f3e8dcc4cb170f3/xxhash-3.6.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6812c25fe0d6c36a46ccb002f40f27ac903bf18af9f6dd8f9669cb4d176ab18f", size = 212384, upload-time = "2025-10-02T14:34:19.182Z" }, - { url = "https://files.pythonhosted.org/packages/0f/18/ccc194ee698c6c623acbf0f8c2969811a8a4b6185af5e824cd27b9e4fd3e/xxhash-3.6.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4ccbff013972390b51a18ef1255ef5ac125c92dc9143b2d1909f59abc765540e", size = 445749, upload-time = "2025-10-02T14:34:20.659Z" }, - { url = "https://files.pythonhosted.org/packages/a5/86/cf2c0321dc3940a7aa73076f4fd677a0fb3e405cb297ead7d864fd90847e/xxhash-3.6.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:297b7fbf86c82c550e12e8fb71968b3f033d27b874276ba3624ea868c11165a8", size = 193880, upload-time = "2025-10-02T14:34:22.431Z" }, - { url = "https://files.pythonhosted.org/packages/82/fb/96213c8560e6f948a1ecc9a7613f8032b19ee45f747f4fca4eb31bb6d6ed/xxhash-3.6.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dea26ae1eb293db089798d3973a5fc928a18fdd97cc8801226fae705b02b14b0", size = 210912, upload-time = "2025-10-02T14:34:23.937Z" }, - { url = "https://files.pythonhosted.org/packages/40/aa/4395e669b0606a096d6788f40dbdf2b819d6773aa290c19e6e83cbfc312f/xxhash-3.6.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7a0b169aafb98f4284f73635a8e93f0735f9cbde17bd5ec332480484241aaa77", size = 198654, upload-time = "2025-10-02T14:34:25.644Z" }, - { url = "https://files.pythonhosted.org/packages/67/74/b044fcd6b3d89e9b1b665924d85d3f400636c23590226feb1eb09e1176ce/xxhash-3.6.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:08d45aef063a4531b785cd72de4887766d01dc8f362a515693df349fdb825e0c", size = 210867, upload-time = "2025-10-02T14:34:27.203Z" }, - { url = "https://files.pythonhosted.org/packages/bc/fd/3ce73bf753b08cb19daee1eb14aa0d7fe331f8da9c02dd95316ddfe5275e/xxhash-3.6.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:929142361a48ee07f09121fe9e96a84950e8d4df3bb298ca5d88061969f34d7b", size = 414012, upload-time = "2025-10-02T14:34:28.409Z" }, - { url = "https://files.pythonhosted.org/packages/ba/b3/5a4241309217c5c876f156b10778f3ab3af7ba7e3259e6d5f5c7d0129eb2/xxhash-3.6.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:51312c768403d8540487dbbfb557454cfc55589bbde6424456951f7fcd4facb3", size = 191409, upload-time = "2025-10-02T14:34:29.696Z" }, - { url = "https://files.pythonhosted.org/packages/c0/01/99bfbc15fb9abb9a72b088c1d95219fc4782b7d01fc835bd5744d66dd0b8/xxhash-3.6.0-cp311-cp311-win32.whl", hash = "sha256:d1927a69feddc24c987b337ce81ac15c4720955b667fe9b588e02254b80446fd", size = 30574, upload-time = "2025-10-02T14:34:31.028Z" }, - { url = "https://files.pythonhosted.org/packages/65/79/9d24d7f53819fe301b231044ea362ce64e86c74f6e8c8e51320de248b3e5/xxhash-3.6.0-cp311-cp311-win_amd64.whl", hash = "sha256:26734cdc2d4ffe449b41d186bbeac416f704a482ed835d375a5c0cb02bc63fef", size = 31481, upload-time = "2025-10-02T14:34:32.062Z" }, - { url = "https://files.pythonhosted.org/packages/30/4e/15cd0e3e8772071344eab2961ce83f6e485111fed8beb491a3f1ce100270/xxhash-3.6.0-cp311-cp311-win_arm64.whl", hash = "sha256:d72f67ef8bf36e05f5b6c65e8524f265bd61071471cd4cf1d36743ebeeeb06b7", size = 27861, upload-time = "2025-10-02T14:34:33.555Z" }, - { url = "https://files.pythonhosted.org/packages/9a/07/d9412f3d7d462347e4511181dea65e47e0d0e16e26fbee2ea86a2aefb657/xxhash-3.6.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:01362c4331775398e7bb34e3ab403bc9ee9f7c497bc7dee6272114055277dd3c", size = 32744, upload-time = "2025-10-02T14:34:34.622Z" }, - { url = "https://files.pythonhosted.org/packages/79/35/0429ee11d035fc33abe32dca1b2b69e8c18d236547b9a9b72c1929189b9a/xxhash-3.6.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b7b2df81a23f8cb99656378e72501b2cb41b1827c0f5a86f87d6b06b69f9f204", size = 30816, upload-time = "2025-10-02T14:34:36.043Z" }, - { url = "https://files.pythonhosted.org/packages/b7/f2/57eb99aa0f7d98624c0932c5b9a170e1806406cdbcdb510546634a1359e0/xxhash-3.6.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:dc94790144e66b14f67b10ac8ed75b39ca47536bf8800eb7c24b50271ea0c490", size = 194035, upload-time = "2025-10-02T14:34:37.354Z" }, - { url = "https://files.pythonhosted.org/packages/4c/ed/6224ba353690d73af7a3f1c7cdb1fc1b002e38f783cb991ae338e1eb3d79/xxhash-3.6.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93f107c673bccf0d592cdba077dedaf52fe7f42dcd7676eba1f6d6f0c3efffd2", size = 212914, upload-time = "2025-10-02T14:34:38.6Z" }, - { url = "https://files.pythonhosted.org/packages/38/86/fb6b6130d8dd6b8942cc17ab4d90e223653a89aa32ad2776f8af7064ed13/xxhash-3.6.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aa5ee3444c25b69813663c9f8067dcfaa2e126dc55e8dddf40f4d1c25d7effa", size = 212163, upload-time = "2025-10-02T14:34:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/ee/dc/e84875682b0593e884ad73b2d40767b5790d417bde603cceb6878901d647/xxhash-3.6.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7f99123f0e1194fa59cc69ad46dbae2e07becec5df50a0509a808f90a0f03f0", size = 445411, upload-time = "2025-10-02T14:34:41.569Z" }, - { url = "https://files.pythonhosted.org/packages/11/4f/426f91b96701ec2f37bb2b8cec664eff4f658a11f3fa9d94f0a887ea6d2b/xxhash-3.6.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:49e03e6fe2cac4a1bc64952dd250cf0dbc5ef4ebb7b8d96bce82e2de163c82a2", size = 193883, upload-time = "2025-10-02T14:34:43.249Z" }, - { url = "https://files.pythonhosted.org/packages/53/5a/ddbb83eee8e28b778eacfc5a85c969673e4023cdeedcfcef61f36731610b/xxhash-3.6.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bd17fede52a17a4f9a7bc4472a5867cb0b160deeb431795c0e4abe158bc784e9", size = 210392, upload-time = "2025-10-02T14:34:45.042Z" }, - { url = "https://files.pythonhosted.org/packages/1e/c2/ff69efd07c8c074ccdf0a4f36fcdd3d27363665bcdf4ba399abebe643465/xxhash-3.6.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6fb5f5476bef678f69db04f2bd1efbed3030d2aba305b0fc1773645f187d6a4e", size = 197898, upload-time = "2025-10-02T14:34:46.302Z" }, - { url = "https://files.pythonhosted.org/packages/58/ca/faa05ac19b3b622c7c9317ac3e23954187516298a091eb02c976d0d3dd45/xxhash-3.6.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:843b52f6d88071f87eba1631b684fcb4b2068cd2180a0224122fe4ef011a9374", size = 210655, upload-time = "2025-10-02T14:34:47.571Z" }, - { url = "https://files.pythonhosted.org/packages/d4/7a/06aa7482345480cc0cb597f5c875b11a82c3953f534394f620b0be2f700c/xxhash-3.6.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7d14a6cfaf03b1b6f5f9790f76880601ccc7896aff7ab9cd8978a939c1eb7e0d", size = 414001, upload-time = "2025-10-02T14:34:49.273Z" }, - { url = "https://files.pythonhosted.org/packages/23/07/63ffb386cd47029aa2916b3d2f454e6cc5b9f5c5ada3790377d5430084e7/xxhash-3.6.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:418daf3db71e1413cfe211c2f9a528456936645c17f46b5204705581a45390ae", size = 191431, upload-time = "2025-10-02T14:34:50.798Z" }, - { url = "https://files.pythonhosted.org/packages/0f/93/14fde614cadb4ddf5e7cebf8918b7e8fac5ae7861c1875964f17e678205c/xxhash-3.6.0-cp312-cp312-win32.whl", hash = "sha256:50fc255f39428a27299c20e280d6193d8b63b8ef8028995323bf834a026b4fbb", size = 30617, upload-time = "2025-10-02T14:34:51.954Z" }, - { url = "https://files.pythonhosted.org/packages/13/5d/0d125536cbe7565a83d06e43783389ecae0c0f2ed037b48ede185de477c0/xxhash-3.6.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0f2ab8c715630565ab8991b536ecded9416d615538be8ecddce43ccf26cbc7c", size = 31534, upload-time = "2025-10-02T14:34:53.276Z" }, - { url = "https://files.pythonhosted.org/packages/54/85/6ec269b0952ec7e36ba019125982cf11d91256a778c7c3f98a4c5043d283/xxhash-3.6.0-cp312-cp312-win_arm64.whl", hash = "sha256:eae5c13f3bc455a3bbb68bdc513912dc7356de7e2280363ea235f71f54064829", size = 27876, upload-time = "2025-10-02T14:34:54.371Z" }, - { url = "https://files.pythonhosted.org/packages/33/76/35d05267ac82f53ae9b0e554da7c5e281ee61f3cad44c743f0fcd354f211/xxhash-3.6.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:599e64ba7f67472481ceb6ee80fa3bd828fd61ba59fb11475572cc5ee52b89ec", size = 32738, upload-time = "2025-10-02T14:34:55.839Z" }, - { url = "https://files.pythonhosted.org/packages/31/a8/3fbce1cd96534a95e35d5120637bf29b0d7f5d8fa2f6374e31b4156dd419/xxhash-3.6.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d8b8aaa30fca4f16f0c84a5c8d7ddee0e25250ec2796c973775373257dde8f1", size = 30821, upload-time = "2025-10-02T14:34:57.219Z" }, - { url = "https://files.pythonhosted.org/packages/0c/ea/d387530ca7ecfa183cb358027f1833297c6ac6098223fd14f9782cd0015c/xxhash-3.6.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d597acf8506d6e7101a4a44a5e428977a51c0fadbbfd3c39650cca9253f6e5a6", size = 194127, upload-time = "2025-10-02T14:34:59.21Z" }, - { url = "https://files.pythonhosted.org/packages/ba/0c/71435dcb99874b09a43b8d7c54071e600a7481e42b3e3ce1eb5226a5711a/xxhash-3.6.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:858dc935963a33bc33490128edc1c12b0c14d9c7ebaa4e387a7869ecc4f3e263", size = 212975, upload-time = "2025-10-02T14:35:00.816Z" }, - { url = "https://files.pythonhosted.org/packages/84/7a/c2b3d071e4bb4a90b7057228a99b10d51744878f4a8a6dd643c8bd897620/xxhash-3.6.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba284920194615cb8edf73bf52236ce2e1664ccd4a38fdb543506413529cc546", size = 212241, upload-time = "2025-10-02T14:35:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/81/5f/640b6eac0128e215f177df99eadcd0f1b7c42c274ab6a394a05059694c5a/xxhash-3.6.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b54219177f6c6674d5378bd862c6aedf64725f70dd29c472eaae154df1a2e89", size = 445471, upload-time = "2025-10-02T14:35:03.61Z" }, - { url = "https://files.pythonhosted.org/packages/5e/1e/3c3d3ef071b051cc3abbe3721ffb8365033a172613c04af2da89d5548a87/xxhash-3.6.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:42c36dd7dbad2f5238950c377fcbf6811b1cdb1c444fab447960030cea60504d", size = 193936, upload-time = "2025-10-02T14:35:05.013Z" }, - { url = "https://files.pythonhosted.org/packages/2c/bd/4a5f68381939219abfe1c22a9e3a5854a4f6f6f3c4983a87d255f21f2e5d/xxhash-3.6.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f22927652cba98c44639ffdc7aaf35828dccf679b10b31c4ad72a5b530a18eb7", size = 210440, upload-time = "2025-10-02T14:35:06.239Z" }, - { url = "https://files.pythonhosted.org/packages/eb/37/b80fe3d5cfb9faff01a02121a0f4d565eb7237e9e5fc66e73017e74dcd36/xxhash-3.6.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b45fad44d9c5c119e9c6fbf2e1c656a46dc68e280275007bbfd3d572b21426db", size = 197990, upload-time = "2025-10-02T14:35:07.735Z" }, - { url = "https://files.pythonhosted.org/packages/d7/fd/2c0a00c97b9e18f72e1f240ad4e8f8a90fd9d408289ba9c7c495ed7dc05c/xxhash-3.6.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6f2580ffab1a8b68ef2b901cde7e55fa8da5e4be0977c68f78fc80f3c143de42", size = 210689, upload-time = "2025-10-02T14:35:09.438Z" }, - { url = "https://files.pythonhosted.org/packages/93/86/5dd8076a926b9a95db3206aba20d89a7fc14dd5aac16e5c4de4b56033140/xxhash-3.6.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40c391dd3cd041ebc3ffe6f2c862f402e306eb571422e0aa918d8070ba31da11", size = 414068, upload-time = "2025-10-02T14:35:11.162Z" }, - { url = "https://files.pythonhosted.org/packages/af/3c/0bb129170ee8f3650f08e993baee550a09593462a5cddd8e44d0011102b1/xxhash-3.6.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f205badabde7aafd1a31e8ca2a3e5a763107a71c397c4481d6a804eb5063d8bd", size = 191495, upload-time = "2025-10-02T14:35:12.971Z" }, - { url = "https://files.pythonhosted.org/packages/e9/3a/6797e0114c21d1725e2577508e24006fd7ff1d8c0c502d3b52e45c1771d8/xxhash-3.6.0-cp313-cp313-win32.whl", hash = "sha256:2577b276e060b73b73a53042ea5bd5203d3e6347ce0d09f98500f418a9fcf799", size = 30620, upload-time = "2025-10-02T14:35:14.129Z" }, - { url = "https://files.pythonhosted.org/packages/86/15/9bc32671e9a38b413a76d24722a2bf8784a132c043063a8f5152d390b0f9/xxhash-3.6.0-cp313-cp313-win_amd64.whl", hash = "sha256:757320d45d2fbcce8f30c42a6b2f47862967aea7bf458b9625b4bbe7ee390392", size = 31542, upload-time = "2025-10-02T14:35:15.21Z" }, - { url = "https://files.pythonhosted.org/packages/39/c5/cc01e4f6188656e56112d6a8e0dfe298a16934b8c47a247236549a3f7695/xxhash-3.6.0-cp313-cp313-win_arm64.whl", hash = "sha256:457b8f85dec5825eed7b69c11ae86834a018b8e3df5e77783c999663da2f96d6", size = 27880, upload-time = "2025-10-02T14:35:16.315Z" }, - { url = "https://files.pythonhosted.org/packages/f3/30/25e5321c8732759e930c555176d37e24ab84365482d257c3b16362235212/xxhash-3.6.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a42e633d75cdad6d625434e3468126c73f13f7584545a9cf34e883aa1710e702", size = 32956, upload-time = "2025-10-02T14:35:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/9f/3c/0573299560d7d9f8ab1838f1efc021a280b5ae5ae2e849034ef3dee18810/xxhash-3.6.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:568a6d743219e717b07b4e03b0a828ce593833e498c3b64752e0f5df6bfe84db", size = 31072, upload-time = "2025-10-02T14:35:18.844Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1c/52d83a06e417cd9d4137722693424885cc9878249beb3a7c829e74bf7ce9/xxhash-3.6.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bec91b562d8012dae276af8025a55811b875baace6af510412a5e58e3121bc54", size = 196409, upload-time = "2025-10-02T14:35:20.31Z" }, - { url = "https://files.pythonhosted.org/packages/e3/8e/c6d158d12a79bbd0b878f8355432075fc82759e356ab5a111463422a239b/xxhash-3.6.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78e7f2f4c521c30ad5e786fdd6bae89d47a32672a80195467b5de0480aa97b1f", size = 215736, upload-time = "2025-10-02T14:35:21.616Z" }, - { url = "https://files.pythonhosted.org/packages/bc/68/c4c80614716345d55071a396cf03d06e34b5f4917a467faf43083c995155/xxhash-3.6.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3ed0df1b11a79856df5ffcab572cbd6b9627034c1c748c5566fa79df9048a7c5", size = 214833, upload-time = "2025-10-02T14:35:23.32Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e9/ae27c8ffec8b953efa84c7c4a6c6802c263d587b9fc0d6e7cea64e08c3af/xxhash-3.6.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0e4edbfc7d420925b0dd5e792478ed393d6e75ff8fc219a6546fb446b6a417b1", size = 448348, upload-time = "2025-10-02T14:35:25.111Z" }, - { url = "https://files.pythonhosted.org/packages/d7/6b/33e21afb1b5b3f46b74b6bd1913639066af218d704cc0941404ca717fc57/xxhash-3.6.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fba27a198363a7ef87f8c0f6b171ec36b674fe9053742c58dd7e3201c1ab30ee", size = 196070, upload-time = "2025-10-02T14:35:26.586Z" }, - { url = "https://files.pythonhosted.org/packages/96/b6/fcabd337bc5fa624e7203aa0fa7d0c49eed22f72e93229431752bddc83d9/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:794fe9145fe60191c6532fa95063765529770edcdd67b3d537793e8004cabbfd", size = 212907, upload-time = "2025-10-02T14:35:28.087Z" }, - { url = "https://files.pythonhosted.org/packages/4b/d3/9ee6160e644d660fcf176c5825e61411c7f62648728f69c79ba237250143/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:6105ef7e62b5ac73a837778efc331a591d8442f8ef5c7e102376506cb4ae2729", size = 200839, upload-time = "2025-10-02T14:35:29.857Z" }, - { url = "https://files.pythonhosted.org/packages/0d/98/e8de5baa5109394baf5118f5e72ab21a86387c4f89b0e77ef3e2f6b0327b/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f01375c0e55395b814a679b3eea205db7919ac2af213f4a6682e01220e5fe292", size = 213304, upload-time = "2025-10-02T14:35:31.222Z" }, - { url = "https://files.pythonhosted.org/packages/7b/1d/71056535dec5c3177eeb53e38e3d367dd1d16e024e63b1cee208d572a033/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d706dca2d24d834a4661619dcacf51a75c16d65985718d6a7d73c1eeeb903ddf", size = 416930, upload-time = "2025-10-02T14:35:32.517Z" }, - { url = "https://files.pythonhosted.org/packages/dc/6c/5cbde9de2cd967c322e651c65c543700b19e7ae3e0aae8ece3469bf9683d/xxhash-3.6.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5f059d9faeacd49c0215d66f4056e1326c80503f51a1532ca336a385edadd033", size = 193787, upload-time = "2025-10-02T14:35:33.827Z" }, - { url = "https://files.pythonhosted.org/packages/19/fa/0172e350361d61febcea941b0cc541d6e6c8d65d153e85f850a7b256ff8a/xxhash-3.6.0-cp313-cp313t-win32.whl", hash = "sha256:1244460adc3a9be84731d72b8e80625788e5815b68da3da8b83f78115a40a7ec", size = 30916, upload-time = "2025-10-02T14:35:35.107Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e6/e8cf858a2b19d6d45820f072eff1bea413910592ff17157cabc5f1227a16/xxhash-3.6.0-cp313-cp313t-win_amd64.whl", hash = "sha256:b1e420ef35c503869c4064f4a2f2b08ad6431ab7b229a05cce39d74268bca6b8", size = 31799, upload-time = "2025-10-02T14:35:36.165Z" }, - { url = "https://files.pythonhosted.org/packages/56/15/064b197e855bfb7b343210e82490ae672f8bc7cdf3ddb02e92f64304ee8a/xxhash-3.6.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ec44b73a4220623235f67a996c862049f375df3b1052d9899f40a6382c32d746", size = 28044, upload-time = "2025-10-02T14:35:37.195Z" }, - { url = "https://files.pythonhosted.org/packages/7e/5e/0138bc4484ea9b897864d59fce9be9086030825bc778b76cb5a33a906d37/xxhash-3.6.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:a40a3d35b204b7cc7643cbcf8c9976d818cb47befcfac8bbefec8038ac363f3e", size = 32754, upload-time = "2025-10-02T14:35:38.245Z" }, - { url = "https://files.pythonhosted.org/packages/18/d7/5dac2eb2ec75fd771957a13e5dda560efb2176d5203f39502a5fc571f899/xxhash-3.6.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a54844be970d3fc22630b32d515e79a90d0a3ddb2644d8d7402e3c4c8da61405", size = 30846, upload-time = "2025-10-02T14:35:39.6Z" }, - { url = "https://files.pythonhosted.org/packages/fe/71/8bc5be2bb00deb5682e92e8da955ebe5fa982da13a69da5a40a4c8db12fb/xxhash-3.6.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:016e9190af8f0a4e3741343777710e3d5717427f175adfdc3e72508f59e2a7f3", size = 194343, upload-time = "2025-10-02T14:35:40.69Z" }, - { url = "https://files.pythonhosted.org/packages/e7/3b/52badfb2aecec2c377ddf1ae75f55db3ba2d321c5e164f14461c90837ef3/xxhash-3.6.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4f6f72232f849eb9d0141e2ebe2677ece15adfd0fa599bc058aad83c714bb2c6", size = 213074, upload-time = "2025-10-02T14:35:42.29Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/ae46b4e9b92e537fa30d03dbc19cdae57ed407e9c26d163895e968e3de85/xxhash-3.6.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:63275a8aba7865e44b1813d2177e0f5ea7eadad3dd063a21f7cf9afdc7054063", size = 212388, upload-time = "2025-10-02T14:35:43.929Z" }, - { url = "https://files.pythonhosted.org/packages/f5/80/49f88d3afc724b4ac7fbd664c8452d6db51b49915be48c6982659e0e7942/xxhash-3.6.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cd01fa2aa00d8b017c97eb46b9a794fbdca53fc14f845f5a328c71254b0abb7", size = 445614, upload-time = "2025-10-02T14:35:45.216Z" }, - { url = "https://files.pythonhosted.org/packages/ed/ba/603ce3961e339413543d8cd44f21f2c80e2a7c5cfe692a7b1f2cccf58f3c/xxhash-3.6.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0226aa89035b62b6a86d3c68df4d7c1f47a342b8683da2b60cedcddb46c4d95b", size = 194024, upload-time = "2025-10-02T14:35:46.959Z" }, - { url = "https://files.pythonhosted.org/packages/78/d1/8e225ff7113bf81545cfdcd79eef124a7b7064a0bba53605ff39590b95c2/xxhash-3.6.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6e193e9f56e4ca4923c61238cdaced324f0feac782544eb4c6d55ad5cc99ddd", size = 210541, upload-time = "2025-10-02T14:35:48.301Z" }, - { url = "https://files.pythonhosted.org/packages/6f/58/0f89d149f0bad89def1a8dd38feb50ccdeb643d9797ec84707091d4cb494/xxhash-3.6.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9176dcaddf4ca963d4deb93866d739a343c01c969231dbe21680e13a5d1a5bf0", size = 198305, upload-time = "2025-10-02T14:35:49.584Z" }, - { url = "https://files.pythonhosted.org/packages/11/38/5eab81580703c4df93feb5f32ff8fa7fe1e2c51c1f183ee4e48d4bb9d3d7/xxhash-3.6.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c1ce4009c97a752e682b897aa99aef84191077a9433eb237774689f14f8ec152", size = 210848, upload-time = "2025-10-02T14:35:50.877Z" }, - { url = "https://files.pythonhosted.org/packages/5e/6b/953dc4b05c3ce678abca756416e4c130d2382f877a9c30a20d08ee6a77c0/xxhash-3.6.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:8cb2f4f679b01513b7adbb9b1b2f0f9cdc31b70007eaf9d59d0878809f385b11", size = 414142, upload-time = "2025-10-02T14:35:52.15Z" }, - { url = "https://files.pythonhosted.org/packages/08/a9/238ec0d4e81a10eb5026d4a6972677cbc898ba6c8b9dbaec12ae001b1b35/xxhash-3.6.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:653a91d7c2ab54a92c19ccf43508b6a555440b9be1bc8be553376778be7f20b5", size = 191547, upload-time = "2025-10-02T14:35:53.547Z" }, - { url = "https://files.pythonhosted.org/packages/f1/ee/3cf8589e06c2164ac77c3bf0aa127012801128f1feebf2a079272da5737c/xxhash-3.6.0-cp314-cp314-win32.whl", hash = "sha256:a756fe893389483ee8c394d06b5ab765d96e68fbbfe6fde7aa17e11f5720559f", size = 31214, upload-time = "2025-10-02T14:35:54.746Z" }, - { url = "https://files.pythonhosted.org/packages/02/5d/a19552fbc6ad4cb54ff953c3908bbc095f4a921bc569433d791f755186f1/xxhash-3.6.0-cp314-cp314-win_amd64.whl", hash = "sha256:39be8e4e142550ef69629c9cd71b88c90e9a5db703fecbcf265546d9536ca4ad", size = 32290, upload-time = "2025-10-02T14:35:55.791Z" }, - { url = "https://files.pythonhosted.org/packages/b1/11/dafa0643bc30442c887b55baf8e73353a344ee89c1901b5a5c54a6c17d39/xxhash-3.6.0-cp314-cp314-win_arm64.whl", hash = "sha256:25915e6000338999236f1eb68a02a32c3275ac338628a7eaa5a269c401995679", size = 28795, upload-time = "2025-10-02T14:35:57.162Z" }, - { url = "https://files.pythonhosted.org/packages/2c/db/0e99732ed7f64182aef4a6fb145e1a295558deec2a746265dcdec12d191e/xxhash-3.6.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c5294f596a9017ca5a3e3f8884c00b91ab2ad2933cf288f4923c3fd4346cf3d4", size = 32955, upload-time = "2025-10-02T14:35:58.267Z" }, - { url = "https://files.pythonhosted.org/packages/55/f4/2a7c3c68e564a099becfa44bb3d398810cc0ff6749b0d3cb8ccb93f23c14/xxhash-3.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1cf9dcc4ab9cff01dfbba78544297a3a01dafd60f3bde4e2bfd016cf7e4ddc67", size = 31072, upload-time = "2025-10-02T14:35:59.382Z" }, - { url = "https://files.pythonhosted.org/packages/c6/d9/72a29cddc7250e8a5819dad5d466facb5dc4c802ce120645630149127e73/xxhash-3.6.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01262da8798422d0685f7cef03b2bd3f4f46511b02830861df548d7def4402ad", size = 196579, upload-time = "2025-10-02T14:36:00.838Z" }, - { url = "https://files.pythonhosted.org/packages/63/93/b21590e1e381040e2ca305a884d89e1c345b347404f7780f07f2cdd47ef4/xxhash-3.6.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51a73fb7cb3a3ead9f7a8b583ffd9b8038e277cdb8cb87cf890e88b3456afa0b", size = 215854, upload-time = "2025-10-02T14:36:02.207Z" }, - { url = "https://files.pythonhosted.org/packages/ce/b8/edab8a7d4fa14e924b29be877d54155dcbd8b80be85ea00d2be3413a9ed4/xxhash-3.6.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b9c6df83594f7df8f7f708ce5ebeacfc69f72c9fbaaababf6cf4758eaada0c9b", size = 214965, upload-time = "2025-10-02T14:36:03.507Z" }, - { url = "https://files.pythonhosted.org/packages/27/67/dfa980ac7f0d509d54ea0d5a486d2bb4b80c3f1bb22b66e6a05d3efaf6c0/xxhash-3.6.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:627f0af069b0ea56f312fd5189001c24578868643203bca1abbc2c52d3a6f3ca", size = 448484, upload-time = "2025-10-02T14:36:04.828Z" }, - { url = "https://files.pythonhosted.org/packages/8c/63/8ffc2cc97e811c0ca5d00ab36604b3ea6f4254f20b7bc658ca825ce6c954/xxhash-3.6.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa912c62f842dfd013c5f21a642c9c10cd9f4c4e943e0af83618b4a404d9091a", size = 196162, upload-time = "2025-10-02T14:36:06.182Z" }, - { url = "https://files.pythonhosted.org/packages/4b/77/07f0e7a3edd11a6097e990f6e5b815b6592459cb16dae990d967693e6ea9/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b465afd7909db30168ab62afe40b2fcf79eedc0b89a6c0ab3123515dc0df8b99", size = 213007, upload-time = "2025-10-02T14:36:07.733Z" }, - { url = "https://files.pythonhosted.org/packages/ae/d8/bc5fa0d152837117eb0bef6f83f956c509332ce133c91c63ce07ee7c4873/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:a881851cf38b0a70e7c4d3ce81fc7afd86fbc2a024f4cfb2a97cf49ce04b75d3", size = 200956, upload-time = "2025-10-02T14:36:09.106Z" }, - { url = "https://files.pythonhosted.org/packages/26/a5/d749334130de9411783873e9b98ecc46688dad5db64ca6e04b02acc8b473/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9b3222c686a919a0f3253cfc12bb118b8b103506612253b5baeaac10d8027cf6", size = 213401, upload-time = "2025-10-02T14:36:10.585Z" }, - { url = "https://files.pythonhosted.org/packages/89/72/abed959c956a4bfc72b58c0384bb7940663c678127538634d896b1195c10/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:c5aa639bc113e9286137cec8fadc20e9cd732b2cc385c0b7fa673b84fc1f2a93", size = 417083, upload-time = "2025-10-02T14:36:12.276Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b3/62fd2b586283b7d7d665fb98e266decadf31f058f1cf6c478741f68af0cb/xxhash-3.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5c1343d49ac102799905e115aee590183c3921d475356cb24b4de29a4bc56518", size = 193913, upload-time = "2025-10-02T14:36:14.025Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9a/c19c42c5b3f5a4aad748a6d5b4f23df3bed7ee5445accc65a0fb3ff03953/xxhash-3.6.0-cp314-cp314t-win32.whl", hash = "sha256:5851f033c3030dd95c086b4a36a2683c2ff4a799b23af60977188b057e467119", size = 31586, upload-time = "2025-10-02T14:36:15.603Z" }, - { url = "https://files.pythonhosted.org/packages/03/d6/4cc450345be9924fd5dc8c590ceda1db5b43a0a889587b0ae81a95511360/xxhash-3.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0444e7967dac37569052d2409b00a8860c2135cff05502df4da80267d384849f", size = 32526, upload-time = "2025-10-02T14:36:16.708Z" }, - { url = "https://files.pythonhosted.org/packages/0f/c9/7243eb3f9eaabd1a88a5a5acadf06df2d83b100c62684b7425c6a11bcaa8/xxhash-3.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:bb79b1e63f6fd84ec778a4b1916dfe0a7c3fdb986c06addd5db3a0d413819d95", size = 28898, upload-time = "2025-10-02T14:36:17.843Z" }, - { url = "https://files.pythonhosted.org/packages/93/1e/8aec23647a34a249f62e2398c42955acd9b4c6ed5cf08cbea94dc46f78d2/xxhash-3.6.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0f7b7e2ec26c1666ad5fc9dbfa426a6a3367ceaf79db5dd76264659d509d73b0", size = 30662, upload-time = "2025-10-02T14:37:01.743Z" }, - { url = "https://files.pythonhosted.org/packages/b8/0b/b14510b38ba91caf43006209db846a696ceea6a847a0c9ba0a5b1adc53d6/xxhash-3.6.0-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5dc1e14d14fa0f5789ec29a7062004b5933964bb9b02aae6622b8f530dc40296", size = 41056, upload-time = "2025-10-02T14:37:02.879Z" }, - { url = "https://files.pythonhosted.org/packages/50/55/15a7b8a56590e66ccd374bbfa3f9ffc45b810886c8c3b614e3f90bd2367c/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:881b47fc47e051b37d94d13e7455131054b56749b91b508b0907eb07900d1c13", size = 36251, upload-time = "2025-10-02T14:37:04.44Z" }, - { url = "https://files.pythonhosted.org/packages/62/b2/5ac99a041a29e58e95f907876b04f7067a0242cb85b5f39e726153981503/xxhash-3.6.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6dc31591899f5e5666f04cc2e529e69b4072827085c1ef15294d91a004bc1bd", size = 32481, upload-time = "2025-10-02T14:37:05.869Z" }, - { url = "https://files.pythonhosted.org/packages/7b/d9/8d95e906764a386a3d3b596f3c68bb63687dfca806373509f51ce8eea81f/xxhash-3.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:15e0dac10eb9309508bfc41f7f9deaa7755c69e35af835db9cb10751adebc35d", size = 31565, upload-time = "2025-10-02T14:37:06.966Z" }, +version = "4.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/a5/1386f35da1475fcaeef42581deae73417c6d2a6a0b2d2e8914de18844dcd/xxhash-4.0.1.tar.gz", hash = "sha256:d55bf4ef10eb09b8b6866790e083d26d087d84caa3cc0946ba87c3ca7ecaf7b7", size = 101513, upload-time = "2026-08-17T08:24:08.557Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/58/bc81e25cceab76ce4b400441e3a43312bf3887fedbb2e5f80cc5a7dd7f75/xxhash-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8b4477edc03091f51f5309406d230851c23cf4822029e3bf40b8df53093fff1c", size = 38473, upload-time = "2026-08-17T08:20:34.303Z" }, + { url = "https://files.pythonhosted.org/packages/64/8d/d95e810c9a2930906f1fbd0e38f77554abb8e042a190aa9cbb24da39e9a2/xxhash-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:04f9a24de11a6647666d5302fd73d6a5224ce50ddc965fb0bb44cee736e6bd7c", size = 36228, upload-time = "2026-08-17T08:20:36.641Z" }, + { url = "https://files.pythonhosted.org/packages/72/be/ebcded2a32ba664a17a1737a91b6baa298bcda6942acdad81af81660b74c/xxhash-4.0.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8c5ce76b94ba49f3be8a8f2611abc6564210702c72ac9e237ca2bebfd17794", size = 253292, upload-time = "2026-08-17T08:21:27.206Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/72d31d787d988d1130253bc34d249c553f02c9387e7dda789b6d5aa7963b/xxhash-4.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4c8842fb19d78b5e8c2a52baf4c8357658cc56c62bc822b86ce0f942f28e286", size = 276545, upload-time = "2026-08-17T08:20:26.726Z" }, + { url = "https://files.pythonhosted.org/packages/39/ae/048f3b1f283a340bef3697fe5a9d4f10de1696a379af9af6b2352c24d82a/xxhash-4.0.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a43418e1a90b4809a9caf64aeb8b0696e3e1f300a323acc1e6ee2f93ae319fcf", size = 296295, upload-time = "2026-08-17T08:20:15.759Z" }, + { url = "https://files.pythonhosted.org/packages/8e/43/e9a593c81445c8e8d669402edb8264144624e7e5e2406df7d33e3c164d90/xxhash-4.0.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b3662719007e059abde7eddacf8517142ba076ddc7b30c807260e57d28c3c191", size = 279966, upload-time = "2026-08-17T08:21:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/f9/4c/29da2b166955a300521c0abd498ad4481916c3743949b106192b781f58fc/xxhash-4.0.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:06d7fbd609503c3be5e65cdb6bb2f040d6a98574404e2e1d5c60815c97fff4aa", size = 509850, upload-time = "2026-08-17T08:20:32.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/ff/e39e1900179ce7f0f23e7000d9fc672471a8d05b6b2dc657cb496c8975d0/xxhash-4.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:101aa300de6ceef3d9c77569706330d8921fc45dd82bceed2084f1e9f2557a24", size = 261005, upload-time = "2026-08-17T08:20:40.735Z" }, + { url = "https://files.pythonhosted.org/packages/89/79/76ee26720d13219458f4b2ec0b22f539fe2bcb1f83dc22a24be4cda4e285/xxhash-4.0.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e4296fcc790876a8b0f297edc83d3b088457b774d8f67b4636807f8a2ec69a79", size = 339620, upload-time = "2026-08-17T08:20:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c7/56b53252d64ecb2f3a0d283b41033561b1bec9c268095150153b694c2e52/xxhash-4.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:57d7fa8f23908d173001c21a9e82bfc6ad997d1b6c270fb121812b7ed158891c", size = 272558, upload-time = "2026-08-17T08:21:33.883Z" }, + { url = "https://files.pythonhosted.org/packages/fc/76/83101a2f2ad3eb6b4b5d571e0aa394a576e3e23121707d507d80197ac385/xxhash-4.0.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85e402dab0f9acd3604539747c6fcc57dc188a18af6ab07eb8189351cd32466c", size = 300417, upload-time = "2026-08-17T08:20:36.183Z" }, + { url = "https://files.pythonhosted.org/packages/34/5d/5be1166ec4fc4bc896e508dc51189a2dad200b373269a430e214fb152693/xxhash-4.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fb59a0dd61fb2ad481c03fda399d78ce57dab6bb62c2c8fdb446a7ba4754b89a", size = 259286, upload-time = "2026-08-17T08:20:21.51Z" }, + { url = "https://files.pythonhosted.org/packages/77/6c/42f6201f13278787c58a6b3a1cd597b47e8665a7d4f68a3440d001c05a75/xxhash-4.0.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:0b20a06454b34f1531fc677c54efe2ecdec691ef9224f7fa919bf2c1363f7ff1", size = 278230, upload-time = "2026-08-17T08:21:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/3cb7f149a5a469c628604c33bec6d79764698b315700a4bbf1c6fb15622b/xxhash-4.0.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f7db035447a0ac8959aa230c5d36545ecf9f547413eb1711c0ca6f0ba1418925", size = 329846, upload-time = "2026-08-17T08:35:10.922Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ee/934eb1e11f4d0e95f3828825f8da4b6d59e338235d63edc3d929769262d2/xxhash-4.0.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:34ed93e20bfd98d722b902121643791eeb4b1641871e2dc63d0d4c2d93f187df", size = 477285, upload-time = "2026-08-17T08:20:46.297Z" }, + { url = "https://files.pythonhosted.org/packages/e2/0b/77835dcd7aec970db74f5724c94207ada83b3e2f5e1474ab44b9c8fe5ea5/xxhash-4.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6247f5e23ee94f2557ac9dab738a336f607c6ff476fcf66ca70c3aef5eee15a", size = 257552, upload-time = "2026-08-17T08:20:58.211Z" }, + { url = "https://files.pythonhosted.org/packages/da/6c/a3ce7a1a9c4ec9ad8babba591a996a71c474ff9630c5fb04c8c9d8b995ca/xxhash-4.0.1-cp311-cp311-win32.whl", hash = "sha256:348c8f288dc961d6bbd1985c8152a3ed7a85c95df00e82320f0c5215d922a399", size = 34630, upload-time = "2026-08-17T08:21:53.323Z" }, + { url = "https://files.pythonhosted.org/packages/d9/58/60d2170e8cda0891aab25dcaa74797b420c3c6cde8a5bc8372f17b30c0cf/xxhash-4.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ac0f291ab6485bd71f33941f9b92771318332a05d505460b41e893a549caadc0", size = 36992, upload-time = "2026-08-17T08:20:41.973Z" }, + { url = "https://files.pythonhosted.org/packages/ac/de/b229a39f9bbbe30cbcf9afaaa8993cb65286715c90a3b058c3769196ae02/xxhash-4.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:72f34834518157a75e7090f328ee7a16c70c804cfc7c694fa069cc888e9fc03e", size = 33289, upload-time = "2026-08-17T08:20:32.176Z" }, + { url = "https://files.pythonhosted.org/packages/26/6c/dc7cffeadd06336cd934947187cd38abb263103bbc552ca0f55fe4ff595a/xxhash-4.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1ee523f51718e41753f04f7102bb4dc55a18d2ea5cbaceef8ec7ca08571bd428", size = 38444, upload-time = "2026-08-17T08:21:54.332Z" }, + { url = "https://files.pythonhosted.org/packages/75/c9/cf736f6db8c3273af18925061572db0d4357818a9ce425f4b5fb0021918e/xxhash-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:515a822c73abbf6a0b7c70976d9662be342835c9d78b8dc7c023411f39c35dbc", size = 36195, upload-time = "2026-08-17T08:35:13.004Z" }, + { url = "https://files.pythonhosted.org/packages/da/a2/ca1929354b6851529d0148f7f335b5e2b0281f83bab3e19f0896dc579796/xxhash-4.0.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f5d031f35962e5483a613214e61f09fe24ab523062c3646d592dc16c4a217451", size = 253113, upload-time = "2026-08-17T08:20:52.152Z" }, + { url = "https://files.pythonhosted.org/packages/de/bb/542005206af59518bc8d78a210f1e0172217bc53beb32f64a5b632e72b6b/xxhash-4.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:da0264844a09b538c894e5eff25313d941deb4dedec2131b98418a71a3c9944e", size = 276525, upload-time = "2026-08-17T08:21:01.886Z" }, + { url = "https://files.pythonhosted.org/packages/1b/df/607cff25dcb0f1d35c3b04493f6ad8471edb03fd4eacbdcc5ceddef1f3e9/xxhash-4.0.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1642907941ee4b75aacc3db688af52ea02ca2305ab22af7ee686ed726b332684", size = 297703, upload-time = "2026-08-17T08:21:57.958Z" }, + { url = "https://files.pythonhosted.org/packages/15/ba/9d2275eea0b9d9c6b02921be23f7588356c60df95c763b25f0e045894d43/xxhash-4.0.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4af350bc3f329970c0e3a59af84a8a30998bf8a9167eb50cd48e59baaa1d7bec", size = 280252, upload-time = "2026-08-17T08:20:47.299Z" }, + { url = "https://files.pythonhosted.org/packages/1d/aa/2299d9f6369e550aef2abb64945e39daa34412725aa46a20d99b74d76f67/xxhash-4.0.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8ba782ca3bf1e81492611152b9a0d5264971339e95e34d69de0ac2c926be496d", size = 511041, upload-time = "2026-08-17T08:20:36.771Z" }, + { url = "https://files.pythonhosted.org/packages/83/97/31bd8b8279e6935a0719f6910ced15e9d5a2cd554b253f6027ce1b5a1c2c/xxhash-4.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:237b8f63a2a0fcfb1ffc06e21dad23add44e6d354b2b014364a1d41e419a4dee", size = 261812, upload-time = "2026-08-17T08:22:00.469Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c1/d180a2da23c105d8e0b02d54f9f5841013fc81c233010ec781e31f1aee4c/xxhash-4.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:81507a68ba84c55241fb61cce1469f473a5da4205fc8ef6f698e5948eea8dd88", size = 339878, upload-time = "2026-08-17T08:35:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3d/f584cd3172fe934f0f5a0a3917d0d7ce781f74d794fd43bb72be71c3ef6f/xxhash-4.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5f1ea31d61bcd2cd2f3ec4ca80a64187bbd7948f490b63cf0dcbc6e717b4c1e9", size = 272871, upload-time = "2026-08-17T08:20:56.067Z" }, + { url = "https://files.pythonhosted.org/packages/34/50/2c7956b2b551682e00b9aebce9ceb0a991a131d65f9850c09f5f9760be2e/xxhash-4.0.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:06713a5aaf1d0905c5579416c020c02e42b3ceb931e86c7d3b7fb85403dee3f3", size = 301440, upload-time = "2026-08-17T08:21:35.911Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/0739f6482184a8026f4b022718f5f815d352059312e80696825433f0a8e7/xxhash-4.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e8cda075b10bb3917b002c74a04f9e02b7d13b5bf732571404d51c52b11c7329", size = 260157, upload-time = "2026-08-17T08:22:01.416Z" }, + { url = "https://files.pythonhosted.org/packages/a1/25/b31a7bcf1d7d116842812e54f9b944843b4236ea4fa85634e8259f342212/xxhash-4.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c10b9206753b64aa791b35b201485477525b26fdec5bf86e8364c388a03e2592", size = 278233, upload-time = "2026-08-17T08:21:15.674Z" }, + { url = "https://files.pythonhosted.org/packages/db/e8/5293bae090fc6119dbc5fcf5c4cc0e1536394b52d73b7904d033836c73db/xxhash-4.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f3e1a44af01b6692de0ec6caba5f0bf93ceb36896e02b7fc00952c6ea7ef39e1", size = 330270, upload-time = "2026-08-17T08:20:51.128Z" }, + { url = "https://files.pythonhosted.org/packages/72/9e/e2ab12d40921f3f34c9317637d65e011aeababf8288356ea8d527de2c1d0/xxhash-4.0.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6fc415b5568bd9accc7187f1729a99707330c0a67a8b9f93c1149ed573ed75d", size = 478555, upload-time = "2026-08-17T08:22:04.183Z" }, + { url = "https://files.pythonhosted.org/packages/6d/32/c6148d39a49efa95f39b4cf0d41ef35a487f3b30f6fb1fc8fe8d8eab577e/xxhash-4.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:96d8de55029d42251945531f6aa7590c32b48163c66a43bf29d8657d7446a377", size = 258174, upload-time = "2026-08-17T08:35:21.18Z" }, + { url = "https://files.pythonhosted.org/packages/8f/fb/0b04b68d6c5bc71c7a2c344f1287327b67e607f28fbcfd937697caca64b6/xxhash-4.0.1-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:0163b5d259de23ae9e07b7eabf435ce4704f6f205589a2b154e6af4be985ce1b", size = 20767, upload-time = "2026-08-17T08:21:00.806Z" }, + { url = "https://files.pythonhosted.org/packages/a6/be/476092aba34d1fcd313e1613a3bb3bc692f253d167b54bc90049043b5034/xxhash-4.0.1-cp312-cp312-win32.whl", hash = "sha256:1216f7ba5683f17a89eb7dcb4bc50a0b743dfe1902278d7b3d0786f538118433", size = 34669, upload-time = "2026-08-17T08:21:49.486Z" }, + { url = "https://files.pythonhosted.org/packages/aa/02/f9413d94fae43cec6d1a74c4f12156c6f4a7f5fd50e1d34defebdee3dec9/xxhash-4.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c2d525a3afabcd8e3549d85fc7e111fde6bc302d06a1893fe73adb79823415e", size = 37073, upload-time = "2026-08-17T08:22:04.886Z" }, + { url = "https://files.pythonhosted.org/packages/c1/83/6fe93c1b95acf962bc61a246df09dc2dcce895ccfc1080c9f48d0b652b92/xxhash-4.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:86b2b12bec60c678ed8f5cca0258ad93a8928ebddb6ca7732f0875afe1451d1a", size = 33299, upload-time = "2026-08-17T08:35:12.708Z" }, + { url = "https://files.pythonhosted.org/packages/f3/dd/c707286b527722f776e1fb81dd202c45623355ba1a2972337a2a26075b2b/xxhash-4.0.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:8c9fe122444e129881afd1d4d1c7ac0d3ce2d91b68c2b40173b6025ff1c31f9a", size = 43639, upload-time = "2026-08-17T08:20:54.945Z" }, + { url = "https://files.pythonhosted.org/packages/1b/3b/bb71639a0f95635f61936a6f2653599c4261b645ddddd8d00f9dfe3613e2/xxhash-4.0.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:1f3346c5c287ac3c7f38b20380f55e8768230e7252af59fabcf3b87ab21e4256", size = 40657, upload-time = "2026-08-17T08:22:12.616Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/76f3f5385faa9886a36f21fcc603f40b4c0c40ce622382f133160c48b4d9/xxhash-4.0.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4e5141543c7f7fe3087500bbb4ac2845cb528a980aa91f8f1e661e2292ff4a5d", size = 34708, upload-time = "2026-08-17T08:35:24.614Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4a/f48f0e3e1b1ab072979fff2a5be899234e28090883e8b519d0b10215d708/xxhash-4.0.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f09ee747e2a5f876cc5ad56947734811828335e13b403dd8ea1e06d77a9dd48d", size = 35650, upload-time = "2026-08-17T08:21:09.337Z" }, + { url = "https://files.pythonhosted.org/packages/c4/53/b73d7472b196101ad1f57ed0674af3af803ac3e9ec2feadd650a7b262562/xxhash-4.0.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:acf52474b2494ef66dc7e0fb6d5e2b50c18313039ad4d275fbf9f9907c804bc5", size = 37958, upload-time = "2026-08-17T08:22:10.616Z" }, + { url = "https://files.pythonhosted.org/packages/d0/f2/024946ad8fa532074af4e4380179da54b7ec9facc8bd0b279ec0fac4e63a/xxhash-4.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1b3cccf75eeb5b01639b2feadb042a8e07889293b7ca72fa2985e7dcb64763cf", size = 38032, upload-time = "2026-08-17T08:22:09.535Z" }, + { url = "https://files.pythonhosted.org/packages/da/e0/934af8d99bb5885711006bec30a691f728edd513d2c40f053f887d8e7577/xxhash-4.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cd878d32f5c6cbce9783f8d6897561fb772211edba9dde49d85672b88ed45276", size = 35895, upload-time = "2026-08-17T08:35:16.53Z" }, + { url = "https://files.pythonhosted.org/packages/20/5f/a8011f6a1558f7ca66d9077bb4f192b1871afcea62fbd5733605d2015755/xxhash-4.0.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:41e579025a6e13a99e6d71e39c9cfc621a0dcdbbf19106325e145fa858f2d794", size = 259464, upload-time = "2026-08-17T08:21:06.72Z" }, + { url = "https://files.pythonhosted.org/packages/ff/89/9665a44397547e7a3d58c0942425a976d58dcfd4b538f33220a312bf6912/xxhash-4.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74379a577a9f3b6afbdedf1b90e5c7764467051977f18a326d7d607336d743bd", size = 283949, upload-time = "2026-08-17T08:22:17.003Z" }, + { url = "https://files.pythonhosted.org/packages/34/2d/78774141266457468f29f3f5803092df4db87d8148ba74e4debd041649db/xxhash-4.0.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:acb31ecdd1a97fab5cd39a84ee9f515e727d319f796fec48703b8339b9998360", size = 303898, upload-time = "2026-08-17T08:35:27.951Z" }, + { url = "https://files.pythonhosted.org/packages/59/48/d78d22de576b42528bff87c14207de50de4f0b888221a50ff7c9d675d670/xxhash-4.0.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5b7875ac1a2edcb691f27642b8b94b904baa6bcecb7d79c72df2228ba8cb5c51", size = 287241, upload-time = "2026-08-17T08:21:13.042Z" }, + { url = "https://files.pythonhosted.org/packages/4c/de/7a1755a59c59fd46176f293bbdd99e399a6537ba9537fc723aa4d1bf6e27/xxhash-4.0.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4751f1d7eecae6b2d2a773630f1a7248f125c9a92a456694d03c15bceffc9d68", size = 519856, upload-time = "2026-08-17T08:22:15.35Z" }, + { url = "https://files.pythonhosted.org/packages/6f/fb/76580c08e916507859b0f335393cb5fdc59452c4402edbc6bcca6e47e7df/xxhash-4.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9a51b061d54cda8b83e62c44458bfbf0dabbef9b975dd9649952ba5076b9f349", size = 268572, upload-time = "2026-08-17T08:22:14.533Z" }, + { url = "https://files.pythonhosted.org/packages/d0/2b/1abde3e07b8f2077a38b4fbfaf764115008bfe0ff03bc7756a52c9fd0607/xxhash-4.0.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:74a164e8b63f1e9cf35c9a7809d082b033d1a00e7375d5d814415436e7867e57", size = 344967, upload-time = "2026-08-17T08:35:23.569Z" }, + { url = "https://files.pythonhosted.org/packages/5c/15/80b6ddf0732eef48a8b5fe717398274794392bd6dbe82af38d189d214772/xxhash-4.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f5e5c6df4b703afcbe9352d238a51efd97c3b91fdc3a2052e40fdacb1e7505f", size = 279956, upload-time = "2026-08-17T08:21:24.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/e0/11cbc43c205bf81fad50d69c7319cd1b1ccc01a66cd4fb8766357126c43d/xxhash-4.0.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d54b8ae068af532c8cdf56abb9e09a60fbe7b10792444c9c27987bb6d3b450fa", size = 307583, upload-time = "2026-08-17T08:22:22.541Z" }, + { url = "https://files.pythonhosted.org/packages/1c/11/cf0bc07feb2791045b6ac075d4bf64f1a5beedef2f46ae70d7104d63a19f/xxhash-4.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1749f0688020209fe0d357ce1e1cd9ec9c6161ed0405ea949d24581c4c43fa91", size = 265848, upload-time = "2026-08-17T08:35:31.298Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c4/7ada4bea2a2795073dfc42d96842930efbe7a0c1857ef4b522e4e90e5d83/xxhash-4.0.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:94ac8a6b8c47951173f0b67bf862bcb971bf24e493b9fbbdb0e010cbbc7d9f54", size = 284409, upload-time = "2026-08-17T08:21:23.156Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f4/d8ce83dd6b99ccfbdadaf2db968ae40334d2e5f73a0297e593b9ddb3df39/xxhash-4.0.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:a33de7633c948ab2dc144af370a66e7e7af29b425dcd0f7e4f59689fb9391b53", size = 335921, upload-time = "2026-08-17T08:22:21.802Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9f/f47d8724bd8bc45b395b06b7cacea2dae0d00031af1b707184a091161df6/xxhash-4.0.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:247ece770647c0aef080561fa996f9774b4dadce2d0c42eeb98229db7dcf820d", size = 487023, upload-time = "2026-08-17T08:22:19.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/54/2d87098f3371cc1e42dd04d2285ad56bca4c56667bc501bff02d2b9fd6b5/xxhash-4.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a4553d36cc0b7fce1f35ba8a94dfd775aa3ed12f5eab2dc3b46ac75a0706b0bb", size = 264333, upload-time = "2026-08-17T08:35:27.001Z" }, + { url = "https://files.pythonhosted.org/packages/27/b8/93795ca5898ec7d7d0455283ad261c0fc76b4f0c0a69e86233bd7badb0bd/xxhash-4.0.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:87aa309a93bd5ec13f14309a305ff4e9bf74c5363fc46c264c0a22edfd5b0670", size = 20581, upload-time = "2026-08-17T08:21:39.207Z" }, + { url = "https://files.pythonhosted.org/packages/b6/96/926f7335a0a1647952c00421e8da877f658094f61336306c7cadc335c94d/xxhash-4.0.1-cp313-cp313-win32.whl", hash = "sha256:cba763d84b06bda2c38d5185dee76f1b9dfdc0789e96e476d9e10005526d0788", size = 34449, upload-time = "2026-08-17T08:22:29.362Z" }, + { url = "https://files.pythonhosted.org/packages/ea/61/8a5aeb811de093bab3434e77eff0e9461624a1a56a6a93d315d080aab2aa/xxhash-4.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:97b94fb29abf21f5f0bde15f7dbdd3a4aa2dc59f37026adc7b4bee8563b84375", size = 36520, upload-time = "2026-08-17T08:35:34.852Z" }, + { url = "https://files.pythonhosted.org/packages/04/14/97f3c74000ca36955e9cb86f6d270dcd5848b5c65afa623453f5cf2d83d6/xxhash-4.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:08ed8da18cd4fd0a6a5d6a444852d8fbd0e565388a74a4937085451b5f1a312a", size = 33428, upload-time = "2026-08-17T08:21:31.713Z" }, + { url = "https://files.pythonhosted.org/packages/81/0e/ea406a02b561d3275232ccfdb3e29df80f7a65414940e3a15721c7bea40f/xxhash-4.0.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:af05a3f650220a6c59fa0ad2410249f2d2470a05225807c378fb67458693f8df", size = 43747, upload-time = "2026-08-17T08:22:31.37Z" }, + { url = "https://files.pythonhosted.org/packages/f9/f0/b0c94d61ccf6b5d1f8847b58ef8f923125ac4919ed5bd0eb082750ca7cbd/xxhash-4.0.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:a6e3653df1a70b8ac4191216324242e4be2bca18c9a7c10934e1bd56dc7ca15e", size = 40749, upload-time = "2026-08-17T08:22:29.431Z" }, + { url = "https://files.pythonhosted.org/packages/2f/c5/8085881a538983be0fd1c865d5df236242fea496044e2c8ca32b9f2ba39c/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:4528cf80ebbbf57d40edfb31521ae265daa6dd636d615b1cf0ac86209579e59d", size = 34734, upload-time = "2026-08-17T08:35:33.68Z" }, + { url = "https://files.pythonhosted.org/packages/d3/94/8803d13c968fc75ca434eea991d29ac5fd8a36b4afc9a6a9803c53933db4/xxhash-4.0.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:90cb2a1c9cc503a054a19612b48ff6e8e47805f618bdb3224a07568aad03a37e", size = 35671, upload-time = "2026-08-17T08:21:48.322Z" }, + { url = "https://files.pythonhosted.org/packages/85/d5/ad91d7f0fd294190d37c08236fe661f5c4e3f83dcd1a121877a2e64681ce/xxhash-4.0.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a949b072ea59c6eca0811ccd9e95133cc50d2afda8d464b5b077c78f78efa269", size = 38094, upload-time = "2026-08-17T08:22:39.763Z" }, + { url = "https://files.pythonhosted.org/packages/89/f4/2b7ebdc1869caca5f02c4cba8379b631050d3c3d4adb9187e4dc1a6b8d3c/xxhash-4.0.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:79a3203aadf39637869dfea1185227d8452844d78b837e54fb1117b4d34ba5c3", size = 38244, upload-time = "2026-08-17T08:35:38.081Z" }, + { url = "https://files.pythonhosted.org/packages/90/9d/f66cf6935f528e575f1ae4d6560d376e7587569747186f4fae8777cadc1b/xxhash-4.0.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d9f3848ffaf010bdbabdbf4c25641fa258b6227ff27bc74a4d06edef521a4873", size = 35904, upload-time = "2026-08-17T08:21:37.358Z" }, + { url = "https://files.pythonhosted.org/packages/07/29/34569d7b482f0dc060074faafd163c588f915cbc3e3e218f1ffd8a3ad340/xxhash-4.0.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9283d9dd6b44acad35118e2976fc763a065509e4118debdb61916ec322ed17b9", size = 259595, upload-time = "2026-08-17T08:22:38.153Z" }, + { url = "https://files.pythonhosted.org/packages/ce/d2/a2370acfcd48732cf5c2b87f06cfbf7fa51c0ce0dd736bde42939eb9ebf7/xxhash-4.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c7c642a0f79c3e3cf2965475507574d3d1a50ec71060039d60cb87358667cb2", size = 284279, upload-time = "2026-08-17T08:22:36.396Z" }, + { url = "https://files.pythonhosted.org/packages/08/15/17d33c24e6c4a1c0b9ddc5584f0c25d51d48b34bacde1416a2235a19db4b/xxhash-4.0.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:96dedccfb09a73a25751053a183159b88f4ee75f388df8166040c152ac0531c6", size = 303973, upload-time = "2026-08-17T08:35:39.22Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e0/4ec0d69ad5738729098a61e631b7ed2df22a922b0e03014b597c72bd863d/xxhash-4.0.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81664268dba92e037b740ecf37fa02f1cab4a391f93f28e35792b3341c60648f", size = 287535, upload-time = "2026-08-17T08:21:52.158Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4f9b17e7a9eb71c65548ecddd9c18b84e3c18ca41c4d436ad2a3000d3f7b/xxhash-4.0.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:839f58c5bd9989875be0fd28446dbf32cace2c2cd8bf2f6762acdc38a95cd1aa", size = 519257, upload-time = "2026-08-17T08:22:43.272Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/3276b3e743b8ddbed9c3f71c76d9dd6a75d72aa4e678b1447b635cfd92e0/xxhash-4.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa44b4c7c5d0ffa31356b4428659516c0e47647825c74079a296b3857b6d99d", size = 268190, upload-time = "2026-08-17T08:35:44.985Z" }, + { url = "https://files.pythonhosted.org/packages/08/d4/f1555de3c96721320930dbb7988c8482d82b85970076aba1a8d40e83ad43/xxhash-4.0.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e681a6fc7e4f715252b9b5acfb30536ec7dd1f75033a32dc617e6fa95af1a3fd", size = 345553, upload-time = "2026-08-17T08:21:41.025Z" }, + { url = "https://files.pythonhosted.org/packages/ac/98/c28908f27007087b61139d290f908dd827ffd40b88af0c43f9e1a1a7ffd5/xxhash-4.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c6301d92545c591ad31c3e050aa40a5f8a4c16413f1f9e6f9322c6f0f9d2b736", size = 280499, upload-time = "2026-08-17T08:22:52.236Z" }, + { url = "https://files.pythonhosted.org/packages/a9/76/3ef57622c65816348f8196273485baab4752aae064959901e85cd867e067/xxhash-4.0.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6efb8f21cc136c79b3e5bb747c8682d37916fb202cdbbc32182de5c4e47f821f", size = 307211, upload-time = "2026-08-17T08:22:40.815Z" }, + { url = "https://files.pythonhosted.org/packages/8a/4c/5804504bbc808968e57d6a50286dd8f8cc06e0ddd6e4ab4b1dc89ae42f35/xxhash-4.0.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:760de77279e9cf9c81d012ce0705cba13afccee9b09c480f17d778c8c5cefae8", size = 265865, upload-time = "2026-08-17T08:35:42.727Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ee/8572fdfd70e7aaaf150af899871c2cc0bb88c3295ca82172a31e04ca5168/xxhash-4.0.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a16a3fa6936e36bb1414d16a6bd012c9033e5161b68b426805b61d895392437d", size = 284545, upload-time = "2026-08-17T08:21:56.965Z" }, + { url = "https://files.pythonhosted.org/packages/c1/f8/6eadcca0904660c848b466524e82a233d16c9d2d5258433aaf3546142d86/xxhash-4.0.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:9c3c4b9aa9a27196b921197f7daf9e6c1412739df06a99cfa6e923879362eff6", size = 336022, upload-time = "2026-08-17T08:22:46.346Z" }, + { url = "https://files.pythonhosted.org/packages/27/df/4aa107b81602d6d6d09ab5a607c530d2d3a6b28e2e9a59b01875bd877c54/xxhash-4.0.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:863f3d3b44110f7243e86cf994aa5c5d88f2348b6e84ab4402fadadfbf9f7da7", size = 486671, upload-time = "2026-08-17T08:35:49.016Z" }, + { url = "https://files.pythonhosted.org/packages/45/b7/b2bf9b5301e9cd5f2e335fea8da0f5cf209a6594cb1fe77754774ad4a6fd/xxhash-4.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:63aa52659bc32bb9bd7cb5caf523b4d14429a477762cfac886132d687c1f80fc", size = 263744, upload-time = "2026-08-17T08:21:56.165Z" }, + { url = "https://files.pythonhosted.org/packages/0b/96/35b1c02177ae26234892c2310fb4822ba62411acccbf425ab8f9fd99354a/xxhash-4.0.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:67e57b834e07ed973cee7b6da1548ff28a56458d77696fd2a5f397f340694848", size = 20563, upload-time = "2026-08-17T08:35:11.924Z" }, + { url = "https://files.pythonhosted.org/packages/51/c2/a06300b165fbd6b0cb4a9742987f2e997a9f447ce3bf7c6ac97b862ce62a/xxhash-4.0.1-cp314-cp314-win32.whl", hash = "sha256:b6c1f9c59bbe593f88a0aad30be4150f15bd57bd64efb95feeabcb8e563f1ecd", size = 35151, upload-time = "2026-08-17T08:22:44.283Z" }, + { url = "https://files.pythonhosted.org/packages/06/96/c5b37296b78f80fc97124c0fee0c7bbd1bdb6f3b18bcd8748bb113b2d8fc/xxhash-4.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:da544672efd9ad76077928a3e6c5d894e52ce82d3bf14002db4a1bf17d1a36a2", size = 37156, upload-time = "2026-08-17T08:35:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/ce/5e/248f9cd169c2fb62236bedfba246d213bce728f74901e99047e3f3c55875/xxhash-4.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:d0d24a4f3fb63852cd09af46ae4b7a4d00cc8b8615a046dca543786e728d1056", size = 34379, upload-time = "2026-08-17T08:21:59.446Z" }, + { url = "https://files.pythonhosted.org/packages/58/c8/db1d37c0da0324d0298f6abd931ca1d4736e049d9f2081230a8421da74d2/xxhash-4.0.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:349775ac30372b344d2338b2a168c0a1312a644194da25b8bec476d55761a128", size = 38656, upload-time = "2026-08-17T08:22:49.119Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/e18998ec465fb977bc74272e5bf3c2e886c13b014cbef916cd607802c709/xxhash-4.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:43e5f9169e73d0f0db33b5f6b8554bcce69ac278c966daf83d5eb4eb2f13829f", size = 36306, upload-time = "2026-08-17T08:35:52.853Z" }, + { url = "https://files.pythonhosted.org/packages/ef/1a/b83f86f8a987a3cbcb7e005a6824ff64aecae35abc1395a0d44ee16c3319/xxhash-4.0.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4a252fb862b0ae2590587e625f47a0e03da05cf0205e8830b67b6596c06038b1", size = 273729, upload-time = "2026-08-17T08:21:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/02/4e/2db15aa8508e0cd5b632927a53b98234f24039ea65377e6cf996c06d2d4f/xxhash-4.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2df3ca8757dc381e75e90a4d7995a6324f58a923c7145220a7b2c0231f66fddc", size = 301083, upload-time = "2026-08-17T08:35:14.113Z" }, + { url = "https://files.pythonhosted.org/packages/26/94/ed759787ffe802bd8e31cfcdad3755cbeca2dcdafd2f790cd6f25d195199/xxhash-4.0.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bfed61996d618eb90d6eaae0178002e3466a28b06bfc557a7a3a7266378d8c5a", size = 312745, upload-time = "2026-08-17T08:22:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/45/7a/f64b4a4cc8b51e950709207f55f7f56ae9c5af6631dd31d7fb443312418c/xxhash-4.0.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9761ff4a0ffa583fe850731ad24fe82c88cccb7a2294727db0955f3279a4cb3f", size = 301419, upload-time = "2026-08-17T08:35:50.143Z" }, + { url = "https://files.pythonhosted.org/packages/a0/71/bac313b8de073569b8db3152044a7cfcce87a3fa9698c18fe9f914dee6b1/xxhash-4.0.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edccc2ec58435a580f96a48a3ccae8cd0a480824119165dd90108718ad81ae6e", size = 534485, upload-time = "2026-08-17T08:22:11.515Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0c/16b5e419f24e59507ee05626d2bb0deafdb03f9f27783bc0785a9849602e/xxhash-4.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4741d42d59e4e5fa1a86c17ab9c27dc8ea459c700d91b6742fdb9138d9a516cb", size = 279605, upload-time = "2026-08-17T08:22:52.934Z" }, + { url = "https://files.pythonhosted.org/packages/5f/55/5787dd6e2d8d5b61256a5039f6b18c2193c7c1de4a2fd2413288d0d9c604/xxhash-4.0.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:440c401e146ce64bdb3beb8ff0c84677b6f21307c28a34779071cecee5d4d70c", size = 358924, upload-time = "2026-08-17T08:35:58.164Z" }, + { url = "https://files.pythonhosted.org/packages/f3/68/89be41991f3b0a2e91f940bdf3128852c3ed571cf560d98ad0f67024afe4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5b7979f71d06ae45a769de0699900a246d8cb632db1e8bfdc79ec019063a503c", size = 295305, upload-time = "2026-08-17T08:22:13.683Z" }, + { url = "https://files.pythonhosted.org/packages/e6/5a/52ff0a0cc361aad393ff9a46ffe3aabbcf9c03d6c8f2612da7d553048276/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:62198213fc3e0c56e567894b318ba45834e007d065f84ba6dc9165d21546fc56", size = 320228, upload-time = "2026-08-17T08:35:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/0f/b5/91c60ff22c7f6cd5f6d7a5bad5a2cdcb4c33987dfa50bf13f0d856279b2e/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b3bece52127ac20044311ee73567f9f0893b5de64f9028aecc90cc740cfd525a", size = 279414, upload-time = "2026-08-17T08:23:03.212Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/9685954804d47d0390871a64bec606a0d536406382d71a784df3a5883fb4/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:a865d2d470220e659220fdb59d5b6c4422802d8d6098e1324bc4d12444798914", size = 297594, upload-time = "2026-08-17T08:35:57.881Z" }, + { url = "https://files.pythonhosted.org/packages/89/62/b67ac9412907b7a07a2a0c08c3440b9e4480231a7b3de0767e87011e4564/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8580aab306888224074c7edeec734de0c3c5ccde65b2da4e6c9a5e28f7c0a1bd", size = 348526, upload-time = "2026-08-17T08:22:18.571Z" }, + { url = "https://files.pythonhosted.org/packages/37/ed/6723cc49a9f567d52d01fd7c1741b0f2e3a13e71d15f7ac49d753a20c115/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2d52dc7c33c1b83082b707f6b7814dc76d2faaa2ea62bd9c5fab4b36f83c087f", size = 499307, upload-time = "2026-08-17T08:22:56.52Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/7b10e101ab988d93b791023be7191d7661271d6ab31ac082276b9091042a/xxhash-4.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6a9f98af872355e0c02439e48583958eee00e60b928bb20476460d9d40cb7b4e", size = 274989, upload-time = "2026-08-17T08:36:01.834Z" }, + { url = "https://files.pythonhosted.org/packages/9b/8d/7eabcc8d29cce40621443cff24c07d7306ef574b8956c47ac59f21098005/xxhash-4.0.1-cp314-cp314t-win32.whl", hash = "sha256:a14578102a6081465aec9cf73c76c3cd3f79f0709bdb3b8ae7ab0b54c9d8b089", size = 35482, upload-time = "2026-08-17T08:22:32.336Z" }, + { url = "https://files.pythonhosted.org/packages/ca/89/2a4268e1971f63038b79fb75e3b9c8de942cd77acabbb0c5625352a31940/xxhash-4.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c57963970d359a72262f7fe6be88f945e2334d4bc41462b7f08c37b0abf35ca6", size = 37490, upload-time = "2026-08-17T08:35:22.475Z" }, + { url = "https://files.pythonhosted.org/packages/90/7b/950ecab1fe4cf421d0a6211ddd9a0ac82e39e55c45a111ceb90953dc6c9a/xxhash-4.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:b659fad79c99b0238c7ad7e9d7dbf4eebfea9097c2dba65fa0a4d18a25b29a2f", size = 34596, upload-time = "2026-08-17T08:23:10.001Z" }, + { url = "https://files.pythonhosted.org/packages/c4/03/7dc3b85fac10751613bfedb0e120734e0e8710054abad3f931e9d3843a14/xxhash-4.0.1-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:5adf927dca8c47fde7e683fe69efdd81bc865c4db1fb6bb00b391e2b6185207b", size = 43749, upload-time = "2026-08-17T08:36:00.47Z" }, + { url = "https://files.pythonhosted.org/packages/a5/55/bfac071c5b1c6d6a3d48ab1ab96a15e958a1d7061f4afc97804292d87264/xxhash-4.0.1-cp315-cp315-android_24_x86_64.whl", hash = "sha256:c30dd1af66a820820398b26e0d74e7a9aa43cae705924f23ed828cd8e5c26c3d", size = 40758, upload-time = "2026-08-17T08:22:30.209Z" }, + { url = "https://files.pythonhosted.org/packages/79/87/49a260e685d1a74c56a69432a8ee0527ddcbd684a3c51f87edc3b75639c5/xxhash-4.0.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1bc591533fc975614f7e13594daee76af96b8e1fbcf8de76c8773858fa9e7cea", size = 34788, upload-time = "2026-08-17T08:23:09.014Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ef/50d72ed2170dae872e1c0fe333d0908e0a2afbffe74c5c9037d5406a4b89/xxhash-4.0.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:567cbc630302a46a8ecfd943b309ccf5372bb3718f1f3762d452df30f033bcf0", size = 35746, upload-time = "2026-08-17T08:36:05.557Z" }, + { url = "https://files.pythonhosted.org/packages/66/f0/969deaa2bab3bfd5ad5b023442124d2255b9961eef6f797ec74eb8683bdf/xxhash-4.0.1-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e998cb3685b92101ec5de0fb4d9485cf01e50bc418211955c55d98064664cf4c", size = 38098, upload-time = "2026-08-17T08:22:36.906Z" }, + { url = "https://files.pythonhosted.org/packages/86/aa/45ed7d7b8d7b66202a47bf8ff3b77cea28d2ea54dfcdd202b4cfe043e3dc/xxhash-4.0.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:c3074db513c81f764053e3da079312ecf85a50d8350c71f4cc0105d9662a9e6c", size = 38251, upload-time = "2026-08-17T08:35:25.774Z" }, + { url = "https://files.pythonhosted.org/packages/f1/9d/45e7520a7856e13800a5dc8cd038d34c6372429465b163af0c5722f16918/xxhash-4.0.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:3088dadbffa33c29e0518578430a7dff2e901a212e487aefa5faaa0dc06dad34", size = 35986, upload-time = "2026-08-17T08:23:25.854Z" }, + { url = "https://files.pythonhosted.org/packages/9e/0e/5ad466e5fea18c9f9bdc5828c0506f62190061b4a1b0e688aa54969d0a9e/xxhash-4.0.1-cp315-cp315-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1b50223d92df94d54e1a31469335a2c74b16692e6c1cb726f1e6949514458706", size = 264609, upload-time = "2026-08-17T08:36:04.229Z" }, + { url = "https://files.pythonhosted.org/packages/aa/cf/8f269f85217e3dbd45e31e25e46cc26f3aff0e159ef05d228b4b982c778c/xxhash-4.0.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:427b62d62d4f967fbb10b82a3813e4875c2a6e7e7634739f17265b650c7f65a6", size = 285910, upload-time = "2026-08-17T08:22:38.589Z" }, + { url = "https://files.pythonhosted.org/packages/ca/30/2fc1a16ee0f9501d074b798ebfae52e24fa602c7117f5c4b81de71eada72/xxhash-4.0.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c6370189e8e66b7e608f533b939a9de092ddca6cce084ca0d3d414d2ed5b5d59", size = 306566, upload-time = "2026-08-17T08:23:16.895Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a7/08375cf2b997e1903663fe7525c5973b1987a4f8ad2b8d47463e9143f2ee/xxhash-4.0.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ec1a470c6db94ac4589c203921e89ac1bc13e796a8b1784d8135e1893559cd3b", size = 287978, upload-time = "2026-08-17T08:36:09.296Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/90a7b404c11add9e53a497d06236152852490c3b2f21e468d97a58f26afe/xxhash-4.0.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37f667dee0f867c42894b34e2a6fe26bf195c0ea4683d9d2b713db023f242c3a", size = 520098, upload-time = "2026-08-17T08:22:41.565Z" }, + { url = "https://files.pythonhosted.org/packages/11/02/7fba10b1b17eb46308f09cc0a4ed513d74dff16b1e22a1c439f011c77129/xxhash-4.0.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f18732adcc271741bd651c3e56fa519d8a237d2cccda01fe3afb226bf87f783b", size = 268273, upload-time = "2026-08-17T08:35:29.043Z" }, + { url = "https://files.pythonhosted.org/packages/54/49/c21b228877357a3be43eeeaa22182ad1685796f415390ada475922c084e4/xxhash-4.0.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b42a5a26607e4b2409fea174773a66f2dff9dfdbf2c1a851bb7b804e2c97535", size = 350164, upload-time = "2026-08-17T08:23:29.494Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/c15bb4aa33d94b78a5553b52e7fa1070565f0199925aeadec3871de20ce9/xxhash-4.0.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:99166cc98637e8bf550cda2aab07f4f1d5f899c45fbd721801aeabcc9d404824", size = 281975, upload-time = "2026-08-17T08:36:08.139Z" }, + { url = "https://files.pythonhosted.org/packages/18/7a/b1d0388315fe7752b7725b68a912667526a1dd48ed492fcc031ac03f4b52/xxhash-4.0.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:6cf633df84d80a1668fcf61e330791dae46825e395549e7d34f376411e75088a", size = 307872, upload-time = "2026-08-17T08:22:42.206Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a1/037cb2dd8cf725c9565dfe3712b2915c0e0276a9154913dbfcbcecbeb672/xxhash-4.0.1-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:e259bb7e1e2d8de6b35f430f5c7220b1c0ebf3962d1ba7ec7545980d5931edb8", size = 268241, upload-time = "2026-08-17T08:23:23.997Z" }, + { url = "https://files.pythonhosted.org/packages/c6/a9/67c44422d0ee082169b238ce24bd2796b82d7c21ed953471365df8c508d8/xxhash-4.0.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:704381264b36a18b9c62ecbabe2e71d0fc58c77c129c15355c989b10bf05b6b0", size = 284970, upload-time = "2026-08-17T08:36:13.476Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d0/254a5f51c4014cacc77a26f321372338b924f54e89efb730164ee336d850/xxhash-4.0.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:e90b4bcf1d9eb1010fdaee7c9209fb667e74c0684f3ba17f9032bd7319da90c9", size = 338074, upload-time = "2026-08-17T08:22:51.166Z" }, + { url = "https://files.pythonhosted.org/packages/64/03/f21c4830118d72ef3a958ce8bf2152f49e0d4cf200907616c9be6caf372a/xxhash-4.0.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:a65785e653573fcd1e33062760ab4c3c3440e8e910765018e4b6ed4ad07b54a0", size = 487626, upload-time = "2026-08-17T08:35:32.768Z" }, + { url = "https://files.pythonhosted.org/packages/45/1f/268a689d741d7da649317eb4ce41760140beb4179aaf43a7216fdbe8100c/xxhash-4.0.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e3996ff9b6f99180357024336bf5749a8ad6476a9a2523e535c5212b995b12a2", size = 263852, upload-time = "2026-08-17T08:23:41.871Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f5/adaf8101cd7f143191a0b390600294d83924b32cb13770fde8803dce27a2/xxhash-4.0.1-cp315-cp315-pyemscripten_2026_5_wasm32.whl", hash = "sha256:99054b838b74d8d3995ea0d410976ae967c46207ae22d6ddfc535e809197dab9", size = 20569, upload-time = "2026-08-17T08:36:11.952Z" }, + { url = "https://files.pythonhosted.org/packages/ee/2c/56a5eb8c993420fc07114c08f447a2b66ee996510b4764cb368b9b44c9f0/xxhash-4.0.1-cp315-cp315-win32.whl", hash = "sha256:6c45258a37fc22721395c09927cb982d3e7a83607cab15be7e2416501bd3a330", size = 35145, upload-time = "2026-08-17T08:22:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/67/c7/65f210db43e62157d0fef3b4d4d7b394821e7733c8bb4ece49f91410a725/xxhash-4.0.1-cp315-cp315-win_amd64.whl", hash = "sha256:0ab851b45c70d4992be7cdeeee16f97a0b677408c758c4b1efb1cfe8030bfd37", size = 37161, upload-time = "2026-08-17T08:23:32.438Z" }, + { url = "https://files.pythonhosted.org/packages/33/ae/1a641d1d60ba219756d9ebe907ff0ecf4445adcf4fa96f6e3da57b91d439/xxhash-4.0.1-cp315-cp315-win_arm64.whl", hash = "sha256:a5b21b42a01a343096a1c018d35e9b7aec9c7065dda53ae8da071e37478b2cea", size = 34378, upload-time = "2026-08-17T08:36:15.912Z" }, + { url = "https://files.pythonhosted.org/packages/48/7f/7698b320b251806d1249e513922a626f19027e104c829a611272250350eb/xxhash-4.0.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:44ab12e8cd17d4f001769f00ad465208b4bcb897ed29e65f058f74466b57a98f", size = 38610, upload-time = "2026-08-17T08:22:55.203Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3d/436497e775b647b3b3e9a4ffe8c76c59fa4aa7a9fab6447cb59acf1b50ea/xxhash-4.0.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:45e88111ebe331de478ef8d4293efbe88f3cf8b863386c9a2357136b838e1af0", size = 36378, upload-time = "2026-08-17T08:35:36.18Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d8/17a4f8182b9257898aa2a77c2a45f70233eb8e50681a280e8e09d2ee76e9/xxhash-4.0.1-cp315-cp315t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bf430c587f447a554c53768ad76b9846fe7c5632180ef6f69c4fce8b0552fbd0", size = 273559, upload-time = "2026-08-17T08:23:51.075Z" }, + { url = "https://files.pythonhosted.org/packages/83/28/121bd5a5c5adb88e0da772c7bef61964cf9da92956a7a237c7d24c4351b8/xxhash-4.0.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adbd48b30e3f82c89fb2b3e6a87cdd28d113b190a5ed0ee2dee286323ee9a621", size = 299133, upload-time = "2026-08-17T08:36:14.731Z" }, + { url = "https://files.pythonhosted.org/packages/11/8f/57c7b6e04642ed738a0d08a31bed7fc63fdacb661d665f98739cc9751b62/xxhash-4.0.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e71b34978e77868cbf2d18c5206a4603f9c644dd7181bec5643bd40141d3b8c5", size = 314527, upload-time = "2026-08-17T08:22:54.224Z" }, + { url = "https://files.pythonhosted.org/packages/8e/18/42793917dbab0ea1ff71458aea4875e17a7263f2797b798af048dc81e867/xxhash-4.0.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:488ca5c5e28ef56ec4bbb12f835b3f1cbecc5f3510062e70117bc6594851932a", size = 299199, upload-time = "2026-08-17T08:23:36.864Z" }, + { url = "https://files.pythonhosted.org/packages/37/60/51dc92443923d8e908d5614f1145d8d696450f9d6c8f1abe243c6f2a0222/xxhash-4.0.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:421b94f3ba7067958d02e38960d987756347aa150df06df11aa68ae1af78c619", size = 531967, upload-time = "2026-08-17T08:36:18.66Z" }, + { url = "https://files.pythonhosted.org/packages/88/c5/d0de77de09661fac71742c4155b1cd65e274f7cc277819d702b6c8ff2db5/xxhash-4.0.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f33cf0baa91eccd2cb7b62bf00f10c2264ef578b71dd33a12962e71a36eb4d32", size = 278764, upload-time = "2026-08-17T08:22:58.15Z" }, + { url = "https://files.pythonhosted.org/packages/08/9a/589929c655aba1bfb2c41ee03e50eec1547c39c3042a66bda9c173a9614b/xxhash-4.0.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:23a4376b4a3183cb50d4d2a3179f887a7773cc695eb2c908e551bec3221b8c60", size = 359876, upload-time = "2026-08-17T08:35:40.35Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a8/c1d8c94d54d91db2215565f4b4151c1593af3e6d27ac4c00fd1e8d714a02/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:38c3d22129a6958846a3098d68bc8e661704461c0be4793ae28836e4690c8478", size = 293548, upload-time = "2026-08-17T08:23:54.951Z" }, + { url = "https://files.pythonhosted.org/packages/8b/67/85d8abca94508a4dd10561d9dea3e6e68843c6986dd6d9c1b3729c8622e4/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:87cbdec1a7dd930079671a60b249f3ca4e773e6fbd0676e21e36fdc9dd0f3b00", size = 318780, upload-time = "2026-08-17T08:36:17.623Z" }, + { url = "https://files.pythonhosted.org/packages/1b/16/2b920ed456b9cdcfc99ddc20c3afe42f9f807ee5850773c12fd891f3c08d/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:6cbf4e21ef0890804b5bb9ad25c48f9c127758d7f6c66bef374efcacc63c738a", size = 277582, upload-time = "2026-08-17T08:22:57.156Z" }, + { url = "https://files.pythonhosted.org/packages/fa/cc/5811b5997aebb8452047f5800d32fc50eaa29d0ba08d4e426f84450b9c2f/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:c101180495cb4ba3617b279a944345c53a5e73b0c150053d1fa8d8af32de9579", size = 295116, upload-time = "2026-08-17T08:23:40.868Z" }, + { url = "https://files.pythonhosted.org/packages/2d/dc/c2f3f9c2f4d6aadb79f17a9f1c9a7ee82638cc873680da044cf29537d2ee/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:c0e6ccc2b19ec8a726b2e26062ac71ea63e15500d6bf85910e42481844fdffc1", size = 347065, upload-time = "2026-08-17T08:36:21.618Z" }, + { url = "https://files.pythonhosted.org/packages/a2/4c/750cc642c92252e10772ec09e1a1d995581ba4c3ceb24f6e2d57c7ce47ca/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:8bcba9456242ebf180a04d9443812fd85ffe6bd12bda464dd116fcece8886ff3", size = 497208, upload-time = "2026-08-17T08:23:17.88Z" }, + { url = "https://files.pythonhosted.org/packages/6c/2d/58693cb13d6395f39b6b9bb40c5e0db53a5df7c9fce805aa7e792f64a1a5/xxhash-4.0.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:83b8c2013edb5dc1f9e7268b6496130705bc48d79c86bb8817b3d210b81a5513", size = 274338, upload-time = "2026-08-17T08:35:44.062Z" }, + { url = "https://files.pythonhosted.org/packages/4a/08/9aa9787586d9b3e92d63343ce7dc24f0f445fd9e74ff5d6e85dd82233df5/xxhash-4.0.1-cp315-cp315t-win32.whl", hash = "sha256:aa6ccc7f31018484d652cf52db020003433f3c9fa83189c028bd807d2adde503", size = 35471, upload-time = "2026-08-17T08:24:05.795Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ab/4615789c333bee331ac417885c50105715eeb8244bfc68d2bc37dcfd63ca/xxhash-4.0.1-cp315-cp315t-win_amd64.whl", hash = "sha256:daade8936c4deaaf7b01561324ce438ba4f885d717e9adc62b4d67212ad7d7bd", size = 37488, upload-time = "2026-08-17T08:36:19.929Z" }, + { url = "https://files.pythonhosted.org/packages/fb/81/49f718beb0c55d0411bc4bd90b50a3fbe5863a0e97a2f4d11682ba13d298/xxhash-4.0.1-cp315-cp315t-win_arm64.whl", hash = "sha256:f00330ac7e24769e2032203f2b01794d670916b0c1799fd261340f1af9499875", size = 34590, upload-time = "2026-08-17T08:23:19.597Z" }, + { url = "https://files.pythonhosted.org/packages/86/79/9127ff42a887a348dc4ce3211cf1a962836887adee6f57078132bfba78b4/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:ff48915bf1871a1f19f74c11834c6329443d306cedc0c05fe7fe617810422a80", size = 31836, upload-time = "2026-08-17T08:36:28.261Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/f238693bfdd642adb59c99683964d46d9947fe721ff44d3bd850ae675407/xxhash-4.0.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a76345f5aceb4ec404918edf9c7f2b5507db864dc0d7455982009ac0890b57b", size = 34453, upload-time = "2026-08-17T08:23:49.795Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/796ace33cdfb75c91ba6d11615c3bd436355b9f3103e05865bbee9abce57/xxhash-4.0.1-graalpy312-graalpy250_312_native-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:31d86f9e81f3e84e00131ac7c54caf5119ae4ddd82c09c31cff597c813ce1ee2", size = 38488, upload-time = "2026-08-17T08:23:59.901Z" }, + { url = "https://files.pythonhosted.org/packages/ad/23/2d549e5d5d7759eaf9ac2d2d2ab81ff60f1bb2b52cdaae8e5ec5c6524354/xxhash-4.0.1-graalpy312-graalpy250_312_native-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deca2a30d983d240b8375ec2ee0a4288e72042827fc61df2f7671f8467e4cb2f", size = 38206, upload-time = "2026-08-17T08:36:32.193Z" }, + { url = "https://files.pythonhosted.org/packages/79/98/1ee576b27f78e6107ee4ea8ac03e8a52888dff256e57d560f8282c195563/xxhash-4.0.1-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:7c343ee174d417a44d0c3355602c0cbbfa52a04d1bbbf1723378c7d2c8f60626", size = 37127, upload-time = "2026-08-17T08:23:42.705Z" }, + { url = "https://files.pythonhosted.org/packages/ea/4f/e0648288a17d0d1084ca4f7bef206097831988fc86af74aa1dff8f1fbd68/xxhash-4.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:554f87034635bcec47c5d72447bf3db7e02da1bf493a0ada010db28a76f891c6", size = 36333, upload-time = "2026-08-17T08:23:52.913Z" }, + { url = "https://files.pythonhosted.org/packages/22/15/34b7f72e9b5a8bfd7e6178de9e1e342bc3de9f07111a5ae26c00506d9edf/xxhash-4.0.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:3c2445edafc300cc40feb6a25a8356a971c30cd0bf47b5349c2ad74c508343b1", size = 33519, upload-time = "2026-08-17T08:36:03.04Z" }, + { url = "https://files.pythonhosted.org/packages/06/4b/e0af324ccf701bf84a7a060bef11c915d45d9e3c5b9caf5b94d62ecb040b/xxhash-4.0.1-pp311-pypy311_pp73-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bdd16718b63aa3ebd68aabb79021a40e47c81374852d41a306b9453141bbcbee", size = 47995, upload-time = "2026-08-17T08:24:14.335Z" }, + { url = "https://files.pythonhosted.org/packages/59/46/cc7130e6ca6b41ab72eb6a03177b933c7c74145545c99a8610ecc208c449/xxhash-4.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b99ebaf9e816ac5069423b1367ee7e8078fbcebcf62545506bb0608d2f4f468", size = 42725, upload-time = "2026-08-17T08:36:34.144Z" }, + { url = "https://files.pythonhosted.org/packages/1c/24/4f26ff9a7dd0998f6d1036bdddef7ce3e78972a74f7fffa7967e7bc3b7e4/xxhash-4.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f484ed57bb3e4142f9d6439568658c38be5f94b702ba00a1ff32c69783b6c66d", size = 39532, upload-time = "2026-08-17T08:23:57.556Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f3/1ac078fc8fceadcf066469acecacb35d2821cbfaf7d6fc5ac2107c7a314d/xxhash-4.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fac4832b638000106207bc44e44b9616a6a416aaee56c62b01d61f3705e49f58", size = 37252, upload-time = "2026-08-17T08:24:06.652Z" }, ] [[package]] name = "yarl" -version = "1.23.0" +version = "1.24.5" 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, upload-time = "2026-03-01T22:07:53.373Z" } -wheels = [ - { 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, upload-time = "2026-03-01T22:04:42.841Z" }, - { 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, upload-time = "2026-03-01T22:04:44.757Z" }, - { 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, upload-time = "2026-03-01T22:04:46.365Z" }, - { 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, upload-time = "2026-03-01T22:04:47.639Z" }, - { 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, upload-time = "2026-03-01T22:04:48.925Z" }, - { 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, upload-time = "2026-03-01T22:04:50.838Z" }, - { 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, upload-time = "2026-03-01T22:04:52.499Z" }, - { 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, upload-time = "2026-03-01T22:04:55.176Z" }, - { 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, upload-time = "2026-03-01T22:04:56.588Z" }, - { 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, upload-time = "2026-03-01T22:04:58.492Z" }, - { 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, upload-time = "2026-03-01T22:05:00.268Z" }, - { 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, upload-time = "2026-03-01T22:05:02.273Z" }, - { 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, upload-time = "2026-03-01T22:05:03.597Z" }, - { 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, upload-time = "2026-03-01T22:05:04.986Z" }, - { 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, upload-time = "2026-03-01T22:05:06.397Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1c/9a1979aec4a81896d597bcb2177827f2dbee3f5b7cc48b2d0dadb644b41d/yarl-1.23.0-cp311-cp311-win32.whl", hash = "sha256:51430653db848d258336cfa0244427b17d12db63d42603a55f0d4546f50f25b5", size = 82602, upload-time = "2026-03-01T22:05:08.444Z" }, - { url = "https://files.pythonhosted.org/packages/93/22/b85eca6fa2ad9491af48c973e4c8cf6b103a73dbb271fe3346949449fca0/yarl-1.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:bf49a3ae946a87083ef3a34c8f677ae4243f5b824bfc4c69672e72b3d6719d46", size = 87461, upload-time = "2026-03-01T22:05:10.145Z" }, - { url = "https://files.pythonhosted.org/packages/93/95/07e3553fe6f113e6864a20bdc53a78113cda3b9ced8784ee52a52c9f80d8/yarl-1.23.0-cp311-cp311-win_arm64.whl", hash = "sha256:b39cb32a6582750b6cc77bfb3c49c0f8760dc18dc96ec9fb55fbb0f04e08b928", size = 82336, upload-time = "2026-03-01T22:05:11.554Z" }, - { 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, upload-time = "2026-03-01T22:05:12.897Z" }, - { 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, upload-time = "2026-03-01T22:05:14.376Z" }, - { 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, upload-time = "2026-03-01T22:05:15.71Z" }, - { 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, upload-time = "2026-03-01T22:05:17.384Z" }, - { 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, upload-time = "2026-03-01T22:05:18.804Z" }, - { 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, upload-time = "2026-03-01T22:05:20.235Z" }, - { 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, upload-time = "2026-03-01T22:05:22.268Z" }, - { 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, upload-time = "2026-03-01T22:05:23.638Z" }, - { 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, upload-time = "2026-03-01T22:05:25.749Z" }, - { 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, upload-time = "2026-03-01T22:05:27.318Z" }, - { 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, upload-time = "2026-03-01T22:05:28.738Z" }, - { 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, upload-time = "2026-03-01T22:05:30.438Z" }, - { 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, upload-time = "2026-03-01T22:05:31.835Z" }, - { 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, upload-time = "2026-03-01T22:05:33.358Z" }, - { 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, upload-time = "2026-03-01T22:05:35.4Z" }, - { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, - { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, - { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, - { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, - { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, - { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, - { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, - { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, - { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, - { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, - { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, - { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, - { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, - { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, - { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, - { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, - { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, - { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, - { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, - { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, - { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, - { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, - { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, - { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, - { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, - { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, - { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, - { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, - { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, - { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, - { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, - { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, - { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, - { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, - { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, - { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, - { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, - { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, - { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, - { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, - { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, - { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, - { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, - { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, - { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, - { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, - { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, - { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, - { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, - { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, - { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, - { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, - { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, - { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, - { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, - { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, - { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, - { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, - { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, - { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, - { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, - { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, - { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, - { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, - { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, - { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, - { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, - { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, - { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, - { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, - { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, - { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/31/33/ebe9e3d1f86c7a0b51094c0a146392045ca1631d2664889539dec8088a33/yarl-1.24.5.tar.gz", hash = "sha256:e81b83143bee16329c23db3c1b2d82b29892fcbcb849186d2f6e98a5abe9a57f", size = 228679, upload-time = "2026-07-20T02:07:45.435Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/db/3cb5df059756a45761cc3dee8fd25ec82b83a6585ea3542b969fda850f99/yarl-1.24.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2c1fe720934a16ea8e7146175cba2126f87f54912c8c5435e7f7c7a51ef808d3", size = 135043, upload-time = "2026-07-20T02:04:52.39Z" }, + { url = "https://files.pythonhosted.org/packages/44/f8/767d6bd5a03db63bc467df2fb56d6fafeae9667d74aea92cd6af399f828b/yarl-1.24.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c687ed078e145f5fd53a14854beff320e1d2ab76df03e2009c98f39a0f68f39a", size = 96942, upload-time = "2026-07-20T02:04:54.26Z" }, + { url = "https://files.pythonhosted.org/packages/ce/97/10b939c44d7b28d1dbc389cfc7012306d1ea8dba01eaef44b39fffaee52a/yarl-1.24.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:709f1efed56c4a145793c046cd4939f9959bcd818979a787b77d8e09c57a0840", size = 97046, upload-time = "2026-07-20T02:04:56.638Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7a/b410dbe39b6255c55fb2a2bcee96eb844d0789235ddc381a889a90dc72d6/yarl-1.24.5-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:874019bd513008b009f58657134e5d0c5e030b3559bd0553976837adf52fe966", size = 110512, upload-time = "2026-07-20T02:04:58.955Z" }, + { url = "https://files.pythonhosted.org/packages/83/c7/da591971f78a5617e1f21f5699858ebccd836fe181a6493788ffc91ba69b/yarl-1.24.5-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a4582acf7ef76482f6f511ebaf1946dae7f2e85ec4728b81a678c01df63bd723", size = 102454, upload-time = "2026-07-20T02:05:00.623Z" }, + { url = "https://files.pythonhosted.org/packages/c4/8e/73b0ed4de47289a78a96045d76d1cfe5e41848bf0da59ce25b2ec87ee05d/yarl-1.24.5-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2cabe6546e41dabe439999a23fcb5246e0c3b595b4315b96ef755252be90caeb", size = 117617, upload-time = "2026-07-20T02:05:02.325Z" }, + { url = "https://files.pythonhosted.org/packages/cf/14/b744747bc4f57a8d55bd744df463457524583e1e9f7538b5ace0346ab92e/yarl-1.24.5-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:17f57620f5475b3c69109376cc87e42a7af5db13c9398e4292772a706ff10780", size = 116135, upload-time = "2026-07-20T02:05:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/66/ca/95aa4d0e5b7ea4f20e4d577c42d001ed9df207569fdb063cc5ed4ebb496b/yarl-1.24.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:570fec8fbd22b032733625f03f10b7ff023bc399213db15e72a7acaef28c2f4e", size = 111935, upload-time = "2026-07-20T02:05:05.738Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/d2ad8d6b147832d177a4e720ba1962fe686eb0913b74503b3eca094b8bba/yarl-1.24.5-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5fede79c6f73ff2c3ef822864cb1ada23196e62756df53bc6231d351a49516a2", size = 110010, upload-time = "2026-07-20T02:05:07.471Z" }, + { url = "https://files.pythonhosted.org/packages/50/18/eb335e4120903903f4865041355ae46256a2406eb2865bc24827f4f27b61/yarl-1.24.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8ccf9aca873b767977c73df497a85dbedee4ee086ae9ae49dc461333b9b79f58", size = 110058, upload-time = "2026-07-20T02:05:09.246Z" }, + { url = "https://files.pythonhosted.org/packages/44/70/97353add32c62ad6f206d948ac5a5ee84398225e534dc6ed6433d1b335b6/yarl-1.24.5-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:ad5d8201d310b031e6cd839d9bac2d4e5a01533ce5d3d5b50b7de1ef3af1de61", size = 103308, upload-time = "2026-07-20T02:05:11.31Z" }, + { url = "https://files.pythonhosted.org/packages/68/39/5e7398d4b6f6b3c9062823ebc60802df5b272e3fe9e788f9734c6ee46c85/yarl-1.24.5-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:841f0852f48fefea3b12c9dfec00704dfa3aef5215d0e3ce564bb3d7cd8d57c6", size = 116898, upload-time = "2026-07-20T02:05:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/09e52f2239e8b96357eccca05915382e4ba5405ebfb623b6036040d99654/yarl-1.24.5-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:9baafc71b04f8f4bb0703b21d6fc9f0c30b346c636a532ff16ec8491a5ea4b1f", size = 109400, upload-time = "2026-07-20T02:05:14.821Z" }, + { url = "https://files.pythonhosted.org/packages/4b/6a/e94133d4c2d1a14d2384310bf3e79d9cf32c9d1eae1c6f034fb80d098fa1/yarl-1.24.5-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d897129df1a22b12aeed2c2c98df0785a2e8e6e0bde87b389491d0025c187077", size = 115934, upload-time = "2026-07-20T02:05:17.78Z" }, + { url = "https://files.pythonhosted.org/packages/4e/3c/34955ed967b976fc38edcbb6d538dee79dbda4cb7fc7f72a0907a7c78e0f/yarl-1.24.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dd625535328fd9882374356269227670189adfcc6a2d90284f323c05862eecbd", size = 112178, upload-time = "2026-07-20T02:05:19.675Z" }, + { url = "https://files.pythonhosted.org/packages/f5/46/d7bd3a8859d47dcfaffd7127af7076032a7da278a9a02e17b5f37bfb6712/yarl-1.24.5-cp311-cp311-win_amd64.whl", hash = "sha256:f4239bbec5a3577ddb49e4b50aeb32d8e5792098262ae2f63723f916a29b1a25", size = 97544, upload-time = "2026-07-20T02:05:21.523Z" }, + { url = "https://files.pythonhosted.org/packages/01/69/c1bfd21e32c638974ea2c542a0b8c53ef1fa9eff336020f5d014f9503ff2/yarl-1.24.5-cp311-cp311-win_arm64.whl", hash = "sha256:3ac6aff147deb9c09461b2d4bbdf6256831198f5d8a23f5d37138213090b6d8a", size = 93359, upload-time = "2026-07-20T02:05:23.493Z" }, + { url = "https://files.pythonhosted.org/packages/1b/84/71d051c850b5af41d168c679d9eb67eb7c55283ac4ee131673edf134bc4e/yarl-1.24.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d693396e5aea78db03decd60aec9ece16c9b40ba00a587f089615ff4e718a81d", size = 136035, upload-time = "2026-07-20T02:05:25.489Z" }, + { url = "https://files.pythonhosted.org/packages/03/4d/8ad27f9a1b7e69313cca5d695b925b48efe51208d3490e0844bae97cabc0/yarl-1.24.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3363fcc96e665878946ad7a106b9a13eac0541766a690ef287c0232ac768b6ec", size = 97642, upload-time = "2026-07-20T02:05:27.429Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b4/05b4131c407006cd1e410e9c6539f16a0945724677e5364447313c15ea3e/yarl-1.24.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9d399bdcfb4a0f659b9b3788bbc89babe63d9a6a65aacdf4d4e7065ff2e6316c", size = 97323, upload-time = "2026-07-20T02:05:29.441Z" }, + { url = "https://files.pythonhosted.org/packages/20/16/e618c875c73e0e39611f20a581b3d5e8d59b8857bf001bee3263044c6deb/yarl-1.24.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90333fd89b43c0d08ac85f3f1447593fc2c66de18c3d6378d7125ea118dc7a54", size = 107741, upload-time = "2026-07-20T02:05:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/c4defeaf3ed33fcb346aacf9c6e971a8d4e2bde04a0310e79abb208e7965/yarl-1.24.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:665b0a2c463cc9423dd647e0bfd9f4ccc9b50f768c55304d5e9f80b177c1de12", size = 103570, upload-time = "2026-07-20T02:05:33.303Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e7/0e0e0de5865ebd5914537ef486f36c727a59865c3ac0cf5ff1b32aececbf/yarl-1.24.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e006d3a974c4ee19512e5f058abedb6eef36a5e553c14812bdeba1758d812e6d", size = 115815, upload-time = "2026-07-20T02:05:35.292Z" }, + { url = "https://files.pythonhosted.org/packages/2b/27/ca56b700cb170aba25a3893b75355b213935657dc5714d2383354a270e62/yarl-1.24.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e7d42c531243450ef0d4d9c172e7ed6ef052640f195629065041b5add4e058d1", size = 116025, upload-time = "2026-07-20T02:05:37.503Z" }, + { url = "https://files.pythonhosted.org/packages/d6/d0/d56c859b8222116f5d68459199f48359e0bf121b6f65a69bf329b3602ba0/yarl-1.24.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f08c7513ecef5aad65687bfdf6bc601ae9fccd04a42904501f8f7141abad9eb9", size = 109835, upload-time = "2026-07-20T02:05:39.506Z" }, + { url = "https://files.pythonhosted.org/packages/70/a2/3a35557e4d1a79425040eba202ccaf08bdc8717680fc77e2498a1ad2e0a5/yarl-1.24.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95b17fe34ed802f17e205112e6e10db92275c34fee290aa9bdc55a9c724027", size = 108884, upload-time = "2026-07-20T02:05:41.584Z" }, + { url = "https://files.pythonhosted.org/packages/e4/35/ef4c26356b7913c68983bac2d72a4212b3347af551cb8d250b99b5ed7b7f/yarl-1.24.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56b149b22de33b23b0c6077ab9518c6dcb538ad462e1830e68d06591ccf6e38b", size = 107308, upload-time = "2026-07-20T02:05:43.697Z" }, + { url = "https://files.pythonhosted.org/packages/d5/91/ff0dc66c2ccf3e0153ab97ff61eabab4400e6a5264af427ab30cd69f1857/yarl-1.24.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:a8fe66b8f300da93798025a785a5b90b42f3810dc2b72283ff84a41aaaebc293", size = 103646, upload-time = "2026-07-20T02:05:45.895Z" }, + { url = "https://files.pythonhosted.org/packages/74/f0/33b9271c7f881766359d58266fa0811d2e5210ed860e28da7dc6d7786344/yarl-1.24.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:377fe3732edbaf78ee74efdf2c9f49f6e99f20e7f9d2649fda3eb4badd77d76e", size = 115305, upload-time = "2026-07-20T02:05:47.832Z" }, + { url = "https://files.pythonhosted.org/packages/ef/65/fd79fb1868c4a80db8661091de525bf430f63c3bea1b20e8b6a84fc7d359/yarl-1.24.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e8ffa78582120024f476a611d7befc123cee59e47e8309d470cf667d806e613b", size = 108404, upload-time = "2026-07-20T02:05:49.604Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ba/dbabe6b262f17a816c70cfc09558dbf03ece3ec76684d02f911a3d3a189c/yarl-1.24.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:daba5e594f06114e37db186efd2dd916609071e59daca901a0a2e71f02b142ce", size = 115940, upload-time = "2026-07-20T02:05:51.741Z" }, + { url = "https://files.pythonhosted.org/packages/a5/43/fab2d1dad9d340a268cdde63756a123d069723efff6a372d123fa74a9517/yarl-1.24.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:65be18ec59496c13908f02a2472751d9ef840b4f3fb5726f129306bf6a2a7bba", size = 110006, upload-time = "2026-07-20T02:05:53.554Z" }, + { url = "https://files.pythonhosted.org/packages/c4/27/41eb51bbd1b8d89546b83897cfb0164f1e109304fd408dbb151b639eec0f/yarl-1.24.5-cp312-cp312-win_amd64.whl", hash = "sha256:a929d878fec099030c292803b31e5d5540a7b6a31e6a3cc76cb4685fc2a2f51b", size = 97618, upload-time = "2026-07-20T02:05:55.57Z" }, + { url = "https://files.pythonhosted.org/packages/3c/25/b2553764b3d65db711d8f45416351ec4f420847558eb669edcbcaadf5780/yarl-1.24.5-cp312-cp312-win_arm64.whl", hash = "sha256:7ce27823052e2013b597e0c738b13e7e36b8ccb9400df8959417b052ab0fd92c", size = 93018, upload-time = "2026-07-20T02:05:57.554Z" }, + { url = "https://files.pythonhosted.org/packages/e1/63/64ef361967cc983573149dc1515d531db5da8a4c92d22bb833d59e01b313/yarl-1.24.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:79af890482fc94648e8cde4c68620378f7fef60932710fa17a66abc039244da2", size = 135075, upload-time = "2026-07-20T02:05:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/bb/89/55920fd853ce43e608adbc3962456f0d649d6bb15250dc2988321da0fe1c/yarl-1.24.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46c2f213e23a04b93a392942d782eb9e413e6ef6bf7c8c53884e599a5c174dcb", size = 97225, upload-time = "2026-07-20T02:06:01.769Z" }, + { url = "https://files.pythonhosted.org/packages/15/f0/7688d3f2cfff7590df2af38ec46d969f4281a4dddb08a9ad2eafbcdddf98/yarl-1.24.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:92ab3e11448f2ff7bf53c5a26eff0edc086898ec8b21fb154b85839ce1d88075", size = 96751, upload-time = "2026-07-20T02:06:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/a851a0f94aaaf379dd4f901bfc80f634280bec51eb260b47363e2a4cd62e/yarl-1.24.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ebb0ec7f17803063d5aeb982f3b1bd2b2f4e4fae6751226cbd6ba1fcfe9e63ff", size = 107960, upload-time = "2026-07-20T02:06:05.699Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a8/faea066c12f9c77ca0de90641f1655f9dd7b412477bf28c76d692f3aecff/yarl-1.24.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:82632daed195dcc8ea664e8556dc9bdbd671960fb3776bd92806ce05792c2448", size = 103500, upload-time = "2026-07-20T02:06:07.556Z" }, + { url = "https://files.pythonhosted.org/packages/fb/9c/1e67084c2a6e2f2db0e3be798328cb3be42c0119b621d25461479a224d21/yarl-1.24.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:53e549287ef628fecba270045c9701b0c564563a9b0577d24a4ec75b8ab8040f", size = 115780, upload-time = "2026-07-20T02:06:09.599Z" }, + { url = "https://files.pythonhosted.org/packages/58/86/1f94664e147474337e3359f52012cf3d02f825f694317b178bfba1078c62/yarl-1.24.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fcd3b77e2f17bbe4ca56ec7bcb07992647d19d0b9c05d84886dcd6f9eb810afd", size = 115308, upload-time = "2026-07-20T02:06:11.352Z" }, + { url = "https://files.pythonhosted.org/packages/0a/43/8e55ae7538ba5f28ccb3c845c6dd4549cf7016d5992e5326512519107cdd/yarl-1.24.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d46b86567dd4e248c6c159fcbcdcce01e0a5c8a7cd2334a0fff759d0fa075b16", size = 110574, upload-time = "2026-07-20T02:06:13.129Z" }, + { url = "https://files.pythonhosted.org/packages/ce/ba/a889ec8765cedcf2ac44dcb02d6a21e4861399b243b263c5f2dde27ee740/yarl-1.24.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7f72c74aa99359e27a2ee8d6613fefa28b5f76a983c083074dfc2aaa4ab46213", size = 109914, upload-time = "2026-07-20T02:06:15.243Z" }, + { url = "https://files.pythonhosted.org/packages/9c/c3/e45f821af67b791c2dbbe4a9f4137a1d33f8d386654a05a0c3f47bdfa25d/yarl-1.24.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3f45789ce415a7ec0820dc4f82925f9b5f7732070be1dec1f5f23ec381435a24", size = 107712, upload-time = "2026-07-20T02:06:17.443Z" }, + { url = "https://files.pythonhosted.org/packages/02/00/2ab0f42c9857fcb490bfaa6647b14540b53d241ab209f23220b958cc5832/yarl-1.24.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6e73e7fe93f17a7b191f52ec9da9dd8c06a8fe735a1ecbd13b97d1c723bff385", size = 104251, upload-time = "2026-07-20T02:06:19.259Z" }, + { url = "https://files.pythonhosted.org/packages/7a/70/709d9a286e98af2c7fd8e4e6cada658b5c0e30d87dd7e2a63c2fb5767217/yarl-1.24.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4a36f9becdd4c5c52a20c3e9484128b070b1dcfc8944c006f3a528295a359a9c", size = 115319, upload-time = "2026-07-20T02:06:21.207Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6c/3eaa515142991fe84cfc483ff986492211f1978f90161ccefdbec919d09b/yarl-1.24.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:7bcbe0fcf850eae67b6b01749815a4f7161c560a844c769ad7b48fcd99f791c4", size = 109163, upload-time = "2026-07-20T02:06:23.006Z" }, + { url = "https://files.pythonhosted.org/packages/bb/64/711dafce66c323a3144d470547a71c5384c57623308ac8bb5e4b903ac148/yarl-1.24.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:24e861e9630e0daddcb9191fb187f60f034e17a4426f8101279f0c475cd74144", size = 115435, upload-time = "2026-07-20T02:06:24.923Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f3/9b9d0e6d84bea851eb1ba99e4bdc755b86fd813e49ec86dfe42f26befdef/yarl-1.24.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9335a099ad87287c37fe5d1a982ff392fa5efe5d14b40a730b1ec1d6a41382b4", size = 110691, upload-time = "2026-07-20T02:06:26.973Z" }, + { url = "https://files.pythonhosted.org/packages/86/e4/62a06b7e87c4246ac76b7c2da136f972eb4a3a1fc94abb07e7022d6fdb0a/yarl-1.24.5-cp313-cp313-win_amd64.whl", hash = "sha256:2dbe06fc16bc91502bca713704022182e5729861ae00277c3a23354b40929740", size = 97454, upload-time = "2026-07-20T02:06:29.163Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c9/5fc8025b318ab10db413b61056bd0d95c557a70e8df4210c7511f866329c/yarl-1.24.5-cp313-cp313-win_arm64.whl", hash = "sha256:6b8536851f9f65e7f00c7a1d49ba7f2be0ffe2c11555367fc9f50d9f842410a1", size = 92813, upload-time = "2026-07-20T02:06:31.113Z" }, + { url = "https://files.pythonhosted.org/packages/a9/08/5f3085fef9564217074db9dd8573de1795bc82cde61a7ad10b6a7234a569/yarl-1.24.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:2729fcfc4f6a596fb0c50f32090400aa9367774ac296a00387e65098c0befa76", size = 135680, upload-time = "2026-07-20T02:06:33.273Z" }, + { url = "https://files.pythonhosted.org/packages/98/35/ba9436e579bd48a8801f2021d842d9ab4994c26e4c7dd3a4c1f1bcb57a9e/yarl-1.24.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ff330d3c30db4eb6b01d79e29d2d0b407a7ecad39cfd9ec993ece57396a2ec0d", size = 97395, upload-time = "2026-07-20T02:06:35.259Z" }, + { url = "https://files.pythonhosted.org/packages/18/a9/a07f76f3c44e02b25cc743af5ef93eef27f7013eadca770451b6a6ccb5db/yarl-1.24.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e42d75862735da90e7fc5a7b23db0c976f737113a54b3c9777a9b665e9cbff75", size = 97223, upload-time = "2026-07-20T02:06:37.216Z" }, + { url = "https://files.pythonhosted.org/packages/77/f7/a9a1d6fa7dd9e388f95b30f6ad3ec4e285f6c8f61f44ce16070c3fcfe414/yarl-1.24.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3732e66413163e72508da9eff9ce9d2846fde51fae45d3605393d3e6cd303e9", size = 108777, upload-time = "2026-07-20T02:06:39.292Z" }, + { url = "https://files.pythonhosted.org/packages/2f/44/e0b86c302471fabd6f02808ecf2ac52b8412b624787849d4bf2cdb466f6f/yarl-1.24.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b8ee53be440a0cffc991a27be3057e0530122548dbe7c0892df08822fce5ede", size = 103119, upload-time = "2026-07-20T02:06:41.456Z" }, + { url = "https://files.pythonhosted.org/packages/d1/16/9c16d180bf8faaf223225eb50e1245870ff1ae0e302a27153988e65c51fd/yarl-1.24.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:af3aefa655adb5869491fa907e652290386800ae99cc50095cba71e2c6aefdca", size = 116471, upload-time = "2026-07-20T02:06:43.696Z" }, + { url = "https://files.pythonhosted.org/packages/d2/8d/b219b9df28a02ce95cfbdd41d2f7caa5669d0ff979c1c9975697145e33c5/yarl-1.24.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2120b96872df4a117cde97d270bac96aea7cc52205d305cf4611df694a487027", size = 115974, upload-time = "2026-07-20T02:06:45.874Z" }, + { url = "https://files.pythonhosted.org/packages/9b/e8/f20557aca240d88e69850ad1ee91756821d094bb1310565c04d25c6682a2/yarl-1.24.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:66410eb6345d467151934b49bfa70fb32f5b35a6140baa40ad97d6436abea2e9", size = 110830, upload-time = "2026-07-20T02:06:47.852Z" }, + { url = "https://files.pythonhosted.org/packages/db/18/199b85109a53eeca64ee19c9cca228287e8e4ab0cc1a09b28f530e65cce0/yarl-1.24.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4af7b7e1be0a69bee8210735fe6dcfc38879adfac6d62e789d53ba432d1ffa41", size = 110054, upload-time = "2026-07-20T02:06:49.84Z" }, + { url = "https://files.pythonhosted.org/packages/aa/2f/ed28147f8cd7f48c49367c90713b30a555284b6105a6a56f3a05568da795/yarl-1.24.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa139875ff98ab97da323cfadfaff08900d1ad42f1b5087b0b812a55c5a06373", size = 108312, upload-time = "2026-07-20T02:06:51.835Z" }, + { url = "https://files.pythonhosted.org/packages/c5/c5/55e16ae0a5c227cea8df1c6871ba57d614a34243146c05729caf2a1bd9c5/yarl-1.24.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0055afc45e864b92729ac7600e2d102c17bef060647e74bca75fa84d66b9ff36", size = 103662, upload-time = "2026-07-20T02:06:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ea/dbd7c2caec459c9a426f18b02688ecbfb58620d0f6a3422d24769fbaf8ab/yarl-1.24.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f0e466ed7511fe9d459a819edbc6c2585c0b6eabde9fa8a8947552468a7a6ef0", size = 116090, upload-time = "2026-07-20T02:06:56.015Z" }, + { url = "https://files.pythonhosted.org/packages/06/84/39ce4ce3059e07fece5fbdbee8c4053406af9aca911ce9fa5f8548aab6af/yarl-1.24.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f141474e85b7e54998ec5180530a7cda99ab29e282fa50e0756d89981a9b43c5", size = 109523, upload-time = "2026-07-20T02:06:57.926Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8b/71ff44137b405c64a7788075669c24010019f57a7464b78c3a6cbee539d9/yarl-1.24.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:e2935f8c39e3b03e83519292d78f075189978f3f4adc15a78144c7c8e2a1cba5", size = 116084, upload-time = "2026-07-20T02:06:59.868Z" }, + { url = "https://files.pythonhosted.org/packages/62/c0/423078fdd4042e1862c11f0ffd977a0ffa393783c12bee94685923bc189e/yarl-1.24.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9d1216a7f6f77836617dba35687c5b78a4170afc3c3f18fc788f785ba26565c4", size = 111006, upload-time = "2026-07-20T02:07:01.907Z" }, + { url = "https://files.pythonhosted.org/packages/cf/52/6daa2ee9d95e5c98b8128f8df91eb692eb423ab274b8cf08db52152fad26/yarl-1.24.5-cp314-cp314-win_amd64.whl", hash = "sha256:5ba4f78df2bcc19f764a4b26a8a4f5049c110090ad5825993aacb052bf8003ad", size = 99215, upload-time = "2026-07-20T02:07:03.852Z" }, + { url = "https://files.pythonhosted.org/packages/ec/0e/464a847d7359e0da75dd9fc5c1d1aa35d0159ea31e5f8e66a3c1c29ff3d0/yarl-1.24.5-cp314-cp314-win_arm64.whl", hash = "sha256:9e4e16c73d717c5cf27626c524d0a2e261ad20e46932b2670f64ad5dde23e26f", size = 94566, upload-time = "2026-07-20T02:07:06.074Z" }, + { url = "https://files.pythonhosted.org/packages/e2/55/e03acc4446772660bc335e86e41ef31e4d0d838fd641531a11a5ee33b493/yarl-1.24.5-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:e1ae548a9d901adca07899a4147a7c826bbcc06239d3ce9a59f57886a28a4c88", size = 142533, upload-time = "2026-07-20T02:07:08.284Z" }, + { url = "https://files.pythonhosted.org/packages/ae/71/4acd3a1fc7cf14345cdb302665ecd2097f62c365b4f14ca17d4f37775cf9/yarl-1.24.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ff405d91509d88e8d44129cd87b18d70acd1f0c1aeabd7bc3c46792b1fe2acba", size = 100776, upload-time = "2026-07-20T02:07:10.197Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/cfb76b7fe99686db264bff829779a539d923e7564ffd7ef18da6c54c3774/yarl-1.24.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:47e98aab9d8d82ff682e7b0b5dded33bf138a32b817fcf7fa3b27b2d7c412928", size = 100913, upload-time = "2026-07-20T02:07:12.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/3f/7116e782992abbd4fb6948488aec72078895e929a23078290739e8396fce/yarl-1.24.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0a658a6d3fafee5c6f63c58f3e785c8c43c93fbc02bf9f2b6663f8185e0971f", size = 106507, upload-time = "2026-07-20T02:07:14.173Z" }, + { url = "https://files.pythonhosted.org/packages/33/90/d4d2d73ee78229cc889872eb8e085d8f5c6f51abdb178409fd9b23cf74fd/yarl-1.24.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4377407001ca3c057773f44d8ddd6358fa5f691407c1ba92210bd3cf8d9e4c95", size = 99219, upload-time = "2026-07-20T02:07:16.019Z" }, + { url = "https://files.pythonhosted.org/packages/3e/fa/a6df1a9bccd644eec00abee0dff4277416222cec435330fd1f2858523ec1/yarl-1.24.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7c0494a31a1ac5461a226e7947a9c9b78c44e1dc7185164fa7e9651557a5d9bc", size = 111804, upload-time = "2026-07-20T02:07:18.141Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/7b2a1f4bcc20e9447156dd2b1c4d01f70d9df0759025ee7d09a84ffae134/yarl-1.24.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a7cff474ab7cd149765bb784cf6d78b32e18e20473fb7bda860bce98ab58e9da", size = 110943, upload-time = "2026-07-20T02:07:20.06Z" }, + { url = "https://files.pythonhosted.org/packages/08/ff/22c92affb0f9b623ca753d27d968b5625b868f12c6378d049d55ae247643/yarl-1.24.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cbb833ccacdb5519eff9b8b71ee618cc2801c878e77e288775d77c3a2ced858a", size = 108251, upload-time = "2026-07-20T02:07:22.217Z" }, + { url = "https://files.pythonhosted.org/packages/45/44/5769b96298c1e195fb412997b6090af2a84105cf59c17613558a2d011d1f/yarl-1.24.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:82f75e05912e84b7a0fe57075d9c59de3cb352b928330f2eb69b2e1f54c3e1f0", size = 106025, upload-time = "2026-07-20T02:07:24.083Z" }, + { url = "https://files.pythonhosted.org/packages/4c/40/009e8e791fd9762c0e1567e69248acb4f49064597e1680874c16dd8bb798/yarl-1.24.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:16a2f5010280020e90f5330257e6944bc33e73593b136cc5a241e6c1dc292498", size = 106573, upload-time = "2026-07-20T02:07:26.248Z" }, + { url = "https://files.pythonhosted.org/packages/20/c6/b7480578f8a0a80946f36ad6df547ecec704f9ba69d2de60f8aa6f1c1cbf/yarl-1.24.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ffcd54362564dc1a30fb74d8b8a6e5a6b11ebd5e27266adc3b7427a21a6c9104", size = 100751, upload-time = "2026-07-20T02:07:28.098Z" }, + { url = "https://files.pythonhosted.org/packages/d4/27/4476f3360b91a48c5cf125e91f59a3bd35299d84a431a258d57f5977bb11/yarl-1.24.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0465ec8cedc2349b97a6b595ace64084a50c6e839eca40aa0626f38b8350e331", size = 111643, upload-time = "2026-07-20T02:07:30.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/5cdd3e5ee944e8af31e52f6cd3d3af5fd7b937e036ccbbba2c9ffebede95/yarl-1.24.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4db9aecb141cb7a5447171b57aa1ed3a8fee06af40b992ffc31206c0b0121550", size = 106312, upload-time = "2026-07-20T02:07:33.06Z" }, + { url = "https://files.pythonhosted.org/packages/18/86/f406b0c2a6f99575de2da671ef47aa06f89a5be83a27a46971c3b86cecdb/yarl-1.24.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:f540c013589084679a6c7fac07096b10159737918174f5dfc5e11bf5bca4dfe6", size = 110379, upload-time = "2026-07-20T02:07:35.155Z" }, + { url = "https://files.pythonhosted.org/packages/f0/6c/9f3adfbd3b30b4fa0f7ccb3a83eba2c1152d3fff554d535e640ba0f7ba2b/yarl-1.24.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a61834fb15d81322d872eaafd333838ae7c9cea84067f232656f75965933d047", size = 108497, upload-time = "2026-07-20T02:07:37.35Z" }, + { url = "https://files.pythonhosted.org/packages/dd/37/91eb2e5ca883a529c1b390348a74cd9fc0512171727f547ce70bfe02be5c/yarl-1.24.5-cp314-cp314t-win_amd64.whl", hash = "sha256:5c88e5815a49d289e599f3513aa7fde0bc2092ff188f99c940f007f90f53d104", size = 102450, upload-time = "2026-07-20T02:07:39.578Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f4/ed5c402ac8fde4403ed3366c2716bfddc8a6677ebd59f3d62772cc7fe468/yarl-1.24.5-cp314-cp314t-win_arm64.whl", hash = "sha256:cf139c02f5f23ef6532040a30ff662c00a318c952334f211046b8e60b7f17688", size = 97222, upload-time = "2026-07-20T02:07:41.55Z" }, + { url = "https://files.pythonhosted.org/packages/61/02/962c1cbfc401a30c1d034dc67ff395f64b52302c6d62de556c1fca99acc0/yarl-1.24.5-py3-none-any.whl", hash = "sha256:a33700d13d9b7d84fd10947b09ff69fb9a792e519c8cb9764a3ca70baa6c23a7", size = 58612, upload-time = "2026-07-20T02:07:43.461Z" }, ] [[package]]