From 83d2e244485d9de6af462434c2737762505ba090 Mon Sep 17 00:00:00 2001 From: Horldsence Date: Thu, 10 Sep 2026 22:38:27 +0800 Subject: [PATCH] refactor(arch): redesign the code structure --- .gitignore | 3 + README.md | 103 ++++- docs/协议规则表与状态机.md | 123 ++++++ docs/问题一解答.md | 24 +- main.py | 6 +- pyproject.toml | 28 +- .../assets}/q1.template.html | 0 .../assets}/standalone.css | 0 {src/scripts => scripts}/benchmark_q1.py | 7 +- {src/scripts => scripts}/build_q1_demo.py | 14 +- {src/scripts => scripts}/capture_practice.py | 4 +- {src/scripts => scripts}/render_q1_figures.py | 0 .../verify_visualization.cjs | 2 +- src/mcm26b/__init__.py | 21 + src/mcm26b/belief/__init__.py | 58 +++ src/mcm26b/cli.py | 78 ++++ src/mcm26b/config.py | 88 +++++ src/mcm26b/coverage/__init__.py | 56 +++ src/mcm26b/evaluation/__init__.py | 81 ++++ {geometry => src/mcm26b/geometry}/__init__.py | 5 +- {geometry => src/mcm26b/geometry}/__main__.py | 6 +- .../mcm26b/geometry}/localization.py | 0 {geometry => src/mcm26b/geometry}/planning.py | 0 src/mcm26b/planner/__init__.py | 80 ++++ src/mcm26b/protocol/__init__.py | 15 + src/mcm26b/protocol/client.py | 288 ++++++++++++++ src/mcm26b/protocol/log.py | 34 ++ src/mcm26b/protocol/rules.py | 94 +++++ src/mcm26b/protocol/state.py | 97 +++++ src/mcm26b/protocol/timing.py | 55 +++ src/mcm26b/protocol/transport.py | 44 +++ src/mcm26b/sim/__init__.py | 8 + src/mcm26b/sim/playback.py | 38 ++ src/mcm26b/sim/simulator.py | 231 +++++++++++ src/mcm26b/sim/transport.py | 53 +++ src/visualization/question1.html | 185 --------- {src/tests => tests}/test_geometry.py | 4 +- .../test_geometry_certificates.py | 7 +- {src/tests => tests}/test_practice_replay.py | 2 +- tests/test_protocol_client.py | 253 ++++++++++++ tests/test_protocol_timing.py | 207 ++++++++++ uv.lock | 363 +++++++++++++++++- 42 files changed, 2514 insertions(+), 251 deletions(-) create mode 100644 docs/协议规则表与状态机.md rename {src/visualization => scripts/assets}/q1.template.html (100%) rename {src/visualization => scripts/assets}/standalone.css (100%) rename {src/scripts => scripts}/benchmark_q1.py (92%) rename {src/scripts => scripts}/build_q1_demo.py (90%) rename {src/scripts => scripts}/capture_practice.py (97%) rename {src/scripts => scripts}/render_q1_figures.py (100%) rename {src/scripts => scripts}/verify_visualization.cjs (97%) create mode 100644 src/mcm26b/__init__.py create mode 100644 src/mcm26b/belief/__init__.py create mode 100644 src/mcm26b/cli.py create mode 100644 src/mcm26b/config.py create mode 100644 src/mcm26b/coverage/__init__.py create mode 100644 src/mcm26b/evaluation/__init__.py rename {geometry => src/mcm26b/geometry}/__init__.py (54%) rename {geometry => src/mcm26b/geometry}/__main__.py (93%) rename {geometry => src/mcm26b/geometry}/localization.py (100%) rename {geometry => src/mcm26b/geometry}/planning.py (100%) create mode 100644 src/mcm26b/planner/__init__.py create mode 100644 src/mcm26b/protocol/__init__.py create mode 100644 src/mcm26b/protocol/client.py create mode 100644 src/mcm26b/protocol/log.py create mode 100644 src/mcm26b/protocol/rules.py create mode 100644 src/mcm26b/protocol/state.py create mode 100644 src/mcm26b/protocol/timing.py create mode 100644 src/mcm26b/protocol/transport.py create mode 100644 src/mcm26b/sim/__init__.py create mode 100644 src/mcm26b/sim/playback.py create mode 100644 src/mcm26b/sim/simulator.py create mode 100644 src/mcm26b/sim/transport.py delete mode 100644 src/visualization/question1.html rename {src/tests => tests}/test_geometry.py (97%) rename {src/tests => tests}/test_geometry_certificates.py (94%) rename {src/tests => tests}/test_practice_replay.py (97%) create mode 100644 tests/test_protocol_client.py create mode 100644 tests/test_protocol_timing.py 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" }, +]