diff --git a/.gitignore b/.gitignore
index 200986c..cbaedff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,3 +21,6 @@ docs/paper/*.fls
docs/paper/*.fdb_latexmk
docs/paper/*.xdv
docs/paper/*.synctex.gz
+
+# pytest
+.pytest_cache/
diff --git a/README.md b/README.md
index 9ab2a54..cef810b 100644
--- a/README.md
+++ b/README.md
@@ -1,29 +1,92 @@
-# B题问题一 交会定位
+# mcm26b — CUMCM 2026 B题:无线电干扰源的快速自动定位与清除
-给定同一干扰源的检测点坐标及带 $\pm 1^\circ$ 误差的示向度,计算前向角域交集、定位区域直径、直径圆覆盖判断与最小包围圆。
+统一框架:有界误差几何定位、主动选点、带覆盖证明的搜索、按真实动作耗时调度。
+分析依据 `issue/B题.pdf`、附件1《模拟器使用说明》、附件2《通信接口说明及编程指南》。
-- [完整中文解答与算法证明](docs/问题一解答.md)
-- [修订后的四问思路与P0–P7计划](B题建模分析与实施计划.md)
-- [核心代码](geometry/localization.py)
-- [离线交互模拟](output/q1/question1.html)
-- [官方演练观测与原始记录](output/q1/practice/)
-- [优化与舍入敏感性报告](output/q1/optimization_report.json)
+- [四问建模分析与 P0–P7 实施计划](docs/B题建模分析与实施计划.md)
+- [协议规则表、字段表与状态机(P0 交付)](docs/协议规则表与状态机.md)
+- [问题一完整解答](docs/问题一解答.md)
+- [论文](docs/paper/main.tex)
-Python3.10及以上,核心算法和演示生成无需安装第三方库。
+## 项目结构
-```powershell
-python -m geometry examples/q1/triangle.json
-python main.py examples/q1/normal.json -o output/q1/result.json
-python -m geometry examples/q1/normal.json --rounding-margin-deg 0.005
-python -m unittest discover -s tests -v
-python scripts/build_q1_demo.py
-python scripts/benchmark_q1.py
+```
+src/mcm26b/
+├── config.py # 参数中心:题面常量[题面] + 工程参数[工程],唯一字面量来源
+├── protocol/ # P0/P1 字段表、计时模型、状态机、串行幂等客户端(已完工)
+├── geometry/ # P2 角域交会、直径、MEC、问题二安全候选盘证书(已完工)
+├── sim/ # 自建模拟器、故障注入、演练日志回放(测试专用,已完工)
+├── belief/ # P3 逐频道候选集合 K_c(骨架)
+├── coverage/ # P4 49点覆盖账本(骨架 + 网格生成)
+├── planner/ # P3 选点 / P4 全向闭环 / P5 定向扩展(骨架)
+└── evaluation/ # 日志复算与统计(复算已完工)
+tests/ # pytest(兼容 unittest 风格),53 项
+scripts/ # 采集、基准、可视化与校核脚本
+output/ examples/ # 演练证据、算例与生成产物
```
-用浏览器打开 `output/q1/question1.html`。可选择合成反例或官方演练,逐个加入测点,切换全景与定位区域放大。
+## 快速开始
-输出新增直径圆覆盖反例顶点、最大超出距离和 20 米清除余量;默认仍为题设 $\pm 1^\circ$,舍入裕量须显式启用。最小包围圆使用可复现的随机增量算法,原枚举法保留作独立对照。`geometry.first_bearing_candidate_disk` 提供问题二首次测向安全区域的解析证书;问题二选点优化器及问题三、四的全清闭环仍待实现。
+Python ≥ 3.10,依赖仅 `requests`(测试另需 `pytest`)。
-可视化事件检查使用Node.js:`node scripts/verify_visualization.cjs`。可选PNG重绘使用Pillow及Windows微软雅黑字体:`python scripts/render_q1_figures.py`;这些只用于图形校核,不是核心求解依赖。
+```bash
+uv sync # 安装(可编辑挂载本包)
+uv run pytest # 全部测试
+uv run mcm26b sim-check # P0 自检:复现附件计时示例 105/111/194/199
+uv run mcm26b q1 examples/q1/normal.json # 问题一求解(等价 python -m mcm26b.geometry ...)
+uv run python main.py examples/q1/triangle.json --rounding-margin-deg 0.005
+```
-本项目只使用官方**演练**采集了问题一所需观测,未使用正式测试。采集器不会启动测试,必须先在界面确认“问题3演练”、接口已就绪,再提供 `--practice-confirmed` 和本局案例编码。参见完整解答中的采集说明。
+## 问题一(P2,已完工)
+
+给定同频道多个检测点及 ±1° 误差示向度,计算前向角域交集(半平面交)、
+定位区域直径、直径圆覆盖判断(含反例顶点与超出距离)与最小包围圆、20m 清除余量。
+
+```bash
+uv run mcm26b q1 examples/q1/normal.json -o output/q1/result.json
+uv run python scripts/build_q1_demo.py # 重建离线交互可视化 output/q1/question1.html
+uv run python scripts/benchmark_q1.py # MEC 基准与舍入敏感性
+```
+
+离线交互页:`output/q1/question1.html`。可选 PNG 重绘需要 Pillow 与 Windows
+雅黑字体:`python scripts/render_q1_figures.py`;事件校核需要 Node.js:
+`node scripts/verify_visualization.cjs`。二者仅为图形校核,非求解依赖。
+
+## 协议基线(P0/P1,已完工)
+
+- `protocol.rules`:四条指令的字段表、结果码与错误语义(可执行表格);
+- `protocol.timing`:纯计时模型,逐步复现附件示例 105/111/194/199;
+- `protocol.state.RobotState`:位置/频道/虚拟时刻状态机;accepted=false 不更新,
+ `/clear` 不切频道,虚拟时刻以 accepted 响应为准;
+- `protocol.client.SimulatorClient`:串行发送、幂等重试(同 ID 同内容原样重发)、
+ 连接/读超时、基于 `remaining_real_duration_s` 的本地截止时钟与退出预留、JSONL 会话日志;
+- `sim.LocalSimulator`:按附件语义复现模拟器(观测门限、定向覆盖角、幂等回放、
+ 400/accepted=false 语义),真值只对测试端开放;`ScriptedFaultTransport` 注入断连/超时/500,
+ `PlaybackTransport` 回放官方演练日志。
+
+```python
+from mcm26b.protocol import SimulatorClient
+client = SimulatorClient("参赛队号") # 默认 http://127.0.0.1:2026
+client.enter()
+outcome = client.measure((300, 400), 1) # MeasureOutcome(result, svd_deg, ...)
+client.exit()
+```
+
+## 阶段状态(对照 P0–P6 计划)
+
+| 阶段 | 状态 |
+| --- | --- |
+| P0 规则固定(规则表/状态机/字段表,示例逐步 105/111/194/199) | ✅ 完成 |
+| P1 协议基线(客户端 + 自建回放/故障注入测试) | ✅ 完成 |
+| P2 几何内核(角域交、直径、MEC、证书) | ✅ 完成(迁入 `mcm26b.geometry`) |
+| P3 问题二选点(K1、候选区域 Q、采样评估) | 🦴 骨架(证书已完工) |
+| P4 问题3全向闭环(49点覆盖、定位队列、清除、停止) | 🦴 骨架 |
+| P5 问题4定向扩展(联合约束、后备清除、预算) | 🦴 骨架 |
+| P6 加速与消融 | 🦴 未开始 |
+
+## 采集与正式测试约束
+
+本项目只使用官方**演练**采集观测,从未使用正式测试。`scripts/capture_practice.py`
+不会启动测试:必须先在界面确认“问题3演练”、接口已就绪,再提供
+`--practice-confirmed` 与本局案例编码。正式测试由团队按计划另行安排;
+加密日志上传上限 2MB,程序避免异常高频循环。
diff --git a/docs/协议规则表与状态机.md b/docs/协议规则表与状态机.md
new file mode 100644
index 0000000..68cbbb2
--- /dev/null
+++ b/docs/协议规则表与状态机.md
@@ -0,0 +1,123 @@
+# 协议规则表、字段表与状态机(P0 交付)
+
+依据:`issue/附件/附件1.docx`《模拟器使用说明》、`issue/附件/附件2.docx`《模拟器通信接口说明及编程指南》。
+本文是人类可读版本;**可执行版本**是 `src/mcm26b/protocol/rules.py`(字段表与错误语义)、
+`src/mcm26b/protocol/timing.py`(计时模型)与 `src/mcm26b/config.py`(全部常量),
+三者由 `tests/test_protocol_timing.py`、`tests/test_protocol_client.py` 固化验收。
+
+## 1. 动作与耗时规则表
+
+| 动作 | 指令 | 虚拟耗时 | 说明 |
+| --- | --- | --- | --- |
+| 开始 | `POST /enter` | 0 | 初始位置 (0,0),初始频道 1;虚拟/程序时钟开始 |
+| 检测 | `POST /measure` | 5 + 移动 + 换频道 | 无论结果(no_signal/near/direction)均为 5s |
+| 清除 | `POST /clear` | 未发现 3 / 成功 5 + 移动 | 5 = 光学定位 3 + 激光清除 2;**不切换测向频道** |
+| 结束 | `POST /exit` | 0 | 成功响应中 exit_reason 固定 `user_exit` |
+| 切换频道 | 无独立指令 | 1 | 只由合法 `/measure` 的 channel 变化推断 |
+| 移动 | 无独立指令 | 直线距离/5(m/s) | 由 `/measure` 或 `/clear` 的 position 推断 |
+
+派生事实(易错点):
+- 一次合法 `/measure` 完成后,测向机频道更新为本次检测频道;
+- `/clear` 的 channel 只是目标频道,既不切频道、也不产生切换耗时,因此
+ "清除后原地再测"的耗时只取决于上一次 **/measure** 的频道;
+- 只有 accepted=true 的请求推进虚拟时钟;`accepted=false` 的 virtual_time_s=0 不是当前时刻;
+- 5 秒检测只是虚拟耗时,不要求现实等待。
+
+## 2. 字段表
+
+公共请求字段(4 条指令都必填):
+
+| 字段 | 类型 | 约束 |
+| --- | --- | --- |
+| arena_id | string | 必须为 `"default"` |
+| robot_id | string | 逐字节等于登录参赛队号;UTF-8 长度 1..64;无控制字符 |
+| request_id | string | 会话内幂等键;UTF-8 长度 1..128;无控制字符 |
+
+动作字段(实现见 `rules.ACTION_FIELDS`):
+
+| 指令 | 字段 | 约束 |
+| --- | --- | --- |
+| /enter | (无附加) | |
+| /measure | position{x,y}, channel | x/y 有限数值且 \|v\|<=2,000,000;channel 整数 1..20(1.0 亦接受,1.5 拒绝) |
+| /clear | position{x,y}, channel | 同上;channel 是目标频道 |
+| /exit | (无附加) | |
+
+响应字段:
+
+| 场景 | 字段 |
+| --- | --- |
+| 所有响应 | accepted, real_timestamp_ms, virtual_time_s |
+| accepted=false | 仅上述 3 个字段(virtual_time_s=0) |
+| /enter 成功 | + max_virtual_duration_s, max_real_duration_s, remaining_real_duration_s |
+| /measure 成功 | + measure_result ∈ {no_signal, near, direction};direction 时另有 svd_deg(保留两位小数) |
+| /clear 成功 | + clear_result ∈ {success, no_target_in_range} |
+| /exit 成功 | + exit_reason = "user_exit" |
+
+错误语义(必须同时检查 HTTP 状态与 accepted):
+
+| 情形 | 结果 | 对 request_id 的影响 |
+| --- | --- | --- |
+| JSON 语法/重复键/缺字段/类型/坐标频道范围 | HTTP 400 | 不占用,修正后可复用 |
+| 未知字段(含 position 下)、arena/robot 不匹配、测试状态拒绝 | HTTP 200, accepted=false | 不占用 |
+| 动作被接受 | HTTP 200, accepted=true | 占用 |
+| 同 ID 不同内容,或并发不同动作 | HTTP 409 | — |
+| 网络失败重试同一动作 | 复用原 ID+原内容,服务器回放首次响应 | 不重复生效 |
+| 429/500 | 可原样重试 | — |
+| 404/405/413/415 | 确定性错误,不得重试 | — |
+
+物理规则速查:Ω 半径 1800m;源数 10–16;频道 1–20;接收半径 [1000,1500]m(逐源未知);
+near 阈值 5m;清除半径 20m(与定向朝向无关);定向覆盖 = 朝向两侧各 90°(含边界);
+示向度误差幅度 ±1°,保留两位小数;虚拟限时 360000s;程序限时 1200s(以 /enter 的
+remaining_real_duration_s 为准,不得假定恒为 1200);测试窗口 25 分钟。
+
+## 3. 状态机
+
+机器狗会话状态(`protocol.state.RobotState` + 客户端会话标志):
+
+```
+ /enter accepted /exit accepted
+ [未进入] ───────────────▶ [已进入] ───────────────▶ [已退出]
+ │ accepted=false │ accepted=false │ 任何动作 accepted=false
+ └── 不变 └── 不变 └── 不变
+```
+
+[已进入] 状态内跟踪:position、测向机 channel、virtual_time_s。转移规则:
+
+| 事件 | position | channel | virtual_time_s |
+| --- | --- | --- | --- |
+| /enter accepted | (0,0) | 1 | 0 |
+| /measure accepted | ← 本次 position | ← 本次 channel | ← 响应值(=旧值+移动+切换+5) |
+| /clear accepted | ← 本次 position | **不变** | ← 响应值(=旧值+移动+3或5) |
+| /exit accepted | 不变 | 不变 | ← 响应值 |
+| accepted=false / 网络 / 400 | 不变 | 不变 | 不变 |
+
+虚拟时刻以最近一次 accepted=true 的响应为准;本地预测(timing 模块)只用于
+规划与一致性核对,偏差记入 `RobotState.mismatches` 并写日志(strict 模式抛错)。
+
+## 4. 计时示例逐步推演(附件1表2 = 附件2第10节)
+
+| 步 | 指令 | 移动 | 换频道 | 动作 | 本步 | 累计 |
+| --- | --- | --- | --- | --- | --- | --- |
+| 1 | /enter | 0 | 0 | 0 | 0 | **0** |
+| 2 | /measure (300,400) ch1 | 500m→100s | 0(初始即1) | 5 | 105 | **105** |
+| 3 | /measure (300,400) ch2 | 0 | 1 | 5 | 6 | **111** |
+| 4 | /clear (300,0) 目标ch3 | 400m→80s | 0(clear不切频道) | 3(未发现) | 83 | **194** |
+| 5 | /measure (300,0) ch2 | 0 | 0(第3步后已是2,clear未改) | 5 | 5 | **199** |
+| 6 | /exit | 0 | 0 | 0 | 0 | **199** |
+
+验收:`uv run mcm26b sim-check` 通过完整客户端→本地模拟器链路复现
+`[105, 111, 194, 199]`;`tests/test_protocol_timing.py::TimingModelTests`
+逐步断言每个分项。
+
+## 5. 客户端设计要点(P1 交付)
+
+- **串行**:完全同步,一个动作未决不发下一个;
+- **幂等重试**:请求体一次性序列化为 bytes,网络失败/429/500 时原样重发
+ (同 request_id 同内容),最多 `max_attempts` 次,指数退避;400/404/405/409/413/415 立即失败;
+- **accepted=false**:抛 `ActionRejected`,状态不更新;virtual_time_s=0 不入时钟;
+- **现实预算**:/enter 成功后 `deadline = monotonic() + remaining_real_duration_s`;
+ 新动作前检查 `剩余 > reserve_exit_s`(默认 60s [工程]),/exit 豁免;
+- **日志**:`SessionLog` 逐事件追加 JSONL(attempt/transport_error/accepted),
+ 进程崩溃不丢已发记录,回放测试与 evaluation 复算共用同一格式;
+- **本地预校验**:channel 范围、坐标有限性与 2e6 上限、robot_id/request_id 格式
+ 在发送前拒绝(与服务器 400 语义一致,省一次往返)。
diff --git a/docs/问题一解答.md b/docs/问题一解答.md
index c875ead..69498ce 100644
--- a/docs/问题一解答.md
+++ b/docs/问题一解答.md
@@ -182,15 +182,15 @@ $$
## 八、运行方式和代码入口
-核心算法仅用Python标准库,支持Python3.10及以上;本次在Python3.12.14中验证。在项目根目录运行:
+核心算法仅用Python标准库,支持Python3.10及以上;工程依赖仅 `requests`(测试另需 `pytest`)。在项目根目录运行:
```powershell
-python -m geometry examples/q1/triangle.json -o output/q1/triangle_recomputed.json
-python main.py examples/q1/normal.json
-python -m geometry examples/q1/normal.json --rounding-margin-deg 0.005
-python -m unittest discover -s tests -v
-python scripts/build_q1_demo.py
-python scripts/benchmark_q1.py
+uv run python -m mcm26b.geometry examples/q1/triangle.json -o output/q1/triangle_recomputed.json
+uv run mcm26b q1 examples/q1/normal.json
+uv run python -m mcm26b.geometry examples/q1/normal.json --rounding-margin-deg 0.005
+uv run pytest
+uv run python scripts/build_q1_demo.py
+uv run python scripts/benchmark_q1.py
```
输入JSON的最小格式:
@@ -209,22 +209,22 @@ python scripts/benchmark_q1.py
| 文件 | 职责 |
| --- | --- |
-| `geometry/localization.py` | 测向角域、半平面、区域分类、直径和包围圆 |
-| `geometry/planning.py` | 问题二首次测向的距离保证候选圆盘;独立于问题一模型 |
-| `geometry/__main__.py`、`main.py` | JSON命令行输入输出 |
+| `src/mcm26b/geometry/localization.py` | 测向角域、半平面、区域分类、直径和包围圆 |
+| `src/mcm26b/geometry/planning.py` | 问题二首次测向的距离保证候选圆盘;独立于问题一模型 |
+| `src/mcm26b/geometry/__main__.py`、`src/mcm26b/cli.py` | JSON命令行输入输出 |
| `tests/test_geometry.py` | 解析反例、随机几何、独立裁剪对照与退化测试 |
| `tests/test_practice_replay.py` | 离线复算演练计时与观测结果,不连接模拟器 |
| `tests/test_geometry_certificates.py` | 增量圆与穷举法对照、覆盖证据、清除阈值、舍入选项及首次选点证书 |
| `scripts/benchmark_q1.py` | 局部性能对比与官方演练误差界敏感性复算 |
| `scripts/capture_practice.py` | 在已经启动且已核验的演练中采集少量测向 |
| `scripts/build_q1_demo.py` | 用Python算法生成案例、结果和交互图数据 |
-| `visualization/q1.template.html` | 可视化源模板;不另写一套求解算法 |
+| `scripts/assets/q1.template.html` | 可视化源模板;不另写一套求解算法 |
| `output/q1/question1.html` | 可直接打开的离线交互式模拟,所有数据已内嵌 |
如需再次采集,先在模拟器明确启动“问题3演练测试”,等待接口就绪,读取案例编码,然后运行:
```powershell
-python scripts/capture_practice.py --robot-id 你的参赛队号 --practice-confirmed --case-code 界面中的演练案例编码
+uv run python scripts/capture_practice.py --robot-id 你的参赛队号 --practice-confirmed --case-code 界面中的演练案例编码
```
接口协议没有返回演练/正式模式的字段,`--practice-confirmed` 是操作者确认已核验界面的显式参数,不能替代界面核查。采集脚本没有启动测试的功能,没有正式模式选项,只会调用 `/enter`、`/measure`、`/exit`。禁止在正式测试会话中运行。
diff --git a/main.py b/main.py
index 1e333a1..d330c56 100644
--- a/main.py
+++ b/main.py
@@ -1,5 +1,5 @@
-from geometry.__main__ import main
-
+"""兼容入口,转发到统一 CLI(mcm26b)。"""
+from mcm26b.cli import main
if __name__ == "__main__":
- main()
+ raise SystemExit(main())
diff --git a/pyproject.toml b/pyproject.toml
index 8cb01ac..12d3ce7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,7 +1,27 @@
[project]
-name = "math"
-version = "0.1.0"
-description = "B题问题一:带误差方位交会定位与可视化"
+name = "mcm26b"
+version = "0.2.0"
+description = "CUMCM 2026 B题:无线电干扰源的快速自动定位与清除(协议基线、几何内核与策略骨架)"
readme = "README.md"
requires-python = ">=3.10"
-dependencies = []
+dependencies = [
+ "requests>=2.31",
+]
+
+[dependency-groups]
+dev = [
+ "pytest>=8",
+]
+
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[tool.hatch.build.targets.wheel]
+packages = ["src/mcm26b"]
+
+[project.scripts]
+mcm26b = "mcm26b.cli:main"
+
+[tool.pytest.ini_options]
+testpaths = ["tests"]
diff --git a/src/visualization/q1.template.html b/scripts/assets/q1.template.html
similarity index 100%
rename from src/visualization/q1.template.html
rename to scripts/assets/q1.template.html
diff --git a/src/visualization/standalone.css b/scripts/assets/standalone.css
similarity index 100%
rename from src/visualization/standalone.css
rename to scripts/assets/standalone.css
diff --git a/src/scripts/benchmark_q1.py b/scripts/benchmark_q1.py
similarity index 92%
rename from src/scripts/benchmark_q1.py
rename to scripts/benchmark_q1.py
index a65f56a..12d969f 100644
--- a/src/scripts/benchmark_q1.py
+++ b/scripts/benchmark_q1.py
@@ -8,10 +8,11 @@ from statistics import median
import sys
from time import perf_counter
+from mcm26b.geometry import (Measurement, solve, smallest_enclosing_circle,
+ first_bearing_candidate_disk)
+from mcm26b.geometry.localization import _enclosing_circle_reference
+
ROOT = Path(__file__).resolve().parents[1]
-sys.path.insert(0, str(ROOT))
-from geometry import Measurement, solve, smallest_enclosing_circle, first_bearing_candidate_disk
-from geometry.localization import _enclosing_circle_reference
def timed(function, points, repeats):
diff --git a/src/scripts/build_q1_demo.py b/scripts/build_q1_demo.py
similarity index 90%
rename from src/scripts/build_q1_demo.py
rename to scripts/build_q1_demo.py
index dcb6a8d..ed0b416 100644
--- a/src/scripts/build_q1_demo.py
+++ b/scripts/build_q1_demo.py
@@ -4,11 +4,11 @@ from dataclasses import asdict
import json
import math
from pathlib import Path
-import sys
-ROOT=Path(__file__).resolve().parents[1]
-sys.path.insert(0,str(ROOT))
-from geometry import Measurement, solve
+from mcm26b import Measurement, solve
+
+ROOT = Path(__file__).resolve().parents[1]
+ASSETS = Path(__file__).resolve().parent / "assets"
def measured(stations, target, errors):
@@ -56,11 +56,11 @@ def main():
(examples/(case['id']+'.json')).write_text(json.dumps({k:v for k,v in data.items() if k!='frames'},ensure_ascii=False,indent=2),encoding='utf-8')
(results/(case['id']+'_result.json')).write_text(json.dumps(frames[-1],ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
(results/'demo_data.json').write_text(json.dumps(dataset,ensure_ascii=False,indent=2,allow_nan=False),encoding='utf-8')
- template=(ROOT/'visualization/q1.template.html').read_text(encoding='utf-8')
+ template=(ASSETS / 'q1.template.html').read_text(encoding='utf-8')
fragment=template.replace('/*__Q1_DATA__*/',json.dumps(dataset,ensure_ascii=False,allow_nan=False))
- (ROOT/'visualization/question1.html').write_text(fragment,encoding='utf-8')
+ (ROOT/'output'/'q1'/'question1_full.html').write_text(fragment,encoding='utf-8')
# A standalone companion uses the same fragment and computation results.
- theme=(ROOT/'visualization/standalone.css').read_text(encoding='utf-8')
+ theme=(ASSETS / 'standalone.css').read_text(encoding='utf-8')
page='
B题问题一交会定位'+fragment+''
(results/'question1.html').write_text(page,encoding='utf-8')
print(json.dumps({'cases':len(dataset),'fragment_bytes':len(fragment.encode('utf-8')),
diff --git a/src/scripts/capture_practice.py b/scripts/capture_practice.py
similarity index 97%
rename from src/scripts/capture_practice.py
rename to scripts/capture_practice.py
index a9fbc88..0bb6c0e 100644
--- a/src/scripts/capture_practice.py
+++ b/scripts/capture_practice.py
@@ -8,14 +8,12 @@ import argparse
import json
import math
from pathlib import Path
-import sys
import time
import uuid
from urllib.error import URLError
from urllib.request import Request, urlopen
-sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
-from geometry import Measurement, solve
+from mcm26b import Measurement, solve
class PracticeClient:
diff --git a/src/scripts/render_q1_figures.py b/scripts/render_q1_figures.py
similarity index 100%
rename from src/scripts/render_q1_figures.py
rename to scripts/render_q1_figures.py
diff --git a/src/scripts/verify_visualization.cjs b/scripts/verify_visualization.cjs
similarity index 97%
rename from src/scripts/verify_visualization.cjs
rename to scripts/verify_visualization.cjs
index 8689f40..f1eadc1 100644
--- a/src/scripts/verify_visualization.cjs
+++ b/scripts/verify_visualization.cjs
@@ -4,7 +4,7 @@
*/
const fs=require('node:fs'),path=require('node:path'),vm=require('node:vm'),assert=require('node:assert');
const rootDir=path.resolve(__dirname,'..');
-const source=fs.readFileSync(path.join(rootDir,'visualization/question1.html'),'utf8');
+const source=fs.readFileSync(path.join(rootDir,'output/q1/question1_full.html'),'utf8');
const script=source.match(/
-
diff --git a/src/tests/test_geometry.py b/tests/test_geometry.py
similarity index 97%
rename from src/tests/test_geometry.py
rename to tests/test_geometry.py
index c39c08b..9ae70bf 100644
--- a/src/tests/test_geometry.py
+++ b/tests/test_geometry.py
@@ -2,8 +2,8 @@ import math
import random
import unittest
-from geometry import Measurement, solve, smallest_enclosing_circle
-from geometry.localization import halfplanes, distance
+from mcm26b.geometry import Measurement, solve, smallest_enclosing_circle
+from mcm26b.geometry.localization import halfplanes, distance
class GeometryTests(unittest.TestCase):
diff --git a/src/tests/test_geometry_certificates.py b/tests/test_geometry_certificates.py
similarity index 94%
rename from src/tests/test_geometry_certificates.py
rename to tests/test_geometry_certificates.py
index 30b10a4..346024c 100644
--- a/src/tests/test_geometry_certificates.py
+++ b/tests/test_geometry_certificates.py
@@ -6,8 +6,9 @@ import subprocess
import sys
import unittest
-from geometry import Measurement, solve, smallest_enclosing_circle, first_bearing_candidate_disk
-from geometry.localization import _enclosing_circle_reference, distance
+from mcm26b.geometry import (Measurement, solve, smallest_enclosing_circle,
+ first_bearing_candidate_disk)
+from mcm26b.geometry.localization import _enclosing_circle_reference, distance
ROOT = Path(__file__).resolve().parents[1]
@@ -93,7 +94,7 @@ class CertificateTests(unittest.TestCase):
first_bearing_candidate_disk(Measurement(0, 0, 0, 46))
def test_cli_explicit_rounding_margin(self):
- base = [sys.executable, '-m', 'geometry', 'examples/q1/triangle.json']
+ base = [sys.executable, '-m', 'mcm26b.geometry', 'examples/q1/triangle.json']
outputs = [json.loads(subprocess.check_output(base+args, cwd=ROOT, encoding='utf-8'))
for args in [[], ['--rounding-margin-deg', '0.005']]]
self.assertEqual(outputs[0]['error_policy']['effective_half_angles_deg'], [1, 1, 1])
diff --git a/src/tests/test_practice_replay.py b/tests/test_practice_replay.py
similarity index 97%
rename from src/tests/test_practice_replay.py
rename to tests/test_practice_replay.py
index 95e984a..e17a260 100644
--- a/src/tests/test_practice_replay.py
+++ b/tests/test_practice_replay.py
@@ -4,7 +4,7 @@ import json
import math
from pathlib import Path
import unittest
-from geometry import Measurement, solve
+from mcm26b.geometry import Measurement, solve
ROOT=Path(__file__).resolve().parents[1]
diff --git a/tests/test_protocol_client.py b/tests/test_protocol_client.py
new file mode 100644
index 0000000..90e4a91
--- /dev/null
+++ b/tests/test_protocol_client.py
@@ -0,0 +1,253 @@
+"""P1 验收测试:幂等重试、故障恢复、现实预算与真实演练日志回放。
+
+对应验收标准(docs/B题建模分析与实施计划.md P1 行):
+- accepted=false 和超时重试不污染状态;
+- 失败重试复用原 request_id 与原内容,服务器端动作恰好生效一次;
+- 用 /enter 的 remaining_real_duration_s 建立本地截止时钟;
+- 官方演练日志可完整回放,本地重建状态与服务器记录逐步吻合。
+"""
+
+import json
+import math
+import tempfile
+import unittest
+from dataclasses import replace
+from pathlib import Path
+
+from mcm26b.config import ProblemConfig
+from mcm26b.evaluation import recompute_virtual_time, summarize
+from mcm26b.protocol.client import (DeadlineExceeded, ProtocolError,
+ SimulatorClient)
+from mcm26b.protocol.log import SessionLog
+from mcm26b.protocol.transport import TransportError
+from mcm26b.sim import (LocalSimTransport, LocalSimulator,
+ PlaybackTransport, ScriptedFaultTransport, Source)
+
+ROOT = Path(__file__).resolve().parents[1]
+
+
+class FaultRecoveryTests(unittest.TestCase):
+ """故障注入:失败重试不污染状态、不重复生效。"""
+
+ def _client(self, script, sources=None):
+ simulator = LocalSimulator(sources or [], robot_id="T001")
+ inner = LocalSimTransport(simulator)
+ faults = ScriptedFaultTransport(inner, script)
+ client = SimulatorClient("T001", transport=faults)
+ return client, faults, simulator
+
+ def test_timeout_after_apply_replays_cached_response(self):
+ """读超时(服务器已生效)后重试:同一响应回放,动作不二次生效。"""
+ # 脚本按请求顺序消耗:enter 放行,measure 第一次注入"已生效但超时"。
+ client, faults, simulator = self._client([None, "timeout_after", None, None])
+ client.enter()
+ outcome = client.measure((0, 0), 1)
+ self.assertEqual(outcome.virtual_time_s, 5.0)
+ # measure 只被服务器执行了一次:请求发出两次,但虚拟时刻只推进一次。
+ measure_bodies = [body for body in faults.sent_requests[1:]]
+ self.assertEqual(len(measure_bodies), 2, "应发出原始请求+一次重试")
+ first, retry = (json.loads(b) for b in measure_bodies)
+ self.assertEqual(first["request_id"], retry["request_id"],
+ "重试必须复用原 request_id")
+ self.assertEqual(first, retry, "重试请求体必须逐字段一致")
+ self.assertEqual(simulator.virtual_time_s, 5.0, "动作只生效一次")
+ self.assertEqual(client.state.virtual_time_s, 5.0)
+
+ def test_drop_before_apply_is_safe_to_retry(self):
+ """连接失败(未送达)后重试成功,无残留效果。"""
+ client, faults, simulator = self._client(["drop_before", None])
+ client.enter()
+ client.measure((100, 0), 1)
+ self.assertEqual(simulator.virtual_time_s, 25.0) # 100/5 移动 + 5 检测
+ self.assertEqual(client.state.virtual_time_s, 25.0)
+
+ def test_server_500_retried_with_same_id(self):
+ client, faults, simulator = self._client([None, ("status", 500), None, None])
+ client.enter()
+ client.measure((0, 0), 1)
+ self.assertEqual(client.state.virtual_time_s, 5.0)
+
+ def test_exhausted_retries_raise_and_leave_state_clean(self):
+ # enter 放行后,measure 连续注入断连直至超过 max_attempts=3。
+ client, faults, simulator = self._client([None, "drop_before", "drop_before",
+ "drop_before"])
+ client.enter()
+ with self.assertRaises(TransportError):
+ client.measure((0, 0), 1)
+ self.assertEqual(client.state.virtual_time_s, 0.0,
+ "失败动作不得污染本地状态")
+ self.assertTrue(simulator.entered) # enter 已成功,会话仍然有效
+ # 后续新动作用新 request_id 正常执行(脚本已耗尽,全部放行)。
+ outcome = client.measure((0, 0), 1)
+ self.assertEqual(outcome.virtual_time_s, 5.0)
+
+ def test_accepted_false_raises_and_state_untouched(self):
+ client, faults, simulator = self._client([None])
+ with self.assertRaises(ProtocolError) as ctx:
+ client.measure((0, 0), 1) # 未 enter -> accepted=false
+ self.assertEqual(ctx.exception.status, 200)
+ self.assertEqual(client.state.virtual_time_s, 0.0)
+
+
+class DeadlineBudgetTests(unittest.TestCase):
+ """现实预算:本地单调时钟、预留余量、/exit 豁免。"""
+
+ def test_deadline_from_enter_remaining(self):
+ now = [1000.0]
+ simulator = LocalSimulator([], robot_id="T001")
+ client = SimulatorClient("T001", transport=LocalSimTransport(simulator),
+ clock=lambda: now[0])
+ client.enter()
+ self.assertAlmostEqual(client.real_remaining_s, 1200.0)
+ now[0] += 300.0
+ self.assertAlmostEqual(client.real_remaining_s, 900.0)
+
+ def test_action_blocked_when_reserve_margin_cannot_be_kept(self):
+ now = [0.0]
+ simulator = LocalSimulator([], robot_id="T001")
+ client = SimulatorClient("T001", transport=LocalSimTransport(simulator),
+ clock=lambda: now[0])
+ client.enter() # remaining = 1200s
+ now[0] += 1145.0 # 只剩 55s < 60s 预留
+ with self.assertRaises(DeadlineExceeded):
+ client.measure((0, 0), 1)
+ info = client.exit() # /exit 不受预算拦截
+ self.assertEqual(info.reason, "user_exit")
+
+ def test_custom_reserve_margin(self):
+ now = [0.0]
+ simulator = LocalSimulator([], robot_id="T001")
+ config = replace(ProblemConfig(),
+ session=replace(ProblemConfig().session,
+ reserve_exit_s=10.0))
+ client = SimulatorClient("T001", config=config,
+ transport=LocalSimTransport(simulator),
+ clock=lambda: now[0])
+ client.enter()
+ now[0] += 1195.0 # 剩 5s < 新预留 10s
+ with self.assertRaises(DeadlineExceeded):
+ client.measure((0, 0), 1)
+
+
+class PracticeReplayTests(unittest.TestCase):
+ """官方演练日志经 PlaybackTransport 驱动完整客户端。"""
+
+ def _events(self):
+ files = sorted((ROOT / "output" / "q1" / "practice").glob("*_actions.json"))
+ self.assertTrue(files, "缺少演练采集证据")
+ events = []
+ for path in files:
+ events.extend(json.loads(path.read_text(encoding="utf-8")))
+ return events
+
+ def test_client_state_matches_recorded_server_clock(self):
+ events = self._events()
+ client = SimulatorClient("任意队号", transport=PlaybackTransport(events))
+ for event in events:
+ path, request = event["path"], event["request"]
+ if path == "/enter":
+ client.enter()
+ elif path == "/measure":
+ client.measure((request["position"]["x"], request["position"]["y"]),
+ request["channel"])
+ elif path == "/exit":
+ client.exit()
+ self.assertAlmostEqual(
+ client.state.virtual_time_s,
+ event["response"]["virtual_time_s"], places=5,
+ msg=f"{path} 后本地虚拟时刻应与服务器记录一致")
+ self.assertTrue(client.state.exited)
+
+ def test_evaluation_recompute_agrees_with_recording(self):
+ events = [dict(path=e["path"], request=e["request"], response=e["response"])
+ for e in self._events()
+ if e["response"].get("accepted") is True]
+ report = recompute_virtual_time(events)
+ self.assertEqual(report["mismatches"], [])
+ self.assertAlmostEqual(report["final_virtual_time_s"],
+ events[-1]["response"]["virtual_time_s"], places=5)
+
+ def test_summarize_counts(self):
+ events = [dict(path=e["path"], request=e["request"], response=e["response"])
+ for e in self._events()]
+ stats = summarize(events)
+ directions = [e for e in events
+ if e["response"].get("measure_result") == "direction"]
+ self.assertGreater(stats["measures"], 0)
+ self.assertEqual(stats["clears"], 0, "该次演练只采集观测,未调用 /clear")
+
+
+class LocalSimObservationTests(unittest.TestCase):
+ """观测语义:no_signal / near / direction 与定向覆盖角。"""
+
+ def test_distance_and_coverage_gate(self):
+ source = Source(channel=1, x=500, y=0, r_eff_m=1000)
+ simulator = LocalSimulator([source], robot_id="T001")
+ client = SimulatorClient("T001", transport=LocalSimTransport(simulator))
+ client.enter()
+ self.assertEqual(client.measure((1505, 0), 1).result, "no_signal") # 距源1005>1000
+ self.assertEqual(client.measure((500, 998), 1).result, "direction") # 1000内
+ self.assertEqual(client.measure((500, 0), 1).result, "near") # <=5m
+
+ def test_directional_source_backside_invisible(self):
+ source = Source(channel=1, x=500, y=0, r_eff_m=1500,
+ directional=True, heading_deg=90.0) # 朝北
+ simulator = LocalSimulator([source], robot_id="T001")
+ client = SimulatorClient("T001", transport=LocalSimTransport(simulator))
+ client.enter()
+ self.assertEqual(client.measure((500, 600), 1).result, "direction") # 正北
+ self.assertEqual(client.measure((500, -300), 1).result, "no_signal") # 正南背面
+ self.assertEqual(client.measure((500, 998), 1).result, "direction") # 边界含90度
+
+ def test_clear_ignores_directional_facing(self):
+ """清除只看距离:定向源背面同样可清除 [题面]。"""
+ source = Source(channel=7, x=0, y=0, r_eff_m=1500,
+ directional=True, heading_deg=180.0)
+ simulator = LocalSimulator([source], robot_id="T001")
+ client = SimulatorClient("T001", transport=LocalSimTransport(simulator))
+ client.enter()
+ self.assertEqual(client.state.channel, 1)
+ outcome = client.clear((15, 0), 7) # 距离15m<=20,位于背面
+ self.assertEqual(outcome.result, "success")
+ outcome = client.clear((0, 0), 7) # 同源只能清除一次
+ self.assertEqual(outcome.result, "no_target_in_range")
+ # 两次 /clear 后测向机频道仍是初始 1:/clear 不切换频道 [题面]。
+ self.assertEqual(client.state.channel, 1)
+ # 清除后同频道无信号;/measure 会把频道更新为 7。
+ self.assertEqual(client.measure((100, 0), 7).result, "no_signal")
+ self.assertEqual(client.state.channel, 7)
+
+ def test_bearing_error_within_bound(self):
+ source = Source(channel=1, x=1000, y=300, r_eff_m=1500)
+ simulator = LocalSimulator([source], error_half_deg=1.0, robot_id="T001")
+ client = SimulatorClient("T001", transport=LocalSimTransport(simulator))
+ client.enter()
+ outcome = client.measure((0, 0), 1)
+ true_bearing = math.degrees(math.atan2(300, 1000))
+ # 误差在 ±1° 内均匀分布,返回值再保留两位小数。
+ self.assertLessEqual(abs(outcome.svd_deg - true_bearing), 1.005)
+
+
+class SessionLogTests(unittest.TestCase):
+ def test_log_records_attempts_and_accepted(self):
+ with tempfile.TemporaryDirectory() as folder:
+ path = Path(folder) / "session.jsonl"
+ simulator = LocalSimulator([], robot_id="T001")
+ faults = ScriptedFaultTransport(LocalSimTransport(simulator),
+ [None, "drop_before", None, None])
+ client = SimulatorClient("T001", transport=faults,
+ log=SessionLog(path=path))
+ client.enter()
+ client.measure((0, 0), 1)
+ client.exit()
+ rows = [json.loads(line) for line in
+ path.read_text(encoding="utf-8").splitlines()]
+ accepted = [row for row in rows if row["event"] == "accepted"]
+ self.assertEqual(len(accepted), 3)
+ self.assertEqual(accepted[1]["attempts"], 2, "重试后成功应记录尝试次数")
+ self.assertEqual(len({row["request_id"] for row in accepted}), 3,
+ "每个被接受动作各占一个 request_id")
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/tests/test_protocol_timing.py b/tests/test_protocol_timing.py
new file mode 100644
index 0000000..2756e39
--- /dev/null
+++ b/tests/test_protocol_timing.py
@@ -0,0 +1,207 @@
+"""P0 验收测试:计时示例、状态机规则与字段表语义。
+
+对应验收标准(docs/B题建模分析与实施计划.md P0 行):
+- 附件计时示例能逐步得到 105、111、194、199 秒;
+- /clear 不切换测向频道;
+- accepted=false 不更新状态,其 virtual_time_s=0 不当作当前虚拟时刻。
+"""
+
+import unittest
+
+from mcm26b.config import CostConfig
+from mcm26b.protocol.rules import CLEAR_NO_TARGET
+from mcm26b.protocol.state import RobotState, StateError
+from mcm26b.protocol.timing import clear_cost, measure_cost
+from mcm26b.sim import LocalSimTransport, LocalSimulator
+from mcm26b.protocol.client import ActionRejected, SimulatorClient
+
+
+class TimingModelTests(unittest.TestCase):
+ """附件1表2 / 附件2第10节示例逐步分解。"""
+
+ def setUp(self):
+ self.cost = CostConfig()
+
+ def test_measure_step2_move_500_no_switch(self):
+ breakdown = measure_cost((0, 0), (300, 400), 1, 1, self.cost)
+ self.assertAlmostEqual(breakdown.move_s, 100) # 500m / 5(m/s)
+ self.assertAlmostEqual(breakdown.switch_s, 0) # 初始频道即1
+ self.assertAlmostEqual(breakdown.action_s, 5)
+ self.assertAlmostEqual(breakdown.total_s, 105)
+
+ def test_measure_step3_in_place_switch_channel(self):
+ breakdown = measure_cost((300, 400), (300, 400), 1, 2, self.cost)
+ self.assertAlmostEqual(breakdown.move_s, 0)
+ self.assertAlmostEqual(breakdown.switch_s, 1)
+ self.assertAlmostEqual(breakdown.total_s, 6)
+
+ def test_clear_step4_move_400_no_target(self):
+ breakdown = clear_cost((300, 400), (300, 0), success=False, cost=self.cost)
+ self.assertAlmostEqual(breakdown.move_s, 80) # 400m / 5
+ self.assertAlmostEqual(breakdown.switch_s, 0) # /clear 不产生换频道耗时
+ self.assertAlmostEqual(breakdown.action_s, 3) # 未发现=光学定位3s
+ self.assertAlmostEqual(breakdown.total_s, 83)
+
+ def test_measure_step5_same_channel_after_clear(self):
+ # 第4步 /clear 后测向机仍是频道2,原地再测频道2无切换耗时。
+ breakdown = measure_cost((300, 0), (300, 0), 2, 2, self.cost)
+ self.assertAlmostEqual(breakdown.total_s, 5)
+
+ def test_virtual_clock_walk(self):
+ """累积虚拟时刻逐步得到附件给定值 0/105/111/194/199/199。"""
+ virtual = 0.0
+ expected = [0, 105, 111, 194, 199, 199]
+ sequence = [
+ measure_cost((0, 0), (300, 400), 1, 1, self.cost),
+ measure_cost((300, 400), (300, 400), 1, 2, self.cost),
+ clear_cost((300, 400), (300, 0), False, self.cost),
+ measure_cost((300, 0), (300, 0), 2, 2, self.cost),
+ None, # /exit
+ ]
+ results = [virtual]
+ for breakdown in sequence:
+ virtual += 0 if breakdown is None else breakdown.total_s
+ results.append(round(virtual, 6))
+ self.assertEqual(results, [float(v) for v in expected])
+
+
+class StateMachineTests(unittest.TestCase):
+ """状态更新规则:accepted=false 不更新;/clear 不动频道。"""
+
+ @staticmethod
+ def _accepted_measure(virtual):
+ return {"accepted": True, "real_timestamp_ms": 0,
+ "virtual_time_s": virtual,
+ "measure_result": "no_signal"}
+
+ def test_clear_keeps_channel(self):
+ state = RobotState()
+ state.entered = True
+ measure_req = {"position": {"x": 300, "y": 400}, "channel": 2}
+ state.apply_measure(measure_req, self._accepted_measure(106))
+ self.assertEqual(state.channel, 2)
+ clear_req = {"position": {"x": 300, "y": 0}, "channel": 3}
+ response = {"accepted": True, "real_timestamp_ms": 0,
+ "virtual_time_s": 189,
+ "clear_result": CLEAR_NO_TARGET}
+ state.apply_clear(clear_req, response)
+ self.assertEqual(state.channel, 2, "/clear 不得改变测向机频道")
+
+ def test_rejected_response_never_updates(self):
+ state = RobotState(position=(10, 20), channel=5, virtual_time_s=77)
+ rejected = {"accepted": False, "real_timestamp_ms": 1, "virtual_time_s": 0}
+ request = {"position": {"x": 0, "y": 0}, "channel": 1}
+ with self.assertRaises(StateError):
+ state.apply_measure(request, rejected)
+ with self.assertRaises(StateError):
+ state.apply_clear(request, rejected)
+ with self.assertRaises(StateError):
+ state.apply_enter(rejected)
+ self.assertEqual((state.position, state.channel, state.virtual_time_s),
+ ((10, 20), 5, 77), "accepted=false 不得更新任何状态")
+
+ def test_virtual_time_zero_of_rejection_is_not_a_clock(self):
+ """accepted=false 的 virtual_time_s=0 不是当前时刻(回归附件2 4.1)。"""
+ state = RobotState(virtual_time_s=300)
+ rejected = {"accepted": False, "real_timestamp_ms": 9, "virtual_time_s": 0}
+ with self.assertRaises(StateError):
+ state.apply_measure({"position": {"x": 0, "y": 0}, "channel": 1}, rejected)
+ self.assertEqual(state.virtual_time_s, 300)
+
+ def test_strict_mode_flags_prediction_gap(self):
+ state = RobotState()
+ state.entered = True
+ request = {"position": {"x": 100, "y": 0}, "channel": 1}
+ bad = {"accepted": True, "real_timestamp_ms": 0, "virtual_time_s": 3,
+ "measure_result": "no_signal"} # 应为 25s,伪造 3s
+ with self.assertRaises(StateError):
+ state.apply_measure(request, bad, strict=True)
+
+
+class FieldTableSimTests(unittest.TestCase):
+ """字段表与错误语义(附件2 5.1/5.3/6.3/7.4)。"""
+
+ def setUp(self):
+ self.sim = LocalSimulator([], robot_id="T001")
+ self.base = {"arena_id": "default", "robot_id": "T001"}
+
+ def _measure(self, **override):
+ payload = {**self.base, "request_id": "m1",
+ "position": {"x": 0, "y": 0}, "channel": 1, **override}
+ return self.sim.handle("/measure", payload)
+
+ def test_measure_before_enter_rejected(self):
+ status, body = self._measure()
+ self.assertEqual((status, body["accepted"]), (200, False))
+ self.assertEqual(body["virtual_time_s"], 0)
+ self.assertEqual(set(body) >= {"accepted", "real_timestamp_ms", "virtual_time_s"}, True)
+
+ def test_unknown_field_rejected_but_id_not_consumed(self):
+ self.sim.handle("/enter", {**self.base, "request_id": "e1"})
+ status, body = self._measure(extra_field=1) # 未声明字段
+ self.assertEqual((status, body["accepted"]), (200, False))
+ status, body = self._measure() # 同 ID 修正后可复用
+ self.assertEqual((status, body["accepted"]), (200, True))
+
+ def test_wrong_robot_id_rejected(self):
+ status, body = self.sim.handle("/enter", {**self.base, "robot_id": "OTHER",
+ "request_id": "e1"})
+ self.assertEqual((status, body["accepted"]), (200, False))
+
+ def test_channel_and_coordinate_validation_400(self):
+ self.sim.handle("/enter", {**self.base, "request_id": "e1"})
+ for channel in (0, 21, 1.5):
+ status, _ = self._measure(request_id=f"c{channel}", channel=channel)
+ self.assertEqual(status, 400)
+ status, _ = self._measure(request_id="far",
+ position={"x": 2_000_001, "y": 0})
+ self.assertEqual(status, 400)
+
+ def test_position_unknown_field_rejected(self):
+ self.sim.handle("/enter", {**self.base, "request_id": "e1"})
+ status, body = self._measure(request_id="p1",
+ position={"x": 0, "y": 0, "z": 0})
+ self.assertEqual((status, body["accepted"]), (200, False))
+
+ def test_double_enter_rejected_then_measure_ok(self):
+ status, body = self.sim.handle("/enter", {**self.base, "request_id": "e1"})
+ self.assertEqual(body["accepted"], True)
+ status, body = self.sim.handle("/enter", {**self.base, "request_id": "e2"})
+ self.assertEqual((status, body["accepted"]), (200, False))
+ status, body = self._measure(request_id="m1")
+ self.assertEqual(body["accepted"], True)
+
+
+class WorkedExampleEndToEndTests(unittest.TestCase):
+ """完整链路:客户端 → 本地模拟器复现附件示例。"""
+
+ def test_attachment_example_reaches_199(self):
+ simulator = LocalSimulator([], robot_id="T001")
+ client = SimulatorClient("T001", transport=LocalSimTransport(simulator))
+ client.enter()
+ self.assertTrue(client.state.entered)
+ self.assertEqual((client.state.position, client.state.channel), ((0.0, 0.0), 1))
+ got = [client.measure((300, 400), 1).virtual_time_s,
+ client.measure((300, 400), 2).virtual_time_s,
+ client.clear((300, 0), 3).virtual_time_s,
+ client.measure((300, 0), 2).virtual_time_s]
+ self.assertEqual([round(v, 6) for v in got], [105, 111, 194, 199])
+ self.assertEqual(client.state.channel, 2)
+ info = client.exit()
+ self.assertEqual((info.reason, round(info.virtual_time_s, 6)),
+ ("user_exit", 199))
+ self.assertEqual(client.state.mismatches, [], "预测与服务器计时应完全一致")
+
+ def test_rejected_action_keeps_client_state(self):
+ simulator = LocalSimulator([], robot_id="T001")
+ client = SimulatorClient("T001", transport=LocalSimTransport(simulator))
+ with self.assertRaises(ActionRejected):
+ client.measure((0, 0), 1) # 未 enter
+ self.assertEqual(client.state.virtual_time_s, 0)
+ self.assertFalse(client.state.entered)
+ client.enter()
+ self.assertEqual(client.state.virtual_time_s, 0)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/uv.lock b/uv.lock
index 61eb41d..8c8f9da 100644
--- a/uv.lock
+++ b/uv.lock
@@ -3,6 +3,363 @@ revision = 3
requires-python = ">=3.10"
[[package]]
-name = "math"
-version = "0.1.0"
-source = { virtual = "." }
+name = "certifi"
+version = "2026.7.22"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775" },
+]
+
+[[package]]
+name = "charset-normalizer"
+version = "3.5.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/71/aa/554e2614f38fc34c58ff1d0911ae8535ad2516440d5482d76fe59f1088b0/charset_normalizer-3.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1ee1e296209fdce05b81b663250eefa02213a2da7b41bf26f7829b8ba3545aa" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/03/6d/439231dfc3ccfa6f8c06477b7da2219cbd41a2de3d49084df8ec7b5100f2/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9fbdce1e47394b09bc9f26ab117dfc8d6491977a11d86f592bb42c779db2fda" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/55/53/7d819bd23a00ef45039146fa2cce1daa2f0771e758c5653ee1f6edac91ed/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:00668ebb0609751758682eb0b5857e7c35b9f00e84dfdef062e103244ec94d45" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b2/2c/45847198c16f4b38090cc7423b2b6a9008e438704d8ab413211832498d31/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba2f37ee79e6338845261a3c5b1784e5d1acdff2c0785b284f1b633033d136ab" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/69/2b/d8be3523ddf9f0b0f3e56d1359034aa10653a4d11564c697f802b4775766/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ce854f5f478050ade5a238731c4ca985a7d3b3cb53ff600a9b5c3b689b5f0a7a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/32/cd/4f564b8f132de25db594efc706897069f016790cea63a5669c9df2675f64/charset_normalizer-3.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:96eefc178f8636b9c760c5829345307fd81cfae9ab1e80997dbddeb0f54ee9a3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f5/e3/38b975422534a608f98c360e79c2f07c763d66dd4272300d45fb1fee54b0/charset_normalizer-3.5.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:366ec70f5547c640d3ce1985722490f23faf4eb5216a7eeba78277490e78dacb" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/87/bd/fbc24d825c66f1c74f6ccdea3742c3d8354a4888e86d1315a197fee69061/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:950f23cb393f85543777b0433f082cddd25b51ab398eac7971146495679efe5f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b9/2d/918d0e98a0e679469ed05bb2d90c2088b4d315bb612969d8499f76fb5210/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:c1dcc36dcb96abc02236e182d17e0f71430152a6c2c7447421da2d2dc144edea" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/20/c8/c36f6e0b2dfec351bd38cbc05362697e58bcd073d7dbd95154290c9714ce/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:07ffd07412fc5d5e84cd8952acf9ff7e4ed7a708e69d1bada19d8ba91711353f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ca/7b/311b3e02e8c4092400c449c850a760d8c45d900983c83a70cc07208c551d/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:f5542f9b941279d82d41eb0aa9f98eba36fe4df5c7086c651df7944935b37182" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b9/90/082cc45599c392f28c036a497f49e0634041a785fc3849c80ccf396d096f/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:a545775cfe815855ea32d7c27731d79da358ef2055b4a25830231b1622dd18aa" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/58/ad/b9aecf38d805cbcf84fa94f14c5d972a16561e20296a11dc799a5dcf3763/charset_normalizer-3.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:494b70049a4d69aec6e8137c13af4cf8db8c9f9820a1392ac293b0dd2987a818" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b7/23/b38a20598d5a825f85d9d7636860e56ff0db1479f86497a6e485aa9326f7/charset_normalizer-3.5.1-cp310-cp310-win32.whl", hash = "sha256:94fbf1c0c6cc0d3d5e50f9a9313a8cdca90dd696d34b381cd1704f8c9e939f20" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d2/21/83fffb77864408b8bf0fe1ca603926401d6f8775a8e150b39aacc9958f8a/charset_normalizer-3.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:be47f99644b208bff7766314013f9acf57b056b04191d570d68ad14022cf5b1d" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/86/2e/b93135b5034b1157fb29554b0d06d4844ce62282f0e0a14036f93d7ee2e7/charset_normalizer-3.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:a6d095662e73e74f0a49988e0593373e243e3a52e27bfeea0a859e88acf4a0f5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/6a/b6/034f6802e9c3f6418966cfabb7db8c9252cc2429c5098f41cc43af804149/charset_normalizer-3.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eda059b6bc8bc0812d626fd91a7ce01bf583df0a61296eff390fd94141a34e30" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d5/fa/6a7e2a7c4b5451912b8c417732df79574354443592a88d616de03da66ae5/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa2bb0b37202dca27175591f761108b5d34096ade1191ffe4808bdf6b1571488" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a4/c8/ab42b07cfd82e919f427fcfaa7c41abae8242833ad1aad66d42bae40b669/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b2b1b3fa5670c127b246df1d0c059defd41f689a868a3b9d79df9b1cac42d22" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e7/80/b9348b5d3041209f98b4cdad7655766369233f1d533f4f4f7558e9717bec/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6e5e4d73d588ca5ed09df1b7dcd1b203d1df3c542e3f50d126c947d432b10731" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/82/38/083a24028304bc85bb9e376fed801178423dcbb67495f73b6ea0624e1894/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b54e7e13267d49ffbfe68e25b3cbd774dab38fa37238f71265e91b36146eb21c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/0d/35/731ac04aa0a097fc1c97f0994c375bdb230c6c96619db794208fe664e9ce/charset_normalizer-3.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7b742bf31c88566b4bb6335a7f393bb322e580b6bb98df7bd0c25e6e3519ce8" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f5/28/c2028e7021fb89c6e56868ed0e387b8e9aa811abdd2ab3208d6578d2c930/charset_normalizer-3.5.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ba32c4d2abf1d2fe7cf27d280f4cca5664233b0f885549c7761719eb977f486" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/28/f0/0c0ceec6d98b7daa62e361e418135d59685811d79ba11529aad5cdf15e84/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0722590aabf9dc6a6c0343d523c05458fa2b5047dbe6302fd526bb570600753f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f0/3e/48f4cd187b1c33189d86039e9cbe4f92c05454175504b44ff81806d4d1bf/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:aa1099b956fb795e686d073568f6dc002a0bb89765ea6d5b055dd7d9bf1b116c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/42/85/f9e22af69af67c54cce42be9455d9c81294f918b4ccc454db01f66efcac2/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bd6c173f04743d483881bffa1478d5a4624475b8cd1d2194956a75548e191c18" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/fd/4c/9044135f42127630b6fa742feb51256353f6ab87a78f2fdd1de3de955a7f/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f298e218441525d3794428b4c8b8fb8662c6d3ea79925d4807ee6b9a96a3bca5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ba/ed/1dd7cfebb4e75812934c49ca3b79757d11948053f7937ab7070c151f3c55/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6e2912d4babbc65196ac13c2f53468dc57fb8b9c25ef913e8c59ddf7c6dc0e1b" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/bf/eb/239c84503cc9e3ba6eb34686a24bc66e84f3924efdd7e38e751a19f6bc10/charset_normalizer-3.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d27167433c0d5f18dc850f07d0b3816221984fecdc405d6c157a6f0b8f8e9e6" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/37/ab/4e4510e1e288478e2c8333131d1c1382382ba8cd2165053c79e39d1da961/charset_normalizer-3.5.1-cp311-cp311-win32.whl", hash = "sha256:ac00177c4831ffa650f8609e4bdddd5fe09c03b1c0c47acece7e6ea20421598b" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e3/57/32f0ccea59e8612057c61d6fd22ef2cb63cca93c9fe594094919696ac170/charset_normalizer-3.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:f9b1e28d0e8dbfa858abdba91d6b547beaf2df1a59bec6da6faae7b96a4991a9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/17/d4/b65c433fc521e58b5f54293982a5e51c05cb5f2dd3f1c7a6acb65b75324e/charset_normalizer-3.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:ae31a1a1db2ee6cc2942fccaf695c934bc7f3db9f2133a3fef1f367cf1a4ab10" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+dependencies = [
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598" },
+]
+
+[[package]]
+name = "idna"
+version = "3.19"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12" },
+]
+
+[[package]]
+name = "mcm26b"
+version = "0.2.0"
+source = { editable = "." }
+dependencies = [
+ { name = "requests" },
+]
+
+[package.dev-dependencies]
+dev = [
+ { name = "pytest" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "requests", specifier = ">=2.31" }]
+
+[package.metadata.requires-dev]
+dev = [{ name = "pytest", specifier = ">=8" }]
+
+[[package]]
+name = "packaging"
+version = "26.3"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.21.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.1.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c" },
+]
+
+[[package]]
+name = "requests"
+version = "2.34.2"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+dependencies = [
+ { name = "certifi" },
+ { name = "charset-normalizer" },
+ { name = "idna" },
+ { name = "urllib3" },
+]
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.1"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9" },
+ { url = "https://mirrors.aliyun.com/pypi/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" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897" },
+ { url = "https://mirrors.aliyun.com/pypi/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" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662" },
+ { url = "https://mirrors.aliyun.com/pypi/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" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d" },
+ { url = "https://mirrors.aliyun.com/pypi/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" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396" },
+ { url = "https://mirrors.aliyun.com/pypi/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.16.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8" },
+]
+
+[[package]]
+name = "urllib3"
+version = "2.7.0"
+source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
+sdist = { url = "https://mirrors.aliyun.com/pypi/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c" }
+wheels = [
+ { url = "https://mirrors.aliyun.com/pypi/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897" },
+]