feat: use each user's local NocoBase login
This commit is contained in:
@@ -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
|
||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -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`。
|
||||||
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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,
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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.")
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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")
|
||||||
|
|||||||
@@ -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] = []
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user