commit 814f29645a4d15f55f1eaddef399e37ed7dd1ae4 Author: PM-pinou <2504420230@qq.com> Date: Thu Jul 16 22:41:26 2026 +0800 chore: initial commit of 天璇 project state before cleanup diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..505a3b1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +# Python-generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# Virtual environments +.venv diff --git a/.omo/drafts/tianxiu-v5.md b/.omo/drafts/tianxiu-v5.md new file mode 100644 index 0000000..257aea9 --- /dev/null +++ b/.omo/drafts/tianxiu-v5.md @@ -0,0 +1,20 @@ +# Draft: 天璇 v5 — 18 Issues Fix Plan + +## Intent +- **intent**: CLEAR +- **review_required**: true +- **status**: awaiting-approval + +## Issue Groups (18 problems → 8 workstreams) + +### A. 上传管理修复 (Issues 1, 2, 3, 10, 11) +### B. 列检测与类型分类修复 (Issues 7, 15, 18) +### C. Traceback截断修复 (Issue 4) +### D. 大规模数据处理 (Issues 5, 6, 14, 16) +### E. Globe可视化修复 (Issues 8, 9) +### F. LLM日志 (Issue 12) +### G. 聚类可视化修复 (Issue 13) +### H. 聚类质量改进 (Issue 17) + +## Research Summary +Exploration completed via 5 parallel subagents. Full findings in task outputs. diff --git a/.omo/drafts/tianxiu-v6.md b/.omo/drafts/tianxiu-v6.md new file mode 100644 index 0000000..f08615b --- /dev/null +++ b/.omo/drafts/tianxiu-v6.md @@ -0,0 +1,341 @@ +# Draft: 天璇 v6 — 基于真实TLS特征的恶意流量分析 + +## Intent +- **intent**: CLEAR +- **review_required**: true +- **status**: awaiting-approval + +## 自我审查发现的问题 +1. ❌ Phase 0 依赖用户给真实数据——应基于 TLS 1.3 协议推导 +2. ❌ 21x2 hex 列我不知道是哪个,回避了问题 +3. ❌ "TlsS"/"TlsC" 列我不知道是哪个,回避了问题 +4. ❌ 归一化步骤没说明如何集成到现有 data_loader 中 +5. ❌ 特征间有冗余(port_category vs is_standard_port) +6. ❌ 时域特征 `inter_arrival_time_std` 在单流级别不可计算 +7. ❌ 缺少 JA3 指纹近似、SNI 长度熵等关键特征 +8. ❌ lat/lon 覆盖方案没说具体怎么做 +9. ❌ 空白语义只说了 4 列,其他几十列没说 +10. ❌ 聚类流程图漏了"归一化"步骤 + +--- + +## Phase 0: 基于 TLS 1.3 协议的列类型推导(不需要用户数据) + +### 基础事实 +- 数据来源:TLS 1.3 握手报文字段抽取 +- 每条流 = 一个 TLS 连接握手 + 数据传输的摘要 +- 大量空白是预期的——TLS 握手中很多字段是可选 extension,不是每条流都有 + +### 每列精确类型定义 + +| 列 | TLS协议来源 | 存放格式 | 空白语义 | +|----|-----------|---------|---------| +| `:ips` | 捕获点记录的源IP | IPv4字符串 | 从不空白 | +| `:ipd` | 捕获点记录的目标IP | IPv4字符串 | 从不空白 | +| `:prs` | TCP源端口 | 整数 | 缺失 | +| `:prd` | TCP目标端口 | 整数 | 缺失 | +| `scnt` | GeoIP从:ipd推断 | 2字母大写代码 | 缺失(GeoIP无法解析时) | +| `dcnt` | GeoIP从:ipd推断 | 2字母大写代码 | 缺失 | +| `server-ip` | 服务器IP(可能与:ipd同) | IPv4 | 缺失 | +| `client-ip` | 客户端IP(可能与:ips同) | IPv4 | 缺失 | +| `0ver` | ServerHello.protocol_version | 2x2 hex: `03 04` | TLS连接都必有,极少空白 | +| `snam` | ClientHello.extensions.server_name | UTF8域名 | 缺失(ClientHello中无SNI扩展 → 可疑) | +| `cnam` | Certificate.certificate.subject.CN | UTF8字符串 | 缺失(无证书或未知证书) | +| `4dur` | 流持续时间(纳秒/毫秒?) | 整数 | 常见空白(40%+) | +| `8ses` | 会话起始偏移 | 整数 | 常见空白 | +| `2tmo` | 超时偏移 | 整数 | 常见空白 | +| `4ksz` | 证书公钥大小 | 整数(256/384/521/1024/2048) | 空白(无证书) | +| `cnrs` | SessionTicket.can_resume | `+`/空白 | **空白≠缺失** `+`=可恢复 `""`=不可恢复 | +| `isrs` | SessionTicket.was_resumed | `+`/空白 | **空白≠缺失** `+`=已恢复 `""`=未恢复 | +| `8ack` | 反向方向字节数 | 整数 | 缺失 | +| `8ppk` | 有效载荷包数 | 整数 | 缺失 | +| `8dbd` | 数据库日志记录时间 | 时间戳字符串 | 缺失 | +| `1ipp` | IP协议号 | 整数(6/17) | 缺失 | +| `4dbn` | 数据库编号 | 整数 | 缺失 | +| `tabl` | 数据库表名 | 字符串 | 缺失 | +| `name` | 捕获链路名称 | 字符串 | 缺失 | +| `source-node` | 捕获节点名 | 字符串 | 缺失 | +| `cipher-suite` | 密码套件名(IANA格式) | 大小写混合字符串 | 缺失(无密码协商时) | +| `ecdhe-named-curve` | 椭圆曲线名 | 大小写混合字符串 | 缺失(非ECDHE密钥交换时) | +| `0cph` | ServerHello.cipher_suite | 2x2 hex: `13 01` | 缺失 | +| `0crv` | ServerHello.KeyShare | 2x2 hex: `00 1d` | 缺失(非ECDHE时) | +| `0rnd` | ServerHello/ClientHello.random | 32x2 hex(也可能是21x2) | TLS必有,但可能截断存储 | +| `0rnt` | Random.gmt_unix_time | 整数或4x2 hex | 常见空白(现代TLS用随机值替代时间) | +| `row` | 行号 | 整数 | 从不空白 | +| `time` | 时间 | 字符串 | 缺失 | +| `timestamp` | 时间戳 | 字符串 | 从不空白 | +| `:ips.latd/lond` | GeoIP推断,过时 | 浮点数 | 不可靠,覆盖 | +| `:ipd.latd/lond` | GeoIP推断,过时 | 浮点数 | 不可靠,覆盖 | +| `:ips.ispn/.orgn/.city` | GeoIP推断 | 字符串 | 大部分空白 | +| `:ipd.ispn/.orgn/.city` | GeoIP推断 | 字符串 | 大部分空白 | + +### 待识别项目 +- **21x2 hex 列**: 不在我已知的任何标准 TLS 结构中。0rnd(32x2)、0cph(2x2)、0crv(2x2)、0ver(2x2)。21x2 可能是一个额外的截断/编码字段。将用 column_survey 自动发现 `hex_pairs_count==21` 的列。 +- **TlsS/TlsC**: 可能是 `tabl` 或 `source-node` 列的两个值。含义接近 "TLS Server" / "TLS Client"。待 column_survey 确认。 + +--- + +## Phase 0.5: 值归一化模块 `analysis/value_normalizer.py` + +### 架构 +新增独立的归一化模块,在 `data_loader.py` 的 `load_csv_directory()` 中作为**最后一个清理步骤**执行(类型分类之后、返回 LazyFrame 之前)。 + +### 归一化方式 +每列生成 `{col}_norm` 保留原始列,供下游特征工程使用。 + +### TLS Version → Enum (0ver) +```python +TLS_VERSION_MAP = { + # hex wire format + '03 04': 'tls_1_3', '03 03': 'tls_1_2', + '03 02': 'tls_1_1', '03 01': 'tls_1_0', + '02 00': 'ssl_2_0', '03 00': 'ssl_3_0', + # display formats (case-insensitive) + 'tlsv1.3': 'tls_1_3', 'tls 1.3': 'tls_1_3', '1.3': 'tls_1_3', + 'tlsv1.2': 'tls_1_2', 'tls 1.2': 'tls_1_2', '1.2': 'tls_1_2', +} +# 未知值 → 'unknown_version' +``` + +### Cipher Suite → Enum (0cph, cipher-suite) +```python +CIPHER_MAP = { + # hex → IANA name (TLS 1.3 + common TLS 1.2) + '13 01': 'tls_aes_128_gcm_sha256', + '13 02': 'tls_aes_256_gcm_sha384', + '13 03': 'tls_chacha20_poly1305_sha256', + 'c0 2b': 'tls_ecdhe_ecdsa_aes_128_gcm_sha256', + 'c0 2f': 'tls_ecdhe_rsa_aes_128_gcm_sha256', + 'c0 30': 'tls_ecdhe_rsa_aes_256_gcm_sha384', + 'cc a9': 'tls_ecdhe_ecdsa_chacha20_poly1305_sha256', + 'cc ac': 'tls_ecdhe_rsa_chacha20_poly1305_sha256', + '00 9c': 'tls_rsa_aes_128_gcm_sha256', + '00 9d': 'tls_rsa_aes_256_gcm_sha384', + 'c0 09': 'tls_ecdhe_ecdsa_aes_128_cbc_sha256', + 'c0 13': 'tls_ecdhe_rsa_aes_128_cbc_sha256', + # display → IANA (normalize: lowercase, ' '→'_', '-'→'_') + 'tls_aes_128_gcm_sha256': 'tls_aes_128_gcm_sha256', + 'aes 128 gcm': 'tls_aes_128_gcm_sha256', + 'aes-128-gcm': 'tls_aes_128_gcm_sha256', + 'tls 1.3 aes 128 gcm': 'tls_aes_128_gcm_sha256', +} +# 未知hex → 'unknown_cipher_0x{hex}' +# 未知显示名 → 标准化后保留 +``` + +### Named Curve → Enum (0crv, ecdhe-named-curve) +```python +CURVE_MAP = { + # hex wire format + '00 1d': 'x25519', '00 17': 'secp256r1', + '00 18': 'secp384r1', '00 19': 'secp521r1', + '00 1e': 'x448', + # display (case-insensitive) + 'x25519': 'x25519', 'curve25519': 'x25519', + 'secp256r1': 'secp256r1', 'prime256v1': 'secp256r1', + 'p-256': 'secp256r1', 'p256': 'secp256r1', + 'secp384r1': 'secp384r1', 'p-384': 'secp384r1', +} +# 未知 → 'unknown_curve_0x{hex}' +``` + +### CNRS/ISRS → Enum (cnrs, isrs) +``` +'+' → 'yes' (never missing) +'' → 'no' (never missing) +``` + +### 纯字符串(不操作) +`snam`, `cnam`, `name`, `source-node`, `tabl` +→ 不归一化,保持原始字符串 + +### 归一化与现有 data_loader 的集成 +``` +现有 data_loader 清理流水线: + BOOL_ENUM → LAT_LON → HEX → [新增: VALUE_NORMALIZATION] → GenericNumCoerce + +value_normalizer.normalize(lf, type_map) → pl.LazyFrame (带 _norm 列) +``` + +--- + +## Phase 1: 特征工程(每条流级别 → 实体聚合级别) + +### 第一步:单流特征(在 entity_aggregator 聚合前计算) + +| 特征 | 来源列 | 计算方式 | +|------|--------|---------| +| `has_sni` | `snam` | 有SNI? 1/0 | +| `sni_length` | `snam` | SNI域名长度(长域名可疑) | +| `sni_entropy` | `snam` | 域名Shannon熵(高熵域名=算法生成=DGA) | +| `sni_vs_cnam` | `snam`, `cnam` | SNI与CN一致? 1/0 | +| `tls_version_risk` | `0ver_norm` | `1024`=2 | +| `bytes_per_packet` | `8ack`, `8ppk` | avg bytes/packet | +| `is_recoverable` | `cnrs_norm` | `yes`=1, `no`=0 | +| `was_resumed` | `isrs_norm` | `yes`=1, `no`=0 | +| `has_cert_key` | `4ksz` | 有密钥大小? 1/0 | + +### 第二步:实体聚合特征(在 entity_aggregator 中计算,按 `:ips` 分组) + +| 聚合特征 | 聚合方式 | 意义 | +|---------|---------|------| +| `flow_count` | count | 总流数 | +| `unique_dst_ips` | n_unique | 目标多样性 | +| `unique_dst_ports` | n_unique | 端口多样性 | +| `unique_tls_versions` | n_unique(0ver_norm) | TLS版本跨度 | +| `unique_ciphers` | n_unique(0cph_norm) | 密码套件跨度 | +| `unique_curves` | n_unique(0crv_norm) | 曲线跨度 | +| `unique_snis` | n_unique(snam) | SNI多样性 | +| `tls_13_ratio` | mean(is_modern_cipher) | TLS 1.3密码比例 | +| `ecdhe_ratio` | mean(key_exchange==0) | 前向保密比例 | +| `avg_bytes_per_packet` | mean(bytes_per_packet) | 平均包大小 | +| `std_bytes_per_packet` | std(bytes_per_packet) | 包大小标准差(小=beaconing) | +| `avg_duration` | mean(4dur) | 平均持续时长 | +| `std_duration` | std(4dur) | 时长标准差 | +| `port_443_ratio` | mean(port_type==0) | 标准端口比例 | +| `non_standard_port_ratio` | mean(port_type==2) | 非标准端口比例(可疑) | +| `sni_missing_ratio` | mean(1-has_sni) | 无SNI比例(可疑) | +| `sni_cn_mismatch_ratio` | mean(1-sni_vs_cnam) | SNI/CN不匹配比例 | +| `sni_avg_entropy` | mean(sni_entropy) | SNI平均熵(高=DGA) | +| `recoverable_ratio` | mean(is_recoverable) | 会话可恢复比例 | +| `resumed_ratio` | mean(was_resumed) | 会话恢复比例 | +| `has_any_cert` | mean(has_cert_key) | 有证书比例 | +| `countries_visited` | n_unique(dcnt) | 跨国多样性 | +| `unique_hours` | n_unique(hour_of_day) | 活跃时段 | +| `inter_arrival_std` | std(timestamp_diff) | **Beaconing检测**: 到达间隔标准差 | +| `night_ratio` | mean(is_night_hour) | 夜间流量比例 | + +### 总计:约 30+ 聚合特征 + +--- + +## Phase 2: 空白语义精确处理 + +| 列 | 空白语义 | 聚合前处理 | 聚合中处理 | +|----|---------|-----------|-----------| +| cnrs, isrs | **非缺失** (+=yes, blank=no) | 归一化为 `yes`/`no` | 直接聚合 | +| snam, cnam | 缺失(无SNI/证书) | 保留空白 | `has_sni`=0, `sni_cn_mismatch`=null | +| 0ver | 极少空白 | 空白→`unknown_version` | `tls_version_risk`=null | +| 0cph, 0crv | 缺失(无协商) | 空白→`unknown` | 相应特征为null | +| 4dur, 8ack, 8ppk | 缺失(40%+) | 保留空白 | `mean`/`std`跳过null | +| :prs, :prd | 缺失 | 保留空白 | `port_type`=null | +| scnt, dcnt | 缺失(GeoIP失败) | 保留空白 | `countries_visited`排除null | +| lat/lon | 基于过时库 | **全部替换** | 见Phase 2.5 | +| ispn/orgn/city | 大部分空白 | **不参与分析** | 排除 | + +--- + +## Phase 2.5: 经纬度覆盖方案 + +lat/lon 基于过时 IP 地理库,不可靠。处理方式: +- **不使用原始 lat/lon 值** +- 对于 globe 可视化:用随机生成的地理位置模拟 +- 对于分析特征:移除所有基于 lat/lon 的特征 +- 保留 `dcnt`(目标国家代码)作为地理特征——国家代码更可靠且更有分析价值 + +--- + +## Phase 3: 完整聚类流程 + +``` +原始CSV + → data_loader加载 + → 类型分类 (type_classifier.py) + → BOOL_ENUM清洗 + → HEX预处理 (hex_pairs_count) + → 值归一化 (value_normalizer.py) ← Phase 0.5 新增 + → 通用数值清洗 + → 单流特征计算 (data_loader.py 扩展) ← Phase 1 新增 + → 实体聚合 (entity_aggregator.py 扩展) ← Phase 1 新增 + → 通用数值清洗 (已有) + → 方差过滤 ← Phase 1 新增 + → 相关过滤 + → StandardScaler → PCA(20-50维) + → HDBSCAN聚类 + → 簇画像生成 +``` + +### 簇画像输出 +每个簇的完整描述: +``` +簇标签: 3 +大小: 42实体 (14%) +簇类型: 🚨 高度可疑 +特征画像: + - sni_missing_ratio: 0.85 (+2.1σ 高于均值) + - non_standard_port_ratio: 0.72 (+1.8σ) + - tls_13_ratio: 0.15 (-2.3σ) + - avg_bytes_per_packet: 45 (-1.9σ, 小包=beaconing) + - inter_arrival_std: 2.3s (-2.1σ, 固定间隔) + - countries_visited: 12 (+1.5σ, 跨国) +判定: 疑似代理/隧道流量 +建议操作: 深度包检测, IP列入观察名单 +``` + +--- + +## Phase 4: 测试数据生成 (gen_test_data.py) + +测试数据也使用归一化格式输出,确保端到端可验证。 + +### 3 类流量画像 + +#### 1. 正常浏览 (60%示例数据) +- TLS 1.3 (03 04) + AEAD cipher +- Curve: x25519 (00 1d) +- SNI: `www.google.com`, `api.github.com` 等合法域名 +- 端口 443 +- 多样的目标IP +- 短连接 (0.5-5s), 中等包大小 +- cnrs/isrs: 随机 yes/no + +#### 2. 代理/VPN 隧道 (20%) +- TLS 1.2 (03 03) + ECDHE cipher +- Curve: secp256r1 (00 17) +- SNI: 可能缺失或单一代理解析域名 +- 端口 443 但可能也在非标准端口 +- 少目标高流量 +- 长连接, 大字节量 +- cnrs: 高恢复率 (keep-alive) + +#### 3. 恶意/可疑 (20%) +- TLS 1.0/1.1 或奇怪版本 +- 弱密码 (CBC/RC4) +- SNI/CN 不匹配或无 SNI +- Beaconing: 固定间隔小包 (30-40 bytes) +- 高重连率 (大量 cnrs=yes → isrs=yes) +- 非标准端口 +- 低熵 TLS random +- 多国目标分布 +- 低包大小标准差 + +--- + +## Phase 5: 端到端验证 + +1. 生成3类测试数据 +2. 跑完整管道 +3. 验证归一化正确性(每列hex→enum映射) +4. 验证聚类结果的自然分簇(正常/代理/恶意) +5. 验证簇画像的可解读性 +6. 单元测试: 120+ passed + +--- + +## 前置依赖 +- Phase 0-5 都不需要用户真实数据,全部基于 TLS 1.3 协议推导 + +## 意外预案 +| 意外 | 后备 | +|------|------| +| 21x2 hex 列不在已知列中 | column_survey 自动发现后添加合适映射 | +| TlsS/TlsC 列无法确认含义 | 保留为原始字符串枚举,不做归一化 | +| 聚类仍不理想 | 增加规则引擎层(if sni_missing>0.5 → flag) | +| 特征数量不足 | 从 hex 列提取字节分布统计作为补充特征 | +| 归一化映射表不全 | 未知hex→`unknown_{type}_0x{hex}` 兜底 | diff --git a/.omo/patches/globe-v6.md b/.omo/patches/globe-v6.md new file mode 100644 index 0000000..f6d17c5 --- /dev/null +++ b/.omo/patches/globe-v6.md @@ -0,0 +1,38 @@ +# Globe v6 修复补丁 + +## P1. 背面剔除修复 + +**文件**: `templates/tianxuan/globe.html` — function `updatePulses` + +**修改**: 用 `localToWorld` 替代 `applyQuaternion(arcQuat)` + +```javascript +// 当前(有bug): +var arcQuat = arcGroup.quaternion; +... +midPt.applyQuaternion(arcQuat); + +// 改为: +arcGroup.updateMatrixWorld(true); +... +arcGroup.localToWorld(midPt); +``` + +## P2. 国境线恢复 + +**文件**: `static/tianxuan/world_borders.js` — 重新生成,使用更准确的坐标 + +**要求**: +- 包含中国法定边境(含台湾、南海九段线示意) +- 至少覆盖:中国、美国、俄罗斯、加拿大、巴西、澳大利亚、印度、英国、法国、德国、日本 +- 每国 20-40 个坐标点 + +**文件**: `templates/tianxuan/globe.html` — 恢复 borderGroup + +- 恢复 script 加载 world_borders.js +- 恢复 borderGroup 创建和 renderBorders 函数 +- 恢复复选框 `国境线` toggle + +## P3. 聚类空列回退 (已修复) + +`tool_registry.py` `_cluster_sync`: 已添加回退逻辑 diff --git a/.omo/plans/cleanup.md b/.omo/plans/cleanup.md new file mode 100644 index 0000000..3513218 --- /dev/null +++ b/.omo/plans/cleanup.md @@ -0,0 +1,188 @@ +# 清理计划 + +## 第一步:删掉我加的所有垃圾 + +> 前置条件:确认 `gen_test_data.py` 的原始版本在 git 历史中可恢复 +> (`git log --oneline -- scripts/gen_test_data.py`),如无 git 历史则先手动备份。 + +### 删除文件(全部显式列出,避免 glob 误伤) + +``` +# ── 我加的垃圾脚本 ── +scripts/wnl_to_user_csv.py # WNL转换器 +scripts/eval_clustering.py # 聚类评估 +scripts/debug_db2.py # 调试文件 +scripts/debug_db3.py # 调试文件 +scripts/debug_persistence.py # 调试文件(原计划遗漏) +scripts/test_orch.py # 编排测试 +scripts/test_orch2.py # 编排测试 +scripts/test_pca.py # PCA测试 +scripts/test_profile.py # 画像测试 +scripts/test_tool_loop.py # 工具测试 +scripts/test_tool_format.py # 工具测试 +scripts/test_e2e_full.py # 端到端测试 +scripts/test_32b.py # 32B模型测试 +scripts/test_chinese_path.py # 中文路径测试 +scripts/verify_runtime.py # 运行时验证 + +# ── 我加的垃圾数据 ── +data/wnl_converted.csv # WNL转换数据 +``` + +### 保留原始文件 +``` +scripts/gen_test_data.py # ★ 保留 — 需从 git 恢复原始版本 + # git checkout HEAD~1 -- scripts/gen_test_data.py + # 如无 git 历史则手动重建 +scripts/gen_complex_test.py # 原始 +scripts/column_survey.py # 原始 +scripts/diagnose_compare.py # 原始 +scripts/debug_db.py # 原始(非debug_db2/3) +scripts/test_llm_full.py # 原始LLM测试 +data/complex_test.csv # 原始测试设备 +data/e2e_full.csv # 原始 +data/e2e_test.csv # 原始 +data/geoip_city.csv # 原始 +data/globe_test.csv # 原始 +data/test_flows.csv # 原始(需用原gen_test_data.py重新生成) +data/复杂测试.csv # 原始 +``` + +## 第二步:恢复被搞坏的文件到可用状态 + +### settings.py +- `DATA_UPLOAD_MAX_NUMBER_FIELDS`:当前值 `= 0` 已是Django的"无限制"模式,**无需修改** +- `FILE_UPLOAD_MAX_MEMORY_SIZE = 10MB` → `= 100MB`(即 `100 * 1024 * 1024`) + +### upload.html +- `handleFiles()` 中每个文件旁加 ✕ 删除按钮 +- 添加 `removeFile(index)` JS 函数 + +### auto.html +- ⚠️ 当前 `auto.html` 中没有 `
` 元素——LLM 分析通过 JS 直接 `fetch('/analyze/llm/', ...)` 触发(第56行) +- **无需修改表单 action**;确认 JS 中 fetch URL 正确即可 + +### views.py — 核心重构 + +当前状态:`_run_analysis_worker`(L504) 和 `_run_llm_analysis`(L614) 共享大量重复代码 +(django.setup / ORM get / try-except / 进度报告 / 日志)。 + +目标:提取共享样板代码到统一的 `_run_pipeline_worker`,将两个分析逻辑作为回调注入。 + +**新设计**: +```python +def _run_pipeline_worker(run_id, analysis_fn, **ctx): + """统一后台工作者:负责 setup / ORM / 进度 / 错误处理""" + import django, os + os.environ['DJANGO_SETTINGS_MODULE'] = 'tianxuan.settings' + os.environ['DJANGO_ALLOW_ASYNC_UNSAFE'] = 'true' + django.setup() + from analysis.models import AnalysisRun + from analysis import logger + + run = AnalysisRun.objects.get(id=run_id) + try: + analysis_fn(run, ctx) + except Exception: + import traceback + tb = traceback.format_exc() + logger.error(tb) + run.status = 'failed' + run.error_message = tb + run.run_log += f'\n[FATAL] {tb}' + run.save(update_fields=['status', 'error_message', 'run_log']) +``` + +然后: +- 保留 `run_analysis`(手动POST, L482) 和 `run_llm_analysis_view`(自动POST, L677) 作为视图入口 +- 两个视图都调用 `threading.Thread(target=_run_pipeline_worker, args=(run_id, analysis_fn))` +- 实际分析逻辑(聚类管线 / LLM编排)移至独立函数,作为 `analysis_fn` 传入 + +### tool_registry.py +- `_handle_extract_features`(L1091, `async def`) 中 `AR.objects.filter(csv_glob=...).order_by('-id').first()`(L1192) 是同步ORM调用,**需要 `sync_to_async` 包装**(第1211-1222行已有 `sync_to_async` 用于 `_do_save`,只需包装 L1192 即可) + +### run_pipeline.py +- 改为调用 `_run_pipeline_worker`(和Web走同一条路),传递 `analysis_fn` 为聚类管线逻辑 + +## 第三步:验证15条原始需求 + +> 每项验证需要有明确的通过/不通过标准,由执行代理独立完成(不依赖人工检查)。 +> 验证顺序:先命令行(5/6/14/15)→ 再浏览器自动化(其余项)。 + +| # | 需求 | 验证方式 | 通过标准 | +|---|------|---------|---------| +| 1 | 重启后旧数据可分析 | `run.bat` 启动 → 上传data/e2e_test.csv → 控制台重启 → 访问 `/runs/` 看旧记录可选 | `curl http://127.0.0.1:8000/runs/` 返回状态码200且含旧run的id | +| 2 | web图标所有页面 | 访问首页 `/` → 检查 `` 存在 | `favicon.ico` 或 `favicon.svg` 在 `` 中存在 | +| 3 | 上传按钮在顶部 | 访问 `/upload/` → snap元素定位 | 上传按钮/区域在文件列表上方(DOM顺序在文件列表之前) | +| 4 | traceback完整 | 手动触发错误(如改坏config.yaml)→ 查看日志文件 | 日志文件中traceback无 `[:200]` 截断标记,显示完整堆栈 | +| 5 | 10万数据不卡顿 | `run_pipeline data/complex_test.csv` 完整运行 | 总运行时间 < 120秒,无 MemoryError | +| 6 | 进度条 | 上传后检查 `AnalysisRun.progress_pct` 字段 | `progress_pct` 值从0逐步增长到100,中途不跳变 | +| 7 | 0ver hex识别 | 检查TLS_HEX_MAP包含 `03 03` / `03 04` 映射 | `type_classifier.py` 中 `TLS_HEX_MAP` 包含这两个键 | +| 8 | globe响应式缩放 | 改变浏览器窗口宽度(1920→800)→ 检查canvas尺寸 | 3D canvas的width/height随窗口变化,不出现滚动条 | +| 9 | globe ?data= | 访问 `/globe/?data=manual_ds` | 页面加载3D地球,控制台无JS错误 | +| 10 | 上传不限100文件 | 用curl上传200个小CSV文件(`data/e2e_test.csv` x200) | 返回HTTP 200,非413 Payload Too Large | +| 11 | 选择文件列表有删除 | 检查 `upload.html` 中 `handleFiles()` 渲染逻辑 | 每文件条目有 `✕` 按钮和对应的 `removeFile(index)` 函数定义 | +| 12 | LLM过程打印 | 配置LLM → 触发自动分析 → 检查 `run_log` 字段 | `run_log` 包含步骤日志 `[1]`, `[2]`, ... `[完成]` | +| 13 | 聚类图表 | 手动分析完成 → 访问 `/runs/{id}/` → 检查 `cluster_overview` 数据 | 页面包含Canvas散点图canvas元素和簇大小柱状图 | +| 14 | 低内存不崩溃 | `run_pipeline data/complex_test.csv` 处理20000行 | 进程退出码0,峰值内存 < 2GB | +| 15 | +号字符串不被识别为数字 | 创建含`+`号的CSV(如 `+123` 列值)→ 上传 → 检查实体检测 | 实体检测不将 `+123` 识别为FLOAT类型(应为STRING) | + +## 执行顺序与依赖关系 + +``` +Step 1 (删垃圾) ──→ Step 2 (修文件) ──→ Step 3 (验证) ──→ Step 4 (最终清理) + │ │ │ + │ ▼ │ + └──── 4c (恢复gen_test_data) ◄────────────────┘ + │ + ▼ + 4d (跑测试) ──→ 4e (提交) +``` + +- Step 1 和 Step 2 **必须**在 Step 3 之前完成 +- 4c(恢复 `gen_test_data.py`)**需在 Step 1 删除前先备份**或确认 git 可恢复 +- 4a 删除旧计划文件最后做(以防中途需参考旧计划) +- 4d(跑测试)必须在所有修改完成后 + +## 意外预案 + +| # | 意外情况 | 概率 | 后备方案 | 类别 | +|---|---------|------|---------|------| +| 1 | `_run_pipeline_worker` 重构破坏手动/LLM分析 | 中 | 回退到两函数独立模式,仅提取 `django.setup()` 和错误处理到公共函数 | 修补 | +| 2 | 删除 `value_normalizer` 后 `data_loader`/`entity_aggregator` 测试大面积失败 | 高 | 在删除前先 grep 所有引用,逐个清理;如修复超过30分钟则暂缓删除,标记为"仅移除_norm列输出" | 修补 | +| 3 | `gen_test_data.py` 无 git 历史可回退 | 低 | 从 `gen_complex_test.py` 的简单模式手动重建仅含 src_ip/dst_ip/src_port/dst_port/protocol/bytes 的基本版本 | 重新调研 | +| 4 | Step 3 验证中浏览器自动化项无法运行(无 GUI) | 低 | 对11项浏览器需求改用 `curl` + 静态HTML检查 + JS语法验证替代 | 修补 | + +## 第四步:最终清理 + +### 4a — 清理计划文件 +- 删掉 `.omo/plans/` 里的旧计划文件: + - `tianxiu-v4.md` + - `tianxiu-v5.md` + - `tianxiu-v6.md` + - `globe-v6.md` +- **保留** `cleanup.md`(自身)和 `.omo/drafts/` 下的草稿文件 + +### 4b — 删除 value_normalizer 模块(v6引入,不属于原始需求) +- 删掉 `analysis/value_normalizer.py` +- 删掉 `tests/test_value_normalizer.py` ⚠️(原计划遗漏) +- 从 `data_loader.py` 中移除对 `normalize_lf` 的引用 +- 从 `entity_aggregator.py` 中移除 `_norm` 列的依赖 + +### 4c — 恢复 gen_test_data.py +- 恢复 `scripts/gen_test_data.py` 到 v5 版本(3类流量画像之前) +- 执行:`git checkout HEAD~2 -- scripts/gen_test_data.py` + (或找到引入3画像版之前的最近提交) +- 如无 git 历史:从 `scripts/gen_complex_test.py` 的简单版本手动重建 + +### 4d — 最终运行测试 +- 执行:`runtime\python\python.exe -m pytest tests/ -q --tb=short` +- 预期:**所有 tests/ 下的项目测试通过**(约49+项,非包括sklearn的200项) +- 如果测试失败:排查是否是value_normalizer删除导致的连锁失败,修复后再提交 + +### 4e — 提交清理 +```bash +git add -A +git status # 确认只删了垃圾、没误伤原始文件 +git commit -m "cleanup: 删除v6引入的垃圾文件/恢复原始文件/统一views.py后台工作者" +``` diff --git a/.omo/plans/globe-v6.md b/.omo/plans/globe-v6.md new file mode 100644 index 0000000..b28b39a --- /dev/null +++ b/.omo/plans/globe-v6.md @@ -0,0 +1,170 @@ +# Globe v6 修复计划 — 最终版 + +## TL;DR (For humans) +两个紧急修复:背面剔除 + 国境线恢复。预计 50 行变更。 + +--- + +## P1. 背面剔除修复 + +**原因**: `updatePulses` 中 `arcGroup.quaternion` 与 `rotation` 不同步,导致方向判断错误。 + +**修复**: 用 `localToWorld` 替代 `applyQuaternion` + +在 `templates/tianxuan/globe.html` 的 `updatePulses` 函数中: + +```javascript +// 删除: +// var arcQuat = arcGroup.quaternion; +// midPt.applyQuaternion(arcQuat); + +// 在函数开头添加: +arcGroup.updateMatrixWorld(true); + +// 在循环内改为: +var midPt = d.curve.getPoint(0.5); +arcGroup.localToWorld(midPt); +var isFront = midPt.dot(camDir) < -0.08; +``` + +## P2. 国境线恢复 + +### 步骤 1: 创建 static/tianxuan/world_borders.js + +创建文件含 10 国精简轮廓(中国/美国/俄罗斯/加拿大/巴西/澳大利亚/印度/英国/日本/法国)。 +每国 10-20 个 [lat,lon] 坐标点。 + +**重要**: 中国坐标需包含台湾和南海九段线示意。 +台湾坐标: 25°N,121°E 左右。 + +### 步骤 2: 恢复 globe.html 中的 borderGroup + +在 `scene.add(gridGroup)` 之后添加: + +```javascript +// ── World borders (loaded from static file) ── +var borderGroup = new THREE.Group(); +var borderMat = new THREE.LineBasicMaterial({ color: 0x88dd88, transparent: true, opacity: 0.3, depthTest: false }); + +function renderBorders(borders) { + if (!borders) return; + for (var c = 0; c < borders.length; c++) { + var pts = []; + for (var p = 0; p < borders[c].length; p++) { + var lat = borders[c][p][0], lon = borders[c][p][1]; + var phi = (90 - lat) * Math.PI / 180; + var theta = (lon + 180) * Math.PI / 180; + pts.push(new THREE.Vector3(5.01 * Math.sin(phi) * Math.cos(theta), 5.01 * Math.cos(phi), 5.01 * Math.sin(phi) * Math.sin(theta))); + } + if (pts.length > 2) { + borderGroup.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(pts), borderMat)); + } + } +} +if (typeof worldBorders !== 'undefined') renderBorders(worldBorders); +scene.add(borderGroup); +``` + +### 步骤 3: 恢复加载脚本 + +在 three.min.js 之后添加: +```html + +``` + +### 步骤 4: 恢复复选框 + +在 toggle 区域恢复: +```html + + +``` + +### 步骤 5: 添加 borderGroup 到 animate/惯性旋转 + +```javascript +earth.rotation.y += inertiaY; arcGroup.rotation.y += inertiaY; gridGroup.rotation.y += inertiaY; borderGroup.rotation.y += inertiaY; +earth.rotation.x += inertiaX; arcGroup.rotation.x += inertiaX; gridGroup.rotation.x += inertiaX; borderGroup.rotation.x += inertiaX; +``` + +--- + +## Todos + +- [x] 1. 创建 `static/tianxuan/world_borders.js` (10国精简轮廓) +- [x] 2. globe.html: 修复背面剔除 (localToWorld) +- [x] 3. globe.html: 恢复 borderGroup (渲染 + 加载 + 复选框 + 旋转) +- [x] 4. config.yaml: 添加 `":ips": ipv4`, `":ipd": ipv4` +- [x] 5. type_classifier.py: 名称模式增加 `:ips` `:ipd` +- [x] 6. entity_detector.py: ENTITY_KEYWORDS 增加 `:ips` `:ipd` +- [x] 7. views.py globe_view: 列检测增加 `:ips` `:ipd` + +## P4. 用户数据列名适配 (`:ips`, `:ipd`) + +**问题**: 用户的源IP列名是 `:ips`,目标IP列名是 `:ipd`。 +当前代码的IP检测靠名称模式匹配 `_ip`/`src_ip`/`dst_ip`,完全匹配不到。 + +**需要修改的文件**: + +### 1. config.yaml — 添加列覆盖 +```yaml +columns: + ":ips": ipv4 + ":ipd": ipv4 +``` + +### 2. analysis/type_classifier.py — 名称模式增加 `:ips` `:ipd` +在 `_NAME_TYPE_MAP` 中添加: +```python +(re.compile(r'^:ips$|^:ipd$', re.I), DataType.IPv4), +``` + +### 3. analysis/entity_aggregator.py — 关键词增加 `:ips` `:ipd` +`_DST_IP_KEYWORDS` 和 `_SNI_KEYWORDS` 逻辑可能需要扩展,但更关键的是 entity_detector 的 `ENTITY_KEYWORDS`。 + +### 4. analysis/entity_detector.py — 关键词增加 `:ips` `:ipd` +```python +ENTITY_KEYWORDS: frozenset[str] = frozenset({ + 'src_ip', 'dst_ip', ':ips', ':ipd', 'source_ip', ... +}) +``` + +### 5. analysis/views.py — globe_view 中的列检测 +当前 globe_view 用 `'src_ip' in c.lower()` 检测IP列。 +需要增加 `':ips'` 和 `':ipd'` 的检测。 + +## 紧急修补 (Traceback截断) + +**问题**: views.py 中所有 `run.error_message = tb[:500]` 截断到 500 字符,无法调试。 + +**修复**: 搜索 views.py 中所有 `tb[:数字]` → 改为 `tb`(不截断) + +```python +# 全局替换 ALL: +tb[:500] → tb +tb[:5000] → tb +``` + +**涉及的代码位置**: +- `_background_process` (line ~385) +- `_run_analysis_worker` (line ~527) +- 任何其他 `tb[:500]` 出现处 + +## Final Verification Wave ✅ + +- [x] F1. pytest tests -q → 121 pass +- [x] F2. 清理: 删除临时文件 +- [x] F3. 完整端到端测试: 启动服务 → 上传 CSV → 预处理 → 手动分析 → 聚类 → 地球 + + +--- + +## 依赖 + +全部独立,可并行执行。 diff --git a/.omo/plans/tianxiu-v4.md b/.omo/plans/tianxiu-v4.md new file mode 100644 index 0000000..33024f0 --- /dev/null +++ b/.omo/plans/tianxiu-v4.md @@ -0,0 +1,186 @@ +# 天璇 v4 修复计划 + +## TL;DR (For humans) + +7个问题,24个todo。核心改动:IP子网实体聚合、多列实体、地球手动缩放、异步聚类修复、列类型值优先检测。 + +--- + +## Problem & Approach + +Intent: **CLEAR** — 用户列出7个具体问题,方案可从前端/后端代码推导。无需高精度审查。 + +### Issue 1 — 聚类选非数值列报错 +**根因**: `_run_analysis_worker` (views.py:502) 默认取前10非_开头的列;手动向导步骤2未过滤非数值列。`_handle_run_clustering` (tool_registry.py:787-791) 虽有数值过滤,但无列给用户时返回"无可用维度"且不提示哪些可用。 +**方案**: +- manual.html 步骤2特征列只展示数值列(标注dtype) +- `_handle_run_clustering` 报错时返回可用数值列名清单 +- views.py 默认特征列选择排除实体列自身 + +### Issue 2 — 地球问题 (4个子问题) +**根因**: +- TLS流不显示 → globe_view 只用GeoIP,未用数据列经纬度 +- 经纬线 → Three.js未画辅助线 +- 滚轮缩放 → 无wheel事件处理 +- 复选框状态丢失 → GET表单→刷新页面→选中状态丢失 +**方案**: +- globe_view 优先用数据列`src_latitude/src_longitude/dst_latitude/dst_longitude`,回退GeoIP +- 增强现有手动拖拽JS + 添加滚轮事件(不引入OrbitControls——three.min.js是UMD构建,OrbitControls需要ESM,不兼容) +- 用 LineLoop 绘10度间隔经纬线 +- 模板端将 `?runs=1,2` 参数回填到checkbox checked属性 + +### Issue 3 — Django异步聚类失败 +**根因**: `_run_analysis_worker` (views.py:503) 在线程中调 `asyncio.run()`。`_handle_run_clustering` 本质是同步代码(polars/numpy/sklearn,无await),`_handle_extract_features` 内有 `sync_to_async` 用于ORM。 +**方案**: +- `_handle_run_clustering` 提取同步核心函数 `_cluster_sync()`,worker直接调同步版 +- `_handle_extract_features` 用 `asgiref.sync.async_to_sync` 包装 + +### Issue 4 — 列类型靠列名而非数值 +**根因**: `type_classifier.py` 优先级: config→名称匹配→值检测。名称匹配(如 _ip→IPv4)过于宽泛。 +**方案**: 改为 config→值检测→名称匹配(仅值检测返回STRING时作为二次提示)。增加MAC地址、端口值模式。 + +### Issue 5 — 数据列初步分析 +**方案**: `scripts/column_survey.py`——standalone脚本,输出每列名称/dtype/唯一值数/空值率/前5非空样本。 + +### Issue 6 — src_ip和dst_ip都作为实体 +**根因**: `entity_detector.py` 只返回单 `recommended`。`_background_process` (views.py:367) 只用单列。 +**实际状态**: manual.html line 46 ` + #{{ run.id }} + {{ run.csv_glob }} + {{ run.entity_column|default:"-" }} + {{ run.status }} + + {% empty %} + 暂无数据。先去 上传 CSV。 + {% endfor %} + + + + {% endif %} + + + + +{% endblock %} diff --git a/templates/tianxuan/config.html b/templates/tianxuan/config.html new file mode 100644 index 0000000..9b27247 --- /dev/null +++ b/templates/tianxuan/config.html @@ -0,0 +1,174 @@ +{% extends 'base.html' %} +{% block title %}配置 - TLS Analyzer{% endblock %} +{% block content %} +
+

系统配置

+

修改配置后点击「保存配置」持久化到 config/config.yaml

+
+ + + {% csrf_token %} + + {# ── Server ── #} +
+

服务器

+
+
+ + +
+
+ + +
+
+ +
+
+
+ + {# ── Data ── #} +
+

数据

+
+
+ +
+
+ +
+
+
+ + {# ── Clustering ── #} +
+

聚类

+
+
+ + +
+
+ + +
+
+
+ + {# ── LLM ── #} +
+

LLM 连接

+
+ +
+
+ + +
+
+ + +
+
+ + +
+ + {# LLM connectivity test button + result area #} +
+ + +
+
+ + {# ── Save button ── #} +
+ +
+
+ +{% if saved %} + +{% endif %} + + + + +{% endblock %} \ No newline at end of file diff --git a/templates/tianxuan/globe.html b/templates/tianxuan/globe.html new file mode 100644 index 0000000..8301b58 --- /dev/null +++ b/templates/tianxuan/globe.html @@ -0,0 +1,235 @@ +{% extends 'base.html' %} +{% load static %} +{% block title %}TLS 流量地球 - 天璇{% endblock %} +{% block content %} +
+

TLS 流量地球可视化

+

三维地球上的流量弧线,显示源和目标之间的 TLS 连接

+

+ 0 条流量 | + 颜色: TLSv1.3 + TLSv1.2 + TLSv1.1/1.0 + 其他 + | 支持 hex 格式 (03 03 → TLSv1.2) +

+
+ +{% if all_runs %} +
+

选择数据运行

+
+
+ {% for r in all_runs %} + + {% endfor %} + +
+
+
+{% endif %} + +
+
+ + + 鼠标拖拽旋转 · 滚轮缩放 +
+
+ +
+
+
+ + + + +{% endblock %} diff --git a/templates/tianxuan/log_viewer.html b/templates/tianxuan/log_viewer.html new file mode 100644 index 0000000..4466218 --- /dev/null +++ b/templates/tianxuan/log_viewer.html @@ -0,0 +1,5 @@ +{% extends 'base.html' %}{% block content %} +

系统日志 (最近200行)

+
{% for line in lines %}{{ line }}{% endfor %}
+ +{% endblock %} diff --git a/templates/tianxuan/manual.html b/templates/tianxuan/manual.html new file mode 100644 index 0000000..686d528 --- /dev/null +++ b/templates/tianxuan/manual.html @@ -0,0 +1,145 @@ +{% extends 'base.html' %} +{% load query_helpers %} +{% block title %}手动分析 - 天璇{% endblock %} +{% block content %} +
+

手动分析 - 第 {{ step }} 步 / 共 3 步

+
+ +{% if step == 1 %} +
+

Step 1: 选择数据集

+

选择已上传且预处理完成的数据集:

+ {% if runs %} +
+ + + + + {% for run in runs %} + + + + + + + + + {% empty %} + + {% endfor %} + +
ID文件行数实体列状态
#{{ run.id }}{{ run.csv_glob }}{{ run.total_flows|default:"-" }}{{ run.entity_column|default:"-" }}{{ run.status }}
暂无已预处理的数据集。先去 上传页面 上传 CSV。
+ +
+ {% endif %} +
+{% endif %} + +{% if step == 2 %} +
+

Step 2: 配置聚类参数

+
+ + +
+ + + 按住 Ctrl 多选,留空则使用自动检测结果 +
+
+ + + 按住 Ctrl 多选,留空则自动选择前10个数值列。仅显示数值类型列。 +
+
+ + +
+
+ + +
+ ← 上一步 + +
+
+{% endif %} + +{% if step == 3 %} +
+

Step 3: 确认并运行

+

请确认以下配置,确认无误后点击"开始分析":

+ + + + + + +
数据集 ID#{{ request.GET.run_id }}
实体列{{ request.GET|getlist:"entity_columns"|default:"自动检测" }}
特征列{{ request.GET.feature_columns|default:"自动选择" }}
聚类算法{{ request.GET.algorithm }}
最小簇大小{{ request.GET.min_cluster_size }}
+
+ ← 上一步 + +
+ +
+ +{% endif %} +{% endblock %} diff --git a/templates/tianxuan/upload.html b/templates/tianxuan/upload.html new file mode 100644 index 0000000..b01306c --- /dev/null +++ b/templates/tianxuan/upload.html @@ -0,0 +1,125 @@ +{% extends 'base.html' %} +{% block title %}上传数据 - 天璇{% endblock %} +{% block content %} + +
+

上传 CSV 文件

+

支持 .csv 文件,多文件批量上传,自动解压 .zip

+
+ {% csrf_token %} +
+
📂
+

拖拽 CSV 文件到此处,或点击选择

+ +
+ +
+
+ +
+ +
+

已上传的数据集

+ {% if runs %} + + + + {% for run in runs %} + + + + + + + + {% endfor %} + +
ID文件行数状态操作
#{{ run.id }}{{ run.csv_glob }}{{ run.total_flows|default:"-" }}{{ run.status }} + {% if run.status == 'loading' or run.status == 'profiling' or run.status == 'aggregating' or run.status == 'clustering' or run.status == 'extracting' %} + + {% else %} + + {% endif %} +
+ {% else %} +

暂无已上传的数据集

+ {% endif %} +
+ + +{% endblock %} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_data_loader.py b/tests/test_data_loader.py new file mode 100644 index 0000000..a5398cf --- /dev/null +++ b/tests/test_data_loader.py @@ -0,0 +1,367 @@ +"""Tests for analysis.data_loader — BOM detection, schema validation, load/config.""" + +import csv +import os +import tempfile +from pathlib import Path + +import pytest +import polars as pl + +from analysis.data_loader import ( + _file_count, + _merge_schema_entries, + _resolve_encoding, + detect_bom, + load_config, + load_csv_directory, + validate_schemas, +) + + +# ═══════════════════════════════════════════════════════════════════════ +# BOM detection +# ═══════════════════════════════════════════════════════════════════════ + +class TestDetectBOM: + """detect_bom() should return the correct encoding string for each BOM.""" + + def test_utf8_bom(self): + """UTF-8 BOM (EF BB BF) → 'utf-8-sig'.""" + path = _write_bytes(b'\xef\xbb\xbfcol1,col2\n1,2\n') + try: + assert detect_bom(path) == 'utf-8-sig' + finally: + os.unlink(path) + + def test_utf16le_bom(self): + """UTF-16 LE BOM (FF FE) → 'utf-16le'.""" + path = _write_bytes(b'\xff\xfec\x00o\x00l\x001\x00,\x00c\x00o\x00l\x002\x00\n\x001\x00,\x002\x00\n') + try: + assert detect_bom(path) == 'utf-16le' + finally: + os.unlink(path) + + def test_utf16be_bom(self): + """UTF-16 BE BOM (FE FF) → 'utf-16be'.""" + path = _write_bytes(b'\xfe\xff\x00c\x00o\x00l\x001\x00,\x00c\x00o\x00l\x002\x00\n\x001\x00,\x002\x00\n') + try: + assert detect_bom(path) == 'utf-16be' + finally: + os.unlink(path) + + def test_no_bom(self): + """Plain ASCII/UTF-8 without BOM → 'utf-8'.""" + path = _write_bytes(b'col1,col2\n1,2\n') + try: + assert detect_bom(path) == 'utf-8' + finally: + os.unlink(path) + + def test_empty_file(self): + """Empty file → 'utf-8'.""" + path = _write_bytes(b'') + try: + assert detect_bom(path) == 'utf-8' + finally: + os.unlink(path) + + +# ═══════════════════════════════════════════════════════════════════════ +# Schema validation +# ═══════════════════════════════════════════════════════════════════════ + +class TestValidateSchemas: + """validate_schemas() must raise ValueError when column sets differ.""" + + def test_matching_schemas(self): + """Identical schemas → no exception.""" + schemas = [ + {':ips': 'Utf8', '8ack': 'Int64'}, + {':ips': 'Utf8', '8ack': 'Int64'}, + ] + validate_schemas(schemas) # should not raise + + def test_empty_list(self): + """Empty list → no exception.""" + validate_schemas([]) + + def test_schema_mismatch_missing_column(self): + """One file missing a column → ValueError.""" + schemas = [ + {':ips': 'Utf8', '8ack': 'Int64'}, + {':ips': 'Utf8'}, # missing '8ack' + ] + with pytest.raises(ValueError, match=r'missing columns'): + validate_schemas(schemas) + + def test_schema_mismatch_extra_column(self): + """One file has an extra column → ValueError.""" + schemas = [ + {':ips': 'Utf8'}, + {':ips': 'Utf8', 'extra_col': 'Int64'}, + ] + with pytest.raises(ValueError, match=r'extra columns'): + validate_schemas(schemas) + + def test_schema_mismatch_both_sides(self): + """Both missing and extra columns → ValueError with both messages.""" + schemas = [ + {'a': 'Int64', 'b': 'Utf8'}, + {'a': 'Int64', 'c': 'Float64'}, + ] + with pytest.raises(ValueError): + validate_schemas(schemas) + + +class TestMergeSchemaEntries: + """_merge_schema_entries() should merge without raising.""" + + def test_simple_merge(self): + merged, conflicts = _merge_schema_entries([ + {'a': 'Int64', 'b': 'Utf8'}, + {'a': 'Int64', 'c': 'Float64'}, + ]) + assert set(merged.keys()) == {'a', 'b', 'c'} + assert merged['a'] == 'Int64' + + def test_type_conflict(self): + """Different dtypes for same column → last wins, conflict recorded.""" + merged, conflicts = _merge_schema_entries([ + {'val': 'Int64'}, + {'val': 'Float64'}, + ]) + assert merged['val'] == 'Float64' + assert 'val' in conflicts + + +# ═══════════════════════════════════════════════════════════════════════ +# CSV loading +# ═══════════════════════════════════════════════════════════════════════ + +class TestLoadCSVDirectory: + """End-to-end load_csv_directory() tests with temporary CSV files.""" + + def test_basic_load(self): + """Single CSV → LazyFrame + correct schema + row/file counts.""" + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / 'test.csv' + _write_csv(path, [ + [':ips', ':ipd', '8ack'], + ['10.0.0.1', '10.0.0.2', '100'], + ['10.0.0.1', '10.0.0.3', '200'], + ['10.0.0.2', '10.0.0.4', '150'], + ]) + + lf, schema, row_count, file_count, memory_mb = load_csv_directory( + str(path), encoding='utf-8' + ) + + assert isinstance(lf, pl.LazyFrame) + assert ':ips' in schema + assert ':ipd' in schema + assert '8ack' in schema + assert file_count == 1 + assert row_count > 0 + assert memory_mb >= 0 + + # Verify data round-trip + df = lf.collect() + assert len(df) == 3 + assert set(df[':ips'].to_list()) == {'10.0.0.1', '10.0.0.2'} + + def test_multiple_files(self): + """Multiple CSVs with same schema → merged LazyFrame.""" + with tempfile.TemporaryDirectory() as tmpdir: + p1 = Path(tmpdir) / 'part1.csv' + p2 = Path(tmpdir) / 'part2.csv' + _write_csv(p1, [['x'], ['1'], ['2']]) + _write_csv(p2, [['x'], ['3'], ['4']]) + + glob_pattern = str(Path(tmpdir) / 'part*.csv') + lf, schema, row_count, file_count, memory_mb = load_csv_directory( + glob_pattern, encoding='utf-8' + ) + + assert file_count == 2 + df = lf.collect() + assert len(df) == 4 + assert sorted(df['x'].to_list()) == [1, 2, 3, 4] + + def test_no_files(self): + """No matching files → FileNotFoundError.""" + with pytest.raises(FileNotFoundError, match=r'No files match'): + load_csv_directory('__nonexistent_glarg_*.csv') + + def test_schema_mismatch_across_files_strict(self): + """Files with different column sets + schema_strict=True → ValueError.""" + with tempfile.TemporaryDirectory() as tmpdir: + _write_csv(Path(tmpdir) / 'a.csv', [['x', 'y'], ['1', '2']]) + _write_csv(Path(tmpdir) / 'b.csv', [['x'], ['3']]) + + with pytest.raises(ValueError, match=r'Schema mismatch'): + load_csv_directory(str(Path(tmpdir) / '*.csv'), schema_strict=True) + + def test_schema_mismatch_across_files_lenient(self): + """Files with different column sets + schema_strict=False → merge with nulls.""" + with tempfile.TemporaryDirectory() as tmpdir: + _write_csv(Path(tmpdir) / 'a.csv', [['x', 'y'], ['1', '2']]) + _write_csv(Path(tmpdir) / 'b.csv', [['x'], ['3']]) + + lf, schema, *_ = load_csv_directory(str(Path(tmpdir) / '*.csv')) + df = lf.collect() + assert len(df) == 2 + assert 'x' in df.columns + assert 'y' in df.columns + # Row from b.csv should have null for 'y' + assert df['y'].is_null().sum() == 1 + + def test_schema_override(self): + """schema_override forces a specific column dtype.""" + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / 'test.csv' + _write_csv(path, [['val'], ['123'], ['456']]) + + lf, schema, *_ = load_csv_directory( + str(path), encoding='utf-8', + schema_override={'val': 'Float64'}, + ) + + df = lf.collect() + assert df['val'].dtype == pl.Float64 + + def test_custom_delimiter(self): + """Tab-delimited file should parse correctly.""" + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / 'test.tsv' + with open(path, 'w', newline='') as f: + f.write('a\tb\n1\t2\n3\t4\n') + + lf, *_ = load_csv_directory(str(path), delimiter='\t') + df = lf.collect() + assert len(df) == 2 + assert df.columns == ['a', 'b'] + + def test_glob_with_subdirectory(self): + """Recursive glob should find files in subdirectories.""" + with tempfile.TemporaryDirectory() as tmpdir: + sub = Path(tmpdir) / 'sub' + sub.mkdir() + _write_csv(sub / 'nested.csv', [['x'], ['42']]) + + lf, schema, row_count, file_count, memory_mb = load_csv_directory( + str(Path(tmpdir) / '**' / '*.csv') + ) + assert file_count == 1 + assert row_count > 0 + + def test_latlon_with_plus_and_blank(self): + """CSV with lat/lon columns containing '+', '' loads without error + and the column is Float64 after cleaning.""" + with tempfile.TemporaryDirectory() as tmpdir: + path = Path(tmpdir) / 'latlon_test.csv' + with open(path, 'w', newline='') as f: + f.write('src_ip,latitude,longitude\n') + f.write('10.0.0.1,+,120.0\n') + f.write('10.0.0.2,35.5,\n') + f.write('10.0.0.3,-,\n') + f.write('10.0.0.4,40.0,121.5\n') + + lf, schema, *_ = load_csv_directory(str(path)) + df = lf.collect() + + # lat/lon columns should be Float64 in schema + assert 'latitude' in schema + assert 'longitude' in schema + assert 'latitude' in df.columns + assert 'longitude' in df.columns + + # Check dtypes are Float64 + assert df['latitude'].dtype == pl.Float64 + assert df['longitude'].dtype == pl.Float64 + + # Standalone '+' and blank should become null + assert df['latitude'].is_null().sum() == 2 # rows 0,2 + assert df['longitude'].is_null().sum() == 2 # rows 1,2 + + # Valid values survive + assert df['latitude'].drop_nulls().to_list() == [35.5, 40.0] + assert df['longitude'].drop_nulls().to_list() == [120.0, 121.5] + + +# ═══════════════════════════════════════════════════════════════════════ +# Config loading +# ═══════════════════════════════════════════════════════════════════════ + +class TestLoadConfig: + """load_config() YAML loading and error handling.""" + + def test_load_valid_yaml(self): + path = _write_yaml('key: value\nnested:\n inner: 42\n') + try: + config = load_config(path) + assert config['key'] == 'value' + assert config['nested']['inner'] == 42 + finally: + os.unlink(path) + + def test_load_empty_yaml(self): + path = _write_yaml('') + try: + config = load_config(path) + assert config == {} + finally: + os.unlink(path) + + def test_config_path_none(self): + assert load_config(None) == {} + + def test_config_not_found(self): + with pytest.raises(FileNotFoundError): + load_config('/__nonexistent__/config.yaml') + + +# ═══════════════════════════════════════════════════════════════════════ +# Utility functions +# ═══════════════════════════════════════════════════════════════════════ + +class TestResolveEncoding: + def test_bom_overrides_default(self): + assert _resolve_encoding('utf-8', 'utf-16le') == 'utf-16le' + + def test_no_bom_uses_default(self): + # Polars >= 1.0 uses 'utf8' (not 'utf-8') + assert _resolve_encoding('utf-8', 'utf-8') == 'utf8' + assert _resolve_encoding('latin-1', 'utf-8') == 'latin-1' + + +class TestFileCount: + def test_count(self): + assert _file_count(['a', 'b', 'c']) == 3 + assert _file_count([]) == 0 + + +# ═══════════════════════════════════════════════════════════════════════ +# Helpers +# ═══════════════════════════════════════════════════════════════════════ + +def _write_csv(path: Path, rows: list[list[str]]) -> None: + """Write rows as a CSV file.""" + with open(path, 'w', newline='') as f: + writer = csv.writer(f) + for row in rows: + writer.writerow(row) + + +def _write_bytes(data: bytes) -> str: + """Write raw bytes to a temp file; return the path string.""" + with tempfile.NamedTemporaryFile(suffix='.csv', delete=False) as f: + f.write(data) + return f.name + + +def _write_yaml(content: str) -> str: + """Write YAML string to a temp file; return the path string.""" + with tempfile.NamedTemporaryFile(suffix='.yaml', mode='w', delete=False) as f: + f.write(content) + return f.name diff --git a/tests/test_e2e.py b/tests/test_e2e.py new file mode 100644 index 0000000..b188c17 --- /dev/null +++ b/tests/test_e2e.py @@ -0,0 +1,365 @@ +"""End-to-end integration test for the full TLS analysis pipeline. + +Pipeline + 1. Create synthetic TLS flow CSV with known entity clusters. + 2. Load via ``load_csv_directory``. + 3. Store in ``SessionStore``. + 4. Auto-detect entity column (``detect_entity_column``). + 5. Aggregate flows → entity profiles (``aggregate_by_entity``). + 6. Run HDBSCAN clustering on aggregated numeric features. + 7. Extract per-cluster distinguishing features. + +All data is synthetic, deterministic (seed=42), and self-contained — +no external files or network access required. +""" + +import tempfile +from pathlib import Path + +import numpy as np +import polars as pl +import pytest + +from analysis.data_loader import load_csv_directory +from analysis.entity_detector import detect_entity_column +from analysis.entity_aggregator import aggregate_by_entity +from analysis.session_store import SessionStore + +# ═══════════════════════════════════════════════════════════════════════ +# Fixtures +# ═══════════════════════════════════════════════════════════════════════ + +@pytest.fixture(autouse=True) +def clean_store(): + """Reset the SessionStore singleton before and after each test.""" + store = SessionStore() + store.drop_all() + yield + store.drop_all() + + +# ═══════════════════════════════════════════════════════════════════════ +# Synthetic data generator +# ═══════════════════════════════════════════════════════════════════════ + +def create_synthetic_flows(seed: int = 42) -> pl.DataFrame: + """Generate deterministic TLS flow data with three known entity groups. + + Uses the user's exact column naming convention only. + + Returns + ------- + pl.DataFrame + ~1000 rows with columns. + Entities (``:ips``) are partitioned into three behavioural + groups that HDBSCAN should recover after aggregation. + """ + rng = np.random.default_rng(seed) + + # ── Entity profiles ────────────────────────────────────────────── + # Group A: 10 high-volume entities (50 flows each) + group_a = [ + { + ':ips': f'10.0.0.{i+1}', + 'n_flows': 50, + '8ack_mu': 50_000, '8ack_sigma': 5_000, + '4dur_mu': 30.0, '4dur_sigma': 5.0, + 'dst_range': (100, 150), + '0ver': 'TLSv1.3', + 'cipher_suite': 'TLS_AES_256_GCM_SHA384', + } + for i in range(10) + ] + + # Group B: 15 medium-volume entities (20 flows each) + group_b = [ + { + ':ips': f'10.0.0.{i+11}', + 'n_flows': 20, + '8ack_mu': 5_000, '8ack_sigma': 1_000, + '4dur_mu': 15.0, '4dur_sigma': 3.0, + 'dst_range': (150, 200), + '0ver': 'TLSv1.2', + 'cipher_suite': 'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256', + } + for i in range(15) + ] + + # Group C: 25 low-volume entities (6 flows each) + group_c = [ + { + ':ips': f'10.0.0.{i+26}', + 'n_flows': 6, + '8ack_mu': 500, '8ack_sigma': 100, + '4dur_mu': 5.0, '4dur_sigma': 1.0, + 'dst_range': (200, 250), + '0ver': 'TLSv1.2', + 'cipher_suite': 'TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256', + } + for i in range(25) + ] + + entities = group_a + group_b + group_c + # Total flows: 10×50 + 15×20 + 25×6 = 500 + 300 + 150 = 950 + + # ── Generate flow rows ──────────────────────────────────────────── + rows: list[dict] = [] + dst_ports = [80, 443, 8080, 8443] + protos = ['TCP', 'UDP'] + proto_weights = [0.9, 0.1] + + for ent in entities: + lo, hi = ent['dst_range'] + for _ in range(ent['n_flows']): + dst_idx = int(rng.integers(lo, hi)) + row = { + ':ips': ent[':ips'], + ':ipd': f"10.0.{dst_idx >> 8}.{dst_idx & 0xff}", + ':prd': int(rng.choice(dst_ports)), + '8ack': int(max(0, rng.normal(ent['8ack_mu'], + ent['8ack_sigma']))), + '4dur': round(max(0.1, abs(rng.normal(ent['4dur_mu'], + ent['4dur_sigma']))), 2), + '1ipp': str(rng.choice(protos, p=proto_weights)), + '0ver': ent['0ver'], + 'cipher_suite': ent['cipher_suite'], + 'snam': f"host{rng.integers(1, 200)}.example.com", + 'timestamp': ( + f"2024-06-15 " + f"{rng.integers(0, 23):02d}:" + f"{rng.integers(0, 59):02d}:" + f"{rng.integers(0, 59):02d}" + ), + } + rows.append(row) + + return pl.DataFrame(rows) + + +# ═══════════════════════════════════════════════════════════════════════ +# Pipeline tests +# ═══════════════════════════════════════════════════════════════════════ + +class TestFullPipeline: + """End-to-end: synthetic CSV → load → entity detect → aggregate → cluster.""" + + def _write_csv(self, df: pl.DataFrame, tmpdir: str) -> str: + """Write a Polars DataFrame to a temp CSV; return the file path.""" + path = Path(tmpdir) / 'synthetic_flows.csv' + df.write_csv(path) + return str(path) + + def test_full_pipeline(self): + """Smoke-test the full pipeline — every step produces valid output.""" + with tempfile.TemporaryDirectory() as tmpdir: + # ── Step 1: Create synthetic data ───────────────────────────── + df_flows = create_synthetic_flows(seed=42) + assert len(df_flows) > 0, 'Synthetic data should not be empty' + + csv_path = self._write_csv(df_flows, tmpdir) + + # ── Step 2: Load data ───────────────────────────────────────── + lf, schema, row_count, file_count, memory_mb = load_csv_directory( + csv_path, encoding='utf-8', + ) + assert isinstance(lf, pl.LazyFrame), 'Expected LazyFrame' + assert file_count == 1 + assert row_count > 0 + assert memory_mb >= 0 + assert ':ips' in schema + + # ── Step 3: Store in session ────────────────────────────────── + store = SessionStore() + store.store_dataset( + 'e2e_ds', lf, schema=schema, + metadata={'row_count': row_count, 'csv_glob': csv_path}, + ) + + # ── Step 4: Entity detection ────────────────────────────────── + det_result = detect_entity_column('e2e_ds') + assert 'error' not in det_result, f'detect_entity_column failed: {det_result.get("error")}' + # :ips is the catch-all entity column (strings with good unique ratio) + assert det_result['recommended'] in (':ips', ':ipd', 'snam'), ( + f'Expected :ips, :ipd or snam, got {det_result["recommended"]}' + ) + assert len(det_result['candidates']) > 0 + + # Verify recommended_multi exists + assert 'recommended_multi' in det_result, 'recommended_multi field missing' + assert len(det_result['recommended_multi']) > 0, 'recommended_multi is empty' + + # ── Step 5: Entity aggregation ──────────────────────────────── + agg_lf, feature_cols = aggregate_by_entity(lf, ':ips', schema) + df_agg = agg_lf.collect() + + # Should be exactly 50 entities (10 + 15 + 25) + entity_count = len(df_agg) + assert entity_count == 50, f'Expected 50 entities, got {entity_count}' + + # Verify per-entity flow counts (sort by flow_count desc so groups + # are contiguous: A=50, B=20, C=6) + df_sorted = df_agg.sort('flow_count', descending=True) + # Group A: 10 entities × 50 flows + for i in range(10): + assert df_sorted['flow_count'][i] == 50, ( + f'Row {i}: expected 50, got {df_sorted["flow_count"][i]}' + ) + # Group B: 15 entities × 20 flows + for i in range(10, 25): + assert df_sorted['flow_count'][i] == 20, ( + f'Row {i}: expected 20, got {df_sorted["flow_count"][i]}' + ) + # Group C: 25 entities × 6 flows + for i in range(25, 50): + assert df_sorted['flow_count'][i] == 6, ( + f'Row {i}: expected 6, got {df_sorted["flow_count"][i]}' + ) + + # Core feature columns are present + for col in ('flow_count', 'total_bytes_sent', + 'avg_duration', 'unique_dst_ips', 'unique_dst_ports', + 'unique_tls_versions', 'unique_protocols', + 'first_seen', 'last_seen'): + assert col in feature_cols, f'Missing feature column: {col}' + + # ── Step 6: Clustering ──────────────────────────────────────── + numeric_cols = [ + 'flow_count', 'total_bytes_sent', + 'avg_duration', 'unique_dst_ips', 'unique_dst_ports', + ] + available_cols = [c for c in numeric_cols if c in df_agg.columns] + + data = df_agg.select(available_cols).to_numpy() + assert data.shape[0] == 50, 'All 50 entities should have features' + + # Standard-scale + HDBSCAN + from sklearn.preprocessing import StandardScaler + from sklearn.cluster import HDBSCAN + + data_scaled = StandardScaler().fit_transform(data) + clusterer = HDBSCAN(min_cluster_size=5, metric='euclidean') + labels = clusterer.fit_predict(data_scaled) + + n_clusters = len(set(labels) - {-1}) + assert n_clusters > 0, ( + f'HDBSCAN should find ≥1 cluster, found {n_clusters} ' + f'(labels={set(labels)})' + ) + + noise_count = int((labels == -1).sum()) + # At most 40% noise allowed (we have well-separated groups) + noise_ratio = noise_count / len(labels) + assert noise_ratio < 0.40, ( + f'Noise ratio too high: {noise_ratio:.2%} ' + f'({noise_count}/{len(labels)})' + ) + + # ── Step 7: Feature extraction ──────────────────────────────── + unique_labels = sorted(set(l for l in labels if l >= 0)) + assert len(unique_labels) > 0, 'Need at least one valid cluster' + + # Compute per-cluster distinguishing features using z-score + features = [] + for label in unique_labels: + mask = labels == label + cluster_data = data[mask] + for col_idx, col_name in enumerate(available_cols): + global_mean = float(data[:, col_idx].mean()) + global_std = float(data[:, col_idx].std()) or 1.0 + cluster_mean = float(cluster_data[:, col_idx].mean()) + z = (cluster_mean - global_mean) / global_std + features.append({ + 'cluster': int(label), + 'feature': col_name, + 'z_score': round(z, 4), + }) + + assert len(features) > 0, 'Expected at least one extracted feature' + # Each cluster should have features for all numeric columns + assert len(features) >= len(unique_labels) * len(available_cols) + + # ── Step 8: Subnet aggregation function ──────────────────────── + from analysis.entity_aggregator import ip_to_subnet + assert callable(ip_to_subnet) + assert ip_to_subnet('10.0.1.15', 24) == '10.0.1.0/24' + + # Note: SessionStore is cleaned up by the ``clean_store`` fixture + + def test_synthetic_data_shape(self): + """Verify the synthetic data generator produces expected shapes.""" + df = create_synthetic_flows(seed=42) + assert len(df.columns) == 10, f'Expected 10 columns, got {len(df.columns)}' + # Total: 10×50 + 15×20 + 25×6 = 950 + assert len(df) == 950, f'Expected 950 rows, got {len(df)}' + + # Verify unique entity count + n_entities = df[':ips'].n_unique() + assert n_entities == 50, f'Expected 50 unique :ips, got {n_entities}' + + # Group A should have TLSv1.3 + group_a_ips = [f'10.0.0.{i+1}' for i in range(10)] + group_a_tls = df.filter( + pl.col(':ips').is_in(group_a_ips) + )['0ver'].unique().to_list() + assert group_a_tls == ['TLSv1.3'] + + # Group C should have CHACHA20 cipher + group_c_ips = [f'10.0.0.{i+26}' for i in range(25)] + group_c_cipher = df.filter( + pl.col(':ips').is_in(group_c_ips) + )['cipher_suite'].unique().to_list() + assert any('CHACHA20' in c for c in group_c_cipher) + + +class TestPipelineEdgeCases: + """Edge cases for the analysis pipeline.""" + + def test_empty_dataset_rejected(self): + """Empty dataset stored → detect_entity_column returns error.""" + store = SessionStore() + empty = pl.DataFrame({':ips': pl.Series([], dtype=pl.Utf8)}) + schema = {} + store.store_dataset( + 'empty_ds', empty.lazy(), schema=schema, + metadata={'row_count': 0}, + ) + result = detect_entity_column('empty_ds') + # May fail or return empty candidates; either is acceptable + # but it should not crash with an unhandled exception. + assert 'error' in result or result['recommended'] == '' + + def test_single_entity_clustering(self): + """Single entity → aggregation works but clustering may not split.""" + data = pl.DataFrame({ + ':ips': ['10.0.0.1'] * 20, + ':ipd': ['10.0.0.2'] * 20, + '8ack': [100] * 20, + '4dur': [1.0] * 20, + '1ipp': ['TCP'] * 20, + '0ver': ['TLSv1.2'] * 20, + 'cipher_suite': ['AES'] * 20, + 'snam': ['host1.example.com'] * 20, + 'timestamp': ['2024-01-01 00:00:00'] * 20, + ':prd': [443] * 20, + }) + schema = {name: str(dtype) for name, dtype in zip(data.columns, data.dtypes)} + lf = data.lazy() + agg_lf, _ = aggregate_by_entity(lf, ':ips', schema) + df_agg = agg_lf.collect() + + assert len(df_agg) == 1 + assert df_agg['flow_count'][0] == 20 + + def test_aggregate_preserves_schema_types(self): + """Aggregated columns should have expected Polars dtypes.""" + data = pl.DataFrame({ + ':ips': ['10.0.0.1'] * 10, + '8ack': [100] * 10, + '4dur': [1.5] * 10, + }) + schema = {name: str(dtype) for name, dtype in zip(data.columns, data.dtypes)} + lf = data.lazy() + agg_lf, _ = aggregate_by_entity(lf, ':ips', schema) + df = agg_lf.collect() + + assert df['flow_count'].dtype == pl.UInt32 or df['flow_count'].dtype == pl.Int64 diff --git a/tests/test_entity.py b/tests/test_entity.py new file mode 100644 index 0000000..838f843 --- /dev/null +++ b/tests/test_entity.py @@ -0,0 +1,322 @@ +"""Tests for entity detection (entity_detector) and aggregation (entity_aggregator). + +Every test isolates the SessionStore singleton via the ``clean_store`` fixture. +""" + +import pytest +import polars as pl + +from analysis.session_store import SessionStore +from analysis.entity_detector import _score_name, detect_entity_column +from analysis.entity_aggregator import ( + _find_column, + aggregate_by_entity, + build_aggregation_params, + ip_to_subnet, +) + + +# ═══════════════════════════════════════════════════════════════════════ +# Fixtures +# ═══════════════════════════════════════════════════════════════════════ + +@pytest.fixture(autouse=True) +def clean_store(): + """Reset the SessionStore singleton before and after each test.""" + store = SessionStore() + store.drop_all() + yield + store.drop_all() + + +def _store_dataset( + df: pl.DataFrame, + dataset_id: str = 'test_ds', +) -> str: + """Helper: store a DataFrame as a dataset in the session store. + + Returns the dataset_id. + """ + store = SessionStore() + schema = {name: str(dtype) for name, dtype in zip(df.columns, df.dtypes)} + store.store_dataset( + dataset_id=dataset_id, + lazyframe=df.lazy(), + schema=schema, + metadata={'row_count': len(df)}, + ) + return dataset_id + + +# ═══════════════════════════════════════════════════════════════════════ +# _score_name (unit-level) +# ═══════════════════════════════════════════════════════════════════════ + +class TestScoreName: + """_score_name() scores column names against entity keywords.""" + + def test_exact_match(self): + """Exact keyword match → 3.0.""" + score, kw = _score_name(':ips') + assert score == 3.0 + assert kw == ':ips' + + def test_suffix_match(self): + """Suffix match via _ separator → 3.0.""" + score, kw = _score_name('my_snam') + assert score == 3.0 + assert kw == 'snam' + + def test_prefix_match(self): + """Prefix match via _ separator → 3.0.""" + score, kw = _score_name('snam_value') + assert score == 3.0 + assert kw == 'snam' + + def test_substring_not_matched(self): + """Substring-only match now returns 0.0 (exact matching only).""" + score, kw = _score_name('myipaddr') + assert score == 0.0 + assert kw == '' + + def test_no_match(self): + score, kw = _score_name('random_column_xyz') + assert score == 0.0 + assert kw == '' + + def test_hyphen_normalised(self): + """Hyphen normalised to underscore for matching.""" + score, kw = _score_name('cipher-suite') + assert score == 3.0 + assert kw == 'cipher_suite' + + +# ═══════════════════════════════════════════════════════════════════════ +# detect_entity_column (integration-level) +# ═══════════════════════════════════════════════════════════════════════ + +class TestDetectEntityColumn: + """detect_entity_column() should find entity columns from stored data.""" + + def test_detect_ips(self): + """Dataset with ``:ips`` → recommended is ``:ips``.""" + data = pl.DataFrame({ + ':ips': [f'10.0.0.{i}' for i in range(1, 51)] * 10, + ':ipd': [f'10.0.1.{i}' for i in range(1, 501)], + '8ack': [100] * 500, + }) + ds_id = _store_dataset(data) + result = detect_entity_column(ds_id) + + assert result['recommended'] == ':ips' + assert len(result['candidates']) > 0 + assert result['candidates'][0]['column'] == ':ips' + assert result['total_rows'] == 500 + assert result['scored_columns'] >= 1 + + def test_detect_snam(self): + """Datasets with ``snam`` but no IP column → snam is recommended.""" + data = pl.DataFrame({ + 'snam': [f'host{i}.example.com' for i in range(100)], + '4dur': [float(i) for i in range(100)], + }) + ds_id = _store_dataset(data) + result = detect_entity_column(ds_id) + + assert result['recommended'] == 'snam' + assert result['candidates'][0]['matched_keyword'] == 'snam' + + def test_no_entity_column(self): + """No entity keywords matched → candidates still found but matched_keyword empty.""" + data = pl.DataFrame({ + 'col_a': [str(i) for i in range(100)], + 'col_b': [str(i * 2) for i in range(100)], + 'value_f64': [float(i) for i in range(100)], + }) + ds_id = _store_dataset(data) + result = detect_entity_column(ds_id) + + # String columns are candidates (detector scores on uniqueness, not just keywords) + assert len(result['candidates']) >= 2 + assert all(c['matched_keyword'] == '' for c in result['candidates']) + # recommended will be the highest-scoring string column + assert result['recommended'] in ('col_a', 'col_b') + + def test_all_numeric_no_strings(self): + """No string/categorical columns → no candidates.""" + data = pl.DataFrame({ + 'value': list(range(100)), + 'flag': [0, 1] * 50, + }) + ds_id = _store_dataset(data) + result = detect_entity_column(ds_id) + + assert result['recommended'] == '' + assert len(result['candidates']) == 0 + + def test_dataset_not_found(self): + """Unknown dataset_id → error response.""" + result = detect_entity_column('nonexistent_ds') + assert 'error' in result + assert result['recommended'] == '' + + +# ═══════════════════════════════════════════════════════════════════════ +# _find_column (helper for aggregation) +# ═══════════════════════════════════════════════════════════════════════ + +class TestFindColumn: + """_find_column() matches keywords against schema column names.""" + + ACK_KEYWORDS = ('8ack',) + + def test_exact_match(self): + schema = {'8ack': 'Int64', 'other': 'Utf8'} + col = _find_column(schema, self.ACK_KEYWORDS) + assert col == '8ack' + + def test_no_match(self): + schema = {'something_else': 'Int64'} + col = _find_column(schema, self.ACK_KEYWORDS) + assert col is None + + +# ═══════════════════════════════════════════════════════════════════════ +# aggregate_by_entity +# ═══════════════════════════════════════════════════════════════════════ + +class TestAggregateByEntity: + """aggregate_by_entity() groups flows and computes per-entity features.""" + + def test_two_entities(self): + """50 flows across 2 entities → 2 aggregated rows with correct counts.""" + data = pl.DataFrame({ + ':ips': ['10.0.0.1'] * 20 + ['10.0.0.2'] * 30, + ':ipd': ['10.0.1.1'] * 10 + ['10.0.1.2'] * 10 + + ['10.0.1.3'] * 30, + ':prd': [80] * 20 + [443] * 30, + '8ack': [100] * 20 + [200] * 30, + '4dur': [1.0] * 20 + [2.0] * 30, + '0ver': ['TLSv1.2'] * 20 + ['TLSv1.3'] * 30, + 'cipher_suite': ['AES'] * 20 + ['CHACHA20'] * 30, + '1ipp': ['TCP'] * 50, + 'timestamp': ['2024-01-01 00:00:00'] * 50, + }) + schema = {name: str(dtype) for name, dtype in zip(data.columns, data.dtypes)} + lf = data.lazy() + + agg_lf, feature_cols = aggregate_by_entity(lf, ':ips', schema) + df = agg_lf.collect() + + # Two entities + assert len(df) == 2 + + # Flow counts + df_sorted = df.sort(':ips') + assert df_sorted['flow_count'][0] == 20 # 10.0.0.1 + assert df_sorted['flow_count'][1] == 30 # 10.0.0.2 + + # Bytes — each column is now perfectly aligned with entity groups + assert df_sorted['total_bytes_sent'][0] == 20 * 100 + assert df_sorted['total_bytes_sent'][1] == 30 * 200 + + # Core feature columns present + assert 'flow_count' in feature_cols + assert 'total_bytes_sent' in feature_cols + assert 'avg_duration' in feature_cols + assert 'unique_dst_ips' in feature_cols + assert 'unique_dst_ports' in feature_cols + assert 'unique_protocols' in feature_cols + assert 'first_seen' in feature_cols + assert 'last_seen' in feature_cols + + def test_single_entity(self): + """Single entity → single aggregated row.""" + data = pl.DataFrame({ + ':ips': ['10.0.0.1'] * 5, + '8ack': [100] * 5, + ':ipd': ['10.0.0.2'] * 5, + }) + schema = {name: str(dtype) for name, dtype in zip(data.columns, data.dtypes)} + lf = data.lazy() + agg_lf, _ = aggregate_by_entity(lf, ':ips', schema) + df = agg_lf.collect() + assert len(df) == 1 + assert df['flow_count'][0] == 5 + + def test_nonexistent_entity_column(self): + """Unknown entity column → ValueError.""" + data = pl.DataFrame({'a': [1, 2, 3]}) + schema = {'a': 'Int64'} + lf = data.lazy() + with pytest.raises(ValueError, match=r"Entity column\(s\) '\[\'xyz\'\]'"): + aggregate_by_entity(lf, 'xyz', schema) + + def test_schema_with_missing_columns(self): + """Aggregation still works when some optional columns are missing.""" + data = pl.DataFrame({ + ':ips': ['10.0.0.1'] * 3 + ['10.0.0.2'] * 3, + ':ipd': ['10.0.0.3'] * 6, + # No 8ack, 4dur, etc. + }) + schema = {name: str(dtype) for name, dtype in zip(data.columns, data.dtypes)} + lf = data.lazy() + agg_lf, feature_cols = aggregate_by_entity(lf, ':ips', schema) + + df = agg_lf.collect() + assert len(df) == 2 + assert df['flow_count'][0] == 3 + # Missing features should have None values + assert df['total_bytes_sent'][0] is None + assert 'flow_count' in feature_cols + assert 'unique_dst_ips' in feature_cols + + +# ═══════════════════════════════════════════════════════════════════════ +# build_aggregation_params +# ═══════════════════════════════════════════════════════════════════════ + +class TestBuildAggregationParams: + """build_aggregation_params() builds correct expressions per schema.""" + + def test_flow_count_always_present(self): + params = build_aggregation_params({'a': 'Int64'}) + names = [p[0] for p in params] + assert 'flow_count' in names + + def test_ack_detected(self): + params = build_aggregation_params({'8ack': 'Int64'}) + names = [p[0] for p in params] + assert 'total_bytes_sent' in names + + def test_ack_not_detected(self): + params = build_aggregation_params({'x': 'Int64'}) + param_map = dict(params) + assert param_map['total_bytes_sent'] is not None # still has a fallback expr + # The value should be a Polars literal (None) + names = [p[0] for p in params] + assert 'total_bytes_sent' in names + + +# ═══════════════════════════════════════════════════════════════════════ +# ip_to_subnet (IP subnet mask conversion) +# ═══════════════════════════════════════════════════════════════════════ + +class TestIpToSubnet: + """ip_to_subnet() converts IP strings to subnet prefixes.""" + + def test_ip_to_subnet_24(self): + """ip_to_subnet with /24 mask.""" + assert ip_to_subnet('10.0.1.15', 24) == '10.0.1.0/24' + assert ip_to_subnet('192.168.1.255', 24) == '192.168.1.0/24' + + def test_ip_to_subnet_28(self): + """ip_to_subnet with /28 mask.""" + assert ip_to_subnet('10.0.1.15', 28) == '10.0.1.0/28' + assert ip_to_subnet('10.0.1.250', 28) == '10.0.1.240/28' + + def test_ip_to_subnet_invalid(self): + """ip_to_subnet with invalid inputs.""" + assert ip_to_subnet(None, 24) is None + assert ip_to_subnet('', 24) == '' + assert ip_to_subnet('not_an_ip', 24) == 'not_an_ip' diff --git a/tests/test_type_classifier.py b/tests/test_type_classifier.py new file mode 100644 index 0000000..d9df48f --- /dev/null +++ b/tests/test_type_classifier.py @@ -0,0 +1,421 @@ +"""Tests for the data type classifier engine (type_classifier) and IP +clustering utilities (ip_clustering). + +Covers: INT, FLOAT, ENUM, HEX, URL, IPv4, BOOL_ENUM detection via both +name-based and value-based heuristics, config override, schema-level +classification, and ``/24`` subnet grouping. +""" +import pytest +import polars as pl + +from analysis.type_classifier import ( + DataType, + classify_column, + classify_schema, + HEX_PATTERN, + URL_PATTERN, + IPv4_PATTERN, + BOOL_ENUM_VALUES, + LAT_LON_NAMES, +) +from analysis.ip_clustering import ip_to_int, get_subnet, cluster_ips + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — INT +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyInt: + def test_int_series(self): + s = pl.Series('bytes_sent', [100, 200, 300]) + assert classify_column('bytes_sent', s) == DataType.INT + + def test_unsigned_int(self): + s = pl.Series('packets', [0, 1, 255, 65535]) + assert classify_column('packets', s) == DataType.INT + + def test_small_int64(self): + s = pl.Series('count', [1, 2, 3], dtype=pl.Int64) + assert classify_column('count', s) == DataType.INT + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — FLOAT +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyFloat: + def test_float_series(self): + s = pl.Series('duration', [1.5, 2.7, 0.3]) + assert classify_column('duration', s) == DataType.FLOAT + + def test_float_dtype(self): + s = pl.Series('score', [0.1, 0.2], dtype=pl.Float32) + assert classify_column('score', s) == DataType.FLOAT + + def test_float_with_ints(self): + """Values look like ints but dtype is float → still FLOAT.""" + s = pl.Series('ratio', [1.0, 2.0, 3.0]) + assert classify_column('ratio', s) == DataType.FLOAT + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — ENUM (< 20 unique values) +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyEnum: + def test_few_unique_strings(self): + s = pl.Series('tls_version', ['TLSv1.2', 'TLSv1.3', 'TLSv1.2']) + assert classify_column('tls_version', s) == DataType.ENUM + + def test_exactly_19_unique(self): + vals = [str(i) for i in range(19)] + s = pl.Series('version_code', vals * 2) + assert classify_column('version_code', s) == DataType.ENUM + + def test_many_unique_strings(self): + vals = [f'str_{i}' for i in range(50)] + s = pl.Series('desc', vals) + assert classify_column('desc', s) == DataType.STRING + + def test_name_version_hint(self): + """Column name contains 'version' → name heuristic returns ENUM.""" + s = pl.Series('tls_ver', ['1.2', '1.3', '1.2']) + assert classify_column('tls_ver', s) == DataType.ENUM + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — HEX ("0e a6 3f" format) +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyHex: + def test_single_hex_pair(self): + s = pl.Series('cipher', ['0e', 'a6', '3f']) + assert classify_column('cipher', s) == DataType.HEX + + def test_multiple_hex_pairs(self): + s = pl.Series('cipher_suite', ['0e a6 3f', 'ab cd ef 01']) + assert classify_column('cipher_suite', s) == DataType.HEX + + def test_name_cipher_hint(self): + """Column name containing 'cipher' triggers name heuristic.""" + s = pl.Series('tls_cipher', ['ab cd', 'ef 01']) + assert classify_column('tls_cipher', s) == DataType.HEX + + def test_name_suite_hint(self): + s = pl.Series('suite_id', ['00 01', '00 02']) + assert classify_column('suite_id', s) == DataType.HEX + + def test_invalid_hex_not_matched(self): + """Not all values are valid hex pairs → should not be HEX.""" + s = pl.Series('x', ['0e a6', 'zz yy']) + result = classify_column('x', s) + assert result != DataType.HEX + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — URL +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyUrl: + def test_https(self): + s = pl.Series('dst_url', ['https://example.com', 'https://test.org']) + assert classify_column('dst_url', s) == DataType.URL + + def test_http(self): + s = pl.Series('url', ['http://example.com/path', 'http://test.org']) + assert classify_column('url', s) == DataType.URL + + def test_name_url_hint(self): + s = pl.Series('request_uri', ['https://api.example.com/v1']) + assert classify_column('request_uri', s) == DataType.URL + + def test_not_url(self): + """Plain domain names without http:// should not be URL.""" + s = pl.Series('domain', ['example.com', 'test.org']) + assert classify_column('domain', s) != DataType.URL + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — IPv4 +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyIPv4: + def test_ip_addresses(self): + s = pl.Series('ip', ['192.168.1.1', '10.0.0.1', '172.16.0.1']) + assert classify_column('ip', s) == DataType.IPv4 + + def test_ip_in_name(self): + s = pl.Series('src_ip', ['10.0.0.1', '10.0.0.2']) + # Name-based heuristic triggers first + assert classify_column('src_ip', s) == DataType.IPv4 + + def test_addr_suffix(self): + s = pl.Series('destination_addr', ['1.2.3.4', '5.6.7.8']) + assert classify_column('destination_addr', s) == DataType.IPv4 + + def test_invalid_octet_range(self): + """Octet > 255 → should not match IPv4 via values.""" + s = pl.Series('x', ['192.168.1.256', '10.0.0.1']) + result = classify_column('x', s) + assert result != DataType.IPv4 # 256 invalid, may fall through + + def test_ip_plus_normal_strings(self): + """Mix of IPs and non-IPs → not IPv4.""" + s = pl.Series('x', ['192.168.1.1', 'hello']) + result = classify_column('x', s) + assert result != DataType.IPv4 + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — BOOL_ENUM (+, '', true/false, etc.) +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyBoolEnum: + def test_plus_and_blank(self): + s = pl.Series('flag', ['+', '', '+', ' ']) + assert classify_column('flag', s) == DataType.BOOL_ENUM + + def test_true_false(self): + s = pl.Series('active', ['true', 'false', 'true']) + assert classify_column('active', s) == DataType.BOOL_ENUM + + def test_yes_no(self): + s = pl.Series('enabled', ['yes', 'no', 'yes']) + assert classify_column('enabled', s) == DataType.BOOL_ENUM + + def test_01_strings(self): + s = pl.Series('ok', ['0', '1', '0']) + assert classify_column('ok', s) == DataType.BOOL_ENUM + + def test_boolean_dtype(self): + """Native pl.Boolean dtype → BOOL_ENUM.""" + s = pl.Series('flag', [True, False, True]) + assert classify_column('flag', s) == DataType.BOOL_ENUM + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — LAT_LON (name-based) +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyLatLon: + def test_lat_column(self): + s = pl.Series('lat', [35.0, 36.5]) + assert classify_column('lat', s) == DataType.LAT_LON + + def test_latitude_column(self): + s = pl.Series('latitude', [40.0, 41.0]) + assert classify_column('latitude', s) == DataType.LAT_LON + + def test_lon_column(self): + s = pl.Series('lon', [120.0, 121.5]) + assert classify_column('lon', s) == DataType.LAT_LON + + def test_longitude_column(self): + s = pl.Series('longitude', [122.0, 123.0]) + assert classify_column('longitude', s) == DataType.LAT_LON + + def test_src_latitude_column(self): + """Column name src_latitude → LAT_LON via prefix heuristic.""" + s = pl.Series('src_latitude', [35.0, 36.0, 37.0]) + assert classify_column('src_latitude', s) == DataType.LAT_LON + + def test_dst_longitude_column(self): + """Column name dst_longitude → LAT_LON via prefix heuristic.""" + s = pl.Series('dst_longitude', [120.0, 121.0, 122.0]) + assert classify_column('dst_longitude', s) == DataType.LAT_LON + + def test_value_based_latlon_strings(self): + """String column with mix of '+', '', and valid floats → LAT_LON.""" + s = pl.Series('coord', ['+', '', '12.345', '-45.678', '90.0']) + assert classify_column('coord', s) == DataType.LAT_LON + + def test_value_based_latlon_rejects_out_of_range(self): + """String column with values > 180 → not LAT_LON.""" + s = pl.Series('coord', ['200', '300', '400']) + result = classify_column('coord', s) + assert result != DataType.LAT_LON + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — Config override (highest priority) +# ═══════════════════════════════════════════════════════════════════════ + +class TestConfigOverride: + def test_config_overrides_value(self): + """config_type='int' forces INT even if values are float.""" + s = pl.Series('anything', [1.5, 2.5, 3.5]) + assert classify_column('anything', s, config_type='int') == DataType.INT + + def test_config_overrides_name(self): + """config_type='string' forces STRING even if name matches IP.""" + s = pl.Series('src_ip', ['192.168.1.1', '10.0.0.1']) + assert classify_column('src_ip', s, config_type='string') == DataType.STRING + + def test_config_type_case_insensitive(self): + s = pl.Series('x', [1, 2]) + assert classify_column('x', s, config_type='FLOAT') == DataType.FLOAT + + def test_config_type_trimmed(self): + s = pl.Series('x', ['a', 'b']) + assert classify_column('x', s, config_type=' enum ') == DataType.ENUM + + def test_unknown_config_type_falls_through(self): + """Unknown config_type string → ignored, falls to name/value.""" + s = pl.Series('bytes_sent', [1, 2, 3]) + result = classify_column('bytes_sent', s, config_type='unknown_type') + assert result == DataType.INT # value-based + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_schema (full schema classification) +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifySchema: + def test_classify_all_columns(self): + data = { + 'src_ip': ['10.0.0.1', '10.0.0.2', '10.0.0.3'], + 'dst_ip': ['10.0.1.1', '10.0.1.2', '10.0.1.3'], + 'bytes_sent': [100, 200, 300], + 'duration': [1.5, 2.5, 0.5], + 'tls_version': ['TLSv1.2', 'TLSv1.3', 'TLSv1.2'], + 'cipher': ['0e a6 3f', 'ab cd ef', '01 02'], + 'url': ['https://a.com', 'https://b.com', 'https://c.com'], + 'lat': [35.0, 36.0, 37.0], + 'lon': [120.0, 121.0, 122.0], + } + lf = pl.DataFrame(data).lazy() + type_map = classify_schema(lf) + + assert type_map['src_ip'] == DataType.IPv4 + assert type_map['dst_ip'] == DataType.IPv4 + assert type_map['bytes_sent'] == DataType.INT + assert type_map['duration'] == DataType.FLOAT + assert type_map['tls_version'] == DataType.ENUM + assert type_map['cipher'] == DataType.HEX + assert type_map['url'] == DataType.URL + assert type_map['lat'] == DataType.LAT_LON + assert type_map['lon'] == DataType.LAT_LON + + def test_classify_with_config_overrides(self): + data = {'src_ip': ['10.0.0.1', '10.0.0.2']} + lf = pl.DataFrame(data).lazy() + overrides = {'src_ip': 'string'} + type_map = classify_schema(lf, config_overrides=overrides) + assert type_map['src_ip'] == DataType.STRING + + +# ═══════════════════════════════════════════════════════════════════════ +# IP clustering utilities +# ═══════════════════════════════════════════════════════════════════════ + +class TestIpToInt: + def test_simple(self): + assert ip_to_int('192.168.1.1') == 3232235777 + + def test_localhost(self): + assert ip_to_int('127.0.0.1') == 2130706433 + + def test_invalid(self): + assert ip_to_int('not_an_ip') is None + + def test_empty_string(self): + assert ip_to_int('') is None + + def test_out_of_range(self): + assert ip_to_int('999.999.999.999') is None + + +class TestGetSubnet: + def test_24_mask(self): + assert get_subnet('192.168.1.5', 24) == '192.168.1.0/24' + + def test_16_mask(self): + assert get_subnet('10.1.2.3', 16) == '10.1.0.0/16' + + def test_8_mask(self): + assert get_subnet('100.200.1.1', 8) == '100.0.0.0/8' + + def test_invalid_ip(self): + assert get_subnet('not_an_ip', 24) is None + + def test_default_mask(self): + """Default mask is /24.""" + assert get_subnet('10.0.0.15') == '10.0.0.0/24' + + +class TestClusterIps: + def test_single_subnet(self): + ips = ['10.0.0.1', '10.0.0.2', '10.0.0.3'] + groups = cluster_ips(ips, mask=24) + assert len(groups) == 1 + assert '10.0.0.0/24' in groups + assert len(groups['10.0.0.0/24']) == 3 + + def test_multiple_subnets(self): + ips = ['10.0.0.1', '10.0.1.1', '192.168.1.1'] + groups = cluster_ips(ips, mask=24) + assert len(groups) == 3 + + def test_invalid_ips_grouped(self): + ips = ['10.0.0.1', 'bad_ip', 'also_invalid'] + groups = cluster_ips(ips, mask=24) + assert '10.0.0.0/24' in groups + assert '__invalid__' in groups + assert len(groups['__invalid__']) == 2 + + def test_empty_list(self): + groups = cluster_ips([], mask=24) + assert groups == {} + + def test_same_subnet_then_16(self): + """10.0.x.x IPs with /16 mask all go to one subnet.""" + ips = ['10.0.1.1', '10.0.2.2', '10.0.255.255'] + groups = cluster_ips(ips, mask=16) + assert len(groups) == 1 + assert '10.0.0.0/16' in groups + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — MAC address (should NOT be HEX) +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyMac: + def test_mac_address_detection(self): + """MAC addresses should NOT be classified as HEX.""" + s = pl.Series('mac', ['aa:bb:cc:dd:ee:ff', '00:1a:2b:3c:4d:5e']) + result = classify_column('mac', s) + assert result != DataType.HEX, f'MAC should not be HEX, got {result}' + # MAC with unique addresses → STRING (high cardinality) + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — PORT values (INT range → ENUM) +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyPort: + def test_port_values_detection(self): + """Integer values in 1-65535 range should be ENUM.""" + s = pl.Series('port', ['80', '443', '8080', '8443']) + assert classify_column('port', s) == DataType.ENUM + + +# ═══════════════════════════════════════════════════════════════════════ +# classify_column — Value-based IPv4 (name-agnostic) +# ═══════════════════════════════════════════════════════════════════════ + +class TestClassifyValueBased: + def test_value_based_ipv4(self): + """IP detection should work from values alone.""" + s = pl.Series('col', ['10.0.0.1', '192.168.1.1', '8.8.8.8']) + assert classify_column('col', s) == DataType.IPv4 + + def test_value_based_url(self): + """URL detection should work from values.""" + s = pl.Series('col', ['https://example.com', 'http://test.org/path']) + assert classify_column('col', s) == DataType.URL + + def test_value_first_over_name(self): + """Value pattern should take priority over name heuristic.""" + # Column named "version" with IP-like values should be IPv4, not ENUM + s = pl.Series('version', ['10.0.0.1', '192.168.1.1']) + assert classify_column('version', s) == DataType.IPv4 diff --git a/tests/test_value_normalizer.py b/tests/test_value_normalizer.py new file mode 100644 index 0000000..3025e13 --- /dev/null +++ b/tests/test_value_normalizer.py @@ -0,0 +1,537 @@ +"""Tests for analysis.value_normalizer — TLS value normalisation. + +Covers: TLS version (hex & display), cipher suite (hex & display), +named curve (hex & display), CNRS/ISRS, and the top-level +:func:`normalize_lf` integration. +""" + +import pytest +import polars as pl + +from analysis.value_normalizer import ( + TLS_VERSION_MAP, + CIPHER_SUITE_MAP, + CURVE_MAP, + normalize_lf, +) + + +# ═════════════════════════════════════════════════════════════════════════ +# TLS Version +# ═════════════════════════════════════════════════════════════════════════ + +class TestNormTlsVersion: + """_norm_tls_version — hex & display → canonical enum name.""" + + def test_hex_03_04_tls_1_3(self): + lf = pl.LazyFrame({'0ver': ['03 04']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_3' + + def test_hex_03_03_tls_1_2(self): + lf = pl.LazyFrame({'0ver': ['03 03']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_2' + + def test_hex_03_02_tls_1_1(self): + lf = pl.LazyFrame({'0ver': ['03 02']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_1' + + def test_hex_03_01_tls_1_0(self): + lf = pl.LazyFrame({'0ver': ['03 01']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_0' + + def test_hex_02_00_ssl_2_0(self): + lf = pl.LazyFrame({'0ver': ['02 00']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'ssl_2_0' + + def test_hex_03_00_ssl_3_0(self): + lf = pl.LazyFrame({'0ver': ['03 00']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'ssl_3_0' + + def test_display_tlsv1_3(self): + lf = pl.LazyFrame({'0ver': ['TLSv1.3']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_3' + + def test_display_tlsv1_2(self): + lf = pl.LazyFrame({'0ver': ['tlsv1.2']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_2' + + def test_display_tls_1_1(self): + lf = pl.LazyFrame({'0ver': ['TLS 1.1']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_1' + + def test_display_bare_1_3(self): + lf = pl.LazyFrame({'0ver': ['1.3']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_3' + + def test_display_bare_1_2(self): + lf = pl.LazyFrame({'0ver': ['1.2']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_2' + + def test_display_tlsv1(self): + lf = pl.LazyFrame({'0ver': ['TLSv1']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'tls_1_0' + + def test_display_sslv2(self): + lf = pl.LazyFrame({'0ver': ['SSLv2']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'ssl_2_0' + + def test_display_sslv3(self): + lf = pl.LazyFrame({'0ver': ['sslv3']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'ssl_3_0' + + def test_display_bare_2(self): + lf = pl.LazyFrame({'0ver': ['2']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'ssl_2_0' + + def test_display_bare_3(self): + lf = pl.LazyFrame({'0ver': ['3']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'ssl_3_0' + + def test_case_insensitive(self): + lf = pl.LazyFrame({'0ver': ['tlsv1.3', 'TLSv1.3', 'TlsV1.3']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'].to_list() == ['tls_1_3', 'tls_1_3', 'tls_1_3'] + + def test_unknown_version(self): + lf = pl.LazyFrame({'0ver': ['ff ff']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'unknown_version' + + def test_unknown_display(self): + lf = pl.LazyFrame({'0ver': ['TLSv9.9']}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] == 'unknown_version' + + def test_null_remains_null(self): + lf = pl.LazyFrame({'0ver': [None]}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'][0] is None + + def test_mixed_values(self): + lf = pl.LazyFrame({'0ver': ['03 04', 'tlsv1.2', '1.3', 'ssl 2', None]}) + result = normalize_lf(lf, {'0ver': None}).collect() + assert result['0ver_norm'].to_list() == [ + 'tls_1_3', 'tls_1_2', 'tls_1_3', 'ssl_2_0', None, + ] + + +# ═════════════════════════════════════════════════════════════════════════ +# Cipher Suite — hex +# ═════════════════════════════════════════════════════════════════════════ + +class TestNormCipher: + """_norm_cipher — hex cipher suite → canonical name.""" + + def test_hex_13_01(self): + lf = pl.LazyFrame({'0cph': ['13 01']}) + result = normalize_lf(lf, {'0cph': None}).collect() + assert result['0cph_norm'][0] == 'tls_aes_128_gcm_sha256' + + def test_hex_13_02(self): + lf = pl.LazyFrame({'0cph': ['13 02']}) + result = normalize_lf(lf, {'0cph': None}).collect() + assert result['0cph_norm'][0] == 'tls_aes_256_gcm_sha384' + + def test_hex_13_03(self): + lf = pl.LazyFrame({'0cph': ['13 03']}) + result = normalize_lf(lf, {'0cph': None}).collect() + assert result['0cph_norm'][0] == 'tls_chacha20_poly1305_sha256' + + def test_hex_c0_2b(self): + lf = pl.LazyFrame({'0cph': ['c0 2b']}) + result = normalize_lf(lf, {'0cph': None}).collect() + assert result['0cph_norm'][0] == 'tls_ecdhe_ecdsa_aes_128_gcm_sha256' + + def test_hex_00_35(self): + lf = pl.LazyFrame({'0cph': ['00 35']}) + result = normalize_lf(lf, {'0cph': None}).collect() + assert result['0cph_norm'][0] == 'tls_rsa_aes_128_cbc_sha' + + def test_hex_00_2f(self): + """00 2f also maps to tls_rsa_aes_128_cbc_sha (same canonical).""" + lf = pl.LazyFrame({'0cph': ['00 2f']}) + result = normalize_lf(lf, {'0cph': None}).collect() + assert result['0cph_norm'][0] == 'tls_rsa_aes_128_cbc_sha' + + def test_unknown_hex(self): + lf = pl.LazyFrame({'0cph': ['ab cd']}) + result = normalize_lf(lf, {'0cph': None}).collect() + assert result['0cph_norm'][0] == 'unknown_cipher_0xab cd' + + def test_null_remains_null(self): + lf = pl.LazyFrame({'0cph': [None]}) + result = normalize_lf(lf, {'0cph': None}).collect() + assert result['0cph_norm'][0] is None + + def test_case_insensitive_hex(self): + lf = pl.LazyFrame({'0cph': ['C0 2B']}) + result = normalize_lf(lf, {'0cph': None}).collect() + assert result['0cph_norm'][0] == 'tls_ecdhe_ecdsa_aes_128_gcm_sha256' + + +# ═════════════════════════════════════════════════════════════════════════ +# Cipher Suite — display name +# ═════════════════════════════════════════════════════════════════════════ + +class TestNormCipherDisplay: + """_norm_cipher_display — IANA/display name → canonical name.""" + + def test_iana_full_name(self): + lf = pl.LazyFrame({'cipher-suite': ['TLS_AES_128_GCM_SHA256']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'tls_aes_128_gcm_sha256' + + def test_iana_full_name_lowercase(self): + lf = pl.LazyFrame({'cipher-suite': ['tls_aes_256_gcm_sha384']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'tls_aes_256_gcm_sha384' + + def test_display_aes128gcm(self): + lf = pl.LazyFrame({'cipher-suite': ['AES 128 GCM']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'tls_aes_128_gcm_sha256' + + def test_display_aes256gcm(self): + lf = pl.LazyFrame({'cipher-suite': ['AES 256 GCM']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'tls_aes_256_gcm_sha384' + + def test_display_chacha20(self): + lf = pl.LazyFrame({'cipher-suite': ['CHACHA20-POLY1305']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'tls_chacha20_poly1305_sha256' + + def test_display_ecdhe_ecdsa_aes128gcm(self): + lf = pl.LazyFrame({'cipher-suite': ['ECDHE-ECDSA-AES128-GCM']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'tls_ecdhe_ecdsa_aes_128_gcm_sha256' + + def test_display_ecdhe_rsa_aes256gcm(self): + lf = pl.LazyFrame({'cipher-suite': ['ECDHE-RSA-AES256-GCM']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'tls_ecdhe_rsa_aes_256_gcm_sha384' + + def test_display_rsa_aes128gcm(self): + lf = pl.LazyFrame({'cipher-suite': ['RSA AES 128 GCM']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'tls_rsa_aes_128_gcm_sha256' + + def test_display_hyphen_variants(self): + lf = pl.LazyFrame({'cipher-suite': ['ECDHE-RSA-AES128-GCM']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'tls_ecdhe_rsa_aes_128_gcm_sha256' + + def test_unknown_display(self): + lf = pl.LazyFrame({'cipher-suite': ['CUSTOM_CIPHER_999']}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] == 'customcipher999_unknown' + + def test_null_remains_null(self): + lf = pl.LazyFrame({'cipher-suite': [None]}) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + assert result['cipher_suite_norm'][0] is None + + def test_mixed_display_values(self): + lf = pl.LazyFrame({ + 'cipher-suite': [ + 'TLS_AES_128_GCM_SHA256', + 'AES 256 GCM', + 'ECDHE-RSA-AES128-GCM', + 'bogus', + None, + ], + }) + result = normalize_lf(lf, {'cipher-suite': None}).collect() + expected = [ + 'tls_aes_128_gcm_sha256', + 'tls_aes_256_gcm_sha384', + 'tls_ecdhe_rsa_aes_128_gcm_sha256', + 'bogus_unknown', + None, + ] + assert result['cipher_suite_norm'].to_list() == expected + + +# ═════════════════════════════════════════════════════════════════════════ +# Named Curve — hex +# ═════════════════════════════════════════════════════════════════════════ + +class TestNormCurve: + """_norm_curve — hex curve → canonical name.""" + + def test_hex_00_1d_x25519(self): + lf = pl.LazyFrame({'0crv': ['00 1d']}) + result = normalize_lf(lf, {'0crv': None}).collect() + assert result['0crv_norm'][0] == 'x25519' + + def test_hex_00_17_secp256r1(self): + lf = pl.LazyFrame({'0crv': ['00 17']}) + result = normalize_lf(lf, {'0crv': None}).collect() + assert result['0crv_norm'][0] == 'secp256r1' + + def test_hex_00_18_secp384r1(self): + lf = pl.LazyFrame({'0crv': ['00 18']}) + result = normalize_lf(lf, {'0crv': None}).collect() + assert result['0crv_norm'][0] == 'secp384r1' + + def test_hex_00_19_secp521r1(self): + lf = pl.LazyFrame({'0crv': ['00 19']}) + result = normalize_lf(lf, {'0crv': None}).collect() + assert result['0crv_norm'][0] == 'secp521r1' + + def test_hex_00_1e_x448(self): + lf = pl.LazyFrame({'0crv': ['00 1e']}) + result = normalize_lf(lf, {'0crv': None}).collect() + assert result['0crv_norm'][0] == 'x448' + + def test_unknown_hex_curve(self): + lf = pl.LazyFrame({'0crv': ['ff ff']}) + result = normalize_lf(lf, {'0crv': None}).collect() + assert result['0crv_norm'][0] == 'unknown_curve_0xff ff' + + def test_null_remains_null(self): + lf = pl.LazyFrame({'0crv': [None]}) + result = normalize_lf(lf, {'0crv': None}).collect() + assert result['0crv_norm'][0] is None + + def test_case_insensitive_hex_curve(self): + lf = pl.LazyFrame({'0crv': ['00 1D']}) + result = normalize_lf(lf, {'0crv': None}).collect() + assert result['0crv_norm'][0] == 'x25519' + + +# ═════════════════════════════════════════════════════════════════════════ +# Named Curve — display name +# ═════════════════════════════════════════════════════════════════════════ + +class TestNormCurveDisplay: + """_norm_curve_display — display name → canonical name.""" + + def test_x25519(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['X25519']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'x25519' + + def test_curve25519(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['Curve25519']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'x25519' + + def test_secp256r1(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['secp256r1']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp256r1' + + def test_prime256v1(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['prime256v1']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp256r1' + + def test_p256(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['P256']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp256r1' + + def test_p_256(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['P-256']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp256r1' + + def test_p_256_underscore(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['P_256']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp256r1' + + def test_secp384r1(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['secp384r1']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp384r1' + + def test_p384(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['P384']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp384r1' + + def test_p_384(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['p-384']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp384r1' + + def test_secp521r1(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['Secp521r1']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp521r1' + + def test_p521(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['P521']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'secp521r1' + + def test_x448(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['x448']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'x448' + + def test_curve448(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['Curve448']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'x448' + + def test_unknown_curve_display(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['brainpoolP256r1']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] == 'unknown_curve_brainpoolp256r1' + + def test_null_remains_null(self): + lf = pl.LazyFrame({'ecdhe-named-curve': [None]}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'][0] is None + + def test_case_insensitive_p256(self): + lf = pl.LazyFrame({'ecdhe-named-curve': ['P-256', 'p_256', 'P256']}) + result = normalize_lf(lf, {'ecdhe-named-curve': None}).collect() + assert result['ecdhe_named_curve_norm'].to_list() == [ + 'secp256r1', 'secp256r1', 'secp256r1', + ] + + +# ═════════════════════════════════════════════════════════════════════════ +# CNRS / ISRS +# ═════════════════════════════════════════════════════════════════════════ + +class TestNormCnrs: + """_norm_cnrs — '+' → 'yes', blank/null → 'no'.""" + + def test_plus_yes(self): + lf = pl.LazyFrame({'cnrs': ['+']}) + result = normalize_lf(lf, {'cnrs': None}).collect() + assert result['cnrs_norm'][0] == 'yes' + + def test_blank_no(self): + lf = pl.LazyFrame({'cnrs': ['']}) + result = normalize_lf(lf, {'cnrs': None}).collect() + assert result['cnrs_norm'][0] == 'no' + + def test_null_no(self): + lf = pl.LazyFrame({'cnrs': [None]}) + result = normalize_lf(lf, {'cnrs': None}).collect() + assert result['cnrs_norm'][0] == 'no' + + def test_whitespace_plus(self): + lf = pl.LazyFrame({'cnrs': [' + ']}) + result = normalize_lf(lf, {'cnrs': None}).collect() + assert result['cnrs_norm'][0] == 'yes' + + def test_anything_else_no(self): + lf = pl.LazyFrame({'cnrs': ['-', 'true', 'maybe', '0', '1']}) + result = normalize_lf(lf, {'cnrs': None}).collect() + assert result['cnrs_norm'].to_list() == ['no', 'no', 'no', 'no', 'no'] + + def test_isrs_plus_yes(self): + lf = pl.LazyFrame({'isrs': ['+']}) + result = normalize_lf(lf, {'isrs': None}).collect() + assert result['isrs_norm'][0] == 'yes' + + def test_isrs_blank_no(self): + lf = pl.LazyFrame({'isrs': ['']}) + result = normalize_lf(lf, {'isrs': None}).collect() + assert result['isrs_norm'][0] == 'no' + + +# ═════════════════════════════════════════════════════════════════════════ +# Integration — normalize_lf +# ═════════════════════════════════════════════════════════════════════════ + +class TestNormalizeLf: + """Top-level normalize_lf — column routing & no-op cases.""" + + def test_all_columns_at_once(self): + lf = pl.LazyFrame({ + '0ver': ['03 04'], + '0cph': ['13 01'], + 'cipher-suite': ['TLS_AES_128_GCM_SHA256'], + '0crv': ['00 1d'], + 'ecdhe-named-curve': ['X25519'], + 'cnrs': ['+'], + 'isrs': [''], + }) + type_map = { + '0ver': None, + '0cph': None, + 'cipher-suite': None, + '0crv': None, + 'ecdhe-named-curve': None, + 'cnrs': None, + 'isrs': None, + } + result = normalize_lf(lf, type_map).collect() + + assert result['0ver_norm'][0] == 'tls_1_3' + assert result['0cph_norm'][0] == 'tls_aes_128_gcm_sha256' + assert result['cipher_suite_norm'][0] == 'tls_aes_128_gcm_sha256' + assert result['0crv_norm'][0] == 'x25519' + assert result['ecdhe_named_curve_norm'][0] == 'x25519' + assert result['cnrs_norm'][0] == 'yes' + assert result['isrs_norm'][0] == 'no' + + def test_no_matching_columns(self): + """Empty type_map → no _norm columns added.""" + lf = pl.LazyFrame({'0ver': ['03 04']}) + result = normalize_lf(lf, {}).collect() + assert '0ver_norm' not in result.columns + assert result['0ver'][0] == '03 04' + + def test_subset_of_columns(self): + """Only columns present in type_map get normalised.""" + lf = pl.LazyFrame({ + '0ver': ['03 04'], + '0cph': ['13 01'], + 'cnrs': ['+'], + }) + result = normalize_lf(lf, {'0ver': None, 'cnrs': None}).collect() + assert '0ver_norm' in result.columns + assert 'cnrs_norm' in result.columns + assert '0cph_norm' not in result.columns + + def test_preserves_original_columns(self): + """Original columns are kept unchanged.""" + lf = pl.LazyFrame({ + '0ver': ['03 04'], + 'src_ip': ['10.0.0.1'], + }) + result = normalize_lf(lf, {'0ver': None}).collect() + assert '0ver' in result.columns + assert 'src_ip' in result.columns + assert result['src_ip'][0] == '10.0.0.1' + + def test_duplicate_normalized_column_exists(self): + """If a _norm column already exists, Polars appends suffixed.""" + lf = pl.LazyFrame({ + '0ver': ['03 04'], + '0ver_norm': ['old'], + }) + result = normalize_lf(lf, {'0ver': None}).collect() + # Polars will create 0ver_norm with new value + assert result['0ver_norm'][0] == 'tls_1_3' + + def test_lazyness_preserved(self): + """normalize_lf returns a LazyFrame, not materialised.""" + lf = pl.LazyFrame({'0ver': ['03 04']}) + result = normalize_lf(lf, {'0ver': None}) + assert isinstance(result, pl.LazyFrame) diff --git a/tianxuan/__init__.py b/tianxuan/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tianxuan/llm_orchestrator.py b/tianxuan/llm_orchestrator.py new file mode 100644 index 0000000..5f8e628 --- /dev/null +++ b/tianxuan/llm_orchestrator.py @@ -0,0 +1,85 @@ +"""LLM orchestrator optimized for 32B model via MCP tool calling. +Short prompts, aggressive truncation, step-by-step guidance.""" +import json, logging, time, urllib.request, urllib.error, asyncio +from typing import Optional +from analysis.tool_registry import handle_call + +logger = logging.getLogger('tianxuan.llm') + +TOOLS = [ + {"type": "function", "function": {"name": "profile_data", + "description": "Examine dataset columns and statistics", + "parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}}, "required": ["dataset_id"]}}}, + {"type": "function", "function": {"name": "build_entity_profiles", + "description": "Group flows by entity for user behavior analysis", + "parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}, "auto_detect": {"type": "boolean"}}, "required": ["dataset_id"]}}}, + {"type": "function", "function": {"name": "run_clustering", + "description": "Cluster entities into behavior groups", + "parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}, "cluster_columns": {"type": "array", "items": {"type": "string"}}, "algorithm": {"type": "string", "enum": ["hdbscan", "kmeans"]}}, "required": ["dataset_id", "cluster_columns"]}}}, + {"type": "function", "function": {"name": "extract_features", + "description": "Identify distinguishing features per cluster", + "parameters": {"type": "object", "properties": {"dataset_id": {"type": "string"}, "cluster_result_id": {"type": "string"}, "method": {"type": "string", "enum": ["zscore", "anova"]}}, "required": ["dataset_id", "cluster_result_id"]}}}, +] + +class LLMConfig: + def __init__(self, base_url="", api_key="", model="deepseek-v4-flash", max_tokens=2048, temperature=0.1): + self.base_url = base_url.rstrip('/') + self.api_key = api_key + self.model = model + self.max_tokens = max_tokens + self.temperature = temperature + +def call_llm(messages, config): + payload = json.dumps({"model": config.model, "messages": messages, "tools": TOOLS, + "tool_choice": "auto", "max_tokens": config.max_tokens, "temperature": config.temperature}).encode() + headers = {"Content-Type": "application/json"} + if config.api_key: headers["Authorization"] = f"Bearer {config.api_key}" + req = urllib.request.Request(f"{config.base_url}/chat/completions", data=payload, headers=headers, method='POST') + logger.info(f'[LLM] model={config.model}') + start = time.monotonic() + with urllib.request.urlopen(req, timeout=90) as resp: + d = json.loads(resp.read().decode()) + logger.info(f'[LLM] latency_ms={int((time.monotonic()-start)*1000)}') + return d + +def run_llm_pipeline(dataset_id, entity_col="", config=None, callback=None): + if config is None: + from config import get_config; cfg = get_config() + config = LLMConfig(cfg.llm.base_url, cfg.llm.api_key, cfg.llm.model) + msgs = [{"role": "system", "content": "Analyze dataset step by step using the available tools."}, + {"role": "user", "content": f"Profile dataset {dataset_id}, then build entities, cluster, and extract features."}] + guides = ["", "Good. Now build entity profiles.", "Good. Now run clustering.", "Good. Now extract features.", "Good. Summarize results."] + gi = 0 + for step in range(15): + if step > 0 and gi < len(guides) and guides[gi]: + msgs.append({"role": "user", "content": guides[gi]}) + gi += 1 + if callback: + callback(step, "thinking", None) + resp = call_llm(msgs, config) + if not resp: + if callback: + callback(step, "error", "LLM call failed") + return {"error": "LLM call failed"} + msg = resp["choices"][0]["message"] + if "tool_calls" not in msg or not msg["tool_calls"]: + if callback: + callback(step, "complete", msg.get("content", "")) + return {"result": msg.get("content","") or "Complete", "steps": step+1} + msgs.append(msg) + for tc in msg["tool_calls"]: + f = tc.get("function",{}); name = f.get("name","") + try: args = json.loads(f.get("arguments","{}")) + except: args = {} + logger.info(f'[TOOL] {name}') + if callback: + callback(step, name, None) + try: result = asyncio.run(handle_call(name, args)) + except Exception as e: result = {"error": str(e)} + if callback: + callback(step, name, result) + msgs.append({"role": "tool", "tool_call_id": tc.get("id",""), + "content": json.dumps(result, default=str, ensure_ascii=False)[:1200]}) + if callback: + callback(15, "max_steps", None) + return {"error": "Max steps", "steps": 15} diff --git a/tianxuan/settings.py b/tianxuan/settings.py new file mode 100644 index 0000000..f7f9982 --- /dev/null +++ b/tianxuan/settings.py @@ -0,0 +1,113 @@ +import os +from pathlib import Path + +BASE_DIR = Path(__file__).resolve().parent.parent +os.makedirs(BASE_DIR / "logs", exist_ok=True) + +SECRET_KEY = "django-insecure-change-me-in-production" + +DEBUG = True + +import socket + +hostname = socket.gethostname() +try: + local_ip = socket.gethostbyname(hostname) +except: + local_ip = "127.0.0.1" +ALLOWED_HOSTS = ["localhost", "127.0.0.1", hostname, local_ip, "*"] + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "analysis.apps.AnalysisConfig", +] + +MIDDLEWARE = [ + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "tianxuan.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [BASE_DIR / "templates"], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.debug", + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "tianxuan.wsgi.application" + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + +# Set SQLite journal mode to DELETE to avoid WAL files during compression +from django.db.backends.signals import connection_created + + +def _set_sqlite_journal(sender, connection, **kwargs): + if connection.vendor == "sqlite": + with connection.cursor() as cursor: + cursor.execute("PRAGMA journal_mode=DELETE;") + + +connection_created.connect(_set_sqlite_journal) + +AUTH_PASSWORD_VALIDATORS = [] + +LANGUAGE_CODE = "zh-hans" +TIME_ZONE = "UTC" +USE_I18N = True +USE_TZ = True + +DATA_UPLOAD_MAX_NUMBER_FIELDS = 0 +DATA_UPLOAD_MAX_MEMORY_SIZE = 256 * 1024 * 1024 +FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 + +STATIC_URL = "static/" +STATICFILES_DIRS = [BASE_DIR / "static"] + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" + +LOGGING = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "verbose": {"format": "[{asctime}] {levelname} {name}: {message}", "style": "{"} + }, + "handlers": { + "file": { + "class": "logging.handlers.RotatingFileHandler", + "filename": BASE_DIR / "logs" / "tianxuan.log", + "maxBytes": 10485760, + "backupCount": 5, + "formatter": "verbose", + "encoding": "utf-8", + }, + "stderr": {"class": "logging.StreamHandler", "formatter": "verbose"}, + }, + "root": {"handlers": ["file", "stderr"], "level": "INFO"}, +} diff --git a/tianxuan/urls.py b/tianxuan/urls.py new file mode 100644 index 0000000..298e650 --- /dev/null +++ b/tianxuan/urls.py @@ -0,0 +1,7 @@ +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path('admin/', admin.site.urls), + path('', include('analysis.urls')), +] diff --git a/tianxuan/wsgi.py b/tianxuan/wsgi.py new file mode 100644 index 0000000..4802e3b --- /dev/null +++ b/tianxuan/wsgi.py @@ -0,0 +1,18 @@ +import os, sqlite3 +from pathlib import Path +from django.core.wsgi import get_wsgi_application + +db_path = Path(__file__).resolve().parent.parent / 'db.sqlite3' +if db_path.exists(): + try: + conn = sqlite3.connect(str(db_path)) + conn.execute('PRAGMA integrity_check') + conn.close() + except Exception: + import shutil + backup = Path(str(db_path) + '.corrupted') + shutil.copy2(str(db_path), str(backup)) + db_path.unlink() + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'tianxuan.settings') +application = get_wsgi_application() diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..f3c0fc3 --- /dev/null +++ b/uv.lock @@ -0,0 +1,1213 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15' and sys_platform == 'win32'", + "python_full_version == '3.14.*' and sys_platform == 'win32'", + "python_full_version >= '3.15' and sys_platform != 'win32'", + "python_full_version == '3.14.*' and sys_platform != 'win32'", + "python_full_version < '3.14' and sys_platform == 'win32'", + "python_full_version < '3.14' and sys_platform != 'win32'", +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.14.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "idna" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/cc/a381afa6efea9f496eff839d4a6a1aed3bfafc7b3ab4b0d1b243a12573dd/anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f", size = 260176, upload-time = "2026-07-12T20:29:07.082Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/35/f2287558c17e29fafc8ef3daf819bb9834061cfa43bff8014f7df7f63bdc/anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494", size = 125813, upload-time = "2026-07-12T20:29:05.763Z" }, +] + +[[package]] +name = "asgiref" +version = "3.11.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/40/f03da1264ae8f7cfdbf9146542e5e7e8100a4c66ab48e791df9a03d3f6c0/asgiref-3.11.1.tar.gz", hash = "sha256:5f184dc43b7e763efe848065441eac62229c9f7b0475f41f80e207a114eda4ce", size = 38550, upload-time = "2026-02-03T13:30:14.33Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/0a/a72d10ed65068e115044937873362e6e32fab1b7dce0046aeb224682c989/asgiref-3.11.1-py3-none-any.whl", hash = "sha256:e8667a091e69529631969fd45dc268fa79b99c92c5fcdda727757e52146ec133", size = 24345, upload-time = "2026-02-03T13:30:13.039Z" }, +] + +[[package]] +name = "ast-serialize" +version = "0.6.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/ad/0d70a3a2d6e01968d985415259e8ec7ad3f777903f9b1c1f3c8c44642c60/ast_serialize-0.6.0.tar.gz", hash = "sha256:aadd3ffcf4858c9726bf3515f7b199c7eadbe504f96028e4a87172c0da65a8fe", size = 61489, upload-time = "2026-06-30T20:02:55.555Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/12/3e5f575f156555547c250a8b0d1347517a3a20fc7f4492e9703a69d4f45e/ast_serialize-0.6.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:a7520b672827885bafeae7501f684d14d47d17e5f45256f9df547686cca52264", size = 1177640, upload-time = "2026-06-30T20:02:06.708Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/a4/921a9e27951627983b0f368859ea00f8330a551dc0bf4c2fdcb11855a98b/ast_serialize-0.6.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:a14191beec7e0c078d2fc1f6edc0aee88bcd4db9f18e1bc9f8052b559c22dddc", size = 1168111, upload-time = "2026-06-30T20:02:08.366Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/69/950cf404de7b8782cf95e5c1237e25e2aa46177b287f39f9eeddf481fd6f/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32ef62ec34cf6be20ad77d4799556638fbdf187f3ae10698dfb20ef9f2c89516", size = 1227656, upload-time = "2026-06-30T20:02:09.843Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/a8/46f8f6a6479d9d2273980957bb091a506c55f5b95d3c029ee58518a78407/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:13b7769970a39983b0adf2f38917b1cd3b8946f76df045756c3d741bc689f089", size = 1227706, upload-time = "2026-06-30T20:02:11.367Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/b9/9ac415bda0a40e49eab8fea3b2741c19c98bb84d57d62c4cfc6230eb67be/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f7a408601bb3edaefb3bc67a4c01f5235e3253653b6a5729a2ee2382b35341c", size = 1431705, upload-time = "2026-06-30T20:02:12.737Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/06/8807115d441444879f7561b5eede5ac18fc80392f11826d61ccf31f503b1/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8670bfa51208a2c0c8d138928e40e998fab158f9200d53bb80c088b5b8eda7b8", size = 1249533, upload-time = "2026-06-30T20:02:14.571Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/c0/c2ba82ef9618650357d9421a1fdb27ffec862a7f57e8e2de82a3ccd11e12/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4826809eb8597a8cd59fd924b6d7c285b8969a1e0007e2cb652cab62376270f", size = 1252619, upload-time = "2026-06-30T20:02:16.219Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/a7/fa31d52dd4102cede29fb9634e98d214129b2783b4f95528c6dc6a8f6587/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:577a6c189068686869f5f1ddc38363f3ae1808a4753b577266f9202071a7bb66", size = 1242983, upload-time = "2026-06-30T20:02:17.813Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/20/ddf742b5ad3c4bafd3466f2265037cfd99bc1b9a5ee46a5d58c90d523242/ast_serialize-0.6.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:085de7f62dc9cc247eb01e965a362707d1d90b1d89a82c5bf78301a60a3c417b", size = 1296148, upload-time = "2026-06-30T20:02:19.146Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/cb/9f6f217cce8b3b632c5568b478d195a35e79dce4dbe309438cb89ba6ea4f/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9f8a8b78b13173de6a9ec22111d9be674874cd5bdccda04f14ae5ebc2bef403a", size = 1403826, upload-time = "2026-06-30T20:02:20.696Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/f8/9d16d4f0107a183924425cc0e7618d8bf76f96b45afa9ff19f924ed1ad57/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:f2ff3baffc3a29c1f15bc9098aa0c09763410262d5e6cef42116f7356c184554", size = 1502943, upload-time = "2026-06-30T20:02:22.034Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/dd/bbc1c38756350dddf7e24acae1c9482ef42051c267417e019aecc1ed4075/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0067b25fce104eaae5b88383de9ab803faeb671831e14ca698b771b356e2600f", size = 1497632, upload-time = "2026-06-30T20:02:23.517Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/7e/9daffefcf5b97e6bb4c3e0b3c024c1aee9722f23d3cf7cd2ff80d6fb4a40/ast_serialize-0.6.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c617417f9cbb0cb144f6283c3cbe0d2e0f01beaf9f608f662b21191058a626ec", size = 1448858, upload-time = "2026-06-30T20:02:24.889Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/1f/f9baaab81a677ea0af7d2458cac2f94ebcc85958f8a3c15ba9d9e5dab653/ast_serialize-0.6.0-cp314-cp314t-win32.whl", hash = "sha256:5337cb256dcea3df9288205213d1601581536526b8f4da44b6974f1180f3252a", size = 1052600, upload-time = "2026-06-30T20:02:26.263Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/1f/41b535866519512d8cf6669cb2cff7823b7672bb6279c0333b4ff89d7d9f/ast_serialize-0.6.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2d947e45cafc4b09bd7528917fa84c517654a43de173c79785574b7b3068ac24", size = 1095570, upload-time = "2026-06-30T20:02:27.639Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/64/e472fe3e3a2d33d874b987e8518aedf24562919e3b6161a4fa1797e89c0f/ast_serialize-0.6.0-cp314-cp314t-win_arm64.whl", hash = "sha256:6e15ec740436e1a0d62de848641abe5f3a2f89a7f94907d534795ac91bbacf14", size = 1067267, upload-time = "2026-06-30T20:02:28.949Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/19/ac8348ae8711c9b5ae834634f635780cab62a0f5e6f988882e048b89c2ae/ast_serialize-0.6.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:093cb8bb91b720d8523580498d031791bb1bbaa048599c3d21085d380e11a596", size = 1185367, upload-time = "2026-06-30T20:02:30.427Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/f6/ec7ec652c51db77c2f61d8573338e13e4704303265ccc658cb4031d9f354/ast_serialize-0.6.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:e61580a69faf47e3689795367ed211f2a10fd741478cc0f36a0f128793360aad", size = 1178657, upload-time = "2026-06-30T20:02:31.964Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/02/613a7534a41d0122f37d1e0c64aa8ac78bfb831f8c92f6db057a311abb3c/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:305802f2ce2a7c4e87835078ea85c58b586ddda8095b92fe2ead9364ae19c80a", size = 1238620, upload-time = "2026-06-30T20:02:33.664Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/21/087957bba486242afc52f49b2d9e21c9dad00289356cf9efe67084015a9d/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c7b8b8f0c42f752ea00b2b7d7c090b3f80d9c1c5c75cadf16423790a0cc74081", size = 1236075, upload-time = "2026-06-30T20:02:34.936Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/04/78128bbb170071c2c72a210a181f1c00e11cc1cec60a8beef747b07f9201/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cd5b91b9e6f2356ace3a556963b0cd783b395fbbb0bb17b4defc283415466e77", size = 1441348, upload-time = "2026-06-30T20:02:36.245Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/64/62fb99d6faf199b4c3e5b08a07136e9a0d7664bb249c6de3670e5b63e9b6/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4d6ef91590258ada18909b9caea344dac4de2013906b035473cd674a43f4b790", size = 1258580, upload-time = "2026-06-30T20:02:37.53Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/87/b4d6c38e0ccd5e85dc54cecdf933a152c60b28fe5d993a6d8a72fa6d5896/ast_serialize-0.6.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcbed41e9386059fc0261d602445ede0976c2ecec2939688bcbcb9ed0b6f28b7", size = 1261693, upload-time = "2026-06-30T20:02:39.123Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/4b/3676ca2191f39bafb75f93f99b2f429ec464586158fece2165f3572805dc/ast_serialize-0.6.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:cdc4e6f930b9090c2f92c9036ad12ffb8e6e44d4a5ba06f1458a05d60f203f7b", size = 1252517, upload-time = "2026-06-30T20:02:40.511Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/58/494ef8c4b4acb2f4a265ac934caf45f792a08fe27d6b853de35ad991941a/ast_serialize-0.6.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:897ac47b5637be41c0c07061c8a912fafa967ef1dc73fa115e4bfa70882a093b", size = 1304843, upload-time = "2026-06-30T20:02:41.961Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/f2/13736d920ab3d49bbee80ef1a277dd7b7aaf3b3545efd9d2a8114fe05525/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c4af9a1386166e40ed01464991806f89038a2d89782576c7774876fa77034e32", size = 1413698, upload-time = "2026-06-30T20:02:44.179Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/5a/e046f3899e2acba4677d7427b76431443a1aa1a0e583dfb05b55b69d55cf/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c901adbd750029b9ac4ad3d6aa56853e0ad4875119fbf52b7b8298afc223828b", size = 1512209, upload-time = "2026-06-30T20:02:45.584Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/c7/e42aaca7bb2d22a7c06d5a8c7930086c5a334e93d716e6fa5e6647a4515f/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3ae22a366b752ab4496191525b78b097b5b72d531752e3c1dd7e383a8f2c8a1a", size = 1508464, upload-time = "2026-06-30T20:02:46.942Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/93/5524a3dc6c3f593de3228ed9cbef73afa047625b7000ec21b7f58e6eb4d4/ast_serialize-0.6.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4ed29121da8b3fdc291002801a1de0f76248fa07dce89157a5f277842cf6126e", size = 1457164, upload-time = "2026-06-30T20:02:48.294Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/c0/36a6ffb4d653cf621427b4c4928671f53ad800c453474de2b82564a44ad9/ast_serialize-0.6.0-cp39-abi3-pyemscripten_2026_0_wasm32.whl", hash = "sha256:b1dac4e09d341c1300ba69cdcbe62867b32a8c75d90db9bf4d083bec3b039f0b", size = 863014, upload-time = "2026-06-30T20:02:49.742Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/c7/7d5ad8b49e1278e1c2a1e0274bd7850560b3f09313aa00c13bc8d5544792/ast_serialize-0.6.0-cp39-abi3-win32.whl", hash = "sha256:82c312a7844d2fdeb4d5c48bd3d215bf940dafd4704e1a9bcf252a99010a99b1", size = 1063165, upload-time = "2026-06-30T20:02:50.98Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/ae/6710c14ecb276031cf10249f6adf5a59e2d3fdb3b5183bd59f70524067ee/ast_serialize-0.6.0-cp39-abi3-win_amd64.whl", hash = "sha256:113b58346f9ceb664352032770caca817d4a3c86f611c6088e6ef65ddaa70f0e", size = 1101444, upload-time = "2026-06-30T20:02:52.554Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/40/c53deb2cd0c9b0fb636d24d9f40924cf2e65028e6b20b10cd5c1eeb2c730/ast_serialize-0.6.0-cp39-abi3-win_arm64.whl", hash = "sha256:ccd132fe8db56f61fe743b1f644d01b8d65b83248a8da506f3132bda86d6ed5e", size = 1072965, upload-time = "2026-06-30T20:02:54.097Z" }, +] + +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + +[[package]] +name = "certifi" +version = "2026.6.17" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/c7/424b75da314c1045981bd9777432fad05a9e0c69daa4ed7e308bbaffe405/certifi-2026.6.17.tar.gz", hash = "sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432", size = 134594, upload-time = "2026-06-17T10:31:07.894Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/2f/c5464532e965badff2f4c4c1a3a83f5697f0d7c407ed0cda44aaa99bb451/certifi-2026.6.17-py3-none-any.whl", hash = "sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db", size = 133289, upload-time = "2026-06-17T10:31:06.348Z" }, +] + +[[package]] +name = "cffi" +version = "2.1.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/5f/ff100cae70ebe9d8df1c01a00e510e45d9adb5c1fdda84791b199141de97/cffi-2.1.0.tar.gz", hash = "sha256:efc1cdd798b1aaf39b4610bba7aad28c9bea9b910f25c784ccf9ec1fa719d1f9", size = 531036, upload-time = "2026-07-06T21:34:30.382Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/85/990925db5df586ec90beb97529c853497e7f85ba0234830447faf41c3057/cffi-2.1.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:df2b82571a1b30f58a87bf4e5a9e78d2b1eff6c6ce8fd3aa3757221f93f0863f", size = 184829, upload-time = "2026-07-06T21:32:44.324Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/92/e7bb136ad6b5352603732cf907ef862ca103f20f2031c1735a46300c20c9/cffi-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78474632761faa0fb96f30b1c928c84ebcf68713cbb80d15bab09dfe61640fde", size = 184728, upload-time = "2026-07-06T21:32:45.683Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/c0/d1ec30ffb370f748f2fb54425972bfef9871e0132e82fb589c46b6676049/cffi-2.1.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5972433ad71a9e46516584ef60a0fda12d9dc459938d1539c3ddecf9bdc1368d", size = 214815, upload-time = "2026-07-06T21:32:48.557Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/dc/5620cf930688be01f2d673804291de757a934c90b946dbdc3d84130c2ea4/cffi-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b6422532152adf4e59b110cb2808cee7a033800952f5c036b4af047ee43199e7", size = 222429, upload-time = "2026-07-06T21:32:49.848Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/a4/77b53abbf7a1e0beb9637edbef2a94d15f9c822f591e85d439ffd91519a6/cffi-2.1.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:46b1c8db8f6122420f32d02fffb924c2fe9bc772d228c7c711748fff56aabb2b", size = 210315, upload-time = "2026-07-06T21:32:51.221Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/0c/f528df19cc94b675087324d4760d9e6d5bfae97d6217aa4fac43de4f5fcc/cffi-2.1.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9fafc5aa2e2a39aaf7f8cc0c1f044a9b07fca12e558dca53a3cc5c654ad67a7", size = 208859, upload-time = "2026-07-06T21:32:52.512Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/f2/c9522a81c32132799a1972c39f5c5f8b4c8b9f00488a23feaa6c06f07741/cffi-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e9f50d192a3e525b15a75ab5114e442d83d657b7ec29182a991bc9a88fd3a66", size = 221844, upload-time = "2026-07-06T21:32:53.704Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/28/bd53988b9833e8f8ad539d26f4c07a6b3f6bcb1e9e02e7ca038250b3428d/cffi-2.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:98fff996e983a36d3aa2eca83af40c5821202e7e6f32d13ae94e3d2286f10cfe", size = 225287, upload-time = "2026-07-06T21:32:54.907Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/99/0d0fd37f055224085f42bbb2c022d002e17dde4a97972822327b07d84101/cffi-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:379de10ce1ba048b1448599d1b37b24caee16309d1ac98d3982fc997f768700b", size = 223681, upload-time = "2026-07-06T21:32:56.329Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/80/c138990aa2a70b1a269f6e06348729836d733d6f970867943f61d367f8cc/cffi-2.1.0-cp312-cp312-win32.whl", hash = "sha256:9b8f0f26ca4e7513c534d351eca551947d053fac438f2a04ac96d882909b0d3a", size = 175269, upload-time = "2026-07-06T21:32:57.777Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/eb/f636456ff21a83fc13c032b58cc5dde061691546ac79efa284b2989b7982/cffi-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:c97f080ea627e2863524c5af3836e2270b5f5dfff1f104392b959f8df0c5d384", size = 185881, upload-time = "2026-07-06T21:32:59.253Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/2c/400ea43e721727dca8a65c4521390e9196757caba4a45643acb2b63271b8/cffi-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:6d194185eabd279f1c05ebe3504265ddfc5ad2b58d0714f7db9f01da592e9eb6", size = 180088, upload-time = "2026-07-06T21:33:02.278Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/88/a996879e2eeccb815f6e3a5967b12a308257412acec882039d386bd2aa7b/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:10537b1df4967ca26d21e5072d7d54188354483b91dc75058968d3f0cf13fbda", size = 194331, upload-time = "2026-07-06T21:33:03.697Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/85/7ae00d5c8dd6266f4e944c3db630f3c5c9a98b61d469c714d848b1d8138a/cffi-2.1.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:a95b05f9baf29b91171b3a8bd2020b028835243e7b0ff6bb23e2a3c228518b1b", size = 196966, upload-time = "2026-07-06T21:33:05.353Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/e9/45c3a76ad8d43ad9261f4c95436da61128d3ca545d72b9612c0ab5be0b1c/cffi-2.1.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:15faec4adfff450819f3aee0e2e02c812de6edb88203aa58807955db2003472a", size = 184795, upload-time = "2026-07-06T21:33:06.699Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/4c/82f132cb4418ee6d953d982b19191e87e2a6372c8a4ce36e50b69d6ade4a/cffi-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:716ff8ec22f20b4d988b12884086bcef0fc99737043e503f7a3935a6be99b1ea", size = 184746, upload-time = "2026-07-06T21:33:08.071Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/1c/4ed5a0e5bdca6cbc275556de3328dd1b76fd0c11cc13c88fe66d1d8715f2/cffi-2.1.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:63960549e4f8dc41e31accb97b975abaecfc44c03e396c093a6436763c2ea7db", size = 214747, upload-time = "2026-07-06T21:33:09.671Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/a6/e879bb68cc23a2bc9ba8f4b7d8019f0c2694bad2ab6c4a3701d429439f58/cffi-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff067a8d8d880e7809e4ac88eb009bb848870115317b306666502ccad30b147f", size = 222392, upload-time = "2026-07-06T21:33:10.896Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/f6/01890cfd63c08f8eb96a8319b0443690197d240a8bd6346048cf7bde9190/cffi-2.1.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3b926723c13eba9f81d2ef3820d63aeceec3b2d4639906047bf675cb8a7a500d", size = 210285, upload-time = "2026-07-06T21:33:12.251Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/cf/2b684132056f438567b61e19d690dd31cd0921ace051e0a458be6074369e/cffi-2.1.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:47ff3a8bfd8cb9da1af7524b965127095055654c177fcfc7578debcb015eecd0", size = 208801, upload-time = "2026-07-06T21:33:13.617Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/08/f2e7d62c460faae0926f2d6e423694aa409ced3bc1fe2927a0a6e5f05416/cffi-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:799416bae98336e400981ff6e532d67d5c709cfb30afb79865a1315f94b0e224", size = 221808, upload-time = "2026-07-06T21:33:15.466Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/37/04f54b8e63a02f3d908332c9effbf8c366167c6f733ed8a3d4f79b7e2a1e/cffi-2.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:961be50688f7fba2fa65f63712d3b9b341a22311f5253460ce933f52f0de1c8c", size = 225241, upload-time = "2026-07-06T21:33:16.869Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/d6/c72eecca433cd3e681c65ed313ab4835d9d4a379704d0f628a6a05f51c2e/cffi-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:bf5c6cf48238b0eb4c086978c492ad1cbc22373fc5b2d7353b3a598ce6db887a", size = 223588, upload-time = "2026-07-06T21:33:18.239Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/4b/e706f67279140f92939da3475ad610df18bfd52d50f14953a8e5fede71d5/cffi-2.1.0-cp313-cp313-win32.whl", hash = "sha256:db3eb7d46527159a878ec3460e9d40615bc25ba337d477db681aea6e4f05c5d2", size = 175248, upload-time = "2026-07-06T21:33:19.799Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/47/59eb7975cb0e4ef0afa764ea945b29a5bb4537a9f771cb7d6c8a5dd74c95/cffi-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:8e74a6135550c4748af665b1b1118b6aab33b1fc6a16f9aff630af107c3b4512", size = 185717, upload-time = "2026-07-06T21:33:21.47Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/af/34fee85c48f8d94efc8597bc09470c9dd274c145f1c12e0fbc6ab6d38d74/cffi-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:2282cd5e38aa8accd03e99d1256af8411c84cdbee6a89d841b563fdbd1f3e50f", size = 180114, upload-time = "2026-07-06T21:33:22.515Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/f0/81478e482afa03f6d18dc8f2afb5edc45b3080853b634b5ed91961be0998/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2117334c3af3bdcb9a88522b844a2bdb5efdc4f71c6c822df55486ae1c3347a", size = 194142, upload-time = "2026-07-06T21:33:23.657Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/95/8de304305cd9204974b0ca051b86d307cafca13aa575a0ef1b44d92c0d8c/cffi-2.1.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:702c436735fbe99d59ada02a1f65cfc0d31c0ee8b7290912f8fbc5cd1e4b16c3", size = 196819, upload-time = "2026-07-06T21:33:25.007Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/71/7c8372d30e42415602ed9f268f7cfd66f1b855fed881ecd168bcb45dbc0b/cffi-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1ff3456eab0d889592d1936d6125bbfbc7ae4d3354a700f8bd80450a66445d4d", size = 184965, upload-time = "2026-07-06T21:33:26.605Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/5c/584e626835f0375c928176c04137c96927165cb8733cdb3150ec04e5ee5e/cffi-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c4165821e131d6d4ca444347c2b694e2311bcfa3fe5a861cc72968f28867beac", size = 184952, upload-time = "2026-07-06T21:33:27.823Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/d2/065fcae1c73979fac8e054462478d0ff8a29c40cdc2ed7ea5676a061df53/cffi-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:276f20fffd7b396e12516ba8edf9509210ac248cbbc5acbc39cd512f9f59ebe6", size = 222353, upload-time = "2026-07-06T21:33:29.178Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/a5/e8bbb1ce5b3ac2f53ad6a10bde44318a5a8d99d4f4a000d44a6e39aeb3e4/cffi-2.1.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:7d5980a3433d4b71a5e120f9dd551403d7824e31e2e67124fe2769c404c06913", size = 210051, upload-time = "2026-07-06T21:33:30.534Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/ed/c127d3ac36e899c965e3361357c3befacd6578c03f40125183e41c3b219e/cffi-2.1.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6ca4919c6e4f89aa99c42510b42cf54596892c00b3f9077f6bdd1505e24b9c8d", size = 208630, upload-time = "2026-07-06T21:33:31.753Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/d7/97d3136f81db489ec8d1d67748c110d6c994268fd7528014aa9f2b085e4e/cffi-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d53d10f7da99ae46f7373b9150393e9c5eab9b224909982b43832668de4779f5", size = 221593, upload-time = "2026-07-06T21:33:33.044Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/27/93195977168ee63aed233a1a0993a2178798654d1f4bddcdd321d6fd3b21/cffi-2.1.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c351efb95e832a853a29361675f33a7ce53de1a109cd73fd47af0712213aa4ce", size = 225146, upload-time = "2026-07-06T21:33:34.224Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/c1/6dbd291ee2ae5a50a034aa057207081f545923bbf15dad4511e985aafff5/cffi-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7c7a88e2bac086f06d14577332760bdeecc42bdec8ac4077f6260557d9326", size = 223240, upload-time = "2026-07-06T21:33:35.57Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/6f/ade5ce9863a57992a6ea3d0d10d7e29b8749fc127204b3d493d667b2815f/cffi-2.1.0-cp314-cp314-win32.whl", hash = "sha256:1854b724d00f6654c742097d5387569021be12d3a0f770eae1df8f8acfcc6acd", size = 177723, upload-time = "2026-07-06T21:33:51.626Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/de/92b9eeed4ae4a21d6fd9b2a2c8505cbed573299902ea73981cc13f7ff62c/cffi-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:1b96bfe2c4bd825681b7d311ad6d9b7280a091f43e8f63da5729638083cd3bfb", size = 187937, upload-time = "2026-07-06T21:33:53.403Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/1a/cc6ae6c2913a03aab8898eee57963cf1035b8df5872ed8b9115fcc7e2be8/cffi-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:7d28dff1db6764108bc30788d85d61c876beff416d9a49cb9dd7c5a9f34f5804", size = 183001, upload-time = "2026-07-06T21:33:54.74Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/f0/134c00ce0779ec86dea2aa1aac69339c2741a8045072676763512363a2ea/cffi-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7ea6b3e2c4250ff1de21c630fe72d0f63eb95c2c32ffbf64a358cf4a8836d714", size = 188538, upload-time = "2026-07-06T21:33:36.792Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/d8/3b86aba791cb610d24e8a3e1b2cd529e71fa15096b04e4d4e360049d4a4c/cffi-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6af371f3767faeffc6ac1ef57cdfd25844403e9d3f476c5537caee499de96376", size = 188230, upload-time = "2026-07-06T21:33:38.011Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/d0/117dcd9209255ad8571fbc8c92ef32593a1d294dcec91ddc4e4db50606f2/cffi-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb4e8997a49aa2c08a3e43c9045d224448b8941d88e7ac163c7d383e560cbf98", size = 223899, upload-time = "2026-07-06T21:33:39.514Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/3d/f20f8b886b254e3ad10e15cd4186d3aed49f3e6a35ab37aab9f8f25f7c03/cffi-2.1.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf01d8c84cbea96b944c73b22182e6c7c432b3475632b8111dbfdc95ddad6e13", size = 211652, upload-time = "2026-07-06T21:33:40.851Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/3b/fad54de07260b93ddeef4b96d0131d57ea900675df1d410ae1deee52d7a6/cffi-2.1.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:33eb1ad83ebe8f313e0df035c406227d55a79456704a863fad9842136af5ad7d", size = 210755, upload-time = "2026-07-06T21:33:42.183Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/82/3d5c705acb7abbba9bbd7d79b8e62e0f25b6120eb7ae6ac49f1b721722fe/cffi-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ac0f1a2d0cfa7eea3f2aaf006ab6e70e8feeb16b75d65b7e5939982ca2f11056", size = 223933, upload-time = "2026-07-06T21:33:43.603Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/d0/47e338384ab6b1004241002fa616301020cea4fc95f283506565d252f276/cffi-2.1.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c16914df9fb7f500e440e6875fa23ff5e0b31db01fa9c06af98d59a91f0dc2e4", size = 226749, upload-time = "2026-07-06T21:33:45.046Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/25/65bd5b58ea4bfdfc15cde02cb5365f89ef8ab8b2adfb8fe5c4bd4233382f/cffi-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5ecbd0499275d57506d397eebe1981cee87b47fcd9ef5c22cab7ed7644a39a94", size = 225703, upload-time = "2026-07-06T21:33:46.374Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/78/aa01ac599a8a4322533d45a1f9bc93b338276d2d59dabbe7c6d92a775c81/cffi-2.1.0-cp314-cp314t-win32.whl", hash = "sha256:7d034dcffa09e9a46c93fa3a3be402096cb5354ac6e41ab8e5cc9cd8b642ad76", size = 182857, upload-time = "2026-07-06T21:33:47.696Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/26/d00496b22de4d4228f32dde94ad996f350c8aad676d63bcca0743c8dea4d/cffi-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0582a58f3051372229ca8e7f5f589f9e5632678208d8636fea3676711fdf7fe5", size = 194065, upload-time = "2026-07-06T21:33:48.953Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/dd/0c7dbf815a579ff005008a2d815a55d6bb047c349eef536d9dc53d3f0a8d/cffi-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:510aeeeac94811b138077451da1fb18b308a5feab47dd2b603af55804155e1c8", size = 186404, upload-time = "2026-07-06T21:33:50.309Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/c7/8c8c50cb11c6750051daf12164098a9a6f027ac4356967fd4d800a07f242/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:2e9dabb9abcb7ad15938c7196ad5c1718a4e6d33cc79b4c0209bdb64c4a54a5c", size = 194121, upload-time = "2026-07-06T21:33:56.109Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/99/e2/67680bf19a6b60d2bb7ff83baefa2a4c3d2d7dc0f3277034b802e1fc504c/cffi-2.1.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:37f525a7e7e50c017fdebe58b787be310ad59357ae43a053943a6e1a6c526001", size = 196820, upload-time = "2026-07-06T21:33:57.288Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/da/4bbe583a3b3a5c8c60892124fe17f3fa3656523faf0d3484eae90f091853/cffi-2.1.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:95f2954c2c9473d892eca6e0409f3568b37ab62a8eedb122461f73cc273476e3", size = 184936, upload-time = "2026-07-06T21:33:58.765Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/4b/1f4c36ab273980d7aa75bb126ea4f8971f24a96108acad3a0a084028c57b/cffi-2.1.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:cdf2448aab5f661c9315308ec8b93f4e8a1a67a3c733f8631067a2b67d5913dc", size = 185045, upload-time = "2026-07-06T21:34:00.085Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/c3/ad299dc38f3583f8d916b299f028af418a9ec98bc695fcbebeae7420691c/cffi-2.1.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:90bec57cf82089383bd06a605b3eb8daebf7e5a668520beaf6e327a83a947699", size = 222342, upload-time = "2026-07-06T21:34:01.814Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/d8/df4543cc087245044ed02ef3ad8e0a26619d0075ac7a77a12dc81177851b/cffi-2.1.0-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6274dcb2d15cef48daa73ed1be5a40d501d74dccd0cd6db364776d12cb6ba022", size = 210073, upload-time = "2026-07-06T21:34:03.255Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/0e/fac738d73728c6cea2a88a2883dca54892496cbba88a1dc1f2909cb8a6f5/cffi-2.1.0-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:2b71d409cccee78310ab5dec549aed052aaea483346e282c7b02362596e01bb0", size = 208551, upload-time = "2026-07-06T21:34:04.433Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/3f/0b04a700dd64f465c93020253a793a82c9b4dff9961f48facd0df945d9b8/cffi-2.1.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7d3538f9c0e50670f4deb93dbb696576e60590369cae2faf7de681e597a8a1f1", size = 221649, upload-time = "2026-07-06T21:34:06.157Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/7c/b7379a5704c79eda57ce075869ba70a0368d1c850f803b3c0d078d39dcaf/cffi-2.1.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:8f9ec95b8a043d3dfbc74d9abc6f7baf524dd27a8dc160b0a32ff9cdab650c28", size = 225203, upload-time = "2026-07-06T21:34:07.489Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/02/d5e6c43ea85c41bda2a184a3418f195fe7cf602967a8d2b94e085b83deef/cffi-2.1.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:af5e2915d41fe6c961694d7bfdc8562942638200f3ce2765dfb8b745cf997629", size = 223263, upload-time = "2026-07-06T21:34:08.712Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/d8/772b8259bf75749adffb1c546828978381fb516f60cf701f6c83daf60c85/cffi-2.1.0-cp315-cp315-win32.whl", hash = "sha256:0a42c688d19fca6e095a53c6a6e2295a5b050a8b289f109adab02a9e61a25de6", size = 177696, upload-time = "2026-07-06T21:34:26.355Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/dd/afa2191fc6d57fedd26e5844a2fe2fcc0bbfa00961bbaa5a41e4921e7cca/cffi-2.1.0-cp315-cp315-win_amd64.whl", hash = "sha256:bccbbb5ee76a61f9d99b5bf3846a51d7fca4b6a732fe46f89295610edaf41853", size = 187914, upload-time = "2026-07-06T21:34:27.58Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/ef/6cd4f8c671517162379dc79cfae5aea9106bc38abb89628d5c16adf6a838/cffi-2.1.0-cp315-cp315-win_arm64.whl", hash = "sha256:8d35c139744adb3e727cd51b1a18324bbe44b8bd41bf8322bca4d41289f48eda", size = 183004, upload-time = "2026-07-06T21:34:28.905Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/b6/12fc55092817a5faa26fb8c40c7f9d662e11a46ee248c137aafc42517d92/cffi-2.1.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:f9912624a0c0b834b7520d7769b3644453aabc0a7e1c839da7359f050750e9bc", size = 188378, upload-time = "2026-07-06T21:34:09.926Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/2e/cdac88979f295fde5daa69622c7d2111e56e7ceb94f211357fbe452339e4/cffi-2.1.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:df92f2aba50eb4d96718b68ef76f2e57a57b54f2fa62333496d16c6d585a85ca", size = 188319, upload-time = "2026-07-06T21:34:11.101Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/27/1d0b408497e41a74795af122d7b603c418c5fed0171450f899afd04e594f/cffi-2.1.0-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0520e1f4c35f44e209cbbb421b67eec42e6a157f59444dfb6058874ff3610e5d", size = 223904, upload-time = "2026-07-06T21:34:12.606Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/31/e115c985105dd7ffb32444505f18ceb874bb42d992af05d5dced7ecf1980/cffi-2.1.0-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:3681e031db29958a7502f5c0c9d6bbc4c36cb20f7b104086fa642d1799631ff8", size = 211554, upload-time = "2026-07-06T21:34:13.987Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/67/9e6e09409336d9e515c58367e7cfcf4f89df06ad25252675595a58eb59d5/cffi-2.1.0-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:762f99479dcb369f60ab9017ad4ab97a36a1dd7c1ee5a3b15db0f4b8659120cd", size = 210795, upload-time = "2026-07-06T21:34:15.972Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/e5/d3cc82a4a0be7902af279c04181ad038449c096734464a5ae1de3e1401bd/cffi-2.1.0-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0611e7ebf90573a535ebdc33ae9da222d037853983e13359f580fab781ca017f", size = 223843, upload-time = "2026-07-06T21:34:17.509Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/65/b434abc97ce7cecc2c640fde160507c0ecc7e21544b483ba3325d2e2ea17/cffi-2.1.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:86cf8755a791f72c85dc287128cc62d4f24d392e3f1e15837245623f4a33cccc", size = 226773, upload-time = "2026-07-06T21:34:19.05Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/9f/d4dc66ca651eb1145a133314cda721abf13cfac3d28c4a0402263ae6ad75/cffi-2.1.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:ba00f661f8ba35d075c937174e27c2c421cec3942fd2e0ea3e66996757c0fdd9", size = 225719, upload-time = "2026-07-06T21:34:20.576Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/5a/e536c528bc8057496c360c0978559a2dc45653f89dd6151078aa7d8fca1a/cffi-2.1.0-cp315-cp315t-win32.whl", hash = "sha256:cb96698e3c7413d906ce83f8ffd245ec1bd94707541f299d0ce4d6b0193e982b", size = 182760, upload-time = "2026-07-06T21:34:22.059Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/0b/0ffe8b82d3875bced5fa1e7986a7a46b748262a40ab7f60b475eb9fb1bb3/cffi-2.1.0-cp315-cp315t-win_amd64.whl", hash = "sha256:f146d154428a2523f9cc7936c02353c2459b8f6cf07d3cd1ee1c0a611109c5d5", size = 193769, upload-time = "2026-07-06T21:34:23.589Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/17/1073b53b68c9b5ca6914adf5f8bf55aacc2d3be102418c90700160ea8605/cffi-2.1.0-cp315-cp315t-win_arm64.whl", hash = "sha256:cbb7640ce37159548d2147b5b8c241f962143d4c71231431820783f4dc78f210", size = 186405, upload-time = "2026-07-06T21:34:24.857Z" }, +] + +[[package]] +name = "click" +version = "8.4.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/d4/81420972a676e8ffea40450d8c8c92943e7218a78fe9b64359836cc9876b/click-8.4.2.tar.gz", hash = "sha256:9a6cea6e60b17ebe0a44c5cc636d94f09bd66142c1cd7d8b4cd731c4917a15f6", size = 338000, upload-time = "2026-06-24T17:45:15.148Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/e2/79c688af8b210d232694e31e59da9f6ec747bae31c3f5946e4e9b98860d5/click-8.4.2-py3-none-any.whl", hash = "sha256:e6f9f66136c816745b9d65817da91d61d957fb16e02e4dcd0552553c5a197b76", size = 119243, upload-time = "2026-06-24T17:45:13.73Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "cryptography" +version = "49.0.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/99/d1c90d6041656cc6ee229dc99cd67fd0cd5aec3c5f7d72fffc27cc750054/cryptography-49.0.0.tar.gz", hash = "sha256:f89660a348f4f78a92366240a61404e337586ef7f5909a2fef59ca88ef505493", size = 854345, upload-time = "2026-06-12T20:02:30.512Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/22/adf66990e63584a68dfb50c24f48a125c07b1699899381c8151e63ed458c/cryptography-49.0.0-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:966fe0e9c67490071f14c0d2b1cb2dfb3023c5ce39457343931415f08382f2db", size = 4032100, upload-time = "2026-06-12T20:02:32.143Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/41/3797cfaf69cae04a13ee78ebd83f0678d9c02b4779d21ce24445326f1a69/cryptography-49.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:36d1709f992593689b45bda411498d62c6e365f2ca00b84657d4dadd24de16db", size = 4692978, upload-time = "2026-06-12T20:01:21.305Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e6/8b/43011f7ebe515a8aa20d61f290a326cd890c2e738e16e59eaff8d9c3a412/cryptography-49.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0e959b578856a3924bc0cbb710fc12c387b9412a951389f3ca61704a9e25f325", size = 4716422, upload-time = "2026-06-12T20:01:48.566Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/91/01ce7303a4579e6d3a6abef01bd322848e9ea7a219adcabc5048b9033571/cryptography-49.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:53ecee2e23f7169b6117e99fc8a944e5e50f79e69758a83b52a00cb98ab2b2d2", size = 4700503, upload-time = "2026-06-12T20:02:47.091Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/99/a2c95cf8293f07491e9e27c20cc4dcd18176d944e674679adeb1d0173fd6/cryptography-49.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:2eda353d8a27bcbcaa4cbed18994a74ab4d19a2ca897db188ea269ab9b71419b", size = 5309779, upload-time = "2026-06-12T20:02:08.987Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/20/2c/0622f20ff02b2ef32558733443805dc82fd4c275be01b2d19d14676f3a1b/cryptography-49.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2afe9051da7ae7bd5905da5a949280c7d2bb75682e188f650a9d0f2756b834c6", size = 4749683, upload-time = "2026-06-12T20:02:03.335Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/5b/c5246635d5fd3b64e0d45ae10e99fd32fe9676a79915ccfe5a61ba9af1a5/cryptography-49.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:0b82e28ee398a386f0807bba7884d30f25218855690f45115831bcce5d90822c", size = 4337874, upload-time = "2026-06-12T20:02:54.323Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/88/05563c7fe2e914e87d1a536d06fe83e66b4e1d95cb593e05aea375531da8/cryptography-49.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:ccac2bfebc306b862133e3bb71f3f6ee8bb525240089b2d952e4144b3a6d5da7", size = 4700283, upload-time = "2026-06-12T20:01:34.822Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/b6/d7696e4e890d6ae1469935164c9e5215c557671cb78d6e3f458ccceaa632/cryptography-49.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:d0527ce944105f257f605a827d6ebead966c752038b6e8656abb9c5edee6fc68", size = 5265844, upload-time = "2026-06-12T20:01:24.09Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a9/3c/f3ad17eecc1a57b0ba236dc01f90e783c51f4a2f35f64777cc4f47a184b2/cryptography-49.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:cbc77da8c523d5abd028635ba850a6966fcee2c82e2bf65a41d1d8afe0f98be9", size = 4749290, upload-time = "2026-06-12T20:01:30.848Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/01/339573cf1023163a400b0b5d16f6d507de413b9f60be6fd1b77feeaf6737/cryptography-49.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b87e65d263b3e5d3bb92a57e2a6638e2f31110fa7aa890c7b2dbba42248d0a3f", size = 4834612, upload-time = "2026-06-12T20:01:29.246Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/fd/577302e213a1be9468f92d1afef66fcf1ef83d516819d9992ca547f592bd/cryptography-49.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:66ec79c3904820572d7e987abdf304281f141d37ad9a489b8e97066e7b9b6459", size = 4980804, upload-time = "2026-06-12T20:01:42.853Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/09/f42b1d190c5ba75f72062a387f8030d1d75f6ab035788f1d9c4b01de6525/cryptography-49.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:e5dfc1e64de5677cec922ffa8da89c546d0415bf6efdf081842e5d44c84e1f0e", size = 3810026, upload-time = "2026-06-12T20:02:39.262Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/9e/db72b3ae7fc9cfad53e630e56c6ae83b9b6ff0bf3718ffb8012d20b3aabf/cryptography-49.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:73a205dce83953d131a4aa1e0fd917a2fd1c5b1eef251e9d7152efefcbf5caf7", size = 4013892, upload-time = "2026-06-12T20:02:10.735Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/12/c48a424f38db03027be9f7ed5c7dc5de9933dbee992865f98b13727a009d/cryptography-49.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:196ecd6a36e4e9aa10270393bb98d8df88fccee0bf1e5128b91ae4eb4375896d", size = 4678835, upload-time = "2026-06-12T20:02:48.743Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/28/8a3ad4653662c93fc44dc4e5d8fd374c25c42e07b34bbfbadf49cf57a5a8/cryptography-49.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7abcee80084cda3f7691f3eb1ce480d8df49cec637b429aa35986c1de71738aa", size = 4697239, upload-time = "2026-06-12T20:02:56.03Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/b2/2193fc74f81aee4f9b62733133b73b5176718932ed8f2e4b03fa040480a6/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:4ae387c9cb68ea569ca17e490d66d8142b81c3cc814bf179974b7d146e490bbb", size = 4685593, upload-time = "2026-06-12T20:02:50.666Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/f1/1d3eaa243bfc5de4a187b22aa8c048b3e4980bfbe830ac46e6bac2e66947/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:f37d847238971164fdbc68ade6f6574aecc9c0af714190e2083429ff68f4ce9d", size = 5289961, upload-time = "2026-06-12T20:01:46.468Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/58/39/2d51306721330c486495853eda1c567880ff036de15a14c4b74f399934af/cryptography-49.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c2bc30226390d60ea19d9f82b19db005fe0452154a23c1c410c12ea801e43561", size = 4731145, upload-time = "2026-06-12T20:02:16.832Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/17/50/983e838c7fd0d87fd8c969bcdd328edaf5f756e38df5281637424c155873/cryptography-49.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:07cab27cc7b7e0fd28e5e26bb9eeedde5c135c868b46de4a27845abe94af6122", size = 4321719, upload-time = "2026-06-12T20:02:52.611Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/f5/8f571d7e27c55bce9f76f026143bcb1e040a4233149ecca0bea5fa5dd5f7/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:b20133d204d2bb56ba047642199603876c872026ca53e79c35b83772ab2cc505", size = 4685209, upload-time = "2026-06-12T20:02:07.282Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/84/0e27016a6fc5a0886f797018b26aa42f40c09a82332bff77822a451deaaa/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b970c6da94d5bb18629db453d14f2a1300f6bf59b61e9b82377931ef95504866", size = 5246285, upload-time = "2026-06-12T20:01:32.439Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/2d/5e1fb307cb5931881516b464c98774b3f2c36b5d4bb9a2830253cf553cad/cryptography-49.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:d8ecde755e2e91bf773fc94e8c9d730cd7f2007004cb492263a794ec3899a1c8", size = 4730441, upload-time = "2026-06-12T20:02:01.469Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/c0/bff5a02ee731d207d6a1ed51732549d8c53d2bc8da1d10ec6f2844201d68/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e3fb64c420688e5319ae25113a354015abbd8dffbfbc41781a1ea66fc7622ac3", size = 4815869, upload-time = "2026-06-12T20:01:36.574Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/26/814681d14248d95d73d5c3eea0c39a94eb8302df966f670a2c60de90974b/cryptography-49.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32703d93296f5c1f4b53349ad3a250c2cae0fdecd3a3dd5d47e616d8d616af27", size = 4960948, upload-time = "2026-06-12T20:02:18.688Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/fe/93ecac273d3738939d023612ad12cca9a3740a5345d69fda04134c43fd96/cryptography-49.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:33cd0565932807baddb67b96dbee92f2c374b5c89dee09fd74079aeb8c8dba61", size = 3799153, upload-time = "2026-06-12T20:01:39.059Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/2a/5bb823f5bedcf80718cea7fbc95ec5515cca3769633c4b01a32be7f30e7c/cryptography-49.0.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:ec5e529fb80935c94fe7b729f9972b50e351a0e6b50aa294fd5cabb109fcc29a", size = 4025947, upload-time = "2026-06-12T20:01:25.745Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/df/40577043ca124e17012f408ddddaeb213b856336ac82ddb3bc915f39e29f/cryptography-49.0.0-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f78ff2c9ed8dc2d036b0f4d640e22522213d047c1b14e61205a7e55c80a494d4", size = 4692429, upload-time = "2026-06-12T20:01:53.628Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/99/2d13299eb3dd27b02dcfaafcc91d6b5cb3329f7cbd6d8f51921acd566c1a/cryptography-49.0.0-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:35b151772baff2c74cba7fa290ceaff4c3b11c0c881eb93eb5dbc05a7cfbba18", size = 4700968, upload-time = "2026-06-12T20:02:45.383Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/4d/9c0cd02f95e2602dd5e563da149ee0830abef3537be8b34dc56281ebe27a/cryptography-49.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0f21641cf4b30fca7aee061ced0ec7ad7b073518088b7c9969a297c0ae796c69", size = 4697758, upload-time = "2026-06-12T20:01:41.13Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/01/186c825898477d77e2324d5360fefe622ff1d8d1963ec0554e2cada8ec77/cryptography-49.0.0-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9e82dcc8e56052715fb18b2429e3bca4823b1629136a2084fc45a9a5cecb9b64", size = 5298863, upload-time = "2026-06-12T20:02:24.579Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/7b/62cbbab75d0659865bf0273790031544a0b16c8072d258f9428dcd8190dc/cryptography-49.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:6f2debedf9ca60cf1d5bd466475638af5130f89965605cd818484d19987d3a21", size = 4735983, upload-time = "2026-06-12T20:01:50.14Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/72/3e798c064bc39e471008075d0f9bc9daf77a80879c092e4a8e170c585ed4/cryptography-49.0.0-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:8c25ceb16df5b9435f3f6a9829204985b0e0cbee3b48aacd432c7d2c850b44d9", size = 4334173, upload-time = "2026-06-12T20:01:44.743Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/ee/6fca21d1ac73e06f8bef71940abfd4d2f6472b4bca284d770f32bd4086f6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:28d8b15e6275f12c8a207dc309dfa957903c927d08d0cc937ee3f63f200693cc", size = 4697298, upload-time = "2026-06-12T20:02:20.918Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/d0/a5fcd3515f0bae49a7b6d0413cc1bdccdcc1fc0047037a0d480642cdc5d6/cryptography-49.0.0-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6fc361c34fb6aac015ce19435876635e5c6d21db31998b0920f675f131e043b8", size = 5254338, upload-time = "2026-06-12T20:02:22.737Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/84/84fe36f19caf857d61cb7fc9c63035a47ffabd84ea12d1d393148efa3615/cryptography-49.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2400ef9c9e2299a25614eb1dea3db54a69b1349efd043bfac9c67630d136df36", size = 4735650, upload-time = "2026-06-12T20:02:41.389Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/a0/db537264e234f7273a73ec020873d6d6b39dfd8a53db78b550ca8320440e/cryptography-49.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:67e1d20ad9ef3a563c59ef22e7a8a0b8210bd26604369ea4a30a7c66aefe504e", size = 4834820, upload-time = "2026-06-12T20:01:51.847Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/77/8df9eb486495979bccecd1062e2eaf435250e84437040295b57d09048b0b/cryptography-49.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:42b0684e0e40cf26122427802486f6d93aea593612603a94fbf260c7eb1e9c1b", size = 4967968, upload-time = "2026-06-12T20:02:12.524Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/e6/f60198ea8d9dfa15fff9ed4ca02ce362f6eadd9ba757dcc50634c4257b63/cryptography-49.0.0-cp39-abi3-win_amd64.whl", hash = "sha256:026ac7423e6fa66872d3bf889be5974507da3944f866f704fa200eadacd00001", size = 3785547, upload-time = "2026-06-12T20:02:26.847Z" }, +] + +[[package]] +name = "django" +version = "4.2.30" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "asgiref" }, + { name = "sqlparse" }, + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/b5/f1a53dc68da6429d6e0345bb848161e2381a2e9f02700148911e8582c2b3/django-4.2.30.tar.gz", hash = "sha256:4ebc7a434e3819db6cf4b399fb5b3f536310a30e8486f08b66886840be84b37c", size = 10468707, upload-time = "2026-04-07T14:05:45.57Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/b7/a7c96f239cf91313a6589233fed55111c7063b26683b226802732c455dbc/django-4.2.30-py3-none-any.whl", hash = "sha256:4d07aaf1c62f9984842b67c2874ebbf7056a17be253860299b93ae1881faad65", size = 7997231, upload-time = "2026-04-07T14:05:38.241Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[[package]] +name = "httpx-sse" +version = "0.4.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/4c/751061ffa58615a32c31b2d82e8482be8dd4a89154f003147acee90f2be9/httpx_sse-0.4.3.tar.gz", hash = "sha256:9b1ed0127459a66014aec3c56bebd93da3c1bc8bb6618c8082039a44889a755d", size = 15943, upload-time = "2025-10-10T21:48:22.271Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, +] + +[[package]] +name = "idna" +version = "3.18" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.26.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/fc/e067678238fa451312d4c62bf6e6cf5ec56375422aee02f9cb5f909b3047/jsonschema-4.26.0.tar.gz", hash = "sha256:0c26707e2efad8aa1bfc5b7ce170f3fccc2e4918ff85989ba9ffa9facb2be326", size = 366583, upload-time = "2026-01-07T13:41:07.246Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/90/f63fb5873511e014207a475e2bb4e8b2e570d655b00ac19a9a0ca0a385ee/jsonschema-4.26.0-py3-none-any.whl", hash = "sha256:d489f15263b8d200f8387e64b4c3a75f06629559fb73deb8fdfb525f2dab50ce", size = 90630, upload-time = "2026-01-07T13:41:05.306Z" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.9.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/74/a633ee74eb36c44aa6d1095e7cc5569bebf04342ee146178e2d36600708b/jsonschema_specifications-2025.9.1.tar.gz", hash = "sha256:b540987f239e745613c7a9176f3edb72b832a4ac465cf02712288397832b5e8d", size = 32855, upload-time = "2025-09-08T01:34:59.186Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, +] + +[[package]] +name = "librt" +version = "0.13.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/2f/3908645ddddab7120b46295e541ead308109fa48dbec7d67d7a778870d60/librt-0.13.0.tar.gz", hash = "sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781", size = 211402, upload-time = "2026-07-08T12:26:29.834Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/f4/b2933ddae222dac338476abb872641169a5cfed2c2bb5444a5b07b32b0c3/librt-0.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30536798f4504c0fad0885b1d371b0539abb081e4570c9d7c641cb51141b49f0", size = 150990, upload-time = "2026-07-08T12:25:02.42Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/ef/db98f744ca50e6efc9c95c70ee49b77aefac31f6a3fc7c83754a42d6a74f/librt-0.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93d24ebb82aa4420b1409c389e7857bc35bd0b668007ac8172427d5c73cc8cc5", size = 155238, upload-time = "2026-07-08T12:25:03.681Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/e7/a197e7bc72baf2c61ce7fdc6906a5054dc05bd8da0819aa894e4857bf87e/librt-0.13.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cb8a1adce42d8b75485a5d56a9623a50bcab995b6079f1dac59fc44034dd93d9", size = 503073, upload-time = "2026-07-08T12:25:05.049Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/e7/7887712e27da7c1ab80fcabb1de6eb24243964f6557cae530d4b70706dbd/librt-0.13.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0763ca2ab66058174f9dee426dc64f5e0a89c24a7df8d3fe3f1836c04e25de4b", size = 496528, upload-time = "2026-07-08T12:25:06.26Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/f0/f2283385bb6b950b26a1410f4ce51ec27231e0b3a4b925c46366d218b198/librt-0.13.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b222493da6e7b6199db9bd79502436cf5a27da3c1f7fa83c7e285444fc93fd03", size = 531786, upload-time = "2026-07-08T12:25:07.658Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/11/69ac3b54766ffba5fd7e5acebfb048d66dbe1f9f2d14516c2b3edc59cf87/librt-0.13.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fadc63331f4388c3dc90090448f682a7e9feafc11481391c1e94f2f907a3976e", size = 524393, upload-time = "2026-07-08T12:25:09.121Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/5f/d72f95fd444a926a3c14b4e24979474116988dd57a45be242077c45d3c22/librt-0.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:70d9c62a4cffd9f23396cd5ef93fc5d11b31596b9b7d6306074abe3d5fcf09bd", size = 543026, upload-time = "2026-07-08T12:25:10.459Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/08/dcd9993ad192737a004ba263d549f8ea605b326b952e7d6205c7d4170b76/librt-0.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:66c0e7e6b02a155576df2c77ec933a70b72da726e248c494abf690923e624348", size = 546829, upload-time = "2026-07-08T12:25:11.716Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/d5/6d9bb2f54e4109a956b7128836529653eb9d740f784bc47ed10a02c1000e/librt-0.13.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ac04bcd3328eb91d99dfedf6a60d9c1f15d3434e6f6daf922f0420f7d90b85c7", size = 535700, upload-time = "2026-07-08T12:25:13.144Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/f2/10946922503858a359492fa27f13e86228bde702116a740ac7b3cd185f24/librt-0.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:db327e7271e653c32040b85ae6188059c924b57d7e1e29f935523fa017cd4e82", size = 573566, upload-time = "2026-07-08T12:25:14.336Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/a8/94f00e3c99479a18088af3685ea016c42f3c7d5d1964d8dbb40c08d7f1aa/librt-0.13.0-cp312-cp312-win32.whl", hash = "sha256:860bd1d8ba48456ce08feaf8d343a8aaeb2fa086f2bcaa2a923fa3f7a3ff9aa3", size = 106099, upload-time = "2026-07-08T12:25:16.159Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/7b/2da9c74c1ed25a89cc4e1c8e007ea2eb4a0f1fafa3e70d757fe3242c5c5c/librt-0.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:e54a315caf843c8d77e388cadc56ea9ded569935ee2d2347d7ea94992e5aa6fa", size = 126934, upload-time = "2026-07-08T12:25:17.275Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/65/aead61bbf3b5358593f9d4779d2a0e88eaf6ec191a6342dde36dd1df6371/librt-0.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:c718e99a0992127af84385378460db624103b559ab260435abcfe77a4e4ed1c1", size = 112236, upload-time = "2026-07-08T12:25:18.425Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/3b/18e7b63255297a2bdc9c25c8d6d4ca8eca9f63aceb1252c0f7427ac7099e/librt-0.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3", size = 151027, upload-time = "2026-07-08T12:25:19.638Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/68/e2248452c00d1a03b45fee1752cdc8f790a476efd2402b75181da88a9e61/librt-0.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c", size = 155152, upload-time = "2026-07-08T12:25:20.851Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/16/52b1c99bf19057a062aac39c900cbb81499f6f75d6c537c14463d247ba78/librt-0.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c", size = 502499, upload-time = "2026-07-08T12:25:22.055Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/54/b811151805c795f55e0dedee6ec687b75f9982a8105d240ea3910737a77b/librt-0.13.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b", size = 496108, upload-time = "2026-07-08T12:25:23.296Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/f8/094d6b2bd93f3fdaa54db54cc788c4a365333bddad65ab02e04da0b1d004/librt-0.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9", size = 531576, upload-time = "2026-07-08T12:25:24.648Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/40/541733d5755824f968f7ec39d78ffbd75d145964157ae5e69a09ec6d7326/librt-0.13.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db", size = 524390, upload-time = "2026-07-08T12:25:25.898Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/b5/255673cfdbf5ba663339d36cd863c897289ab4337577e19f9405ce059f36/librt-0.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6", size = 543053, upload-time = "2026-07-08T12:25:27.436Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/11/ab5005e9c9850710f21e354201bf090646349d3fabf5f951eaf70235729e/librt-0.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7", size = 546387, upload-time = "2026-07-08T12:25:28.65Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/04/a5d7ce1d1df1afd15ca283dcdf7530ac073e12d69ae8c40879dda96f7868/librt-0.13.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1", size = 535970, upload-time = "2026-07-08T12:25:30.171Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/76/927e267a6daa290174ac281b23c9804c8829b042ade9c6f24a065f540958/librt-0.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a", size = 573582, upload-time = "2026-07-08T12:25:31.507Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/24/b6c5213efe39c19f9e13605644d0cf063b4ddaa33ac2e45b088e23a70e2e/librt-0.13.0-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628", size = 82189, upload-time = "2026-07-08T12:25:32.675Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/00/d29736be177a906ac0b84a5b04b4fbfa22c776dc2f366de4172b0f968c08/librt-0.13.0-cp313-cp313-win32.whl", hash = "sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927", size = 106193, upload-time = "2026-07-08T12:25:33.692Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/ac/aff6fb45393cb8912f39dfb156ef6b2d1cadb207ff465fc8f66141054be8/librt-0.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650", size = 126962, upload-time = "2026-07-08T12:25:34.769Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/3a/d68cb2b334d53fd30fac81d3a489ce4ba0d9506f4df43fcf676b68352b19/librt-0.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566", size = 112127, upload-time = "2026-07-08T12:25:35.981Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/66/f49ae0d592bd45b6941e9a8bafcb6a87cddcd501ee7874707e767f01b585/librt-0.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71", size = 149818, upload-time = "2026-07-08T12:25:37.203Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/50/51c76d74014d04fb95b6506d286808984b78a2f7a41039094e6b2194ac48/librt-0.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6", size = 154071, upload-time = "2026-07-08T12:25:39.399Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/fe/f19b0f5f82d5a1f2da736586bc840abd00ce07d6388136ae80b7333883fc/librt-0.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f", size = 494168, upload-time = "2026-07-08T12:25:40.641Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/bc/b8550c75775127fd31a5f20e8775997f7b527ad661fc8ddccd7497c064f7/librt-0.13.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180", size = 491054, upload-time = "2026-07-08T12:25:41.905Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/14/4d0204867623df3f33f86efd3d3692ba5e01321443f4d6eab35a22697618/librt-0.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6", size = 523006, upload-time = "2026-07-08T12:25:43.327Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/0a/c45fc9a260934696bace1ac5df1e148ac92bd71767aee3bf7cd7a4534f4c/librt-0.13.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9", size = 515058, upload-time = "2026-07-08T12:25:44.541Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/13/0a/50c5ce45b326854ef8fa6ae4c36cf5142e5c55315eaf9e51d0ae73ac4da3/librt-0.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007", size = 534025, upload-time = "2026-07-08T12:25:45.825Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/2d/08c413c8f93fc13b8103624fce38e5caa86cd08cbbc8465870ab287af54b/librt-0.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0", size = 540557, upload-time = "2026-07-08T12:25:47.059Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/c1/93af71fb4a364952210051811dd4e40174e79656b050c89cacac18af3330/librt-0.13.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1", size = 523201, upload-time = "2026-07-08T12:25:48.392Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/6e/9766f07b676a4889d9f8bc2864e9ba5fff165653143ef4dda7df6aa34d16/librt-0.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d", size = 565740, upload-time = "2026-07-08T12:25:49.678Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/1e/664e3472ce2b6e10e9b83f29d4a36eb982ff6b5a169ae7567bba3a4c4ff5/librt-0.13.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16", size = 81611, upload-time = "2026-07-08T12:25:50.857Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2f/d4/8582a4d65e2234673685e07309d02c230b28a85724eb0acbf13f019b7f6e/librt-0.13.0-cp314-cp314-win32.whl", hash = "sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37", size = 100106, upload-time = "2026-07-08T12:25:52.03Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/ce/0cb99efe6086b46cd985dc26672166fae312a239690e75871f7fafbd3fc5/librt-0.13.0-cp314-cp314-win_amd64.whl", hash = "sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39", size = 121209, upload-time = "2026-07-08T12:25:53.166Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/85/4f3ccb083a3c9b0d42e223acdb3c3f507953324a59cdcab4826e8e2e3b89/librt-0.13.0-cp314-cp314-win_arm64.whl", hash = "sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6", size = 106404, upload-time = "2026-07-08T12:25:54.253Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/77/333191499538c8e8189de7a4cba8e6f49ee949fd6d6e6324b21fd1522466/librt-0.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5", size = 159231, upload-time = "2026-07-08T12:25:55.432Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/9e/2aa83758f22c278b837a1d8025898434ce2b8bff36678d5330ecaef56dff/librt-0.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46", size = 161300, upload-time = "2026-07-08T12:25:56.585Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bb/c0/86791e936553ca763d6b3c2fb4d31d596cd00e14fa631c283a40ba01559a/librt-0.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e", size = 582056, upload-time = "2026-07-08T12:25:58.144Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/d3/a9ec15984a185e000c4d2a16ba28bd623124ad4c38a10974c7ff78e3a893/librt-0.13.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a", size = 562758, upload-time = "2026-07-08T12:25:59.544Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/af/dbe36b78b19c06a55097f99305e4ea9458e2273e6ae16a3cbecaad7ee978/librt-0.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22", size = 602095, upload-time = "2026-07-08T12:26:00.991Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/a8/2966891b4dd2830f5203fbee92ac2c4947653a2390ba73dfa44244fad025/librt-0.13.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c", size = 593452, upload-time = "2026-07-08T12:26:02.352Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/f5/4df8bfc8405ecf8c0d525b4d69636f694bdd8620b313ec8b76e54a5926cc/librt-0.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0", size = 623729, upload-time = "2026-07-08T12:26:04.294Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/13/9ac202dffc8db06f75d06c08c2f9f6ff054be67d21272dcc078fa1cc0c57/librt-0.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04", size = 617077, upload-time = "2026-07-08T12:26:05.845Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/f0/ebe38610716aee5cb28efd95089bb90192096179802779381e1c5dcf239c/librt-0.13.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61", size = 599561, upload-time = "2026-07-08T12:26:07.21Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/5c/c2e72e236fff7abc716d5b1753b8b8cd3ea85ac46fe17d2e7c51d4e1c723/librt-0.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9", size = 645511, upload-time = "2026-07-08T12:26:08.562Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/99/6203ce619dee940d6bfbe099ec3fe4be00a68e9d60f70abf906cf124fe66/librt-0.13.0-cp314-cp314t-win32.whl", hash = "sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18", size = 104357, upload-time = "2026-07-08T12:26:09.828Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/dd/843b6314087c41657c7036d7914d8f294bdf9b580aa8513ea0588c8e9a3d/librt-0.13.0-cp314-cp314t-win_amd64.whl", hash = "sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259", size = 126998, upload-time = "2026-07-08T12:26:10.975Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/5d/3dcec2884ba1b0806d1408612555c38dd5d68e90156b59f75f6e36435c3a/librt-0.13.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99", size = 110771, upload-time = "2026-07-08T12:26:12.303Z" }, +] + +[[package]] +name = "mcp" +version = "1.28.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "anyio" }, + { name = "httpx" }, + { name = "httpx-sse" }, + { name = "jsonschema" }, + { name = "pydantic" }, + { name = "pydantic-settings" }, + { name = "pyjwt", extra = ["crypto"] }, + { name = "python-multipart" }, + { name = "pywin32", marker = "sys_platform == 'win32'" }, + { name = "sse-starlette" }, + { name = "starlette" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, + { name = "uvicorn", marker = "sys_platform != 'emscripten'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/77/9450b8f251a13affb6281997d0523c4615f8a8b35d0b21ff30db3a5aac9d/mcp-1.28.1.tar.gz", hash = "sha256:d51e36a5f5644faea4f85ea649bfffa6bc6c26770d42798ad6a3de3d2ba69683", size = 638501, upload-time = "2026-06-26T12:57:29.093Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/5e/d118fce19f87a2e7d8101c35c8ae0ec289098a4df0ff244cec23e415aca0/mcp-1.28.1-py3-none-any.whl", hash = "sha256:2726bca5e7193f61c5dde8b12500a6de2d9acf6d1a1c0be9e8c2e706437991df", size = 222620, upload-time = "2026-06-26T12:57:27.218Z" }, +] + +[[package]] +name = "mypy" +version = "2.3.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "ast-serialize" }, + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/af/4e516a05d3ca2eb9283e9ec45b2c02225c1514dd6da49fd3c9eaa6639370/mypy-2.3.0.tar.gz", hash = "sha256:465965d41cd9a2726694e983e8ce7113259327bec798115d1e1dfa2a52fb666e", size = 3988104, upload-time = "2026-07-13T11:34:53.387Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/94/0e7e592619e2133596a47cdd642534b0456545c218430bd3b9d8fefdd1b1/mypy-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d53fc67b9d28a43c6199077f49fea0f05839e36cf6158500331c9549225e5a5", size = 15026523, upload-time = "2026-07-13T11:34:49.206Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/d2/1e1731df090a857df2807177a4626863e5ac0f0256513c35780efe53986f/mypy-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fbc00cee7bdbb9291979ddc9d08034a29dfcda4932628c9bbc28c1edd589df0c", size = 14032189, upload-time = "2026-07-13T11:33:57.168Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/95/cab921f4a806e171f34113e6181dd23c55358ccf6a80741269ef594a410e/mypy-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:04e617030eca5221909c8b7d8d7fd1c637948199aa2100b2ad9813feb07e1491", size = 14198696, upload-time = "2026-07-13T11:32:12.767Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/80/e6d008bb19fe446e3662d85e0e2717bf9f2d611a2164fb29d6e067dbf46c/mypy-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56c184d2c20ca6b6378d58d1960270a767f41f5e44acbbd27f05effef4f4e1d7", size = 15286904, upload-time = "2026-07-13T11:34:27.594Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/83/94397c9293608a364aa03e8084fb34ede4ae976a260384b9b52929308135/mypy-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3961a4a34b05f7c74b0f05aa51fbfe99a2d1e126038df40318d15c8f558b7ef3", size = 15528342, upload-time = "2026-07-13T11:34:07.819Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/96/d8b37d819adec6cfccfb1fd3afc1735d94717ddeafb45536db9c6943e09b/mypy-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b1942b9314d4c784b8ea1dbab4972603290e5dd5630f06675f13aec97526bc4c", size = 11218346, upload-time = "2026-07-13T11:28:27.745Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/cd/cd9f725b19b19e5b530a154cf9bcf9e94279c5d55b3c34fb42b3aa48ea1b/mypy-2.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:be51653d7669d7d7955d613b8d0bb57d5b652eaf71a873ddf65ac87254dd2595", size = 10204525, upload-time = "2026-07-13T11:31:02.552Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/ae/f7d056eb0294586a572d0d0d89580ec633c064db520f11d37d5a2fb833bd/mypy-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:91ad22a52ae2c7e621c2f67c94d5a17f66b3209a4cff5cf8a573579835c69e97", size = 14947298, upload-time = "2026-07-13T11:27:47.734Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/d5/db3e7af01e7844d21662c6ddc1f7825ec7cb4053f0391ac02faf3638396f/mypy-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:99ac767cc5d3b64c8d0ae226ead10c96694f94e4e7da1668642225dcd4e75aac", size = 13950768, upload-time = "2026-07-13T11:27:57.726Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/fb/43c031f0190513d1ec248ed037eceb742ddd2a4d74bbf406658a28173837/mypy-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6d2c484742a4d7b0ed6d07b143375624d3b899c5749c7b3c947f56261f48a6", size = 14151586, upload-time = "2026-07-13T11:29:18.615Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/c3/f8b2ffc60883084da91be51af58e88a7ffd4ff9795acb7d902ff88d31eb1/mypy-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7da939dd335cfd2ad788bdfd081c9f4e47634ab995e5a45eb15fd1e5bc052f8b", size = 15227411, upload-time = "2026-07-13T11:30:29.904Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/2e/16b917fc7adcf03f1aadddfc93aab804ffb234b1ab09c0ffd6d92a5d34a2/mypy-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7247eb2824f996722a949530183394921ca71deb9680052a338cf53cff7925c2", size = 15478790, upload-time = "2026-07-13T11:33:14.686Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/88/aaa65a93c73d0cdae7e42f8adb302bf6885bb281302084f99d0290a35347/mypy-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:75b0984bb3cbd76bb5c9291a8671f7ae66ca3b51c7584c358fc2e923259f0757", size = 11234919, upload-time = "2026-07-13T11:33:39.28Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/19/b40de63f1a80e63bc2d40f0679a6a8dbd34e95176c8122119bdf406aa552/mypy-2.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:d78fcf900b59cb7e82cb7e3a235e31b462d9333d92285bd1e4952d355b8ffba1", size = 10201510, upload-time = "2026-07-13T11:31:52.619Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/58/fa0ae047da911f540284009b4f44b96fe09d83c076d7c103e9d645f46303/mypy-2.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ea317b060ce83e26050f8f9e4d7d6bf44ed7597c8ff9990bccffbb9d1d8522db", size = 14941909, upload-time = "2026-07-13T11:32:34.332Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/15/14/2ba1d61452d7c2a7fe12741e8d374e52b183476b07aa7f9e2a0d02b0720a/mypy-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:094af99f92638aa92852326188b85a89e50f4a472f44827c03362228482f0762", size = 13967581, upload-time = "2026-07-13T11:30:00.587Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/5a/483fb9e5ffbbb1a28dccc7b0a13d141b17ac769b6c9f488c0a0c63698962/mypy-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de121747278144fc9ae7caa2e978cf5df12aebc82933182f5b3b86081a30baef", size = 14168807, upload-time = "2026-07-13T11:28:48.6Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/77/70d7a10732063beb74ad713682cf871e88f5c5fa39bfc8beff8a524bf9cb/mypy-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37fa4de896a84e2dc9200d91e614c22563b43d1a266789d4bbac7b22ebe6192b", size = 15200144, upload-time = "2026-07-13T11:31:25.283Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/72/766218ac783be4fdfcd699b90037b63017348a3e86fb2c1fbfb18302637d/mypy-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f1b3a98dfd21058bc759bb3337d5d1f61d0fdf9f3cf9c00f4291790fb5427bff", size = 15460389, upload-time = "2026-07-13T11:29:29.077Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/4e/8a9db7411ecb8ec0cb1fd05dba432f28bafffcd38b4e887714a4a0506689/mypy-2.3.0-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:944c665d984157cb96a679dfb7a4a81dd1d36b24b9c284b699514e6e626b82d4", size = 7753664, upload-time = "2026-07-13T11:29:08.147Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/4c/c3f8bfd6ed0e5e38b5a244403b27f821d433443df5a15a278417c10a3a3c/mypy-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:4359424140d985192c778c1ce2c114a10c1ca58a381ed79cfa70d37df94b299f", size = 11417237, upload-time = "2026-07-13T11:33:47.467Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/00/89a32eaf5ccf174bc4f90db0eaea5d70636c01b8d49f384bdab2e8834390/mypy-2.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:3dd0bed92c4bdec57c42505b96416fb9e6a5aa7be84d2809bcd5f2ecec2860d7", size = 10389252, upload-time = "2026-07-13T11:31:43.81Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/56/104f93d69aa9f339b6b9d3b0a7faa699b8b466c942cf3ae86cc2a2ec0915/mypy-2.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:691fdc37132b1ae628d834f672e74de83462d9fb4aff621835767fb43a8dd373", size = 16385495, upload-time = "2026-07-13T11:29:49.818Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/03/f1d2123313f55efafdd27706960f43a771c62f1b68426c76043f3ab9ebf3/mypy-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:aec15d465d477558fd842757b487849007311cf3897849cdda0e3162ac0ac556", size = 15098155, upload-time = "2026-07-13T11:30:40.301Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/5d/d5f9200399b445e81726c4f23becee33f233aee81c72680b1ef3a258b641/mypy-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b352b7e49f5e6576009e8df730e1ff4f915cb565b851b396d2ffe2f5a6f5da88", size = 15514155, upload-time = "2026-07-13T11:34:38.569Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/ce/69977c555f08faa3190cfde44189b89dbd56861b1ab97aa18fc5f3a2e4a3/mypy-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c6c6bf687b17f90dbfcad95b960d32eaa0154c00da45f03ab50bf8952e047fe", size = 16766351, upload-time = "2026-07-13T11:33:29.195Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/92/6648b6caa3ab9e00f9ac0c2a78307805f873dd48139b24a6f6f7c3667bbf/mypy-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f4ed18f111bfe2d599bca7468e7f9251042c1c2118f762c8de2766a56d773c60", size = 17043490, upload-time = "2026-07-13T11:30:53.927Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/ab/0dc91d80f3f016634c68d451f294a97320fe903a9b6f90b9e57b3f7f1717/mypy-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:0b025a93cffb9781d231f232be07a17912f35f10a313c24f301c81e842870654", size = 12146869, upload-time = "2026-07-13T11:29:38.874Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/b5/4c964d02634ba81f4d1c84838e5c5b18ab06d13ed568960f5d6318495ccc/mypy-2.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:adebc76aab4f3495a88b41d48aa4aff0c03f2822501da76625afcca5975f19e5", size = 10965113, upload-time = "2026-07-13T11:28:07.056Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/fa/fdc54fe583ba3cafbcedfb70eeeaf03849f75b1827a07096c7bd996f582d/mypy-2.3.0-py3-none-any.whl", hash = "sha256:6b1cdb579446b60432432b2b2403a6201b4b475a004d7f488511c9ba177c9e88", size = 2753292, upload-time = "2026-07-13T11:33:18.48Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "numpy" +version = "1.26.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129, upload-time = "2024-02-06T00:26:44.495Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" }, +] + +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + +[[package]] +name = "pathspec" +version = "1.1.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "polars" +version = "1.42.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "polars-runtime-32" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/99/fe77f10a13a778705ef05b499fc708c9a0b0a3680d9eb6bc6e1b6a6b9914/polars-1.42.1.tar.gz", hash = "sha256:2fe94f3059334650bd850ae19a9c165dcd5d9cb12cd95ea04de2201662e70e8a", size = 741532, upload-time = "2026-06-30T04:57:51.504Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/6a/edd939cc6fa04b6415aaa9bf19720fc74ead81234b3d38542e0005816d4d/polars-1.42.1-py3-none-any.whl", hash = "sha256:3c0c65cdfa21a621650c4bdcbbccf93964d052fd766c3e70e84a55d961c259fd", size = 837622, upload-time = "2026-06-30T04:56:34.686Z" }, +] + +[[package]] +name = "polars-runtime-32" +version = "1.42.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/59/15bcc4dac380c6d63efa5446d8317f22671cbd6c9dadd576bd17a334c45a/polars_runtime_32-1.42.1.tar.gz", hash = "sha256:4d4809e1c1b9a6611f6944f27b24abea902b5159e6b6fa262fd716e947af5afd", size = 3045460, upload-time = "2026-06-30T04:57:52.866Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/29/16ff6e4e91d71e530d3581f45e342a9cc35072ac6b31dcbc2fa33de2569e/polars_runtime_32-1.42.1-cp310-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bbdc26d68ee5b23b0ce227fa0599220aa35b77c826b6b0a6b2d8e7f6c1c36974", size = 53117325, upload-time = "2026-06-30T04:56:37.972Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/8e/4f8296fcfd1347f1351342fecf13bf2430d7efbae2f1f45964ec7930a99e/polars_runtime_32-1.42.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f6c0288be940b607dc4a7476c01e67fb6bbee93f5f1dd42c64970274c71008ba", size = 47446251, upload-time = "2026-06-30T04:56:41.459Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/2e/0d66a7deadc453b890c3391034ca8ab4b05d0beaebbb92a7d65199fba61b/polars_runtime_32-1.42.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635d9dbcae2302ae223afb395d5cd220bffa61a53d0ab6871d17c8bc830101cf", size = 51359402, upload-time = "2026-06-30T04:56:44.595Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/10/ffb85fa380bc9c9000dc35f40f44954dde49023018501c54faab94b3a39e/polars_runtime_32-1.42.1-cp310-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d059e8e53cc114ff82f9bd791fd341dc53534a2c745e6f6aa37594c3a93f01fe", size = 57302723, upload-time = "2026-06-30T04:56:47.609Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/63/ca50adc62e44224ca5c622a842ba6f35ee87d1d40ef0df7ea2ed6c6edb08/polars_runtime_32-1.42.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f91f0b13588324905682809d270e1de5f1990c908721c8527657d77a044c9919", size = 51515673, upload-time = "2026-06-30T04:56:50.88Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/c3/08fbbf38deaa17bf34a601d327cb7451074098673c78b7c1a8538dde9794/polars_runtime_32-1.42.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8b8bf972d99d48aaaa2582e2bce966a6f43bc815bd8725d15f5cab9e2fb15d17", size = 55217259, upload-time = "2026-06-30T04:56:53.834Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/0e/51db89361668fe077a835fc579277f824ba526e7daf7b94d23d25439e0d0/polars_runtime_32-1.42.1-cp310-abi3-win_amd64.whl", hash = "sha256:e9364c26da389a8b7339e4d29e20a3d12af730247e6ed3b7804bddce2477f428", size = 52715432, upload-time = "2026-06-30T04:56:57.109Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/c5/2fb8592d691bd114de25d9c84300b23541dca7060eac11d7b4bed0327786/polars_runtime_32-1.42.1-cp310-abi3-win_arm64.whl", hash = "sha256:7051226e6b42ffc395a7a9190377cd28649fbfb991b8f85c6271f4e1cfb736fb", size = 46718300, upload-time = "2026-06-30T04:56:59.855Z" }, +] + +[[package]] +name = "pyarrow" +version = "25.0.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/f3/95428098d1fa7d04432fb750eed06b41304c2f6a5d3319985e64db2d9d41/pyarrow-25.0.0.tar.gz", hash = "sha256:d2d697008b5ec06d75952ef260c2e9a8a0f6ccfce24266c04c9c8ade927cb3b4", size = 1199181, upload-time = "2026-07-10T08:29:50.116Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/44/fdd3a4377807b7dcabe2d4b5aa99dbbc98e2e5df3f1ca4e7f0aec492d987/pyarrow-25.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:149730a3d1f0fb59d663a0b8aa210adfd9c17c27cd94a0d143e60daea8320d4e", size = 35850884, upload-time = "2026-07-10T08:26:47.357Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/71/9f053177a7709b8c90abb00a2375b916286f9f0d6cfb21a5cadd4ef811e8/pyarrow-25.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:0721332c30fdd453fdd1fc203b2ac1f4c9db5aea28fa38d41f2574c4b068b9ec", size = 37616197, upload-time = "2026-07-10T08:26:53.564Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/1a/22bfb6597dcdc861fa83c39c06e1457cb56f698940eff42fbb25de30e8e5/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fa1482b3da10cac2d4db6e26b81da543e237616af2ef6d466018b31ca586496f", size = 46841966, upload-time = "2026-07-10T08:27:07.685Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/0e/cd705c042bc4fe7022478db577fcab4abdcfabb9bc37ab7a75556b3fcb2b/pyarrow-25.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:5d1dbf24e151042f2fa3c129563f65d66674128868496fb008c4272b16bdf778", size = 50088993, upload-time = "2026-07-10T08:27:14.268Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/ee/d822e1ee31fe31ec5d057210e0605c950b975dcd8d9a332976cc859a9df8/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:20887a762dd61dcc530f93a140840ab1f6aa7836b33270e42d627ab3cf11e537", size = 49941005, upload-time = "2026-07-10T08:27:21.274Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/33/1b/207a90cc64619a095eb75a263ae069735f2810056d43c667befd573ec083/pyarrow-25.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:58d1ab556b0cea1c93fdb799b24ad58adb2f2a2788dbce782a94f64ae1a5cc9b", size = 53112355, upload-time = "2026-07-10T08:27:27.911Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/fe/81d1e5f8beed15c01e98649d5c6e2167b67fd395884a2488f18bf1cf0dba/pyarrow-25.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:3f356afe61186395c861d5cd63dc21ff7d5fa335012a4668d979257df7fea0f5", size = 27945954, upload-time = "2026-07-10T08:27:32.903Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/c8/098ce17d778fd9d29e40bb8c5f19a40cc90c3f0b46c9057b0d7993f42f54/pyarrow-25.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:8831a3ba52fa7cdb78d368d968b1dcd06171e6dff5461e16d90de91d371e47bc", size = 35844549, upload-time = "2026-07-10T08:27:37.956Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/66/24c28877219abf6263d909b1592c97ff82c59f13a59acbed11fc87c0654f/pyarrow-25.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:5f4bacb60f91dd2fca6c52f1b9a0012cd090e0294f1f781dc1881a247a352f8e", size = 37610397, upload-time = "2026-07-10T08:27:43.803Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/55/6d1d5f5aff317ec5de9421594679ed51ed828fe7e2ce209327f819d801e4/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:59516c822d5fd8e544aaa0dfe72f36fed5d4c24ea8390aab1bcd31d7e959c6be", size = 46841701, upload-time = "2026-07-10T08:27:49.741Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/5d/f790fb6965ab54c9da0dda7856abc75fd0d7648d865f8d603c111d203a64/pyarrow-25.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f9dbd83e91c239a1f5ee7ce13f108b5f6c0efbe40a4375260d8f08b43ad05e9", size = 50090118, upload-time = "2026-07-10T08:27:56.051Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/8c/faf025357ebf31bc96777f234277aa31e2aeca6dd4ecaa391f29085473c2/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18dcc8cc50b5e72eae6fcbfc6c8776c21a007176b27a3cdec5c2f5bcf126708d", size = 49945559, upload-time = "2026-07-10T08:28:01.927Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/a1/bd051871708ea99a5e0fc711926c26c6f2c6d0130c7aaac8093e34998af6/pyarrow-25.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4ec1895a87aa834c3b99b7a1e758747eb8bb57f922b32c0e0fa04afb8d6998b1", size = 53114238, upload-time = "2026-07-10T08:28:08.594Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/31/737f0c3cffcd6af647849477d1dd68045deac2e3963c3f9f211bedc48540/pyarrow-25.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:77c8d1ae46a44b4006e8db1cc977bbcc6ce4873c92f74137d68e45503b97fb18", size = 27861162, upload-time = "2026-07-10T08:28:12.975Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/c7/581ccbcdb3d897eb2893328d68db3d52eca373bf2a7e964d0a6276b8e85b/pyarrow-25.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:72132b9a8a0a1840197794d4dea26080069b6b0981c116bc078762dc9691b21b", size = 35878945, upload-time = "2026-07-10T08:28:18.222Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/d1/ccb01db7329ea0411ef4fbd9b62a04d3268b36777d4e758d5e39b91ddeab/pyarrow-25.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e009ef945e498dca2f050ea10d2e9764cb44017254826fc4574fdb8d2530173b", size = 37630854, upload-time = "2026-07-10T08:28:23.452Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/9f/2d81ba89d1e4198d0cb25fe7529de936830fdaec0db926bb52a1ef7080d4/pyarrow-25.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:f57a39dbcb416345401c2e77a4373669b45fd111a1768e6cf267a7a0607ff0ec", size = 46905617, upload-time = "2026-07-10T08:28:29.376Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/29/0ed312ec800fb536f93783215126cee4b8977dcfeccba6f0f44df0cc87d7/pyarrow-25.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:447df764beb07c544f0178a5f6b70ef44b9ecf382b3cdfad4c2d7867353c3887", size = 50119765, upload-time = "2026-07-10T08:28:35.826Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/88/cab5063ba0c4d46a9f6b4b7eb1c9029dc0302d65cd5ab3510c949a386568/pyarrow-25.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ac5dfeee59f9ceb4d45ba76e83b026c38c24334135bb329d8274baa49cec3c62", size = 50027563, upload-time = "2026-07-10T08:28:43.848Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/fb/4d24f1b7fe2e042dc4ef315ef75e4e702d8e46fe10c37e63caff00502b03/pyarrow-25.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f0f100dacf2c0f400601664a79d1a907ced4740514bb2b00917341038e2ce76f", size = 53162437, upload-time = "2026-07-10T08:28:52.819Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/65/da20806de93ca6ee91e72cb6a9b08b3ac890b46efc8d94a7326c651c4c81/pyarrow-25.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:2e093efbecb5317372f819228fa4b4e6157eee48d3f0a7b0303705ebf81a7104", size = 28613262, upload-time = "2026-07-10T08:29:47.544Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/9f/c632afb1d3ef4a7814cee236718235f3a47eac46e97eb87df40f550b6b48/pyarrow-25.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:26be35b80780d2d21f4bae3d568b1666337c3a89722cc1794c956a77017cb24e", size = 36120702, upload-time = "2026-07-10T08:28:59.577Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/0a/093d53a0e72ad06e45d6443e00651bbc2d21af4211295086cbf4d873d3b9/pyarrow-25.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:6f4812bfbf11ca7d8faf59eb8fff8bf4dd25ce3a38b62baa010cc17a0926d1b2", size = 37750674, upload-time = "2026-07-10T08:29:06.916Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/18/b37fc31a69cff4bdfb8842683def5612f551b93fff6f44375e4a4a6a5535/pyarrow-25.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:b8af8ceedf0c9c160fd2b63440f2d205b9404db85866c1217bfea601de7cfb50", size = 46912304, upload-time = "2026-07-10T08:29:14.656Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/35/5cae19ba72493e5598022468b56f6a5571f399f485bf412f157356476caa/pyarrow-25.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:c70a5fd9a82bd1a702fd482bdc62d38dcb672fb2b449b1d7c0d7d1f4be7b7bfe", size = 50073652, upload-time = "2026-07-10T08:29:22.467Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/a5/ddd508424bdfd5e6945765e9e2ffc687e2f6115972badc8ecf423076c407/pyarrow-25.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0490a7f8b38ffe11cc26526b50c65d111cb54ddac3717cec781806793f1244dc", size = 50058654, upload-time = "2026-07-10T08:29:29.689Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/a4/324d0db203ff5eebe8694ec2d6ec5a23f9aaa5d02e5b8c692914c518c33c/pyarrow-25.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e83916bbcf380866b4e14255850b33323ff678dc9758411d0409cdd2523880b0", size = 53140153, upload-time = "2026-07-10T08:29:36.041Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/8d/d236e9c82fe315f9128885c8be3ec719f41965a1eb6b6f4b42470904cd41/pyarrow-25.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:13240f0d3dc5932ccd0bfa90cd76d835680b9d94a7661c635df4b703d40ce849", size = 28743657, upload-time = "2026-07-10T08:29:42.742Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + +[[package]] +name = "pydantic" +version = "2.13.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.46.4" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" }, +] + +[[package]] +name = "pydantic-settings" +version = "2.14.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/b5/8f48e906c3e0205276e8bd8cb7512217a87b2685304d64be27cad5b3019f/pydantic_settings-2.14.2.tar.gz", hash = "sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f", size = 237700, upload-time = "2026-06-19T13:44:56.324Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/c1/6e422f34e569cf8e18df68d1939c81c099d2b61e4f7d9621c8a77560799c/pydantic_settings-2.14.2-py3-none-any.whl", hash = "sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440", size = 61715, upload-time = "2026-06-19T13:44:55.02Z" }, +] + +[[package]] +name = "pygments" +version = "2.20.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, +] + +[[package]] +name = "pyjwt" +version = "2.13.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/81/58d0ac84e1ef3a3843791d6954d94c0b33d526c75eeb1efbce9d0a4c4077/pyjwt-2.13.0.tar.gz", hash = "sha256:41571c89ca91598c79e8ef18a2d07367d4810fbbd6f637794879baf1b7703423", size = 107515, upload-time = "2026-05-21T19:54:36.618Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/5e/ecf12fdb62546d64385c158514e9b2b671f7832108ef2ecd2020ce0af2d1/pyjwt-2.13.0-py3-none-any.whl", hash = "sha256:66adcc2aff09b3f1bbd95fc1e1577df8ac8723c978552fd43304c8a290ac5728", size = 31274, upload-time = "2026-05-21T19:54:35.362Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography" }, +] + +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-mock" +version = "3.15.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/14/eb014d26be205d38ad5ad20d9a80f7d201472e08167f0bb4361e251084a9/pytest_mock-3.15.1.tar.gz", hash = "sha256:1849a238f6f396da19762269de72cb1814ab44416fa73a8686deac10b0d87a0f", size = 34036, upload-time = "2025-09-16T16:37:27.081Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/cc/06253936f4a7fa2e0f48dfe6d851d9c56df896a9ab09ac019d70b760619c/pytest_mock-3.15.1-py3-none-any.whl", hash = "sha256:0a25e2eb88fe5168d535041d09a4529a188176ae608a6d249ee65abc0949630d", size = 10095, upload-time = "2025-09-16T16:37:25.734Z" }, +] + +[[package]] +name = "python-dotenv" +version = "1.2.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/ed/0301aeeac3e5353ef3d94b6ec08bbcabd04a72018415dcb29e588514bba8/python_dotenv-1.2.2.tar.gz", hash = "sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3", size = 50135, upload-time = "2026-03-01T16:00:26.196Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.32" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/42/55c32bb9b12693c092ad250a0e82edb5b31ddeda6eb772de5f308b3804ad/python_multipart-0.0.32.tar.gz", hash = "sha256:be54b7f3fa167bb83e4fcd936b887b708f4e57fe75911c02aebf53efaf8d938e", size = 46881, upload-time = "2026-06-04T16:18:58.647Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e1/04/e8135ebd1ad02c56ec633277529b2602ff99ff634be76cdba5744cf554fd/python_multipart-0.0.32-py3-none-any.whl", hash = "sha256:ff6d3f776f16878c894e52e107296ffc890e913c611b1a4ec6c44e2821fe2e23", size = 30042, upload-time = "2026-06-04T16:18:57.319Z" }, +] + +[[package]] +name = "pywin32" +version = "312" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/ff/32aa7d2ed0ab12b323aaa64f9b75e6ad4f8fd09f9ccfc28c79414d46838d/pywin32-312-cp312-cp312-win32.whl", hash = "sha256:dab4f65ac9c4e48400a2a0530c46c3c579cd5905ecd11b80692373915269208b", size = 6371877, upload-time = "2026-06-04T07:49:28.836Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/d9/77040d3b43df3f3be32ea289433d660d2727f5ba327bc73be835127d9d60/pywin32-312-cp312-cp312-win_amd64.whl", hash = "sha256:b457f6d628a47e8a7346ce22acb7e1a46a4a78b52e1d17e1af56871bd19a93bc", size = 6914841, upload-time = "2026-06-04T07:49:31.85Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/cc/7b1ec671775756020a0ee7f4feeaf3c568f0ab86bd3900088cf986937a92/pywin32-312-cp312-cp312-win_arm64.whl", hash = "sha256:6017c58e12f6809fbb0555b75df144c2922a9ffd18e4b9b5afa863b6c1a9d950", size = 6727901, upload-time = "2026-06-04T07:49:34.244Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/41/12fbfd7f36ed2146d8bc9de96c2741296bf0d490b98508496cff322e274c/pywin32-312-cp313-cp313-win32.whl", hash = "sha256:7a27df850933d16a8eabfbaeb73d52b273e2da667f80d70b01a89d1f6828d02c", size = 6370184, upload-time = "2026-06-04T07:49:36.253Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/db/36a78e3403099d31d9746d13fdcde5accc43c1155f375a34d15983a479a7/pywin32-312-cp313-cp313-win_amd64.whl", hash = "sha256:c53e878d15a1c44788082bfe712a905433473aa38f86375b7cf8b45e3acbaaf9", size = 6914298, upload-time = "2026-06-04T07:49:38.876Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/37/c1697194092b76de9ed47ca124323f02c57ffc8a45c06f88a3d5acaf01eb/pywin32-312-cp313-cp313-win_arm64.whl", hash = "sha256:59aba5d5940842075343a5ddc6b11f1cdf0d1567fe745290359dfbcc7c2eb831", size = 6727640, upload-time = "2026-06-04T07:49:41.083Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/2b/1f3cded5822fd49c02f40544cbb5f58c7cfd6b1694869fd476cb6170ee97/pywin32-312-cp314-cp314-win32.whl", hash = "sha256:a77a90fbb6881238d2ca9c6fd797b25817f3768fe78d214a90137ff055a75f5b", size = 6468928, upload-time = "2026-06-04T07:49:43.188Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/82/3bf86d2e2808902013132e1ce905a7da0da53790f3836c64bf44d55e24f3/pywin32-312-cp314-cp314-win_amd64.whl", hash = "sha256:a4dd3a848290ef724347b19f301045831d8e802fa4464f491b98b1e0a081432e", size = 7024157, upload-time = "2026-06-04T07:49:45.34Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/0e/73f6d6800b4f27655abd9e9f6aaeaefcddb2b946e4674efa2bab184a7f7b/pywin32-312-cp314-cp314-win_arm64.whl", hash = "sha256:9fce94568364e0155e6dfb781ac5d95903be8baf28670632beab1b523f300daa", size = 6839598, upload-time = "2026-06-04T07:49:47.613Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/61/caa39686032d2ebdd04ff0ab5cbe163126c0066d98e00c9018646e42393b/pywin32-312-cp315-cp315-win32.whl", hash = "sha256:5c1fbe4a937a73ae9297384a3da38518cbc694c68ad8a809b2e19acd350f03ed", size = 6471159, upload-time = "2026-06-04T07:49:50.035Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/cd/7e1de64a4a6f69c04214169657ccab0d93a670ea50e35eb8f489d7378249/pywin32-312-cp315-cp315-win_amd64.whl", hash = "sha256:c2f03a0f73f804a13c2735b99392b0cd426bb4f2c4d0178e5ac966a0f21618d5", size = 7025293, upload-time = "2026-06-04T07:49:54.857Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "referencing" +version = "0.37.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/f5/df4e9027acead3ecc63e50fe1e36aca1523e1719559c499951bb4b53188f/referencing-0.37.0.tar.gz", hash = "sha256:44aefc3142c5b842538163acb373e24cce6632bd54bdb01b21ad5863489f50d8", size = 78036, upload-time = "2025-10-13T15:30:48.871Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2c/58/ca301544e1fa93ed4f80d724bf5b194f6e4b945841c5bfd555878eea9fcb/referencing-0.37.0-py3-none-any.whl", hash = "sha256:381329a9f99628c9069361716891d34ad94af76e461dcb0335825aecc7692231", size = 26766, upload-time = "2025-10-13T15:30:47.625Z" }, +] + +[[package]] +name = "rpds-py" +version = "2026.6.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/be/2e8974163072e7bab7df1a5acd54c4498e75e35d6d18b864d3a9d5dadc92/rpds_py-2026.6.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a0811d33247c3d6128a3001d763f2aa056bb3425204335400ac54f89eec3a0d0", size = 343691, upload-time = "2026-06-30T07:15:14.96Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/73/319dfa745dd668efe89309141ded489126461fcecd2b8f3a3cda185129b6/rpds_py-2026.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:538949e262e46caa31ac01bdb3c1e8f642622922cacbabbae6a8445d9dc33eaf", size = 338542, upload-time = "2026-06-30T07:15:16.267Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/63/4239893be1c4d09b709b1a8f6be4188f0870084ff547f46606b8a75f1b03/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55927d532399c2c646100ff7feb48eaa940ad70f42cd68e1328f3ded9f81ca24", size = 368180, upload-time = "2026-06-30T07:15:17.62Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/ca/9c5de382225234ceb37b1844ebdb140db12b2a278bb9efe2fcd19f6c82ce/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f56f1695bc5c0871cbc33dc0130fcf503aab0c57dcc5a6700a4f49eba4f2652e", size = 375067, upload-time = "2026-06-30T07:15:18.952Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/dc/863f69d1bf04ade34b7fe0d59b9fdf6f0135fe2d7cbca74f1d665589559d/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:270b293dae9058fc9fcedab50f13cebf46fb8ed1d1d54e0521a9da5d6b211975", size = 490509, upload-time = "2026-06-30T07:15:20.434Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/ef/eac16a12048b45ec7c7fa94f2be3438a5f26bf9cc8580b18a1cfd609b7f6/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:127565fead0a10943b282957bd5447804ff3160ad79f2ad2635e6d249e380680", size = 382754, upload-time = "2026-06-30T07:15:21.831Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/8f/d2f3f532616be4d06c316ef119683e832bd3d41e112bf3a88f4151c95b17/rpds_py-2026.6.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecabd69db66de867690f9797f2f8fa27ba501bbc24540cbdbdc649cd15888ba6", size = 366189, upload-time = "2026-06-30T07:15:23.371Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/29/41a7b0e98a4b44cd676ab7598419623373eb43b20be68c084935c1a8cf88/rpds_py-2026.6.3-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:58eadac9cd119677b60e1cf8ac4052f35949d71b8a9e5556efccbe82533cf22a", size = 377750, upload-time = "2026-06-30T07:15:24.659Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/05/ecda0bec46f9a1565090bcdc941d023f6a25aff85fda28f89f8d19878152/rpds_py-2026.6.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7491ee23305ac3eb59e492b6945881f5cd77a6f731061a3f25b77fd40f9e99a4", size = 395576, upload-time = "2026-06-30T07:15:25.987Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/a8/6ed52f03ee6cb854ce78785cc9a9a672eb880e83fd7224d471f667d151f1/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2c99f7e8ccb3dd6e3e4bfeac657a7b208c9bac8075f4b078c02d7404c34107fa", size = 543807, upload-time = "2026-06-30T07:15:27.356Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/d6/156c0d3eea27ba09b92562ba2364ba124c0a061b199e17eac637cd25a5e2/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:62698275682bf121181861295c9181e789030a2d516071f5b8f3c23c170cd0fc", size = 611187, upload-time = "2026-06-30T07:15:28.931Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/31/774212ed989c62f7f310220089f9b0a3fb8f40f5443d1727abd5d9f52bc9/rpds_py-2026.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a214c993455f99a89aaeadc9b21241900037adc9d97203e374d75513c5911822", size = 573030, upload-time = "2026-06-30T07:15:30.553Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/50/22f73127a41f1ce4f87fe39aadfb9a126345801c274aa93ae88456249327/rpds_py-2026.6.3-cp312-cp312-win32.whl", hash = "sha256:501f9f04a588d6a09179368c57071301445191767c64e4b52a6aa9871f1ef5ed", size = 202185, upload-time = "2026-06-30T07:15:32.027Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/3a/f0ee4d4dde9d3b69dedf1b5f74e7a40017046d55052d173e418c6a94f960/rpds_py-2026.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:2c958bf94822e9290a40aaf2a822d4bc5c88099093e3948ad6c571eca9272e5f", size = 220394, upload-time = "2026-06-30T07:15:33.359Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/83/3382fe37f809b59f02aac04dbc4e765b480b46ee0227ed516e3bdc4d3dfc/rpds_py-2026.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:22bffe6042b9bcb0822bcd1955ec00e245daf17b4344e4ed8e9551b976b63e96", size = 215753, upload-time = "2026-06-30T07:15:34.778Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/9e/b818ee580026ec578138e961027a68820c40afeb1ec8f6819b54fb99e196/rpds_py-2026.6.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3cfe765c1da0072636ca06628261e0ea05688e160d5c8a03e0217c3854037223", size = 343012, upload-time = "2026-06-30T07:15:36.005Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/6b/686d9dc4359a8f163cfbbf89ee0b4e586431de22fe8248edb63a8cf50d49/rpds_py-2026.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f4d78253f6996be4901669ad25319f842f740eccf4d58e3c7f3dd39e6dde1d8f", size = 338203, upload-time = "2026-06-30T07:15:37.462Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/9b/069aa329940f8207615e091f5eedbbd40e1e15eac68a0790fd05ccdf796c/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:54f45a148e28767bf343d33a684693c70e451c6f4c0e9904709a723fafbdfc1f", size = 367984, upload-time = "2026-06-30T07:15:39.008Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/db/34c203e4becff3703e4d3bc121842c00b8689197f398161203a880052f4e/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:842e7b070435622248c7a2c44ae53fa1440e073cc3023bc919fed570884097a7", size = 374815, upload-time = "2026-06-30T07:15:40.253Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/7d/8071067d2cc453d916ad836e828c943f575e8a44612537759002a1e07381/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8020133a74bd81b4572dd8e4be028a6b1ebcd70e6726edc3918008c08bee6ee6", size = 490545, upload-time = "2026-06-30T07:15:41.729Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/42/da06c5aa8f0484ff07f270787434204d9f4535e2f8c3b51ed402267e63c3/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cdc7e35386f3847df728fbcb5e887e2d79c19e2fa1eba9e51b6621d23e3243af", size = 382828, upload-time = "2026-06-30T07:15:43.327Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/d7/fe978efc2ae50abe48eb7464668ea99f53c010c60aeebb7b35ad27f23661/rpds_py-2026.6.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acac386b453c2516111b50985d60ce46e7fadb5ea71ae7b25f4c946935bf27cf", size = 365678, upload-time = "2026-06-30T07:15:44.992Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/9d/1d8922e1990b2a6eb532b6ff53d3e73d2b3bbffc84116c75826bee73dfc6/rpds_py-2026.6.3-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:425560c6fa0415f27261727bb20bd097568485e5eb0c121f1949417d1c516885", size = 377811, upload-time = "2026-06-30T07:15:46.523Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/3d/198dceafb4fb034a6a47347e1b0735d34e0bd4a50be4e898d408ee66cb14/rpds_py-2026.6.3-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a550fb4950a06dde3beb4721f5ad4b25bf4513784665b0a8522c792e2bd822a4", size = 395382, upload-time = "2026-06-30T07:15:47.955Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1f/f1/13968e49655d40b6b19d8b9140296bbc6f1d86b3f0f6c346cf9f1adddf4b/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4f4bca01b63096f606e095734dd56e74e175f94cfbf24ff3d63281cec61f7bb7", size = 543832, upload-time = "2026-06-30T07:15:49.33Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/ab/289bcb1b90bd3e40a2900c561fa0e2087345ecbb094f0b870f2345142b7c/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ccffae9a092a00deb7efd545fe5e2c33c33b88e7c054337e9a74c179347d0b7d", size = 611011, upload-time = "2026-06-30T07:15:50.847Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/16/5043105e679436ccfbc8e5e0dd2d663ed18a8b8113515fd06a5e5d77c83e/rpds_py-2026.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1cf01971c4f2c5553b772a542e4aaf191789cd331bc2cd4ff0e6e65ba49e1e97", size = 572431, upload-time = "2026-06-30T07:15:52.394Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/ed/adab103321c0a6565d5ae1c2998349bc3ee175b82ccc5ae8fc04cc413075/rpds_py-2026.6.3-cp313-cp313-win32.whl", hash = "sha256:8c3d1e9c15b9d51ca0391e13da1a25a0a4df3c58a37c9dc368e0736cf7f69df0", size = 201710, upload-time = "2026-06-30T07:15:53.894Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/ed/a03b09668e74e5dabbf2e211f6468e1820c0552f7b0500082da31841bf7b/rpds_py-2026.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:9250a9a0a6fd4648b3f868da8d91a4c52b5811a62df58e753d50ae4454a36f80", size = 219454, upload-time = "2026-06-30T07:15:55.25Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/17/b8642c12930b71bc2b25831f6708ccf0f75abcd11883932ec9ce54ba3a78/rpds_py-2026.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:900a67df3fd1660b035a4761c4ce73c382ea6b35f90f9863c36c6fd8bf8b09bb", size = 215063, upload-time = "2026-06-30T07:15:56.573Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b6/36/7fbe9dcdaf857fb3f63c2a2284b62492d95f5e8334e947e5fb6e7f68c9be/rpds_py-2026.6.3-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:931908d9fc855d8f74783377822be318edb6dcb19e47169dc038f9a1bf60b06e", size = 344510, upload-time = "2026-06-30T07:15:57.921Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/54/f785cc3d3f60839ca57a5af4927a9f347b07b2799c373fc20f7949f87c7e/rpds_py-2026.6.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:d7469697dce35be237db177d42e2a2ee26e6dcc5fc052078a6fefabd288c6edd", size = 339495, upload-time = "2026-06-30T07:15:59.238Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/ef/d4cdaf309e6b095b43597103cf8c0b951d6cca2acce68c474f75ec12e0c7/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcfbcf66006befb9fd2aeaa9e01feaf881b4dc330a02ba07d2322b1c11be7b5d", size = 369454, upload-time = "2026-06-30T07:16:01.021Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/4a/9559a68b7ee15db09d7981212e8c2e219d2a1d6d4faa0391d813c3496a36/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:847927daf4cffbd4e90e42bc890069897101edd015f956cb8721b3473372edda", size = 374583, upload-time = "2026-06-30T07:16:02.287Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/75/8964aa7d2c6e8ac43eba8eb6e6b0fdda1f46d39f2fc3e6aa9f2cb17f485d/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c1ef08a82bfe327cc156da694660f599923e2e6665b6d81c9c2d0ac9ffc8", size = 492919, upload-time = "2026-06-30T07:16:03.723Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/97/6908094ac804115e65aedfd90f1b5fee4eebebd3f6c4cfc5419939267565/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ae50181a047c871561212bb97f7932a2d45fb53e947bd9b57ebad85b529cbc53", size = 383725, upload-time = "2026-06-30T07:16:05.305Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d1/9c/0d1fdc2e7aba23e290d603bc494e97bd205bae262ce33c6b32a69768ed5e/rpds_py-2026.6.3-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc319e5a1de4b6913aac94bf6a2f9e847371e0a140a43dd4991db1a09bc2d504", size = 367255, upload-time = "2026-06-30T07:16:07.086Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/fe/f0209ca4a9ed074bc8acb44dfd0e81c3122e94c9689f5645b7973a866719/rpds_py-2026.6.3-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:e4316bf32babbed84e691e352faf967ce2f0f024174a8643c37c94a1080374fc", size = 379060, upload-time = "2026-06-30T07:16:08.525Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/8d/f1cc54c616b9d8897de8738aac148d20afca93f68187475fe194d09a71b9/rpds_py-2026.6.3-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8c6e5a2f750cc71c3e3b11d71661f21d6f9bc6cebc6564b1466417a1ec03ec77", size = 395960, upload-time = "2026-06-30T07:16:09.989Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/04/aafff00f73aeca2945f734f1d483c64ab8f472d0864ab02377fd8e89c3b2/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4470ce197d4090875cf6affbf1f853338387428df97c4fb7b7106317b8214698", size = 545356, upload-time = "2026-06-30T07:16:11.816Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/cc/e229663b9e4ddac5a4acbe9085dd80a71af2a5d356b8b39d6bff233f24b0/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:ea964164cc9afa72d4d9b23cc28dafae93693c0a53e0b42acbff15b22c3f9ddd", size = 612319, upload-time = "2026-06-30T07:16:13.586Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/7a/8a0e6d3e6cd066af108b71b43122c3fe158dd9eb86acac626593a2582eb1/rpds_py-2026.6.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:639c8929aa0afe81be836b04de888460d6bed38b9c54cfc18da8f6bfabf5af5d", size = 573508, upload-time = "2026-06-30T07:16:15.23Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/03/2a69ab618a789cf6cf85c86bb844c62d090e700ab1a2aa676b3741b6c516/rpds_py-2026.6.3-cp314-cp314-win32.whl", hash = "sha256:882076c00c0a608b131187055ddc5ae29f2e7eaf870d6168980420d58528a5c8", size = 202504, upload-time = "2026-06-30T07:16:16.893Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/62/a3892ba945f4e24c78f352e5de3c7620d8479f73f211406a97263d13c7d2/rpds_py-2026.6.3-cp314-cp314-win_amd64.whl", hash = "sha256:0be972be84cfcaf46c8c6edf690ca0f154ac17babf1f6a955a51579b34ad2dc5", size = 220380, upload-time = "2026-06-30T07:16:18.108Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/e7/c2bd44dc831931815ad11ebb5f430b5a0a4d3caa9de837107876c30c3432/rpds_py-2026.6.3-cp314-cp314-win_arm64.whl", hash = "sha256:2a9c6f195058cb45335e8cc3802745c603d716eb96bc9625950c1aac71c0c703", size = 215976, upload-time = "2026-06-30T07:16:19.654Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/79/9c/fff7b74bce9a091ec9a012a03f9ff5f69364eaf9451060dfc4486da2ffdd/rpds_py-2026.6.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:f90938e92afda60266da758ee7d363447f7f0138c9559f9e1811629580582d90", size = 346840, upload-time = "2026-06-30T07:16:21.268Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/77bcb1168b33704908295533d27f10eb811e9e3e193e8993dc99572211d3/rpds_py-2026.6.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ec829541c45bca16e61c7ae50c20501f213605beb75d1aba91a6ee37fbbb56a4", size = 340282, upload-time = "2026-06-30T07:16:22.875Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/3c/7a9081c7c9e645b39efe19e4ffbeccd80add246327cd9b888aecffd72317/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afd70d95892096cdb26f15a00c45907b17817577aa8d1c76b2dcc2788391f9e9", size = 370403, upload-time = "2026-06-30T07:16:24.415Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/69/af47021eb7dad6ff3396cb001c08f0f3c4d06c20253f75be6421a59fe6b7/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:29dfa0533a5d4c94d4dfa1b694fcb56c9c63aad8330ffdd816fd225d0a7a162f", size = 376055, upload-time = "2026-06-30T07:16:26.111Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/fc/a3bcf517084396a6dd258c592567a3c011ba4557f2fde23dceaf26e74f2e/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:af05d726809bff6b141be124d4c7ce998f9c9c7f30edb1f46c07aa103d540b41", size = 494419, upload-time = "2026-06-30T07:16:27.596Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/eb/13d529d1788135425c7bf207f8463458ca5d92e43f3f701365b83e9dffc1/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9826217f048f620d9a712672818bf231442c1b35d96b227a07eabd11b4bb6945", size = 384848, upload-time = "2026-06-30T07:16:29.183Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/f4/b7ac49f30013aba8f7b9566b1dd07e81de95e708c1374b7bacc5b9bc5c9c/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:536bceea4fa4acf7e1c61da2b5786304367c816c8895be71b8f537c480b0ea1f", size = 371369, upload-time = "2026-06-30T07:16:30.912Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/86/6260bafa622f788b07ddec0e52d810305c8b9b0b8c27f58a2ab04bf62b4f/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:bc0011654b91cc4fb2ae701bec0a0ba1e552c0714247fa7af6c59e0ccfa3a4e1", size = 379673, upload-time = "2026-06-30T07:16:32.486Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/c3/03f1ee79a047b48daeca157c89a18509cde22b6b951d642b9b0af1be660a/rpds_py-2026.6.3-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:539d75de9e0d536c84ff18dfeb805398e58227001ce09231a26a08b9aed1ee0e", size = 397500, upload-time = "2026-06-30T07:16:34.471Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/95/8ed0cd8c377dca12aea498f119fe639fc474d1461545c39d2b5872eb1c0f/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:166cf54d9f44fc6ceb53c7860258dde44a81406646de79f8ed3234fca3b6e538", size = 545978, upload-time = "2026-06-30T07:16:36.45Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/f2/0eb57f0eaa83f8fc152a7e03de968ab77e1f00732bebc892b190c6eebde7/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:d34c20167764fbcf927194d532dd7e0c56772f0a5f943fa5ef9e9afbba8fb9db", size = 613350, upload-time = "2026-06-30T07:16:38.213Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/de/e0674bdbc3ef7634989b3f854c3f34bc1f587d36e5bfdc5c378d57034619/rpds_py-2026.6.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea7bb13b7c9a29791f87a0387ba7d3ad3a6d783d827e4d3f27b40a0ff44495e2", size = 576486, upload-time = "2026-06-30T07:16:39.797Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/f6/21101359743cd136ada781e8210a85769578422ba460672eea0e29739200/rpds_py-2026.6.3-cp314-cp314t-win32.whl", hash = "sha256:6de4744d05bd1aa1be4ed7ea1189e3979196808008113bbbf899a460966b925e", size = 201068, upload-time = "2026-06-30T07:16:41.316Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a6/b2/9574d4d44f7760c2aa32d92a0a4f41698e33f5b204a0bf5c9758f52c79d5/rpds_py-2026.6.3-cp314-cp314t-win_amd64.whl", hash = "sha256:c7b9a2f8f4d8e90af72571d3d495deebdd7e3c75451f5b41719aee166e940fc2", size = 220600, upload-time = "2026-06-30T07:16:43.091Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/08/ae/f23a2697e6ee6340a578b0f136be6483657bef0c6f9497b752bb5c0964bb/rpds_py-2026.6.3-cp315-cp315-macosx_10_12_x86_64.whl", hash = "sha256:e059c5dde6452b44424bd1834557556c226b57781dee1227af23518459722b13", size = 344726, upload-time = "2026-06-30T07:16:44.5Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/63/e7b3a1a5358dd32c930a1062d8e15b67fd6e8922e81df9e91706d66ee5c8/rpds_py-2026.6.3-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2f7c26fbc5acd2522b95d4177fe4710ffd8e9b20529e703ffbf8db4d93903f05", size = 339587, upload-time = "2026-06-30T07:16:46.255Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/64/10a85681916ca55fffb91b0a211f84e34297c109243484dd6394660a8a7c/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3086b538543802f84c843911242db20447de00d8752dd0efc936dbcf02218ba", size = 369585, upload-time = "2026-06-30T07:16:48.101Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/c2/baf95c7c38823e12ba34407c5f5767a89e5cf2233895e56f608167ae9493/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f2e5c5ee828d42cb11760761c0af6507927bec42d0ad5458f97c9203b054617", size = 375479, upload-time = "2026-06-30T07:16:49.93Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/94/0aad06c72d65101e11d33528d438cda99a39ce0da99466e156158f2541d3/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed0c1e5d10cdc7135537988c74a0188da68e2f3c30813ba3744ab1e42e0480f9", size = 492418, upload-time = "2026-06-30T07:16:51.641Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b5/17/de3f5a479a1f056535d7489819639d8cd591ea6281d700390b43b1abd745/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c2642a7603ec0b16ed77da4555db3b4b472341904873788327c0b0d7b95f1bb", size = 384123, upload-time = "2026-06-30T07:16:53.622Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/7d/bf09bd1b145bb2671c03e1e6d1ab8651858d90d8c7dfeadd85a37a934fd8/rpds_py-2026.6.3-cp315-cp315-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e4320744c1ffdd95a603def63344bfab2d33edeab301c5007e7de9f9f5b3885", size = 367351, upload-time = "2026-06-30T07:16:55.241Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/ea/1bb734f314b8be319149ddee80b18bd41372bdcfbdf88d28131c0cd37719/rpds_py-2026.6.3-cp315-cp315-manylinux_2_31_riscv64.whl", hash = "sha256:a9f4645593036b81bbdb36b9c8e0ea0d1c3fee968c4d59db0344c14087ef143a", size = 378827, upload-time = "2026-06-30T07:16:56.841Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/93/d9611e5b25e26df9a3649813ed66193ace9347a7c7fc4ab7cf70e94851c0/rpds_py-2026.6.3-cp315-cp315-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e55d236be29255554da47abe5c577637db7c24a02b8b46f0ca9524c855801868", size = 395966, upload-time = "2026-06-30T07:16:58.557Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/cb/99d77e16e5534ae1d90629bbe419ba6ee170833a6a85e3aa1cc41726fbbc/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:24e9c5386e16669b674a69c156c8eeefcb578f3b3397b713b08e6d60f3c7b187", size = 545680, upload-time = "2026-06-30T07:17:00.164Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/15/11a29755f790cef7a2f755e8e14f4f0c33f39489e1893a632a2eee59672b/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_i686.whl", hash = "sha256:c60924535c75f1566b6eb75b5c31a48a43fef04fa2d0d201acbad8a9969c6107", size = 611853, upload-time = "2026-06-30T07:17:01.962Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/86/0c27547e21644da938fb530f7e1a8148dd24d02db07e7a5f2567a17ce710/rpds_py-2026.6.3-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:38a2fea2787428f811719ceb9114cb78964a3138838320c29ac39526c79c16ba", size = 573715, upload-time = "2026-06-30T07:17:03.693Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/71/4d8fcf700931815594bce892255bbd973b94efaf0fc1932b0590df18d886/rpds_py-2026.6.3-cp315-cp315-win32.whl", hash = "sha256:d483fe17f01ad64b7bf7cc38fcefff1ca9fb83f8c2b2542b68f97ffe0611b369", size = 202864, upload-time = "2026-06-30T07:17:05.746Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/62/b577562de0edbb55b2be85ce5fd09c33e386b9b13eee09833af4240fd5c4/rpds_py-2026.6.3-cp315-cp315-win_amd64.whl", hash = "sha256:67e3a721ffc5d8d2210d3671872298c4a84e4b8035cfe42ffd7cde35d772b146", size = 220430, upload-time = "2026-06-30T07:17:07.471Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/95/d6d0b2509825141eef60669a5739eec88dbc6a48053d6c92993a5704defe/rpds_py-2026.6.3-cp315-cp315-win_arm64.whl", hash = "sha256:6e84adbcf4bf841aed8116a8264b9f50b4cb3e7bd89b516122e616ac56ca269e", size = 215877, upload-time = "2026-06-30T07:17:09.008Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/bf/f3ea278f0afd615c1d0f19cb69043a41526e2bb600c2b536eb192218eb27/rpds_py-2026.6.3-cp315-cp315t-macosx_10_12_x86_64.whl", hash = "sha256:ae6dd8f10bd17aad820876d24caec9efdafd80a318d16c0a48edb5e136902c6b", size = 346933, upload-time = "2026-06-30T07:17:10.762Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/29/9907bdf1c5346763cf10b7f6852aad86652168c259def904cbe0082c5864/rpds_py-2026.6.3-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:bdbd97738551fca3917c1bd7188bec1920bb520104f28e7e1007f9ceb17b7690", size = 340274, upload-time = "2026-06-30T07:17:12.266Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/2c/8e03767b5778ef25cebf74a7a91a2c3806f8eced4c92cb7406bbe060756d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b95977e7211527ab0ba576e286d023389fbeeb32a6b7b771665d333c60e5342", size = 370763, upload-time = "2026-06-30T07:17:14.107Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/e1/df2a7e1ba2efd796af26194250b8d42c821b46592311595162af9ef0528d/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d15fde0e6fb0d88a60d221204873743e5d9f0b7d29165e62cd86d0413ad74ba6", size = 376467, upload-time = "2026-06-30T07:17:15.76Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6b/de/8a0814d1946af29cb068fb259aa8622f856df1d0bab58429448726b537f5/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a136d453475ac0fcbda502ef1e6504bd28d6d904700915d278deeab0d00fe140", size = 496689, upload-time = "2026-06-30T07:17:17.308Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/f3/f19e0c852ba13694f5a79f3b719331051573cb5693feacf8a88ffffc3a71/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f826877d462181e5eb1c26a0026b8d0cab05d99844ecb6d8bf3627a2ca0c0442", size = 385340, upload-time = "2026-06-30T07:17:18.928Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/ae/7ec3a9d2d4351f99e37bcb06b6b6f954512646bfdbf9742e1de727865daf/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:79486287de1730dbaff3dbd124d0ca4d2ef7f9d29bf2544f1f93c09b5bcbbd12", size = 372179, upload-time = "2026-06-30T07:17:20.539Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/ac/9cee911dff2aaa9a5a8354f6610bf2e6a616de9197c5fff4f54f82585f1e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_31_riscv64.whl", hash = "sha256:808345f53cb952433ca2816f1604ff3515608a81784954f38d4452acfe8e61d5", size = 379993, upload-time = "2026-06-30T07:17:22.212Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/6b/7c2a07ba88d1e9a936612f7a5d067467ed03d971d5a06f7d309dff044a7e/rpds_py-2026.6.3-cp315-cp315t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1967debc37f64f2c4dc90a7f563aec558b471966e12adcac4e1c4240496b6ebf", size = 398909, upload-time = "2026-06-30T07:17:23.66Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/97/0b/776ffcb66783637b0031f6d58d6fb55913c8b5abf00aeecd46bf933fb477/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:f0840b5b17057f7fd918b76183a4b5a0635f43e14eb2ce60dce1d4ee4707ea00", size = 546584, upload-time = "2026-06-30T07:17:25.264Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/33/ba3bc04d7092bd553c9b2b195624992d2cc4f3de1f380b7b93cbee67bd79/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_i686.whl", hash = "sha256:faa679d19a6696fd54259ad321251ad77a13e70e03dd834daa762a44fb6196ef", size = 614357, upload-time = "2026-06-30T07:17:26.888Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/71/14edf065f04630b1a8472f7653cad03f6c478bcf95ea0e6aed55451e33ea/rpds_py-2026.6.3-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:23a439f31ccbeff1574e24889128821d1f7917470e830cf6544dced1c662262a", size = 576533, upload-time = "2026-06-30T07:17:28.546Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/76/65002b08596c389105720a8c0d22298b8dc25a4baf89b2ce431343c8b1de/rpds_py-2026.6.3-cp315-cp315t-win32.whl", hash = "sha256:913ca42ccad3f8cc6e292b587ae8ae49c8c823e5dce51a736252fc7c7cdfa577", size = 201204, upload-time = "2026-06-30T07:17:30.193Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/97/d855d6b3c322d1f27e26f5241c42016b56cf01377ea8ed348285f54652f0/rpds_py-2026.6.3-cp315-cp315t-win_amd64.whl", hash = "sha256:ae3d4fe8c0b9213624fdce7279d70e3b148b682ca20719ebd193a23ebfa47324", size = 220719, upload-time = "2026-06-30T07:17:31.788Z" }, +] + +[[package]] +name = "ruff" +version = "0.15.21" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0f/36/6f65aa9989acdec45d417192d8f4e7921931d8a6cf87ac74bce3eed98a8e/ruff-0.15.21.tar.gz", hash = "sha256:d0cfc841c572283c36548f82664a54ce6565567f1b0d5b4cf2caac693d8b7500", size = 4769401, upload-time = "2026-07-09T20:01:34.005Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/c6/ede15cac6839f3dbce52565c8f5164a8210e669c7bc4decb03e5bdf47d0d/ruff-0.15.21-py3-none-linux_armv6l.whl", hash = "sha256:63ea0e965e5d73c90e95b2434beeafc70820536717f561b32ab6e777cb9bdf5d", size = 10854342, upload-time = "2026-07-09T20:00:53.998Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/9d/d825b07ee7ea9e2d61df92a860033c94e06e7300d50a1c2653aac27d24fe/ruff-0.15.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0f212c5d7d54c01bbfe6dcab02b724a39300f3e34ed7acbe995ccb320a2c58bd", size = 11139539, upload-time = "2026-07-09T20:00:57.809Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/de/3b107712e642f063c7a9e0887c427b22cb44097de5aab36c05f2e280670c/ruff-0.15.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6312e41bc96791299614995ea3a977c5857c3b5662b1ecef6755b02b87cb646", size = 10595437, upload-time = "2026-07-09T20:01:00.006Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/6f/b4523cc90ba239ede441447a19d0c968846a3012e5a0b0c5b62831a3d5e3/ruff-0.15.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:01d65b4831c6b2a4ba8ee6faa84049d44d982b7a706e622c4094c509e51673be", size = 10990053, upload-time = "2026-07-09T20:01:02.187Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/cc/c6a9872a5375f0628875481cf2f66b13d7d865bf3ca2e57f91c7e762d976/ruff-0.15.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c5a913a589120ce67933d5d05fd6ddbcc2481c6a054980ee767f7414c72b4fd", size = 10666096, upload-time = "2026-07-09T20:01:04.299Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ab/97/c621f7a17e097f1790fa3af6374138823b330b2d03fc38337945daca212c/ruff-0.15.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5ef04b681d02ad4dc9620f00f83ac5c22f652d0e9a9cfe431d219b16ad5ccc41", size = 11537011, upload-time = "2026-07-09T20:01:06.771Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/51/d928727e476e25ccc57c6f449ffd80241a651a973ad949d39cfb2a771d28/ruff-0.15.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:16d090c0740916594157e75b80d666eab8e78083b39b3b0e1d698f4670a17b86", size = 12347101, upload-time = "2026-07-09T20:01:08.859Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/88/8cd62026802b16018ad06931d87997cf795ba2a6239ab659606c87d96bf0/ruff-0.15.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a10e74757dd65004d779b73e2f3c5210156d9980b41224d50d2ebcf1db51e67", size = 11572001, upload-time = "2026-07-09T20:01:11.092Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/97/f63084cf55444fc110e8cb985ebfcc592af47f597d44453d778cb81bc156/ruff-0.15.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bab0905d2f29e0d9fbc3c373ed23db0095edaa3f71f1f4f519ec15134d9e85c8", size = 11549239, upload-time = "2026-07-09T20:01:13.27Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/77/f107da4a2874b7715914b03f09ba9c54424de3ff8a1cc5d015d3ee2ce0ac/ruff-0.15.21-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:00eca240af5789fec6fe7df74c088cc1f9644ed83027113468efba7c92b94075", size = 11535340, upload-time = "2026-07-09T20:01:15.206Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/e9/601deb322d3303a7bf212b0100ead6f2ee3f6a044d89c30f2f92bf83c731/ruff-0.15.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:262ab31557a75141325e32d3357f3597645a7f084e732b6b054dde428ecd9341", size = 10964048, upload-time = "2026-07-09T20:01:17.723Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/2e/0f2176d1e99c15192caea19c8c3a0a955246b4cb4de795042eeb616345cd/ruff-0.15.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:659c4e7a4212f83306045ec7c5e5a356d16d9a6ef4ae0c7a4d872914fc655d9d", size = 10667055, upload-time = "2026-07-09T20:01:19.73Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/60/abd74a02e0c4214f12a68becfd30af7165cfdcb0e661ecdc60bbb949c09a/ruff-0.15.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9e866eab611a5f959d36df2d10e446973a3610bc42b0c15b31dc27977d59c233", size = 11242043, upload-time = "2026-07-09T20:01:21.947Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/c6/583075d8ccabb4b229345edcaf1545eb3d8d6be90f686a479d7e94088bbf/ruff-0.15.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e89bc93c0d3803ba870b55c29671bad9dc6d94bb1eb181b056b52eb05b52854f", size = 11648064, upload-time = "2026-07-09T20:01:24.023Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3a/3c/37d0ecb729a7cc2d393ea7dce316fc585680f35d93b8d62139d7d0a3700c/ruff-0.15.21-py3-none-win32.whl", hash = "sha256:01f8d5be84823c172b389e123174f781f9daf86d6c58719d603f941932195cdd", size = 10896555, upload-time = "2026-07-09T20:01:26.941Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/b8/e43466b2a6067ce91e669068f6e28d6c719a920f014b070d5c8731725de3/ruff-0.15.21-py3-none-win_amd64.whl", hash = "sha256:d4b8d9a2f0f12b816b50447f6eccb9f4bb01a6b82c86b50fb3b5354b458dc6d3", size = 12038772, upload-time = "2026-07-09T20:01:29.497Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dd/75/e90ab9aeece218a9fc5a5bc3ec97d0ee6bb3c4ff95869463c1de58e29a1c/ruff-0.15.21-py3-none-win_arm64.whl", hash = "sha256:6e83115d4b9377c1cbc13abf0e051f069fab0ef815ea0504a8a008cee24dd0a8", size = 11375265, upload-time = "2026-07-09T20:01:31.772Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.5.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/59/44985a2bdc95c74e34fef3d10cb5d93ce13b0e2a7baefffe1b53853b502d/scikit_learn-1.5.2.tar.gz", hash = "sha256:b4237ed7b3fdd0a4882792e68ef2545d5baa50aca3bb45aa7df468138ad8f94d", size = 7001680, upload-time = "2024-09-11T15:50:10.957Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/db/b485c1ac54ff3bd9e7e6b39d3cc6609c4c76a65f52ab0a7b22b6c3ab0e9d/scikit_learn-1.5.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f932a02c3f4956dfb981391ab24bda1dbd90fe3d628e4b42caef3e041c67707a", size = 12110344, upload-time = "2024-09-11T15:49:46.253Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/1a/7deb52fa23aebb855431ad659b3c6a2e1709ece582cb3a63d66905e735fe/scikit_learn-1.5.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:3b923d119d65b7bd555c73be5423bf06c0105678ce7e1f558cb4b40b0a5502b1", size = 11033502, upload-time = "2024-09-11T15:49:48.656Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/32/4a7a205b14c11225609b75b28402c196e4396ac754dab6a81971b811781c/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f60021ec1574e56632be2a36b946f8143bf4e5e6af4a06d85281adc22938e0dd", size = 12085794, upload-time = "2024-09-11T15:49:51.388Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c6/29/044048c5e911373827c0e1d3051321b9183b2a4f8d4e2f11c08fcff83f13/scikit_learn-1.5.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:394397841449853c2290a32050382edaec3da89e35b3e03d6cc966aebc6a8ae6", size = 12945797, upload-time = "2024-09-11T15:49:53.579Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/ce/c0b912f2f31aeb1b756a6ba56bcd84dd1f8a148470526a48515a3f4d48cd/scikit_learn-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:57cc1786cfd6bd118220a92ede80270132aa353647684efa385a74244a41e3b1", size = 10985467, upload-time = "2024-09-11T15:49:56.446Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/50/8891028437858cc510e13578fe7046574a60c2aaaa92b02d64aac5b1b412/scikit_learn-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e9a702e2de732bbb20d3bad29ebd77fc05a6b427dc49964300340e4c9328b3f5", size = 12025584, upload-time = "2024-10-02T18:35:29.369Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/79/17feef8a1c14149436083bec0e61d7befb4812e272d5b20f9d79ea3e9ab1/scikit_learn-1.5.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:b0768ad641981f5d3a198430a1d31c3e044ed2e8a6f22166b4d546a5116d7908", size = 10959795, upload-time = "2024-10-02T18:35:34.22Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/c8/f08313f9e2e656bd0905930ae8bf99a573ea21c34666a813b749c338202f/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:178ddd0a5cb0044464fc1bfc4cca5b1833bfc7bb022d70b05db8530da4bb3dd3", size = 12077302, upload-time = "2024-10-02T18:35:38.911Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/48/fbfb4dc72bed0fe31fe045fb30e924909ad03f717c36694351612973b1a9/scikit_learn-1.5.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7284ade780084d94505632241bf78c44ab3b6f1e8ccab3d2af58e0e950f9c12", size = 13002811, upload-time = "2024-10-02T18:35:43.28Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/e7/0c869f9e60d225a77af90d2aefa7a4a4c0e745b149325d1450f0f0ce5399/scikit_learn-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:b7b0f9a0b1040830d38c39b91b3a44e1b643f4b36e36567b80b7c6bd2202a27f", size = 10951354, upload-time = "2024-10-02T18:35:47.954Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "sqlparse" +version = "0.5.5" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/76/437d71068094df0726366574cf3432a4ed754217b436eb7429415cf2d480/sqlparse-0.5.5.tar.gz", hash = "sha256:e20d4a9b0b8585fdf63b10d30066c7c94c5d7a7ec47c889a2d83a3caa93ff28e", size = 120815, upload-time = "2025-12-19T07:17:45.073Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/4b/359f28a903c13438ef59ebeee215fb25da53066db67b305c125f1c6d2a25/sqlparse-0.5.5-py3-none-any.whl", hash = "sha256:12a08b3bf3eec877c519589833aed092e2444e68240a3577e8e26148acc7b1ba", size = 46138, upload-time = "2025-12-19T07:17:46.573Z" }, +] + +[[package]] +name = "sse-starlette" +version = "3.4.5" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "anyio" }, + { name = "starlette" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d2/1b/bc9e3e7a72dcdad7dc7888758f5d00f56f8909ed5cfdff822bd72bb4c520/sse_starlette-3.4.5.tar.gz", hash = "sha256:83072538bc211a2f68b7b0422226c4af3e9b62e106e07034664b832ca019842a", size = 35249, upload-time = "2026-06-20T17:36:58.322Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/75/c88d3f5dafd59c791da1ce27650d30bf5b70cbf1cbf01cd00e5f9e360915/sse_starlette-3.4.5-py3-none-any.whl", hash = "sha256:e71bad53323f65573c3864a6c3bd0c1eb6e5f092b2e48082b0c35927d19ca296", size = 16518, upload-time = "2026-06-20T17:36:56.729Z" }, +] + +[[package]] +name = "starlette" +version = "1.3.1" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tls-analyzer" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "django" }, + { name = "mcp" }, + { name = "numpy" }, + { name = "polars" }, + { name = "pyarrow" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "scikit-learn" }, +] + +[package.optional-dependencies] +dev = [ + { name = "mypy" }, + { name = "pytest" }, + { name = "pytest-mock" }, + { name = "ruff" }, +] + +[package.metadata] +requires-dist = [ + { name = "django", specifier = ">=4.2,<5.0" }, + { name = "mcp", specifier = ">=1.0" }, + { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10.0" }, + { name = "numpy", specifier = ">=1.26,<2.0" }, + { name = "polars", specifier = "==1.42.1" }, + { name = "pyarrow", specifier = ">=15.0" }, + { name = "pydantic", specifier = ">=2.0" }, + { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" }, + { name = "pytest-mock", marker = "extra == 'dev'", specifier = ">=3.12.0" }, + { name = "pyyaml", specifier = ">=6.0" }, + { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4.0" }, + { name = "scikit-learn", specifier = ">=1.5,<1.6" }, +] +provides-extras = ["dev"] + +[[package]] +name = "typing-extensions" +version = "4.16.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "tzdata" +version = "2026.3" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.51.0" +source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/65/b7c6c443ccc58678c91e1e973bbe2a878591538655d6e1d47f24ba1c51f3/uvicorn-0.51.0.tar.gz", hash = "sha256:f6f4b69b657c312f516dd2d268ab9ae6f254b11e4bac504f37b2ab58b24dd0b0", size = 94412, upload-time = "2026-07-08T10:59:05.962Z" } +wheels = [ + { url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/ec/dbb7e5a6b91f86bfb9eb7d2988a2730907b6a729875b949c7f022e8b88fa/uvicorn-0.51.0-py3-none-any.whl", hash = "sha256:5d38af6cd620f2ae3849fb44fd4879e0890aa1febe8d47eb355fb45d93fe6a5b", size = 73219, upload-time = "2026-07-08T10:59:04.44Z" }, +]