feat: use each user's local NocoBase login

This commit is contained in:
nac
2026-08-14 11:33:14 +00:00
parent 76e4cbf58a
commit 51d1493a2f
20 changed files with 1561 additions and 315 deletions
+40
View File
@@ -0,0 +1,40 @@
name: CI
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
version: "0.8.8"
enable-cache: true
- name: Set up Python
run: uv python install 3.12
- name: Verify lockfile
run: uv lock --check
- name: Install locked dependencies
run: uv sync --locked --group dev --python 3.12
- name: Lint and formatting
run: |
uv run ruff check nocobase_crm_mcp scripts tests
uv run ruff format --check nocobase_crm_mcp scripts tests
- name: Type check
run: uv run mypy
- name: Compile package
run: uv run --python 3.12 python -m compileall -q nocobase_crm_mcp scripts
- name: Run tests
run: uv run --python 3.12 python -m unittest discover -s tests -v
- name: Build distribution
run: uv build
- name: Audit dependencies
run: uv run pip-audit
+42 -30
View File
@@ -1,12 +1,24 @@
# NocoBase CRM MCP # NocoBase CRM MCP
MCP server for remote agents that need customer, contact, follow-up, CRM overview, and tightly scoped project write access in NocoBase. Local MCP server for end users who need customer, contact, follow-up, CRM overview, and tightly scoped project write access in NocoBase.
The default stdio server remains read-only. Its managed HTTP mode adds only fixed-schema project create/update tools and authenticated attachment uploads; it never exposes arbitrary collections, fields, commands, or delete operations. Each user runs this MCP on their own computer and signs in to the remote NocoBase environment with `nb` CLI. Every query and write therefore uses that user's NocoBase identity and ACL. The project does not require a shared remote MCP service or a shared NocoBase service account.
The default stdio server is read-only. Add `--enable-writes` for fixed-schema project create/update tools. Optional local HTTP mode also provides authenticated attachment uploads. Neither mode exposes arbitrary collections, fields, commands, or delete operations.
## Configure ## Configure
This repository already includes a local `config.json` generated for the current CRM. To regenerate it from the example: First save the remote NocoBase environment and sign in with your own account:
```bash
nb env add nb-mcp --api-base-url https://nocobase.example/api --auth-type oauth
nb env auth nb-mcp
nb env info nb-mcp
```
`nb env auth` opens the NocoBase login flow in the user's browser. Do not distribute an API key or copy another user's `nb` configuration directory. The MCP invokes `nb --env nb-mcp`, so NocoBase reapplies the logged-in user's roles, row-level filters, and field permissions to every operation.
This repository includes a `config.json` generated for the current CRM. To regenerate it from the example:
```bash ```bash
cp config.example.json config.json cp config.example.json config.json
@@ -45,6 +57,12 @@ export NOCOBASE_MCP_CONFIG=/home/nac/repo/nocobase-mcp/config.json
uv run nocobase-crm-mcp --config config.json uv run nocobase-crm-mcp --config config.json
``` ```
To expose the two fixed project write tools using the same user's CLI login:
```bash
uv run nocobase-crm-mcp --config config.json --enable-writes
```
Exposed tools: Exposed tools:
- `crm_health` - `crm_health`
@@ -79,6 +97,8 @@ Use `crm_filter_options` to discover status/source values and owner candidates b
List tools clamp `limit` to `1..100`, normalize negative offsets to `0`, and implement exact arbitrary offsets even when an offset is not divisible by the limit. Responses include `has_more` and `next_offset`. Compact date-time values use `Asia/Shanghai` and include an explicit `+08:00` offset; date-only project fields stay unchanged. List tools clamp `limit` to `1..100`, normalize negative offsets to `0`, and implement exact arbitrary offsets even when an offset is not divisible by the limit. Responses include `has_more` and `next_offset`. Compact date-time values use `Asia/Shanghai` and include an explicit `+08:00` offset; date-only project fields stay unchanged.
Nested bounded lists are explicit as well: `get_customer` includes `contacts_pagination` and `recent_followups_pagination`, while `get_project` includes `followups_pagination`. Large analytical scans return a `truncation` object with `total`, `returned`, `limit`, and `next_offset`; aggregation queries fail with a narrowing hint instead of silently discarding buckets. Team member resolution paginates through all related users rather than stopping at 100.
## Response shapes ## Response shapes
Every tool returns readable keys, never NocoBase field codes. Relations always Every tool returns readable keys, never NocoBase field codes. Relations always
@@ -88,6 +108,8 @@ detail. Keys whose value is empty are omitted — **an absent key means empty, n
unknown**. Pass `include_raw=true` on any of these tools when the original unknown**. Pass `include_raw=true` on any of these tools when the original
NocoBase row is genuinely needed; it is then attached under `raw`. NocoBase row is genuinely needed; it is then attached under `raw`.
Raw rows are a separate data-exposure capability and are disabled at the MCP boundary by default. An administrator must explicitly set `NOCOBASE_MCP_ENABLE_RAW_DATA=1` on that user's local installation after reviewing the configured fields and organizational policy. NocoBase ACL still applies, but enabling raw output can send more permitted business fields to the AI client than compact mode.
A `list_followups` row: A `list_followups` row:
```json ```json
@@ -145,46 +167,36 @@ Tool runtime failures are returned as structured errors:
## Project writes and uploads ## Project writes and uploads
Managed HTTP mode exposes two additional MCP tools: Local HTTP mode, or stdio with `--enable-writes`, exposes two additional MCP tools:
- `create_project(name, customer_id?, inquiry_date?, progress?, applications?, part_numbers?, period_usage?, details?, attachment_ids?)` - `create_project(name, customer_id?, inquiry_date?, progress?, applications?, part_numbers?, period_usage?, details?, attachment_ids?)`
- `update_project(project_id, name?, customer_id?, inquiry_date?, progress?, applications?, part_numbers?, period_usage?, details?, add_attachment_ids?, remove_attachment_ids?)` - `update_project(project_id, name?, customer_id?, inquiry_date?, progress?, applications?, part_numbers?, period_usage?, details?, add_attachment_ids?, remove_attachment_ids?)`
They target only `t_projects` and the fixed field mapping in `projects` in `config.json`. A project name is required for creation; `inquiry_date` uses `YYYY-MM-DD`; progress accepts only `询价阶段`, `已报价`, `送样`, `已定点`, `SOP`, or `终止`. Attachments are added or removed incrementally—there is no replacement attachment-list operation. They target only `t_projects` and the fixed field mapping in `projects` in `config.json`. A project name is required for creation; `inquiry_date` uses `YYYY-MM-DD`; progress accepts only `询价阶段`, `已报价`, `送样`, `已定点`, `SOP`, or `终止`. Attachments are added or removed incrementally—there is no replacement attachment-list operation.
The same HTTP service exposes authenticated `POST /uploads`. Submit exactly one multipart field named `file`; the default single-file limit is 25 MiB (`NOCOBASE_MCP_UPLOAD_MAX_BYTES=26214400`). A successful upload returns an `upload_id`, which callers can pass to either project tool. The upload route and `/mcp` use the same MCP bearer token. The same local HTTP service exposes authenticated `POST /uploads`. Submit exactly one multipart field named `file`; the default single-file limit is 25 MiB (`NOCOBASE_MCP_UPLOAD_MAX_BYTES=26214400`). A successful upload returns an `upload_id`, which callers can pass to either project tool. The upload route and `/mcp` use the same local MCP bearer token.
HTTP write mode also requires one server-side NocoBase API key. The same key is supplied explicitly to every `nb api` CRM query and every direct HTTP upload/association request, so the service does not depend on the saved `nb-mcp` basic login session. Do not put these values in `config.json`, pass them to MCP clients, or log them: Queries and writes use the saved `nb-mcp` login. For multipart uploads and attachment associations, the MCP asks `nb env info nb-mcp --json --show-secrets` for the current credential at request time. The credential is held only in memory: it is not placed in `config.json`, the MCP client configuration, logs, or subprocess arguments. If login is missing or expired, run `nb env auth nb-mcp` again.
- `NOCOBASE_MCP_API_BASE_URL` (for example `https://nocobase.example/api`) `scripts/mcp_service.sh start` creates `.runtime/nocobase-crm-mcp.env` with mode `0600`, its parent directory with mode `0700`, a random local MCP bearer token, and only non-secret runtime settings. The legacy `NOCOBASE_MCP_API_BASE_URL` / `NOCOBASE_MCP_API_TOKEN` override remains accepted for existing operator deployments, but it is not part of the end-user installation path.
- `NOCOBASE_MCP_API_TOKEN` (a scoped NocoBase API key)
Use a dedicated NocoBase account and role with only the permissions listed below. Choose an appropriate API-key validity period and rotate the key deliberately; if NocoBase rejects the key, MCP tools return `nb_auth_failed` or `nb_upload_unauthorized` instead of attempting a password login or retry loop.
`scripts/mcp_service.sh start` creates `.runtime/nocobase-crm-mcp.env` with mode `0600`, its parent directory with mode `0700`, and only the non-secret upload size setting. HTTP write mode fails at startup with the missing variable name when either credential is absent. For migration, the legacy pair `NOCOBASE_MCP_UPLOAD_API_BASE_URL` / `NOCOBASE_MCP_UPLOAD_JWT` remains accepted when both are present; conflicting canonical and legacy values are rejected.
First-install flow: First-install flow:
```bash ```bash
# Creates the private env file; the service is expected to fail until credentials are added. # Complete browser login as the same operating-system user that runs the MCP.
bash scripts/mcp_service.sh start nb env add nb-mcp --api-base-url https://nocobase.example/api --auth-type oauth
nb env auth nb-mcp
# Add the server-side NocoBase API key to the systemd EnvironmentFile. # Create and start the private, loopback-only user service.
cat >> .runtime/nocobase-crm-mcp.env <<'EOF'
NOCOBASE_MCP_API_BASE_URL=https://nocobase.example/api
NOCOBASE_MCP_API_TOKEN=<scoped-api-key>
EOF
# Start the service with the newly configured private env file.
bash scripts/mcp_service.sh start bash scripts/mcp_service.sh start
curl -X POST -H "Authorization: Bearer <mcp-token>" -F "file=@quote.pdf" http://<host>:10087/uploads curl -X POST -H "Authorization: Bearer <mcp-token>" -F "file=@quote.pdf" http://127.0.0.1:10087/uploads
``` ```
The NocoBase credentials belong in `.runtime/nocobase-crm-mcp.env`, not in shell-local variables or client configuration. MCP bearer-token rotation preserves these operator-managed env lines. `scripts/mcp_service.sh status` reports whether canonical or legacy NocoBase credentials are configured without printing their values. An uploaded attachment that is not yet associated with a project is intentionally retained; this release does not auto-clean orphan attachments. It also provides no delete operation for projects or attachments. The NocoBase login belongs to the user's own `nb` environment, not `.runtime/nocobase-crm-mcp.env`. The MCP bearer token protects only the loopback HTTP endpoint. An uploaded attachment that is not yet associated with a project is intentionally retained; this release does not auto-clean orphan attachments. It also provides no delete operation for projects or attachments.
## Permissions ## Permissions
Runtime tools are read-only, but the role behind `nb-mcp` needs these read permissions: The logged-in user's NocoBase roles need these read permissions:
- CRM collections: `t_nt3pjadelqi`, `t_j77ftx1rf84`, `t_07667a5evtr` - CRM collections: `t_nt3pjadelqi`, `t_j77ftx1rf84`, `t_07667a5evtr`
- user lookup collection: `users`, at least `id`, `username`, `nickname`, `email`, `f_yh_english_name` - user lookup collection: `users`, at least `id`, `username`, `nickname`, `email`, `f_yh_english_name`
@@ -199,7 +211,7 @@ Use a super-admin or configuration-capable role for this one-time discovery step
For the read-only stdio/default server, do not grant write permissions such as collection apply/destroy, fields apply, or business data create/update/destroy. For the read-only stdio/default server, do not grant write permissions such as collection apply/destroy, fields apply, or business data create/update/destroy.
For the managed HTTP write mode only, the NocoBase JWT role additionally needs: For either write-enabled mode, the logged-in user additionally needs:
- `t_projects`: create and update - `t_projects`: create and update
- `attachments`: upload/create - `attachments`: upload/create
@@ -207,18 +219,18 @@ For the managed HTTP write mode only, the NocoBase JWT role additionally needs:
Do not grant project or attachment delete permissions. This server does not offer deletes, and it retains uploaded but unassociated attachments. Do not grant project or attachment delete permissions. This server does not offer deletes, and it retains uploaded but unassociated attachments.
## Remote HTTP MCP ## Local HTTP MCP (optional)
HTTP mode uses streamable HTTP with a separate bearer token for MCP access: HTTP mode uses streamable HTTP with a separate local bearer token for MCP access:
```bash ```bash
export NOCOBASE_MCP_TOKEN='<long-random-token>' export NOCOBASE_MCP_TOKEN='<long-random-token>'
uv run nocobase-crm-mcp --transport streamable-http --config config.json uv run nocobase-crm-mcp --transport streamable-http --config config.json
``` ```
Default bind is `0.0.0.0:10087` with path `/mcp`. Default bind is `127.0.0.1:10087` with path `/mcp`. The supported end-user deployment is loopback-only; do not expose this port to a LAN or the public internet.
Remote clients must send: Local clients must send:
```http ```http
Authorization: Bearer <long-random-token> Authorization: Bearer <long-random-token>
@@ -238,7 +250,7 @@ If `nb` is installed somewhere unusual, set its absolute path before starting:
NOCOBASE_MCP_NB_BIN=/absolute/path/to/nb bash scripts/mcp_service.sh start NOCOBASE_MCP_NB_BIN=/absolute/path/to/nb bash scripts/mcp_service.sh start
``` ```
The generated env file also includes the non-secret 25 MiB upload limit. The service fails at startup until the operator adds `NOCOBASE_MCP_API_BASE_URL` and `NOCOBASE_MCP_API_TOKEN` to that private file. Run `start` again after adding them; `new-token` preserves those operator-managed credential lines. The generated env file also includes the non-secret 25 MiB upload limit. NocoBase access comes from the same operating-system user's saved `nb-mcp` login; run `nb env auth nb-mcp` again if the MCP reports `nb_auth_failed` or `nb_upload_unauthorized`.
Show the saved URL/header/client config: Show the saved URL/header/client config:
+151
View File
@@ -0,0 +1,151 @@
# 本机交付与登录手册
## 交付模型
每位用户在自己的电脑运行 MCP,并使用自己的 NocoBase 账号登录 `nb` CLI:
```text
本机 AI 客户端 → 本机 NocoBase CRM MCP → 本机 nb CLI 登录态 → 远程 NocoBase ACL
```
MCP 不保存公共业务账号,也不需要部署一个多人共用的远程 MCP 服务。NocoBase 仍是最终权限裁决者;同一个工具由不同用户调用时,只能返回或修改其账号 ACL 允许的数据。
## 环境要求
- Python 3.12 或更高版本
- `uv`
- NocoBase `nb` CLI 2.1 或更高版本
- 可通过浏览器访问目标 NocoBase
- 管理员已为用户配置所需的 NocoBase 角色与数据权限
## 首次安装
```bash
git clone https://github.com/Donaldttt/nocobase-mcp.git
cd nocobase-mcp
uv sync --locked
cp config.example.json config.json
```
`config.json` 中的 `env` 默认是 `nb-mcp`。用自己的账号创建并登录同名环境:
```bash
nb env add nb-mcp --api-base-url https://nocobase.example/api --auth-type oauth
nb env auth nb-mcp
nb env info nb-mcp
```
`nb env auth` 会打开浏览器。用户必须亲自完成登录;不要向用户分发共享 API Key,也不要复制其他人的 `nb` 配置目录。
先进行健康检查:
```bash
uv run nocobase-crm-mcp --config config.json
```
然后让 MCP 客户端调用 `crm_health`。如果返回 `nb_auth_failed`,重新执行:
```bash
nb env auth nb-mcp
```
## MCP 客户端配置
推荐使用 stdio。把绝对路径替换成用户电脑上的实际安装目录:
```json
{
"mcpServers": {
"nocobase-crm": {
"command": "uv",
"args": [
"--directory",
"/absolute/path/to/nocobase-mcp",
"run",
"--no-sync",
"nocobase-crm-mcp",
"--config",
"/absolute/path/to/nocobase-mcp/config.json"
]
}
}
}
```
默认只读。如需项目创建和更新,在参数末尾加入 `--enable-writes`。这些写工具只允许预先定义的项目字段,不提供删除或任意资源访问能力。
附件上传需要可选的本机 HTTP 模式。它默认只监听 `127.0.0.1:10087`,不得暴露到局域网或公网:
```bash
NOCOBASE_MCP_TOKEN='<随机长字符串>' \
uv run nocobase-crm-mcp --transport streamable-http --config config.json
```
Linux 用户也可以执行 `bash scripts/mcp_service.sh start` 安装用户级 systemd 服务。
## 权限验收
交付前至少使用两个权限不同的测试账号验证:
1. 两台电脑或两个隔离的操作系统账号分别执行 `nb env auth nb-mcp`。
2. 使用相同参数调用 `list_customers`、`list_contacts`、`list_followups` 和 `list_projects`。
3. 确认各自只能看到 NocoBase UI 中本人有权查看的数据。
4. 对无项目写权限的账号调用 `create_project`,确认 NocoBase 拒绝。
5. 对有写权限的账号创建测试项目,确认审计字段记录的是该用户,而不是共享服务账号。
6. 退出或使 Token 过期后,确认 MCP 返回 `nb_auth_failed`,且不会降级使用其他凭证。
## 凭证安全
- NocoBase Token 由 `nb` CLI 保存,不能写入 `config.json` 或 MCP 客户端配置。
- MCP 获取附件请求凭证时,Token 只存在于进程内存,不进入子进程参数和错误日志。
- `.runtime/` 只保存本机 HTTP MCP 的独立 Bearer Token,并已被 Git 忽略。
- 不要共享操作系统账号运行 MCP;本机权限隔离依赖每位用户拥有独立的 `nb` 登录存储。
- `NOCOBASE_MCP_API_TOKEN` 只为旧部署兼容保留,不用于终端用户交付。
- `include_raw=true` 默认被 MCP 拒绝。只有在管理员审核 `config.json` 字段和 AI 数据使用政策后,才可在该用户本机设置 `NOCOBASE_MCP_ENABLE_RAW_DATA=1`。
## 更新、回滚与恢复
更新前记录当前版本,更新后重新同步锁定依赖并运行测试:
```bash
git rev-parse HEAD
git pull --ff-only
uv sync --locked
uv run python -m unittest discover -s tests
```
使用 systemd 本机 HTTP 服务时,再执行:
```bash
bash scripts/mcp_service.sh start
bash scripts/mcp_service.sh status
journalctl --user -u nocobase-crm-mcp.service -n 200 --no-pager
```
如果新版本验收失败,切回上一步记录的已发布 Tag 或 Commit,然后重新同步并启动:
```bash
git switch --detach <previous-release-tag-or-commit>
uv sync --locked
bash scripts/mcp_service.sh start
```
发布交付必须来自工作区干净、CI 全绿的带注释 Git Tag;不要从开发分支任意提交直接打包给用户。
需要备份的业务配置是 `config.json`。它不应包含 Token,可以进入受控版本库或企业配置备份。`.runtime/nocobase-crm-mcp.env` 只包含本机 HTTP Token 和运行参数;丢失后运行 `bash scripts/mcp_service.sh new-token` 重新生成即可。不要备份、复制或分发用户的 `nb` OAuth Token;恢复电脑后重新执行 `nb env add` 和 `nb env auth`。
建议监控:
- MCP 客户端定期调用 `crm_health`。
- 本机 HTTP 用户执行 `bash scripts/mcp_service.sh status`,确认 systemd active 且 HTTP probe reachable。
- 日志只检查错误码和命令退出状态;发现业务原始数据或 Token 时立即停止交付并按安全事件处理。
## 常见故障
常见故障:
- `nb_not_found`:确认 `nb` 在 PATH 中,或设置 `NOCOBASE_MCP_NB_BIN` 为绝对路径。
- `nb_auth_failed`:执行 `nb env auth nb-mcp`。
- `NO_PERMISSIONS`:由 NocoBase 管理员调整该用户的角色、字段或行级数据权限。
- 附件返回 `nb_upload_unauthorized`:重新登录后重试;不要把其他用户的 Token 写入环境变量。
- 环境连接错误:执行 `nb env info nb-mcp`,确认 `apiBaseUrl` 指向目标环境的 `/api`。
+4 -5
View File
@@ -6,7 +6,6 @@ from dataclasses import dataclass, field, replace
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
PROJECT_PROGRESS_VALUES = { PROJECT_PROGRESS_VALUES = {
"询价阶段": "fk9xj8akn46", "询价阶段": "fk9xj8akn46",
"已报价": "k4w2vcz819w", "已报价": "k4w2vcz819w",
@@ -250,9 +249,7 @@ def _customer_config(data: dict[str, Any] | None) -> CustomerConfig:
fields=_string_list(_pick(data, "fields")) or base.fields, fields=_string_list(_pick(data, "fields")) or base.fields,
search_fields=_string_list(_pick(data, "searchFields", "search_fields")) or base.search_fields, search_fields=_string_list(_pick(data, "searchFields", "search_fields")) or base.search_fields,
name_fields=_string_list(_pick(data, "nameFields", "name_fields")) or base.name_fields, name_fields=_string_list(_pick(data, "nameFields", "name_fields")) or base.name_fields,
english_name_field=str( english_name_field=str(_pick(data, "englishNameField", "english_name_field") or base.english_name_field),
_pick(data, "englishNameField", "english_name_field") or base.english_name_field
),
territorial_name_field=str( territorial_name_field=str(
_pick(data, "territorialNameField", "territorial_name_field") or base.territorial_name_field _pick(data, "territorialNameField", "territorial_name_field") or base.territorial_name_field
), ),
@@ -392,7 +389,9 @@ def load_config(path: str | Path | None = None) -> CrmConfig:
raise ValueError("CRM config collections must be a JSON object") raise ValueError("CRM config collections must be a JSON object")
env_name = os.environ.get("NOCOBASE_MCP_ENV") or raw.get("env") or raw.get("envName") or "nb-mcp" env_name = os.environ.get("NOCOBASE_MCP_ENV") or raw.get("env") or raw.get("envName") or "nb-mcp"
data_source = os.environ.get("NOCOBASE_MCP_DATA_SOURCE") or raw.get("dataSource") or raw.get("data_source") or "main" data_source = (
os.environ.get("NOCOBASE_MCP_DATA_SOURCE") or raw.get("dataSource") or raw.get("data_source") or "main"
)
timezone = os.environ.get("NOCOBASE_MCP_TIMEZONE") or raw.get("timezone") or "Asia/Shanghai" timezone = os.environ.get("NOCOBASE_MCP_TIMEZONE") or raw.get("timezone") or "Asia/Shanghai"
return CrmConfig( return CrmConfig(
+81 -77
View File
@@ -1,8 +1,9 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass from dataclasses import dataclass
from datetime import UTC, date, datetime from datetime import UTC, date, datetime
from typing import Any, Mapping from typing import Any
from zoneinfo import ZoneInfo from zoneinfo import ZoneInfo
from .config import CrmConfig from .config import CrmConfig
@@ -16,14 +17,11 @@ from .filters import (
) )
from .nb_client import NbClient, NbCommandError from .nb_client import NbClient, NbCommandError
DEFAULT_PAGE_SIZE = 20 DEFAULT_PAGE_SIZE = 20
MAX_PAGE_SIZE = 100 MAX_PAGE_SIZE = 100
MAX_SCAN_RECORDS = 5000 MAX_SCAN_RECORDS = 5000
UNKNOWN_NAME_PREFIX = "#" UNKNOWN_NAME_PREFIX = "#"
PROJECTION_FALLBACK_WARNING = ( PROJECTION_FALLBACK_WARNING = "NocoBase rejected relation field projection; check nameFields in the CRM config"
"NocoBase rejected relation field projection; check nameFields in the CRM config"
)
CUSTOMER_STATUS_LABELS = { CUSTOMER_STATUS_LABELS = {
"kh_p_not_start": "未开始", "kh_p_not_start": "未开始",
@@ -221,8 +219,7 @@ class CrmService:
or self.config.contact.title_field, or self.config.contact.title_field,
"phoneField": _find_named_field(contact_fields, ["khlxr_phone_number"]) "phoneField": _find_named_field(contact_fields, ["khlxr_phone_number"])
or self.config.contact.phone_field, or self.config.contact.phone_field,
"emailField": _find_named_field(contact_fields, ["khlxr_email"]) "emailField": _find_named_field(contact_fields, ["khlxr_email"]) or self.config.contact.email_field,
or self.config.contact.email_field,
"customerField": _find_named_field(contact_fields, ["kehulianxiren_client"]) "customerField": _find_named_field(contact_fields, ["kehulianxiren_client"])
or _relation_field(contact_fields, ["公司", "客户"]) or _relation_field(contact_fields, ["公司", "客户"])
or self.config.contact.customer_field, or self.config.contact.customer_field,
@@ -293,7 +290,7 @@ class CrmService:
"contacts": self._count(self.config.contact.resource, self.config.contact.id_field, {}), "contacts": self._count(self.config.contact.resource, self.config.contact.id_field, {}),
"followups": self._count(self.config.followup.resource, self.config.followup.id_field, followup_filter), "followups": self._count(self.config.followup.resource, self.config.followup.id_field, followup_filter),
} }
overview = { overview: dict[str, Any] = {
"totals": totals, "totals": totals,
"customer_status": self._group_count( "customer_status": self._group_count(
self.config.customer.resource, self.config.customer.resource,
@@ -354,9 +351,7 @@ class CrmService:
), ),
# Follow-up rows render type and status as labels only, so the raw # Follow-up rows render type and status as labels only, so the raw
# codes the filters expect are published here instead. # codes the filters expect are published here instead.
"followup_type": [ "followup_type": [{"value": value, "label": label} for value, label in FOLLOWUP_TYPE_LABELS.items()],
{"value": value, "label": label} for value, label in FOLLOWUP_TYPE_LABELS.items()
],
"followup_record_status": [ "followup_record_status": [
{"value": value, "label": label} for value, label in FOLLOWUP_STATUS_LABELS.items() {"value": value, "label": label} for value, label in FOLLOWUP_STATUS_LABELS.items()
], ],
@@ -526,9 +521,7 @@ class CrmService:
lookup = self.client.list_records( lookup = self.client.list_records(
self.config.customer.resource, self.config.customer.resource,
filter={self.config.customer.id_field: {"$in": identifiers[:max_lookups]}}, filter={self.config.customer.id_field: {"$in": identifiers[:max_lookups]}},
fields=_unique_strings( fields=_unique_strings([self.config.customer.id_field, *(self.config.customer.name_fields or [])]),
[self.config.customer.id_field, *(self.config.customer.name_fields or [])]
),
page=1, page=1,
page_size=max_lookups, page_size=max_lookups,
) )
@@ -644,7 +637,7 @@ class CrmService:
limit=limit, limit=limit,
offset=offset, offset=offset,
filter=filter_value, filter=filter_value,
fields=self.config.customer.fields if include_raw else self._customer_compact_fields(), fields=(self.config.customer.fields or []) if include_raw else self._customer_compact_fields(),
sort=self.config.customer.sort, sort=self.config.customer.sort,
) )
result["results"] = self._compact_customers(result["results"], include_raw=include_raw) result["results"] = self._compact_customers(result["results"], include_raw=include_raw)
@@ -682,19 +675,23 @@ class CrmService:
if include_contacts: if include_contacts:
# Phone and email are deliberately withheld here: a customer overview # Phone and email are deliberately withheld here: a customer overview
# does not need personal contact details. list_contacts still serves them. # does not need personal contact details. list_contacts still serves them.
result["contacts"] = self.list_contacts( contacts_result = self.list_contacts(
customer_id=customer_id, customer_id=customer_id,
limit=50, limit=50,
offset=0, offset=0,
include_details=False, include_details=False,
)["results"] )
result["contacts"] = contacts_result["results"]
result["contacts_pagination"] = _pagination_metadata(contacts_result)
if include_recent_followups: if include_recent_followups:
result["recent_followups"] = self.list_followups( followups_result = self.list_followups(
customer_id=customer_id, customer_id=customer_id,
limit=followup_limit, limit=followup_limit,
offset=0, offset=0,
include_raw=include_raw, include_raw=include_raw,
)["results"] )
result["recent_followups"] = followups_result["results"]
result["recent_followups_pagination"] = _pagination_metadata(followups_result)
result["timezone"] = self.config.timezone result["timezone"] = self.config.timezone
return result return result
@@ -741,9 +738,7 @@ class CrmService:
fields=self._project_fields(), fields=self._project_fields(),
sort=["-updatedAt"], sort=["-updatedAt"],
) )
result["results"] = [ result["results"] = [self._compact_project(row, include_raw=include_raw) for row in result["results"]]
self._compact_project(row, include_raw=include_raw) for row in result["results"]
]
result["timezone"] = self.config.timezone result["timezone"] = self.config.timezone
return result return result
@@ -768,13 +763,13 @@ class CrmService:
result: dict[str, Any] = {"project": project, "timezone": self.config.timezone} result: dict[str, Any] = {"project": project, "timezone": self.config.timezone}
if include_contacts: if include_contacts:
# Was returning raw contact rows, phone and email included. # Was returning raw contact rows, phone and email included.
result["contacts"] = _relation_refs( result["contacts"] = _relation_refs(row, [self.config.project.contact_field], self._contact_spec())
row, [self.config.project.contact_field], self._contact_spec()
)
if include_followups: if include_followups:
result["followups"] = self.list_followups(project_id=project_id, limit=100, offset=0)["results"] followups_result = self.list_followups(project_id=project_id, limit=100, offset=0)
result["followups"] = followups_result["results"]
result["followups_pagination"] = _pagination_metadata(followups_result)
if include_updates: if include_updates:
updates, truncated = self._scan_records( updates, truncation = self._scan_records(
self.config.project.update_resource, self.config.project.update_resource,
filter=build_and_filter( filter=build_and_filter(
[build_relation_id_condition(self.config.project.update_project_field, project_id)] [build_relation_id_condition(self.config.project.update_project_field, project_id)]
@@ -791,8 +786,9 @@ class CrmService:
sort=[self.config.project.update_date_field, "createdAt"], sort=[self.config.project.update_date_field, "createdAt"],
) )
result["updates"] = [self._compact_project_update(update) for update in updates] result["updates"] = [self._compact_project_update(update) for update in updates]
if truncated: if truncation:
result["data_quality_warnings"] = ["project updates were truncated at the scan limit"] result["data_quality_warnings"] = ["project updates were truncated at the scan limit"]
result["truncation"] = {"updates": truncation}
return result return result
def get_project_timeline( def get_project_timeline(
@@ -811,7 +807,7 @@ class CrmService:
if "error" in project_result: if "error" in project_result:
return project_result return project_result
project = project_result["project"] project = project_result["project"]
followup_rows, followups_truncated = self._scan_records( followup_rows, followups_truncation = self._scan_records(
self.config.followup.resource, self.config.followup.resource,
filter=build_and_filter( filter=build_and_filter(
[ [
@@ -822,7 +818,7 @@ class CrmService:
fields=self._followup_compact_fields(), fields=self._followup_compact_fields(),
sort=self.config.followup.sort, sort=self.config.followup.sort,
) )
update_rows, updates_truncated = self._scan_records( update_rows, updates_truncation = self._scan_records(
self.config.project.update_resource, self.config.project.update_resource,
filter=build_and_filter( filter=build_and_filter(
[ [
@@ -871,12 +867,16 @@ class CrmService:
"timezone": self.config.timezone, "timezone": self.config.timezone,
} }
warnings: list[str] = [] warnings: list[str] = []
if followups_truncated: truncation: dict[str, Any] = {}
if followups_truncation:
warnings.append("follow-up events were truncated at the scan limit") warnings.append("follow-up events were truncated at the scan limit")
if updates_truncated: truncation["followups"] = followups_truncation
if updates_truncation:
warnings.append("project update events were truncated at the scan limit") warnings.append("project update events were truncated at the scan limit")
truncation["updates"] = updates_truncation
if warnings: if warnings:
result["data_quality_warnings"] = warnings result["data_quality_warnings"] = warnings
result["truncation"] = truncation
return result return result
def list_stale_customers( def list_stale_customers(
@@ -916,13 +916,13 @@ class CrmService:
{self.config.followup.status_field: {"$ne": INVALID_FOLLOWUP_STATUS}}, {self.config.followup.status_field: {"$ne": INVALID_FOLLOWUP_STATUS}},
] ]
) )
customer_rows, customers_truncated = self._scan_records( customer_rows, customers_truncation = self._scan_records(
self.config.customer.resource, self.config.customer.resource,
filter=customer_filter, filter=customer_filter,
fields=self._customer_compact_fields(), fields=self._customer_compact_fields(),
sort=self.config.customer.sort, sort=self.config.customer.sort,
) )
followup_rows, followups_truncated = self._scan_records( followup_rows, followups_truncation = self._scan_records(
self.config.followup.resource, self.config.followup.resource,
filter=followup_filter, filter=followup_filter,
# This scan can pull thousands of rows and only needs the customer id, # This scan can pull thousands of rows and only needs the customer id,
@@ -964,9 +964,7 @@ class CrmService:
stale.append( stale.append(
{ {
"customer": self._compact_customer(customer), "customer": self._compact_customer(customer),
"last_followup_at": ( "last_followup_at": (last.astimezone(ZoneInfo(self.config.timezone)).isoformat() if last else None),
last.astimezone(ZoneInfo(self.config.timezone)).isoformat() if last else None
),
"days_since_last_followup": days_since, "days_since_last_followup": days_since,
"never_followed_up": last is None, "never_followed_up": last is None,
} }
@@ -988,12 +986,16 @@ class CrmService:
} }
) )
warnings: list[str] = [] warnings: list[str] = []
if customers_truncated: truncation: dict[str, Any] = {}
if customers_truncation:
warnings.append("customer scan was truncated") warnings.append("customer scan was truncated")
if followups_truncated: truncation["customers"] = customers_truncation
if followups_truncation:
warnings.append("follow-up scan was truncated") warnings.append("follow-up scan was truncated")
truncation["followups"] = followups_truncation
if warnings: if warnings:
result["data_quality_warnings"] = warnings result["data_quality_warnings"] = warnings
result["truncation"] = truncation
return result return result
def list_contacts( def list_contacts(
@@ -1018,7 +1020,7 @@ class CrmService:
offset=offset, offset=offset,
filter=filter_value, filter=filter_value,
fields=( fields=(
self.config.contact.fields (self.config.contact.fields or [])
if include_raw if include_raw
else self._contact_compact_fields(include_details=include_details) else self._contact_compact_fields(include_details=include_details)
), ),
@@ -1069,7 +1071,7 @@ class CrmService:
limit=limit, limit=limit,
offset=offset, offset=offset,
filter=filter_value, filter=filter_value,
fields=self.config.followup.fields if include_raw else self._followup_compact_fields(), fields=(self.config.followup.fields or []) if include_raw else self._followup_compact_fields(),
sort=self.config.followup.sort, sort=self.config.followup.sort,
) )
result["results"] = self._compact_followups(result["results"], include_raw=include_raw) result["results"] = self._compact_followups(result["results"], include_raw=include_raw)
@@ -1113,7 +1115,7 @@ class CrmService:
page_size: int = MAX_PAGE_SIZE, page_size: int = MAX_PAGE_SIZE,
max_records: int = MAX_SCAN_RECORDS, max_records: int = MAX_SCAN_RECORDS,
**kwargs: Any, **kwargs: Any,
) -> tuple[list[dict[str, Any]], bool]: ) -> tuple[list[dict[str, Any]], dict[str, int] | None]:
rows: list[dict[str, Any]] = [] rows: list[dict[str, Any]] = []
page = 1 page = 1
total: int | None = None total: int | None = None
@@ -1131,8 +1133,15 @@ class CrmService:
if not page_rows or len(page_rows) < page_size or len(rows) >= total: if not page_rows or len(page_rows) < page_size or len(rows) >= total:
break break
page += 1 page += 1
truncated = total is not None and len(rows) < total selected = rows[:max_records]
return rows[:max_records], truncated if total is None or len(selected) >= total:
return selected, None
return selected, {
"total": total,
"returned": len(selected),
"limit": max_records,
"next_offset": len(selected),
}
def _project_fields(self) -> list[str]: def _project_fields(self) -> list[str]:
project = self.config.project project = self.config.project
@@ -1166,7 +1175,7 @@ class CrmService:
"customer_id": row.get(project.customer_field), "customer_id": row.get(project.customer_field),
"customers": _relation_refs(row, [project.customer_relation_field], self._customer_spec()), "customers": _relation_refs(row, [project.customer_relation_field], self._customer_spec()),
"inquiry_date": row.get(project.inquiry_date_field), "inquiry_date": row.get(project.inquiry_date_field),
"stage": progress_labels.get(progress, progress), "stage": progress_labels.get(str(progress), progress) if progress is not None else None,
"applications": row.get(project.applications_field), "applications": row.get(project.applications_field),
"part_numbers": row.get(project.part_numbers_field), "part_numbers": row.get(project.part_numbers_field),
"period_usage": row.get(project.period_usage_field), "period_usage": row.get(project.period_usage_field),
@@ -1262,9 +1271,7 @@ class CrmService:
} }
members = self._team_users(matches[0], self.config.team.member_field, formal_only=True) members = self._team_users(matches[0], self.config.team.member_field, formal_only=True)
member_ids = [ member_ids = [
row.get(self.config.user.id_field) row.get(self.config.user.id_field) for row in members if row.get(self.config.user.id_field) is not None
for row in members
if row.get(self.config.user.id_field) is not None
] ]
if not member_ids: if not member_ids:
return None, { return None, {
@@ -1284,9 +1291,7 @@ class CrmService:
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
related = _relation_rows(team_row.get(relation_field)) related = _relation_rows(team_row.get(relation_field))
user_ids = [ user_ids = [
row.get(self.config.user.id_field) row.get(self.config.user.id_field) for row in related if row.get(self.config.user.id_field) is not None
for row in related
if row.get(self.config.user.id_field) is not None
] ]
if not user_ids: if not user_ids:
return [] return []
@@ -1306,15 +1311,14 @@ class CrmService:
self.config.team.user_status_field, self.config.team.user_status_field,
] ]
) )
return self._data( users, _truncation = self._scan_records(
self.client.list_records( self.config.user.resource,
self.config.user.resource, filter=build_and_filter(conditions),
filter=build_and_filter(conditions), fields=fields,
fields=fields, page_size=min(MAX_PAGE_SIZE, max(1, len(user_ids))),
page=1, max_records=len(user_ids),
page_size=min(MAX_PAGE_SIZE, max(1, len(user_ids))),
)
) )
return users
def _compact_team( def _compact_team(
self, self,
@@ -1426,7 +1430,7 @@ class CrmService:
rows: list[dict[str, Any]] = [] rows: list[dict[str, Any]] = []
batch_size = MAX_PAGE_SIZE batch_size = MAX_PAGE_SIZE
offset = 0 offset = 0
while len(rows) < MAX_SCAN_RECORDS: while True:
response = self.client.query_records( response = self.client.query_records(
resource, resource,
dimensions=[{"field": dimension_path, "alias": dimension_alias}], dimensions=[{"field": dimension_path, "alias": dimension_alias}],
@@ -1440,8 +1444,12 @@ class CrmService:
rows.extend(batch) rows.extend(batch)
if len(batch) < batch_size: if len(batch) < batch_size:
break break
if len(rows) >= MAX_SCAN_RECORDS:
raise NbCommandError(
f"Aggregation produced at least {MAX_SCAN_RECORDS} buckets; add filters to narrow the result"
)
offset += batch_size offset += batch_size
return rows[:MAX_SCAN_RECORDS] return rows
def _decorate_status_counts(self, rows: list[dict[str, Any]]) -> list[dict[str, Any]]: def _decorate_status_counts(self, rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
return _decorate_dimension_counts( return _decorate_dimension_counts(
@@ -1542,9 +1550,7 @@ class CrmService:
include_details: bool = True, include_details: bool = True,
include_raw: bool = False, include_raw: bool = False,
) -> list[dict[str, Any]]: ) -> list[dict[str, Any]]:
return [ return [self._compact_contact(row, include_details=include_details, include_raw=include_raw) for row in rows]
self._compact_contact(row, include_details=include_details, include_raw=include_raw) for row in rows
]
def _contact_name_candidates(self) -> list[str]: def _contact_name_candidates(self) -> list[str]:
return [*(self.config.contact.name_fields or []), "name", "title"] return [*(self.config.contact.name_fields or []), "name", "title"]
@@ -1636,8 +1642,10 @@ class CrmService:
"contacts": summary["contacts"], "contacts": summary["contacts"],
"participants": participants, "participants": participants,
"projects": summary["projects"], "projects": summary["projects"],
"type": FOLLOWUP_TYPE_LABELS.get(followup_type, followup_type), "type": FOLLOWUP_TYPE_LABELS.get(str(followup_type), followup_type) if followup_type is not None else None,
"status": FOLLOWUP_STATUS_LABELS.get(record_status, record_status), "status": (
FOLLOWUP_STATUS_LABELS.get(str(record_status), record_status) if record_status is not None else None
),
"location": _first_text(row, [followup.location_field]), "location": _first_text(row, [followup.location_field]),
"content": summary["content"], "content": summary["content"],
} }
@@ -1755,6 +1763,10 @@ def _paginated_rows(rows: list[dict[str, Any]], total: int, limit: int, offset:
} }
def _pagination_metadata(result: Mapping[str, Any]) -> dict[str, Any]:
return {key: result.get(key) for key in ("total", "returned", "limit", "offset", "has_more", "next_offset")}
def _beijing_datetime(value: Any, timezone: str) -> Any: def _beijing_datetime(value: Any, timezone: str) -> Any:
if not isinstance(value, str) or not value.strip(): if not isinstance(value, str) or not value.strip():
return value return value
@@ -1971,11 +1983,7 @@ def _relation_rows(value: Any) -> list[dict[str, Any]]:
def _relation_ids(value: Any, id_field: str = "id") -> list[Any]: def _relation_ids(value: Any, id_field: str = "id") -> list[Any]:
return [ return [row.get(id_field) for row in _relation_rows(value) if row.get(id_field) is not None]
row.get(id_field)
for row in _relation_rows(value)
if row.get(id_field) is not None
]
def _unique_values(values: list[Any]) -> list[Any]: def _unique_values(values: list[Any]) -> list[Any]:
@@ -2126,16 +2134,12 @@ def _template_sort(date_field: str | None, fallback: list[str] | None) -> list[s
def _pick_user_matches(rows: list[dict[str, Any]], query: str, match_fields: list[str]) -> list[dict[str, Any]]: def _pick_user_matches(rows: list[dict[str, Any]], query: str, match_fields: list[str]) -> list[dict[str, Any]]:
normalized = query.casefold() normalized = query.casefold()
exact = [ exact = [
row row for row in rows if any(str(row.get(field) or "").strip().casefold() == normalized for field in match_fields)
for row in rows
if any(str(row.get(field) or "").strip().casefold() == normalized for field in match_fields)
] ]
if exact: if exact:
return exact return exact
fuzzy = [ fuzzy = [
row row for row in rows if any(normalized in str(row.get(field) or "").strip().casefold() for field in match_fields)
for row in rows
if any(normalized in str(row.get(field) or "").strip().casefold() for field in match_fields)
] ]
return fuzzy return fuzzy
+2 -2
View File
@@ -1,7 +1,7 @@
from __future__ import annotations from __future__ import annotations
from typing import Any, Iterable, Sequence from collections.abc import Iterable, Sequence
from typing import Any
Filter = dict[str, Any] Filter = dict[str, Any]
+41 -2
View File
@@ -4,10 +4,10 @@ import json
import os import os
import re import re
import subprocess import subprocess
from collections.abc import Callable, Mapping, Sequence
from collections.abc import Set as AbstractSet from collections.abc import Set as AbstractSet
from pathlib import Path from pathlib import Path
from typing import Any, Callable, Mapping, Sequence from typing import Any
Runner = Callable[..., subprocess.CompletedProcess[str]] Runner = Callable[..., subprocess.CompletedProcess[str]]
@@ -54,6 +54,45 @@ class NbClient:
"stderr": "", "stderr": "",
} }
def environment_credentials(self) -> tuple[str, str]:
"""Return the selected CLI environment's API URL and current user token.
The token is read from ``nb env info`` output and is never added to a
subprocess command line. This is intended for the few multipart or
relation endpoints that the CLI does not expose as first-class commands.
"""
command = [
self.nb_bin,
"env",
"info",
self.env_name,
"--json",
"--show-secrets",
]
result = self._run_raw(command[1:], full_command=command)
if result.returncode != 0:
raise NbCommandError(
f"Could not read nb environment {self.env_name!r} (exit code {result.returncode}). "
f"Run `nb env info {self.env_name}` to verify it."
)
try:
payload = self._parse_json_object(result.stdout)
except (json.JSONDecodeError, NbCommandError) as error:
raise NbCommandError(f"nb env info returned invalid JSON for environment {self.env_name!r}") from error
api = payload.get("api")
auth = api.get("auth") if isinstance(api, Mapping) else None
api_base_url = api.get("apiBaseUrl") if isinstance(api, Mapping) else None
access_token = auth.get("accessToken") if isinstance(auth, Mapping) else None
if not isinstance(api_base_url, str) or not api_base_url.strip():
raise NbCommandError(f"nb environment {self.env_name!r} has no API base URL")
if not isinstance(access_token, str) or not access_token.strip() or access_token in {"-", "********"}:
raise NbAuthenticationError(
f"nb environment {self.env_name!r} is not authenticated. "
f"Run `nb env auth {self.env_name}` and complete login in your browser."
)
return api_base_url.strip().rstrip("/"), access_token.strip()
def list_records( def list_records(
self, self,
resource: str, resource: str,
+44 -19
View File
@@ -1,7 +1,8 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Callable, Sequence
from pathlib import Path from pathlib import Path
from typing import Any, Sequence from typing import Any
from urllib.parse import quote from urllib.parse import quote
import httpx import httpx
@@ -27,32 +28,36 @@ class NocoBaseHttpClient:
def __init__( def __init__(
self, self,
api_base_url: str, api_base_url: str | None = None,
api_token: str, api_token: str | None = None,
timeout: float = 30.0, timeout: float = 30.0,
transport: httpx.BaseTransport | None = None, transport: httpx.BaseTransport | None = None,
credential_provider: Callable[[], tuple[str, str]] | None = None,
) -> None: ) -> None:
self.api_base_url = api_base_url.strip().rstrip("/") self.api_base_url = api_base_url.strip().rstrip("/") if api_base_url else ""
self.api_token = api_token.strip() self.api_token = api_token.strip() if api_token else ""
if not self.api_base_url: self._credential_provider = credential_provider
if credential_provider is None and not self.api_base_url:
raise ValueError("NocoBase API base URL must not be blank") raise ValueError("NocoBase API base URL must not be blank")
if not self.api_token: if credential_provider is None and not self.api_token:
raise ValueError("NocoBase API token must not be blank") raise ValueError("NocoBase API token must not be blank")
if credential_provider is not None and (self.api_base_url or self.api_token):
raise ValueError("Provide static NocoBase credentials or a credential provider, not both")
self._client = httpx.Client( self._client = httpx.Client(
base_url=self.api_base_url,
headers={"Authorization": f"Bearer {self.api_token}"},
timeout=timeout, timeout=timeout,
transport=transport, transport=transport,
) )
def upload_attachment(self, path: Path, filename: str, mimetype: str | None) -> dict[str, Any]: def upload_attachment(self, path: Path, filename: str, mimetype: str | None) -> dict[str, Any]:
api_base_url, api_token = self._credentials()
with Path(path).open("rb") as handle: with Path(path).open("rb") as handle:
response = self._client.post( response = self._client.post(
"/attachments:create", f"{api_base_url}/attachments:create",
params={"attachmentField": self._ATTACHMENT_FIELD}, params={"attachmentField": self._ATTACHMENT_FIELD},
files={"file": (filename, handle, mimetype or "application/octet-stream")}, files={"file": (filename, handle, mimetype or "application/octet-stream")},
headers={"Authorization": f"Bearer {api_token}"},
) )
payload = self._json_response(response) payload = self._json_response(response, api_token)
data = payload.get("data") data = payload.get("data")
if not isinstance(data, dict): if not isinstance(data, dict):
raise NocoBaseHttpError(response.status_code, "NocoBase response did not contain an attachment object") raise NocoBaseHttpError(response.status_code, "NocoBase response did not contain an attachment object")
@@ -66,14 +71,16 @@ class NocoBaseHttpClient:
def _post_attachment_ids(self, project_id: Any, attachment_ids: Sequence[Any], action: str) -> None: def _post_attachment_ids(self, project_id: Any, attachment_ids: Sequence[Any], action: str) -> None:
project_id_segment = _project_id_segment(project_id) project_id_segment = _project_id_segment(project_id)
api_base_url, api_token = self._credentials()
response = self._client.post( response = self._client.post(
f"/t_projects/{project_id_segment}/f_projects_attachments:{action}", f"{api_base_url}/t_projects/{project_id_segment}/f_projects_attachments:{action}",
json={"tk": list(attachment_ids)}, json={"tk": list(attachment_ids)},
headers={"Authorization": f"Bearer {api_token}"},
) )
self._raise_for_error(response) self._raise_for_error(response, api_token)
def _json_response(self, response: httpx.Response) -> dict[str, Any]: def _json_response(self, response: httpx.Response, api_token: str) -> dict[str, Any]:
self._raise_for_error(response) self._raise_for_error(response, api_token)
try: try:
payload = response.json() payload = response.json()
except ValueError as error: except ValueError as error:
@@ -82,18 +89,36 @@ class NocoBaseHttpClient:
raise NocoBaseHttpError(response.status_code, "NocoBase response was not a JSON object") raise NocoBaseHttpError(response.status_code, "NocoBase response was not a JSON object")
return payload return payload
def _raise_for_error(self, response: httpx.Response) -> None: def _raise_for_error(self, response: httpx.Response, api_token: str) -> None:
if response.is_success: if response.is_success:
return return
message = response.text.strip() or response.reason_phrase or "NocoBase request failed" message = response.text.strip() or response.reason_phrase or "NocoBase request failed"
raise NocoBaseHttpError( raise NocoBaseHttpError(
response.status_code, response.status_code,
self._redact(message), self._redact(message, api_token),
authentication_failed=_is_authentication_failure(response), authentication_failed=_is_authentication_failure(response),
) )
def _redact(self, value: str) -> str: def _credentials(self) -> tuple[str, str]:
return value.replace(self.api_token, "<redacted>") try:
api_base_url, api_token = (
self._credential_provider()
if self._credential_provider is not None
else (self.api_base_url, self.api_token)
)
except NocoBaseHttpError:
raise
except Exception as error:
raise NocoBaseHttpError(401, str(error), authentication_failed=True) from error
normalized_url = str(api_base_url).strip().rstrip("/")
normalized_token = str(api_token).strip()
if not normalized_url or not normalized_token:
raise NocoBaseHttpError(401, "NocoBase CLI environment is not authenticated", authentication_failed=True)
return normalized_url, normalized_token
@staticmethod
def _redact(value: str, api_token: str) -> str:
return value.replace(api_token, "<redacted>")
def _project_id_segment(project_id: Any) -> str: def _project_id_segment(project_id: Any) -> str:
+132 -63
View File
@@ -5,27 +5,16 @@ import hmac
import os import os
import subprocess import subprocess
import tempfile import tempfile
from collections.abc import Callable
from pathlib import Path from pathlib import Path
from typing import Annotated, Any, Callable from typing import Annotated, Any
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.server import TransportSecuritySettings
from pydantic import Field
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.routing import Mount, Route from starlette.routing import Mount, Route
try:
from pydantic import Field
except ModuleNotFoundError:
def Field(*args: Any, **kwargs: Any) -> Any:
return None
try:
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp.server import TransportSecuritySettings
except ModuleNotFoundError:
FastMCP = None
TransportSecuritySettings = None
from .config import load_config from .config import load_config
from .crm import CrmService from .crm import CrmService
from .nb_client import NbAuthenticationError, NbClient, NbCommandError from .nb_client import NbAuthenticationError, NbClient, NbCommandError
@@ -33,12 +22,12 @@ from .nocobase_http import NocoBaseHttpClient
from .projects import ProjectService from .projects import ProjectService
from .uploads import UploadEndpoint from .uploads import UploadEndpoint
DEFAULT_HTTP_HOST = "127.0.0.1"
DEFAULT_HTTP_HOST = "0.0.0.0"
DEFAULT_HTTP_PORT = 10087 DEFAULT_HTTP_PORT = 10087
DEFAULT_HTTP_PATH = "/mcp" DEFAULT_HTTP_PATH = "/mcp"
DEFAULT_TOKEN_ENV = "NOCOBASE_MCP_TOKEN" DEFAULT_TOKEN_ENV = "NOCOBASE_MCP_TOKEN"
ENABLE_CONFIG_TOOLS_ENV = "NOCOBASE_MCP_ENABLE_CONFIG_TOOLS" ENABLE_CONFIG_TOOLS_ENV = "NOCOBASE_MCP_ENABLE_CONFIG_TOOLS"
ENABLE_RAW_DATA_ENV = "NOCOBASE_MCP_ENABLE_RAW_DATA"
UPLOAD_API_BASE_URL_ENV = "NOCOBASE_MCP_UPLOAD_API_BASE_URL" UPLOAD_API_BASE_URL_ENV = "NOCOBASE_MCP_UPLOAD_API_BASE_URL"
UPLOAD_JWT_ENV = "NOCOBASE_MCP_UPLOAD_JWT" UPLOAD_JWT_ENV = "NOCOBASE_MCP_UPLOAD_JWT"
UPLOAD_MAX_BYTES_ENV = "NOCOBASE_MCP_UPLOAD_MAX_BYTES" UPLOAD_MAX_BYTES_ENV = "NOCOBASE_MCP_UPLOAD_MAX_BYTES"
@@ -108,6 +97,28 @@ def _make_service(config_path: str | Path | None = None) -> CrmService:
return CrmService(config, client) return CrmService(config, client)
def _make_write_services(
config_path: str | Path | None = None,
*,
api_base_url: str | None = None,
api_token: str | None = None,
) -> tuple[CrmService, ProjectService, NocoBaseHttpClient]:
config = load_config(config_path)
nb_bin = os.environ.get("NOCOBASE_MCP_NB_BIN", "").strip() or "nb"
client = NbClient(
env_name=config.env_name,
data_source=config.data_source,
nb_bin=nb_bin,
api_base_url=api_base_url,
api_token=api_token,
)
if api_base_url and api_token:
http_client = NocoBaseHttpClient(api_base_url, api_token)
else:
http_client = NocoBaseHttpClient(credential_provider=client.environment_credentials)
return CrmService(config, client), ProjectService(config.project, client, http_client), http_client
def _safe_tool_result(operation: Callable[[], dict[str, Any]]) -> dict[str, Any]: def _safe_tool_result(operation: Callable[[], dict[str, Any]]) -> dict[str, Any]:
try: try:
return operation() return operation()
@@ -115,7 +126,7 @@ def _safe_tool_result(operation: Callable[[], dict[str, Any]]) -> dict[str, Any]
return _tool_error( return _tool_error(
code="nb_auth_failed", code="nb_auth_failed",
message=str(error), message=str(error),
hint=f"Rotate {API_TOKEN_ENV} in the private service environment file, then restart the service.", hint="Run `nb env auth <configured-env>` on this computer and complete login with your own NocoBase account.",
retryable=False, retryable=False,
) )
except NbCommandError as error: except NbCommandError as error:
@@ -148,6 +159,17 @@ def _safe_tool_result(operation: Callable[[], dict[str, Any]]) -> dict[str, Any]
) )
def _safe_raw_tool_result(include_raw: bool, operation: Callable[[], dict[str, Any]]) -> dict[str, Any]:
if include_raw and os.environ.get(ENABLE_RAW_DATA_ENV, "").strip() != "1":
return _tool_error(
code="raw_data_disabled",
message="Raw NocoBase rows are disabled for this local MCP installation.",
hint=f"Keep include_raw=false, or set {ENABLE_RAW_DATA_ENV}=1 after approving the data exposure policy.",
retryable=False,
)
return _safe_tool_result(operation)
def _tool_error(*, code: str, message: str, hint: str, retryable: bool) -> dict[str, Any]: def _tool_error(*, code: str, message: str, hint: str, retryable: bool) -> dict[str, Any]:
return {"error": {"code": code, "message": message, "hint": hint, "retryable": retryable}} return {"error": {"code": code, "message": message, "hint": hint, "retryable": retryable}}
@@ -202,11 +224,15 @@ def create_server(
def crm_overview( def crm_overview(
date_from: Annotated[ date_from: Annotated[
str | None, str | None,
Field(description="Optional lower bound for follow-up date, in YYYY-MM-DD or an accepted NocoBase date shortcut."), Field(
description="Optional lower bound for follow-up date, in YYYY-MM-DD or an accepted NocoBase date shortcut."
),
] = None, ] = None,
date_to: Annotated[ date_to: Annotated[
str | None, str | None,
Field(description="Optional upper bound for follow-up date, in YYYY-MM-DD or an accepted NocoBase date shortcut."), Field(
description="Optional upper bound for follow-up date, in YYYY-MM-DD or an accepted NocoBase date shortcut."
),
] = None, ] = None,
owner: Annotated[ owner: Annotated[
str | None, str | None,
@@ -311,12 +337,16 @@ def create_server(
def list_customers( def list_customers(
query: Annotated[str | None, Field(description="Optional customer keyword search.")] = None, query: Annotated[str | None, Field(description="Optional customer keyword search.")] = None,
status: Annotated[str | None, Field(description="Optional exact customer status.")] = None, status: Annotated[str | None, Field(description="Optional exact customer status.")] = None,
owner: Annotated[str | None, Field(description="Optional customer owner name, username, email, or English name.")] = None, owner: Annotated[
str | None, Field(description="Optional customer owner name, username, email, or English name.")
] = None,
team: Annotated[str | None, Field(description="Optional CRM team name.")] = None, team: Annotated[str | None, Field(description="Optional CRM team name.")] = None,
source: Annotated[str | None, Field(description="Optional exact customer source.")] = None, source: Annotated[str | None, Field(description="Optional exact customer source.")] = None,
limit: Annotated[int, Field(description="Maximum rows to return. Defaults to 20.")] = 20, limit: Annotated[int, Field(description="Maximum rows to return. Defaults to 20.")] = 20,
offset: Annotated[int, Field(description="Zero-based result offset. Defaults to 0.")] = 0, offset: Annotated[int, Field(description="Zero-based result offset. Defaults to 0.")] = 0,
include_raw: Annotated[bool, Field(description="When true, include the original NocoBase row under raw. Defaults to false.")] = False, include_raw: Annotated[
bool, Field(description="When true, include the original NocoBase row under raw. Defaults to false.")
] = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""List CRM customers with compact rows and pagination metadata. """List CRM customers with compact rows and pagination metadata.
@@ -324,7 +354,8 @@ def create_server(
owners: [{id, name}], managers, createdAt, updatedAt}. Empty values are owners: [{id, name}], managers, createdAt, updatedAt}. Empty values are
omitted. A name shown as "#123" means that customer has no name recorded. omitted. A name shown as "#123" means that customer has no name recorded.
""" """
return _safe_tool_result( return _safe_raw_tool_result(
include_raw,
lambda: service.list_customers( lambda: service.list_customers(
query=query, query=query,
status=status, status=status,
@@ -334,7 +365,7 @@ def create_server(
limit=limit, limit=limit,
offset=offset, offset=offset,
include_raw=include_raw, include_raw=include_raw,
) ),
) )
@server.tool() @server.tool()
@@ -357,19 +388,22 @@ def create_server(
for those. Follow-ups use the same row shape as list_followups. Empty values for those. Follow-ups use the same row shape as list_followups. Empty values
are omitted, so a missing key means empty rather than unknown. are omitted, so a missing key means empty rather than unknown.
""" """
return _safe_tool_result( return _safe_raw_tool_result(
include_raw,
lambda: service.get_customer( lambda: service.get_customer(
customer_id, customer_id,
include_contacts=include_contacts, include_contacts=include_contacts,
include_recent_followups=include_recent_followups, include_recent_followups=include_recent_followups,
followup_limit=followup_limit, followup_limit=followup_limit,
include_raw=include_raw, include_raw=include_raw,
) ),
) )
@server.tool() @server.tool()
def list_contacts( def list_contacts(
customer_id: Annotated[Any | None, Field(description="Optional customer primary key for relation filtering.")] = None, customer_id: Annotated[
Any | None, Field(description="Optional customer primary key for relation filtering.")
] = None,
query: Annotated[str | None, Field(description="Optional contact keyword search.")] = None, query: Annotated[str | None, Field(description="Optional contact keyword search.")] = None,
limit: Annotated[int, Field(description="Maximum rows to return. Defaults to 20.")] = 20, limit: Annotated[int, Field(description="Maximum rows to return. Defaults to 20.")] = 20,
offset: Annotated[int, Field(description="Zero-based result offset. Defaults to 0.")] = 0, offset: Annotated[int, Field(description="Zero-based result offset. Defaults to 0.")] = 0,
@@ -377,7 +411,9 @@ def create_server(
bool, bool,
Field(description="Include contact phone and email. Defaults to true."), Field(description="Include contact phone and email. Defaults to true."),
] = True, ] = True,
include_raw: Annotated[bool, Field(description="When true, include the original NocoBase row under raw. Defaults to false.")] = False, include_raw: Annotated[
bool, Field(description="When true, include the original NocoBase row under raw. Defaults to false.")
] = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""List CRM contacts as compact rows, optionally filtered by customer. """List CRM contacts as compact rows, optionally filtered by customer.
@@ -385,7 +421,8 @@ def create_server(
createdAt, updatedAt}. Set include_details=false to omit phone and email. createdAt, updatedAt}. Set include_details=false to omit phone and email.
Empty values are omitted. Empty values are omitted.
""" """
return _safe_tool_result( return _safe_raw_tool_result(
include_raw,
lambda: service.list_contacts( lambda: service.list_contacts(
customer_id=customer_id, customer_id=customer_id,
query=query, query=query,
@@ -393,14 +430,20 @@ def create_server(
offset=offset, offset=offset,
include_details=include_details, include_details=include_details,
include_raw=include_raw, include_raw=include_raw,
) ),
) )
@server.tool() @server.tool()
def list_followups( def list_followups(
customer_id: Annotated[Any | None, Field(description="Optional customer primary key for relation filtering.")] = None, customer_id: Annotated[
contact_id: Annotated[Any | None, Field(description="Optional contact primary key for relation filtering.")] = None, Any | None, Field(description="Optional customer primary key for relation filtering.")
owner: Annotated[str | None, Field(description="Optional follow-up participant name, username, email, or English name.")] = None, ] = None,
contact_id: Annotated[
Any | None, Field(description="Optional contact primary key for relation filtering.")
] = None,
owner: Annotated[
str | None, Field(description="Optional follow-up participant name, username, email, or English name.")
] = None,
team: Annotated[str | None, Field(description="Optional CRM team name.")] = None, team: Annotated[str | None, Field(description="Optional CRM team name.")] = None,
project_id: Annotated[Any | None, Field(description="Optional CRM project ID.")] = None, project_id: Annotated[Any | None, Field(description="Optional CRM project ID.")] = None,
followup_type: Annotated[str | None, Field(description="Optional exact follow-up type value.")] = None, followup_type: Annotated[str | None, Field(description="Optional exact follow-up type value.")] = None,
@@ -409,7 +452,9 @@ def create_server(
date_to: Annotated[str | None, Field(description="Optional upper follow-up date bound.")] = None, date_to: Annotated[str | None, Field(description="Optional upper follow-up date bound.")] = None,
limit: Annotated[int, Field(description="Maximum rows to return. Defaults to 20.")] = 20, limit: Annotated[int, Field(description="Maximum rows to return. Defaults to 20.")] = 20,
offset: Annotated[int, Field(description="Zero-based result offset. Defaults to 0.")] = 0, offset: Annotated[int, Field(description="Zero-based result offset. Defaults to 0.")] = 0,
include_raw: Annotated[bool, Field(description="When true, include the original NocoBase row under raw. Defaults to false.")] = False, include_raw: Annotated[
bool, Field(description="When true, include the original NocoBase row under raw. Defaults to false.")
] = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""List customer follow-up records as compact rows with pagination metadata. """List customer follow-up records as compact rows with pagination metadata.
@@ -420,7 +465,8 @@ def create_server(
filters expect. creator appears only when the submitter is not also a filters expect. creator appears only when the submitter is not also a
participant. Empty values are omitted. participant. Empty values are omitted.
""" """
return _safe_tool_result( return _safe_raw_tool_result(
include_raw,
lambda: service.list_followups( lambda: service.list_followups(
customer_id=customer_id, customer_id=customer_id,
contact_id=contact_id, contact_id=contact_id,
@@ -434,7 +480,7 @@ def create_server(
limit=limit, limit=limit,
offset=offset, offset=offset,
include_raw=include_raw, include_raw=include_raw,
) ),
) )
@server.tool() @server.tool()
@@ -447,7 +493,8 @@ def create_server(
include_raw: Annotated[bool, Field(description="Include original NocoBase project rows.")] = False, include_raw: Annotated[bool, Field(description="Include original NocoBase project rows.")] = False,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""List CRM projects linked to customers, teams, and project stages.""" """List CRM projects linked to customers, teams, and project stages."""
return _safe_tool_result( return _safe_raw_tool_result(
include_raw,
lambda: service.list_projects( lambda: service.list_projects(
customer_id=customer_id, customer_id=customer_id,
team=team, team=team,
@@ -455,7 +502,7 @@ def create_server(
limit=limit, limit=limit,
offset=offset, offset=offset,
include_raw=include_raw, include_raw=include_raw,
) ),
) )
@server.tool() @server.tool()
@@ -482,9 +529,7 @@ def create_server(
date_to: Annotated[str | None, Field(description="Optional upper event date bound.")] = None, date_to: Annotated[str | None, Field(description="Optional upper event date bound.")] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Return a chronological CRM project timeline from follow-ups and project updates.""" """Return a chronological CRM project timeline from follow-ups and project updates."""
return _safe_tool_result( return _safe_tool_result(lambda: service.get_project_timeline(project_id, date_from=date_from, date_to=date_to))
lambda: service.get_project_timeline(project_id, date_from=date_from, date_to=date_to)
)
@server.tool() @server.tool()
def list_stale_customers( def list_stale_customers(
@@ -513,15 +558,22 @@ def create_server(
def create_project( def create_project(
name: Annotated[str, Field(description="Project name. It must not be blank.")], name: Annotated[str, Field(description="Project name. It must not be blank.")],
customer_id: Annotated[Any | None, Field(description="Optional NocoBase customer ID.")] = None, customer_id: Annotated[Any | None, Field(description="Optional NocoBase customer ID.")] = None,
inquiry_date: Annotated[str | None, Field(description="Optional project inquiry date in YYYY-MM-DD format.")] = None, inquiry_date: Annotated[
progress: Annotated[str | None, Field(description="Optional configured Chinese project progress label.")] = None, str | None, Field(description="Optional project inquiry date in YYYY-MM-DD format.")
] = None,
progress: Annotated[
str | None, Field(description="Optional configured Chinese project progress label.")
] = None,
applications: Annotated[str | None, Field(description="Optional project applications.")] = None, applications: Annotated[str | None, Field(description="Optional project applications.")] = None,
part_numbers: Annotated[str | None, Field(description="Optional project part numbers.")] = None, part_numbers: Annotated[str | None, Field(description="Optional project part numbers.")] = None,
period_usage: Annotated[str | None, Field(description="Optional project period usage.")] = None, period_usage: Annotated[str | None, Field(description="Optional project period usage.")] = None,
details: Annotated[str | None, Field(description="Optional project details.")] = None, details: Annotated[str | None, Field(description="Optional project details.")] = None,
attachment_ids: Annotated[list[Any] | None, Field(description="Optional uploaded attachment IDs to associate.")] = None, attachment_ids: Annotated[
list[Any] | None, Field(description="Optional uploaded attachment IDs to associate.")
] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Create one fixed-schema NocoBase project and optionally associate uploaded attachments.""" """Create one fixed-schema NocoBase project and optionally associate uploaded attachments."""
assert project_service is not None
return _safe_tool_result( return _safe_tool_result(
lambda: project_service.create_project( lambda: project_service.create_project(
name=name, name=name,
@@ -541,16 +593,25 @@ def create_server(
project_id: Annotated[Any, Field(description="NocoBase project ID.")], project_id: Annotated[Any, Field(description="NocoBase project ID.")],
name: Annotated[str | None, Field(description="Optional project name.")] = None, name: Annotated[str | None, Field(description="Optional project name.")] = None,
customer_id: Annotated[Any | None, Field(description="Optional NocoBase customer ID.")] = None, customer_id: Annotated[Any | None, Field(description="Optional NocoBase customer ID.")] = None,
inquiry_date: Annotated[str | None, Field(description="Optional project inquiry date in YYYY-MM-DD format.")] = None, inquiry_date: Annotated[
progress: Annotated[str | None, Field(description="Optional configured Chinese project progress label.")] = None, str | None, Field(description="Optional project inquiry date in YYYY-MM-DD format.")
] = None,
progress: Annotated[
str | None, Field(description="Optional configured Chinese project progress label.")
] = None,
applications: Annotated[str | None, Field(description="Optional project applications.")] = None, applications: Annotated[str | None, Field(description="Optional project applications.")] = None,
part_numbers: Annotated[str | None, Field(description="Optional project part numbers.")] = None, part_numbers: Annotated[str | None, Field(description="Optional project part numbers.")] = None,
period_usage: Annotated[str | None, Field(description="Optional project period usage.")] = None, period_usage: Annotated[str | None, Field(description="Optional project period usage.")] = None,
details: Annotated[str | None, Field(description="Optional project details.")] = None, details: Annotated[str | None, Field(description="Optional project details.")] = None,
add_attachment_ids: Annotated[list[Any] | None, Field(description="Attachment IDs to add incrementally.")] = None, add_attachment_ids: Annotated[
remove_attachment_ids: Annotated[list[Any] | None, Field(description="Attachment IDs to remove incrementally.")] = None, list[Any] | None, Field(description="Attachment IDs to add incrementally.")
] = None,
remove_attachment_ids: Annotated[
list[Any] | None, Field(description="Attachment IDs to remove incrementally.")
] = None,
) -> dict[str, Any]: ) -> dict[str, Any]:
"""Update fixed project fields and incrementally add or remove uploaded attachments.""" """Update fixed project fields and incrementally add or remove uploaded attachments."""
assert project_service is not None
return _safe_tool_result( return _safe_tool_result(
lambda: project_service.update_project( lambda: project_service.update_project(
project_id, project_id,
@@ -591,7 +652,7 @@ def create_http_app(server: Any, token: str, upload_endpoint: UploadEndpoint | N
return BearerTokenAuthMiddleware(app, token) return BearerTokenAuthMiddleware(app, token)
def _read_nocobase_environment() -> tuple[str, str, int]: def _read_nocobase_environment() -> tuple[str | None, str | None, int]:
canonical_url = os.environ.get(API_BASE_URL_ENV, "").strip().rstrip("/") canonical_url = os.environ.get(API_BASE_URL_ENV, "").strip().rstrip("/")
canonical_token = os.environ.get(API_TOKEN_ENV, "").strip() canonical_token = os.environ.get(API_TOKEN_ENV, "").strip()
legacy_url = os.environ.get(UPLOAD_API_BASE_URL_ENV, "").strip().rstrip("/") legacy_url = os.environ.get(UPLOAD_API_BASE_URL_ENV, "").strip().rstrip("/")
@@ -610,7 +671,7 @@ def _read_nocobase_environment() -> tuple[str, str, int]:
elif legacy_any: elif legacy_any:
api_base_url, api_token = legacy_url, legacy_token api_base_url, api_token = legacy_url, legacy_token
else: else:
raise RuntimeError(f"{API_BASE_URL_ENV} and {API_TOKEN_ENV} must be set for HTTP write mode") api_base_url, api_token = None, None
raw_max_bytes = os.environ.get(UPLOAD_MAX_BYTES_ENV, str(DEFAULT_UPLOAD_MAX_BYTES)).strip() raw_max_bytes = os.environ.get(UPLOAD_MAX_BYTES_ENV, str(DEFAULT_UPLOAD_MAX_BYTES)).strip()
try: try:
@@ -636,18 +697,11 @@ def run_http_server(
token = _read_token_from_env(token_env) token = _read_token_from_env(token_env)
api_base_url, api_token, max_bytes = _read_nocobase_environment() api_base_url, api_token, max_bytes = _read_nocobase_environment()
config = load_config(config_path) crm_service, project_service, http_client = _make_write_services(
nb_bin = os.environ.get("NOCOBASE_MCP_NB_BIN", "").strip() or "nb" config_path,
nb_client = NbClient(
env_name=config.env_name,
data_source=config.data_source,
nb_bin=nb_bin,
api_base_url=api_base_url, api_base_url=api_base_url,
api_token=api_token, api_token=api_token,
) )
crm_service = CrmService(config, nb_client)
http_client = NocoBaseHttpClient(api_base_url, api_token)
project_service = ProjectService(config.project, nb_client, http_client)
server = create_server( server = create_server(
crm_service=crm_service, crm_service=crm_service,
project_service=project_service, project_service=project_service,
@@ -664,7 +718,7 @@ def run_http_server(
def main() -> None: def main() -> None:
parser = argparse.ArgumentParser(description="Run the read-only NocoBase CRM MCP server.") parser = argparse.ArgumentParser(description="Run the local NocoBase CRM MCP server.")
parser.add_argument("--config", default=None, type=Path, help="CRM mapping JSON config path.") parser.add_argument("--config", default=None, type=Path, help="CRM mapping JSON config path.")
parser.add_argument( parser.add_argument(
"--transport", "--transport",
@@ -675,12 +729,27 @@ def main() -> None:
parser.add_argument("--host", default=DEFAULT_HTTP_HOST, help="HTTP bind host.") parser.add_argument("--host", default=DEFAULT_HTTP_HOST, help="HTTP bind host.")
parser.add_argument("--port", default=DEFAULT_HTTP_PORT, type=int, help="HTTP port.") parser.add_argument("--port", default=DEFAULT_HTTP_PORT, type=int, help="HTTP port.")
parser.add_argument("--path", default=DEFAULT_HTTP_PATH, help="Streamable HTTP MCP path.") parser.add_argument("--path", default=DEFAULT_HTTP_PATH, help="Streamable HTTP MCP path.")
parser.add_argument("--token-env", default=DEFAULT_TOKEN_ENV, help="Environment variable containing the HTTP bearer token.") parser.add_argument(
"--token-env", default=DEFAULT_TOKEN_ENV, help="Environment variable containing the HTTP bearer token."
)
parser.add_argument("--allowed-host", action="append", dest="allowed_hosts") parser.add_argument("--allowed-host", action="append", dest="allowed_hosts")
parser.add_argument("--allowed-origin", action="append", dest="allowed_origins") parser.add_argument("--allowed-origin", action="append", dest="allowed_origins")
parser.add_argument(
"--enable-writes",
action="store_true",
help="Expose fixed-schema project create/update tools in local stdio mode.",
)
args = parser.parse_args() args = parser.parse_args()
if args.transport == "stdio": if args.transport == "stdio":
create_server(config_path=args.config).run(transport="stdio") if args.enable_writes:
crm_service, project_service, _http_client = _make_write_services(args.config)
create_server(
crm_service=crm_service,
project_service=project_service,
readonly=False,
).run(transport="stdio")
else:
create_server(config_path=args.config).run(transport="stdio")
return return
run_http_server( run_http_server(
config_path=args.config, config_path=args.config,
+1 -4
View File
@@ -12,7 +12,6 @@ from starlette.responses import JSONResponse
from .nocobase_http import NocoBaseHttpError from .nocobase_http import NocoBaseHttpError
_COPY_CHUNK_BYTES = 1024 * 1024 _COPY_CHUNK_BYTES = 1024 * 1024
_MULTIPART_OVERHEAD_BYTES = 64 * 1024 _MULTIPART_OVERHEAD_BYTES = 64 * 1024
@@ -83,9 +82,7 @@ class UploadEndpoint:
attachment_id = _attachment_id(attachment) attachment_id = _attachment_id(attachment)
if attachment_id is None: if attachment_id is None:
return _error_response("nb_upload_failed", "NocoBase attachment upload failed", 502) return _error_response("nb_upload_failed", "NocoBase attachment upload failed", 502)
return JSONResponse( return JSONResponse({"upload_id": attachment_id, "filename": filename, "size": size, "mimetype": mimetype})
{"upload_id": attachment_id, "filename": filename, "size": size, "mimetype": mimetype}
)
except NocoBaseHttpError as error: except NocoBaseHttpError as error:
code = "nb_upload_unauthorized" if error.authentication_failed else "nb_upload_failed" code = "nb_upload_unauthorized" if error.authentication_failed else "nb_upload_failed"
return _error_response(code, "NocoBase attachment upload failed", 502) return _error_response(code, "NocoBase attachment upload failed", 502)
+29 -2
View File
@@ -1,14 +1,22 @@
[project] [project]
name = "nocobase-crm-mcp" name = "nocobase-crm-mcp"
version = "0.1.0" version = "0.2.0"
description = "Read-only MCP server for querying NocoBase CRM data through nb CLI." description = "Local, ACL-aware MCP server for NocoBase CRM through each user's nb CLI login."
requires-python = ">=3.12" requires-python = ">=3.12"
dependencies = [ dependencies = [
"cryptography>=50,<51",
"httpx>=0.27,<1", "httpx>=0.27,<1",
"mcp[cli]>=1.27,<2", "mcp[cli]>=1.27,<2",
"python-multipart>=0.0.9,<1", "python-multipart>=0.0.9,<1",
] ]
[dependency-groups]
dev = [
"mypy>=1.17,<2",
"pip-audit>=2.9,<3",
"ruff>=0.12,<1",
]
[project.scripts] [project.scripts]
nocobase-crm-mcp = "nocobase_crm_mcp.server:main" nocobase-crm-mcp = "nocobase_crm_mcp.server:main"
@@ -18,3 +26,22 @@ build-backend = "setuptools.build_meta"
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
include = ["nocobase_crm_mcp*"] include = ["nocobase_crm_mcp*"]
[tool.ruff]
target-version = "py312"
line-length = 120
[tool.ruff.lint]
select = ["E4", "E7", "E9", "F", "I", "UP"]
[tool.ruff.lint.per-file-ignores]
"tests/test_mcp_service.py" = ["E402"]
[tool.mypy]
python_version = "3.12"
files = ["nocobase_crm_mcp"]
check_untyped_defs = true
ignore_missing_imports = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
+38 -22
View File
@@ -13,14 +13,13 @@ import subprocess
import time import time
import urllib.error import urllib.error
import urllib.request import urllib.request
from collections.abc import Sequence
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import Sequence
SERVICE_NAME = "nocobase-crm-mcp.service" SERVICE_NAME = "nocobase-crm-mcp.service"
ENV_RELATIVE_PATH = Path(".runtime") / "nocobase-crm-mcp.env" ENV_RELATIVE_PATH = Path(".runtime") / "nocobase-crm-mcp.env"
DEFAULT_HOST = "0.0.0.0" DEFAULT_HOST = "127.0.0.1"
DEFAULT_PORT = 10087 DEFAULT_PORT = 10087
DEFAULT_PATH = "/mcp" DEFAULT_PATH = "/mcp"
DEFAULT_NB_ENV = "nb-mcp" DEFAULT_NB_ENV = "nb-mcp"
@@ -80,7 +79,9 @@ def find_nb_path() -> Path:
raise RuntimeError(f"NOCOBASE_MCP_NB_BIN points to {candidate_path}, but it is not an executable file.") raise RuntimeError(f"NOCOBASE_MCP_NB_BIN points to {candidate_path}, but it is not an executable file.")
discovered = shutil.which(candidate) discovered = shutil.which(candidate)
if not discovered: if not discovered:
raise RuntimeError("Could not find nb on PATH. Install the NocoBase CLI or set NOCOBASE_MCP_NB_BIN to its absolute path.") raise RuntimeError(
"Could not find nb on PATH. Install the NocoBase CLI or set NOCOBASE_MCP_NB_BIN to its absolute path."
)
return Path(os.path.abspath(str(Path(discovered).expanduser()))) return Path(os.path.abspath(str(Path(discovered).expanduser())))
@@ -213,9 +214,13 @@ def saved_service_config(project_root: Path) -> tuple[str, str, int, str, str, s
return token, host, port, path, nb_env, config_path, nb_bin return token, host, port, path, nb_env, config_path, nb_bin
def write_service_unit(paths: ServicePaths, *, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, path: str = DEFAULT_PATH) -> None: def write_service_unit(
paths: ServicePaths, *, host: str = DEFAULT_HOST, port: int = DEFAULT_PORT, path: str = DEFAULT_PATH
) -> None:
paths.unit_path.parent.mkdir(parents=True, exist_ok=True) paths.unit_path.parent.mkdir(parents=True, exist_ok=True)
unit = render_service_unit(project_root=paths.project_root, env_file=paths.env_path, host=host, port=port, path=path) unit = render_service_unit(
project_root=paths.project_root, env_file=paths.env_path, host=host, port=port, path=path
)
paths.unit_path.write_text(unit, encoding="utf-8") paths.unit_path.write_text(unit, encoding="utf-8")
@@ -250,7 +255,7 @@ def render_remote_config(
path: str = DEFAULT_PATH, path: str = DEFAULT_PATH,
service_status: str | None = None, service_status: str | None = None,
) -> str: ) -> str:
host = hosts[0] if hosts else "<server-ip>" host = hosts[0] if hosts else "127.0.0.1"
url = f"http://{host}:{port}{path}" url = f"http://{host}:{port}{path}"
alternatives = "\n".join(f"- http://{candidate}:{port}{path}" for candidate in hosts[1:]) alternatives = "\n".join(f"- http://{candidate}:{port}{path}" for candidate in hosts[1:])
alternatives_section = f"\nOther detected addresses:\n{alternatives}\n" if alternatives else "" alternatives_section = f"\nOther detected addresses:\n{alternatives}\n" if alternatives else ""
@@ -259,7 +264,7 @@ def render_remote_config(
"Saved MCP client config.\n" "Saved MCP client config.\n"
"\n" "\n"
f"{status_section}" f"{status_section}"
"Use this URL from another machine:\n" "Use this loopback URL on this machine:\n"
f"{url}\n" f"{url}\n"
f"{alternatives_section}" f"{alternatives_section}"
"\n" "\n"
@@ -282,9 +287,10 @@ def render_remote_config(
def show_config(project_root: Path) -> str: def show_config(project_root: Path) -> str:
token, _host, port, path, _nb_env, _config_path, _nb_bin = saved_service_config(project_root) token, host, port, path, _nb_env, _config_path, _nb_bin = saved_service_config(project_root)
display_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
return render_remote_config( return render_remote_config(
hosts=detect_remote_hosts(), hosts=[display_host],
token=token, token=token,
port=port, port=port,
path=path, path=path,
@@ -372,7 +378,8 @@ def new_token(
nb_bin=find_nb_path(), nb_bin=find_nb_path(),
) )
restart_service_if_installed(paths) restart_service_if_installed(paths)
return render_remote_config(hosts=detect_remote_hosts(), token=token, port=port, path=path, service_status=service_state()) display_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
return render_remote_config(hosts=[display_host], token=token, port=port, path=path, service_status=service_state())
def start_service(*, project_root: Path) -> str: def start_service(*, project_root: Path) -> str:
@@ -411,7 +418,8 @@ def start_service(*, project_root: Path) -> str:
probe = wait_for_http_service("127.0.0.1", port, path, token=token) probe = wait_for_http_service("127.0.0.1", port, path, token=token)
if not probe.get("reachable"): if not probe.get("reachable"):
raise RuntimeError(f"{SERVICE_NAME} is active but HTTP probe failed: {probe.get('error')}") raise RuntimeError(f"{SERVICE_NAME} is active but HTTP probe failed: {probe.get('error')}")
return render_remote_config(hosts=detect_remote_hosts(), token=token, port=port, path=path, service_status="active") display_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
return render_remote_config(hosts=[display_host], token=token, port=port, path=path, service_status="active")
def stop_service() -> None: def stop_service() -> None:
@@ -443,11 +451,11 @@ def service_status(*, project_root: Path) -> str:
] ]
if paths.env_path.exists(): if paths.env_path.exists():
values = read_service_env_file(paths.env_path) values = read_service_env_file(paths.env_path)
token, _host, port, path, _nb_env, _config_path, nb_bin = saved_service_config(project_root) token, host, port, path, nb_env, _config_path, nb_bin = saved_service_config(project_root)
hosts = detect_remote_hosts() display_host = "127.0.0.1" if host in {"0.0.0.0", "::"} else host
host = hosts[0] if hosts else "<server-ip>" lines.append(f"MCP URL: http://{display_host}:{port}{path}")
lines.append(f"MCP URL: http://{host}:{port}{path}")
lines.append(f"nb binary: {nb_bin}") lines.append(f"nb binary: {nb_bin}")
lines.append(f"NocoBase CLI env: {nb_env} (authenticate with `nb env auth {nb_env}`)")
canonical_configured = bool(values.get(API_BASE_URL_ENV) and values.get(API_TOKEN_ENV)) canonical_configured = bool(values.get(API_BASE_URL_ENV) and values.get(API_TOKEN_ENV))
legacy_configured = bool(values.get(LEGACY_API_BASE_URL_ENV) and values.get(LEGACY_API_TOKEN_ENV)) legacy_configured = bool(values.get(LEGACY_API_BASE_URL_ENV) and values.get(LEGACY_API_TOKEN_ENV))
if canonical_configured: if canonical_configured:
@@ -460,8 +468,8 @@ def service_status(*, project_root: Path) -> str:
): ):
credential_state = "incomplete" credential_state = "incomplete"
else: else:
credential_state = "not configured" credential_state = "not configured; using the local nb CLI login"
lines.append(f"NocoBase API credentials: {credential_state}") lines.append(f"Legacy NocoBase credential override: {credential_state}")
probe = probe_http_service("127.0.0.1", port, path, token=token) probe = probe_http_service("127.0.0.1", port, path, token=token)
if probe.get("reachable"): if probe.get("reachable"):
lines.append(f"HTTP probe: reachable (status {probe.get('status')})") lines.append(f"HTTP probe: reachable (status {probe.get('status')})")
@@ -477,7 +485,9 @@ def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Manage the HTTP NocoBase CRM MCP user service.") parser = argparse.ArgumentParser(description="Manage the HTTP NocoBase CRM MCP user service.")
subparsers = parser.add_subparsers(dest="command", required=True) subparsers = parser.add_subparsers(dest="command", required=True)
new_token_parser = subparsers.add_parser("new-token", help="Generate a new token and print remote MCP client configuration.") new_token_parser = subparsers.add_parser(
"new-token", help="Generate a new token and print local MCP client configuration."
)
new_token_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path) new_token_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
new_token_parser.add_argument("--host", default=DEFAULT_HOST) new_token_parser.add_argument("--host", default=DEFAULT_HOST)
new_token_parser.add_argument("--port", default=DEFAULT_PORT, type=int) new_token_parser.add_argument("--port", default=DEFAULT_PORT, type=int)
@@ -486,15 +496,21 @@ def main(argv: Sequence[str] | None = None) -> int:
new_token_parser.add_argument("--config-path", default=DEFAULT_CONFIG_PATH) new_token_parser.add_argument("--config-path", default=DEFAULT_CONFIG_PATH)
new_token_parser.add_argument("--token-bytes", default=DEFAULT_TOKEN_BYTES, type=int) new_token_parser.add_argument("--token-bytes", default=DEFAULT_TOKEN_BYTES, type=int)
start_parser = subparsers.add_parser("start", help="Install or update the user service using the saved token, then start it.") start_parser = subparsers.add_parser(
"start", help="Install or update the user service using the saved token, then start it."
)
start_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path) start_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
subparsers.add_parser("stop", help="Stop the user service without deleting service files.") subparsers.add_parser("stop", help="Stop the user service without deleting service files.")
uninstall_parser = subparsers.add_parser("uninstall", help="Stop and remove the managed user service and token file.") uninstall_parser = subparsers.add_parser(
"uninstall", help="Stop and remove the managed user service and token file."
)
uninstall_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path) uninstall_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
show_config_parser = subparsers.add_parser("show-config", help="Print the saved token and remote MCP client configuration.") show_config_parser = subparsers.add_parser(
"show-config", help="Print the saved token and local MCP client configuration."
)
show_config_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path) show_config_parser.add_argument("--project-root", default=Path(__file__).resolve().parent.parent, type=Path)
status_parser = subparsers.add_parser("status", help="Show systemd status for the managed service.") status_parser = subparsers.add_parser("status", help="Show systemd status for the managed service.")
+64 -3
View File
@@ -1,7 +1,7 @@
import unittest
import json import json
import re import re
import tempfile import tempfile
import unittest
from pathlib import Path from pathlib import Path
from nocobase_crm_mcp.config import load_config from nocobase_crm_mcp.config import load_config
@@ -200,7 +200,7 @@ class FakeNbClient:
{"name": "gjjl_client", "title": "客户", "type": "belongsToMany"}, {"name": "gjjl_client", "title": "客户", "type": "belongsToMany"},
{"name": "gjjl_client_contacts", "title": "客户联系人", "type": "belongsToMany"}, {"name": "gjjl_client_contacts", "title": "客户联系人", "type": "belongsToMany"},
{"name": "gjjl_followup_person", "title": "参与跟进人员", "type": "belongsToMany"}, {"name": "gjjl_followup_person", "title": "参与跟进人员", "type": "belongsToMany"},
{"name": "createdBy", "title": "{{t(\"Created by\")}}", "type": "belongsTo"}, {"name": "createdBy", "title": '{{t("Created by")}}', "type": "belongsTo"},
{"name": "f_srysmjkwfq1", "title": "跟进时间", "type": "date"}, {"name": "f_srysmjkwfq1", "title": "跟进时间", "type": "date"},
{"name": "f_19iplxz0nds", "title": "跟进内容", "type": "text"}, {"name": "f_19iplxz0nds", "title": "跟进内容", "type": "text"},
] ]
@@ -283,7 +283,13 @@ class CrmServiceTests(unittest.TestCase):
# Personal contact details do not belong in a customer overview. # Personal contact details do not belong in a customer overview.
self.assertNotIn("phone", result["contacts"][0]) self.assertNotIn("phone", result["contacts"][0])
self.assertNotIn("email", result["contacts"][0]) self.assertNotIn("email", result["contacts"][0])
self.assertEqual(
result["contacts_pagination"],
{"total": 1, "returned": 1, "limit": 50, "offset": 0, "has_more": False, "next_offset": None},
)
self.assertEqual(result["recent_followups"][0]["id"], 1) self.assertEqual(result["recent_followups"][0]["id"], 1)
self.assertEqual(result["recent_followups_pagination"]["limit"], 5)
self.assertFalse(result["recent_followups_pagination"]["has_more"])
self.assertEqual( self.assertEqual(
result["recent_followups"][0]["content"], result["recent_followups"][0]["content"],
"Called customer about sample status and confirmed next procurement meeting.", "Called customer about sample status and confirmed next procurement meeting.",
@@ -305,7 +311,10 @@ class CrmServiceTests(unittest.TestCase):
self.assertEqual(result["totals"]["contacts"], 3) self.assertEqual(result["totals"]["contacts"], 3)
self.assertEqual(result["totals"]["followups"], 3) self.assertEqual(result["totals"]["followups"], 3)
self.assertEqual(result["customer_status"][0]["label"], "已下单") self.assertEqual(result["customer_status"][0]["label"], "已下单")
self.assertEqual(result["recent_followups"][0]["content"], "Called customer about sample status and confirmed next procurement meeting.") self.assertEqual(
result["recent_followups"][0]["content"],
"Called customer about sample status and confirmed next procurement meeting.",
)
self.assertEqual(result["customer_owner"][0]["owner_id"], 54) self.assertEqual(result["customer_owner"][0]["owner_id"], 54)
self.assertGreaterEqual(len(client.query_calls), 3) self.assertGreaterEqual(len(client.query_calls), 3)
self.assertEqual(client.query_calls[0][0], "customers") self.assertEqual(client.query_calls[0][0], "customers")
@@ -743,6 +752,31 @@ class CrmServiceTests(unittest.TestCase):
) )
self.assertEqual(result["results"][0]["owners"], [{"id": 54, "name": "Alice Wang"}]) self.assertEqual(result["results"][0]["owners"], [{"id": 54, "name": "Alice Wang"}])
def test_team_user_resolution_pages_past_one_hundred_members(self) -> None:
class PagedUsersClient(FakeNbClient):
def list_records(self, resource, **kwargs):
self.list_calls.append((resource, kwargs))
if resource != "users":
return super().list_records(resource, **kwargs)
page = kwargs["page"]
page_size = kwargs["page_size"]
start = (page - 1) * page_size
rows = [
{"id": index, "nickname": f"User {index}", "yh_status": "nkluuumb1r1"}
for index in range(start + 1, min(start + page_size, 150) + 1)
]
return {"data": rows, "meta": {"count": 150}}
client = PagedUsersClient()
service = CrmService(load_config(None), client)
team_row = {"tdgl_team_members": [{"id": index} for index in range(1, 151)]}
users = service._team_users(team_row, "tdgl_team_members", formal_only=True)
self.assertEqual(len(users), 150)
user_calls = [kwargs for resource, kwargs in client.list_calls if resource == "users"]
self.assertEqual([call["page"] for call in user_calls], [1, 2])
def test_project_query_and_timeline_use_crm_project_relations(self) -> None: def test_project_query_and_timeline_use_crm_project_relations(self) -> None:
class ProjectClient(FakeNbClient): class ProjectClient(FakeNbClient):
project = { project = {
@@ -783,6 +817,7 @@ class CrmServiceTests(unittest.TestCase):
service = CrmService(load_config(None), client) service = CrmService(load_config(None), client)
listed = service.list_projects(stage="已报价") listed = service.list_projects(stage="已报价")
detail = service.get_project(41)
timeline = service.get_project_timeline(41) timeline = service.get_project_timeline(41)
self.assertEqual(listed["results"][0]["stage"], "已报价") self.assertEqual(listed["results"][0]["stage"], "已报价")
@@ -791,6 +826,8 @@ class CrmServiceTests(unittest.TestCase):
project_call = next(kwargs for resource, kwargs in client.list_calls if resource == "t_projects") project_call = next(kwargs for resource, kwargs in client.list_calls if resource == "t_projects")
self.assertNotIn("appends", project_call) self.assertNotIn("appends", project_call)
self.assertIn("f_projects_client.f_q2q60q7bqbg", project_call["fields"]) self.assertIn("f_projects_client.f_q2q60q7bqbg", project_call["fields"])
self.assertEqual(detail["followups_pagination"]["total"], 1)
self.assertFalse(detail["followups_pagination"]["has_more"])
self.assertEqual( self.assertEqual(
{event["event_type"] for event in timeline["events"]}, {event["event_type"] for event in timeline["events"]},
{"followup", "project_update"}, {"followup", "project_update"},
@@ -816,5 +853,29 @@ class CrmServiceTests(unittest.TestCase):
self.assertEqual(result["results"][0]["customer"]["id"], 7) self.assertEqual(result["results"][0]["customer"]["id"], 7)
self.assertEqual(result["results"][0]["days_since_last_followup"], 33) self.assertEqual(result["results"][0]["days_since_last_followup"], 33)
def test_scan_limit_returns_explicit_continuation_metadata(self) -> None:
class ThreeRowClient(FakeNbClient):
def list_records(self, resource, **kwargs):
page = kwargs["page"]
page_size = kwargs["page_size"]
start = (page - 1) * page_size
return {
"data": [{"id": index} for index in range(start + 1, min(start + page_size, 3) + 1)],
"meta": {"count": 3},
}
rows, truncation = CrmService(load_config(None), ThreeRowClient())._scan_records(
"customers",
page_size=2,
max_records=2,
)
self.assertEqual([row["id"] for row in rows], [1, 2])
self.assertEqual(
truncation,
{"total": 3, "returned": 2, "limit": 2, "next_offset": 2},
)
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()
+24 -14
View File
@@ -1,13 +1,12 @@
import os import os
import stat import stat
import subprocess import subprocess
import sys
import tempfile import tempfile
import unittest import unittest
from pathlib import Path from pathlib import Path
from unittest.mock import call, patch from unittest.mock import call, patch
import sys
SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts" SCRIPTS_DIR = Path(__file__).resolve().parent.parent / "scripts"
if str(SCRIPTS_DIR) not in sys.path: if str(SCRIPTS_DIR) not in sys.path:
sys.path.insert(0, str(SCRIPTS_DIR)) sys.path.insert(0, str(SCRIPTS_DIR))
@@ -47,7 +46,7 @@ class McpServiceConfigTests(unittest.TestCase):
self.assertIn("EnvironmentFile=/opt/nocobase-mcp/.runtime/nocobase-crm-mcp.env", unit) self.assertIn("EnvironmentFile=/opt/nocobase-mcp/.runtime/nocobase-crm-mcp.env", unit)
self.assertIn("/home/nac/.local/bin/uv run --no-sync nocobase-crm-mcp", unit) self.assertIn("/home/nac/.local/bin/uv run --no-sync nocobase-crm-mcp", unit)
self.assertIn("--transport streamable-http", unit) self.assertIn("--transport streamable-http", unit)
self.assertIn("--host 0.0.0.0", unit) self.assertIn("--host 127.0.0.1", unit)
self.assertIn("--port 10087", unit) self.assertIn("--port 10087", unit)
self.assertNotIn("NOCOBASE_MCP_TOKEN=", unit) self.assertNotIn("NOCOBASE_MCP_TOKEN=", unit)
@@ -103,17 +102,26 @@ class McpServiceConfigTests(unittest.TestCase):
with self.assertRaisesRegex(RuntimeError, "not an executable"): with self.assertRaisesRegex(RuntimeError, "not an executable"):
manage_mcp_service.find_nb_path() manage_mcp_service.find_nb_path()
with patch.dict("os.environ", {"NOCOBASE_MCP_NB_BIN": "nb-custom"}), patch.object( with (
manage_mcp_service.shutil, patch.dict("os.environ", {"NOCOBASE_MCP_NB_BIN": "nb-custom"}),
"which", patch.object(
return_value="/usr/local/bin/nb-custom", manage_mcp_service.shutil,
"which",
return_value="/usr/local/bin/nb-custom",
),
): ):
self.assertEqual(manage_mcp_service.find_nb_path(), Path("/usr/local/bin/nb-custom")) self.assertEqual(manage_mcp_service.find_nb_path(), Path("/usr/local/bin/nb-custom"))
with patch.dict("os.environ", {}, clear=True), patch.object(manage_mcp_service.shutil, "which", return_value="/usr/local/bin/nb"): with (
patch.dict("os.environ", {}, clear=True),
patch.object(manage_mcp_service.shutil, "which", return_value="/usr/local/bin/nb"),
):
self.assertEqual(manage_mcp_service.find_nb_path(), Path("/usr/local/bin/nb")) self.assertEqual(manage_mcp_service.find_nb_path(), Path("/usr/local/bin/nb"))
with patch.dict("os.environ", {}, clear=True), patch.object(manage_mcp_service.shutil, "which", return_value=None): with (
patch.dict("os.environ", {}, clear=True),
patch.object(manage_mcp_service.shutil, "which", return_value=None),
):
with self.assertRaisesRegex(RuntimeError, "Could not find nb"): with self.assertRaisesRegex(RuntimeError, "Could not find nb"):
manage_mcp_service.find_nb_path() manage_mcp_service.find_nb_path()
@@ -149,7 +157,7 @@ class McpServiceConfigTests(unittest.TestCase):
self.assertIn("NOCOBASE_MCP_API_BASE_URL=https://nocobase.example/api\n", content) self.assertIn("NOCOBASE_MCP_API_BASE_URL=https://nocobase.example/api\n", content)
self.assertIn("NOCOBASE_MCP_API_TOKEN=operator-api-key\n", content) self.assertIn("NOCOBASE_MCP_API_TOKEN=operator-api-key\n", content)
def test_render_remote_config_prints_url_and_authorization_header(self) -> None: def test_render_http_config_prints_url_and_authorization_header(self) -> None:
output = manage_mcp_service.render_remote_config( output = manage_mcp_service.render_remote_config(
hosts=["192.168.1.10"], hosts=["192.168.1.10"],
token="test-token", token="test-token",
@@ -164,6 +172,7 @@ class McpServiceConfigTests(unittest.TestCase):
self.assertIn('"type": "streamable-http"', output) self.assertIn('"type": "streamable-http"', output)
self.assertIn("Service status: inactive", output) self.assertIn("Service status: inactive", output)
self.assertNotIn("MCP service is running.", output) self.assertNotIn("MCP service is running.", output)
self.assertIn("Use this loopback URL on this machine", output)
def test_new_token_writes_token_without_touching_systemd_when_service_is_not_installed(self) -> None: def test_new_token_writes_token_without_touching_systemd_when_service_is_not_installed(self) -> None:
with tempfile.TemporaryDirectory() as tmp: with tempfile.TemporaryDirectory() as tmp:
@@ -184,7 +193,7 @@ class McpServiceConfigTests(unittest.TestCase):
self.assertIn("NOCOBASE_MCP_TOKEN=new-token\n", env_content) self.assertIn("NOCOBASE_MCP_TOKEN=new-token\n", env_content)
self.assertIn("NOCOBASE_MCP_PORT=10087\n", env_content) self.assertIn("NOCOBASE_MCP_PORT=10087\n", env_content)
self.assertIn("NOCOBASE_MCP_NB_BIN=/opt/node/bin/nb\n", env_content) self.assertIn("NOCOBASE_MCP_NB_BIN=/opt/node/bin/nb\n", env_content)
self.assertIn("http://10.0.0.2:10087/mcp", output) self.assertIn("http://127.0.0.1:10087/mcp", output)
run_systemctl.assert_not_called() run_systemctl.assert_not_called()
def test_new_token_preserves_operator_upload_settings_and_other_env_lines(self) -> None: def test_new_token_preserves_operator_upload_settings_and_other_env_lines(self) -> None:
@@ -252,7 +261,7 @@ class McpServiceConfigTests(unittest.TestCase):
self.assertIn("/home/nac/.local/bin/uv run --no-sync nocobase-crm-mcp", unit_content) self.assertIn("/home/nac/.local/bin/uv run --no-sync nocobase-crm-mcp", unit_content)
self.assertIn("EnvironmentFile=", unit_content) self.assertIn("EnvironmentFile=", unit_content)
self.assertIn("NOCOBASE_MCP_NB_BIN=/opt/node/bin/nb\n", env_content) self.assertIn("NOCOBASE_MCP_NB_BIN=/opt/node/bin/nb\n", env_content)
self.assertIn("http://10.0.0.2:10087/mcp", output) self.assertIn("http://127.0.0.1:10087/mcp", output)
self.assertIn("Bearer saved-token", output) self.assertIn("Bearer saved-token", output)
run_systemctl.assert_has_calls( run_systemctl.assert_has_calls(
[ [
@@ -400,7 +409,8 @@ class McpServiceConfigTests(unittest.TestCase):
self.assertIn("Service status: active", output) self.assertIn("Service status: active", output)
self.assertIn("Unit file:", output) self.assertIn("Unit file:", output)
self.assertIn("Env file:", output) self.assertIn("Env file:", output)
self.assertIn("http://10.0.0.2:10087/mcp", output) self.assertIn("http://127.0.0.1:10087/mcp", output)
self.assertIn("NocoBase CLI env: nb-mcp", output)
self.assertIn("HTTP probe: reachable", output) self.assertIn("HTTP probe: reachable", output)
def test_service_status_reports_canonical_credentials_without_exposing_them(self) -> None: def test_service_status_reports_canonical_credentials_without_exposing_them(self) -> None:
@@ -424,7 +434,7 @@ class McpServiceConfigTests(unittest.TestCase):
): ):
output = manage_mcp_service.service_status(project_root=project_root) output = manage_mcp_service.service_status(project_root=project_root)
self.assertIn("NocoBase API credentials: configured (canonical)", output) self.assertIn("Legacy NocoBase credential override: configured (canonical)", output)
self.assertNotIn("never-print-this", output) self.assertNotIn("never-print-this", output)
self.assertNotIn("https://nb.example/api", output) self.assertNotIn("https://nb.example/api", output)
+53 -1
View File
@@ -95,6 +95,55 @@ class NbClientTests(unittest.TestCase):
self.assertEqual(commands[0][commands[0].index("--page-size") + 1], "1") self.assertEqual(commands[0][commands[0].index("--page-size") + 1], "1")
self.assertEqual(commands[0][commands[0].index("--token") + 1], "long-lived-key") self.assertEqual(commands[0][commands[0].index("--token") + 1], "long-lived-key")
def test_environment_credentials_reads_saved_user_login_without_token_in_argv(self) -> None:
commands: list[list[str]] = []
def runner(cmd, **kwargs):
commands.append(cmd)
return subprocess.CompletedProcess(
cmd,
0,
stdout=json.dumps(
{
"api": {
"apiBaseUrl": "https://nb.example/api/",
"auth": {"type": "oauth", "accessToken": "user-access-token"},
}
}
),
stderr="",
)
credentials = NbClient(env_name="customer-prod", runner=runner).environment_credentials()
self.assertEqual(credentials, ("https://nb.example/api", "user-access-token"))
self.assertEqual(
commands[0],
["nb", "env", "info", "customer-prod", "--json", "--show-secrets"],
)
self.assertNotIn("user-access-token", commands[0])
def test_environment_credentials_requires_the_users_cli_login(self) -> None:
def runner(cmd, **kwargs):
return subprocess.CompletedProcess(
cmd,
0,
stdout=json.dumps(
{
"api": {
"apiBaseUrl": "https://nb.example/api",
"auth": {"type": "oauth", "accessToken": "-"},
}
}
),
stderr="",
)
with self.assertRaises(NbAuthenticationError) as caught:
NbClient(env_name="customer-prod", runner=runner).environment_credentials()
self.assertIn("nb env auth customer-prod", str(caught.exception))
def test_get_record_and_query_records_use_expected_commands(self) -> None: def test_get_record_and_query_records_use_expected_commands(self) -> None:
commands: list[list[str]] = [] commands: list[list[str]] = []
@@ -273,6 +322,7 @@ class NbClientTests(unittest.TestCase):
for stderr in cases: for stderr in cases:
with self.subTest(stderr=stderr): with self.subTest(stderr=stderr):
def runner(cmd, **kwargs): def runner(cmd, **kwargs):
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr=stderr) return subprocess.CompletedProcess(cmd, 1, stdout="", stderr=stderr)
@@ -281,7 +331,9 @@ class NbClientTests(unittest.TestCase):
def test_plain_forbidden_error_remains_an_acl_command_failure(self) -> None: def test_plain_forbidden_error_remains_an_acl_command_failure(self) -> None:
def runner(cmd, **kwargs): def runner(cmd, **kwargs):
return subprocess.CompletedProcess(cmd, 1, stdout="", stderr="Request failed with status 403: No permissions") return subprocess.CompletedProcess(
cmd, 1, stdout="", stderr="Request failed with status 403: No permissions"
)
with self.assertRaises(NbCommandError) as caught: with self.assertRaises(NbCommandError) as caught:
NbClient(api_token="secret", runner=runner).list_records("customers") NbClient(api_token="secret", runner=runner).list_records("customers")
+39 -3
View File
@@ -42,9 +42,7 @@ class NocoBaseHttpClientTests(unittest.TestCase):
with TemporaryDirectory() as directory: with TemporaryDirectory() as directory:
path = Path(directory) / "quote.pdf" path = Path(directory) / "quote.pdf"
path.write_bytes(b"pdf") path.write_bytes(b"pdf")
client = NocoBaseHttpClient( client = NocoBaseHttpClient("https://nb.example/api/", "jwt", transport=httpx.MockTransport(handler))
"https://nb.example/api/", "jwt", transport=httpx.MockTransport(handler)
)
self.assertEqual(client.upload_attachment(path, "quote.pdf", "application/pdf")["id"], 42) self.assertEqual(client.upload_attachment(path, "quote.pdf", "application/pdf")["id"], 42)
@@ -67,6 +65,44 @@ class NocoBaseHttpClientTests(unittest.TestCase):
self.assertEqual(str(requests[1].url), "https://nb.example/api/t_projects/7/f_projects_attachments:remove") self.assertEqual(str(requests[1].url), "https://nb.example/api/t_projects/7/f_projects_attachments:remove")
self.assertEqual(json.loads(requests[1].content), {"tk": [10]}) self.assertEqual(json.loads(requests[1].content), {"tk": [10]})
def test_credential_provider_is_resolved_per_request_for_local_users(self) -> None:
requests: list[httpx.Request] = []
credentials = iter(
[
("https://nb.example/api", "first-user-token"),
("https://nb.example/api", "refreshed-user-token"),
]
)
def handler(request: httpx.Request) -> httpx.Response:
requests.append(request)
return httpx.Response(204)
client = NocoBaseHttpClient(
credential_provider=lambda: next(credentials),
transport=httpx.MockTransport(handler),
)
client.add_attachment_ids(7, [8])
client.remove_attachment_ids(7, [9])
self.assertEqual(requests[0].headers["authorization"], "Bearer first-user-token")
self.assertEqual(requests[1].headers["authorization"], "Bearer refreshed-user-token")
def test_credential_provider_failure_is_reported_as_authentication_failure(self) -> None:
def missing_login() -> tuple[str, str]:
raise RuntimeError("run nb env auth customer-prod")
client = NocoBaseHttpClient(
credential_provider=missing_login,
transport=httpx.MockTransport(lambda request: self.fail("request must not be sent")),
)
with self.assertRaises(NocoBaseHttpError) as caught:
client.add_attachment_ids(7, [8])
self.assertTrue(caught.exception.authentication_failed)
self.assertIn("nb env auth customer-prod", str(caught.exception))
def test_attachment_association_encodes_project_id_as_one_path_segment(self) -> None: def test_attachment_association_encodes_project_id_as_one_path_segment(self) -> None:
requests: list[httpx.Request] = [] requests: list[httpx.Request] = []
+1 -3
View File
@@ -45,9 +45,7 @@ class ProjectServiceTests(unittest.TestCase):
self.service = ProjectService(self.config, self.nb, self.http) self.service = ProjectService(self.config, self.nb, self.http)
def test_create_maps_fixed_fields_then_adds_attachment_ids(self) -> None: def test_create_maps_fixed_fields_then_adds_attachment_ids(self) -> None:
result = self.service.create_project( result = self.service.create_project(name=" EV inverter ", customer_id=3, progress="SOP", attachment_ids=[8, 9])
name=" EV inverter ", customer_id=3, progress="SOP", attachment_ids=[8, 9]
)
self.assertEqual( self.assertEqual(
self.nb.create_calls, self.nb.create_calls,
+76 -19
View File
@@ -4,8 +4,8 @@ from contextlib import asynccontextmanager
from unittest.mock import patch from unittest.mock import patch
import httpx import httpx
from starlette.responses import JSONResponse
from starlette.applications import Starlette from starlette.applications import Starlette
from starlette.responses import JSONResponse
from nocobase_crm_mcp import server as server_module from nocobase_crm_mcp import server as server_module
from nocobase_crm_mcp.nb_client import NbAuthenticationError, NbCommandError from nocobase_crm_mcp.nb_client import NbAuthenticationError, NbCommandError
@@ -15,9 +15,10 @@ from nocobase_crm_mcp.server import (
DEFAULT_HTTP_PORT, DEFAULT_HTTP_PORT,
BearerTokenAuthMiddleware, BearerTokenAuthMiddleware,
FastMCP, FastMCP,
_make_write_services,
_read_nocobase_environment,
create_http_app, create_http_app,
create_server, create_server,
_read_nocobase_environment,
run_http_server, run_http_server,
) )
@@ -199,6 +200,19 @@ class ServerTests(unittest.TestCase):
self.assertIn("include_raw", tools["get_customer"].inputSchema["properties"]) self.assertIn("include_raw", tools["get_customer"].inputSchema["properties"])
self.assertIn("include_details", tools["list_contacts"].inputSchema["properties"]) self.assertIn("include_details", tools["list_contacts"].inputSchema["properties"])
def test_raw_rows_require_explicit_local_policy_opt_in(self) -> None:
if FastMCP is None:
self.skipTest("MCP SDK is not installed")
server = create_server(crm_service=FakeCrmService(), readonly=True)
with patch.dict("os.environ", {}, clear=True):
_content, blocked = asyncio.run(server.call_tool("list_customers", {"include_raw": True}))
with patch.dict("os.environ", {"NOCOBASE_MCP_ENABLE_RAW_DATA": "1"}, clear=True):
_content, allowed = asyncio.run(server.call_tool("list_customers", {"include_raw": True}))
self.assertEqual(blocked["error"]["code"], "raw_data_disabled")
self.assertEqual(allowed, {"results": []})
def test_tools_return_structured_error_for_nb_failures(self) -> None: def test_tools_return_structured_error_for_nb_failures(self) -> None:
if FastMCP is None: if FastMCP is None:
self.skipTest("MCP SDK is not installed") self.skipTest("MCP SDK is not installed")
@@ -211,7 +225,7 @@ class ServerTests(unittest.TestCase):
self.assertIn("denied", structured["error"]["message"]) self.assertIn("denied", structured["error"]["message"])
self.assertFalse(structured["error"]["retryable"]) self.assertFalse(structured["error"]["retryable"])
def test_tools_return_specific_error_for_invalid_nocobase_api_key(self) -> None: def test_tools_return_specific_error_for_missing_or_expired_user_login(self) -> None:
if FastMCP is None: if FastMCP is None:
self.skipTest("MCP SDK is not installed") self.skipTest("MCP SDK is not installed")
@@ -220,7 +234,7 @@ class ServerTests(unittest.TestCase):
_content, structured = asyncio.run(server.call_tool("list_customers", {})) _content, structured = asyncio.run(server.call_tool("list_customers", {}))
self.assertEqual(structured["error"]["code"], "nb_auth_failed") self.assertEqual(structured["error"]["code"], "nb_auth_failed")
self.assertIn("NOCOBASE_MCP_API_TOKEN", structured["error"]["hint"]) self.assertIn("nb env auth", structured["error"]["hint"])
self.assertFalse(structured["error"]["retryable"]) self.assertFalse(structured["error"]["retryable"])
def test_tools_return_specific_error_when_nb_is_not_found(self) -> None: def test_tools_return_specific_error_when_nb_is_not_found(self) -> None:
@@ -239,7 +253,9 @@ class ServerTests(unittest.TestCase):
with ( with (
patch.dict("os.environ", {"NOCOBASE_MCP_NB_BIN": "/opt/node/bin/nb"}), patch.dict("os.environ", {"NOCOBASE_MCP_NB_BIN": "/opt/node/bin/nb"}),
patch.object(server_module, "NbClient") as nb_client, patch.object(server_module, "NbClient") as nb_client,
patch.object(server_module, "CrmService", side_effect=lambda config, client: {"config": config, "client": client}), patch.object(
server_module, "CrmService", side_effect=lambda config, client: {"config": config, "client": client}
),
): ):
service = server_module._make_service() service = server_module._make_service()
@@ -267,7 +283,9 @@ class ServerTests(unittest.TestCase):
app = create_http_app(server, "secret", upload_endpoint=upload_endpoint) app = create_http_app(server, "secret", upload_endpoint=upload_endpoint)
async def request_upload() -> httpx.Response: async def request_upload() -> httpx.Response:
async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://testserver") as client: async with httpx.AsyncClient(
transport=httpx.ASGITransport(app=app), base_url="http://testserver"
) as client:
return await client.post("/uploads", headers={"Authorization": "Bearer secret"}) return await client.post("/uploads", headers={"Authorization": "Bearer secret"})
response = asyncio.run(request_upload()) response = asyncio.run(request_upload())
@@ -309,6 +327,10 @@ class ServerTests(unittest.TestCase):
("https://nb.example/api", "shared-key", 123), ("https://nb.example/api", "shared-key", 123),
) )
def test_nocobase_environment_defaults_to_the_users_saved_cli_login(self) -> None:
with patch.dict("os.environ", {}, clear=True):
self.assertEqual(_read_nocobase_environment(), (None, None, 26214400))
def test_nocobase_environment_accepts_matching_legacy_credentials(self) -> None: def test_nocobase_environment_accepts_matching_legacy_credentials(self) -> None:
with patch.dict( with patch.dict(
"os.environ", "os.environ",
@@ -339,20 +361,55 @@ class ServerTests(unittest.TestCase):
with self.assertRaises(RuntimeError): with self.assertRaises(RuntimeError):
_read_nocobase_environment() _read_nocobase_environment()
def test_http_write_mode_requires_shared_api_base_url(self) -> None: def test_write_services_use_the_selected_nb_environment_credentials_by_default(self) -> None:
with (
patch.object(server_module, "NbClient") as nb_client,
patch.object(server_module, "CrmService") as crm_service,
patch.object(server_module, "ProjectService") as project_service,
patch.object(server_module, "NocoBaseHttpClient") as http_client,
):
result = _make_write_services()
nb_client.assert_called_once_with(
env_name="nb-mcp",
data_source="main",
nb_bin="nb",
api_base_url=None,
api_token=None,
)
http_client.assert_called_once_with(credential_provider=nb_client.return_value.environment_credentials)
crm_service.assert_called_once()
project_service.assert_called_once()
self.assertEqual(result, (crm_service.return_value, project_service.return_value, http_client.return_value))
def test_http_write_mode_uses_saved_cli_login_without_shared_api_credentials(self) -> None:
with patch.dict("os.environ", {"NOCOBASE_MCP_TOKEN": "secret"}, clear=True): with patch.dict("os.environ", {"NOCOBASE_MCP_TOKEN": "secret"}, clear=True):
with patch("uvicorn.run") as uvicorn_run: with (
with self.assertRaisesRegex(RuntimeError, "NOCOBASE_MCP_API_BASE_URL"): patch.object(server_module, "_make_write_services") as make_services,
run_http_server( patch.object(server_module, "create_server") as create_server_mock,
config_path=None, patch.object(server_module, "UploadEndpoint"),
host=DEFAULT_HTTP_HOST, patch.object(server_module, "create_http_app", return_value="app"),
port=DEFAULT_HTTP_PORT, patch("uvicorn.run") as uvicorn_run,
token_env="NOCOBASE_MCP_TOKEN", ):
streamable_http_path=DEFAULT_HTTP_PATH, make_services.return_value = ("crm", "projects", "http")
allowed_hosts=None, run_http_server(
allowed_origins=None, config_path=None,
) host=DEFAULT_HTTP_HOST,
uvicorn_run.assert_not_called() port=DEFAULT_HTTP_PORT,
token_env="NOCOBASE_MCP_TOKEN",
streamable_http_path=DEFAULT_HTTP_PATH,
allowed_hosts=None,
allowed_origins=None,
)
make_services.assert_called_once_with(None, api_base_url=None, api_token=None)
create_server_mock.assert_called_once()
uvicorn_run.assert_called_once_with(
"app",
host=DEFAULT_HTTP_HOST,
port=DEFAULT_HTTP_PORT,
log_level="info",
)
def test_config_template_tool_is_opt_in(self) -> None: def test_config_template_tool_is_opt_in(self) -> None:
if FastMCP is None: if FastMCP is None:
+4 -2
View File
@@ -12,7 +12,6 @@ from starlette.routing import Route
from nocobase_crm_mcp.nocobase_http import NocoBaseHttpError from nocobase_crm_mcp.nocobase_http import NocoBaseHttpError
from nocobase_crm_mcp.uploads import UploadEndpoint from nocobase_crm_mcp.uploads import UploadEndpoint
AUTH = {"Authorization": "Bearer secret"} AUTH = {"Authorization": "Bearer secret"}
@@ -183,7 +182,10 @@ class UploadEndpointTests(unittest.IsolatedAsyncioTestCase):
) )
for upstream_error, error_code in cases: for upstream_error, error_code in cases:
with self.subTest(status_code=upstream_error.status_code, error_code=error_code), TemporaryDirectory() as temp_dir: with (
self.subTest(status_code=upstream_error.status_code, error_code=error_code),
TemporaryDirectory() as temp_dir,
):
http_client = FakeHttpClient() http_client = FakeHttpClient()
http_client.error = upstream_error http_client.error = upstream_error
async with await self._client_for(http_client, temp_dir) as client: async with await self._client_for(http_client, temp_dir) as client:
Generated
+695 -44
View File
@@ -2,8 +2,10 @@ version = 1
revision = 3 revision = 3
requires-python = ">=3.12" requires-python = ">=3.12"
resolution-markers = [ resolution-markers = [
"python_full_version >= '3.14' and sys_platform == 'win32'", "python_full_version >= '3.15' and sys_platform == 'win32'",
"python_full_version >= '3.14' and sys_platform != 'win32'", "python_full_version == '3.14.*' and sys_platform == 'win32'",
"python_full_version >= '3.15' and sys_platform != 'win32'",
"python_full_version == '3.14.*' and sys_platform != 'win32'",
"python_full_version < '3.14' and sys_platform == 'win32'", "python_full_version < '3.14' and sys_platform == 'win32'",
"python_full_version < '3.14' and sys_platform != 'win32'", "python_full_version < '3.14' and sys_platform != 'win32'",
] ]
@@ -48,6 +50,33 @@ 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" }, { 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 = "boolean-py"
version = "5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c4/cf/85379f13b76f3a69bca86b60237978af17d6aa0bc5998978c3b8cf05abb2/boolean_py-5.0.tar.gz", hash = "sha256:60cbc4bad079753721d32649545505362c754e121570ada4658b852a3a318d95", size = 37047, upload-time = "2025-04-03T10:39:49.734Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/ca/78d423b324b8d77900030fa59c4aa9054261ef0925631cd2501dd015b7b7/boolean_py-5.0-py3-none-any.whl", hash = "sha256:ef28a70bd43115208441b53a045d1549e2f0ec6e3d08a9d142cbc41c1938e8d9", size = 26577, upload-time = "2025-04-03T10:39:48.449Z" },
]
[[package]]
name = "cachecontrol"
version = "0.14.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "msgpack" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/2d/f6/c972b32d80760fb79d6b9eeb0b3010a46b89c0b23cf6329417ff7886cd22/cachecontrol-0.14.4.tar.gz", hash = "sha256:e6220afafa4c22a47dd0badb319f84475d79108100d04e26e8542ef7d3ab05a1", size = 16150, upload-time = "2025-11-14T04:32:13.138Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ef/79/c45f2d53efe6ada1110cf6f9fca095e4ff47a0454444aefdde6ac4789179/cachecontrol-0.14.4-py3-none-any.whl", hash = "sha256:b7ac014ff72ee199b5f8af1de29d60239954f223e948196fa3d84adaffc71d2b", size = 22247, upload-time = "2025-11-14T04:32:11.733Z" },
]
[package.optional-dependencies]
filecache = [
{ name = "filelock" },
]
[[package]] [[package]]
name = "certifi" name = "certifi"
version = "2026.6.17" version = "2026.6.17"
@@ -142,6 +171,137 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, { url = "https://files.pythonhosted.org/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" },
] ]
[[package]]
name = "charset-normalizer"
version = "3.5.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/cb/31/4971872b3ed8715346231fb6eb4da8fcba65a4143c189db151ee28a2812b/charset_normalizer-3.5.0.tar.gz", hash = "sha256:49bd5feb59b0bf3cbf6ebcf4352e371c95b9da9bacd4449f8b64d0ad2c10a26e", size = 169295, upload-time = "2026-08-12T14:35:31.624Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/3c/045ea64ea5a550870dd8ab60b2242870328d53f17d2be593b4f9f3121474/charset_normalizer-3.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:98820e1ceb25c6df7a80c4fd8efa59cb121f99bc7c4c1693ad94a2caff5b311d", size = 343861, upload-time = "2026-08-12T14:32:27.254Z" },
{ url = "https://files.pythonhosted.org/packages/fc/1b/7502be709db899d5b4801509829188b3a5a10969411da9c846115a5f1b70/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:608553f476fca509537e804c4a71f5eb166ce63b75141f89c2c686ce1aa36956", size = 237550, upload-time = "2026-08-12T14:32:28.385Z" },
{ url = "https://files.pythonhosted.org/packages/ca/ce/66392661375148d9455c17bee25509a54e28c39969e34befa48ec8777936/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6753de11eef42f1c321b26d682957d92c7f7bbce6530f34bbe0f9291dd37cc6f", size = 229673, upload-time = "2026-08-12T14:32:29.668Z" },
{ url = "https://files.pythonhosted.org/packages/6a/64/f58c32a8d4ecf55b82ee61ee9aa6a664d4afcd36c72feb4c926fd6fe9af8/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f76dc0a47f94cb9b69d86f01e477f4b0371ca70208b9ccea7e063c41eed9046", size = 260768, upload-time = "2026-08-12T14:32:30.891Z" },
{ url = "https://files.pythonhosted.org/packages/b1/ea/36c90e59a96386174377e855479ec154221ef001e96637e0b23be92489c4/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c387c6bf91b4774e359a48a179e2872b8e8bf741e4fde06ba8d1665eb9a4760a", size = 257880, upload-time = "2026-08-12T14:32:32.175Z" },
{ url = "https://files.pythonhosted.org/packages/23/35/5b85772eb82528ef22ba29487ad544a7049dfd27f35b1a5a55dbc0843048/charset_normalizer-3.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14f6904a3cf870abf044df3a8c4924ac6c8ef77e9896586fd37e73ae96cff2af", size = 247547, upload-time = "2026-08-12T14:32:33.495Z" },
{ url = "https://files.pythonhosted.org/packages/b6/14/ba11a99c2a22ab04c2d5383a700b378cb463a78ab15f36444cabc10cd671/charset_normalizer-3.5.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0cce46dd29d73e135e8087b96eb62a4aca6d69391b7f97808c6588ebed3178f3", size = 243326, upload-time = "2026-08-12T14:32:34.794Z" },
{ url = "https://files.pythonhosted.org/packages/4c/4a/cadba3f2400b45aa1d62a4ae0298bf58a3b30b1158baf15c338c7ce5b601/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:b476cdb63df22da2b91837593380be3ddbe406f36c506c1c91d80e7196b66288", size = 238820, upload-time = "2026-08-12T14:32:35.984Z" },
{ url = "https://files.pythonhosted.org/packages/47/41/d5188b9342d75b72c2b05d3ee373f01a691397e770f001ee05e3b37925f5/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1f56ce84b317ef2a59d7d3461891c7597c79247d2192bb8114c68a1a1debfcc0", size = 231661, upload-time = "2026-08-12T14:32:37.191Z" },
{ url = "https://files.pythonhosted.org/packages/13/5f/df38fa972c4e945c3d8cee2bc4e610613af522fd359c7dc7a74c419f0278/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:9ce0f885239357379d92fd9a5fddbe20f0e30e0527c29ba69f8e99eeb1304a76", size = 261459, upload-time = "2026-08-12T14:32:38.369Z" },
{ url = "https://files.pythonhosted.org/packages/a5/d7/043ff7720067a3beee05523465ac9c1c846c68b7884930dd483f72ee5ab6/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:96ae7ab5d8155fde927aa0864fbc8ba3cc4fde6d41ab0c7cea9d6012b4978603", size = 242300, upload-time = "2026-08-12T14:32:39.505Z" },
{ url = "https://files.pythonhosted.org/packages/19/f6/33980b7b802a048e546a6d9ad2ea783a6cf6b10a86aaccd10db462d8b913/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bf91921009025e96ce57a03ced6d14604fc3baf0530351638e9504a55da6fa3b", size = 259101, upload-time = "2026-08-12T14:32:41.02Z" },
{ url = "https://files.pythonhosted.org/packages/a6/b7/0d19bde844bff9165377c1da9ef3c4792a4c24bd49b5b7094d9e6f6ab58b/charset_normalizer-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0b2e44e6d42d1a4ff78ccc219a93c5449105d10b16198d1aea581080df8073f9", size = 249246, upload-time = "2026-08-12T14:32:42.47Z" },
{ url = "https://files.pythonhosted.org/packages/1c/cc/2c34fdfaacdf0e96e880ef562cbf80a9b5f8ea97e0dd9e57ba348a9c65cf/charset_normalizer-3.5.0-cp312-cp312-win32.whl", hash = "sha256:deb99535e9bf0bea8e274c6413eb939a21be35a3f492678dba4d5b1f4d70f142", size = 178025, upload-time = "2026-08-12T14:32:43.909Z" },
{ url = "https://files.pythonhosted.org/packages/76/d0/c34dbd1df23bcbdc1b5d2f48256340d72fa747f1eb03924a9d2fa35ed85b/charset_normalizer-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54dd1a66fa4bce0ccaf0db9dde336e49b3eec646dc4c1c0991279369d373a14", size = 200143, upload-time = "2026-08-12T14:32:45.254Z" },
{ url = "https://files.pythonhosted.org/packages/c0/ea/4e6bf1465d60c3d8f488d5bde140d0cb91cd23ccab0dc2895cc6c6982047/charset_normalizer-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:b8ea208b304587d47931b36481342d20336e0d338ab052f8b4305926482598d6", size = 180046, upload-time = "2026-08-12T14:32:46.545Z" },
{ url = "https://files.pythonhosted.org/packages/1d/be/cc7b7b6fc41984902c0d31b06f5d9297e67705c1dae9352608e5540fad09/charset_normalizer-3.5.0-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:5c23fa4f6eccdd601949cb00f3988c01d64e671d8faba356397971077022e144", size = 211050, upload-time = "2026-08-12T14:32:47.81Z" },
{ url = "https://files.pythonhosted.org/packages/d3/ae/e3ec8f17313609f43f7b323012fdb1ee37b83432277ca4eceba83e00366c/charset_normalizer-3.5.0-cp313-cp313-android_24_x86_64.whl", hash = "sha256:07f6f42b5a6325df35b458004fb5f9f29bf502d89287a33c7cdef3590e31de0f", size = 222768, upload-time = "2026-08-12T14:32:49.027Z" },
{ url = "https://files.pythonhosted.org/packages/c5/50/9f9c0d7ccc1512d49e27a0e7c12c58ec71dfe91698fa4326f058c33e1f1b/charset_normalizer-3.5.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8efc3f1563ed431882dd0dc0411b5f8ace1b1b89074981deaf6bd8af77dbe1bc", size = 193907, upload-time = "2026-08-12T14:32:50.414Z" },
{ url = "https://files.pythonhosted.org/packages/fd/1d/cfe7b745ef7f4c3b7214581955b5a0869ba2ac551a58fc11036281ae167c/charset_normalizer-3.5.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:368eb2fc9482158b3a3386e8f01fa61f479c968e9a19ceab8f0188b86b312991", size = 197135, upload-time = "2026-08-12T14:32:51.605Z" },
{ url = "https://files.pythonhosted.org/packages/28/55/30fafdcfca9ba616bc394240545e4cd52f4f66dea43ded81b7d2d5274fde/charset_normalizer-3.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:826a295a039178479a325be1ae60eded1f0b10f7dda749df59e2440de8f61d64", size = 339892, upload-time = "2026-08-12T14:32:52.82Z" },
{ url = "https://files.pythonhosted.org/packages/f4/08/bdca5fc2bdc36ee443673dc7d12b23885a5a7b282bef85a1a4c3b325b40e/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70ff1c16eb0eb5ee6bb12739292347f981a5ba764cc4df1bc2e69b0405d4ac3b", size = 239439, upload-time = "2026-08-12T14:32:54.058Z" },
{ url = "https://files.pythonhosted.org/packages/d1/9e/506c8d7a7722bba7c8cdd78c1b5ef23bda92bfbe0b3e28ea84673d519a0f/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3cfdab178a4add5483e26a9bb1c16d8018ccf39b4be7a3aea6c3979e6828f2ee", size = 227896, upload-time = "2026-08-12T14:32:55.326Z" },
{ url = "https://files.pythonhosted.org/packages/0d/1a/dd828f2b1d6f4bf10821b9a74d866be05ffcdbfcddfc501d6fe6428762a7/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6083d10a846218502d664375b9448508d9fa580bd834567423156c6abfbe899d", size = 262548, upload-time = "2026-08-12T14:32:56.484Z" },
{ url = "https://files.pythonhosted.org/packages/be/81/196d26f6bd78b93e0d451b69082a71027ceeddd4b0be9170b81bb038f824/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0f211c21aa316cb6e2662e54a1194633a79d98a50a876addacfce7ba5b34b09f", size = 259986, upload-time = "2026-08-12T14:32:57.661Z" },
{ url = "https://files.pythonhosted.org/packages/3a/6a/5b964a1eb0f9075ecd45083eeb21aaec215334f98bac3d400302ea73875d/charset_normalizer-3.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b08ebf9488c7ff5eff038e48e6ea938178dfd9dcc8598b5ca941e4ae27b20be", size = 249853, upload-time = "2026-08-12T14:32:59.123Z" },
{ url = "https://files.pythonhosted.org/packages/c8/b4/aee3a9d82edd0e931091ef3e9f03e46491ae3590e96e998d0975dadbe17c/charset_normalizer-3.5.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6c95450fce59f00c6d08eff6572ec2e736e5054c9450253afd5748f8416f2eb9", size = 244217, upload-time = "2026-08-12T14:33:00.341Z" },
{ url = "https://files.pythonhosted.org/packages/22/3e/33f72ca11c1b619b220fd9f35905ebd171cbd0e0470f2357e467b9e861ee/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5780a29823e1d2bec69b7a104ead4195a43f3e97782efaedbf1f79a0157af715", size = 241307, upload-time = "2026-08-12T14:33:01.589Z" },
{ url = "https://files.pythonhosted.org/packages/78/27/6029dccba958621c7f3a65136f87c5512d712aef9e890f09512cc171bd03/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:054420b5db984971d886e5e4e2c37c760ae6682aedbd066687ff0949d9ed5f08", size = 233305, upload-time = "2026-08-12T14:33:02.866Z" },
{ url = "https://files.pythonhosted.org/packages/18/d7/691c967be459153fe9faf49bf78bc95639ef8bf6dd008f38cc6389a349eb/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d016dc857136c726958102c3b8a3986acdc65ace6fbf12cfdc09cc4bfa2935b2", size = 263465, upload-time = "2026-08-12T14:33:04.166Z" },
{ url = "https://files.pythonhosted.org/packages/7c/2d/9202221be5c90b2a835924191e362690ed8dc8c7d6606100c2bd03fe0f8c/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f2ce3d39fb4a9d674e6639dd5d3146b2e273475d2260f10163228d66fc04433d", size = 245060, upload-time = "2026-08-12T14:33:05.325Z" },
{ url = "https://files.pythonhosted.org/packages/b3/9a/298772fd0a0cbccadf36451a1cd7eef4b66a11e99b4a7f6fafc47cc62c75/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:a5613a3a82c974227bde18f03409e30c467f8065cb56d822e3eb83708a5f223d", size = 261091, upload-time = "2026-08-12T14:33:06.475Z" },
{ url = "https://files.pythonhosted.org/packages/82/3b/1a11fe66e555dbe2f5714ade6ba74fa29edc9155d9cf1001d4d6ed096aa7/charset_normalizer-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fded2e82ff082e5d8e017e2ddcc1411bd8cb83b8585097fc401ef574f756b888", size = 251857, upload-time = "2026-08-12T14:33:07.784Z" },
{ url = "https://files.pythonhosted.org/packages/17/fc/73b817e8af3f1d25ec5cf458d405abba5a144cf9812238a61530f5eac186/charset_normalizer-3.5.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:8f006866047c6ec4b627ec144b1e0bbc7427cb31fd7c08d19897d0ac9032af3d", size = 139745, upload-time = "2026-08-12T14:33:09.123Z" },
{ url = "https://files.pythonhosted.org/packages/b3/fb/ddb66303c86f7dc5043a457dad9fa82b4d6d0cb97094f9bcdde21693fd58/charset_normalizer-3.5.0-cp313-cp313-win32.whl", hash = "sha256:196e270c4e80827b5072eed7d6aa661d133afada94fe366669f9609e718d305e", size = 177217, upload-time = "2026-08-12T14:33:10.305Z" },
{ url = "https://files.pythonhosted.org/packages/fb/88/6018cc8d76ea2b7cb02918f37e23e86c261d1a102713d7e88d2cfb8b211c/charset_normalizer-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:72982d9958a42f8132bf2d6b90214ed66477295ef1188731f98ae3511c6eeb5a", size = 198896, upload-time = "2026-08-12T14:33:11.51Z" },
{ url = "https://files.pythonhosted.org/packages/20/2e/04c0bbfc8d9abf91959f7a3d207d45cbf63a8116984caae2381890019bb5/charset_normalizer-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:0b373bab0b867b68b8eb249da9478cab9181a42993437cd2f5dba5fb0b4fbd1b", size = 179193, upload-time = "2026-08-12T14:33:12.726Z" },
{ url = "https://files.pythonhosted.org/packages/43/14/d098868dac5ff27e0258f548b1c74c6484be528384965d8fcf8fc6a4011d/charset_normalizer-3.5.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:d95244906ed69d0f79f190893c65e336c15959003e21449256dc05c001b52ea2", size = 211664, upload-time = "2026-08-12T14:33:14.153Z" },
{ url = "https://files.pythonhosted.org/packages/e7/da/a944b32a46601ae5a4c3499e8d64ecd14fe82313f00da74dcdf00273a0b4/charset_normalizer-3.5.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:d788e2ded0c4c47efa4d73cfe59eaf975ee32f425219873d2cb3e3fbaa00f636", size = 224375, upload-time = "2026-08-12T14:33:15.472Z" },
{ url = "https://files.pythonhosted.org/packages/f7/db/eabb5996be2f529744755e7b2fc9396eff4a64961f034e7fd49d54b9afb2/charset_normalizer-3.5.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:f9f91d3e8382900f3a68fa0ce94294479de9cd2de6bc0c70acd0f0dfd511836b", size = 194364, upload-time = "2026-08-12T14:33:16.607Z" },
{ url = "https://files.pythonhosted.org/packages/78/65/4ad3c5be108930310d8003f5602861d5b89f728293b9f09c3a4837f7ba10/charset_normalizer-3.5.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d54625cbf4e6b60bf0639728cb8b4cb541e340f6d7cafae5806051a40ddf4c45", size = 197643, upload-time = "2026-08-12T14:33:17.88Z" },
{ url = "https://files.pythonhosted.org/packages/3d/39/8fee3201b98d52289be60a775797d69be05a04fb6cfb48c1587dad33e649/charset_normalizer-3.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:1f99a8c3a1da5d955edbad18208b3d627bdd54c48a6e739fa877bdca98c686d6", size = 341384, upload-time = "2026-08-12T14:33:19.239Z" },
{ url = "https://files.pythonhosted.org/packages/a7/dd/9e757101d1f76c35c0643684ba499ac3a181fb2b264c68174bf727d627e8/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf1e75dc07a3850b53d1e5f75e04d3ae12afe56284be7821771eaa2466350c73", size = 241637, upload-time = "2026-08-12T14:33:20.619Z" },
{ url = "https://files.pythonhosted.org/packages/eb/e4/7857023015400bc4aa0a82fbcca29fa2dc7ec25f971a130764cb2dc7a589/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ac5a9cc079c67d75f4ddf343276031879eadbb333d1bb231cce297b8d7b9aae8", size = 226170, upload-time = "2026-08-12T14:33:21.773Z" },
{ url = "https://files.pythonhosted.org/packages/7d/ae/8b52935b304f7b6bbf33151ed2b75266b09aa4b6f8f04230d948885b2577/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0dfe83c1b4d00abbf433998117a14f56a5c2bc68226c0d331709eed0d1ce539b", size = 265093, upload-time = "2026-08-12T14:33:22.999Z" },
{ url = "https://files.pythonhosted.org/packages/dc/78/6e838f6bb059f2c0afc60a4e7f294252f043c254656ad4114c50302cae4d/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e8fa586df2208ef040684751345f10f503834a757c9a74ecd19c1a2f9b1ccd", size = 262789, upload-time = "2026-08-12T14:33:24.214Z" },
{ url = "https://files.pythonhosted.org/packages/c2/08/189b27e51fddc9d6b3695331da0e31792c1d88b953ad854e57f06e9b2cc8/charset_normalizer-3.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82cc5835997ec78afe293a192e385099355770a7db94b2fb1239d36b32796f1c", size = 250580, upload-time = "2026-08-12T14:33:25.707Z" },
{ url = "https://files.pythonhosted.org/packages/ac/55/64854e99b25841f83e8e37d9df2f3d1f96f693439f80e5fabd542a7e47ab/charset_normalizer-3.5.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:19e52bda45086df8a4be4bb5910af6f5d9d3b538c78712c8ae09ef10b85bf458", size = 245008, upload-time = "2026-08-12T14:33:26.971Z" },
{ url = "https://files.pythonhosted.org/packages/4f/01/7720c904fa635d4260b4dced6029cf3d298c57b26741365d5a8d28c54043/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:3418edd0ecb72a0a3861cf72f31be0ad9b7fe338ce2b58fb5cc80b9aeb792700", size = 243892, upload-time = "2026-08-12T14:33:28.237Z" },
{ url = "https://files.pythonhosted.org/packages/70/50/7bfcb327631d4870c720872b548745f6ec8baa044d51c21b5d1d32ac4e3a/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:125ee619611019471b177c70bc3e9d4cda9fad7e01d93523501d3b188df0193a", size = 230996, upload-time = "2026-08-12T14:33:29.511Z" },
{ url = "https://files.pythonhosted.org/packages/24/51/40c45d6d940c04005ed721aa54bdebf1ebb2930f8a2ae537e8d60484fb27/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:a3ad0e3da22852533858663848608f3f24c0d35e5cde415a4903476f2b4c88ec", size = 265834, upload-time = "2026-08-12T14:33:30.689Z" },
{ url = "https://files.pythonhosted.org/packages/eb/d4/ef7a227ef89d215b47f9df79c3966610b17faa13bb2f236989207a631622/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:4ebebb410bc517e1d284c52a123e82704b21e4e7e26a21ebecf7439d0647b8a3", size = 245544, upload-time = "2026-08-12T14:33:31.859Z" },
{ url = "https://files.pythonhosted.org/packages/37/a9/a4ca9156964ded61c7718eba410ce11be2fd2b263fda4bcf08367b6578cd/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:91f9f7c151e772acebe489eaec96e96a2877202d7dd144e3f96b8676881715a0", size = 264110, upload-time = "2026-08-12T14:33:33.13Z" },
{ url = "https://files.pythonhosted.org/packages/38/6a/838364bb8702229c6e5f8b23f80ff0f052a12dfaf3113a12fd6acbe92a44/charset_normalizer-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:401ea6e7af9e7852ed818f64714b579c1935482049670847ca3bd7ba45dc63fb", size = 252303, upload-time = "2026-08-12T14:33:34.98Z" },
{ url = "https://files.pythonhosted.org/packages/9c/5f/d88032edce951f499a2321cf7ae0d35a043c74be12bc22d81084cc7afbcc/charset_normalizer-3.5.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f7496aed56b06325a1ad419c5bf23c6dd042558e874f71dd1b958f3e255f3053", size = 139964, upload-time = "2026-08-12T14:33:36.195Z" },
{ url = "https://files.pythonhosted.org/packages/37/ae/1c4a46b6b00d1c34d2ee355ef99ad6173674166800d1af0f05f85028d513/charset_normalizer-3.5.0-cp314-cp314-win32.whl", hash = "sha256:606a86c1c3196f3738de39a67a7490bbd61cb31c0e0436070bd0c6a48170b38e", size = 179790, upload-time = "2026-08-12T14:33:37.356Z" },
{ url = "https://files.pythonhosted.org/packages/01/51/f94dcf34fa8eba48c1fb89b6490a5f1426e19488fe5f38aac6c648c99057/charset_normalizer-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:ec6c464cf45867f66a2273e2214d9199a8fbad5cb95ca0fd45f6a2fe1d9d2cf4", size = 203723, upload-time = "2026-08-12T14:33:38.639Z" },
{ url = "https://files.pythonhosted.org/packages/9f/ba/91d386870b5d9e4b0d8c4034f63877cc2e47b99c81ef05f3e6d42bf9a53f/charset_normalizer-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:dc28949de1bb5f7f30a46f15d74ce7ac5aaa63e03c5de04d68f571c7423af834", size = 183423, upload-time = "2026-08-12T14:33:39.899Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c9/534ecb17b7fb95f9052c4a44cf316316a27d4a8f73e8475ff55e778dcdd7/charset_normalizer-3.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:68b7e84ae8239a94f8d2c8f3f3a3a81bcde54805ec8f42a34de927d155688ec6", size = 368967, upload-time = "2026-08-12T14:33:41.093Z" },
{ url = "https://files.pythonhosted.org/packages/3c/b2/ad7c3242d7fe55cd55126c22c65cb1b49779782cdf8932fd01d12232d86a/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58ca5dc0a0ef99f2801ec0574214c978e9574055bc783830bbb6e7433218609f", size = 239478, upload-time = "2026-08-12T14:33:42.428Z" },
{ url = "https://files.pythonhosted.org/packages/7e/62/77f0b850048e430fc350ec58876b0c020f5c8d0d3956fd1a4d6ae2fa292f/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6c57af4084c10cb3286688d65e4c654190ff5edcbc2411d08cdca0a8a44c59a1", size = 227036, upload-time = "2026-08-12T14:33:43.635Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ba/47d951e1a51dddbaad0a1410baf49fb1d897ceb00281568f1183b79bce9a/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1619a3cc174a7e3963dd34348e6fceb6e50db0ddeb0031bd7c73a58286454fa", size = 260772, upload-time = "2026-08-12T14:33:44.96Z" },
{ url = "https://files.pythonhosted.org/packages/b0/61/8c7ff4c81b2a88271126acf4b83ab3e31f6d63868b0f01d331eaa0f9cb67/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1328cc57dd4372be1265f68232cee890e087416e3e6e93e6ffb32c2bad4d36a4", size = 259273, upload-time = "2026-08-12T14:33:46.185Z" },
{ url = "https://files.pythonhosted.org/packages/e1/fd/36129689be08dc287b951306946657ff70d76e287dd57018861f86d0e474/charset_normalizer-3.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:06f4fb62a9139bef056b8b2da6773c94c2f259f90e4b8e53b166f3d0372d7cf6", size = 248086, upload-time = "2026-08-12T14:33:47.54Z" },
{ url = "https://files.pythonhosted.org/packages/cf/f8/bcae67f994c8fd31dda445e5ebf84045823c31443fe46f0e9ee6aca99aa0/charset_normalizer-3.5.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:478650a70a750d75d5add401606c77f77069c32e4ba2c9131dc6cee566962ca0", size = 242671, upload-time = "2026-08-12T14:33:48.746Z" },
{ url = "https://files.pythonhosted.org/packages/61/92/0472cdad1061c2f0e4d3aee29973eb6e81bb8fe256ff2860cf115b15f1c9/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:48920bf6fe83eb2226756ac623fa54940487154eb18f80889d5735cf234965c0", size = 241311, upload-time = "2026-08-12T14:33:50.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/89/04a03de5d27c77c624d9fcf6287073754bd438df1b58cb7d030c57c2824d/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:168a0cb536b5123a77bc42ecf5e0bf6f923d0d9ae43c42a14eb0677c19ac6c19", size = 229898, upload-time = "2026-08-12T14:33:51.523Z" },
{ url = "https://files.pythonhosted.org/packages/42/a2/639c4278adcb7ed1f4db608dd9ac19b6774fa2285a96b1c0bdb9c124ccbd/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f278e131afa96a3622cef9211c406ea2ad1b68eb06f8837cd443684a40e0ae50", size = 262852, upload-time = "2026-08-12T14:33:52.924Z" },
{ url = "https://files.pythonhosted.org/packages/9d/95/02e34c97bedfd0c5574efb9179c850591acc7f967ba039ed8dd29d332b73/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d6100f877d2ed95f0856a3fde25334153add94bf2224c43f45f88e7039262aaa", size = 242913, upload-time = "2026-08-12T14:33:54.18Z" },
{ url = "https://files.pythonhosted.org/packages/a3/64/0946aeab6462dad9f160a50dfb4704d3f58a5ee708f085abc2105fbbff0c/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:4c440122e1ea68b1f8b44a631ebf49c39180f6869b1da22d76e8a724208ec6e9", size = 257938, upload-time = "2026-08-12T14:33:55.802Z" },
{ url = "https://files.pythonhosted.org/packages/79/77/36787d41ead124746506a4425c729f4f17c68280af8a6a5baa0a598cae86/charset_normalizer-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e5f834965c2fe589837bac1002e07e25734ff70381903ccd95b3d649e22bfa40", size = 249467, upload-time = "2026-08-12T14:33:57.081Z" },
{ url = "https://files.pythonhosted.org/packages/65/10/d9f6c5589cd24198d4ce6cd2948191c18e657272f433e5a00d258d9f5c22/charset_normalizer-3.5.0-cp314-cp314t-win32.whl", hash = "sha256:076cf9d3f3c7e410295c09d96355cf3b1bcae74990034d80e4371e20fe1ba4c6", size = 190624, upload-time = "2026-08-12T14:33:58.449Z" },
{ url = "https://files.pythonhosted.org/packages/6c/81/43e0584a802051a22c725795ebe1df78263abc7de858eef6cdc9b36637e9/charset_normalizer-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:3288a560dc3114d5d2ebe309b1ef43f8af355eafe25856832415c2a8196c9db3", size = 215902, upload-time = "2026-08-12T14:33:59.753Z" },
{ url = "https://files.pythonhosted.org/packages/30/f3/af6a1160fef0eac4510d035241e11eccf78e5350e4cd4de79e79fe02a5e5/charset_normalizer-3.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a284c36b9c6616bf0a8aa4aabba668a0c75ba65ccf40a79868aeaa69ad996897", size = 193452, upload-time = "2026-08-12T14:34:01.017Z" },
{ url = "https://files.pythonhosted.org/packages/42/a4/dee470afb7a55c4f78b6fef37306c51fed17ebf94dbe530798c91d394350/charset_normalizer-3.5.0-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:c38d1e9bc2073b0984d2099ea647fd7f6c0d8f83a1e14e0cd32926f16e4c44ce", size = 341595, upload-time = "2026-08-12T14:34:02.4Z" },
{ url = "https://files.pythonhosted.org/packages/6a/32/9c3126dc429c6d9d7f79c52681a7c4453ed20a26267c9a8275d7ab620aba/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9f45186390aee4d1f26f723c615b67df346766c3b16df000d84d6e374f06757", size = 242177, upload-time = "2026-08-12T14:34:03.741Z" },
{ url = "https://files.pythonhosted.org/packages/4b/9d/5b616a887301ff4cc0916b39ba44257390d3da80deeed6e8b6f2f26b14a8/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f0fde5e5100c735b2274ab898f0742a5dcde492796296cfbe7e0ad6a4cd1a396", size = 236730, upload-time = "2026-08-12T14:34:04.991Z" },
{ url = "https://files.pythonhosted.org/packages/be/b4/d6d3e70be93ebe5fabef65e4c7ac113e1d1705cbaeb5fb72467e713aca17/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3d00e18e7bbf47e332ab63903d18bae31efc701b1d8cca0382b97784a621fc44", size = 265158, upload-time = "2026-08-12T14:34:06.235Z" },
{ url = "https://files.pythonhosted.org/packages/30/e7/3f1fafa87e2643257474f9c4eec609f2193a61d907dce7dd4f3f2390ebd5/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5b81980668800dd1c69faad8aea6e85a8cee0e13bcd3bba7671695ff16260293", size = 262931, upload-time = "2026-08-12T14:34:07.511Z" },
{ url = "https://files.pythonhosted.org/packages/0a/df/ebeb224a949d91829e5e114c6b64372a3c792b00762a9e951ce416f3a32d/charset_normalizer-3.5.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9bb3e0d1345b9c0fe73673ea656375f38a78ec679c2edeae0c24800f04798a85", size = 251388, upload-time = "2026-08-12T14:34:08.949Z" },
{ url = "https://files.pythonhosted.org/packages/a0/64/9a6ce2e7acc5cf1b4636f78f82e89ff581e06a0216a40678b28bd4d832c4/charset_normalizer-3.5.0-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2401f7671242e921e604f609d429f6b282ea4ca787a6ffd22ed7372011ddb9d1", size = 251821, upload-time = "2026-08-12T14:34:10.138Z" },
{ url = "https://files.pythonhosted.org/packages/f1/b1/6e69b8056f615e5ccff6b91ca16db2d47922251f016821a300c115267fef/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:96720f2aeed3434bc48f4d52fbad64ecc820cfed88915d664780ed9ba09ede78", size = 244507, upload-time = "2026-08-12T14:34:11.488Z" },
{ url = "https://files.pythonhosted.org/packages/0f/34/02c15d6a0aa6b934dcdc136b111da63ae857b9fd51cf5505b0736337c2eb/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:c455829625df983f716cbaecbba77f2d1dc2e0e0ed1638c059cece15a279344b", size = 240951, upload-time = "2026-08-12T14:34:12.991Z" },
{ url = "https://files.pythonhosted.org/packages/ae/15/0fe893d3e1c7d111280bd6c4bd4c1e431487a1124a1bcbce78dfeda3a3a8/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:c41b067eddcfa5ee6b1169c287605be7fb6b0ea22bba6474c5bb978a668def4f", size = 266162, upload-time = "2026-08-12T14:34:14.232Z" },
{ url = "https://files.pythonhosted.org/packages/55/ea/eca03527307670f5d102c295671a800c404ca958cf94fefd10fc963a72f0/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:e31786a947b136329bfdc458c82c06d4ec539b4a4436b7da4df4aafc9902ee80", size = 251835, upload-time = "2026-08-12T14:34:15.48Z" },
{ url = "https://files.pythonhosted.org/packages/03/a8/fee5633081e595fe9e191df6f215106c791ad596eddf5e41e39b8ea0f2e2/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:c5c6d47a865147e0ae3322ce92e7fb52ba3169d94b447deda56897ea2aa6fac9", size = 264314, upload-time = "2026-08-12T14:34:16.679Z" },
{ url = "https://files.pythonhosted.org/packages/cf/fb/17f47ae6ca35b562fb6e6f4b05f7aec6034217353eb4a23aaa3566dc7340/charset_normalizer-3.5.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:c75191e3c8052045179646cb40e280800a4e0bdfda34d9c949c2f268d44e80e4", size = 253194, upload-time = "2026-08-12T14:34:17.975Z" },
{ url = "https://files.pythonhosted.org/packages/59/88/f2b0f7ebb92493e925889ff29239b3b0073ffafd91230dbfc69e5cf9389c/charset_normalizer-3.5.0-cp315-cp315-win32.whl", hash = "sha256:83b62410bd36bb1178a7d563e2ee0cf21eb1c980c912ab99c2c78f06227f1731", size = 179800, upload-time = "2026-08-12T14:34:19.409Z" },
{ url = "https://files.pythonhosted.org/packages/e2/f0/afb5bfdea52fd943b1960403847a276b8e900c6e4cd6a38752321b4eda64/charset_normalizer-3.5.0-cp315-cp315-win_amd64.whl", hash = "sha256:e3b9eaa99a6d8c9ace4cd303915947ef55088d4cd87c6676874f98c5c03aa040", size = 203726, upload-time = "2026-08-12T14:34:20.656Z" },
{ url = "https://files.pythonhosted.org/packages/fc/71/219783eb691aa2ec879c0e521afdfe2b826f9678eed51b9c039d03e0db2b/charset_normalizer-3.5.0-cp315-cp315-win_arm64.whl", hash = "sha256:fec352b793cdc183cc9e7e0b6c10fd7bff38ec54ba44cc43599b9b56f7f3db2e", size = 183428, upload-time = "2026-08-12T14:34:21.975Z" },
{ url = "https://files.pythonhosted.org/packages/d1/d0/14aef3b9f80f2593c039d897e89034635b9eb0eb44b6ce5173bbd79ff338/charset_normalizer-3.5.0-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:c9bde7a960720c8b8e1b5ef7afaa0c9a2f3b55c44abd635b2b29dd066b298e3a", size = 368728, upload-time = "2026-08-12T14:34:23.221Z" },
{ url = "https://files.pythonhosted.org/packages/10/fc/b249466ddbbeffa448b6597631e9091d1f01b5132ff8e7a0e21a6eb72b63/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f8cd1283a9fe6c2065c807e9d5da81afe5e1e004caef39adc0d8ae86dd883698", size = 240925, upload-time = "2026-08-12T14:34:24.504Z" },
{ url = "https://files.pythonhosted.org/packages/2b/b9/c17e72aaa1b3e1ca6c184e8025cf138ed492d01a54f85286ff7d31253a4b/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1c010dd86d3f4c4433c9634d33ce8147393b270dfa54f217f965540b8ae8e075", size = 234932, upload-time = "2026-08-12T14:34:25.822Z" },
{ url = "https://files.pythonhosted.org/packages/18/d7/f84ef0966bbe216f71029e34e7fa425a16b1682e2a40265e679dedf2b655/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5e68229977b2dea28e7061c0c0630a23f2f9f6e9c6fb38d77d3d6dbfe3768b74", size = 261733, upload-time = "2026-08-12T14:34:27.112Z" },
{ url = "https://files.pythonhosted.org/packages/3b/73/3e887fa0781a395339355ed934ab6561ceb5bb52574160f070224039c630/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a60773eb5fda796e6e6f76b9c152d270fe59f9788a51a6ff8ba44082d8548ae4", size = 258460, upload-time = "2026-08-12T14:34:28.431Z" },
{ url = "https://files.pythonhosted.org/packages/b3/81/52ebd9849bf9e35d0b21fff115cb6543162a8e1f2f564e8f87121a336b8c/charset_normalizer-3.5.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9419f44e568f7fafcdc0b3b5c766a2364e705a9b34fb8a56b431e0d1f3f4258", size = 249894, upload-time = "2026-08-12T14:34:29.698Z" },
{ url = "https://files.pythonhosted.org/packages/85/f3/9366492b8a5fe0187de282e001d61345740cf79eb4a5f20181d769be02b5/charset_normalizer-3.5.0-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:75e243abbb528c1a774390ed71e3f868a9f37b1373442e4bbadd401cfc505ff4", size = 249540, upload-time = "2026-08-12T14:34:30.96Z" },
{ url = "https://files.pythonhosted.org/packages/0b/af/28bb5e5dbd3e67cb9196a62781ac2b6d79492f4fc7a069b6ca7d6d6c8d58/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:54c963ce6404e52255b737e8a06d356fc762d59096ae566203a67cf2b7d050f2", size = 242734, upload-time = "2026-08-12T14:34:32.481Z" },
{ url = "https://files.pythonhosted.org/packages/26/d6/7ccfa62b53b40fc06b2d3504825aa400764740bd10cf248fdc4272441b93/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:63ea0cc840c66670183578c2630d138c0e944aeadfc33f25173ee240f5db780d", size = 239580, upload-time = "2026-08-12T14:34:33.916Z" },
{ url = "https://files.pythonhosted.org/packages/0b/82/71c0c9b046697b8da66b3acefa8d5f92d00a9ef433ad7c3522b971d0369a/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6c06875a1d4a7537bef70f659b55c6b55b9a47ec3ba8f2db610350c2d9915e6e", size = 263281, upload-time = "2026-08-12T14:34:35.333Z" },
{ url = "https://files.pythonhosted.org/packages/d6/01/d027583c869f40ba980c1c76994adbd522c360a6327e72beb44d7c267385/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:4253da1b4456b633651a8d59eb1dc7a8a8fa38241014dd7c217b353e547ae394", size = 250027, upload-time = "2026-08-12T14:34:36.991Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e9/6475d739e0ec8bb1236e06263dc3affaffdf947d8114ad27024932f325da/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:f044cb1cf44012184715f46584658993b5fee9344d71c4b0c455a17a299730c0", size = 257547, upload-time = "2026-08-12T14:34:38.277Z" },
{ url = "https://files.pythonhosted.org/packages/a5/60/d1f502fcaa048a2aca3ab80bfef8407659c131e4f1792fa805fec14b4960/charset_normalizer-3.5.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:a17864853f7c518ae7d4b368af98f427f9396805476af40af8698560f09d7d97", size = 251718, upload-time = "2026-08-12T14:34:39.562Z" },
{ url = "https://files.pythonhosted.org/packages/c3/69/76343dcf4381a698807ff8a20d89f66bbdd9f6222b0b17740f77ab764335/charset_normalizer-3.5.0-cp315-cp315t-win32.whl", hash = "sha256:9e726478d7a213847860219d74665a6892a643ac93b8f76580f6cf9ed39996b7", size = 190757, upload-time = "2026-08-12T14:34:41.053Z" },
{ url = "https://files.pythonhosted.org/packages/e1/ea/d18147626a1667cc773c42104ab155a4ca5d6d4d174b7a35e01062213ea5/charset_normalizer-3.5.0-cp315-cp315t-win_amd64.whl", hash = "sha256:d7229a99120c6c2792d96f4857c2648ce5530e93667a2c2388c5ef69a6b84775", size = 215431, upload-time = "2026-08-12T14:34:42.501Z" },
{ url = "https://files.pythonhosted.org/packages/47/21/4869598aae0872d94faa5933918a4fe37ab2c5af9d095786e241f9506fed/charset_normalizer-3.5.0-cp315-cp315t-win_arm64.whl", hash = "sha256:527e28a5e751d9e11369b9c5f9ab35c748eb9c109101920c7deb40d6eadf8d03", size = 193205, upload-time = "2026-08-12T14:34:43.781Z" },
{ url = "https://files.pythonhosted.org/packages/5b/f3/7b523d807cb5e73562ef8acf21d39cdb9d704955327362c781bc3478a73d/charset_normalizer-3.5.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:5a4ee37248dfac25107c758bda99d545ce73e60b44d2dd39e4a2bb9f2831e9f5", size = 330840, upload-time = "2026-08-12T14:34:45.06Z" },
{ url = "https://files.pythonhosted.org/packages/f0/de/fc68978fe78ca97063c96d764e41ff92ca639948f319271e0ff450e577a2/charset_normalizer-3.5.0-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a864bdcacd8bff58bb4845304e031f821a3ec64b2b7259f2d409cd49c9e59ca3", size = 251862, upload-time = "2026-08-12T14:34:46.58Z" },
{ url = "https://files.pythonhosted.org/packages/a9/cb/82b41a0ab7fb1a88065f1d78ad32696ad88ea3fe8e25b8189d08833938de/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84b736e3b391601bc47b86da381c749c0f894e9191aaca9f31f30c2632206df3", size = 239484, upload-time = "2026-08-12T14:34:47.869Z" },
{ url = "https://files.pythonhosted.org/packages/e8/0c/19608b631f4538f908098d4a2d56a8f79a665e27cc58e9d90479761a9227/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6abb1f356fb865baeb6ebc3fadd843e9a96fbf49b9adcca55037f3cceccb7438", size = 230602, upload-time = "2026-08-12T14:34:49.265Z" },
{ url = "https://files.pythonhosted.org/packages/29/db/f648eb30e14eba301aed61e11672156f137905c1bdbb530151abe8065943/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d366548d2ee28a8cfdcc4296363978cc644a728333be9824d2de4652e83df0a", size = 259208, upload-time = "2026-08-12T14:34:50.632Z" },
{ url = "https://files.pythonhosted.org/packages/d3/e0/ed2c8bdbac484d69614d6993143aeb6cb0f4dd1561c883402517b623c8ef/charset_normalizer-3.5.0-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d672f329ae504ee240eb39b6effb3318aa8e7e8924c0ce8eee5760b3fad98539", size = 253659, upload-time = "2026-08-12T14:34:52.11Z" },
{ url = "https://files.pythonhosted.org/packages/32/08/b4907cb9ec5b521d9d024ced13611240b86ef065c2eb15b3ad2334dc9940/charset_normalizer-3.5.0-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c54036a518748b6c02e666f6d46c3817561998fb904c3be25b56fb4fe3dc5706", size = 248821, upload-time = "2026-08-12T14:34:53.399Z" },
{ url = "https://files.pythonhosted.org/packages/12/b2/e2d1abcfbc05822f0030869efb4e9f8a3658e13b4821796d4b62da917327/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:22a1889f1c9b752c63c36758a0c2145458e3cadb20fced7a0790002e9dd12b26", size = 240271, upload-time = "2026-08-12T14:34:55.09Z" },
{ url = "https://files.pythonhosted.org/packages/dc/f9/4ba127ad610542fa3eabfa41c45bf12d357860a815b3566374ec0188e213/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:b7eb3eab5c646d3de7dcb14a7c9caebace5249c5767da39e1761cb1576e521a3", size = 232155, upload-time = "2026-08-12T14:34:56.543Z" },
{ url = "https://files.pythonhosted.org/packages/01/68/40613182366d00bd6dbd5f6c84a926cbd120960e038a8269e9ae7d782762/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:2080aa129a28267984cdc902898993d788c995c384e285d0d19199f56760d52e", size = 259674, upload-time = "2026-08-12T14:34:57.815Z" },
{ url = "https://files.pythonhosted.org/packages/0a/53/4574a14fa4c9de4a6c9f31725354bfa40b67f653e6d594ce1654f9a41b32/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:fd68c825548a611158230e2f9222e210ceb2e3391995c0aa5865cbdf3ab4bd49", size = 246122, upload-time = "2026-08-12T14:34:59.337Z" },
{ url = "https://files.pythonhosted.org/packages/5a/02/bd8030d13d92c058ca7b2b9615bbb3169569e144db64d65c149cd45abf5e/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:d8a9316f4da85e937242642b537c6d55d7e9287dd38e5634732f8233932aff45", size = 255221, upload-time = "2026-08-12T14:35:00.71Z" },
{ url = "https://files.pythonhosted.org/packages/77/9d/10ecd3bcbe2666b3d4d4026c97b48f73990682815db516052a1e8f4a31c5/charset_normalizer-3.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d90254c8f609338c53ec180fcd4c4f9c16502e238e3fc88ca7fd4c2f38d445b8", size = 253450, upload-time = "2026-08-12T14:35:02.217Z" },
{ url = "https://files.pythonhosted.org/packages/e4/0f/d044c4872c0938a84f87b5027a698c0e61bacfc5c3551a4e749ca9b7bc5c/charset_normalizer-3.5.0-cp37-abi3-win32.whl", hash = "sha256:8b3e9e29b8b07cc461b9ce7768db7693a93979d0dadf22046f6f3555ded2f516", size = 173594, upload-time = "2026-08-12T14:35:03.845Z" },
{ url = "https://files.pythonhosted.org/packages/10/6b/6046773901f1944b9a89436351529811ee958afc7b774563be9d74a6f0c3/charset_normalizer-3.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:0c8953d9d1617794cfc40d81179571c9ba3805dd029623a15c93f1fb70e60a74", size = 198959, upload-time = "2026-08-12T14:35:05.187Z" },
{ url = "https://files.pythonhosted.org/packages/ab/a6/b57708ac92aefc8e8389d51d5178129b81f03196da61ee2c23e687b8178a/charset_normalizer-3.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:562d24ca7797c1af8852994950c2e623a907b201fc4b0ed29e92af173d3828ca", size = 267055, upload-time = "2026-08-12T14:35:06.533Z" },
{ url = "https://files.pythonhosted.org/packages/22/c7/754d09943a616937df61e4ba367c409ded2a987e872972098d51a6fcf73b/charset_normalizer-3.5.0-py3-none-any.whl", hash = "sha256:993dfcbe75a85a3784abb5084f2c41b915767c90546fcc92803cffa28611baea", size = 67943, upload-time = "2026-08-12T14:35:30.363Z" },
]
[[package]] [[package]]
name = "click" name = "click"
version = "8.4.2" version = "8.4.2"
@@ -165,52 +325,86 @@ wheels = [
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "49.0.0" version = "50.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, { name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
] ]
sdist = { url = "https://files.pythonhosted.org/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } sdist = { url = "https://files.pythonhosted.org/packages/de/41/6cbdcf9142d00fe82836fbb51e503e58088575cf7a0fe1dbff6695bf0840/cryptography-50.0.0.tar.gz", hash = "sha256:eeac2acb5a20ed25e0ad6d1df9891a520b78b404266b6d11778f25d5d691a6c9", size = 880201, upload-time = "2026-07-31T14:25:10.11Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, { url = "https://files.pythonhosted.org/packages/c5/5c/59086b4aac5e879d38ddbcf74e4be7ade89cebc3eb199a55da998c3bb46a/cryptography-50.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:031e2d5dd4bb9caa3ca9c82e5a197fd8ae680232cee62603d1a813f3f07e3d03", size = 4001252, upload-time = "2026-07-31T14:23:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, { url = "https://files.pythonhosted.org/packages/57/ef/8f2df13c7216bcad3e1c74e07f6e193d93e998e114f524a53877c9af27ad/cryptography-50.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd9192b7b70c573d7f214eb1ae35e00d359f6f5e4b27c7e21e30de1fc6204645", size = 4719554, upload-time = "2026-07-31T14:23:35.611Z" },
{ url = "https://files.pythonhosted.org/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, { url = "https://files.pythonhosted.org/packages/d9/41/029086c34d91052fc3b88bcc8056f709a7c915c7a23b235a54eb800b1c97/cryptography-50.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:06a32a980526a6ab9a4b9bf8f7385800791e2bb960903cb6b530e4817509a3b7", size = 4702130, upload-time = "2026-07-31T14:23:37.635Z" },
{ url = "https://files.pythonhosted.org/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, { url = "https://files.pythonhosted.org/packages/7d/ff/b6ce0954962e7f7b969f850a883744197bb3910bdfd7b6da162eab7d9f68/cryptography-50.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a1b30560f2acc95aa8b2e06e716a13dbfc97314747b80d9707e307f77b40d6b3", size = 4725244, upload-time = "2026-07-31T14:23:39.471Z" },
{ url = "https://files.pythonhosted.org/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, { url = "https://files.pythonhosted.org/packages/06/1e/63a1027cb7fec360a182208e1b7767d5aa1fe57be3d6aa856e69a321edc0/cryptography-50.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8d89f3976b10b4ce31118de72329025f70d2c6ead14a8217c5514dd2c6d5a78f", size = 5342265, upload-time = "2026-07-31T14:23:41.286Z" },
{ url = "https://files.pythonhosted.org/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, { url = "https://files.pythonhosted.org/packages/6b/72/a1116d683a6d7ece94590013882515de087edf9ef0e6292aae615a44df73/cryptography-50.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b42a28c1844fd9de8f3f7d540e36b66f3a9c83fceac7170ebc7a6a19edd9dcae", size = 4734609, upload-time = "2026-07-31T14:23:43.139Z" },
{ url = "https://files.pythonhosted.org/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, { url = "https://files.pythonhosted.org/packages/15/37/36a9c479bbe49acea2636c7fd3360d20f7b7e079c300352011c44850b181/cryptography-50.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:900131fafd8aead39ac7dd3a7e833be754c17a95cfd91221636949fe4eb0aa8a", size = 4356517, upload-time = "2026-07-31T14:23:44.939Z" },
{ url = "https://files.pythonhosted.org/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, { url = "https://files.pythonhosted.org/packages/32/98/8a151d64367204cbc63ec65d37502f1d9c53cf4bfc6ec3c532614dbec60d/cryptography-50.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:07949c449a1abcf60d1ee6e88956d89404c7df3c8258f46589e912988e551987", size = 4724529, upload-time = "2026-07-31T14:23:46.93Z" },
{ url = "https://files.pythonhosted.org/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, { url = "https://files.pythonhosted.org/packages/22/f6/ec13b470172126464a86bf54d2294a46d29837fc51ba3e45d4047946fb5e/cryptography-50.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f89831ef99dd7dd169ab06d63a831adb9e20a87aac6d380266bbda5823349169", size = 5299852, upload-time = "2026-07-31T14:23:48.851Z" },
{ url = "https://files.pythonhosted.org/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, { url = "https://files.pythonhosted.org/packages/da/3a/f05e32c99d440c9bb891ea0e36c9091891e36be5a9a87ab2ee6ea20729f6/cryptography-50.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:82148ec5bddac30b51a5b3c1945075f896fa022cb93f8e4a01e9f6ee95292c5f", size = 4734462, upload-time = "2026-07-31T14:23:50.861Z" },
{ url = "https://files.pythonhosted.org/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, { url = "https://files.pythonhosted.org/packages/ca/dc/bd72b26be8953f80625f63151efd38eee71c76ca6cf591c08ff34615a79e/cryptography-50.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1489e263a8048bb8b6a8bac662eb2d402ea5d2b7b4699b72f385f1e2772db105", size = 4852708, upload-time = "2026-07-31T14:23:52.715Z" },
{ url = "https://files.pythonhosted.org/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, { url = "https://files.pythonhosted.org/packages/27/20/c930314a2ab476d15dec966ec87e2e9637bb02b06106b12c0396c57bb603/cryptography-50.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7cec5b856506da6defb290f30c9ee687d5f5e8cb0bd3f6459dde43b0b4fa40ef", size = 5004179, upload-time = "2026-07-31T14:23:54.887Z" },
{ url = "https://files.pythonhosted.org/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, { url = "https://files.pythonhosted.org/packages/32/2e/c9db68a0c4bfa28e310707527c0ee3a2bd254104d2e02e68f368e197aa4c/cryptography-50.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bd1c592e4d5974f0d08d4888e432157adba757c66da0246918e43677fafa2d30", size = 3840395, upload-time = "2026-07-31T14:23:56.677Z" },
{ url = "https://files.pythonhosted.org/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, { url = "https://files.pythonhosted.org/packages/c3/fb/951032a3bf22a5697c83183fb6294a4843772947a70e616c57b3ff5f522e/cryptography-50.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:49e7d93abdbd2990caced757e5fade25302f719c3c8fb6e6fff2dde98999fc41", size = 3989258, upload-time = "2026-07-31T14:23:58.881Z" },
{ url = "https://files.pythonhosted.org/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, { url = "https://files.pythonhosted.org/packages/d4/67/91eb047e69c5e845f2f14b8a2e4a1aab0f283cb885531e9e22c8adb176bc/cryptography-50.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:19736989797678c6af1e55cd49055cdbcb55d8f6b5583ac5335f933aba9101dc", size = 4700648, upload-time = "2026-07-31T14:24:00.702Z" },
{ url = "https://files.pythonhosted.org/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, { url = "https://files.pythonhosted.org/packages/30/82/85f0f7425c856b9f96459411eb12e74ef72df9caf6f8f15bf23a33ff131f/cryptography-50.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80b63928fa35083b33966ce1efb70e5b9607181e49dcd1c22c8c005e319f667f", size = 4682442, upload-time = "2026-07-31T14:24:02.538Z" },
{ url = "https://files.pythonhosted.org/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, { url = "https://files.pythonhosted.org/packages/1a/28/b555a365adff1cca2fbe7b9e487d68a40de6bc67ff2cb587473eb43de0e7/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d58c3db7cd6eed54e6c06744db55456b65ebd7492ddeae9c1e93cfca7aa857d3", size = 4707596, upload-time = "2026-07-31T14:24:04.394Z" },
{ url = "https://files.pythonhosted.org/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, { url = "https://files.pythonhosted.org/packages/72/d8/f52538140cc719df62a01cf87d1c7142318d235817109d6f4054d7c352d6/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:df2a58a472f332225671c35b0a830208b86d004f82baa8530fa3782c85646533", size = 5314552, upload-time = "2026-07-31T14:24:06.31Z" },
{ url = "https://files.pythonhosted.org/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, { url = "https://files.pythonhosted.org/packages/38/14/6120e5bd7c5aa022ad15424ba4d5c5269d0d9448ed4d55e492ea91e3c1c4/cryptography-50.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11b74db56cdbe3cdee6e3f6982ecb70334fa10dce99ed58bf7894aaaa3b2a037", size = 4717113, upload-time = "2026-07-31T14:24:08.349Z" },
{ url = "https://files.pythonhosted.org/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, { url = "https://files.pythonhosted.org/packages/fa/71/190bf38c3ee2e0f8efc9860ae100c9df4169742eef274b91e7aa1cb133b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:f59e38625469987d7ef6d495323c55e7db6c212eaf6112267e0d3b565a2e9c9f", size = 4338580, upload-time = "2026-07-31T14:24:10.227Z" },
{ url = "https://files.pythonhosted.org/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, { url = "https://files.pythonhosted.org/packages/3a/63/504ccfbbe61fd8aa983f7f146399cdf034c72c2fc55f5b2dfdcdcdb20c99/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ecfed7367f965a0328cfbdd70da860f15441f002f613185668c6e6ebf5a0ac11", size = 4707038, upload-time = "2026-07-31T14:24:12.169Z" },
{ url = "https://files.pythonhosted.org/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, { url = "https://files.pythonhosted.org/packages/01/77/2cf79bbfc4d12ca106437a6e170d6aaa01a373e93093118aaaef0e801bd4/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:9aa87839c383bdbab6ef865787a1fb877af8dd03464c4400322726feaaadfc6d", size = 5273110, upload-time = "2026-07-31T14:24:14.38Z" },
{ url = "https://files.pythonhosted.org/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, { url = "https://files.pythonhosted.org/packages/e5/45/8aae2972c520145377ea3559a605a899bebe227bf070b33cdb445929a9b9/cryptography-50.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:6ba6a53445bd3cfa809ef3ef5f1589aa6ba08784a1d962bf47d0940e871dab1c", size = 4716439, upload-time = "2026-07-31T14:24:16.415Z" },
{ url = "https://files.pythonhosted.org/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, { url = "https://files.pythonhosted.org/packages/7b/20/4fe50b619a48c2525cc46e2dbc1ac490708d704be5d467bdaac6dc955682/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3f5735ffe4996d28b809371756219f5354864902a3b9e7c0b9ee87041209fc9c", size = 4837383, upload-time = "2026-07-31T14:24:18.553Z" },
{ url = "https://files.pythonhosted.org/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, { url = "https://files.pythonhosted.org/packages/92/91/3a31366e183343d3703f8995c095f5734676bd6938118047e50fcf279eb4/cryptography-50.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:1b4a266766514614f8aa60416e71f2fc6e575d36e7bdc90f644fadb2f4b75b95", size = 4985772, upload-time = "2026-07-31T14:24:20.385Z" },
{ url = "https://files.pythonhosted.org/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, { url = "https://files.pythonhosted.org/packages/74/9a/02ffe35b2853d121689871eb5dce862092562b3a1ed5cc98f1aaed441506/cryptography-50.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:12b9c6996425c76ea6c457ace4f3073e715b8c545add07cd1a8f3a4f90691269", size = 3816291, upload-time = "2026-07-31T14:24:22.125Z" },
{ url = "https://files.pythonhosted.org/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, { url = "https://files.pythonhosted.org/packages/03/37/73d005be173aff344af30e9fd2a576575cb2391a7101d9cd3842e1fa8cce/cryptography-50.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ccdc4a71a4dabae05de219404f9f4abc38e3b58422177ff93d0da05967dafa07", size = 4036009, upload-time = "2026-07-31T14:24:24.122Z" },
{ url = "https://files.pythonhosted.org/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, { url = "https://files.pythonhosted.org/packages/ff/c6/7a6202a534e32103a285b7834a120869557fe198d51d7cfe59754c8bda9c/cryptography-50.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:910e1d2668e7de9648f2bcee30e180db2a6b15c30f887d7c4c93ddf96e3992e3", size = 4745252, upload-time = "2026-07-31T14:24:26.118Z" },
{ url = "https://files.pythonhosted.org/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, { url = "https://files.pythonhosted.org/packages/85/4f/0fa8c2f4428198f15d9ff8d63400e27afbf94ce833f6108da1eb3753f945/cryptography-50.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a91296cb61e8df6f86d0c19cc4068228da256bf59bf86049fbd821084565327f", size = 4728939, upload-time = "2026-07-31T14:24:27.994Z" },
{ url = "https://files.pythonhosted.org/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, { url = "https://files.pythonhosted.org/packages/d1/63/54dd723490ba2dc09b299682c10b38db38f159728bcaae8c591b8af2f22d/cryptography-50.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:e722f16708d854fe924790e051061f6704a472c3bac347b6fd88033ea8dd0dc5", size = 4748483, upload-time = "2026-07-31T14:24:30.254Z" },
{ url = "https://files.pythonhosted.org/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, { url = "https://files.pythonhosted.org/packages/1d/dd/7c77d26285cc7f6991efce64a0f5b4f9383bfa5dd8c5033003eaf7db4cdb/cryptography-50.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:d764dcf130c428ef66786f866dd750f53182bc608813489915e9fc106bb0c82f", size = 5367599, upload-time = "2026-07-31T14:24:32.457Z" },
{ url = "https://files.pythonhosted.org/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, { url = "https://files.pythonhosted.org/packages/46/c9/f60aed34c013f317f92817b6c171c2d22a78270fa41109bd4b08af26b194/cryptography-50.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:105110f43a471dbd0060b9c9516cb8a6a79233631a04cc2ba16f28323ac6e025", size = 4762647, upload-time = "2026-07-31T14:24:34.599Z" },
{ url = "https://files.pythonhosted.org/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, { url = "https://files.pythonhosted.org/packages/be/f3/f9a0173b139372c3a48ed98154b45cc6b9de17c789d5ab552e621c293609/cryptography-50.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:828743d939e9629bc267b8e2d08d8bb67cd4319c771a33d4b18b22dd8fb7440a", size = 4385197, upload-time = "2026-07-31T14:24:36.647Z" },
{ url = "https://files.pythonhosted.org/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, { url = "https://files.pythonhosted.org/packages/d8/36/83bb81f6e569bc38e1e4a7bc80f29b46bb9601920bc455fc8e888f5d5742/cryptography-50.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:2a8183b489dc1f7f80f135780fadc1108f14b31b8a40411c7a5b17425f65f28b", size = 4748095, upload-time = "2026-07-31T14:24:39.493Z" },
{ url = "https://files.pythonhosted.org/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, { url = "https://files.pythonhosted.org/packages/6b/16/d3008eff98c764979865834c3d386d4fd041b5f52e7f34fc29ac1a5eb515/cryptography-50.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6e7d61120573a7f2cd94cc095f9e81f6967c61ccdf194285aa143ecec8e0b708", size = 5325948, upload-time = "2026-07-31T14:24:41.556Z" },
{ url = "https://files.pythonhosted.org/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, { url = "https://files.pythonhosted.org/packages/9c/f8/d97f9603efda3888187bfdb893f26c41be4735c10631d05d284ee6b047c4/cryptography-50.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:37fdb0d0111f1e2ff07139dfb79f1b49531f8e213c46f1163dd7642979b58c47", size = 4762400, upload-time = "2026-07-31T14:24:43.636Z" },
{ url = "https://files.pythonhosted.org/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, { url = "https://files.pythonhosted.org/packages/64/a2/4615c8f7d81a00b1d6e6afe19f694e1543582349fb5f4076f6cb5dc36485/cryptography-50.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c87f62a3d3b9888ed0fdde100ec06aa61ca9cd44bad9057d1dff9a516b5f5bb9", size = 4878208, upload-time = "2026-07-31T14:24:45.522Z" },
{ url = "https://files.pythonhosted.org/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, { url = "https://files.pythonhosted.org/packages/d2/1a/efcfb02f91407149a0dacffffab791f7e19bf6385f63b3666dc8b5e5c9c8/cryptography-50.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:65c2c3add92b45fd0709db8594536aea39c2a67af0e27ffcf049c498501140b7", size = 5037050, upload-time = "2026-07-31T14:24:47.697Z" },
{ url = "https://files.pythonhosted.org/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, { url = "https://files.pythonhosted.org/packages/57/30/4a22984d4f1bdfb8c054f07a92bc176b97a3134cc1d6c4b3bffb1f3688b4/cryptography-50.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:d24fead1d4d076e1bfb006dcec392074a3cd8d7b4fc8a595aa64073b2b7a96ba", size = 3874135, upload-time = "2026-07-31T14:24:50.085Z" },
]
[[package]]
name = "cyclonedx-python-lib"
version = "11.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "license-expression" },
{ name = "packageurl-python" },
{ name = "py-serializable" },
{ name = "sortedcontainers" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/17/40/6509e6cfd7f2f3255501690f46375fdd224949e21fd1e96f4f4c8a9041b1/cyclonedx_python_lib-11.12.0.tar.gz", hash = "sha256:16767c4039de90c04e9f03348f8f0ed4b8ff842eaa7eefcad3a95685f970dacf", size = 1445378, upload-time = "2026-08-13T07:52:18.675Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cf/f0/b2cb999244f2f4194df63ecff6939228178fa63a33be809c412684ca8db7/cyclonedx_python_lib-11.12.0-py3-none-any.whl", hash = "sha256:0e807521a921a5c3cb8ce1153f8a61d29eedfe76a46aac2796b7c6b573391a54", size = 529453, upload-time = "2026-08-13T07:52:16.836Z" },
]
[[package]]
name = "defusedxml"
version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" },
]
[[package]]
name = "filelock"
version = "3.32.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7d/64/a02e6765de08964ed371eca577870593245afc9dfac16d037de7c10d18e6/filelock-3.32.3.tar.gz", hash = "sha256:0ffa185a3540854c95caa7fa76b76cb219d907415e2c5dc9af25fd970563487f", size = 218135, upload-time = "2026-08-13T16:00:05.577Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a7/8e/50f46a9c0ce8d2861a394c1347caae037ea0431d2f67d7feb151cbc4649a/filelock-3.32.3-py3-none-any.whl", hash = "sha256:7f0ca4bcc0e181c60dbbd8aa9ab5b120ebb99e4e064e83636340056f833a1f09", size = 98901, upload-time = "2026-08-13T16:00:03.974Z" },
] ]
[[package]] [[package]]
@@ -295,6 +489,119 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" },
] ]
[[package]]
name = "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/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]]
name = "license-expression"
version = "30.4.4"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "boolean-py" },
]
sdist = { url = "https://files.pythonhosted.org/packages/40/71/d89bb0e71b1415453980fd32315f2a037aad9f7f70f695c7cec7035feb13/license_expression-30.4.4.tar.gz", hash = "sha256:73448f0aacd8d0808895bdc4b2c8e01a8d67646e4188f887375398c761f340fd", size = 186402, upload-time = "2025-07-22T11:13:32.17Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/af/40/791891d4c0c4dab4c5e187c17261cedc26285fd41541577f900470a45a4d/license_expression-30.4.4-py3-none-any.whl", hash = "sha256:421788fdcadb41f049d2dc934ce666626265aeccefddd25e162a26f23bcbf8a4", size = 120615, upload-time = "2025-07-22T11:13:31.217Z" },
]
[[package]] [[package]]
name = "markdown-it-py" name = "markdown-it-py"
version = "4.2.0" version = "4.2.0"
@@ -347,23 +654,246 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
] ]
[[package]]
name = "msgpack"
version = "1.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/31/f9/c0a1c127f9049db9155afc316952ea571720dd01833ff5e4d7e8e6352dbb/msgpack-1.2.1.tar.gz", hash = "sha256:04c721c2c7448767e9e3f2520a475663d8ee0f09c31890f6d2bd70fd636a9647", size = 183960, upload-time = "2026-06-18T16:13:52.594Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bc/dd/9e8cbd8f5582ca4b590336f2b91ee5662f6a6ca562b565abaf696a0f81ff/msgpack-1.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2ef59c659f289eddf8aa6623823f19fa2f40a4029266889eac7a2505dd210c35", size = 83531, upload-time = "2026-06-18T16:12:58.249Z" },
{ url = "https://files.pythonhosted.org/packages/50/2e/ebdb85a8da151397a2790363676b7ed7c125924fe618e4c6d8befb0cc62c/msgpack-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d3567748a5107cb40cdf66a275430c2f87c07777698f4bfd25c35f44d533258c", size = 82657, upload-time = "2026-06-18T16:12:59.396Z" },
{ url = "https://files.pythonhosted.org/packages/26/aa/753ad8b007b464e1d8aa0c8e650b9c5f4f725e658fc5ac8a7635c55b7f6e/msgpack-1.2.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:60926b75d00c8e816ef98f3034f484a8bc64242d66839cef4cf7e503142316a0", size = 410634, upload-time = "2026-06-18T16:13:00.383Z" },
{ url = "https://files.pythonhosted.org/packages/6a/fd/6adabd4f6d5e686f97dd02ce7fce3fe4cf672cbac36b8f67ff4040e8ad8b/msgpack-1.2.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:020e881a764b20d8d7ca1a54fc01b8175519d108e3c3f194fddc200bda95951a", size = 419989, upload-time = "2026-06-18T16:13:01.776Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cc/85039b7b0eb168aaad7383a23c97e291a11f08351cb45a606ce865e4e3f1/msgpack-1.2.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4202c74688ca06591f78cb18988228bd4cca2cc75d57b60008372892d2f1e6e6", size = 377544, upload-time = "2026-06-18T16:13:03.637Z" },
{ url = "https://files.pythonhosted.org/packages/ed/bf/35963899493b32030c85fc513b723ae66144ac70c11ebc52e889e16e3d99/msgpack-1.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8b267ce94efb76fbd1b3373511420074ee3187f0f7811bf394531de13294735a", size = 400842, upload-time = "2026-06-18T16:13:05.012Z" },
{ url = "https://files.pythonhosted.org/packages/a6/df/8e2ac970c8f99264cd9997d1c73df5466bc19da3301d7dc5500862a9b089/msgpack-1.2.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f1d0f8f98ade9634e01fb704a408f9336c0a8f1117b369f5db83dc7551d8b1", size = 374108, upload-time = "2026-06-18T16:13:06.232Z" },
{ url = "https://files.pythonhosted.org/packages/17/dd/fa8bd265110dfa51c20cb529f9e6d240a16fafe7e645004c6af2d01353ba/msgpack-1.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f02cf17a6ca1abe29b5f980644f7551f94d71f2011509b26d8625ce038f0df64", size = 414939, upload-time = "2026-06-18T16:13:07.478Z" },
{ url = "https://files.pythonhosted.org/packages/2e/b9/8377a5ad8953fc0437c70cc98d9ae29f27fe5ac5109fbec0812085865735/msgpack-1.2.1-cp312-cp312-win32.whl", hash = "sha256:0c0d9802354507bcba62af19c17918e3eb437cc25e6f50657d511b5856a77aac", size = 64504, upload-time = "2026-06-18T16:13:08.822Z" },
{ url = "https://files.pythonhosted.org/packages/57/7f/ce1e377df7e62461fefd9eb23bfb93a4a523f40a517b377b8f844d836828/msgpack-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:5c24aa15d5963051e1a5c62b12c50cd705992502b5ec1f3bece6046f33c9fc24", size = 71421, upload-time = "2026-06-18T16:13:09.828Z" },
{ url = "https://files.pythonhosted.org/packages/8f/32/ebfe84c9929f08f188d56c7a2fd913406a9ddad76a634697c1c43b8112e6/msgpack-1.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:4227224aaec8f7fbcbfbd4272319347b2bb4030366502600f8c45588c5187b07", size = 64775, upload-time = "2026-06-18T16:13:11.056Z" },
{ url = "https://files.pythonhosted.org/packages/b0/ac/dcddcab6f6c20ecb387ca5e980371cdb3f87ff69aeca388be97eebc4c074/msgpack-1.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0a70e3cf2804a300d921bb0940426e35f4e489a23adfb77a808892241db0a064", size = 83151, upload-time = "2026-06-18T16:13:12.173Z" },
{ url = "https://files.pythonhosted.org/packages/64/71/fbcfa83a1d6a9c6091942d1cfd070962244664b87427a9a49a6897b1b219/msgpack-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:491cc39455ca765fad51fb451bf2915eb2cf41192ab5801ce8d67c1d614fe056", size = 82351, upload-time = "2026-06-18T16:13:13.194Z" },
{ url = "https://files.pythonhosted.org/packages/e3/10/ddf7b06db879e8792d13934ddda09ff20bd2a583fd84c9b59aae9b0e650b/msgpack-1.2.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f310233ef7fb9c14e201c93639fe5f5260b005f56f0b29048e999c30935596cc", size = 407518, upload-time = "2026-06-18T16:13:14.233Z" },
{ url = "https://files.pythonhosted.org/packages/79/d3/36a46a8ed992b781acbc05928bd5bee3c810cb0c3563bf81a7b0c04a1a76/msgpack-1.2.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787c9bebb5833e8f6fc8abca3c0597683d8d87f56a8842b6b89c75a5f3176e2d", size = 416405, upload-time = "2026-06-18T16:13:15.435Z" },
{ url = "https://files.pythonhosted.org/packages/f9/84/e8e9598b557c0ba6ddae901a73780a4c75ac667dddf59414b1e56a42fb34/msgpack-1.2.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:dc871b997a9370d855b7394465f2f350e847a5b806dd38dcc9c989e7d87da155", size = 376257, upload-time = "2026-06-18T16:13:17.022Z" },
{ url = "https://files.pythonhosted.org/packages/40/16/738fe6d875ad7e2a9429c165322a4ec088f4f273cdfae63d96a89c467961/msgpack-1.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:85f57e960d877f2977f6430896191b04a21f8901b3b4baf2e4604329f4db5402", size = 397469, upload-time = "2026-06-18T16:13:18.287Z" },
{ url = "https://files.pythonhosted.org/packages/ca/be/6d5952df75a7f24f35833af764c3a6860780364cb3a0030beb8099e1b2b4/msgpack-1.2.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:1233ee2dd0cefba127583de50ea654677277047d238303521db35def3d7b2e7c", size = 372802, upload-time = "2026-06-18T16:13:19.685Z" },
{ url = "https://files.pythonhosted.org/packages/e1/39/e2ef7dbf0473bcb8dc7c50bf782a892d67414877b63e47fc88eb189ef5e6/msgpack-1.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e3dc2feb0876209d9c38aa56cb1de169bd6c4348f1aa48271f241226590993e6", size = 411273, upload-time = "2026-06-18T16:13:21.028Z" },
{ url = "https://files.pythonhosted.org/packages/ef/c5/133f4512a56e983a93445c836c9d94d88f3bc2e0980ff4b9e577bd8416ce/msgpack-1.2.1-cp313-cp313-win32.whl", hash = "sha256:6d09badf350af2be9d189184e04e64cf54ad93569ab3d96fca58bd3e84aad707", size = 64471, upload-time = "2026-06-18T16:13:22.293Z" },
{ url = "https://files.pythonhosted.org/packages/e2/98/577e10b055096a7dd40732358cabaf7180a20c79ed1dcdbb618e4b9deac7/msgpack-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:33f14fba63278b714efe6ad07e50ea5f03d91537aa6a1c5f1ceca4cf44013ca9", size = 71274, upload-time = "2026-06-18T16:13:23.455Z" },
{ url = "https://files.pythonhosted.org/packages/ba/ee/0c0048e7cfbef23c6a94791b8959ab28155232e7956de8a305b5ff588f05/msgpack-1.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc5febcd4c99effbc02b528e49d6fd0760b2b7d48c05239e345a5fa6e743d9a", size = 64795, upload-time = "2026-06-18T16:13:24.687Z" },
{ url = "https://files.pythonhosted.org/packages/77/58/cce442852c6b9e1639c7c8ac8fd9143121cb32dab0f308df4d1426a8eb9c/msgpack-1.2.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:05f340e47e7e47d2da8db9b53e1bb1d294369e9ef45a747441309f6650b8351d", size = 83610, upload-time = "2026-06-18T16:13:25.724Z" },
{ url = "https://files.pythonhosted.org/packages/60/5c/15b4c7a0182f75ffa90751958ba36a9c01cafee367d49a3edc10ed140b01/msgpack-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:810b916696c86ef0deb3b74588480224df4c1b071136c34183e4a2a4284d7ac7", size = 83138, upload-time = "2026-06-18T16:13:26.781Z" },
{ url = "https://files.pythonhosted.org/packages/b8/a6/99e58722feaffc5f2fbcc0c8c0d1451ab9f84097f7af87291b46af2390f4/msgpack-1.2.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca0dacff965c47afdc3749a8469d7302a8f801d6a28758d55120d75e66ce6889", size = 406090, upload-time = "2026-06-18T16:13:28.072Z" },
{ url = "https://files.pythonhosted.org/packages/19/03/8c63e8cf52958534ef688625965ab04c269a6cadd8caef16758b380a821a/msgpack-1.2.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e2bf9280bceb5efca998435904b5d3e9fdbcc11d90dc9df30aec7973252b720", size = 412106, upload-time = "2026-06-18T16:13:29.427Z" },
{ url = "https://files.pythonhosted.org/packages/63/d2/155d9e71b40e41fd934bc0c48b9b2770f22263e1ac20aad8e29fdca7be3f/msgpack-1.2.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:aa6c4be5d1c02a42b066ca6ddb71adf36432868fdcdb6ee87e634e86e0674190", size = 374851, upload-time = "2026-06-18T16:13:30.631Z" },
{ url = "https://files.pythonhosted.org/packages/98/48/deaf2326262a8d5ea3295ce9649912ecd3f551ba7ec8e33c665d2ba583f3/msgpack-1.2.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec0e675d59150a6269ddc9139087c722292664a37d071a849c05c473350f1f2d", size = 396168, upload-time = "2026-06-18T16:13:31.977Z" },
{ url = "https://files.pythonhosted.org/packages/10/2a/b4410f906c2ec0008f1608d3ab5143afc3ad3f4e6da0fed3ea2231d0bef4/msgpack-1.2.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:dd3bfe82d53edfe4b7fc9a7ec9761e23a7a5b1dac22264505af428253c29ed24", size = 371959, upload-time = "2026-06-18T16:13:33.282Z" },
{ url = "https://files.pythonhosted.org/packages/59/86/1edc67270099a528fa2093ea60fe191233cd238e4bd30cfacf7db79fc959/msgpack-1.2.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ad5467fc3f68b5468e06c5f788d712e9f8ffc8b0cd1bcb160c105c1ee92dae7", size = 408457, upload-time = "2026-06-18T16:13:34.567Z" },
{ url = "https://files.pythonhosted.org/packages/82/90/8b630fef07d8c5ab457b71ff2c217910c83d333c7a68472c186e87cc504a/msgpack-1.2.1-cp314-cp314-win32.whl", hash = "sha256:98b58bdb89c46190e4609bb36abe17c6d4105ad13f9c5f8f6f64d320f8ced3fb", size = 65942, upload-time = "2026-06-18T16:13:36.056Z" },
{ url = "https://files.pythonhosted.org/packages/16/f1/467b81e98b24dd3885d7b1857728797b4ffc76a7a7483af4fb321a07de3c/msgpack-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:74847557e28ce71bd3c438a447ca90e4b507e997ddbdef8a12a7b283b86c156b", size = 72627, upload-time = "2026-06-18T16:13:37.079Z" },
{ url = "https://files.pythonhosted.org/packages/a7/1d/5d8c4c89985feb6acefb82a09e501c60392261856d2408d20bfe4f0360b1/msgpack-1.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:b50b727bd652bdc37d950336c848ef20ec54a4cafc38dce19b1cd86ad625d0f7", size = 66908, upload-time = "2026-06-18T16:13:38.23Z" },
{ url = "https://files.pythonhosted.org/packages/1b/02/ad2afb678b4de94496cd432b581759b756a92c1192d8c767edd6b132efdc/msgpack-1.2.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:8d00f177ca88a77c1cf848d204a38f249751650b601cb6532acc68805d8a8273", size = 86000, upload-time = "2026-06-18T16:13:39.44Z" },
{ url = "https://files.pythonhosted.org/packages/54/74/0b797484013128837f3b1cbb6cea019277c4de4e377dc512b4d9a0f92940/msgpack-1.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5bb9c386f0a329c035ddbab4b72d1028bf9627add8dda41070288563d57ed1b1", size = 86544, upload-time = "2026-06-18T16:13:40.447Z" },
{ url = "https://files.pythonhosted.org/packages/a9/b4/b774d7eb95561739907fec675582f83203cf41c597a418c2589b4bfb8e9d/msgpack-1.2.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:20466cca18c49c7292a8984bc15d65857b171e7264bdcb5f96baf8be238791fc", size = 427661, upload-time = "2026-06-18T16:13:41.574Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f9/3243191dc9937e00756c8bc1b0272fed8f23758e43df2a3b46f533e5090f/msgpack-1.2.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:196300e7e5d6e74d50f1607ab9c06c4a1484c383cd22defd727902591f7e8dde", size = 426375, upload-time = "2026-06-18T16:13:42.936Z" },
{ url = "https://files.pythonhosted.org/packages/23/c7/1693111db9944ba4ad4b67a1e788400d78a0b6af7a6523dc7e4e58f8274b/msgpack-1.2.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575957e79cd51903a4e8495a242442949641e08f1efd5197b43bebd3ea7682b4", size = 380495, upload-time = "2026-06-18T16:13:44.306Z" },
{ url = "https://files.pythonhosted.org/packages/3e/2b/92f86956a0c13e8662f7e2ad630c4eb4db07497b967589bd5245e018b2c1/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c2ed1e48cc0f460bf3c7780e7137ff21a4e18433451916f2442c1b21036cd7d", size = 410897, upload-time = "2026-06-18T16:13:45.629Z" },
{ url = "https://files.pythonhosted.org/packages/da/ea/1479f72d200313a76fc2f823a79d1e07ed052ab7b8a0280640aa7b95de42/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:5f6277e5f783c36786a145e0247fc189a03f35f84b251646e53592d2bc12b355", size = 378519, upload-time = "2026-06-18T16:13:46.998Z" },
{ url = "https://files.pythonhosted.org/packages/f5/4d/fa006060ffa1011d32bfae826fe766fe73e02982183601633b7121058ab3/msgpack-1.2.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f9389552ecf4784886345ead0647e4edc96bee37cbab05b75540f542f766c48c", size = 419815, upload-time = "2026-06-18T16:13:48.205Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e1/aab6c946570496b78e67804721f3d5e2d62a93081b9b37df77764ef56347/msgpack-1.2.1-cp314-cp314t-win32.whl", hash = "sha256:c1c79a604a2969a868a78b6ebd27a887e00c624f14f66b3038e0590cb23332d1", size = 70914, upload-time = "2026-06-18T16:13:49.385Z" },
{ url = "https://files.pythonhosted.org/packages/13/0a/e608956488a2af014cfe6e3d665e090b8ee42aa14b07f8f95b8880d66b09/msgpack-1.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:f12038a35fabd52e56a3547bab42401af49a45caa6dd00b34c44de235bc93ee2", size = 77999, upload-time = "2026-06-18T16:13:50.467Z" },
{ url = "https://files.pythonhosted.org/packages/d2/8a/27e2e57055176e366a46b85d02d68e7a5bcfbdd8474c9706375d965f24d3/msgpack-1.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:0adcf06ffde0777c0e1a9b771a2b1c4226ba1bbf748c8efcc02fcdeca3299107", size = 71160, upload-time = "2026-06-18T16:13:51.498Z" },
]
[[package]]
name = "mypy"
version = "1.20.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "librt", marker = "platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions" },
{ name = "pathspec" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" },
{ url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" },
{ url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" },
{ url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" },
{ url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" },
{ url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" },
{ url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" },
{ url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" },
{ url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" },
{ url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" },
{ url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" },
{ url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" },
{ url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" },
{ url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" },
{ url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" },
{ url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" },
{ url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" },
{ url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" },
{ url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" },
{ url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" },
{ url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" },
{ url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" },
{ url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" },
{ url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" },
{ url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" },
{ url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" },
{ url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" },
{ url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" },
{ url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
]
[[package]] [[package]]
name = "nocobase-crm-mcp" name = "nocobase-crm-mcp"
version = "0.1.0" version = "0.2.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "cryptography" },
{ name = "httpx" }, { name = "httpx" },
{ name = "mcp", extra = ["cli"] }, { name = "mcp", extra = ["cli"] },
{ name = "python-multipart" }, { name = "python-multipart" },
] ]
[package.dev-dependencies]
dev = [
{ name = "mypy" },
{ name = "pip-audit" },
{ name = "ruff" },
]
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "cryptography", specifier = ">=50,<51" },
{ name = "httpx", specifier = ">=0.27,<1" }, { name = "httpx", specifier = ">=0.27,<1" },
{ name = "mcp", extras = ["cli"], specifier = ">=1.27,<2" }, { name = "mcp", extras = ["cli"], specifier = ">=1.27,<2" },
{ name = "python-multipart", specifier = ">=0.0.9,<1" }, { name = "python-multipart", specifier = ">=0.0.9,<1" },
] ]
[package.metadata.requires-dev]
dev = [
{ name = "mypy", specifier = ">=1.17,<2" },
{ name = "pip-audit", specifier = ">=2.9,<3" },
{ name = "ruff", specifier = ">=0.12,<1" },
]
[[package]]
name = "packageurl-python"
version = "0.17.6"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f5/d6/3b5a4e3cfaef7a53869a26ceb034d1ff5e5c27c814ce77260a96d50ab7bb/packageurl_python-0.17.6.tar.gz", hash = "sha256:1252ce3a102372ca6f86eb968e16f9014c4ba511c5c37d95a7f023e2ca6e5c25", size = 50618, upload-time = "2025-11-24T15:20:17.998Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b1/2f/c7277b7615a93f51b5fbc1eacfc1b75e8103370e786fd8ce2abf6e5c04ab/packageurl_python-0.17.6-py3-none-any.whl", hash = "sha256:31a85c2717bc41dd818f3c62908685ff9eebcb68588213745b14a6ee9e7df7c9", size = 36776, upload-time = "2025-11-24T15:20:16.962Z" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.org/simple" }
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/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]]
name = "pip"
version = "26.2.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ae/15/4500e320e6b101ec3b719ae85b697d9940b6cda672bc555bd6016fc60c6f/pip-26.2.1.tar.gz", hash = "sha256:f6ad667e89a1fe78046c8f13232b247200f5258d7828f3f7883d660878e0813f", size = 1848877, upload-time = "2026-08-04T22:51:14.148Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f3/6e/1736e5b4ae2b778ef2f81c47d797de9f891d4d8acb047a24ca37a60294dd/pip-26.2.1-py3-none-any.whl", hash = "sha256:71138adf1f4ca900cdb7d289c21b7494329f2332b6d85f0e1c42108c0384ed3e", size = 1816632, upload-time = "2026-08-04T22:51:12.472Z" },
]
[[package]]
name = "pip-api"
version = "0.0.34"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pip" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b9/f1/ee85f8c7e82bccf90a3c7aad22863cc6e20057860a1361083cd2adacb92e/pip_api-0.0.34.tar.gz", hash = "sha256:9b75e958f14c5a2614bae415f2adf7eeb54d50a2cfbe7e24fd4826471bac3625", size = 123017, upload-time = "2024-07-09T20:32:30.641Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/91/f7/ebf5003e1065fd00b4cbef53bf0a65c3d3e1b599b676d5383ccb7a8b88ba/pip_api-0.0.34-py3-none-any.whl", hash = "sha256:8b2d7d7c37f2447373aa2cf8b1f60a2f2b27a84e1e9e0294a3f6ef10eb3ba6bb", size = 120369, upload-time = "2024-07-09T20:32:29.099Z" },
]
[[package]]
name = "pip-audit"
version = "2.10.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cachecontrol", extra = ["filecache"] },
{ name = "cyclonedx-python-lib" },
{ name = "packaging" },
{ name = "pip-api" },
{ name = "pip-requirements-parser" },
{ name = "platformdirs" },
{ name = "requests" },
{ name = "rich" },
{ name = "tomli" },
{ name = "tomli-w" },
]
sdist = { url = "https://files.pythonhosted.org/packages/66/a4/f21d5f0a0edabcbce31560b73c7c5a6f72ae87af4236fd1069c8f59a353d/pip_audit-2.10.1.tar.gz", hash = "sha256:1eb4565d19ebe5d48996f4b770b4d2b32887e12cb12cfa637f1a064011b55ffc", size = 54275, upload-time = "2026-06-10T22:17:01.744Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a3/a7/b0c504148114047bd1bc9d97447453c6850ca176bb2f3c0038835994e8b7/pip_audit-2.10.1-py3-none-any.whl", hash = "sha256:99ef3f600a317c1945f1e89e227ef26e1c2d618429b8bd3fa6f4f7c440c4611a", size = 62023, upload-time = "2026-06-10T22:17:00.309Z" },
]
[[package]]
name = "pip-requirements-parser"
version = "32.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "pyparsing" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5e/2a/63b574101850e7f7b306ddbdb02cb294380d37948140eecd468fae392b54/pip-requirements-parser-32.0.1.tar.gz", hash = "sha256:b4fa3a7a0be38243123cf9d1f3518da10c51bdb165a2b2985566247f9155a7d3", size = 209359, upload-time = "2022-12-21T15:25:22.732Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/54/d0/d04f1d1e064ac901439699ee097f58688caadea42498ec9c4b4ad2ef84ab/pip_requirements_parser-32.0.1-py3-none-any.whl", hash = "sha256:4659bc2a667783e7a15d190f6fccf8b2486685b6dba4c19c3876314769c57526", size = 35648, upload-time = "2022-12-21T15:25:21.046Z" },
]
[[package]]
name = "platformdirs"
version = "4.11.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b8/d7/e7bfbc86e9f99ff7807e24de7703f032e9c9ba80bb355cf26e0e9bc5a75e/platformdirs-4.11.3.tar.gz", hash = "sha256:66a73d38a849810252df809a3d8bcbda8e26f6c189920e7535ad608a48dbb5ab", size = 33050, upload-time = "2026-08-13T22:43:27.52Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/19/a9/c34aebedd3a4c9afe5101b1b8713710b3fec18087c8a36c35d2f909861bd/platformdirs-4.11.3-py3-none-any.whl", hash = "sha256:5ed065d443751de711da036041a7a214122efc4a4de393b3f4137ba5576540e7", size = 23491, upload-time = "2026-08-13T22:43:26.121Z" },
]
[[package]]
name = "py-serializable"
version = "2.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "defusedxml" },
]
sdist = { url = "https://files.pythonhosted.org/packages/73/21/d250cfca8ff30c2e5a7447bc13861541126ce9bd4426cd5d0c9f08b5547d/py_serializable-2.1.0.tar.gz", hash = "sha256:9d5db56154a867a9b897c0163b33a793c804c80cee984116d02d49e4578fc103", size = 52368, upload-time = "2025-07-21T09:56:48.07Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9b/bf/7595e817906a29453ba4d99394e781b6fabe55d21f3c15d240f85dd06bb1/py_serializable-2.1.0-py3-none-any.whl", hash = "sha256:b56d5d686b5a03ba4f4db5e769dc32336e142fc3bd4d68a8c25579ebb0a67304", size = 23045, upload-time = "2025-07-21T09:56:46.848Z" },
]
[[package]] [[package]]
name = "pycparser" name = "pycparser"
version = "3.0" version = "3.0"
@@ -500,6 +1030,15 @@ crypto = [
{ name = "cryptography" }, { name = "cryptography" },
] ]
[[package]]
name = "pyparsing"
version = "3.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
]
[[package]] [[package]]
name = "python-dotenv" name = "python-dotenv"
version = "1.2.2" version = "1.2.2"
@@ -551,6 +1090,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, { url = "https://files.pythonhosted.org/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" },
] ]
[[package]]
name = "requests"
version = "2.34.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
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/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]] [[package]]
name = "rich" name = "rich"
version = "15.0.0" version = "15.0.0"
@@ -660,6 +1214,31 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, { url = "https://files.pythonhosted.org/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" },
] ]
[[package]]
name = "ruff"
version = "0.16.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/61/b3/3213589383f8f1b3938781bd1278713f6d18621a14992b3e81fefb8a5ef9/ruff-0.16.3.tar.gz", hash = "sha256:e76d33a347661a84b5be6d043d0347fdc745dfdcf825a8f4fed64b5e26eebdf2", size = 4891904, upload-time = "2026-08-13T15:17:13.381Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/bf/96/493770daebd68c0a67f1549fdf519f53be51fc435186c0585bcc272fd76c/ruff-0.16.3-py3-none-linux_armv6l.whl", hash = "sha256:0c5710e247a58a4521e66e124ba9a74655b414f61ba3a2e9e3811e11098f48f7", size = 10902799, upload-time = "2026-08-13T15:16:27.382Z" },
{ url = "https://files.pythonhosted.org/packages/5e/e6/2becf3942fddc29a29b8df47691d456fb1085391a694f74d84513251418c/ruff-0.16.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:fe155130631a2471fd2e14a7a664a4dfbd7194b8229c3d7b2a40b21178639081", size = 11135539, upload-time = "2026-08-13T15:16:30.87Z" },
{ url = "https://files.pythonhosted.org/packages/3e/1e/4b8b72f0d006dbf19326aa99f9ca0ee2ff374187c4d301cf529a51aa06fe/ruff-0.16.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e2ed719e14aa64d895c2ee922594a90a43c861a93f0575a95ff8c47cdbd13eb9", size = 10475095, upload-time = "2026-08-13T15:16:33.259Z" },
{ url = "https://files.pythonhosted.org/packages/92/32/2201fa49ba1f6c101ee321e83f051ac7a4b8d07b0ef6b4d3f2772b302275/ruff-0.16.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9e0b1da805eb043654645d74d5de1e5ce2edc686e40790d2b86f56d71cc06a84", size = 10668771, upload-time = "2026-08-13T15:16:35.65Z" },
{ url = "https://files.pythonhosted.org/packages/c3/66/4afc5c8363bd04d45effce1b7c8713ca037d7a6740b7451a2403a6e3a972/ruff-0.16.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a37bdea0bbe21780f590bf437d6412c8c4e1b6cd010f91a65c2c40c5e5f5f870", size = 10699568, upload-time = "2026-08-13T15:16:38.195Z" },
{ url = "https://files.pythonhosted.org/packages/53/fd/c67d246bf36bf1698551c56de39e95cd07f70e64433e0098e6267d77061b/ruff-0.16.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09571e6d1288ed9be475207a3ac04ada404f1cd898104be0f6ab8d7df438575b", size = 11499365, upload-time = "2026-08-13T15:16:40.623Z" },
{ url = "https://files.pythonhosted.org/packages/67/0b/00ecbceb99a263af7b12f6f05ac3c92bc47b905e91adc3f207a836e3bc01/ruff-0.16.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2c18c5a101eb540010638cc1ff3c84944d3adb3df62b8d98ca8f22ba484d3413", size = 12311728, upload-time = "2026-08-13T15:16:43.564Z" },
{ url = "https://files.pythonhosted.org/packages/54/b2/b7b3bb54f4d3f7db504e476ad4ab8de530dceebe2c061384b2757ee419e8/ruff-0.16.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8457c44f15033c85ddbb77b15d451df9e24e4bd03b628396dd3610cedc3b8f82", size = 11699896, upload-time = "2026-08-13T15:16:46.209Z" },
{ url = "https://files.pythonhosted.org/packages/c7/30/4c468429ac195addc5ee1b717b6ab1b66632786737ca3b2ed3443fb0c26a/ruff-0.16.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:294b95c4ae0cda9388525c2047778aa758d6b8d4bb876fd4e9eaa3ebc92343eb", size = 11058736, upload-time = "2026-08-13T15:16:48.823Z" },
{ url = "https://files.pythonhosted.org/packages/43/67/7a113cdaddf24b64d7f75b1242a99d04c82fcef4f6921fdbb832beaffb5f/ruff-0.16.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3d0c7c40c87c2a820509c31ba007968da6e1306468c067b2d82fbfdbcd0e8474", size = 11586911, upload-time = "2026-08-13T15:16:51.913Z" },
{ url = "https://files.pythonhosted.org/packages/f1/c1/2e66f24c0f3ead25a5e660111778685e505e5da353c82802bf49f0cbe7b9/ruff-0.16.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:9f738c0fdfa8eed0b2ce7fb27ee7258208a92a68d7949e62aa15164bc7b389da", size = 10954265, upload-time = "2026-08-13T15:16:54.763Z" },
{ url = "https://files.pythonhosted.org/packages/c2/ba/4cee23bf52cba9a058d3726de623624daf50ef9638868edd86f4126157f6/ruff-0.16.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fb785f0be25abe69d320415cd4f833b59e17ba7613d9ba6a958023b6bceb0a50", size = 10709886, upload-time = "2026-08-13T15:16:57.339Z" },
{ url = "https://files.pythonhosted.org/packages/82/df/7da7194fa5d9dc0a285f7e6fa5a4722e7c63faac0b45b614ded9314363a1/ruff-0.16.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c5536e3acfbf9563085aa2be7b13c629c3077e902afc5b941ac44024dbb9f506", size = 11210392, upload-time = "2026-08-13T15:17:00.171Z" },
{ url = "https://files.pythonhosted.org/packages/35/85/7795f6e817af050e7517bf3e7aa9b061cce70ef33d280aad902c956c1ecf/ruff-0.16.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a2d85c02f9b8e165d85e6779184d38c4132de12603dab59c51c28e22584f9e4d", size = 11626910, upload-time = "2026-08-13T15:17:03.299Z" },
{ url = "https://files.pythonhosted.org/packages/78/9b/475b927cf27a5cbbda3c7bafb69ed6ff77e1d7923d5d85f17c2749d7ae32/ruff-0.16.3-py3-none-win32.whl", hash = "sha256:388cdf2166642bd9b13d52b5932d3170f34f8abed7e8d9a855f1d84b83645a0a", size = 10931415, upload-time = "2026-08-13T15:17:05.726Z" },
{ url = "https://files.pythonhosted.org/packages/b2/99/e2a2bfc4fbf0a1e8a916bc9ebe6fe6c58cc34c28e0ffc6ce281d572d1c2e/ruff-0.16.3-py3-none-win_amd64.whl", hash = "sha256:e80a7d69ca2a6d1c4d352ec91458cdca6e56c83cdbcabd93e4abe1e53591d948", size = 11445993, upload-time = "2026-08-13T15:17:08.353Z" },
{ url = "https://files.pythonhosted.org/packages/69/3e/4132e539aed78c148854d4997a2685b0ed4dc4e87110b59ce528564e184e/ruff-0.16.3-py3-none-win_arm64.whl", hash = "sha256:b8ca152da82c1acc1fa8d5874b15951935f0eef46f10e6954c83859011b6178a", size = 11399302, upload-time = "2026-08-13T15:17:10.908Z" },
]
[[package]] [[package]]
name = "shellingham" name = "shellingham"
version = "1.5.4" version = "1.5.4"
@@ -669,6 +1248,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
] ]
[[package]]
name = "sortedcontainers"
version = "2.4.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" },
]
[[package]] [[package]]
name = "sse-starlette" name = "sse-starlette"
version = "3.4.5" version = "3.4.5"
@@ -695,6 +1283,60 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, { url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
] ]
[[package]]
name = "tomli"
version = "2.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
]
[[package]]
name = "tomli-w"
version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/19/75/241269d1da26b624c0d5e110e8149093c759b7a286138f4efd61a60e75fe/tomli_w-1.2.0.tar.gz", hash = "sha256:2dd14fac5a47c27be9cd4c976af5a12d87fb1f0b4512f81d69cce3b35ae25021", size = 7184, upload-time = "2025-01-15T12:07:24.262Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/18/c86eb8e0202e32dd3df50d43d7ff9854f8e0603945ff398974c1d91ac1ef/tomli_w-1.2.0-py3-none-any.whl", hash = "sha256:188306098d013b691fcadc011abd66727d3c414c571bb01b1a174ba8c983cf90", size = 6675, upload-time = "2025-01-15T12:07:22.074Z" },
]
[[package]] [[package]]
name = "typer" name = "typer"
version = "0.26.8" version = "0.26.8"
@@ -731,6 +1373,15 @@ 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/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
] ]
[[package]]
name = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://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]] [[package]]
name = "uvicorn" name = "uvicorn"
version = "0.51.0" version = "0.51.0"