Compare commits

..

No commits in common. "ef9c6f09c6627b55576dad6e5176113cd64e70b0" and "5a3951362a7c53a8ed7292adbe2f2a7c90d66134" have entirely different histories.

37 changed files with 0 additions and 4442 deletions

View File

@ -1,279 +0,0 @@
# 选品:跨平台仓序 + 拆量落表
| 文档属性 | 内容 |
| --- | --- |
| 日期 | 2026-10-02 |
| 状态 | 已锁定,本轮实施依据 |
| 范围 | `选品流程/` 规则、班次判定、表 2 落盘。不加购,不推接口 2 |
| 替代 | `选品流程/README.md` 规则节;口径文档 3.1 / 3.3 中「单仓满量、两边都写满量」的作业语义 |
| 不变 | 接口 1 收件、表结构、查询键(条形码)、两平台登录/搜索、接口 2 报文形态 |
一张 `omsId` 一个品种。`status` 仍是该品种整单采购状态。
---
## 1. 要做成什么
从表 1 领一行,两个商城都搜,**合并候选后统一判定**:
1. 硬过滤:自营仓、毛利 ≥ 4%、效期 ≥ 12 个月。库存不再是过滤项。
2. 按四级仓序(同仓低价、再高库存)排序。
3. 同一平台同一仓只留 1 条候选。
4. 按序贪心拆量,直到计划采购量凑齐。
5. 凑齐:每个仓一行写入表 2,`status=1`,数量是该仓拆到的件数,合计等于计划量。
6. 凑不齐:不写成功行,表 2 `status=2`,表 1 `deal_flag=2`。
完成标准:`选品流程` 单测全绿;用内存仓库能演示「杭州 3 + 广东 7 = 计划 10」落两行,以及「合计 9 < 10」整单失败且成功行为 0。
---
## 2. 不做什么
- 真加购、清空购物车、页面点击
- 接口 2 推送(`push_status` 仍为 0)
- 批准文号搜索、商家名模糊匹配
- 中包装 / 起订量
- 同一仓多 SKU 拆成两行
- 改表结构、改接口 1
---
## 3. 已锁定决策
| 项 | 决定 |
| --- | --- |
| 毛利 | `(salePrice - 平台价) / salePrice >= 0.04`,恰好 4% 过 |
| 拆量 | 跨仓跨平台贪心拆。凑不齐一件都不落成功行 |
| 仓序 | 见第 4 节全称表。级内顺序即距离顺序 |
| 湖南壹号、甘肃壹号云 | 移出自营集合 |
| 同一平台同一仓 | 只留 1 条(低价,再高库存)。余量给下一仓 |
| `multi_platform_flag` | 实际拆到两个 `platform_type` 时为 1;同一平台拆两仓为 0 |
| 一边故障、另一边单独凑齐 | 按成功落,不写故障平台失败行 |
| 一边故障、剩下的凑不齐 | **系统故障**(可把 `deal_flag` 改回 0 重跑),禁止写成库存不足 |
| 两边都搜到、合计不够 | 业务失败「库存不足」,该 `omsId` 不重跑选品 |
| 查询键 | 仍是条形码 |
---
## 4. 自营仓全称与顺序
商家名去掉 `[自营]`、压缩空白后必须**等于**下表某一行。对不上 = 非自营,淘汰。
| 序 | 级 | 平台 | 全称 |
| ---: | ---: | --- | --- |
| 0 | 1 | 药帮忙 | 杭州小药药自营旗舰店 |
| 1 | 1 | 药帮忙 | 长沙小药药自营旗舰店 |
| 2 | 1 | 药帮忙 | 湖北小药药自营旗舰店 |
| 3 | 2 | 1号药城 | 广东壹号药业有限公司 |
| 4 | 2 | 1号药城 | 昆山亿方药业有限公司 |
| 5 | 2 | 1号药城 | 湖北亿昊药业有限公司 |
| 6 | 3 | 药帮忙 | 济南小药药自营旗舰店 |
| 7 | 3 | 1号药城 | 天津壹号药业有限公司 |
| 8 | 3 | 1号药城 | 重庆亿昊药业有限公司 |
| 9 | 4 | 1号药城 | 辽宁亿昊药业有限公司 |
| 10 | 4 | 1号药城 | 陕西壹号药业有限公司 |
| 11 | 4 | 1号药城 | 河南壹号医药有限公司 |
| 12 | 4 | 1号药城 | 重庆壹号医药有限公司 |
| 13 | 4 | 药帮忙 | 重庆小药药自营旗舰店 |
删除 `WAREHOUSE_ALIASES` 里甘肃那条。平台字段与名单不一致(例如 1 号药城搜出杭州小药药)仍按**商家全称**定序,不按搜索来源改序。
---
## 5. 算法
计划量 `qty = int(plan_purchase_qty)`。`qty <= 0` 仍走现有 `NO_QTY`。库存按件向下取整后再拆。
### 5.1 过滤(单条候选)
`evaluate_candidate` 不再看库存。淘汰原因仅:
- `NOT_SELF_WAREHOUSE`
- `PRICE_UNPARSEABLE`
- `MARGIN_LT_4PCT`(原 `MARGIN_LT_5PCT`)
- `EXPIRY_UNPARSEABLE` / `EXPIRY_LT_12M`
- `NO_SALE_PRICE`(采购行级,先于搜品)
库存为 `None` 或向下取整后 `<= 0`:可以过过滤,但折叠和拆量时当作 0,拆不到量。
### 5.2 折叠
`key = (mall_name, vendor_norm)`。同一 key 多条时保留 `(price, -stock)` 最优的一条。其余记入 rejected,原因 `SAME_WAREHOUSE_NOT_BEST`。
### 5.3 排序
` (WAREHOUSE_RANK[vendor_norm], price, -stock) `。`price` 缺失视为 `+inf`(过滤阶段已淘汰,排序不应再见到)。
### 5.4 拆量
```
remaining = qty
lines = []
for candidate in sorted_passed:
take = min(int(stock), remaining)
if take <= 0:
continue
lines.append(Allocation(candidate, take))
remaining -= take
if remaining == 0:
break
```
库存 `None` 时 `take` 当 0,不要对 `None` 做 `int()`。
`remaining == 0` → 成功,`lines` 即为表 2 成功行。
`remaining > 0` → 见第 6 节,**丢弃 `lines`**,不落成功行。
禁止:先落部分成功行再标失败。
---
## 6. 班次:先搜齐再判定
`job.run_batch` 对一行先跑完本次启用的全部平台搜索,再调用一次统一判定。禁止再「每平台独立选仓、独立满量写入」。
每个平台的搜索结果是三种之一:
| 结果 | 含义 |
| --- | --- |
| `candidates` | 搜到 0..n 条(空列表 = 无命中,不是故障) |
| `LOGIN_FAILED:...` | 本平台本班不可用 |
| `QUERY_FAILED:...` | 本行本平台查询异常 |
无条形码:两个平台都不搜,业务失败 `NO_BAR_CODE`(与现在相同)。
统一判定:
1. 采购行缺销售价 / 数量:`SKIPPED`,不搜也行(保持现有,在搜之前拦)。
2. 把所有成功搜到的 `candidates` 合并,走第 5 节。
3. 凑齐:成功。故障平台的错误忽略,不写该平台失败行。
4. 凑不齐,且存在任一平台 `LOGIN_FAILED` / `QUERY_FAILED`:整单 **系统故障**。
5. 凑不齐,且无系统故障:业务失败。
只跑 `--platform yaoex` 时,可用候选只有 1 号药城。凑不齐且该平台无故障 → 业务失败;该平台故障 → 系统故障。
---
## 7. 落表
成功行(每条 `Allocation` 一行):
| 列 | 值 |
| --- | --- |
| `operate_type` | 1 |
| `oms_id` | 本行 |
| `platform_type` | 48 药帮忙 / 66 1号药城 |
| `supplier_name` | `vendor_raw` |
| `bar_code` / `org_code` | 原样 |
| `purchase_price` | 该候选平台价,四位小数 |
| `purchase_qty` | 该仓 `take`,**不是**计划全量 |
| `purchase_total` | 单价 × 该仓数量,四位小数 |
| `status` | 1 |
| `fail_reason` | null |
| `push_status` | 0 |
| `multi_platform_flag` | 本单成功行里出现 ≥2 个不同 `platform_type` 则为 1,否则 0 |
表 1:`deal_flag=1`,`deal_result` 形如 `选品成功:药帮忙、1号药城`(按成功行出现的商城去重,顺序:药帮忙、1号药城)。
业务失败:每个**已启用且已判定**的平台落一行 `status=2`,`purchase_qty` 仍用计划量(造数校验:数量 > 0),`purchase_price` 仍用销售价兜底。`supplier_name` 仍为「无符合规则供应商」。`multi_platform_flag` 按失败行平台数(≥2 个平台则为 1)。
业务失败原因(`fail_reason` / `deal_result`)。先看过滤后是否留下可拆候选(含库存为 0、以及被 `SAME_WAREHOUSE_NOT_BEST` 折叠掉但仍算「该仓有过过滤候选」——以折叠**之前**过过滤的集合为准):
1. `NO_BAR_CODE` / `NO_SALE_PRICE` / `NO_QTY`:沿用现有中文
2. **过过滤集合非空,但拆不满**:一律 `无符合规则商品,库存不足`。rejected 里即使同时有非自营、效期、毛利淘汰,也不改这条。
3. **过过滤集合为空**时,才按候选清单(含淘汰原因)往下看,取第一条能成立的:
- 含 `MARGIN_LT_4PCT`:`无符合规则商品,毛利不足 4%`
- 含 `EXPIRY_LT_12M` / `EXPIRY_UNPARSEABLE`:`无符合规则商品,效期不足 12 个月`
- 含 `NOT_SELF_WAREHOUSE`:`无符合规则商品,非自营仓`
- 全部无命中:`无符合规则商品,平台无命中`
- 其它:`无符合规则商品`
第 9 节必测补一条:杭州自营库存 4 + 若干非自营/效期短淘汰、计划 10 → 原因必须是库存不足,不能是非自营或效期。
系统故障:每个启用平台一行 `status=2`,原因带 `系统故障:`。有故障的平台写该平台错误;无故障但被连带失败的平台写 `系统故障:对端平台不可用,合计库存不足`。表 1 `deal_result` 以 `系统故障:` 开头。`SelectionResult.mall_errors` 按平台给出原始错误串,persist 据此写各行,禁止所有失败行共用一句。
`JobSummary` 按表 2 行计:`added` = `status=1` 行数,`skipped` = 业务失败行数,`failed` = 系统故障行数。
---
## 8. 模块怎么改
| 文件 | 改什么 |
| --- | --- |
| `rules.py` | 新名单;`MIN_MARGIN=0.04`;`evaluate_candidate` 去掉库存;新增 `allocate()`(过滤+折叠+排序+拆量);`select_warehouse` 删除或改为调用 `allocate` 后取第一仓——**禁止**再作为班次入口 |
| `pipeline.py` | 新增 `run_selection(purchase, mall_outcomes)`,一次产出整单结果。`run_one_platform` 只留给单测过渡,班次不再调用 |
| `job.py` | 先收集各平台搜索结果,再 `run_selection` |
| `persist.py` | 吃整单结果:成功按拆量多行;`multi_platform_flag` 按平台种类数;失败原因 5% → 4%,并增加库存不足 |
| `README.md` | 规则节换成本文摘要 |
| `资料/RPA订单中心接口开发口径_20260930.md` | 3.1 改为毛利 4%、库存改为拆量;3.2 示例「毛利不足 5%」改为 4%;3.3 「只凑到一部分」改为整单失败、不落部分成功 |
| `资料/采购平台选品条件优先项_20260902.txt` | 文首加「已被 2026-10-02 spec 替代」;不改历史原文 |
搜索、登录、`repo.py`、表 DDL 不动。
建议的数据结构(名称可改,语义不要改):
```python
@dataclass
class MallOutcome:
mall_name: str
candidates: list[Candidate] | None = None
failed_reason: str | None = None # LOGIN_FAILED: / QUERY_FAILED:
@dataclass
class Allocation:
candidate: Candidate
qty: int
@dataclass
class SelectionResult:
kind: str # SUCCESS / BUSINESS_FAIL / SYSTEM_FAIL
reason: str # SELECTED / STOCK_INSUFFICIENT / NO_HIT / LOGIN_FAILED:... 等
allocations: list[Allocation]
rejected: list[Rejected]
malls: list[str] # 本行启用过的平台,落失败行用
mall_errors: dict[str, str] # 平台 → LOGIN_FAILED:... / QUERY_FAILED:...
```
`reason` 在 `run_selection` / `allocate` 里用折叠**前**过过滤集合算完。persist 只做中文映射,禁止从 `rejected` 反推库存不足(库存为 0 的过过滤候选不在 rejected 里)。
`kind` 决定落表,不要用「有没有 FAILED 决策」去猜:
- `SUCCESS`:`allocations` 合计数量必须等于 `int(qty)`;表 1 `deal_flag=1`,表 2 只写成功行
- `BUSINESS_FAIL`:`allocations` 必须为空;表 1 `deal_flag=2`,原因无「系统故障」前缀,该 `omsId` 不重跑
- `SYSTEM_FAIL`:`allocations` 必须为空;表 1 `deal_flag=2`,`deal_result` 以 `系统故障:` 开头
`pipeline` 的展示字段若仍用 `结论=ADDED/SKIPPED/FAILED`,映射为 SUCCESS→ADDED、BUSINESS_FAIL→SKIPPED、SYSTEM_FAIL→FAILED。`persist` 只认 `kind`。
成功行 `platform_type` 按该候选的 `mall_name`(搜索来源)映射 48/66,仓序只按商家全称。第 4 节「不按搜索来源改序」只约束排序,不改 `platform_type`。
---
## 9. 必须覆盖的行为(单测)
规则:
- 毛利恰好 4% 过;3.9% 淘汰(销售价 100、平台价 96 过,96.1 淘汰)
- 杭州小药药优先于广东壹号
- 湖南壹号、甘肃壹号云视为非自营
- 近仓 3、次仓 20、计划 10 → 近仓 3 + 次仓 7
- 合计 9、计划 10 → 无 allocation
- 同一仓两 SKU(价 90 库存 5、价 92 库存 100)、计划 10 → 只用价 90 那条的 5,余量 5 给下一仓,不吃同仓 92 的库存
- 库存 3.9 按 3 件拆
班次 / 落表:
- 两平台拆开:药帮忙 3 + 1 号药城 7,两行 `status=1`,数量分别为 3 和 7,`multi_platform_flag=1`,表 1 `deal_flag=1`
- 只在药帮忙拆两仓:两行、同一 `platform_type=48`、`multi_platform_flag=0`
- 药帮忙登录失败、广东壹号库存 20、计划 10 → 只落 1 号药城成功一行
- 药帮忙登录失败、1 号药城库存 4、计划 10 → 系统故障,无成功行
- 两边都搜到、合计库存 4、计划 10 → 业务失败「库存不足」,无成功行
- 杭州自营库存 4 + 路边店/效期短淘汰、计划 10 → 业务失败「库存不足」(不是非自营、不是效期)
- 无条形码仍不搜
---
## 10. 风险
- 搜到落表之间库存会变。这期只落判定,真加购时再处理。
- 中包装可能导致拆出的件数加不进车。真加购时再对。
- 订单中心若假定「一个 `omsId` + 一个 `platform_type` 只有一行」,同一平台拆两仓会撞上。表结构允许多行;联调时若对方拒,再改成同平台合一行(本期不合)。

View File

@ -1,173 +0,0 @@
# 洁达 RPA × 订单中心接口——开发口径
对照开发,避免跑偏。口径锁于 2026-09-29~30。接口路径暂时空着,本阶段用造数。
依据:
- `Downloads/RPA与订单中心接口设计_20260922.xlsx`(以当时最新一份为准)
- `资料/API鉴权.doc`
- `资料/合同返利系统字段映射_20260911.xlsx`(返利总单默认值)
- `选品流程/` 既有规则
---
## 1. 总原则
- 四条作业独立:选品、加购/支付回传、合同、返利。不要一个任务从加购挂到付款、录完协议。
- 对码在订单中心做完。接口 1 进来时已有 `goodsId`,本方不再做对码。
- 订单中心推接口 1、3;本方推接口 2、4。表是落盘,不是接口本身。
- 给什么填什么。领导批不批、驳回,RPA 不盯。驳回后要再录,等他们重新推数。
- 这期只做账期客户。`customerType` 固定按 `1`。预付不做。
- 出问题、接口没写清的数,打回订单中心。不要猜着填。
---
## 2. 四张表和四个口
| 口 | 方向 | 底表 | 用途 |
| --- | --- | --- | --- |
| 1 | 订单中心 → RPA | 销售商品对码数据 | 选品收件箱 |
| 2 | RPA → 订单中心 | 加购及结算商品数据 | 加购结果、支付单 |
| 3 | 订单中心 → RPA | 采购合同数据 | 合同 + 返利源数据(行级) |
| 4 | RPA → 订单中心 | 合同协议录入结果 | 合同结果、协议结果 |
- 表 1:接口收下才有数,不是手填。选品只读待处理行。
- 幂等:接口 1 按 `omsId`,已存在回 `DUPLICATE`,不更新、不重跑。接口 3 按对方 `fid`。
- 平台:`48` 药帮忙,`66` 1 药城。
- MDM:`goodsMdmCode`、`supplierMdmCode`(及选品侧货品/客户 MDM)按必填收、必填存。
- 货主 `orgCode` 原样落表、回写带上。
---
## 3. 选品
### 3.1 入口
从表 1 捞 `deal_flag=0`,一条条处理。一张 `omsId` 一个品种。
商城查询键:**条形码**。批准文号照收,不拿去搜。
规则沿用 `选品流程/`(自营仓、毛利 ≥ 4%、效期 ≥ 12 个月;两平台合并后按仓序拆量,凑齐计划量才算成功)。毛利用 `salePrice`,拆量目标用 `planPurchaseQty`。详见 `docs/superpowers/specs/2026-10-02-selection-split-allocation-design.md`。
### 3.2 加购结果(接口 2,`operateType=1`)
`status` 只给加购用。
| 现场 | 表 1 | 表 2 | 回订单中心 |
| --- | --- | --- | --- |
| 规则过了,已加购 | 处理成功 | `status=1`,带平台/供应商/价/量 | 推送 |
| 登录挂、页面报错 | 处理失败 | `status=2`,原因写系统故障 | 推送;对方可重推再跑 |
| 规则不过,故意不买 | 处理失败 | `status=2`,原因如「无符合规则商品,毛利不足 4%」或「库存不足」 | 推送;**此 `omsId` 不重推、不重跑选品** |
不再要第三种状态码。失败原因必须能区分「业务不买」和「系统故障」。
### 3.3 一单多平台
入站一行一个药。数量要两个平台才凑得齐时:
- 表 2 多行,同一个 `omsId`
- `multi_platform_flag=1`
- **同一次报文**推回,禁止先回一个平台再补另一个
- 这组平台都判定完之前不要推
凑不齐计划量:不落部分成功行,整单 `status=2`,原因「库存不足」。一边平台故障且另一边单独凑不齐:系统故障,可重跑。
### 3.4 支付(接口 2,`operateType=2`)
采购员在商城自己付。选品任务加购回传后结束,不要挂着等付款。
另排班(对准中午 12 点、下午 5 点付完后,例如 12:30、17:30)导出已支付订单:
- 付了:推 `operateType=2`,带 `payOrderNo`、`payOrderDtlId`。有单号就是成功。`status` 填空。
- 没付:**不推** 这一条。不是支付失败。
- 没有「支付失败」回写。
- 数量单价以商城实付为准,不要把加购行原样再推一遍。
### 3.5 造数阶段
失败行的单价/数量/总价接口仍标必填且数量大于 0:先填能过校验的数。`purchaseTotal` 舍入没写:先按单价×数量四位小数。对不上再打回。
---
## 4. 合同
### 4.1 班次
一天两次(中午、下午)。任务启动瞬间冻结本班未处理名单,跑的过程中新进行留给下一班。
### 4.2 合单
按 `supplierId` 合成一份合同。不用 `omsId` 合,不用供应商名称。
- 头字段(账期、签订日、起止日、采购员、计划员)取该班该供应商**第一行**。
- 下午再推来的,哪怕同一供应商,**另开一份**。不追加、不改上午已录合同。
- `contractEntryStatus=0` 且带 `contractNo`:只录协议,不进「按供应商合」,不重开合同。
### 4.3 录入
这期一律按账期客户填(不勾预付款,付款期限用 `paymentTerm`)。尽调仍看合同系统页面;未尽调本单失败,原因「客户未尽调」,不录。
点提交(不要拿暂存号去录返利)。提交成功:
- 接口 4 `actionCode=1`,`contractStatus=1` 起草,带上合同号
- **合同任务到此结束。不要重录合同。**
同一份合同下多个 `fid`:回写同一套合同号和状态。
### 4.4 合同「成功」
接口成功定义是:合同号能在返利中心搜到。不是提交成功。
查号挂在**返利班**上,不单开查状态任务。搜到再回 `contractStatus=2`。搜不到:不重录,留给下一班再搜。
---
## 5. 返利
### 5.1 班次
与合同同一天两班,建议合同班跑完再跑。不做「合同审完订单中心再推返利包」。二次推只处理协议失败重录(带合同号)。
### 5.2 查号 + 录入
拿已有合同号、尚未回合同成功的单,以及对方推来只录协议的单,打开返利中心按合同号搜:
- 搜到:先回合同成功,再录协议
- 搜不到:本班不录协议、不重开合同,下一班再搜
总单默认值跟映射表(计划员用订单中心给的,承付「下月 N 号结」,协议结束当天等)。不要用方案 5.5 的「登录人 / 账期 / 当年最后一天」。
### 5.3 协议「成功」
现场据说无审批流,点送审即成功。点完回 `actionCode=2`、`agreementStatus=1`,带上当时的协议总单/细单号。
测试环境先点一次确认:送审后号是否马上能看见。以后若打开审批流,改成送审后下一班再查,不能再点完就回成功。
---
## 6. 联调与造数
本阶段路径空着,造数跑通落表和作业。
鉴权(调订单中心时):
- Token:`POST https://gzmpc-paas-restcloud-test.gzmpc.com/gw/rest/core/auth/token`
- 请求头:`identitytoken`
- 测试用户名:`rpa`(密码放本机环境变量,禁止写入仓库)
- 接口 2、4 真实 path 仍缺(文档 `gw/xxxx/xxxx`)
- 示例里的 `orgSecret` 是否要带,未写,真联调再问
- 订单中心调本方 1、3 的鉴权由本方定,不在这份鉴权文档里
---
## 7. 开发时不要做的
- 选品按批准文号搜商城
- 加购成功后顺手推支付
- 规则不过报 `status=1`,或报失败却让对方当故障重跑
- 多平台加购拆成两次推送
- 按 `omsId` 或「同供应商同日」另写合单
- 同一班两套账期时整组停掉(已定为取第一行照录)
- 合同搜不到号就再录一遍
- 用方案 5.5 默认值填返利总单
- 把密码、token 写进 Git

View File

@ -1,26 +0,0 @@
已被 2026-10-02 spec 替代(毛利 4%、两平台混排四级仓、按库存拆量)。本文保留 9 月 2 日原文,不要当作业口径。
选品条件优先级:
1)自营仓:勾选的自营仓
2)价格:跟销售价格对比,需要有5%以上盈利的商品;
3)有效期:大于等于12个月
4)满足库存数量
前4个选品条件必须满足后,再从仓库距离中选择。
5)仓库距离:优先选择距离近的店
1. [自营] 广东壹号药业有限公司
2. [自营] 昆山亿方药业有限公司
3. [自营] 湖北亿昊药业有限公司
4. [自营] 天津壹号药业有限公司
5. [自营] 重庆亿昊药业有限公司
6. [自营] 重庆壹号医药有限公司
7. [自营] 湖南壹号药业股份有限公司
8. [自营] 辽宁亿昊药业有限公司
9. [自营] 河南壹号医药有限公司
10. [自营] 甘肃壹号云医药有限责任公司企业店
11. [自营] 陕西壹号药业有限公司

View File

@ -1,12 +0,0 @@
YAOEX_USERNAME=
YAOEX_PASSWORD=
YBM_USERNAME=
YBM_PASSWORD=
RPA_ORACLE_HOST=
RPA_ORACLE_PORT=1521
RPA_ORACLE_SERVICE=rpa
RPA_ORACLE_USER=
RPA_ORACLE_PASSWORD=
RPA_ORACLE_THICK=1
# RPA_ORACLE_CLIENT_LIB=

View File

@ -1 +0,0 @@
3.12

View File

@ -1,3 +0,0 @@
{
"recommendations": ["ms-python.python", "ms-python.debugpy"]
}

View File

@ -1,25 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "pytest",
"type": "debugpy",
"request": "launch",
"module": "pytest",
"args": ["tests", "-v"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"justMyCode": true
},
{
"name": "选品班次",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"args": ["--limit", "5", "--keep-browser"],
"cwd": "${workspaceFolder}",
"console": "integratedTerminal",
"justMyCode": true
}
]
}

View File

@ -1,7 +0,0 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.testing.pytestEnabled": true,
"python.testing.unittestEnabled": false,
"python.testing.pytestArgs": ["tests"],
"python.terminal.activateEnvironment": true
}

View File

@ -1,78 +0,0 @@
# 洁达商城选品
班次作业:从表 1 捞 `deal_flag=0`,按**条形码**搜 1 号药城 / 药帮忙,规则选出仓,结果写入表 2。不加购,不推订单中心。
口径:`资料/RPA订单中心接口开发口径_20260930.md` 第 3 节。
## 数据流
```text
rpa_sales_goods_mapping_data (deal_flag=0)
→ 领取并置 3
→ 登录一次 / 平台
→ 按条形码搜索(批准文号只落表,不拿去搜)
→ 选品规则(两平台合并,按仓序拆量)
→ rpa_cart_settlement_goods_data(operate_type=1, push_status=0)
→ 表 1 deal_flag=1 成功 / 2 失败
```
两平台搜完后**合并统一判定**。凑齐计划量才落成功行;凑不齐不落部分成功。一边故障、另一边单独能凑齐:按成功落。一边故障且剩下的凑不齐:系统故障(可把 `deal_flag` 改回 0 重跑)。两边都搜到但合计不够:业务失败「库存不足」,该 `omsId` 不重跑。
## 规则
硬过滤三项,再按仓序拆量(同一平台同一仓只留低价那条):
1. 自营仓(商家名去 `[自营]` 后必须等于四级名单全称)
2. 毛利 `(销售价格 - 平台价) / 销售价格 >= 4%`
3. 效期 ≥ 12 个月
4. 仓序(级内由近到远):药帮忙杭州/长沙/湖北小药药 → 1 药城广东/昆山/湖北亿昊 → 济南小药药、天津壹号、重庆亿昊 → 辽宁亿昊、陕西壹号、河南壹号、重庆壹号、重庆小药药
5. 按序贪心拆量。凑齐则每个仓一行,`purchase_qty` 为该仓件数;凑不齐整单失败
`multi_platform_flag=1` 仅当成功行出现两个 `platform_type`。湖南壹号、甘肃壹号云不在自营集合。口径见 `docs/superpowers/specs/2026-10-02-selection-split-allocation-design.md`。
## 环境
```bash
cd 选品流程
uv sync --python 3.12
uv run playwright install chromium
```
Oracle 与 `rpa_oms` 同一套变量:`RPA_ORACLE_HOST` / `PORT` / `SERVICE` / `USER` / `PASSWORD`,11.2 用 thick。
商城账号(任选其一,不要提交真实密码):
- 复制 `.env.example` 为 `.env`,填 `YAOEX_USERNAME` / `YAOEX_PASSWORD` / `YBM_USERNAME` / `YBM_PASSWORD`
- 或复制 `secrets.example.json` 为 `secrets.json`
已有登录态可跳过浏览器:`--yaoex-auth auth.json`、`--ybm-cookies cookies.json`。
## 运行
```bash
cd 选品流程
uv run pytest -v
uv run python main.py --limit 5
uv run python main.py --platform yaoex
```
`--limit` 只领取本班前 N 条 `deal_flag=0`。跑的过程中新进入的留给下一班。
中途崩溃会停在 `deal_flag=3`。改回 `0` 再跑。系统故障不能靠订单中心重推同一 `oms_id`(接口 1 幂等),同样把 `deal_flag` 改回 `0`。
## 模块
| 文件 | 职责 |
| --- | --- |
| `main.py` | 登录编排 |
| `job.py` | 班次:领取 → 按行判定 → 落表 |
| `persist.py` | 判定结果 → 表 2 行 + 表 1 处理标志 |
| `repo.py` | Oracle / 内存仓库 |
| `rules.py` | 选品规则,两平台共用 |
| `pipeline.py` | 单行两平台统一判定 |
| `search_yaoex.py` / `search_ybm.py` | 搜索接口;查询键是条形码 |
| `auth_login.py` | Playwright 登录 |
## 本期不做
加购、接口 2 推送、支付回传、批准文号搜索、商家名模糊匹配。

View File

@ -1,293 +0,0 @@
from __future__ import annotations
import json
import os
import time
from pathlib import Path
from typing import Any
from http_util import parse_cookie_value
from mall_config import (
YAOEX_HOME_URL,
YAOEX_LOGIN_URL,
YBM_HOME_URL,
YBM_LOGIN_URL,
)
from search_yaoex import YaoexAuth
from util import normalize_text
PROJECT_ROOT = Path(__file__).resolve().parent
AUTH_DIR = PROJECT_ROOT / "data" / ".auth"
DEFAULT_YAOEX_AUTH_PATH = AUTH_DIR / "yaoex.json"
DEFAULT_YBM_COOKIE_PATH = AUTH_DIR / "ybm.json"
def load_dotenv(path: Path) -> None:
if not path.exists():
return
for line in path.read_text(encoding="utf-8").splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#") or "=" not in stripped:
continue
key, _, value = stripped.partition("=")
key = key.strip()
value = value.strip().strip('"').strip("'")
os.environ.setdefault(key, value)
def load_secrets(secrets_path: Path | None = None) -> dict[str, dict[str, str]]:
load_dotenv(PROJECT_ROOT / ".env")
result: dict[str, dict[str, str]] = {"yaoex": {}, "ybm": {}}
path = secrets_path or (PROJECT_ROOT / "secrets.json")
if path.exists():
data = json.loads(path.read_text(encoding="utf-8"))
if isinstance(data, dict):
for mall in ("yaoex", "ybm"):
block = data.get(mall) or {}
if isinstance(block, dict):
result[mall] = {
"username": normalize_text(block.get("username")),
"password": normalize_text(block.get("password")),
}
env_map = {
"yaoex": ("YAOEX_USERNAME", "YAOEX_PASSWORD"),
"ybm": ("YBM_USERNAME", "YBM_PASSWORD"),
}
for mall, (user_key, pass_key) in env_map.items():
result[mall]["username"] = os.environ.get(user_key) or result[mall].get("username", "")
result[mall]["password"] = os.environ.get(pass_key) or result[mall].get("password", "")
return result
def load_yaoex_auth_file(path: Path) -> YaoexAuth:
data = json.loads(path.read_text(encoding="utf-8"))
return YaoexAuth(
token=normalize_text(data.get("token")),
user_id=normalize_text(data.get("user_id")),
buyer_code=normalize_text(data.get("buyer_code")),
)
def load_cookie_file(path: Path) -> dict[str, str]:
data = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(data, dict):
raise ValueError(f"cookie 文件不是对象:{path}")
return {str(key): parse_cookie_value(str(value)) for key, value in data.items()}
def save_yaoex_auth(auth: YaoexAuth, path: Path = DEFAULT_YAOEX_AUTH_PATH) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(
json.dumps(
{"token": auth.token, "user_id": auth.user_id, "buyer_code": auth.buyer_code},
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
def save_cookie_file(cookies: dict[str, str], path: Path = DEFAULT_YBM_COOKIE_PATH) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(cookies, ensure_ascii=False, indent=2), encoding="utf-8")
def _require_account(mall_label: str, username: str, password: str) -> None:
if not username or not password:
raise SystemExit(
f"缺少{mall_label}账号。请在 .env / 环境变量或 secrets.json 中配置用户名和密码。"
)
class BrowserRuntime:
"""同一轮脚本共用一套 Playwright。keep=True 时搜完仍留着窗口。"""
def __init__(self, keep: bool = False, hold_seconds: int = 180) -> None:
self.keep = keep
self.hold_seconds = hold_seconds
self._playwright: Any = None
self._browsers: list[Any] = []
def start(self) -> None:
if self._playwright is not None:
return
from playwright.sync_api import sync_playwright
self._playwright = sync_playwright().start()
def new_page(self, *, viewport: dict[str, int] | None = None) -> tuple[Any, Any]:
self.start()
browser = self._playwright.chromium.launch(
headless=False,
channel="chrome",
args=["--new-window", "--start-maximized"],
)
self._browsers.append(browser)
context = browser.new_context(viewport=viewport or {"width": 1440, "height": 900})
page = context.new_page()
page.bring_to_front()
return context, page
def close(self) -> None:
if self.keep and self._browsers:
if self.hold_seconds <= 0:
print("浏览器保持打开,不会自动关。要关时在终端停掉进程。")
while True:
time.sleep(3600)
else:
print(f"浏览器保持打开 {self.hold_seconds} 秒,请看屏幕上的 Chrome。")
time.sleep(self.hold_seconds)
for browser in self._browsers:
try:
browser.close()
except Exception:
pass
self._browsers.clear()
if self._playwright is not None:
try:
self._playwright.stop()
except Exception:
pass
self._playwright = None
def _ensure_yaoex_logged_in(page: Any, username: str, password: str) -> None:
page.goto(YAOEX_LOGIN_URL, wait_until="domcontentloaded")
page.wait_for_timeout(2000)
if "/login" not in page.url:
return
account_input = page.locator("#username").first
password_input = page.locator("#password").first
captcha_button = page.locator(
"#captcha-box .geetest_btn_click, #captcha-box .geetest_btn, #captcha-box .geetest_radar_btn"
).first
login_button = page.locator("#login-btn").first
account_input.wait_for(state="visible", timeout=30000)
account_input.fill(username)
password_input.fill(password)
print("1号药城:已填账号。请看 Chrome——若出现滑块拼图,拖过去。窗口会等 3 分钟。")
if captcha_button.count() > 0:
captcha_button.click(force=True)
try:
page.wait_for_function(
"() => !window.location.href.includes('/login')",
timeout=20000,
)
except Exception:
try:
login_button.click(force=True)
except Exception:
pass
try:
page.wait_for_function(
"() => !window.location.href.includes('/login')",
timeout=180000,
)
except Exception:
pass
if "/login" in page.url:
login_error = ""
error_locator = page.locator(".error-tips span, .layui-layer-content, .geetest_tip")
if error_locator.count() > 0:
login_error = normalize_text(error_locator.first.inner_text())
shot = PROJECT_ROOT / "output" / "结果" / "yaoex_login_fail.png"
shot.parent.mkdir(parents=True, exist_ok=True)
page.screenshot(path=str(shot), full_page=True)
detail = f",页面提示:{login_error}" if login_error else ""
raise RuntimeError(
f"1号药城登录后仍停留在登录页,登录可能失败{detail}。截图:{shot}"
)
def _ensure_ybm_logged_in(page: Any, username: str, password: str) -> None:
page.goto(YBM_LOGIN_URL, wait_until="domcontentloaded")
page.wait_for_timeout(2000)
if "/new/login" not in page.url:
return
account_input = page.locator("form.login-form input[type='text']").first
password_input = page.locator("form.login-form input[type='password']").first
agree_input = page.locator(".login-section-agree input[type='checkbox']").first
agree_visible_box = page.locator(".login-section-agree .el-checkbox__inner").first
login_button = page.locator("button.login-button").first
account_input.wait_for(state="visible", timeout=30000)
account_input.fill(username)
password_input.fill(password)
if not agree_input.is_checked():
agree_visible_box.click()
login_button.click()
page.wait_for_timeout(5000)
if "/new/login" in page.url:
login_error = ""
error_locator = page.locator(".el-message__content")
if error_locator.count() > 0:
login_error = normalize_text(error_locator.first.inner_text())
detail = f",页面提示:{login_error}" if login_error else ""
raise RuntimeError(f"药帮忙登录后仍停留在登录页,登录可能失败{detail}")
def _playwright_login_yaoex(username: str, password: str, runtime: BrowserRuntime) -> YaoexAuth:
context, page = runtime.new_page()
_ensure_yaoex_logged_in(page, username, password)
page.goto(YAOEX_HOME_URL, wait_until="domcontentloaded")
page.wait_for_timeout(3000)
print("1号药城窗口应已打开首页。")
cookies = context.cookies()
cookie_map = {item["name"]: item["value"] for item in cookies}
token = parse_cookie_value(cookie_map.get("ycgltoken")) or parse_cookie_value(
cookie_map.get("yctoken")
)
user_id = parse_cookie_value(cookie_map.get("ycuserId"))
buyer_code = parse_cookie_value(cookie_map.get("ycenterpriseId"))
if not token or not user_id or not buyer_code:
raise RuntimeError("未能从1号药城 cookie 提取完整认证字段(token/user_id/buyer_code)。")
return YaoexAuth(token=token, user_id=user_id, buyer_code=buyer_code)
def _playwright_login_ybm(username: str, password: str, runtime: BrowserRuntime) -> dict[str, str]:
context, page = runtime.new_page()
_ensure_ybm_logged_in(page, username, password)
page.goto(YBM_HOME_URL, wait_until="domcontentloaded")
page.wait_for_timeout(3000)
print("药帮忙窗口应已打开首页。")
cookies = context.cookies()
return {item["name"]: parse_cookie_value(item["value"]) for item in cookies}
def login_yaoex(
secrets_path: Path | None = None,
runtime: BrowserRuntime | None = None,
) -> YaoexAuth:
secrets = load_secrets(secrets_path)
username = secrets["yaoex"].get("username", "")
password = secrets["yaoex"].get("password", "")
_require_account("1号药城", username, password)
own = runtime is None
session = runtime or BrowserRuntime(keep=False)
try:
return _playwright_login_yaoex(username, password, session)
finally:
if own:
session.close()
def login_ybm(
secrets_path: Path | None = None,
runtime: BrowserRuntime | None = None,
) -> dict[str, str]:
secrets = load_secrets(secrets_path)
username = secrets["ybm"].get("username", "")
password = secrets["ybm"].get("password", "")
_require_account("药帮忙", username, password)
own = runtime is None
session = runtime or BrowserRuntime(keep=False)
try:
return _playwright_login_ybm(username, password, session)
finally:
if own:
session.close()

View File

@ -1,46 +0,0 @@
"""Oracle 11.2 thick 连接。环境变量与 rpa_oms 相同。"""
from __future__ import annotations
import os
DEAL_USER_NAME = "rpa-select"
_client_ready = False
class RepoUnavailable(Exception):
def __init__(self, message: str = "数据库不可用"):
self.message = message
super().__init__(message)
def oracle_dsn() -> tuple[str, str, str]:
user = os.getenv("RPA_ORACLE_USER", "")
password = os.getenv("RPA_ORACLE_PASSWORD", "")
host = os.getenv("RPA_ORACLE_HOST", "")
if not user or not password or not host:
raise RepoUnavailable("未配置 RPA_ORACLE_HOST / RPA_ORACLE_USER / RPA_ORACLE_PASSWORD")
port = os.getenv("RPA_ORACLE_PORT", "1521")
service = os.getenv("RPA_ORACLE_SERVICE", "rpa")
return user, password, f"{host}:{port}/{service}"
def connect_from_env():
user, password, dsn = oracle_dsn()
_ensure_client()
import oracledb
return oracledb.connect(user=user, password=password, dsn=dsn)
def _ensure_client() -> None:
global _client_ready
if _client_ready:
return
import oracledb
if os.getenv("RPA_ORACLE_THICK", "1") == "1":
lib_dir = os.getenv("RPA_ORACLE_CLIENT_LIB", "")
kwargs = {"lib_dir": lib_dir} if lib_dir else {}
oracledb.init_oracle_client(**kwargs)
_client_ready = True

View File

@ -1,84 +0,0 @@
from __future__ import annotations
import json
import time
from typing import Any
from urllib.parse import unquote
import requests
import urllib3
from mall_config import API_MAX_RETRIES, API_REQUEST_TIMEOUT_SECONDS, API_RETRY_BASE_SECONDS
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
def parse_cookie_value(raw_value: str | None) -> str:
if not raw_value:
return ""
return unquote(raw_value.strip().strip('"'))
def parse_response_json_or_raise(
response: requests.Response,
platform_name: str,
query_key: str,
) -> dict[str, Any]:
try:
payload_json = response.json()
except ValueError as exc:
raw_text = response.text or ""
cleaned_text = raw_text.lstrip("\ufeff").strip()
for prefix in (")]}',", "while(1);"):
if cleaned_text.startswith(prefix):
cleaned_text = cleaned_text[len(prefix) :].strip()
try:
payload_json = json.loads(cleaned_text)
except ValueError:
snippet = cleaned_text.replace("\r", " ").replace("\n", " ")[:200]
raise RuntimeError(
f"{platform_name} 接口返回非 JSON,查询键 {query_key},"
f"HTTP {response.status_code},响应片段:{snippet}"
) from exc
if not isinstance(payload_json, dict):
raise RuntimeError(
f"{platform_name} 接口 JSON 根结构异常,查询键 {query_key}:{type(payload_json)}"
)
return payload_json
def post_json_with_retry(
session: requests.Session,
url: str,
*,
platform_name: str,
query_key: str,
**request_kwargs: Any,
) -> dict[str, Any]:
last_error: Exception | None = None
for attempt in range(1, API_MAX_RETRIES + 1):
try:
response = session.post(
url,
timeout=API_REQUEST_TIMEOUT_SECONDS,
verify=False,
**request_kwargs,
)
response.raise_for_status()
return parse_response_json_or_raise(response, platform_name, query_key)
except (requests.RequestException, RuntimeError) as exc:
last_error = exc
if attempt >= API_MAX_RETRIES:
break
sleep_seconds = API_RETRY_BASE_SECONDS * attempt
print(
f"[重试] {platform_name} 查询键 {query_key} 第 {attempt}/{API_MAX_RETRIES} 次失败:"
f"{exc},{sleep_seconds:.1f} 秒后重试。"
)
time.sleep(sleep_seconds)
raise RuntimeError(
f"{platform_name} 接口请求失败,查询键 {query_key},"
f"重试 {API_MAX_RETRIES} 次后仍未成功:{last_error}"
) from last_error

View File

@ -1,118 +0,0 @@
"""选品班次:领取表 1 → 按条形码搜平台 → 统一拆量写表 2。"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Callable
from persist import STATUS_OK, build_result
from pipeline import KIND_BUSINESS_FAIL, MallOutcome, SelectionResult, run_selection
from repo import MappingRow
from rules import Candidate, PurchaseLine, validate_purchase
Searcher = Callable[[str], list[Candidate]]
@dataclass
class JobSummary:
claimed: int = 0
added: int = 0
skipped: int = 0
failed: int = 0
def mapping_to_purchase(row: MappingRow) -> PurchaseLine:
sale_price = float(row.sale_price) if row.sale_price is not None else None
qty = float(row.plan_purchase_qty) if row.plan_purchase_qty is not None else None
return PurchaseLine(
approval_no=row.approval_no or "",
sale_price=sale_price,
qty=qty,
row_no=row.fid,
generic_name=row.goods_name or "",
spec=row.goods_spec or "",
manufacturer=row.manufacturer or "",
oms_id=row.oms_id,
bar_code=(row.bar_code or "").strip(),
org_code=row.org_code or "",
)
def run_batch(
repo,
*,
platforms: list[str],
searchers: dict[str, Searcher],
login_errors: dict[str, str],
limit: int | None = None,
) -> JobSummary:
rows = repo.claim_pending(limit=limit)
summary = JobSummary(claimed=len(rows))
for row in rows:
purchase = mapping_to_purchase(row)
selection = _select_row(purchase, platforms, searchers, login_errors)
result = build_result(purchase, selection)
_count(summary, result.cart_rows)
repo.save_result(
result.oms_id, result.deal_flag, result.deal_result, result.cart_rows
)
return summary
def _select_row(
purchase: PurchaseLine,
platforms: list[str],
searchers: dict[str, Searcher],
login_errors: dict[str, str],
) -> SelectionResult:
row_reason = validate_purchase(purchase)
if row_reason:
return SelectionResult(
kind=KIND_BUSINESS_FAIL,
reason=row_reason,
allocations=[],
rejected=[],
malls=list(platforms),
)
if not purchase.bar_code:
return SelectionResult(
kind=KIND_BUSINESS_FAIL,
reason="NO_BAR_CODE",
allocations=[],
rejected=[],
malls=list(platforms),
)
outcomes = [
_search_one(purchase, mall, searchers, login_errors) for mall in platforms
]
return run_selection(purchase, outcomes)
def _search_one(
purchase: PurchaseLine,
mall: str,
searchers: dict[str, Searcher],
login_errors: dict[str, str],
) -> MallOutcome:
login_error = login_errors.get(mall)
if login_error:
return MallOutcome(mall, failed_reason=f"LOGIN_FAILED:{login_error}")
searcher = searchers.get(mall)
if searcher is None:
return MallOutcome(mall, failed_reason="LOGIN_FAILED:未登录")
try:
candidates = searcher(purchase.bar_code)
except Exception as exc:
return MallOutcome(mall, failed_reason=f"QUERY_FAILED:{exc}")
return MallOutcome(mall, candidates=candidates)
def _count(summary: JobSummary, cart_rows: list[dict]) -> None:
for row in cart_rows:
if row.get("status") == STATUS_OK:
summary.added += 1
continue
reason = str(row.get("fail_reason") or "")
if reason.startswith("系统故障"):
summary.failed += 1
else:
summary.skipped += 1

View File

@ -1,191 +0,0 @@
from __future__ import annotations
import argparse
import time
from pathlib import Path
import requests
from auth_login import (
DEFAULT_YAOEX_AUTH_PATH,
DEFAULT_YBM_COOKIE_PATH,
PROJECT_ROOT,
BrowserRuntime,
load_cookie_file,
load_dotenv,
load_yaoex_auth_file,
login_yaoex,
login_ybm,
save_cookie_file,
save_yaoex_auth,
)
from db import RepoUnavailable
from job import run_batch
from mall_config import YAOEX_MALL_NAME, YBM_MALL_NAME
from repo import OracleSelectRepo
from rules import Candidate
from search_yaoex import YaoexAuth
from search_yaoex import search_candidates as search_yaoex_candidates
from search_ybm import search_candidates as search_ybm_candidates
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="选品班次:捞表1 deal_flag=0,按条形码搜平台,结果写入表2。"
)
parser.add_argument("--limit", type=int, default=None, help="本班最多领取 N 条")
parser.add_argument(
"--platform",
choices=["all", "yaoex", "ybm"],
default="all",
help="all=两平台都跑",
)
parser.add_argument("--secrets", type=Path, default=None, help="secrets.json 路径")
parser.add_argument(
"--yaoex-auth",
type=Path,
default=None,
help="已登录的 1号药城认证 JSON(token/user_id/buyer_code),跳过浏览器",
)
parser.add_argument(
"--ybm-cookies",
type=Path,
default=None,
help="已登录的药帮忙 cookie JSON,跳过浏览器",
)
parser.add_argument(
"--keep-browser",
action="store_true",
help="登录后不关 Chrome,搜完再留一段时间方便查看",
)
parser.add_argument(
"--hold-seconds",
type=int,
default=180,
help="--keep-browser 时窗口再留多少秒,默认 180",
)
parser.add_argument(
"--fresh-login",
action="store_true",
help="忽略本地登录缓存,强制重新打开浏览器",
)
return parser.parse_args(argv)
def _platforms(choice: str) -> list[str]:
if choice == "yaoex":
return [YAOEX_MALL_NAME]
if choice == "ybm":
return [YBM_MALL_NAME]
return [YAOEX_MALL_NAME, YBM_MALL_NAME]
def main(argv: list[str] | None = None, repo=None) -> None:
args = parse_args(argv)
load_dotenv(PROJECT_ROOT / ".env")
flow_start = time.perf_counter()
if repo is None:
try:
repo = OracleSelectRepo.from_env()
except RepoUnavailable as exc:
print(f"数据库不可用:{exc}")
raise SystemExit(2) from exc
yaoex_auth: YaoexAuth | None = None
ybm_cookies: dict[str, str] | None = None
login_errors: dict[str, str] = {}
runtime = BrowserRuntime(keep=args.keep_browser, hold_seconds=args.hold_seconds)
platforms = _platforms(args.platform)
try:
if YAOEX_MALL_NAME in platforms:
try:
cached = args.yaoex_auth or (
None if args.fresh_login else DEFAULT_YAOEX_AUTH_PATH
)
if cached and cached.exists():
yaoex_auth = load_yaoex_auth_file(cached)
print(f"1号药城使用缓存登录:{cached}")
else:
yaoex_auth = login_yaoex(args.secrets, runtime=runtime)
save_yaoex_auth(yaoex_auth)
print(f"1号药城登录已缓存:{DEFAULT_YAOEX_AUTH_PATH}")
print("1号药城认证就绪。")
except SystemExit:
raise
except Exception as exc:
login_errors[YAOEX_MALL_NAME] = str(exc)
print(f"1号药城登录失败,本平台中止:{exc}")
if YBM_MALL_NAME in platforms:
try:
cached = args.ybm_cookies or (
None if args.fresh_login else DEFAULT_YBM_COOKIE_PATH
)
if cached and cached.exists():
ybm_cookies = load_cookie_file(cached)
print(f"药帮忙使用缓存登录:{cached}")
else:
ybm_cookies = login_ybm(args.secrets, runtime=runtime)
save_cookie_file(ybm_cookies)
print(f"药帮忙登录已缓存:{DEFAULT_YBM_COOKIE_PATH}")
print("药帮忙认证就绪。")
except SystemExit:
raise
except Exception as exc:
login_errors[YBM_MALL_NAME] = str(exc)
print(f"药帮忙登录失败,本平台中止:{exc}")
yaoex_session = requests.Session()
ybm_session = requests.Session()
yaoex_cache: dict[str, list[Candidate]] = {}
ybm_cache: dict[str, list[Candidate]] = {}
if ybm_cookies:
for name, value in ybm_cookies.items():
ybm_session.cookies.set(name, value)
searchers = {}
if yaoex_auth is not None:
def search_yaoex(query_key: str) -> list[Candidate]:
assert yaoex_auth is not None
return search_yaoex_candidates(
yaoex_auth, query_key, session=yaoex_session, cache=yaoex_cache
)
searchers[YAOEX_MALL_NAME] = search_yaoex
if ybm_cookies is not None:
def search_ybm(query_key: str) -> list[Candidate]:
return search_ybm_candidates(
ybm_cookies, query_key, session=ybm_session, cache=ybm_cache
)
searchers[YBM_MALL_NAME] = search_ybm
try:
summary = run_batch(
repo,
platforms=platforms,
searchers=searchers,
login_errors=login_errors,
limit=args.limit,
)
finally:
yaoex_session.close()
ybm_session.close()
elapsed = time.perf_counter() - flow_start
print(
f"领取={summary.claimed} ADDED={summary.added} "
f"SKIPPED={summary.skipped} FAILED={summary.failed} "
f"用时 {elapsed:.2f} 秒"
)
print("已写入 rpa_cart_settlement_goods_data,push_status=0,未推订单中心,未加购。")
finally:
runtime.close()
if __name__ == "__main__":
main()

View File

@ -1,21 +0,0 @@
YAOEX_LOGIN_URL = "https://mall.yaoex.com/login"
YAOEX_HOME_URL = "https://mall.yaoex.com/v2/home/"
YAOEX_SEARCH_API_URL = "https://gateway-b2b.fangkuaiyi.com/home/search/homeSearchList"
YAOEX_PRICE_DECRYPT_BASE_KEY = "GDLSAUO1KUMIIBCE"
YAOEX_MALL_NAME = "1号药城"
YAOEX_PLATFORM_TYPE = 66
YBM_LOGIN_URL = "https://www.ybm100.com/new/login"
YBM_HOME_URL = "https://www.ybm100.com/new/"
YBM_SEARCH_API_URL = "https://www.ybm100.com/new-front/search/list-products"
YBM_SORT_STRATEGY = 1
YBM_MALL_NAME = "药帮忙"
YBM_PLATFORM_TYPE = 48
MALL_PLATFORM_TYPE = {
YAOEX_MALL_NAME: YAOEX_PLATFORM_TYPE,
YBM_MALL_NAME: YBM_PLATFORM_TYPE,
}
API_MAX_RETRIES = 3
API_RETRY_BASE_SECONDS = 1.5
API_REQUEST_TIMEOUT_SECONDS = 30

View File

@ -1,95 +0,0 @@
# 双商城查价流程技术可行性报告
## 1. 报告目的
评估当前已实现的双商城自动查价流程(1号药城、药帮忙)在技术上的可行性、稳定性与可维护性,并给出上线建议。
## 2. 结论摘要
- 结论:技术可行,已具备稳定运行条件,可投入日常使用。
- 现状:已完成双平台登录、接口抓取、字段映射、价格解密、异常兜底、结果导出与运行统计。
- 风险等级:中低(主要风险来自第三方页面结构和接口字段变更)。
## 3. 当前实现范围
- 输入:桌面采购模板 `【单独】采购单比价模板.xlsx`(按“批准文号”查询)。
- 平台顺序:固定为 `1号药城 -> 药帮忙`。
- 输出:桌面目录 `比价结果` 下 `查价结果_时间戳.xlsx`。
- 输出表头(固定顺序):
- 通用名, 规格, 生产厂家, 批准文号, 采购数量, 采购价格, 商城, 平台名称, 平台规格, 平台价格, 效期, 商家, 库存, 库存说明
## 4. 技术方案与可行性分析
### 4.1 技术栈
- Python 3.12(`uv` 管理)
- `isa-rpa`:浏览器会话与密码箱读取
- `requests`:接口请求
- `pycryptodome`:1号药城价格 AES 解密
- `openpyxl`:Excel 读写
可行性判断:
- 均为成熟组件,生态稳定,维护成本低。
- 方案是“浏览器登录 + 接口抓取”的混合模式,较纯 UI 点击方案更稳定、速度更快。
### 4.2 登录与认证方案
- 1号药城:自动填账号密码,点击“开始验证”,登录后从 Cookie 提取 `token/userId/buyerCode`。
- 药帮忙:自动填账号密码,勾选“阅读并同意”,登录后复用 Cookie 进行接口查询。
- 凭据管理:使用 `SecretClient` 从密码箱读取,不在代码中硬编码明文凭据。
可行性判断:
- 认证链路已跑通,多次回归可复现。
- 密码箱方案满足凭据安全与后续轮换需求。
### 4.3 接口与字段映射方案
- 1号药城接口:`homeSearchList`
- 新增映射:`平台名称 <- shortName`,`平台规格 <- spec`
- 价格字段:`price` 支持解密后写入
- 药帮忙接口:`list-products`
- 新增映射:`平台名称 <- showName`,`平台规格 <- spec`
- 价格字段:优先 `price`,为空兜底 `fob`
- 无货规则:`availableQty=0` 时 `平台价格=0`,`采购价格`保留原始值,`库存说明=无货`
可行性判断:
- 字段来源明确、映射稳定,且包含业务兜底规则,能覆盖常见脏数据场景。
### 4.4 数据处理与输出方案
- 读取采购模板后按“批准文号”查询,空批准文号自动跳过。
- 对同一批准文号做查询缓存,减少重复请求。
- 输出时强制按目标表头重排,避免模板历史列顺序导致错位。
- 运行结束输出统计指标:
- 查询品种个数(批准文号去重)
- 获取价格行数
- 流程运行用时
可行性判断:
- 具备业务可用性与可审计性(统计输出可用于日常巡检)。
## 5. 已验证结果(最新回归)
- 运行命令:`uv run main.py`
- 实测结果(最新一次):
- 采购模板有效条目:7
- 抓取总行数:216(1号药城 133,药帮忙 83)
- 查询品种个数:7
- 获取价格行数:216
- 流程运行用时:约 33 秒
- 最新结果文件:`查价结果_20260904_115656.xlsx`
可行性判断:
- 说明在当前网络和账号状态下可稳定产出完整结果。
## 6. 风险与应对
- 风险1:第三方站点登录页元素变更
- 应对:保留统一定位函数,定期做元素回归检查。
- 风险2:接口字段或鉴权参数调整
- 应对:对关键字段增加空值兜底,异常报错保留原始响应摘要。
- 风险3:价格密文规则变更(1号药城)
- 应对:将解密逻辑独立函数化,变更时可局部更新。
- 风险4:网络波动导致请求失败
- 应对:可追加重试机制(建议后续迭代增加)。
## 7. 上线建议
- 建议上线:是。
- 上线前建议完成:
- 增加接口请求失败的有限重试(如 2~3 次指数退避)。
- 固化一份“字段变更检查清单”(登录元素、接口字段、返回码)。
- 建立日常监控阈值(如抓取行数异常、无价格占比异常告警)。
## 8. 最终结论
基于当前实现与回归结果,该双商城查价流程在技术上可行、稳定性可接受、维护成本可控,满足当前业务使用条件。

View File

@ -1,186 +0,0 @@
"""选品判定 → 表 2 行 + 表 1 deal_flag。
成功按拆出行落 status=1。凑不齐不落成功行。
"""
from __future__ import annotations
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
from typing import Any
from mall_config import MALL_PLATFORM_TYPE, YAOEX_MALL_NAME, YBM_MALL_NAME
from pipeline import KIND_BUSINESS_FAIL, KIND_SUCCESS, KIND_SYSTEM_FAIL, SelectionResult
from rules import Allocation, PurchaseLine
OPERATE_TYPE_ADD_CART = 1
STATUS_OK = 1
STATUS_FAIL = 2
PUSH_NOT_SENT = 0
DEAL_OK = 1
DEAL_FAIL = 2
PLACEHOLDER_SUPPLIER = "无符合规则供应商"
DEAL_RESULT_MAX = 200
FAIL_REASON_MAX = 500
MALL_REPORT_ORDER = (YBM_MALL_NAME, YAOEX_MALL_NAME)
PEER_UNAVAILABLE = "系统故障:对端平台不可用,合计库存不足"
@dataclass
class SelectResult:
oms_id: int
deal_flag: int
deal_result: str
cart_rows: list[dict[str, Any]]
def money4(value: object) -> Decimal:
return Decimal(str(value)).quantize(Decimal("0.0001"), rounding=ROUND_HALF_UP)
def build_result(purchase: PurchaseLine, selection: SelectionResult) -> SelectResult:
if purchase.oms_id is None:
raise ValueError("选品落表需要 oms_id")
if selection.kind == KIND_SUCCESS:
rows = [_success_row(purchase, item) for item in selection.allocations]
_set_multi_flag(rows)
malls = _malls_in_rows(rows)
return SelectResult(
oms_id=purchase.oms_id,
deal_flag=DEAL_OK,
deal_result=_clip(f"选品成功:{'、'.join(malls)}", DEAL_RESULT_MAX),
cart_rows=rows,
)
malls = selection.malls or list(MALL_PLATFORM_TYPE)
if selection.kind == KIND_SYSTEM_FAIL:
rows = [
_fail_row(purchase, mall, _system_fail_reason(mall, selection.mall_errors))
for mall in malls
]
detail = "; ".join(selection.mall_errors.values()) or "未知错误"
deal_result = _clip(f"系统故障:{detail}", DEAL_RESULT_MAX)
else:
reason = _business_fail_reason(selection.reason)
rows = [_fail_row(purchase, mall, reason) for mall in malls]
deal_result = _clip(reason, DEAL_RESULT_MAX)
_set_multi_flag(rows)
return SelectResult(
oms_id=purchase.oms_id,
deal_flag=DEAL_FAIL,
deal_result=deal_result,
cart_rows=rows,
)
def _set_multi_flag(rows: list[dict[str, Any]]) -> None:
types = {row["platform_type"] for row in rows}
flag = 1 if len(types) > 1 else 0
for row in rows:
row["multi_platform_flag"] = flag
def _malls_in_rows(rows: list[dict[str, Any]]) -> list[str]:
types = {row["platform_type"] for row in rows}
names: list[str] = []
for mall in MALL_REPORT_ORDER:
if MALL_PLATFORM_TYPE[mall] in types:
names.append(mall)
for mall, platform_type in MALL_PLATFORM_TYPE.items():
if platform_type in types and mall not in names:
names.append(mall)
return names
def _success_row(purchase: PurchaseLine, item: Allocation) -> dict[str, Any]:
price = money4(item.candidate.price)
qty = item.qty
row = _base_row(purchase, item.candidate.mall_name)
row.update(
{
"supplier_name": item.candidate.vendor_raw or PLACEHOLDER_SUPPLIER,
"purchase_price": price,
"purchase_qty": qty,
"purchase_total": money4(price * qty),
"status": STATUS_OK,
"fail_reason": None,
}
)
return row
def _fail_row(purchase: PurchaseLine, mall_name: str, reason: str) -> dict[str, Any]:
price = _fail_price(purchase)
qty = _qty(purchase)
row = _base_row(purchase, mall_name)
row.update(
{
"supplier_name": PLACEHOLDER_SUPPLIER,
"purchase_price": price,
"purchase_qty": qty,
"purchase_total": money4(price * qty),
"status": STATUS_FAIL,
"fail_reason": _clip(reason, FAIL_REASON_MAX),
}
)
return row
def _base_row(purchase: PurchaseLine, mall_name: str) -> dict[str, Any]:
if mall_name not in MALL_PLATFORM_TYPE:
raise ValueError(f"未知商城:{mall_name}")
return {
"operate_type": OPERATE_TYPE_ADD_CART,
"oms_id": purchase.oms_id,
"platform_type": MALL_PLATFORM_TYPE[mall_name],
"bar_code": purchase.bar_code,
"org_code": purchase.org_code or None,
"push_status": PUSH_NOT_SENT,
}
def _qty(purchase: PurchaseLine) -> int:
qty = purchase.qty
if qty is None or qty <= 0:
return 1
return int(qty)
def _fail_price(purchase: PurchaseLine) -> Decimal:
if purchase.sale_price is not None and purchase.sale_price > 0:
return money4(purchase.sale_price)
return money4("0.0100")
def _business_fail_reason(code: str) -> str:
mapping = {
"NO_HIT": "无符合规则商品,平台无命中",
"NO_BAR_CODE": "缺少条形码,无法检索平台",
"NO_SALE_PRICE": "缺少销售价格",
"NO_QTY": "缺少计划采购量",
"STOCK_INSUFFICIENT": "无符合规则商品,库存不足",
"MARGIN_LT_4PCT": "无符合规则商品,毛利不足 4%",
"EXPIRY_LT_12M": "无符合规则商品,效期不足 12 个月",
"EXPIRY_UNPARSEABLE": "无符合规则商品,效期不足 12 个月",
"NOT_SELF_WAREHOUSE": "无符合规则商品,非自营仓",
"NO_CANDIDATE_AFTER_RULES": "无符合规则商品",
}
if code in mapping:
return mapping[code]
if code:
return f"无符合规则商品,{code}"
return "无符合规则商品"
def _system_fail_reason(mall: str, mall_errors: dict[str, str]) -> str:
if mall in mall_errors:
return _clip(f"系统故障:{mall_errors[mall]}", FAIL_REASON_MAX)
return PEER_UNAVAILABLE
def _clip(text: str, max_len: int) -> str:
text = text.strip()
if len(text) <= max_len:
return text
return text[: max_len - 1] + "…"

View File

@ -1,121 +0,0 @@
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import date
from rules import (
REASON_EXPIRY,
REASON_EXPIRY_UNPARSEABLE,
REASON_MARGIN,
REASON_NOT_SELF,
Allocation,
Candidate,
PurchaseLine,
Rejected,
allocate,
validate_purchase,
)
KIND_SUCCESS = "SUCCESS"
KIND_BUSINESS_FAIL = "BUSINESS_FAIL"
KIND_SYSTEM_FAIL = "SYSTEM_FAIL"
@dataclass
class MallOutcome:
mall_name: str
candidates: list[Candidate] | None = None
failed_reason: str | None = None
@dataclass
class SelectionResult:
kind: str
reason: str
allocations: list[Allocation]
rejected: list[Rejected]
malls: list[str]
mall_errors: dict[str, str] = field(default_factory=dict)
def run_selection(
purchase: PurchaseLine,
mall_outcomes: list[MallOutcome],
today: date | None = None,
) -> SelectionResult:
malls = [item.mall_name for item in mall_outcomes]
mall_errors = {
item.mall_name: item.failed_reason
for item in mall_outcomes
if item.failed_reason
}
row_reason = validate_purchase(purchase)
if row_reason:
return SelectionResult(
kind=KIND_BUSINESS_FAIL,
reason=row_reason,
allocations=[],
rejected=[],
malls=malls,
mall_errors=mall_errors,
)
merged: list[Candidate] = []
had_hit = False
for item in mall_outcomes:
if item.failed_reason:
continue
rows = item.candidates or []
if rows:
had_hit = True
merged.extend(rows)
allocation = allocate(merged, purchase, today=today)
if allocation.allocations:
return SelectionResult(
kind=KIND_SUCCESS,
reason="SELECTED",
allocations=allocation.allocations,
rejected=allocation.rejected,
malls=malls,
mall_errors=mall_errors,
)
if mall_errors:
return SelectionResult(
kind=KIND_SYSTEM_FAIL,
reason="SYSTEM_FAULT",
allocations=[],
rejected=allocation.rejected,
malls=malls,
mall_errors=mall_errors,
)
return SelectionResult(
kind=KIND_BUSINESS_FAIL,
reason=_business_reason(allocation.passed_before_fold, allocation.rejected, had_hit),
allocations=[],
rejected=allocation.rejected,
malls=malls,
mall_errors={},
)
def _business_reason(
passed_before_fold: list[Candidate],
rejected: list[Rejected],
had_hit: bool,
) -> str:
if passed_before_fold:
return "STOCK_INSUFFICIENT"
reasons = [item.reason for item in rejected]
if REASON_MARGIN in reasons:
return REASON_MARGIN
if REASON_EXPIRY in reasons or REASON_EXPIRY_UNPARSEABLE in reasons:
return REASON_EXPIRY
if REASON_NOT_SELF in reasons:
return REASON_NOT_SELF
if not had_hit:
return "NO_HIT"
return "NO_CANDIDATE_AFTER_RULES"

View File

@ -1,30 +0,0 @@
[project]
name = "jieda-procure"
version = "0.1.0"
description = "洁达选品班次:表1 deal_flag=0 → 双平台搜品 → 写表2"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"oracledb>=2.4.1",
"playwright>=1.49.0",
"pycryptodome>=3.23.0",
"requests>=2.34.2",
"urllib3>=2.0.0",
]
[dependency-groups]
dev = [
"pytest>=8.0.0",
]
[tool.uv]
package = false
[tool.pytest.ini_options]
pythonpath = ["."]
testpaths = ["tests"]
addopts = "-q"
[[tool.uv.index]]
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
default = true

View File

@ -1,219 +0,0 @@
"""选品读写:表 1 领取、表 2 落结果。
claim 时 deal_flag 0 → 3;save 时写成 1 或 2。
中途崩溃会停在 3,需人工改回 0 再跑。
"""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from db import DEAL_USER_NAME, RepoUnavailable, connect_from_env, oracle_dsn
_SELECT_PENDING = """
SELECT fid, oms_id, bar_code, sale_price, plan_purchase_qty,
NVL(approval_no, '') AS approval_no,
NVL(goods_name, '') AS goods_name,
NVL(goods_spec, '') AS goods_spec,
NVL(manufacturer, '') AS manufacturer,
NVL(org_code, '') AS org_code,
deal_flag
FROM rpa_sales_goods_mapping_data
WHERE deal_flag = 0
ORDER BY fid
"""
_SELECT_PENDING_LIMIT = f"""
SELECT * FROM (
{_SELECT_PENDING}
) WHERE ROWNUM <= :limit
"""
_CLAIM_SQL = """
UPDATE rpa_sales_goods_mapping_data
SET deal_flag = 3,
deal_time = SYSDATE,
update_time = SYSDATE,
deal_user_name = :deal_user_name
WHERE fid = :fid AND deal_flag = 0
"""
_INSERT_CART_SQL = """
INSERT INTO rpa_cart_settlement_goods_data (
operate_type, oms_id, platform_type, supplier_name, bar_code,
purchase_price, purchase_qty, purchase_total, status, fail_reason,
push_status, create_time, org_code, multi_platform_flag, create_user_name
) VALUES (
:operate_type, :oms_id, :platform_type, :supplier_name, :bar_code,
:purchase_price, :purchase_qty, :purchase_total, :status, :fail_reason,
:push_status, SYSDATE, :org_code, :multi_platform_flag, :create_user_name
)
"""
_COMPLETE_SQL = """
UPDATE rpa_sales_goods_mapping_data
SET deal_flag = :deal_flag,
deal_result = :deal_result,
deal_time = SYSDATE,
update_time = SYSDATE,
deal_user_name = :deal_user_name
WHERE oms_id = :oms_id
"""
_CART_BIND_KEYS = (
"operate_type",
"oms_id",
"platform_type",
"supplier_name",
"bar_code",
"purchase_price",
"purchase_qty",
"purchase_total",
"status",
"fail_reason",
"push_status",
"org_code",
"multi_platform_flag",
)
@dataclass
class MappingRow:
fid: int
oms_id: int
bar_code: str
sale_price: float | None
plan_purchase_qty: float | None
approval_no: str
goods_name: str
goods_spec: str
manufacturer: str
org_code: str
deal_flag: int
deal_result: str = ""
class MemorySelectRepo:
def __init__(self, rows: list[MappingRow]):
self.mapping: dict[int, MappingRow] = {row.oms_id: row for row in rows}
self.cart: list[dict[str, Any]] = []
def claim_pending(self, limit: int | None = None) -> list[MappingRow]:
pending = sorted(
(row for row in self.mapping.values() if row.deal_flag == 0),
key=lambda row: row.fid,
)
if limit is not None:
pending = pending[:limit]
for row in pending:
row.deal_flag = 3
return pending
def save_result(
self,
oms_id: int,
deal_flag: int,
deal_result: str,
cart_rows: list[dict[str, Any]],
) -> None:
row = self.mapping[oms_id]
row.deal_flag = deal_flag
row.deal_result = deal_result
self.cart.extend(dict(item) for item in cart_rows)
class OracleSelectRepo:
def __init__(self, connect: Callable):
self._connect = connect
@classmethod
def from_env(cls) -> "OracleSelectRepo":
oracle_dsn()
return cls(connect=connect_from_env)
def claim_pending(self, limit: int | None = None) -> list[MappingRow]:
conn = self._connect()
try:
with conn.cursor() as cur:
if limit is None:
cur.execute(_SELECT_PENDING)
else:
cur.execute(_SELECT_PENDING_LIMIT, {"limit": int(limit)})
fetched = [_mapping_from_cursor(cur, rec) for rec in cur.fetchall()]
claimed: list[MappingRow] = []
for row in fetched:
cur.execute(
_CLAIM_SQL,
{"fid": row.fid, "deal_user_name": DEAL_USER_NAME},
)
if cur.rowcount == 1:
row.deal_flag = 3
claimed.append(row)
conn.commit()
return claimed
except Exception as exc:
try:
conn.rollback()
except Exception:
pass
raise RepoUnavailable(str(exc)) from exc
finally:
conn.close()
def save_result(
self,
oms_id: int,
deal_flag: int,
deal_result: str,
cart_rows: list[dict[str, Any]],
) -> None:
conn = self._connect()
try:
with conn.cursor() as cur:
for row in cart_rows:
payload = {key: row.get(key) for key in _CART_BIND_KEYS}
payload["create_user_name"] = DEAL_USER_NAME
cur.execute(_INSERT_CART_SQL, payload)
cur.execute(
_COMPLETE_SQL,
{
"deal_flag": deal_flag,
"deal_result": deal_result,
"deal_user_name": DEAL_USER_NAME,
"oms_id": oms_id,
},
)
conn.commit()
except Exception as exc:
try:
conn.rollback()
except Exception:
pass
raise RepoUnavailable(str(exc)) from exc
finally:
conn.close()
def _mapping_from_cursor(cur, rec) -> MappingRow:
cols = [item[0].lower() for item in cur.description]
data = dict(zip(cols, rec))
return MappingRow(
fid=int(data["fid"]),
oms_id=int(data["oms_id"]),
bar_code=str(data.get("bar_code") or ""),
sale_price=_float_or_none(data.get("sale_price")),
plan_purchase_qty=_float_or_none(data.get("plan_purchase_qty")),
approval_no=str(data.get("approval_no") or ""),
goods_name=str(data.get("goods_name") or ""),
goods_spec=str(data.get("goods_spec") or ""),
manufacturer=str(data.get("manufacturer") or ""),
org_code=str(data.get("org_code") or ""),
deal_flag=int(data.get("deal_flag") or 0),
)
def _float_or_none(value: object) -> float | None:
if value is None or value == "":
return None
return float(value)

View File

@ -1,317 +0,0 @@
from __future__ import annotations
import re
from calendar import monthrange
from dataclasses import dataclass, field
from datetime import date, datetime
from typing import Any
WAREHOUSE_ORDER = [
"杭州小药药自营旗舰店",
"长沙小药药自营旗舰店",
"湖北小药药自营旗舰店",
"广东壹号药业有限公司",
"昆山亿方药业有限公司",
"湖北亿昊药业有限公司",
"济南小药药自营旗舰店",
"天津壹号药业有限公司",
"重庆亿昊药业有限公司",
"辽宁亿昊药业有限公司",
"陕西壹号药业有限公司",
"河南壹号医药有限公司",
"重庆壹号医药有限公司",
"重庆小药药自营旗舰店",
]
WAREHOUSE_RANK = {name: index for index, name in enumerate(WAREHOUSE_ORDER)}
WAREHOUSE_ALIASES: dict[str, str] = {}
SELF_OPERATED_PREFIX = "[自营]"
MIN_MARGIN = 0.04
MIN_EXPIRY_MONTHS = 12.0
DAYS_PER_MONTH = 30.4375
REASON_NOT_SELF = "NOT_SELF_WAREHOUSE"
REASON_MARGIN = "MARGIN_LT_4PCT"
REASON_EXPIRY = "EXPIRY_LT_12M"
REASON_EXPIRY_UNPARSEABLE = "EXPIRY_UNPARSEABLE"
REASON_PRICE_UNPARSEABLE = "PRICE_UNPARSEABLE"
REASON_SAME_WAREHOUSE = "SAME_WAREHOUSE_NOT_BEST"
@dataclass
class Candidate:
mall_name: str
sku_id: str
name: str
spec: str
price: float | None
expiry_raw: str
expiry_months: float | None
vendor_raw: str
vendor_norm: str
stock: float | None
extra: dict[str, Any] = field(default_factory=dict)
@dataclass
class PurchaseLine:
approval_no: str
sale_price: float | None
qty: float | None
row_no: int | None = None
generic_name: str = ""
spec: str = ""
manufacturer: str = ""
extra: dict[str, Any] = field(default_factory=dict)
oms_id: int | None = None
bar_code: str = ""
org_code: str = ""
@dataclass
class Rejected:
candidate: Candidate
reason: str
@dataclass
class Allocation:
candidate: Candidate
qty: int
@dataclass
class AllocationResult:
allocations: list[Allocation]
rejected: list[Rejected]
unmatched_vendors: list[str]
passed_before_fold: list[Candidate]
def normalize_vendor(raw: Any) -> str:
text = "" if raw is None else str(raw)
text = text.replace("\u3000", " ").strip()
if text.startswith(SELF_OPERATED_PREFIX):
text = text[len(SELF_OPERATED_PREFIX) :]
text = " ".join(text.split())
return WAREHOUSE_ALIASES.get(text, text)
def is_self_warehouse(vendor_norm: str) -> bool:
return vendor_norm in WAREHOUSE_RANK
def margin(sale_price: float, platform_price: float) -> float:
return (sale_price - platform_price) / sale_price
def margin_ok(sale_price: float, platform_price: float) -> bool:
return margin(sale_price, platform_price) >= MIN_MARGIN
def parse_expiry_months(raw: Any, today: date | None = None) -> float | None:
text = "" if raw is None else str(raw).strip()
if not text or text in {"-", "—", "/", "无", "null", "None"}:
return None
text = text.split()[0].strip()
today = today or date.today()
if "长期" in text:
return 120.0
chinese = re.fullmatch(r"(\d{4})年(\d{1,2})月(?:(\d{1,2})日?)?", text)
if chinese:
year, month = int(chinese.group(1)), int(chinese.group(2))
day = int(chinese.group(3) or 0)
return remaining_months(today, _safe_date(year, month, day))
if "月" in text:
matched = re.search(r"(\d+(?:\.\d+)?)", text)
if matched is None:
return None
return float(matched.group(1))
dotted = re.fullmatch(r"(\d{4})[./-](\d{1,2})(?:[./-](\d{1,2}))?", text)
if dotted:
year, month = int(dotted.group(1)), int(dotted.group(2))
day = int(dotted.group(3) or 0)
return remaining_months(today, _safe_date(year, month, day))
if re.fullmatch(r"\d{13}", text):
expiry_date = datetime.fromtimestamp(int(text) / 1000).date()
return remaining_months(today, expiry_date)
if re.fullmatch(r"\d{8}", text):
parsed = datetime.strptime(text, "%Y%m%d").date()
return remaining_months(today, parsed)
if re.fullmatch(r"\d{6}", text):
parsed = datetime.strptime(text, "%Y%m")
last_day = monthrange(parsed.year, parsed.month)[1]
return remaining_months(today, date(parsed.year, parsed.month, last_day))
if re.fullmatch(r"\d+(?:\.\d+)?", text):
value = float(text)
if value > 240:
return None
return value
expiry_date = _parse_expiry_date(text)
if expiry_date is None:
return None
return remaining_months(today, expiry_date)
def _safe_date(year: int, month: int, day: int) -> date:
last_day = monthrange(year, month)[1]
if day <= 0 or day > last_day:
day = last_day
return date(year, month, day)
def remaining_months(today: date, expiry: date) -> float:
months = (expiry.year - today.year) * 12 + (expiry.month - today.month)
months += (expiry.day - today.day) / DAYS_PER_MONTH
return months
def _parse_expiry_date(text: str) -> date | None:
for fmt in ("%Y-%m-%d", "%Y/%m/%d"):
try:
return datetime.strptime(text, fmt).date()
except ValueError:
continue
for fmt in ("%Y-%m", "%Y/%m"):
try:
parsed = datetime.strptime(text, fmt)
last_day = monthrange(parsed.year, parsed.month)[1]
return date(parsed.year, parsed.month, last_day)
except ValueError:
continue
return None
def validate_purchase(purchase: PurchaseLine) -> str | None:
if purchase.sale_price is None or purchase.sale_price <= 0:
return "NO_SALE_PRICE"
if purchase.qty is None or purchase.qty <= 0:
return "NO_QTY"
return None
def evaluate_candidate(
candidate: Candidate,
purchase: PurchaseLine,
today: date | None = None,
) -> str | None:
if not is_self_warehouse(candidate.vendor_norm):
return REASON_NOT_SELF
if candidate.price is None:
return REASON_PRICE_UNPARSEABLE
if purchase.sale_price is None or purchase.sale_price <= 0:
return "NO_SALE_PRICE"
if not margin_ok(purchase.sale_price, candidate.price):
return REASON_MARGIN
expiry_months = candidate.expiry_months
if expiry_months is None:
expiry_months = parse_expiry_months(candidate.expiry_raw, today=today)
if expiry_months is None:
return REASON_EXPIRY_UNPARSEABLE
if expiry_months < MIN_EXPIRY_MONTHS:
return REASON_EXPIRY
return None
def stock_units(stock: float | None) -> int:
if stock is None or stock <= 0:
return 0
return int(stock)
def plan_qty(purchase: PurchaseLine) -> int:
qty = purchase.qty
if qty is None or qty <= 0:
return 0
return int(qty)
def allocate(
candidates: list[Candidate],
purchase: PurchaseLine,
today: date | None = None,
) -> AllocationResult:
rejected: list[Rejected] = []
passed: list[Candidate] = []
unmatched: list[str] = []
seen_unmatched: set[str] = set()
for candidate in candidates:
if not is_self_warehouse(candidate.vendor_norm):
raw = candidate.vendor_raw
if raw and raw not in seen_unmatched:
unmatched.append(raw)
seen_unmatched.add(raw)
reason = evaluate_candidate(candidate, purchase, today=today)
if reason is None:
passed.append(candidate)
else:
rejected.append(Rejected(candidate, reason))
folded = _fold_same_warehouse(passed, rejected)
folded.sort(key=_sort_key)
remaining = plan_qty(purchase)
lines: list[Allocation] = []
if remaining > 0:
for candidate in folded:
take = min(stock_units(candidate.stock), remaining)
if take <= 0:
continue
lines.append(Allocation(candidate, take))
remaining -= take
if remaining == 0:
break
if remaining != 0:
lines = []
return AllocationResult(
allocations=lines,
rejected=rejected,
unmatched_vendors=unmatched,
passed_before_fold=passed,
)
def _fold_same_warehouse(
passed: list[Candidate],
rejected: list[Rejected],
) -> list[Candidate]:
best: dict[tuple[str, str], Candidate] = {}
order: list[tuple[str, str]] = []
for candidate in passed:
key = (candidate.mall_name, candidate.vendor_norm)
current = best.get(key)
if current is None:
best[key] = candidate
order.append(key)
continue
if _sku_better(candidate, current):
rejected.append(Rejected(current, REASON_SAME_WAREHOUSE))
best[key] = candidate
else:
rejected.append(Rejected(candidate, REASON_SAME_WAREHOUSE))
return [best[key] for key in order]
def _sku_better(left: Candidate, right: Candidate) -> bool:
left_price = left.price if left.price is not None else float("inf")
right_price = right.price if right.price is not None else float("inf")
if left_price != right_price:
return left_price < right_price
return stock_units(left.stock) > stock_units(right.stock)
def _sort_key(candidate: Candidate) -> tuple[int, float, int]:
rank = WAREHOUSE_RANK[candidate.vendor_norm]
price = candidate.price if candidate.price is not None else float("inf")
return (rank, price, -stock_units(candidate.stock))

View File

@ -1,201 +0,0 @@
from __future__ import annotations
import base64
import time
from binascii import Error as BinasciiError
from dataclasses import dataclass
from typing import Any
import requests
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from http_util import post_json_with_retry
from mall_config import (
YAOEX_MALL_NAME,
YAOEX_PRICE_DECRYPT_BASE_KEY,
YAOEX_SEARCH_API_URL,
)
from rules import Candidate, normalize_vendor, parse_expiry_months
from util import normalize_text, parse_float_or_none
@dataclass
class YaoexAuth:
token: str
user_id: str
buyer_code: str
def build_yaoex_search_request(
auth: YaoexAuth, query_key: str
) -> tuple[dict[str, Any], dict[str, Any]]:
timestamp = str(int(time.time() * 1000))
common_part: dict[str, Any] = {
"traderName": "yaoex_pc",
"trader": "pc",
"closesignature": "yes",
"signature_method": "md5",
"signature": "",
"timestamp": timestamp,
"token": auth.token,
"userToken": auth.token,
}
form_payload = {
**common_part,
"userId": auth.user_id,
"roleId": "",
"userType": "",
"buyerCode": auth.buyer_code,
"nowPage": 1,
"per": 20,
"keyword": query_key,
"catSearchId": "",
"specs": "",
"factoryIds": "",
"sellerCodes": "",
"sellerFilterMode": 1,
"sortColumn": "default",
"sortMode": "default",
"ver": 1,
"stock_mode": 1,
"showExtendCard": "true",
"needDinnerPrice": "true",
"limitStart": "",
"limitEnd": "",
"deadLineStart": 12,
"deadLineEnd": "",
"filterDtos": "",
"showWholePurchase": "true",
}
return common_part, form_payload
def build_yaoex_price_decrypt_key(user_id: Any) -> bytes:
user_text = normalize_text(user_id)
if not user_text:
return YAOEX_PRICE_DECRYPT_BASE_KEY.encode("utf-8")
dynamic_key = YAOEX_PRICE_DECRYPT_BASE_KEY[:10] + user_text[:6].rjust(6, "0")
return dynamic_key.encode("utf-8")
def decrypt_yaoex_price(raw_price: Any, user_id: Any) -> Any:
if raw_price is None:
return ""
if isinstance(raw_price, (int, float)):
return raw_price
text = normalize_text(raw_price)
if not text:
return ""
try:
return float(text)
except ValueError:
pass
try:
cipher_bytes = base64.b64decode(text)
key_bytes = build_yaoex_price_decrypt_key(user_id)
plain_bytes = AES.new(key_bytes, AES.MODE_ECB).decrypt(cipher_bytes)
plain_text = unpad(plain_bytes, AES.block_size).decode("utf-8").strip()
except (BinasciiError, ValueError, UnicodeDecodeError):
return text
if plain_text == "":
return 0
try:
return float(plain_text)
except ValueError:
return plain_text
def query_yaoex_products(
session: requests.Session,
auth: YaoexAuth,
query_key: str,
) -> tuple[list[dict[str, Any]], str]:
params, payload = build_yaoex_search_request(auth, query_key)
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/x-www-form-urlencoded",
"Origin": "https://mall.yaoex.com",
"Referer": "https://mall.yaoex.com/",
"X-Request-Agent": "Axios",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"
),
}
payload_json = post_json_with_retry(
session,
YAOEX_SEARCH_API_URL,
platform_name=YAOEX_MALL_NAME,
query_key=query_key,
params=params,
data=payload,
headers=headers,
)
data_section = payload_json.get("data") or {}
products = data_section.get("shopProducts") or []
if not isinstance(products, list):
raise RuntimeError(f"1号药城接口返回 shopProducts 结构异常:{type(products)}")
response_user_id = normalize_text(data_section.get("userId")) or auth.user_id
return products, response_user_id
def map_yaoex_products(products: list[dict[str, Any]], user_id: Any) -> list[Candidate]:
candidates: list[Candidate] = []
if products:
sample = products[0]
expiry_keys = {
key: sample.get(key)
for key in sample
if any(token in str(key).lower() for token in ("dead", "valid", "expir", "period", "effect"))
}
print(f"1号药城效期相关字段示例:{expiry_keys}")
for product in products:
vendor_raw = normalize_text(product.get("vendorName"))
expiry_raw = ""
for key in ("deadLine", "deadLineDesc", "validDate", "expireDate", "period"):
expiry_raw = normalize_text(product.get(key))
if expiry_raw:
break
raw_price = decrypt_yaoex_price(product.get("price", ""), user_id)
candidates.append(
Candidate(
mall_name=YAOEX_MALL_NAME,
sku_id=normalize_text(product.get("spuCode")),
name=normalize_text(product.get("shortName")),
spec=normalize_text(product.get("spec")),
price=parse_float_or_none(raw_price),
expiry_raw=expiry_raw,
expiry_months=parse_expiry_months(expiry_raw),
vendor_raw=vendor_raw,
vendor_norm=normalize_vendor(vendor_raw),
stock=parse_float_or_none(product.get("stockCount")),
extra={"stockCountDesc": product.get("stockCountDesc", "")},
)
)
return candidates
def search_candidates(
auth: YaoexAuth,
query_key: str,
session: requests.Session | None = None,
cache: dict[str, list[Candidate]] | None = None,
) -> list[Candidate]:
store = cache if cache is not None else {}
if query_key in store:
return store[query_key]
own_session = session or requests.Session()
try:
products, user_id = query_yaoex_products(own_session, auth, query_key)
mapped = map_yaoex_products(products, user_id)
finally:
if session is None:
own_session.close()
store[query_key] = mapped
return mapped

View File

@ -1,120 +0,0 @@
from __future__ import annotations
from typing import Any
import requests
from http_util import post_json_with_retry
from mall_config import YBM_MALL_NAME, YBM_SEARCH_API_URL, YBM_SORT_STRATEGY
from rules import Candidate, normalize_vendor, parse_expiry_months
from util import normalize_text, parse_float_or_none, parse_int_or_none
def build_ybm_search_payload(query_key: str) -> dict[str, Any]:
return {
"queryWord": query_key,
"searchScene": 1,
"nearEffect": "",
"minPrice": "",
"maxPrice": "",
"drugClassificationsStr": "",
"isThirdCompany": 0,
"isEffectPeriodOverOneYear": 1,
"sortStrategy": YBM_SORT_STRATEGY,
}
def pick_ybm_platform_price(product: dict[str, Any]) -> Any:
price = product.get("price")
if normalize_text(price) != "":
return price
fob = product.get("fob")
if normalize_text(fob) != "":
return fob
return ""
def query_ybm_products(
session: requests.Session,
query_key: str,
) -> list[dict[str, Any]]:
headers = {
"Accept": "application/json, text/plain, */*",
"Content-Type": "application/json",
"Origin": "https://www.ybm100.com",
"Referer": "https://www.ybm100.com/new/",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/152.0.0.0 Safari/537.36"
),
}
payload = build_ybm_search_payload(query_key)
payload_json = post_json_with_retry(
session,
YBM_SEARCH_API_URL,
platform_name=YBM_MALL_NAME,
query_key=query_key,
json=payload,
headers=headers,
)
if not payload_json.get("success"):
error_msg = normalize_text(payload_json.get("msg")) or "未知错误"
raise RuntimeError(f"药帮忙查询失败:{error_msg}")
data_section = payload_json.get("data") or {}
rows = data_section.get("rows") or []
if not isinstance(rows, list):
raise RuntimeError(f"药帮忙接口返回 data.rows 结构异常:{type(rows)}")
return rows
def map_ybm_products(rows: list[dict[str, Any]]) -> list[Candidate]:
candidates: list[Candidate] = []
for row in rows:
product = row.get("productInfo") or {}
available_qty = product.get("availableQty", "")
available_num = parse_int_or_none(available_qty)
no_stock = available_num == 0 or normalize_text(available_qty) == "0"
raw_price = 0 if no_stock else pick_ybm_platform_price(product)
vendor_raw = normalize_text(product.get("shopName"))
expiry_raw = normalize_text(product.get("nearEffect"))
candidates.append(
Candidate(
mall_name=YBM_MALL_NAME,
sku_id=normalize_text(product.get("skuId", row.get("skuId", ""))),
name=normalize_text(product.get("showName")),
spec=normalize_text(product.get("spec")),
price=parse_float_or_none(raw_price),
expiry_raw=expiry_raw,
expiry_months=parse_expiry_months(expiry_raw),
vendor_raw=vendor_raw,
vendor_norm=normalize_vendor(vendor_raw),
stock=parse_float_or_none(available_qty),
extra={"库存说明": "无货" if no_stock else "有货"},
)
)
return candidates
def search_candidates(
cookies: dict[str, str],
query_key: str,
session: requests.Session | None = None,
cache: dict[str, list[Candidate]] | None = None,
) -> list[Candidate]:
store = cache if cache is not None else {}
if query_key in store:
return store[query_key]
own_session = session or requests.Session()
try:
if session is None:
for name, value in cookies.items():
own_session.cookies.set(name, value)
rows = query_ybm_products(own_session, query_key)
mapped = map_ybm_products(rows)
finally:
if session is None:
own_session.close()
store[query_key] = mapped
return mapped

View File

@ -1,10 +0,0 @@
{
"yaoex": {
"username": "",
"password": ""
},
"ybm": {
"username": "",
"password": ""
}
}

View File

@ -1,104 +0,0 @@
from __future__ import annotations
import pytest
import main
from job import run_batch
from mall_config import YAOEX_MALL_NAME, YBM_MALL_NAME
from repo import MappingRow, MemorySelectRepo
from rules import Candidate, normalize_vendor
from search_yaoex import YaoexAuth
def test_old_excel_and_cart_flags_are_gone():
with pytest.raises(SystemExit):
main.parse_args(["--execute-cart"])
with pytest.raises(SystemExit):
main.parse_args(["--input", "x.xlsx"])
with pytest.raises(SystemExit):
main.parse_args(["--dry-run"])
def test_main_writes_cart_rows_from_mapping(monkeypatch):
repo = MemorySelectRepo(
[
MappingRow(
fid=1,
oms_id=2026100201,
bar_code="06901234567890",
sale_price=100.0,
plan_purchase_qty=10,
approval_no="国药准字H20100001",
goods_name="药A",
goods_spec="0.25g",
manufacturer="示例",
org_code="901",
deal_flag=0,
)
]
)
def fake_login_yaoex(*_args, **_kwargs):
return YaoexAuth(token="t", user_id="u", buyer_code="b")
def fake_login_ybm(*_args, **_kwargs):
return {"sid": "1"}
def fake_search_yaoex(*_args, **_kwargs):
vendor = "广东壹号药业有限公司"
return [
Candidate(
mall_name=YAOEX_MALL_NAME,
sku_id="S1",
name="药A",
spec="0.25g",
price=90.0,
expiry_raw="13",
expiry_months=13.0,
vendor_raw=vendor,
vendor_norm=normalize_vendor(vendor),
stock=20.0,
)
]
def fake_search_ybm(*_args, **_kwargs):
vendor = "某第三方"
return [
Candidate(
mall_name=YBM_MALL_NAME,
sku_id="Y1",
name="药A",
spec="0.25g",
price=70.0,
expiry_raw="13",
expiry_months=13.0,
vendor_raw=vendor,
vendor_norm=normalize_vendor(vendor),
stock=20.0,
)
]
monkeypatch.setattr(main, "login_yaoex", fake_login_yaoex)
monkeypatch.setattr(main, "login_ybm", fake_login_ybm)
monkeypatch.setattr(main, "search_yaoex_candidates", fake_search_yaoex)
monkeypatch.setattr(main, "search_ybm_candidates", fake_search_ybm)
main.main(["--fresh-login"], repo=repo)
assert repo.mapping[2026100201].deal_flag == 1
assert len(repo.cart) == 1
assert repo.cart[0]["platform_type"] == 66
assert repo.cart[0]["status"] == 1
assert repo.cart[0]["bar_code"] == "06901234567890"
def test_run_batch_empty_claim_is_noop():
repo = MemorySelectRepo([])
summary = run_batch(
repo,
platforms=[YAOEX_MALL_NAME],
searchers={},
login_errors={},
)
assert summary.claimed == 0
assert repo.cart == []

View File

@ -1,21 +0,0 @@
from __future__ import annotations
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import pad
from search_yaoex import build_yaoex_price_decrypt_key, decrypt_yaoex_price
def test_decrypt_plain_number_passthrough():
assert decrypt_yaoex_price(12.5, "123") == 12.5
assert decrypt_yaoex_price("33.0", "123") == 33.0
def test_decrypt_aes_ciphertext():
user_id = "123456789"
key = build_yaoex_price_decrypt_key(user_id)
cipher = AES.new(key, AES.MODE_ECB)
encrypted = base64.b64encode(cipher.encrypt(pad(b"88.5", AES.block_size))).decode("utf-8")
assert decrypt_yaoex_price(encrypted, user_id) == 88.5

View File

@ -1,210 +0,0 @@
from __future__ import annotations
from repo import MappingRow, MemorySelectRepo
from rules import Candidate, normalize_vendor
from job import run_batch
HANGZHOU = "杭州小药药自营旗舰店"
GUANGDONG = "广东壹号药业有限公司"
def _row(**overrides) -> MappingRow:
data = {
"fid": 1,
"oms_id": 2026100201,
"bar_code": "06901234567890",
"sale_price": 100.0,
"plan_purchase_qty": 10,
"approval_no": "国药准字H20100001",
"goods_name": "阿莫西林",
"goods_spec": "0.25g",
"manufacturer": "示例制药",
"org_code": "901",
"deal_flag": 0,
}
data.update(overrides)
return MappingRow(**data)
def _ok_candidate(mall: str = "1号药城", **overrides) -> Candidate:
vendor = overrides.pop("vendor", GUANGDONG if mall == "1号药城" else HANGZHOU)
data = {
"mall_name": mall,
"sku_id": "S1",
"name": "阿莫西林",
"spec": "0.25g",
"price": 90.0,
"expiry_raw": "13",
"expiry_months": 13.0,
"vendor_raw": vendor,
"vendor_norm": normalize_vendor(vendor),
"stock": 20.0,
}
data.update(overrides)
if "vendor_raw" in overrides and "vendor_norm" not in overrides:
data["vendor_norm"] = normalize_vendor(overrides["vendor_raw"])
return Candidate(**data)
def test_claim_pending_sets_processing_flag():
repo = MemorySelectRepo([_row(), _row(fid=2, oms_id=2026100202, deal_flag=1)])
claimed = repo.claim_pending()
assert [row.oms_id for row in claimed] == [2026100201]
assert repo.mapping[2026100201].deal_flag == 3
assert repo.mapping[2026100202].deal_flag == 1
def test_job_searches_barcode_not_approval_no():
repo = MemorySelectRepo([_row()])
seen: list[str] = []
def search_yaoex(query_key: str):
seen.append(query_key)
return [_ok_candidate()]
summary = run_batch(
repo,
platforms=["1号药城"],
searchers={"1号药城": search_yaoex},
login_errors={},
)
assert seen == ["06901234567890"]
assert summary.added == 1
assert repo.mapping[2026100201].deal_flag == 1
assert len(repo.cart) == 1
assert repo.cart[0]["bar_code"] == "06901234567890"
assert repo.cart[0]["status"] == 1
assert repo.cart[0]["purchase_qty"] == 10
def test_job_skips_search_when_barcode_missing():
repo = MemorySelectRepo([_row(bar_code="")])
called = {"n": 0}
def search_yaoex(_query_key: str):
called["n"] += 1
return [_ok_candidate()]
summary = run_batch(
repo,
platforms=["1号药城"],
searchers={"1号药城": search_yaoex},
login_errors={},
)
assert called["n"] == 0
assert summary.skipped == 1
assert repo.mapping[2026100201].deal_flag == 2
assert repo.cart[0]["status"] == 2
assert "条形码" in (repo.cart[0]["fail_reason"] or "")
def test_job_limit_claims_first_n():
repo = MemorySelectRepo(
[
_row(fid=1, oms_id=1, bar_code="111"),
_row(fid=2, oms_id=2, bar_code="222"),
_row(fid=3, oms_id=3, bar_code="333"),
]
)
claimed = repo.claim_pending(limit=2)
assert [row.oms_id for row in claimed] == [1, 2]
assert repo.mapping[3].deal_flag == 0
def test_both_malls_login_failed_marks_system_fault():
repo = MemorySelectRepo([_row()])
summary = run_batch(
repo,
platforms=["1号药城", "药帮忙"],
searchers={},
login_errors={"1号药城": "chrome down", "药帮忙": "timeout"},
)
assert summary.failed == 2
assert repo.mapping[2026100201].deal_flag == 2
assert repo.mapping[2026100201].deal_result.startswith("系统故障")
assert len(repo.cart) == 2
assert all(row["status"] == 2 for row in repo.cart)
def test_split_across_malls_writes_two_success_rows():
repo = MemorySelectRepo([_row()])
def search_yaoex(_key: str):
return [_ok_candidate("1号药城", stock=20.0)]
def search_ybm(_key: str):
return [_ok_candidate("药帮忙", vendor=HANGZHOU, sku_id="HZ", stock=3.0)]
summary = run_batch(
repo,
platforms=["1号药城", "药帮忙"],
searchers={"1号药城": search_yaoex, "药帮忙": search_ybm},
login_errors={},
)
assert summary.added == 2
assert repo.mapping[2026100201].deal_flag == 1
ybm = next(row for row in repo.cart if row["platform_type"] == 48)
yaoex = next(row for row in repo.cart if row["platform_type"] == 66)
assert ybm["purchase_qty"] == 3
assert yaoex["purchase_qty"] == 7
assert ybm["multi_platform_flag"] == 1
def test_peer_login_failed_but_other_fills_is_success():
repo = MemorySelectRepo([_row()])
def search_yaoex(_key: str):
return [_ok_candidate(stock=20.0)]
summary = run_batch(
repo,
platforms=["1号药城", "药帮忙"],
searchers={"1号药城": search_yaoex},
login_errors={"药帮忙": "timeout"},
)
assert summary.added == 1
assert repo.mapping[2026100201].deal_flag == 1
assert len(repo.cart) == 1
assert repo.cart[0]["platform_type"] == 66
def test_peer_login_failed_and_short_stock_is_system_fault():
repo = MemorySelectRepo([_row()])
def search_yaoex(_key: str):
return [_ok_candidate(stock=4.0)]
summary = run_batch(
repo,
platforms=["1号药城", "药帮忙"],
searchers={"1号药城": search_yaoex},
login_errors={"药帮忙": "timeout"},
)
assert summary.failed == 2
assert summary.added == 0
assert repo.mapping[2026100201].deal_flag == 2
assert repo.mapping[2026100201].deal_result.startswith("系统故障")
assert all(row["status"] == 2 for row in repo.cart)
def test_combined_short_stock_is_business_fail():
repo = MemorySelectRepo([_row()])
def search_yaoex(_key: str):
return [_ok_candidate(stock=2.0)]
def search_ybm(_key: str):
return [_ok_candidate("药帮忙", vendor=HANGZHOU, sku_id="HZ", stock=2.0)]
summary = run_batch(
repo,
platforms=["1号药城", "药帮忙"],
searchers={"1号药城": search_yaoex, "药帮忙": search_ybm},
login_errors={},
)
assert summary.added == 0
assert summary.skipped == 2
assert repo.mapping[2026100201].deal_flag == 2
assert "库存不足" in repo.mapping[2026100201].deal_result
assert all(row["status"] == 2 for row in repo.cart)

View File

@ -1,27 +0,0 @@
from __future__ import annotations
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
def test_isa_rpa_removed_from_project():
pyproject = (ROOT / "pyproject.toml").read_text(encoding="utf-8")
assert "isa-rpa" not in pyproject
assert "isa_rpa" not in pyproject
main_src = (ROOT / "main.py").read_text(encoding="utf-8")
assert "isa_rpa" not in main_src
assert "SecretClient" not in main_src
assert "BrowserSession" not in main_src
def test_purchase_price_band_removed():
forbidden = (
"is_in_purchase_price_band",
"filter_rows_by_purchase_price_band",
"价格区间过滤后总行数",
)
for path in ROOT.glob("*.py"):
text = path.read_text(encoding="utf-8")
for token in forbidden:
assert token not in text, f"{path.name} still contains {token}"

View File

@ -1,76 +0,0 @@
from __future__ import annotations
from search_yaoex import map_yaoex_products
from search_ybm import map_ybm_products
def test_yaoex_field_mapping_and_price_passthrough():
products = [
{
"spuCode": "SPU1",
"shortName": "阿莫西林",
"spec": "0.25g",
"price": 88.5,
"deadLine": "13",
"vendorName": "[自营] 广东壹号药业有限公司",
"stockCount": "40",
}
]
candidates = map_yaoex_products(products, user_id="123456")
assert len(candidates) == 1
item = candidates[0]
assert item.sku_id == "SPU1"
assert item.name == "阿莫西林"
assert item.spec == "0.25g"
assert item.price == 88.5
assert item.expiry_raw == "13"
assert item.vendor_norm == "广东壹号药业有限公司"
assert item.stock == 40.0
assert item.mall_name == "1号药城"
def test_ybm_field_mapping_zero_stock_price_is_zero():
rows = [
{
"skuId": "fallback",
"productInfo": {
"skuId": "SKU9",
"showName": "头孢",
"spec": "0.5g",
"price": 120,
"fob": 110,
"nearEffect": "14个月",
"shopName": "昆山亿方药业有限公司",
"availableQty": 0,
},
}
]
candidates = map_ybm_products(rows)
assert len(candidates) == 1
item = candidates[0]
assert item.sku_id == "SKU9"
assert item.name == "头孢"
assert item.price == 0
assert item.stock == 0.0
assert item.expiry_raw == "14个月"
assert item.mall_name == "药帮忙"
def test_ybm_uses_fob_when_price_empty():
rows = [
{
"productInfo": {
"skuId": "SKU8",
"showName": "头孢",
"spec": "0.5g",
"price": "",
"fob": 77,
"nearEffect": "18",
"shopName": "湖北亿昊药业有限公司",
"availableQty": 12,
}
}
]
item = map_ybm_products(rows)[0]
assert item.price == 77.0
assert item.stock == 12.0

View File

@ -1,74 +0,0 @@
from __future__ import annotations
import pytest
from db import RepoUnavailable
from repo import OracleSelectRepo
def test_from_env_without_user_raises(monkeypatch):
monkeypatch.delenv("RPA_ORACLE_USER", raising=False)
monkeypatch.delenv("RPA_ORACLE_PASSWORD", raising=False)
monkeypatch.delenv("RPA_ORACLE_HOST", raising=False)
with pytest.raises(RepoUnavailable):
OracleSelectRepo.from_env()
def test_save_result_inserts_cart_then_updates_mapping():
executed: list[tuple[str, dict]] = []
class FakeCursor:
def __enter__(self):
return self
def __exit__(self, *args):
return False
def execute(self, sql, params=None):
executed.append((sql, params or {}))
class FakeConn:
def __init__(self):
self.committed = False
def cursor(self):
return FakeCursor()
def commit(self):
self.committed = True
def rollback(self):
raise AssertionError("不应 rollback")
def close(self):
pass
conn = FakeConn()
repo = OracleSelectRepo(connect=lambda: conn)
repo.save_result(
2026100201,
1,
"选品成功:1号药城",
[
{
"operate_type": 1,
"oms_id": 2026100201,
"platform_type": 66,
"supplier_name": "广东壹号药业有限公司",
"bar_code": "06901234567890",
"purchase_price": 90,
"purchase_qty": 10,
"purchase_total": 900,
"status": 1,
"fail_reason": None,
"push_status": 0,
"org_code": "901",
"multi_platform_flag": 0,
}
],
)
assert conn.committed is True
assert "rpa_cart_settlement_goods_data" in executed[0][0].lower()
assert executed[0][1]["oms_id"] == 2026100201
assert "rpa_sales_goods_mapping_data" in executed[1][0].lower()
assert executed[1][1]["deal_flag"] == 1

View File

@ -1,184 +0,0 @@
from __future__ import annotations
from decimal import Decimal
from persist import build_result
from pipeline import KIND_BUSINESS_FAIL, KIND_SUCCESS, KIND_SYSTEM_FAIL, SelectionResult
from rules import Allocation, Candidate, PurchaseLine, Rejected, normalize_vendor
HANGZHOU = "杭州小药药自营旗舰店"
CHANGSHA = "长沙小药药自营旗舰店"
GUANGDONG = "广东壹号药业有限公司"
def _purchase(**overrides) -> PurchaseLine:
data = {
"approval_no": "国药准字H20100001",
"sale_price": 100.0,
"qty": 10.0,
"oms_id": 2026100201,
"bar_code": "06901234567890",
"generic_name": "阿莫西林",
"org_code": "901",
}
data.update(overrides)
return PurchaseLine(**data)
def _candidate(**overrides) -> Candidate:
vendor_raw = overrides.get("vendor_raw", GUANGDONG)
data = {
"mall_name": "1号药城",
"sku_id": "S1",
"name": "阿莫西林",
"spec": "0.25g",
"price": 90.0,
"expiry_raw": "13",
"expiry_months": 13.0,
"vendor_raw": vendor_raw,
"vendor_norm": normalize_vendor(vendor_raw),
"stock": 20.0,
"extra": {},
}
data.update(overrides)
if "vendor_raw" in overrides and "vendor_norm" not in overrides:
data["vendor_norm"] = normalize_vendor(overrides["vendor_raw"])
return Candidate(**data)
def _success(allocations: list[Allocation], malls: list[str] | None = None) -> SelectionResult:
return SelectionResult(
kind=KIND_SUCCESS,
reason="SELECTED",
allocations=allocations,
rejected=[],
malls=malls or ["1号药城", "药帮忙"],
)
def test_split_two_platforms_writes_allocated_qty():
result = build_result(
_purchase(),
_success(
[
Allocation(
_candidate(
mall_name="药帮忙",
vendor_raw=HANGZHOU,
price=88.0,
),
3,
),
Allocation(_candidate(price=90.0), 7),
]
),
)
assert result.deal_flag == 1
assert "选品成功" in result.deal_result
assert len(result.cart_rows) == 2
ybm = next(row for row in result.cart_rows if row["platform_type"] == 48)
yaoex = next(row for row in result.cart_rows if row["platform_type"] == 66)
assert ybm["purchase_qty"] == 3
assert ybm["purchase_price"] == Decimal("88.0000")
assert ybm["purchase_total"] == Decimal("264.0000")
assert ybm["supplier_name"] == HANGZHOU
assert yaoex["purchase_qty"] == 7
assert yaoex["purchase_total"] == Decimal("630.0000")
assert all(row["status"] == 1 for row in result.cart_rows)
assert all(row["multi_platform_flag"] == 1 for row in result.cart_rows)
assert all(row["push_status"] == 0 for row in result.cart_rows)
def test_two_warehouses_same_platform_flag_off():
result = build_result(
_purchase(),
_success(
[
Allocation(
_candidate(mall_name="药帮忙", vendor_raw=HANGZHOU, price=88.0),
3,
),
Allocation(
_candidate(mall_name="药帮忙", vendor_raw=CHANGSHA, price=90.0),
7,
),
],
malls=["药帮忙"],
),
)
assert len(result.cart_rows) == 2
assert all(row["platform_type"] == 48 for row in result.cart_rows)
assert all(row["multi_platform_flag"] == 0 for row in result.cart_rows)
def test_stock_insufficient_business_fail():
result = build_result(
_purchase(),
SelectionResult(
kind=KIND_BUSINESS_FAIL,
reason="STOCK_INSUFFICIENT",
allocations=[],
rejected=[Rejected(_candidate(vendor_raw="路边店"), "NOT_SELF_WAREHOUSE")],
malls=["1号药城", "药帮忙"],
),
)
assert result.deal_flag == 2
assert "库存不足" in result.deal_result
assert len(result.cart_rows) == 2
assert all(row["status"] == 2 for row in result.cart_rows)
assert all("库存不足" in (row["fail_reason"] or "") for row in result.cart_rows)
assert all(row["multi_platform_flag"] == 1 for row in result.cart_rows)
def test_margin_fail_text_is_4_percent():
result = build_result(
_purchase(),
SelectionResult(
kind=KIND_BUSINESS_FAIL,
reason="MARGIN_LT_4PCT",
allocations=[],
rejected=[],
malls=["1号药城"],
),
)
assert "毛利不足 4%" in result.deal_result
assert "毛利不足 4%" in (result.cart_rows[0]["fail_reason"] or "")
def test_system_fault_uses_per_mall_errors():
result = build_result(
_purchase(),
SelectionResult(
kind=KIND_SYSTEM_FAIL,
reason="SYSTEM_FAULT",
allocations=[],
rejected=[],
malls=["1号药城", "药帮忙"],
mall_errors={"药帮忙": "LOGIN_FAILED:timeout"},
),
)
assert result.deal_flag == 2
assert result.deal_result.startswith("系统故障")
ybm = next(row for row in result.cart_rows if row["platform_type"] == 48)
yaoex = next(row for row in result.cart_rows if row["platform_type"] == 66)
assert "LOGIN_FAILED:timeout" in (ybm["fail_reason"] or "")
assert "对端平台不可用" in (yaoex["fail_reason"] or "")
def test_failure_total_is_price_times_plan_qty():
result = build_result(
_purchase(sale_price=12.5, qty=3),
SelectionResult(
kind=KIND_BUSINESS_FAIL,
reason="NO_HIT",
allocations=[],
rejected=[],
malls=["1号药城"],
),
)
row = result.cart_rows[0]
assert row["purchase_price"] == Decimal("12.5000")
assert row["purchase_qty"] == 3
assert row["purchase_total"] == Decimal("37.5000")
assert row["supplier_name"] == "无符合规则供应商"

View File

@ -1,185 +0,0 @@
from __future__ import annotations
from pipeline import MallOutcome, run_selection
from rules import Candidate, PurchaseLine, normalize_vendor
HANGZHOU = "杭州小药药自营旗舰店"
GUANGDONG = "广东壹号药业有限公司"
def _purchase(**overrides) -> PurchaseLine:
data = {
"approval_no": "国药准字H20100001",
"sale_price": 100.0,
"qty": 10.0,
"row_no": 2,
"generic_name": "阿莫西林",
"oms_id": 2026100201,
"bar_code": "06901234567890",
}
data.update(overrides)
return PurchaseLine(**data)
def _candidate(**overrides) -> Candidate:
vendor_raw = overrides.get("vendor_raw", GUANGDONG)
data = {
"mall_name": "1号药城",
"sku_id": "SKU1",
"name": "测试药品",
"spec": "0.25g",
"price": 90.0,
"expiry_raw": "13",
"expiry_months": 13.0,
"vendor_raw": vendor_raw,
"vendor_norm": normalize_vendor(vendor_raw),
"stock": 100.0,
"extra": {},
}
data.update(overrides)
if "vendor_raw" in overrides and "vendor_norm" not in overrides:
data["vendor_norm"] = normalize_vendor(overrides["vendor_raw"])
return Candidate(**data)
def test_missing_sale_price_is_business_fail():
result = run_selection(
_purchase(sale_price=None),
[MallOutcome("1号药城", candidates=[_candidate()])],
)
assert result.kind == "BUSINESS_FAIL"
assert result.reason == "NO_SALE_PRICE"
assert result.allocations == []
def test_missing_qty_is_business_fail():
result = run_selection(_purchase(qty=0), [MallOutcome("药帮忙")])
assert result.kind == "BUSINESS_FAIL"
assert result.reason == "NO_QTY"
def test_query_error_without_enough_stock_is_system_fail():
result = run_selection(
_purchase(),
[
MallOutcome("1号药城", failed_reason="QUERY_FAILED:timeout"),
MallOutcome("药帮忙", candidates=[]),
],
)
assert result.kind == "SYSTEM_FAIL"
assert result.allocations == []
assert result.mall_errors["1号药城"] == "QUERY_FAILED:timeout"
def test_empty_hit_is_no_hit():
result = run_selection(
_purchase(),
[
MallOutcome("1号药城", candidates=[]),
MallOutcome("药帮忙", candidates=[]),
],
)
assert result.kind == "BUSINESS_FAIL"
assert result.reason == "NO_HIT"
def test_all_rejected_uses_rule_reason():
result = run_selection(
_purchase(),
[MallOutcome("1号药城", candidates=[_candidate(vendor_raw="路边药店")])],
)
assert result.kind == "BUSINESS_FAIL"
assert result.reason == "NOT_SELF_WAREHOUSE"
assert any(item.reason == "NOT_SELF_WAREHOUSE" for item in result.rejected)
def test_split_across_malls_is_success():
result = run_selection(
_purchase(),
[
MallOutcome(
"药帮忙",
candidates=[
_candidate(
sku_id="HZ",
mall_name="药帮忙",
vendor_raw=HANGZHOU,
stock=3.0,
)
],
),
MallOutcome(
"1号药城",
candidates=[_candidate(sku_id="GD", stock=20.0)],
),
],
)
assert result.kind == "SUCCESS"
assert result.reason == "SELECTED"
qtys = [(item.candidate.sku_id, item.qty) for item in result.allocations]
assert qtys == [("HZ", 3), ("GD", 7)]
def test_one_mall_login_failed_other_fills_is_success():
result = run_selection(
_purchase(),
[
MallOutcome("药帮忙", failed_reason="LOGIN_FAILED:timeout"),
MallOutcome("1号药城", candidates=[_candidate(stock=20.0)]),
],
)
assert result.kind == "SUCCESS"
assert result.allocations[0].qty == 10
def test_one_mall_login_failed_other_short_is_system_fail():
result = run_selection(
_purchase(),
[
MallOutcome("药帮忙", failed_reason="LOGIN_FAILED:timeout"),
MallOutcome("1号药城", candidates=[_candidate(stock=4.0)]),
],
)
assert result.kind == "SYSTEM_FAIL"
assert result.allocations == []
assert result.mall_errors["药帮忙"].startswith("LOGIN_FAILED")
def test_short_stock_with_rejects_is_stock_insufficient():
result = run_selection(
_purchase(),
[
MallOutcome(
"药帮忙",
candidates=[
_candidate(
sku_id="HZ",
mall_name="药帮忙",
vendor_raw=HANGZHOU,
stock=4.0,
),
_candidate(
sku_id="ROAD",
mall_name="药帮忙",
vendor_raw="路边药店",
stock=100.0,
),
],
),
MallOutcome(
"1号药城",
candidates=[
_candidate(
sku_id="EXP",
expiry_raw="6",
expiry_months=6.0,
stock=100.0,
)
],
),
],
)
assert result.kind == "BUSINESS_FAIL"
assert result.reason == "STOCK_INSUFFICIENT"
assert result.allocations == []

View File

@ -1,253 +0,0 @@
from __future__ import annotations
from datetime import date
from rules import (
Candidate,
PurchaseLine,
WAREHOUSE_ORDER,
allocate,
normalize_vendor,
parse_expiry_months,
validate_purchase,
)
TODAY = date(2026, 9, 14)
HANGZHOU = "杭州小药药自营旗舰店"
CHANGSHA = "长沙小药药自营旗舰店"
GUANGDONG = "广东壹号药业有限公司"
HUNAN = "湖南壹号药业股份有限公司"
GANSU = "甘肃壹号云医药有限责任公司企业店"
def _candidate(**overrides) -> Candidate:
data = {
"mall_name": "1号药城",
"sku_id": "SKU1",
"name": "测试药品",
"spec": "0.25g",
"price": 90.0,
"expiry_raw": "13",
"expiry_months": 13.0,
"vendor_raw": GUANGDONG,
"vendor_norm": GUANGDONG,
"stock": 100.0,
"extra": {},
}
data.update(overrides)
if "vendor_raw" in overrides and "vendor_norm" not in overrides:
data["vendor_norm"] = normalize_vendor(overrides["vendor_raw"])
if "expiry_raw" in overrides and "expiry_months" not in overrides:
data["expiry_months"] = parse_expiry_months(overrides["expiry_raw"], today=TODAY)
return Candidate(**data)
def _purchase(**overrides) -> PurchaseLine:
data = {
"approval_no": "国药准字H20100001",
"sale_price": 100.0,
"qty": 10.0,
"row_no": 2,
}
data.update(overrides)
return PurchaseLine(**data)
def _alloc(candidates, purchase=None):
return allocate(candidates, purchase or _purchase(), today=TODAY)
def _qtys(result):
return [(item.candidate.sku_id, item.qty) for item in result.allocations]
def test_warehouse_order_is_four_tier_mixed_malls():
assert WAREHOUSE_ORDER == [
"杭州小药药自营旗舰店",
"长沙小药药自营旗舰店",
"湖北小药药自营旗舰店",
"广东壹号药业有限公司",
"昆山亿方药业有限公司",
"湖北亿昊药业有限公司",
"济南小药药自营旗舰店",
"天津壹号药业有限公司",
"重庆亿昊药业有限公司",
"辽宁亿昊药业有限公司",
"陕西壹号药业有限公司",
"河南壹号医药有限公司",
"重庆壹号医药有限公司",
"重庆小药药自营旗舰店",
]
def test_margin_exactly_4_percent_passes():
result = _alloc([_candidate(price=96.0)])
assert result.allocations
assert result.allocations[0].qty == 10
def test_margin_3_9_percent_rejected():
result = _alloc([_candidate(price=96.1)])
assert result.allocations == []
assert result.rejected[0].reason == "MARGIN_LT_4PCT"
def test_vendor_not_in_warehouse_list_rejected():
result = _alloc([_candidate(vendor_raw="某第三方药店", vendor_norm="某第三方药店")])
assert result.allocations == []
assert result.rejected[0].reason == "NOT_SELF_WAREHOUSE"
assert "某第三方药店" in result.unmatched_vendors
def test_hunan_and_gansu_are_not_self_warehouses():
result = _alloc(
[
_candidate(sku_id="HN", vendor_raw=HUNAN),
_candidate(sku_id="GS", vendor_raw=GANSU),
]
)
assert result.allocations == []
reasons = {item.reason for item in result.rejected}
assert reasons == {"NOT_SELF_WAREHOUSE"}
def test_self_operated_prefix_normalized():
raw = "[自营] 广东壹号药业有限公司"
result = _alloc([_candidate(vendor_raw=raw)])
assert result.allocations
assert result.allocations[0].candidate.vendor_norm == GUANGDONG
def test_hangzhou_beats_guangdong():
result = _alloc(
[
_candidate(sku_id="GD", vendor_raw=GUANGDONG, mall_name="1号药城", price=90.0),
_candidate(
sku_id="HZ",
vendor_raw=HANGZHOU,
mall_name="药帮忙",
price=94.0,
),
]
)
assert _qtys(result) == [("HZ", 10)]
def test_split_near_warehouse_then_next():
result = _alloc(
[
_candidate(
sku_id="HZ",
vendor_raw=HANGZHOU,
mall_name="药帮忙",
stock=3.0,
),
_candidate(sku_id="GD", vendor_raw=GUANGDONG, stock=20.0),
]
)
assert _qtys(result) == [("HZ", 3), ("GD", 7)]
def test_total_stock_short_returns_no_allocation():
result = _alloc(
[
_candidate(sku_id="HZ", vendor_raw=HANGZHOU, mall_name="药帮忙", stock=4.0),
_candidate(sku_id="GD", vendor_raw=GUANGDONG, stock=5.0),
]
)
assert result.allocations == []
assert result.passed_before_fold
def test_same_warehouse_keeps_cheaper_sku_only():
result = _alloc(
[
_candidate(sku_id="CHEAP", vendor_raw=HANGZHOU, mall_name="药帮忙", price=90.0, stock=5.0),
_candidate(sku_id="DEAR", vendor_raw=HANGZHOU, mall_name="药帮忙", price=92.0, stock=100.0),
_candidate(sku_id="GD", vendor_raw=GUANGDONG, price=94.0, stock=20.0),
]
)
assert _qtys(result) == [("CHEAP", 5), ("GD", 5)]
dominated = [item for item in result.rejected if item.reason == "SAME_WAREHOUSE_NOT_BEST"]
assert len(dominated) == 1
assert dominated[0].candidate.sku_id == "DEAR"
def test_stock_fraction_floors_to_int():
result = _alloc(
[
_candidate(sku_id="HZ", vendor_raw=HANGZHOU, mall_name="药帮忙", stock=3.9),
_candidate(sku_id="GD", vendor_raw=GUANGDONG, stock=20.0),
]
)
assert _qtys(result) == [("HZ", 3), ("GD", 7)]
def test_none_stock_does_not_fill():
result = _alloc([_candidate(stock=None)], _purchase(qty=10.0))
assert result.allocations == []
assert result.passed_before_fold
def test_expiry_11_months_rejected():
result = _alloc([_candidate(expiry_raw="11", expiry_months=11.0)])
assert result.allocations == []
assert result.rejected[0].reason == "EXPIRY_LT_12M"
def test_expiry_date_one_year_later_passes():
result = _alloc(
[
_candidate(
expiry_raw="2027-09-14",
expiry_months=parse_expiry_months("2027-09-14", today=TODAY),
)
]
)
assert result.allocations
def test_unparseable_expiry_rejected():
result = _alloc([_candidate(expiry_raw="即将到期", expiry_months=None)])
assert result.allocations == []
assert result.rejected[0].reason == "EXPIRY_UNPARSEABLE"
def test_year_month_expiry_uses_last_day_of_month():
september = parse_expiry_months("2027-09", today=TODAY)
august = parse_expiry_months("2027-08", today=TODAY)
assert september is not None and september >= 12.0
assert august is not None and august < 12.0
def test_datetime_string_strips_time():
assert parse_expiry_months("2028-06-16 00:00:00", today=TODAY) >= 12.0
assert parse_expiry_months("2027-11-30 00:00:00", today=TODAY) >= 12.0
def test_dotted_and_chinese_expiry_dates():
assert parse_expiry_months("2027.09.14", today=TODAY) >= 12.0
assert parse_expiry_months("2027.09", today=TODAY) >= 12.0
assert parse_expiry_months("2027年09月14日", today=TODAY) >= 12.0
assert parse_expiry_months("2027年9月", today=TODAY) >= 12.0
assert parse_expiry_months("20270914", today=TODAY) >= 12.0
assert parse_expiry_months("长期", today=TODAY) >= 12.0
def test_eight_digit_date_is_not_treated_as_month_count():
months = parse_expiry_months("20270914", today=TODAY)
assert months is not None
assert 11 < months < 14
def test_gansu_alias_no_longer_maps_into_self_set():
assert normalize_vendor("甘肃壹号云医药有限责任公司") == "甘肃壹号云医药有限责任公司"
def test_validate_purchase_reasons():
assert validate_purchase(_purchase(sale_price=None)) == "NO_SALE_PRICE"
assert validate_purchase(_purchase(sale_price=0)) == "NO_SALE_PRICE"
assert validate_purchase(_purchase(qty=None)) == "NO_QTY"
assert validate_purchase(_purchase(qty=0)) == "NO_QTY"
assert validate_purchase(_purchase()) is None

View File

@ -1,15 +0,0 @@
from __future__ import annotations
from search_yaoex import YaoexAuth, build_yaoex_search_request
from search_ybm import build_ybm_search_payload
def test_yaoex_keyword_is_the_query_key():
auth = YaoexAuth(token="t", user_id="u", buyer_code="b")
_, payload = build_yaoex_search_request(auth, "06901234567890")
assert payload["keyword"] == "06901234567890"
def test_ybm_query_word_is_the_query_key():
payload = build_ybm_search_payload("06901234567890")
assert payload["queryWord"] == "06901234567890"

View File

@ -1,33 +0,0 @@
from __future__ import annotations
from typing import Any
def normalize_text(value: Any) -> str:
if value is None:
return ""
return str(value).strip()
def parse_int_or_none(value: Any) -> int | None:
text = normalize_text(value)
if text == "":
return None
try:
return int(float(text))
except ValueError:
return None
def parse_float_or_none(value: Any) -> float | None:
text = normalize_text(value)
if text == "":
return None
try:
return float(text)
except ValueError:
cleaned = text.replace(",", "").replace(",", "")
try:
return float(cleaned)
except ValueError:
return None

View File

@ -1,604 +0,0 @@
version = 1
revision = 3
requires-python = ">=3.12"
[[package]]
name = "certifi"
version = "2026.7.22"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/c2/24167ea9858356b47a87a50d39908bfdb72ceeefe0041586e704e5376b3a/certifi-2026.7.22.tar.gz", hash = "sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55", size = 138112, upload-time = "2026-07-22T03:35:12.644Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/a7/71ac2cff56fec219ed242bb11b8efb69fcc4bec75db06fb7bfe35de520e6/certifi-2026.7.22-py3-none-any.whl", hash = "sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775", size = 136983, upload-time = "2026-07-22T03:35:11.276Z" },
]
[[package]]
name = "cffi"
version = "2.1.1"
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/9e/ef/008a1939e372c06329a3fce4279c02f328488f3526744906eeec3da7ad5f/cffi-2.1.1.tar.gz", hash = "sha256:dd31f52ea1086513bb9df30f8fcee9b8918323ae067a3d5b78bc826a000712be", size = 530807, upload-time = "2026-08-03T21:21:18.939Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/69/43965eccfdead3b9220015fd1320e117be8c6ed01a62ffab76eeb752f5d5/cffi-2.1.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:c8c69575568085ba0b1b10c0249d779a214aea6f6522e949a0fc9fb0fcb449d0", size = 184821, upload-time = "2026-08-03T21:19:44.887Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/7d/16e5a096677b5e313ca80cd5e5170efa3ea44624a82bb111925522da64b1/cffi-2.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f81b3b8f3d4e343550fa4baa0e479bba9f2d29ce9c2e9b51d1ce1718d7442fcf", size = 184719, upload-time = "2026-08-03T21:19:46.129Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/e6/8941622732edec876dd17d0453dce07317ae96db34f2ec1436c9d3785986/cffi-2.1.1-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:811bd1e21d32de12efca32393a0ab3f5133b54fce9bd44b8bd77ab07da14bf6a", size = 214799, upload-time = "2026-08-03T21:19:47.218Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/de/f98430906df1545ffde0d543dd124a7a439bc2cd32b36b9c53f805df7333/cffi-2.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:68e62fe11f30d5ca8289242866f0a5291402d8529ca2178ab8afc5c9694ae890", size = 222389, upload-time = "2026-08-03T21:19:48.331Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/5b/717f1526b9957b34456313c31645c5b82b8fb5c3fe9e4752999be7128bfc/cffi-2.1.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:4a7c934f7360e8cd64fe9efadcbd10c7c6364f531e432b9a4bf5ccbc9e0e8b50", size = 210249, upload-time = "2026-08-03T21:19:49.543Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/64/b3/f8aa4f3e34986c7e4ec45072d1b1b9dd295b6b18007b45518d79726dd725/cffi-2.1.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:3143d81e29e1e20a9ce10901ec369012947876596f75a222235965f2b7ae832e", size = 208775, upload-time = "2026-08-03T21:19:50.918Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/db/dceb9dd5b231e1da801793f8acc9f3c52a7e1afe40bb1aae37e02b0faad5/cffi-2.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c1453022f490d2459a11819d83ad1d586e9ff65a12ac3e705ffebd46d3685dcf", size = 221822, upload-time = "2026-08-03T21:19:52.054Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/d2/6cd24ae3be000a634109c247d1475d62e5616d0dc78c82770942ec384248/cffi-2.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:208f941bb9d18e768138677f0a6d2ce01f590df56043dda1df1535ac57c88517", size = 225232, upload-time = "2026-08-03T21:19:53.109Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cb/52/3fa190537004dd7f0ab860a6dc7c0175b8667f68d1e618a46f5498d30250/cffi-2.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:210019b6c7cf07f081b4c54635c8cf744377001350e29cc0f81c4377b4797735", size = 223597, upload-time = "2026-08-03T21:19:54.515Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/fb/0bb75b7039588c074b37ae99f40d9bfddf990ecb2fbc346ebccd2e56b9be/cffi-2.1.1-cp312-cp312-win32.whl", hash = "sha256:046bfc24911b37851ee1b51aab8bffe713d89c68c6a057b09484ce9fd5f69b4e", size = 175292, upload-time = "2026-08-03T21:19:55.566Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/79/615cc094e2fb508cade7de88d3b4f6c4ec2bab695c97bce9153dc65aadf5/cffi-2.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:f53e442b08449d42821fa4a4fba000095af9f62742a500f978a9f557ec44339a", size = 185919, upload-time = "2026-08-03T21:19:56.89Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/c6/d0ea84713fe46b243a436a18fcd47d639732747e21635c8a27191b06dc30/cffi-2.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:7bde5e4cc5c10140859842b9d383af292b22639a4dffb725314baf45968cef80", size = 180093, upload-time = "2026-08-03T21:19:58.155Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/f4/035513d4117049066b4779dc3b7c0c0fdad175fa13731c9f4003f1cd1478/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:b5bdfd1c873d4e093aabc0ca84c4ca6dbc4f752afb5c86f146d9742580c9da2e", size = 194248, upload-time = "2026-08-03T21:19:59.399Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/af/2aeb4dbb5fc41a04161ae9ff1518de7cec08e164f44a8ce6a4cf7fd2cd1d/cffi-2.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:31348097ff5bbe827ccc41795d4dd099d9f0625e7def00ee653c137a490c2a6c", size = 196908, upload-time = "2026-08-03T21:20:00.746Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/46/2e5fdde8555706dd98139a910ca11be02809f3f605ce956f655d0214e100/cffi-2.1.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:9d2055050ea716bd38b7f7f1579c275386646b4894c155a3e2f3cd62ed41b7c6", size = 184805, upload-time = "2026-08-03T21:20:02.02Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/41/4c7042f317b9217502988f0873af87e16ad606dc20f84e546e3e6ce9764c/cffi-2.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19ee6127ee34de7d83ce3d371ebc5ed91addbdcc39f9ab15ce4eb35a4e534971", size = 184764, upload-time = "2026-08-03T21:20:03.141Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/1f/1c3d90d91811c8f86ced9ed637956c54bfe5b79ca98fe976d7f8c8979f6b/cffi-2.1.1-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6a8dddef476fab96d066d578fc88526767b836ab5ab21754e1d5bf3879c31c7c", size = 214722, upload-time = "2026-08-03T21:20:04.377Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/6f/3b5ce4c3b2192d250f04908f2bfd91ef34552ec8f7716a5d4abdb8d67bb2/cffi-2.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f16c709686a78c727bbbf059f92b0bf41c6fc60deec706d2dc19f529175a6125", size = 222369, upload-time = "2026-08-03T21:20:05.544Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/10/4b3c75dde3d9663c9e02ba05c2668b954f671d4bbe346413ca8c696b295a/cffi-2.1.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:fcd22650c908d7b7da162bbfaab594a1227a15d1643a98c68b122ac642fa2264", size = 210175, upload-time = "2026-08-03T21:20:06.75Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/62/14f74b9543e605d17701dc797b815958b8bb70b7624ce1b832ddad48ed6c/cffi-2.1.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:aa9511c62d14da7aacc9b4bf51f3f697a621e83b2d6919008243c3aad168eea3", size = 208670, upload-time = "2026-08-03T21:20:08.04Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/95/86342356ff5953b3fb06f7ef7c5bee212d45e770abc7218d451b9148313c/cffi-2.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a931079504ecc49efed7744c476a5c343a92fabf66dec2db95edb1b2fdc770e2", size = 221824, upload-time = "2026-08-03T21:20:09.274Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/ff/7b3429ff53aafe931ed8a5fc69f481bbef7ba6de87ddcbb63d08f483f613/cffi-2.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a2d7755bef5a12ed488f4ef1f1b69ee9191d7396083b755a5d2295f6edb4768b", size = 225148, upload-time = "2026-08-03T21:20:10.7Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/34/a95870b9221e09cf4f2ce3178b1a210abdfe63a1bd357da940418d7b8d15/cffi-2.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e0bcb7e0f677f543555d2adff3bf19c05f66cdb4796e5ff602442ab2fe3c4ef7", size = 223564, upload-time = "2026-08-03T21:20:12.165Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/70/ea/839b50531021a647fb5e929f72cf97bc1ff702b5472166164b5b6e76b851/cffi-2.1.1-cp313-cp313-win32.whl", hash = "sha256:334644fbac4eff73d985a17a91226df55d0f394160c4cfb880e084c8f7161cac", size = 175263, upload-time = "2026-08-03T21:20:13.559Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/a6/8b149b2c3f2e11aaa1618ef64500b45f50f22c57a977a4dff1aff1f91042/cffi-2.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:1aa5645c30469b09530c4ebca77ebf8f17618293c58f8549cb1a543a50236e7d", size = 185688, upload-time = "2026-08-03T21:20:14.69Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/9a/11f687cb39d6a3504060d5242f04f48c735afb4d3d533958a20594890cb2/cffi-2.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:63bbfd5ded17c4840ac07cd8f1c21ba9d9708141f840b324f422f41b207e3973", size = 180078, upload-time = "2026-08-03T21:20:15.917Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/7b/d6bbf82b8b96e7391438898c42f5bd96dd02030fd5b64937d248220003e2/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7dbb61fe3a7699468030f71bbe5f8a0e326a151daa91beb11a6fc1f980c55e1c", size = 194064, upload-time = "2026-08-03T21:20:17.148Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/94/e6/bcc91b283be94735e268487a054004f0aa19947b6348fa367db53230abc8/cffi-2.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:f24fb43132a4c6b4cb4eb029492919b2db645be6808d738f244fd146c03c32cb", size = 196720, upload-time = "2026-08-03T21:20:18.268Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/99/c4b0c17cacdc9c3b8f280026286a9826d6a208c0f047591a3c3ce99b91fd/cffi-2.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d28630f5854ab07ab1fd4aba756de52326c82e6be15d414b12793f1975048b54", size = 184964, upload-time = "2026-08-03T21:20:19.708Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b3/a9/9db617d05d7367c1ad0ab00b3aa6e6f9281edd689b4ee9ea0e5a84e89c97/cffi-2.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:661c298b4821edebead0c91edd2b00374d67ad7c5a1f7a91d4442633b79d6a72", size = 184962, upload-time = "2026-08-03T21:20:20.833Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/b8/b42132ca113dc567d37684437b46ca1dafc885902b02a110a02d5b511857/cffi-2.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:58acb8ab8e295e6c5ea12f888cbb13cf21511ef2a3303a23f4325c29d17fe5c1", size = 222328, upload-time = "2026-08-03T21:20:22.118Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/10/c5c0cbf0a657aecf59ef511409734230bf556f05a0d6c9eed7aa5c0a0166/cffi-2.1.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:456a61fa52d579ebf9df2e9552ead5129855dbaff6c1e5a9b1bc408809bdc062", size = 209985, upload-time = "2026-08-03T21:20:23.401Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/6c/bfa0b87b03b9238148beca990292843c9396ba069b54496596594173de7b/cffi-2.1.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a4f00aa42f75d6e4595e8866e748cc1705adc0cddfeb2ca86d0d03993d63ba03", size = 208530, upload-time = "2026-08-03T21:20:24.628Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/02/4e7d553a7ac4b4238b38b3c1b80d486e9d4436f8d2acbf87a0997fe3f402/cffi-2.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b0431303acaea1089ad4b3e9ce4e6518193def1118d4073ca848635ee4ea2e96", size = 221525, upload-time = "2026-08-03T21:20:25.758Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/1d/a4aaf9babd75acb4d5f223bff71533bee748dd770a382619a798960ee9ba/cffi-2.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:64faea20f4e2613363a1a9b9c7dd73058f3ecd00133a511e72ad7c511658f527", size = 225053, upload-time = "2026-08-03T21:20:26.985Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/10/5dc0e7bdd18e22107054288283380fc97a06ae3f1656a106908d666a3c88/cffi-2.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5c58fe613dc5e5336357eff555824a314d8e43282600435c8d1cb6a7a2fedd13", size = 223213, upload-time = "2026-08-03T21:20:28.277Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/e9/d0061c364cde06ee43168a0d076ac1da512cbc380d44767b844ba34fe2b6/cffi-2.1.1-cp314-cp314-win32.whl", hash = "sha256:1a18a57b58cfb21fc28d72e876acf10eaed67a1ed96226f92af4df681d571c4c", size = 177682, upload-time = "2026-08-03T21:20:44.288Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/06/1c3e01e3ba14c39f6d10bfbac52753b7e22259e38088e5cfe1d704918690/cffi-2.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:3222ba5d678f80a030e6afbcc33dc1ae5cb45facabb61cee2c7016b8432fde48", size = 187949, upload-time = "2026-08-03T21:20:45.623Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/87/5b/da4e39efe18eeb89cf580ea9cfc66b6a7c3eadb808fc0cc1d3a295cb5a5d/cffi-2.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:ab36d55f9ed2d067327667c2fea18dda018eb628dd6347aa01dda6cf1f5d3836", size = 182947, upload-time = "2026-08-03T21:20:46.955Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/59/40338bf421c5accea1d45158170c87006ef1cd371b05c077e76476949728/cffi-2.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7750c6449dff7864bb9bb27ddfb0267756189201a3afc911d82b3caacd70dfc3", size = 188504, upload-time = "2026-08-03T21:20:29.495Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/47/5ecf1023850036e674c77ec4de86182d309ae344e39e7cba984b7df5d647/cffi-2.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0beceaabe56af686895136a2de78db54ecd8e4046b236b8fd6d6cb61389e9bf2", size = 188259, upload-time = "2026-08-03T21:20:31.291Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/9c/92934c3bea9f785b23eba304538c0b4d37a2a96d2431eb3a1bc87a11aa19/cffi-2.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:49cbc70e6542d4ccccb936558d1064a8012541e78f821f955cff24e357776c94", size = 223864, upload-time = "2026-08-03T21:20:32.571Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/45/ba4c93527bc38616a8bd36488acb69a2212d60486794f0c1f318949bbb76/cffi-2.1.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e2d65b31f36619cda3999b78b2aa9632e76b78448e7a56fc4240824200e7c4fc", size = 211538, upload-time = "2026-08-03T21:20:33.808Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/e9/b6ef565e452acb932fb0cb5443f44a78efbd1233e566f02b5a83855e9115/cffi-2.1.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:28907ab9bfb6aa13184cfc17c6b8e1023c5ab6fd7076d8c20a35e59fe04f8f29", size = 210688, upload-time = "2026-08-03T21:20:34.974Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/95/eff5f0cee78d2eabc7eebffec40d3fc1876b5f3c95582e018bb4b99601f2/cffi-2.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51b31d1c98274844cfd7838ce00bfc27c7423a4dc00fc0772fc3331c2cc90676", size = 223803, upload-time = "2026-08-03T21:20:36.564Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fa/01/579d39fb8bef00a335a23d83757b44feb24cd6345a2c451b64cb67b9c362/cffi-2.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e7cecbaadb83884793e05828cee59b210b24583b9c7425d0ba6a754fe22eb4e", size = 226763, upload-time = "2026-08-03T21:20:37.816Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8d/b0/0b44f47c60b01b57b6e2bbd92343f13a85a1d93bc46ccf6e47e244acd99c/cffi-2.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:25792eac27877609e7bb06d42ff88278a6624fff2ba9bbb523c09616b117e80f", size = 225688, upload-time = "2026-08-03T21:20:38.959Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/d2/3b7176cb570a1d3e27faf67b72f591af508036e0d8b2be2ef9af9e8c84bb/cffi-2.1.1-cp314-cp314t-win32.whl", hash = "sha256:8ef53b2de9bcb9197d31854256575d59dbac0cba72ac627bb291ef5eceb74be4", size = 182868, upload-time = "2026-08-03T21:20:40.388Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/56/78/31f00c1bcd97c9bbf55f1bfdf5bc809a5de8887473e90bb9960dca825e80/cffi-2.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:616f097f2fe415bc92a247f02e11f634e1f9e9a83d327e3c915c15089c87869e", size = 194104, upload-time = "2026-08-03T21:20:41.725Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/1b/58496f2ed0a35de575250c02a43ab3cc2c04d494a88fed31c1cabc0fd176/cffi-2.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ad2c86c495b899d862ea0f4b42891b8713a3bd45dd4105c7fd51c2a72f39f3a5", size = 186402, upload-time = "2026-08-03T21:20:43.042Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/8f/9ebe220eab48a093d1a5a5e339ab0dc7316eef3bb04d63c42f0251b61f50/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:dddad92b554513a31f272570678ba307fb9f618f05e3d4a5eacafff9eae03e1d", size = 194043, upload-time = "2026-08-03T21:20:48.179Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/69/844bad3ece306c4782c2ecb93597035b6690d48704b803914c199da1e8b3/cffi-2.1.1-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:da0e573f9f97159390c89d9f1a9e41908b66d408cc5b58d08cf3847d844c531b", size = 196737, upload-time = "2026-08-03T21:20:49.457Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/8a/af668013284634733f02d683458a0728739c7d6ddb5e14cb0c20832266fe/cffi-2.1.1-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:fb92203a88b3d3053034db775110081c49d28be6551923805e039924093761e4", size = 184933, upload-time = "2026-08-03T21:20:50.639Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/75/2f5207ff6d1a613133b23a5203cc0c2a628313b5eb3974d7956ae3c57950/cffi-2.1.1-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:2ae64be792b8966f2c69538199728b290e34726562896df1e5dc8ffd8d8188e8", size = 185002, upload-time = "2026-08-03T21:20:52.173Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/31/9e1313b0a6e30e91b3b3d3fff51ae99c857c07738e3afcce1f7334e1b7ab/cffi-2.1.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:507a24c282e0f42f8ed737cf048572cbf580468da5555764a8331735e9c736b6", size = 222271, upload-time = "2026-08-03T21:20:53.462Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/e3/f6234a833e6e08c7007003074723c406559eecf9b48dfc97471e5a8eb7a0/cffi-2.1.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:246fa40ce8645a614ff682e0b70f37134e460eaf93a775e0cbe3cca585a67a80", size = 209919, upload-time = "2026-08-03T21:20:54.783Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0d/fc/5f74e293fced6edb51af3a46c4ccf6c23c9943774ecb375ddbd522c76add/cffi-2.1.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:471cee653ae88de62096552e6d24ccb4a5adb8c8c9f10b5054d0122c15bf2779", size = 208529, upload-time = "2026-08-03T21:20:56.066Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/44/16/29e6d01b388bef055ecd6ca8244b3f4d336bd09e92d5d892187b9601084e/cffi-2.1.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aeae0e330c9f6acd681f647d46cefd30c29f93e3392882e792e82080c9691399", size = 221630, upload-time = "2026-08-03T21:20:57.336Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/18/fa7f1f6857d5eb88a4ca99ffcbfb7c387a287ccc154c64a73e86314745d7/cffi-2.1.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:42a494cee34437f05546455144f2b5d9ac09b1face62bcfce597d2e521066688", size = 225134, upload-time = "2026-08-03T21:20:58.675Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/9f/e8e3dfa04a1b4c241f8c91faacad872b4d4efd051d49764ad4e2fd4b9fea/cffi-2.1.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:cc572dace3f60ef98d7b12ff411d20f5362feb31a0439eab0085bbfd349982d7", size = 223197, upload-time = "2026-08-03T21:20:59.968Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/7e/8debeb04f1ab9fe2a6963964cd6f1aaf7192627b83926586a6a4e089c9fa/cffi-2.1.1-cp315-cp315-win32.whl", hash = "sha256:4f42141fc14250de6dde5ee7ea4432be017252d91f19c5ad043c084cea629cac", size = 177683, upload-time = "2026-08-03T21:21:14.901Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/31/5158704cc474ab65c1647932e88be78dc0873f47130e253be38bcaf13d01/cffi-2.1.1-cp315-cp315-win_amd64.whl", hash = "sha256:e6e8cff14d6fb0be70a09c0bdc58096f501952d04624ebf867e0e56da2df8960", size = 187897, upload-time = "2026-08-03T21:21:16.108Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/4b/b3a2da8570c704ffc0f9762cdc3ec0f02c8573798e0b5cf7f11c82bbb70f/cffi-2.1.1-cp315-cp315-win_arm64.whl", hash = "sha256:27350daa11d4f10c540e6e89dada4c54feb7256ad03e9a4dc075ebad7ba360d1", size = 182935, upload-time = "2026-08-03T21:21:17.271Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/ef/5443574510a1207e6f6bc38ba6e1f1de36cb48fef07b2728bb896a21f430/cffi-2.1.1-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c26608d2222fb1e94487e4a387d85f13eb55d5ed725cb25a0c589ac4ee60e7bc", size = 188464, upload-time = "2026-08-03T21:21:01.163Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/ae/a56fa8c4686ad50e148fcbc8d3ae0d03915ff5c30d795058988c24118cef/cffi-2.1.1-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:4be96343e422f2dfcd12ab5c9f5aebe03f82f737c6bffeca6830b3875cb44aab", size = 188262, upload-time = "2026-08-03T21:21:02.382Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/b2/6187f46f2912276a3ae284076109cc5c8680482f11f766ccf26db4a86427/cffi-2.1.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:937c0052c05a31ca1daf18de3158eed4dbfcb9cc107adbea227728d647be701e", size = 223779, upload-time = "2026-08-03T21:21:03.553Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/f6/c3ad28bd19f77047a03084424fbd4cbe997303267c14423737324be0385d/cffi-2.1.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:df423d40ee8654634421812bc3b196da3f9bd7d32929da813f8394c4348a5358", size = 211520, upload-time = "2026-08-03T21:21:04.863Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/cd/ccac9013a5bd9fd764de118674ab9c805b5ca10c19270d90ee273f8b2240/cffi-2.1.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:a730a083190634c65cca36ba5f489531576ebd79bcd5c8e172130f6453127231", size = 210673, upload-time = "2026-08-03T21:21:06.223Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/86/2976131c639aead931c5bee5aba67e4b09fbeb8018b6f282f70803f923a7/cffi-2.1.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:363e05fa78e15116c3c32c210ee36884fd6b9afa6d440e47112c3bd511d64cb6", size = 223835, upload-time = "2026-08-03T21:21:07.539Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/0c/33a7aeab2f9c76918c52e084beb39c570db3588133412929e8ec06fab90b/cffi-2.1.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:770de9db11e84213beec501cfcaa013b019820ca881e03344dea5844f7876d94", size = 226705, upload-time = "2026-08-03T21:21:08.774Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e3/26/2cde30fdde421130bfc18f70395731a6e6b2053c6a1978a5258ff04e72fa/cffi-2.1.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:7da0c5eff80f0197f3b3d1232ec5a682a9325f4ae9016a78f5f5ca35f9ced1f5", size = 225539, upload-time = "2026-08-03T21:21:09.911Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/cd/a361394c94b2129d604bb846f624a8e88255a3ee33129c434a00d715e64f/cffi-2.1.1-cp315-cp315t-win32.whl", hash = "sha256:06c72bb76605a4b0cd0aad6930b69d4baf7dd5d806cfc409b824191099700e66", size = 182707, upload-time = "2026-08-03T21:21:11.226Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/b5/ba2b299993c26577d529b6ae29841f9e15b9fcf004d65f423f4fcf94ade9/cffi-2.1.1-cp315-cp315t-win_amd64.whl", hash = "sha256:d9c275eaacd24aa73f94ffd6de08fc3f932424d8b6c376f4bed7cde376fe7bc3", size = 193772, upload-time = "2026-08-03T21:21:12.39Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/29/35e016098c814cd93de9cd320c66b5bfba14dc6ecedd3cb518fa7c408c69/cffi-2.1.1-cp315-cp315t-win_arm64.whl", hash = "sha256:d18e5ac0f2f03f4f518d3e23db0f0cad7faa1da8620e9c09461d443bbf6e6692", size = 186360, upload-time = "2026-08-03T21:21:13.636Z" },
]
[[package]]
name = "charset-normalizer"
version = "3.5.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/3f/143b048436775b0f76ac3eec145c019e8173ccc2885c8f20319b996d5e83/charset_normalizer-3.5.1.tar.gz", hash = "sha256:6117b84ea48435e5356dc737f5121485c30920ba43375fa7b434fd753df0eac3", size = 171764, upload-time = "2026-08-15T08:20:44.807Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/27/78873dc8b6a56357517b74b6bb9568b80450e7bb4f6ef7e3fa9d22aa0bd7/charset_normalizer-3.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:5b6d1386bf0096d26d3a863dc0a487a5b4eb9aa93cf5ba69683d29dde6b9d60f", size = 344456, upload-time = "2026-08-15T08:17:10.072Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/4c/be49ada26b1f0232d57aa89bbebf997a5cc2332a5616b6eca26ff680044d/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4582c27e8c889d64811987b5967fbd3ae0c823fe1fd933b543d55ac20bb475fa", size = 238530, upload-time = "2026-08-15T08:17:11.563Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/84/6f1290fa07ae6978d3960caa3eb1b8019bf9284ab7c2297b00c099ef4250/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d1c7a53a6c2103925cdd6d7229f8c567379f211c869793df679f2e9f738c369", size = 230200, upload-time = "2026-08-15T08:17:12.919Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e7/a0/47b18adeed31c8f16ba9700f32c1b18594cfa09f47eb672a488c273c22bf/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e6621fb2a4988d6e53eedc455e5903e2679f3967b8acb3d639f1b63c14a2e893", size = 262222, upload-time = "2026-08-15T08:17:14.571Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/fe/341861ac118dae06f3ec0eb487488af52128f2ef2faf0b11003944d22259/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c0c10730342b0c9b35dd1d619beb8214e520bd96a1f870f452680b238aab3e0", size = 258951, upload-time = "2026-08-15T08:17:16.158Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6f/89/bb5108dc6c3651dca963f2b0a3ba19bbcb370c94e1b6d3e0e844a58e6dca/charset_normalizer-3.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b9af956078716df40d985fb0dfeb2c2120c5ca92ba4ff4b388acfd01cdc14d08", size = 248801, upload-time = "2026-08-15T08:17:17.683Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b1/ba/ef83ae3aca816393decfa3530976f38a79812d707b80b580ac33b83f9877/charset_normalizer-3.5.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f9f8405c2c758532c74fed975dbee57be1f31a6e865c031870c79a6ed3212ada", size = 244070, upload-time = "2026-08-15T08:17:19.191Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/0b/c5292a2462d69b7378ea89793bbb5b2b6fcf6f7dd6d1667f9619094ad553/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:96fef3e886d6a9874b14f27fc193fbdc69d5d8035783d86aa4e1cea594e695f9", size = 240110, upload-time = "2026-08-15T08:17:20.547Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/46/22/111e5be3b740d5c2a5bfcedb3d237b6591e5c2e82ae9d6ffcb121fe0909c/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5d8531a6569d025f68e2321e7638fb7978f23db58e5f69f56913837aae03816e", size = 232836, upload-time = "2026-08-15T08:17:21.895Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/d2/d2aad6fe0dbb44b194bf3becb60f5a0ac48446ade999a47fe7bb41eb09a7/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:aae2ee51122d3ae968a3837d97dc24a0aeebb0dea23694422cd172bd30017cd6", size = 262712, upload-time = "2026-08-15T08:17:23.727Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/5a/337e4663a5eae6de99db940ee8066d4145caafb61327db62deda15313cce/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7235dc28fc6dd9d832ac7c7bce95367dedb85929f17368a0c2bee1e080b9acbf", size = 242977, upload-time = "2026-08-15T08:17:25.157Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ca/85/f82f8a92e31c7519410e2e1afdc630f28ec47490ce2c09a11c1a43cbb459/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4abdc5f9ad448c1ecbfae2974b820535d6bc6e7eef63babbab3d81cf46968c71", size = 260207, upload-time = "2026-08-15T08:17:26.602Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/52/643d11ffd60e9ac2fd1fb87e167a19285b9eefeff4a40e63c87cbfbeab36/charset_normalizer-3.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba501e667c17d8411f98e67a022d9604ef179aff0e459b7e292c796837c13573", size = 250562, upload-time = "2026-08-15T08:17:27.971Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/16/46556278c2168d12df9da7fede5dc6fc70e60301b26a82bbeec238c9cfe3/charset_normalizer-3.5.1-cp312-cp312-win32.whl", hash = "sha256:cfa1c0cc3a8f9f53f1243a5a99ac36fd003880199383b37672e86ddda9cb07e2", size = 178507, upload-time = "2026-08-15T08:17:29.277Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9d/7a/4c6c298171e6b3e745633180ff59350fc0ca0db1ffd28df1e369e0579f71/charset_normalizer-3.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:3617ac3cfd8b9888f145ad89dd6e692285834b0201c6074a5eeaad3fd4d668c2", size = 200551, upload-time = "2026-08-15T08:17:30.668Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/d7/eb95a042f0dd22e304b0b6472b154f3546a1a039a9ee89ccb2a7f61591fc/charset_normalizer-3.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:88e85ab89cb822c1e635f51d6d32e488f94e002e70e2f492bdb8b945543f345a", size = 180700, upload-time = "2026-08-15T08:17:32.028Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/61/2cb6ad133dbbb449fa2d37ccae973232f4827e799af258d15e589a3d1e9e/charset_normalizer-3.5.1-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:4f298bdadb8f0b9e5672877f647d1be9373ef5320c9e2f049795e26cad28b6a9", size = 211584, upload-time = "2026-08-15T08:17:33.597Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/57/a305c968be1ca13f3dd1b32f445877e97addf55d80b65c7cb35fac82b777/charset_normalizer-3.5.1-cp313-cp313-android_24_x86_64.whl", hash = "sha256:88ca277405c2d3b71c4e1c2ee0e7966e807bcba86a69d11e19ba199d18ae4491", size = 223359, upload-time = "2026-08-15T08:17:35.022Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/0a/d3646670292ce8d8f8cc11ac067d44885e697a5591f57a9221128da5e7b3/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9362dd90aa7dab48c0054a21187791ccf05473f7dba5d92b8033ae62164675e7", size = 194464, upload-time = "2026-08-15T08:17:36.452Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/93/d51ec556e01042fed6f993ea859311bc7917b466684182fbbceb6ca24762/charset_normalizer-3.5.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:977cdbd483a9cff38179bea4fd754289a6f2195c7abd414aba85410b3e66cc5e", size = 197676, upload-time = "2026-08-15T08:17:37.819Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/a0/562247944386f7d4ef94467e84876600cc1e0f1b93239aaa9213d2bc3cbd/charset_normalizer-3.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e90251c0c7bdd54a100a0dce3c07b7e637278c93af29dbf78ebb89a58c4bac7d", size = 340473, upload-time = "2026-08-15T08:17:39.303Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/31/e7/1d994be1b93d41e9502b8b0460eaa88a1dd8df335df415db87d6c3e91ab2/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:94d78ecec2605a8d0398b0f365d5f12a63248438516f5dac536a5eff7337df4a", size = 240156, upload-time = "2026-08-15T08:17:40.66Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/09/53/27923ce5cc6cbccb832037b27dca98882d9c53e9b69e866bbbef4aae7fc8/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d59b75732e9b6f27388e10c14b0259cc5f2e48c78627d185e6a177b58ad3cffe", size = 228246, upload-time = "2026-08-15T08:17:42.003Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/48/5a97e84d63af1d55c07439cb80e56d99a8efb4295700eb4e18c0d1615d2c/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0d929fc574b4d6fd9e7c0f5c2ede8716a41911923aa7fa5fce38e0818aa4a1ac", size = 263660, upload-time = "2026-08-15T08:17:43.627Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/c2/071575791dcc88316c0a9a65ce38897a82e4cfe4a325f0f7fe1b1ac47bcf/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:394fea06235c8543390050ed5f529187074b029fb027213f6c46ac11ab5d950e", size = 260354, upload-time = "2026-08-15T08:17:45.094Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/af/63240b0c0248c075c2535a1f1bd992821d8251b9f173abc13329661d09e4/charset_normalizer-3.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62b55f6722735a6c472f88361cde6640608773d9443cebdbb51abf436a1fcdd3", size = 250638, upload-time = "2026-08-15T08:17:46.496Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/66/70dfad64f15be09c15ccfee81330a7e515895dbe296dd23114e9a231268a/charset_normalizer-3.5.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa48b1b63d639f9483e0633e092f5851e2348c352f1f9bb6c8182f87884ef876", size = 244583, upload-time = "2026-08-15T08:17:47.963Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c0/24/ef36367d38b9ddd4bccbf72888c342e8de1f5ae506fa0b2dcf970e2732a1/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c71fb0d56c920c269cd3e2e3fe7c610e3f1fdb21a6ce60efa6430ff63676cea6", size = 242038, upload-time = "2026-08-15T08:17:49.481Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/ab/55e683ba0fff2e43adafc10daa3001eac90fdaa419a97227d5a7067eedde/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:485a0d363cafefcd2538a73c7c838daa2035f09b2c9f9b5e3133f80c6aeb84c2", size = 233677, upload-time = "2026-08-15T08:17:50.845Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/67/0f40eaf8d1b6e7cf15e82382a2965efaca787fc1c2794b7021d37aaf5036/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c0ea61a470e070686aa30892fed79e297d2c8d0ab46b8bcdf027d38c51da591", size = 264491, upload-time = "2026-08-15T08:17:52.61Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/64/12b4c2a11ee8df4fcc518c78b0d93e3a92bd3d5253d1617ce74ff0e8c7ef/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:90b7481fb62fbe172c558bc6fd1c4c98d82004a54a7551f20e11ac9bf0b8708c", size = 245196, upload-time = "2026-08-15T08:17:54.023Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/2e/651d910af6d0fba325eee1cda37ec5443462ed25360e666c144166eb6091/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:35fe081843b35aad20ffeccec3eeffbe637b15d14f3fb22cc1b59cd8ec17e93c", size = 261660, upload-time = "2026-08-15T08:17:55.491Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/c6/b09e05e6db7f64338e0dc067c79577b1138da86c1e38369096851d96be88/charset_normalizer-3.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fd0350afdc3aabd5576f60ea109228bd5538139713c7b094c5cd27c73a98bc6f", size = 252618, upload-time = "2026-08-15T08:17:57.025Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/4e/362d4f9fdcdf5556fb2aa3ce7d4a58ebce03ed1ff03aa1d9aca8d02f13f3/charset_normalizer-3.5.1-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:9d9a0dc7cbe9bec24c3f767c9122c41fe5a1bc43f47cd099d00d393e09769de4", size = 140362, upload-time = "2026-08-15T08:17:58.425Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b4/d4/703be739b26acce318bd29eb3b25b7209e1b1f527f9eae3d1f1f01fdde2b/charset_normalizer-3.5.1-cp313-cp313-win32.whl", hash = "sha256:d63600d620ad0064c3a748b950ac5ea38a80190e5498532efefa4b7b3f1da1f3", size = 177755, upload-time = "2026-08-15T08:18:00.037Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/33/56d97ade41c8db611e727168c52ae46c9224c362ec28d4b65d7e9869e8da/charset_normalizer-3.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:aea996a6aba25260827c9ea511d1addfde2da9eb686ac961838509086188b7e6", size = 199295, upload-time = "2026-08-15T08:18:01.506Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/75/5b20dd1e6573a01a08158fe104104fa2c8abf941745596954185726cd46c/charset_normalizer-3.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:fd0a274c0e5f9a21565cd9d3dd749b61f96b7aa1e20a93aa1ba4029518f2e5c0", size = 179856, upload-time = "2026-08-15T08:18:02.929Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/29/cd/2b812ce5e888f1ce69a5350281e58aab07ae64a958ecae8912f30865718e/charset_normalizer-3.5.1-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:774d157f112367ff4abd29019f38f023c24e00e56edc7829c20e358a5a913ad8", size = 212318, upload-time = "2026-08-15T08:18:04.403Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/4a/a6ee107430768a5334e6d63f31f148a04a1a491ef161a1ac9415a73f2fa8/charset_normalizer-3.5.1-cp314-cp314-android_24_x86_64.whl", hash = "sha256:26422d45fd13551cf564c58932f7d72b4f58b93b0fcf18c35ba6be12b46bb102", size = 224897, upload-time = "2026-08-15T08:18:05.997Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/d9/35ae3f64f29d0179c35c3baefe575904df2913dde519129c7f75995a2b1d/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:09a7bba9f739468c8e78c36a75c33768e53cb1959fc638f510454c14683f00d5", size = 194848, upload-time = "2026-08-15T08:18:07.397Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/74/76/f2fc7380f056cc273a53af37f50d08ad54b2c59f61078f31432edcf1c2bd/charset_normalizer-3.5.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c9548dc78002099910abaebc0a72ac58b7d30931869e0351c09b507dff4ece3", size = 198163, upload-time = "2026-08-15T08:18:08.989Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/40/095ce62fa078483cccc1fa2b36e6bc9580b85422a20ee9f925341c50e44f/charset_normalizer-3.5.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c428c6c31eb5f4277d7f8eccaf767fbd548ddd5ce3c8b4f4cbbfab3d96b5904c", size = 341823, upload-time = "2026-08-15T08:18:10.458Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/5a/0e58b1c04a1596e0256f407274a92d5fb2ee21324409d1fab1da48a65b5b/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2f06b7eae9dbe77fe1d644ca244dad508de8d302870a43f3c559b521270938a0", size = 242458, upload-time = "2026-08-15T08:18:11.989Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/95/b4618ce912e6db0b1aae89ba788e38e8a7eba0f3025cc66e8c0699f977b2/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b7430cf5728e68f6c462254009a6ef4086e1bea43cf2f57aa9c55fb4f50ff96", size = 226717, upload-time = "2026-08-15T08:18:13.401Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8a/76/c681192bbda3d55356db5dadd64381d5202b37c6b598fcda5282e88b5d3d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab743e9bc90c1f73552ec33e10e3331315acd2c397b36065b591b0181de533cc", size = 266111, upload-time = "2026-08-15T08:18:14.961Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/be/55127bfca72c0cff6c022488d140d7c5b04c771e3b72e9bdb4836d54979d/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f6f7deae3feb4edfa2efaf7c574fe88cbf055038a6abdb40188e4fff66d5699f", size = 263128, upload-time = "2026-08-15T08:18:16.515Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e0/91/39c3af510b0aa32bbda03374259200f28430febfd1bf5e511fe765282ce5/charset_normalizer-3.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15f024313246a4ed976c60f440bb8d257815513a681d212ff74fd46f7d715a90", size = 251240, upload-time = "2026-08-15T08:18:18.127Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/a5/cbe418bbc6ecdfc3e05a0116002897c4b403a5e838d697e64c78e9f0190d/charset_normalizer-3.5.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:823f82903d189af463d7df250ef1f7f696f3cee08cc8d91deb565e8d425f6506", size = 245282, upload-time = "2026-08-15T08:18:19.625Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/a4/689bb42e8e7cd492f3cb64907c6bc00ad247ec9a3628cd3f8eed126e8ae1/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:01e93745f7f219b703b60ba7afead36cfc4242782be5af484673fc500df12da5", size = 244597, upload-time = "2026-08-15T08:18:21.121Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/ce/9962938e179cf9f699d3f1e7b3114b5d7642dee6a893745229f9dd04f274/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:329fc3ccb63ad22d867d84c2adea759a64079a37ba4a343433b02c7a2816871e", size = 231376, upload-time = "2026-08-15T08:18:22.57Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/54/46000450ada53bd9eac5429a2c8c54cd2d9b39c0c255f229aea9af0948a5/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:bb57753e36e4855b8ca375069482250a6246372331a3e4f3407eaebb007443f5", size = 266715, upload-time = "2026-08-15T08:18:24.235Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3d/bb/618749d70f792b44252a777bf89bfb86823b9bbc1ea13fe8ce759b07f38a/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fce8cbd4997efeb450bd298b54f755dcdff18d496f7a5ddbb4867c6d7c88fdc3", size = 245848, upload-time = "2026-08-15T08:18:25.726Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/3f/ffb64458527c7668031d5eb095d978de561958dc9f5b53f8e488a533e603/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6c9cdde8becb25a7fde49924511aa2644d6f8081cc8df8e9452724303348d8e3", size = 264521, upload-time = "2026-08-15T08:18:27.193Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/ab/74a55fd803916a35ac461daf002708191aac19b546b80dc8cabfedc63d98/charset_normalizer-3.5.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9ac4444d8d4fd4c4bd08bf451ed3167aa9e7ec6cdb41b648794f1d1103652e36", size = 253054, upload-time = "2026-08-15T08:18:28.568Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/2a/6a9034b7d3c60b17499afb482df5878bf9fa20b50cc3887d5ef017a833db/charset_normalizer-3.5.1-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:f03ac127268b43ef4fe9e6ab6794a6794b49485a0cc0c1db79876d2f33f75bc7", size = 140580, upload-time = "2026-08-15T08:18:30.214Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f3/46/1d362e1a00d035d66b9869e1281eee115907f7e390a16a07824ab5737360/charset_normalizer-3.5.1-cp314-cp314-win32.whl", hash = "sha256:1f5883d77fd409a261abb5dc8ccbe335720d798b1de4abb3b1d47ccbbc76b53b", size = 180325, upload-time = "2026-08-15T08:18:31.877Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7a/7c/4938c329b6a9d446f6a59aa2092ff7118f274209b5ed0e26893d1d30a63c/charset_normalizer-3.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:c658c50ac0c98cd755a2dd50b7977d3bca7df401dcc47fbdfa87db53ef7d4e8b", size = 204175, upload-time = "2026-08-15T08:18:33.466Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/33/eeb384dbd8dec570661354592f4f2e1b2fcc92585624d146a000caf53841/charset_normalizer-3.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:4bea7f8ebe90bbd7f0e4a2de42ca6924ba23e3e76418c408ff82f1d46fabd687", size = 184123, upload-time = "2026-08-15T08:18:34.913Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1c/6c/c73fa9d5a85f6ab05395de61c5f6984e0a9ff40bb5ff888d46dff02526c6/charset_normalizer-3.5.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:fbc597639158fd7c14d55e808718848319540f51b0e6746e3eefa59723a4a348", size = 381682, upload-time = "2026-08-15T08:18:36.349Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/c7/63565f860921457feba93bae6c86fb7746deb4cffeed2f375cb845318146/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e71c909f353863b2b89c83de2ebed71ea6d0df8a6ef65a128193c5e650766bef", size = 240826, upload-time = "2026-08-15T08:18:37.887Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/ae/7ae8807410dfa33f8e6f1715740adeaafa8a816cc4cb33508f54b1f7c896/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7ac76cf9afd34929d76eb7fcb63be476a4853d8a96f0dcf2d0db68a0cbdf9885", size = 227861, upload-time = "2026-08-15T08:18:39.315Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/a3/887c1642f0da26000b0e0652d91071113c0e72cea33952e225cf589f49a9/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a3a370082ce34d0612f421e15fe011c53bb1feff21a26d06ad4fb244dab5a375", size = 260758, upload-time = "2026-08-15T08:18:40.88Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/11/e6f5b9a3d0e55b0ef7505cd3765cdd48f22db89994c947b316f52f801fd8/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:256dd4d85d9e4dc595e2bc983c980e73f62ddeb3165c58b4c3dfe78c5c8548c1", size = 259950, upload-time = "2026-08-15T08:18:42.351Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1b/ee/e4e10a94d51cd1ee638aa7e00b65399e6b2a4e8376ab6d2eac9f95586671/charset_normalizer-3.5.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:58d4aa13a59c969dbfdf9e6a9560e242cbfd9e8a8f50c2747714df1a423adf65", size = 249329, upload-time = "2026-08-15T08:18:43.914Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/25/d5f4198819e6059735a84e8d0bfb72dc33976da67b97adcd3fb5a5e07ec6/charset_normalizer-3.5.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0c6dfb5ca6723eeed15aa8e564a014d69fcb8812f94eef11fe3631e0508199f5", size = 243137, upload-time = "2026-08-15T08:18:45.368Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/e9/e925ca7569cf9fb9701fd82503fee73eea5268fdb856bdd64947092d3daa/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c010f5581d9c612804cc59fcf7b524b707fbcb72828551237ab545bb5c7034af", size = 242820, upload-time = "2026-08-15T08:18:46.842Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/17/672c251a888ed2aebcdd2fe830ad0104e25ff83c43f5c4f9c15e9fc6853c/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:52ec005752a56ae79547a05c0139ca2501a0c866390b6115008456b9f0e7cde1", size = 230504, upload-time = "2026-08-15T08:18:48.353Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/fc/f6a85abebd42ce4da2f1db0aa56cc6a0df1995e318b3875d14401b8381d1/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2bced4061f000f7187254a02ad3433ae17eaf991747ceea2f478422590a5bba9", size = 263087, upload-time = "2026-08-15T08:18:49.859Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/98/66/7c42677e739ba66746b297e2046918d793078094dc239e1e72768cffccc6/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:9eea3ab2597a5e65fe65296e2d6a84570845a6b55532d90333d740d48bbc850a", size = 243269, upload-time = "2026-08-15T08:18:51.601Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/d8/a50b79237f417af10f8c2a501ce8d1ca87829a22e69117891ca4ba20a69e/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:496846868fea80e479324862fa877f02411f2fd0f83b79ccee2607aa68b2a032", size = 258766, upload-time = "2026-08-15T08:18:53.23Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/1d/0fc91aeaeb3c83b748f532399ce67cf84604b48297405d740000f7a9e786/charset_normalizer-3.5.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:85d5855daafc240cc045c026d7a15fd198a09b0fc8ff6f5ecbb5297b509cb11e", size = 250814, upload-time = "2026-08-15T08:18:54.768Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ae/10/3d8c777cf9024615295aa1b808324ad5b4a77855869c00824bad74ffaf8a/charset_normalizer-3.5.1-cp314-cp314t-win32.whl", hash = "sha256:58d3e12c88e0950bca850ae1f7c256055c097639c2edb9eb123af9807d8b15e4", size = 191074, upload-time = "2026-08-15T08:18:56.305Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4d/81/ae557d3c44d1a1d688696d60563413a0866a91b7ebc50f20df838be3d8c8/charset_normalizer-3.5.1-cp314-cp314t-win_amd64.whl", hash = "sha256:acaf604462bf330b0d07e7a07c1d6e4adac79e5fb13e9c5140590542cafacc00", size = 216476, upload-time = "2026-08-15T08:18:57.889Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/27/e9/61c01fb8b804692569c036b3fc50495814502dcf13a60649c6055390b02c/charset_normalizer-3.5.1-cp314-cp314t-win_arm64.whl", hash = "sha256:fdb8a068947befafba9952162645dc2fecaeb400e64584829ed5e9b2fbe21a7f", size = 194115, upload-time = "2026-08-15T08:18:59.418Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4a/4e/8544831ef59d8f27ce92c80871380fdacc8076a8a56ed62f82e54f991333/charset_normalizer-3.5.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9085f87b0e38a2b92b8923059b4e8789fe40d9279712d15dcc670048d77079af", size = 342048, upload-time = "2026-08-15T08:19:01.054Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/a6/e3b46852424246065355644f4fb6dbccc0239a42a2eee27ecfc8957f0bcd/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2679de311c7946dde5d3b6f44941844133ff5c7cb86099c0061ab1e8901c20a8", size = 242997, upload-time = "2026-08-15T08:19:02.492Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/3b/0cc9a26777334ab2f2e3089b948bbf4e4fe72ea70b897715ef6415043ec8/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:baf3775a2635e5a11fbd5e4e64ee69c7e86875d224a5c72aca4c141064589a90", size = 237014, upload-time = "2026-08-15T08:19:03.943Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/c2/027335f0aa337a2a2e121bac1ad88c4f02ba6053ea0926802784f3db11af/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ac8c94b6539074e0f40899301273ac8402b9b3e01c7b7ba269ff30340aaaf20", size = 266174, upload-time = "2026-08-15T08:19:05.598Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/86/d3/e367787febe4e74769dec0f406f2c3c8d1b955fce5aee1fd0f94e8367a45/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe532b3c966d1fb794e0698e4589d0444017ae77fc0b31edea13c0e35bcc449", size = 263361, upload-time = "2026-08-15T08:19:07.251Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/3d/391b193eb9f3e84b02f9314088c386debdc0debee843535aaea2e2c6715d/charset_normalizer-3.5.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5c84bec0ab5ae0c64bfe73a7d2adcb5ce73b467523fc27fd6a28ab2aa6cbe35a", size = 252143, upload-time = "2026-08-15T08:19:08.816Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/57/de221f1745a90d418199761967e2776bfe2c275a1194220985e8c1d37833/charset_normalizer-3.5.1-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:854066be00447fa8de2ccbbe893e2ffc4b123ef16d897af794c1e18bd4a714b0", size = 252086, upload-time = "2026-08-15T08:19:10.255Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/e3/d119f86a01f9331e8186175f24873b1d74a7ee9e2e4b4d68f9947dae5afd/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:21b82d8082f6f5e7f456ef0bd16323d08de1266efbfeb476e64b2a91d1471a4e", size = 245231, upload-time = "2026-08-15T08:19:11.807Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/de/d8e48c135ae480879539cdb179c8d3b50c7879497d75dd899b5763b69cee/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:838648accb3a7fd9803fd45c87bce8509648eb0c11bc34e216141300977244f2", size = 241546, upload-time = "2026-08-15T08:19:13.416Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/c4/217755fd1abc50d326c252922cd642002758095a81ff45010337b8b3ef65/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:195ce897c6153c0700078142cf8efe3e6454ca4cf4357499e4078dfd83396626", size = 267033, upload-time = "2026-08-15T08:19:14.981Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/d7/34d8e404e358d2adcc5a228c2134643af00104c8fb0bf525f3688d756f05/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:978eab16f55b4ab2c2a745be9a0a840bf8f09a7f227d9c76eb30214d078865a5", size = 252045, upload-time = "2026-08-15T08:19:16.618Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/fa/40414471acf0aa0692ca77305aa00e434fcd8288f0941c93c30e9a5f8f2f/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:cc0329df4caaceb950d2f580b5ac716a377f7059624a0bafaeaf8a218c6ed774", size = 264866, upload-time = "2026-08-15T08:19:18.101Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/90/fcc850bae791abd2e0c041847f13e270aa08692a79f3e00de6d2dce1cb50/charset_normalizer-3.5.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:687c9ca3035544b113bea2055e180af96fb63c0c476e22a9180f51925186e7b7", size = 253932, upload-time = "2026-08-15T08:19:19.734Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/af/53afe99068b3c10b4cbae592a52ef72a7c92c0188440e83ee3a078fd8f75/charset_normalizer-3.5.1-cp315-cp315-win32.whl", hash = "sha256:706bfd38730a5ac7a365793269a00f4e988178cec121391f4248d84ad8c972e9", size = 180320, upload-time = "2026-08-15T08:19:21.37Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c9/bc/f46a132041b29e4a8779ed712d3df1bf112e94ca8de58b66d7ec2c0cf8b9/charset_normalizer-3.5.1-cp315-cp315-win_amd64.whl", hash = "sha256:92caef967d287a407085d61176fce4012b1dd62daed4eb6d5ceb26d3d2538712", size = 204174, upload-time = "2026-08-15T08:19:23.088Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a1/5d/9ed554480eda8e447b673648628fdc29574d23dbad01fe11837adedd1cae/charset_normalizer-3.5.1-cp315-cp315-win_arm64.whl", hash = "sha256:5fc45d653ea8c9a20479167e11d4a0f8cb2fa3470737ab6f9c827532313187b7", size = 184126, upload-time = "2026-08-15T08:19:24.471Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/32/9b8929bf384061ee1fe5d9c27c6f9776d3d824039ad4e14c88ec00c7808e/charset_normalizer-3.5.1-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:59171c6e45bf07d0d5cab3b0bf81d945035530f6873398b3b531c31184d46663", size = 381441, upload-time = "2026-08-15T08:19:26.038Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/96/10/e9aa7923d3ddac652c99a1c5f7be494e737e151566a44abe018daf757f2c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9dbdd9205662134957cf0c324f639bdc5031c0ca056e2369e238db75187c0f11", size = 241742, upload-time = "2026-08-15T08:19:27.532Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/53/a2d249ebddf47b889a100c0bdcb61a2f9dbb8bc24ef325cc062e4f476877/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e4b018dc5a0eee4676e38fe84a47a427816c590b93b55d9025274ec4d6ffc2dc", size = 235298, upload-time = "2026-08-15T08:19:29.274Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/07/469f78af590f7d5cd48e20d8dbfa3d66deeff9ba37768c04d886b5afd45c/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ced3fdd71aaa83ce593746c2edb42b7a59cb4c19c8b5c407781c72e493aae55a", size = 262500, upload-time = "2026-08-15T08:19:30.955Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/55/66/3bb56a47f7dcba014055b1a1d33c6f08bbe9c1e74dba154cfa25f90ae885/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:19a3dd5aa73cef1c99687c4fc57db016a9c17104ae1185da88ba566a5d3bebe4", size = 258888, upload-time = "2026-08-15T08:19:32.458Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ff/c1/2adc2800903fb013210349313b710a5376856578d9e33e6b9a1d8b36714a/charset_normalizer-3.5.1-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cc5d36d96478aa9c60654bd932525bf32964c62a7281eafdf16d85003a8d6004", size = 250243, upload-time = "2026-08-15T08:19:33.94Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/b5/a18d0dd1157ab655cc2cb14a545f4a4784bbad70ab3502412e36097502d9/charset_normalizer-3.5.1-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04368edf83514385ffc3e1cfd4546e595f4f1272dd23ba437a93a9cc3741d47b", size = 249871, upload-time = "2026-08-15T08:19:35.413Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ad/c3/525f508cd1e58d0450ac55ed40ac75bc3a97482c59def5278456a5fbf03c/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:9b5db6052055d34d41230fb78d7c439c23dc536a9896f6cb039e8dd92cfc1263", size = 243580, upload-time = "2026-08-15T08:19:36.886Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/c1/49a91fe7e97c8140094ca5c64161ab623a70d9f636bf834eace14048acb5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:252d099029bcbea642f2a06c4ed5046bdf8b5a8150b64afa5e027e88b106e5ee", size = 239807, upload-time = "2026-08-15T08:19:38.392Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/58/56a48c296601274c4689b864a8e2dfb209b81dfcb39472753ce95eea662b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:6199d5606e2bbf2b096cf64d03f8b6790c91081d5ac866b8e7bb6422738cc60c", size = 264083, upload-time = "2026-08-15T08:19:39.856Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/4c/dc48409274a1817ff349711d26c62aa0c597df865d4d69ef79160c859193/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:77efcff2b23071c349402ac1066667a3d011f62398d81408c9b88ad991747c9e", size = 250317, upload-time = "2026-08-15T08:19:41.53Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/58/d325912115caec62d6bdd77bbab5e0b7da5d234a9f20affdffcbcb530d0b/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:a5cbd90ecf0fc62e64726917ad083b73001f0563657a87ec3c0b504e277dc90d", size = 258173, upload-time = "2026-08-15T08:19:43.07Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/f7/b13b1ccae2c8ec63980d13be1890eb73f8aeabbfce02a24aabc0908788f5/charset_normalizer-3.5.1-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:4d26f14f041e83dd8edfd61f4cd4fa7285d31798b5bf1f28e70c367ba6c41d61", size = 251960, upload-time = "2026-08-15T08:19:44.587Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1e/25/ed3f9919c5aef8cc818be1f972f565f7610d7b2076b8ebb98839516ffc3c/charset_normalizer-3.5.1-cp315-cp315t-win32.whl", hash = "sha256:ac13b004224fb341e1e25a1ed5e19d32f57cdb2a403e01f003b46f051a550f6f", size = 191186, upload-time = "2026-08-15T08:19:46.293Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/d5/43c2b3e9d8267092b913eb8b0603f0f71993c395632886bd37a7223f96cf/charset_normalizer-3.5.1-cp315-cp315t-win_amd64.whl", hash = "sha256:35aea775dc2bd5f54cd84a1cd2696cc3207c479cb9cf0bd346f0d343e4300ddb", size = 215947, upload-time = "2026-08-15T08:19:47.853Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a8/76/9aad3e9c8865e5e0efa9a7f6f81c37a67635a985145ecd44528a81e088ee/charset_normalizer-3.5.1-cp315-cp315t-win_arm64.whl", hash = "sha256:fb78f6e7fcd8ad785d28cd577168bc1aaee827b25bb8755638f694794ea98f0a", size = 193909, upload-time = "2026-08-15T08:19:49.383Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5b/97/fb4e82231aba271ffd775a1b4993b0defc4e3059f286ae41d9433409fe85/charset_normalizer-3.5.1-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:41876ee62a3dddf48ff1121ad8f0798032aa03f2fd35f21f34a4cab14f18d8d2", size = 331467, upload-time = "2026-08-15T08:19:50.959Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9f/2f/fe3f187327aac18e2d54e9d2b08e15d27bf9b642d9e51c219f130fc34d1a/charset_normalizer-3.5.1-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6dac12ff6b846103483683f60c5f8fee205121adc58ffd87e90a90a3af69e99", size = 253057, upload-time = "2026-08-15T08:19:52.654Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d7/c7/9e48cee5c161fe24da823b61bf381921d77cb994a0a4de148e95018c1984/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cee5dd7c6fb5dd52a0fe2a740f9bc6e3593f5f8b1788bde49de02086f30182b2", size = 240930, upload-time = "2026-08-15T08:19:54.163Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/e0/716601f3cc69be7b198951150c75ead1ece33c3c8036ff6ffa46029659a0/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:343fb4f2821043bd87095f7b08a1a181febc8e36ac64212143bbfd0a0e1bc235", size = 230822, upload-time = "2026-08-15T08:19:55.807Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/05/71bfc5caa0abcc45aea1f6a4d50ac68e59605ddc7666fe8494f4cd229665/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ae4a097991662cd4fff0ddc74e0fe7874f82e00042fa0ea00855645ed0c79598", size = 260037, upload-time = "2026-08-15T08:19:57.312Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c3/92/de7e32ed05341e7a9c4c877c318418197b7f2d66a3b68d561bf2ac57ca3e/charset_normalizer-3.5.1-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4b599739b93b2cbeded49645ae3c8d1405c29ddfbceac1545c87a3f9580a9e96", size = 255097, upload-time = "2026-08-15T08:19:59.056Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/7b/ade0a122600319dfa0b1000ab0f9731c94a817904cf3c5de408c73a4ede7/charset_normalizer-3.5.1-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b39b69b347e5e47a3b5b8cfc005c68c1ba347474e3960236c4944a8ecd174962", size = 250166, upload-time = "2026-08-15T08:20:00.612Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/9c/019fbb9f4834491a160951349b1a3714439376f66e5f7cf18b4f18f0c7aa/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a2028475ba855475b8b4d3cfeb4994269c967aea8b9892dfba907f4263a863a3", size = 241821, upload-time = "2026-08-15T08:20:02.321Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/b8/11d4840bfc99330cc7fbcc2681ee5a044553a6e77655508d8f9b2bff7b34/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:36047af20e17097c3bb9476c2b7655f2f7aa51322c0ba58c07695bedf755a950", size = 232529, upload-time = "2026-08-15T08:20:04.008Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/96/2b3a21492d9f65171ac75d872f5018260013d00bfa0ff70ec9f179148cbd/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4c4fb141a727957c93edfe5c32a26ceb6b5f6461d67146e2d39f51e16170bea8", size = 260348, upload-time = "2026-08-15T08:20:05.877Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/aa/a69a2028e8bd052476c245460ab19d7de595de084dd968f2d75cd50c3e25/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:2f293479cce755c75f1697e87c409b7ae4c555c7dfecb6e988ad13abba943031", size = 247234, upload-time = "2026-08-15T08:20:07.487Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/8a/3d130aeabcaf3d2466af76b7b141c08d9e89c9016ab4b7cdd0f7dc2d1c62/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:3588e376b3ea2eea84976f67273d679f229e24c66dce7b82ae45aef04ff6e072", size = 256917, upload-time = "2026-08-15T08:20:09.142Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/80/c2/a7379b840292d0c1ab9fbd17d1f3967aa81794dc95bc74be8999d7fedcf7/charset_normalizer-3.5.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e199fb99720074809a7720f1c0b4d919eea8b87e88713e0f8f602f7bef543d9d", size = 254846, upload-time = "2026-08-15T08:20:10.727Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/01/65/d43b714731bb2f40d4053dfa00ecfc1c5a301f8e3316c5db3a09af59fe94/charset_normalizer-3.5.1-cp37-abi3-win32.whl", hash = "sha256:dd732602a7009217f658d5863d12d79d373a4de0eebc111094bcdd3bb8e0a6cc", size = 174216, upload-time = "2026-08-15T08:20:12.334Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/4f/b911ed898b26a09789eba9c9200c999aff6c61b4bafaf4838e56d1a1e1a3/charset_normalizer-3.5.1-cp37-abi3-win_amd64.whl", hash = "sha256:70055ff39b97c99e7ae40ea3e393fb62aa2e44dbd9b29f8d14f42fb0025c3959", size = 199764, upload-time = "2026-08-15T08:20:13.908Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/a7/920baf467bfd9bf689f3b318340f37aee4572a71f162bd8db51da55ba4fa/charset_normalizer-3.5.1-cp37-abi3-win_arm64.whl", hash = "sha256:87e4f41d375c0b9be2fb5251aee4b8a689169e134535aed81bf085c3b647451e", size = 287318, upload-time = "2026-08-15T08:20:15.551Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" },
]
[[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 = "50.0.2"
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/9d/af/182eb91b0df3fe75c4d9f26fe70684569566745f6ba7e5c9c73a862c5252/cryptography-50.0.2.tar.gz", hash = "sha256:7b46165bb56eb4704e2eaaf86f3c940d19154535d9b0ca7d6d590b04060e00d5", size = 880623, upload-time = "2026-09-30T15:30:04.884Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/56/d194340cc4a57535e82e1bee9e89667ac4b7c13b5d3f59686deae3094dd5/cryptography-50.0.2-cp311-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa8f5efb344d6908a1ce62f4a24e2e5780f825d6f53f5f50ec5ffacac72936cb", size = 3914904, upload-time = "2026-09-30T14:43:44.339Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d9/69/c9bd862c3bf43d6399c433caf002df16e2dffd4be49bdf515cda38038711/cryptography-50.0.2-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:79def8d059362e7831389ed3be0ecdf58a89386e1271e35dd9f5af84e81bffd0", size = 4731146, upload-time = "2026-09-30T14:43:47.113Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/21/69/64cef1f702bf6657e0cc186ed1a2891d50d29fb41586b254e1c07adea261/cryptography-50.0.2-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:630ebfea3bf689d075f82316324ff7433dc447fe6bc1bfc76524b74b4a9567d2", size = 4719841, upload-time = "2026-09-30T14:43:49.01Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/6b/61a3f8d8c5e1e49a6cddccafc4015cc1c0021360ab0acb4080e7a423644a/cryptography-50.0.2-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f9f6143a8c75945eb960d9eb98905a441394abfa24afaae239d514ffb2586480", size = 4738340, upload-time = "2026-09-30T14:43:50.932Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7b/2e/7212ca32fd43dc91f2f41db20160b268098874b4c9a0e7be94d6835f5b2e/cryptography-50.0.2-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:a582ab2ae1d34f67112cadc86702774c9ea4374df6bca6afe672817203c99134", size = 5367029, upload-time = "2026-09-30T14:43:52.911Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1a/f1/b474e930c4d910328780e3940da76f5aa5cbc48ce1fc14e44d239d9ea9db/cryptography-50.0.2-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4061c0079120205fb760c58acab6443e217307dcf05e3702cf970e0689972856", size = 4753050, upload-time = "2026-09-30T14:43:55.272Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7c/52/9af10e80ac16b0fcc2123f9cbd5e7afbd0fd5075bb7a607c592258a39cda/cryptography-50.0.2-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:ac9ed99d81760c62fe89d5f0815cdfa1ba9a35141cf30f1c2d044f04b4803d2e", size = 4376724, upload-time = "2026-09-30T14:43:57.24Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/37/6202e488cc1eb625ea110c292c6bda92823176e023f427d8d5660ce8d632/cryptography-50.0.2-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:87e9ce85beb6b328ba370cc6e6aea483c92617b4c95b1d33a49297eb662bfb04", size = 4737859, upload-time = "2026-09-30T14:43:59.541Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8f/30/e86d7d518489b0ae2497091a35287abcb1a2ce4037837a34afbe9b1d6964/cryptography-50.0.2-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f265528741e048bce55c3463ed721fb0aa45a5888d8add8cfeccb3035451bbdc", size = 5324103, upload-time = "2026-09-30T14:44:01.901Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/69/2c833a049475e0a3444e94c7d0aca0aa51d166374a449b09e92ac98138de/cryptography-50.0.2-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9dab55f57c74c3cad24c323bacbbd04be4705ba6eb0d92e920b1fc4837ed5079", size = 4752576, upload-time = "2026-09-30T14:44:04.545Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/5d/906970b83bbfc1f5bbfb677a143c181f2801f23b6a7204a3b47c42c97e65/cryptography-50.0.2-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:25784ce8b9621c90c643efb9e1e2162ab3b0224cae446ad5e70e7fcb1ce18b51", size = 4870819, upload-time = "2026-09-30T14:44:06.884Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/68/e3/f2298d3bb55e0c4a91841ec4d01b3f020ba8c5fbf15ccdcc6dcf03f97025/cryptography-50.0.2-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:85d0d9a31b9098e98534226d5686b47264b95e62ce459dc2e62fdfc809f9fe93", size = 5030152, upload-time = "2026-09-30T14:44:09.443Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/4f/adfc442765721292fff86d314ce385d3249d22db42295c0dd057727b60f3/cryptography-50.0.2-cp311-abi3-win_amd64.whl", hash = "sha256:7afa5a6602a9f29af1f3a2965f831bae7c9d5d597b7cbb716d41ab3b7d89879c", size = 3824692, upload-time = "2026-09-30T14:44:11.671Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/cb/52eb3770c0d0be2702a98c6e96065ddc0a2877cf0845aa9c23397c142cd4/cryptography-50.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f785f6161f202ab04d8ca194158968798e480ca058943907972da5f12e2881e8", size = 3892731, upload-time = "2026-09-30T14:44:13.485Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/19/8e/aa1fc533d4546b127b45de8aa024eb5933d23eff9debfe25931e56861095/cryptography-50.0.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ecbc5652bdb6fc9eaf89a7d196e20941adfe812f43bc4ca05d9150496821047", size = 4710431, upload-time = "2026-09-30T14:44:15.427Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6a/64/72bc3f75176e7e406b748a3e3830432b8c51297b38368713df04dc04898a/cryptography-50.0.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ab50ee449bf968271e820086f10a33d101dd060370abc10bcd22279be2656539", size = 4694824, upload-time = "2026-09-30T14:44:17.69Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/c6/62c77550edfa5ca3f14bf44a1e6739b9fa09d6e998a11d97ed8213bccc98/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:a9f7355e6fab51f6c369b86fb7571cffa05edee2c2121e0380a37fb9ac1cd5c1", size = 4716967, upload-time = "2026-09-30T14:44:19.661Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/37/cce70f150c432914460157a6ecc161752e053aa5ec0ef3b3f7dc6e31039a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:94e5e9f108ee10471288214d3d233fbfbb492840a8457eb85178d643ddeb32c7", size = 5328676, upload-time = "2026-09-30T14:44:21.744Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/9a/6f2f0304d634ceafdeaf23e84537336664ac419b5d07611675c2ad3f6b7a/cryptography-50.0.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:241449bf940a5d27309bd317e6f9a2af6932113818bb2b8f5c59ddc7ef16da18", size = 4727698, upload-time = "2026-09-30T14:44:24.178Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/1d/de/66bcf9244d118663b2e1aaded8990f4640e3d7b7411870a5765f252074d2/cryptography-50.0.2-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d8947001be83df1394050758ce0e745dd74fb134eef0a4b5124208dfc3a68c37", size = 4354821, upload-time = "2026-09-30T14:44:26.263Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/e6/db28a28c7b6c676addce89136de3d8db49ea825a8c863472e36e42ead4ad/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4a20ce1e5cb4284a86692fdcba7cb8754185c6b2e5c56fcef3751cf451d3cdc2", size = 4716748, upload-time = "2026-09-30T14:44:28.447Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/30/96/01546c7f69ea0e2ab790a2e4f0934a4052fb9b388147fbf83c2fd72f1e57/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:84f964e537f916e2cc85199e5a88742e964939b575ac8598b3f9d6cc416cdaf1", size = 5285085, upload-time = "2026-09-30T14:44:30.704Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/01/03263395f74d50b071e9e66daace3f8bef80493e5d410726f2ba8554736b/cryptography-50.0.2-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:828d49b0ff5a0e3975865571c5d91dbbdd0d38d8289b249a163e9425413a5e05", size = 4727268, upload-time = "2026-09-30T14:44:32.92Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/94/2bfe8f29ec0cc9c0d99359c4161adf32858e4934b72c6d100d2ac0bbe962/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:deb9fde5c60e437ee4821bc9bc39ff31b42135c27e1dc61ef0a629389c1de62e", size = 4849503, upload-time = "2026-09-30T14:44:34.969Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/44/e80651ecbf0e42b62e2bb5f5768916e07eea72e1297338956a61df361f88/cryptography-50.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8c71ba2cd31fc93748c38e1b613200ff1c2665cbfd5341fe3a61cfde35a1430e", size = 5004057, upload-time = "2026-09-30T14:44:37.064Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/cc/1d33befb3cd7ea7e77d2d73f43f2066471da1b21f24a6156efcaabf6d2e8/cryptography-50.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:78198641e5be9521beea5aa782bb551a58068d10e6eb04c9c680c1b69f2e7d45", size = 3795868, upload-time = "2026-09-30T14:44:39.71Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/49/93f6a6e7a87c9aa68d44d3e1cdb5fe8f60c90d5d2f46acae9a56892816b8/cryptography-50.0.2-cp315-abi3.abi3t-macosx_11_0_arm64.whl", hash = "sha256:edc3342adf8f697fc5f59c887a304356f147b397809440ed64e2fa6af2f50f37", size = 4133708, upload-time = "2026-09-30T14:44:41.807Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/75/32ac2a56243d778805c16ca6a32b8f74fb757df7e28d7ecb560afafb59cf/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d370b8d1dfcdf7130178137f6fbee6140774a1acc6cacefc4b42643ec11d0a3a", size = 4956267, upload-time = "2026-09-30T14:44:43.693Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/a4/2c8d734e43d97f0842ee9f1b7b4bfb3d0cf5e19edebf43c2afe6675c2320/cryptography-50.0.2-cp315-abi3.abi3t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f2f9bd7f90c64fe89253f0a2c05e3c4856072660429ce8831b4235bf29403a67", size = 4966465, upload-time = "2026-09-30T14:44:45.769Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c2/58/ee288c829a6f41f6235ae9dd33d82fd19b45442b65b4c8a3da36963d9f7a/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_aarch64.whl", hash = "sha256:e275096ea1e60cc595cda2836fd4a6c725d1125108b868be17f53684d164e2cc", size = 4959356, upload-time = "2026-09-30T14:44:48.211Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/20/9ded6d51ddd9897f6b6e81fb9ebea7951d7cc5d6c890b0ed8abf77a51a80/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_ppc64le.whl", hash = "sha256:b13478603dcd0a2479ff8e87e2c19a7d525734686fe3c49542472293a204212d", size = 5548822, upload-time = "2026-09-30T14:44:50.86Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/02/a8/8df951850d6b31d2a00218f19e2b3f999523437ed7a819df7fa427942fca/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_28_x86_64.whl", hash = "sha256:58a0c478eeca76fe5e07993c5a0703def34a6dc6a0cda4f5564639b33112ffe7", size = 5001199, upload-time = "2026-09-30T14:44:53.379Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/f9/36b3022218ce75b7cdf068fb95f809f9bd0d820e4955ef43b90c255cc7ac/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_31_armv7l.whl", hash = "sha256:d38cdff612d06fa6a32840d5e1b1f7a27cee4a349aa9085d94a67789d6bfd408", size = 4629333, upload-time = "2026-09-30T14:44:55.635Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/72/20f99a219f6af47cdd1cbd978c243b92d71496e168a746138af44ded4f29/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_aarch64.whl", hash = "sha256:fdd28f912fccfec1846a94e2e1e8f9b0012f557f0c46fe4f3eb0d7a87afcf90b", size = 4958822, upload-time = "2026-09-30T14:44:59.639Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/20/196f112617fb08eb4d608a2a6c422373d46f9cc2857f38fc0667033c0899/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_ppc64le.whl", hash = "sha256:cbc8738fd8526d80f35cb3a40d41f41a2e7030bb3b18b09a6778ef63d291c2fd", size = 5506351, upload-time = "2026-09-30T14:45:02.267Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/95/83378121ef3eaaaf71d4b781577ff794acb39b9e1b87a3f156898c8497ed/cryptography-50.0.2-cp315-abi3.abi3t-manylinux_2_34_x86_64.whl", hash = "sha256:e105ab60406787da31fccc883fc0f733af1efd78f0136a4599692c4083a73d0c", size = 5000859, upload-time = "2026-09-30T14:45:05.009Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/f7/70fd7ae4d1dbfa7ba29b02e1b9068771519a86027756510b700ce81086a8/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_aarch64.whl", hash = "sha256:6f8700550aa1474a91e5dc07049c46f98b423b5b1ddd0483e0b51362eeeaf5be", size = 5092151, upload-time = "2026-09-30T15:29:15.932Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/be/688367b74de86984bd58d8efacfc7c9e68b89a6a22ced0fb4f38db50254a/cryptography-50.0.2-cp315-abi3.abi3t-musllinux_1_2_x86_64.whl", hash = "sha256:c71be1cbfa5cd9a41ee452acf1eccd82b2c05950358b106ec8ceb83411d1a020", size = 5286120, upload-time = "2026-09-30T15:29:18.309Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/39/d1/55f8a3f2ef5d1529e16835ef10cf0fe3d559ce237b46dddc440c0bba3649/cryptography-50.0.2-cp315-abi3.abi3t-win_amd64.whl", hash = "sha256:c423ab384a46c4dff7217b2ea5ba2e11cffdeab6441acd04cf65a369caf0366c", size = 4111557, upload-time = "2026-09-30T15:29:20.155Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/23/ad/ac987755d00e1e64273760228d2635ae38dae2be83e3c6e0d3289d91dec3/cryptography-50.0.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:0ec5f09541743261e66e291b4a0cbf0fb2997aeaab6d9e9c740b9dba1b58d1c2", size = 3943588, upload-time = "2026-09-30T15:29:22.265Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/8d/6d585339bedf85d45044c85d8412dac53f2bb6f918e8b7777efba1787844/cryptography-50.0.2-cp39-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c5e67125c7dca78d199ec4e116aa93dbb83494808ecbb8211a2cb09b1bf41dbd", size = 4756166, upload-time = "2026-09-30T15:29:24.58Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/f1/1c1f6874e8550cfddd4b688ceb38cefb6ed15ceed224d56f133f3d88c214/cryptography-50.0.2-cp39-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee247f5c245c9a2fe7c8e2214e295918838e44e00a45a6718451e4004219e767", size = 4749145, upload-time = "2026-09-30T15:29:26.807Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/63/61b15dc1a8de03fe0adbe3fd7608b3ad5c73bf50993bbcb1faaa930afe33/cryptography-50.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dfe9763530994147d9af1def057a5b9658b00e8f8fe8743d144d1e0911c2e454", size = 4763638, upload-time = "2026-09-30T15:29:28.588Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/35/b345bdfa40c9126df1a9d33236aa98418367931b8725f84fc3ae2b98dc59/cryptography-50.0.2-cp39-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:58ddb5a8e3179d12f19e4ea34d2d32e9d63a4baa142c875c1eb59f41b7243acd", size = 5382217, upload-time = "2026-09-30T15:29:30.589Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/87/ef344a9e616871f2519c22d6afcda79ddd5d35e9592d95eb6e677608d055/cryptography-50.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f21e8a22c8605750c7af886bab299a363721264061b4ac0a30efb73cfd58efc5", size = 4781387, upload-time = "2026-09-30T15:29:32.605Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/5b/f2fdb13cd0b96f6f932c8627bb292a45f11c64d21620a8e120aee9a3b848/cryptography-50.0.2-cp39-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:9c8402a82ea0dc4ceeab793db05f0fafa8ca139ca34fcde5df0f596103c74107", size = 4403790, upload-time = "2026-09-30T15:29:34.374Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bc/ce/7e4f662b1e3c393513569e402cfc85ac7da0bd3d5435e122a3140219eb2d/cryptography-50.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:0ddc924c04591c2811ca024d62ecad4f7f6f08af8939c211438f48a16bd23602", size = 4764319, upload-time = "2026-09-30T15:29:36.149Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3c/3f/86ff33ce34cc0de6847fb96e035a1a760d81652e38643f617c02ad32ef7a/cryptography-50.0.2-cp39-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a6557e5f38e065ca9fbdaf7cfc7435ecb1d113aa81a022d1b51921ee7432e227", size = 5338560, upload-time = "2026-09-30T15:29:39.053Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/40/cf/6b5c8e2fd9202d98988ab7cb5cc5c991704c4ad55f492ff408e4969f83f1/cryptography-50.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:1981f1db4630889b9ef7803fadef12b056f428cb6b85c27ba57b774793b6093c", size = 4780973, upload-time = "2026-09-30T15:29:41.251Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/bf/8d6ebc7dded797bd0f0160d52188021211f011a2b164ef0ae1dac4587465/cryptography-50.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a8701d6b584d76e909e3d305b7d126b41439876a5aaf76cddc67fc230eafa2e", size = 4897738, upload-time = "2026-09-30T15:29:43.106Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/aa/f3f6e0de7e6253b8baa8b2d8fb9d50924fa75cee3d4624bd4bc1208ee923/cryptography-50.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ce47f66801c20ec6c6632453bb5960fe38939e9306970b48b3a5a26de7745d94", size = 5058280, upload-time = "2026-09-30T15:29:44.827Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f6/b6/a1faf3a27ae9405fb34b1713cc73b2d8a26b04d5c561578fa2e6ef3e5bb9/cryptography-50.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:4e81d95e5bafc2d6e34e4bed780e53e4d5b9a2f928573428aa4d35fbec1eb0de", size = 3854095, upload-time = "2026-09-30T15:29:46.782Z" },
]
[[package]]
name = "greenlet"
version = "3.5.5"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" },
]
[[package]]
name = "idna"
version = "3.19"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/f7/abb373e5757eaec4b922b92f97ec8d6d7e057cf06778247604fbc4e7c3f3/idna-3.19.tar.gz", hash = "sha256:5e0811a4383b21dc5838069f801c4fb62113b7447663d2530d2bd6e77b49bf15", size = 215237, upload-time = "2026-08-18T05:14:24.27Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
]
[[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 = "jieda-procure"
version = "0.1.0"
source = { virtual = "." }
dependencies = [
{ name = "oracledb" },
{ name = "playwright" },
{ name = "pycryptodome" },
{ name = "requests" },
{ name = "urllib3" },
]
[package.dev-dependencies]
dev = [
{ name = "pytest" },
]
[package.metadata]
requires-dist = [
{ name = "oracledb", specifier = ">=2.4.1" },
{ name = "playwright", specifier = ">=1.49.0" },
{ name = "pycryptodome", specifier = ">=3.23.0" },
{ name = "requests", specifier = ">=2.34.2" },
{ name = "urllib3", specifier = ">=2.0.0" },
]
[package.metadata.requires-dev]
dev = [{ name = "pytest", specifier = ">=8.0.0" }]
[[package]]
name = "oracledb"
version = "26.0.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "typing-extensions" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8c/f3/22113415f48b6608ada31ebb46047b3b8ac361dba393d1b5ed1f768d8f6a/oracledb-26.0.1.tar.gz", hash = "sha256:786397a6b37e94ebfa6c1c6cd026755eb8fccfd39e15bcb64cb5879491882aef", size = 910463, upload-time = "2026-09-22T21:23:20.378Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/83/fa/349ee179b3e106f210ac070d199dd341f37e2af9f5faf2563b80d8aa6ab1/oracledb-26.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eea4d0824fa82b03c0d90aad81f10dbbacdaf351979c0130190a362a539e63ae", size = 4908265, upload-time = "2026-09-22T21:23:49.524Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/60/68/2dc6a99d385835290825c0d6689097af29637db8754197040b95814bb5cd/oracledb-26.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c05707b2ba57d8210bdf062ba3419900591f3369d1532fb116af51445c2993d", size = 2448388, upload-time = "2026-09-22T21:23:50.993Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/4b/ce48ed072c2328b429a6bc87a35ddd735945151145d9fe49c1e177b0da83/oracledb-26.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:36cbf2be185412fee95be96aa6f187a73fd5181bb03186b53d5b70ac18fa354b", size = 2663296, upload-time = "2026-09-22T21:23:52.426Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f2/c8/73166ff29389c5ce3272fcc534360ab65248f90fcdf175a99b5fa2bb8466/oracledb-26.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5ad1b6f4db3d8a43182e7734922342fac5901456b34acead92f6fc60cc59882", size = 2493289, upload-time = "2026-09-22T21:23:53.949Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/c0/f6b4f82d6dc7f7d7654c0e4889caa1abbe019ec76daa1b4b4ee80859172b/oracledb-26.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aecc4cdd03cb5a5ad51f43680d16d74132ae7df65c991e7bd69bbe2ccc0fd209", size = 2694730, upload-time = "2026-09-22T21:23:55.609Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9b/da/a3ad79e18c5b27b357de005ce9d79dff1ef1646b7c55b6e6557b238f03bd/oracledb-26.0.1-cp312-cp312-win32.whl", hash = "sha256:f55d6fd9ebc3e8bf3bf4828fcd6ae152b363859628d27f92a4764111adfb1001", size = 1578393, upload-time = "2026-09-22T21:23:56.975Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/06/c6/8e2ad66bda2cda6d085f3516ad8f7834b1d7d4c9da21f96834467525b551/oracledb-26.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c6f2f0d7290b15f1a166c0d24187e7746a6f08ea6e886e521110f1bf8d2002c", size = 1970972, upload-time = "2026-09-22T21:23:58.352Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ba/ce/98c3a83142a562e40135abb8503b59eba7af1135c11c8077986a289f3a35/oracledb-26.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:c31b6f7440047c867a1b41cbaace7867db0750ecab4dc6454c9f501991152deb", size = 1622016, upload-time = "2026-09-22T21:23:59.996Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f4/d7/2a911476339d9fa6eb83e6a4639b007c13af26d7261907ecc2d8722a6712/oracledb-26.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:339dd6c4f0d50dab4026a36ae89d902240b2a879a17cdf18e3feadc113957b4e", size = 4911060, upload-time = "2026-09-22T21:24:01.757Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/e8/97e3b2c283ab996efe1d12b6afa86042c746b0ce4e408942bb4b7c35e39a/oracledb-26.0.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5907bb5c9df123e417ddf358f540554dfc6766064c2feb84121c099bc4e039a3", size = 2445641, upload-time = "2026-09-22T21:24:03.587Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/f7/bbf75b91248ea489e8393ae84e9985b37c895781c658b6d9e5d83d02c1c1/oracledb-26.0.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:16d08328bf9b02e980ad4d5f3e0e99596f9a42aaf6bd1e244ea85be753f2c292", size = 2646590, upload-time = "2026-09-22T21:24:05.22Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3b/32/cbdacb302a2ca047e36e0f804c493391844aad83822d428184fa1d4203d5/oracledb-26.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:50e42111d5c620f60478e4571a5847c335286e8650e63a8789f8bd51035f90da", size = 2499129, upload-time = "2026-09-22T21:24:06.801Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/1e/ce86b581a4f1c8d5844019f84eacc9f4084c882ba676a90c94d7eaeb749e/oracledb-26.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0453f8f1a6748c982dcdac8af09bcd81971979c7ec9d1d151e15595f58d9164b", size = 2675979, upload-time = "2026-09-22T21:24:08.153Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/82/7c/49e6af631b84db68d801b6d03ff58c4fa7b767ffa8a8402de643584e9d63/oracledb-26.0.1-cp313-cp313-win32.whl", hash = "sha256:93e6a7cda6ad7b14e2ce8689c9694872b3d69d57015aab070028c9a93faf3bf1", size = 1576622, upload-time = "2026-09-22T21:24:09.492Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/10/ba7a5ce82fda9bbfcf222930b9a6f9a967c2b3d49f787ba7f4f99b598c6f/oracledb-26.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:6cf0c4d8662072917e2f98598cb97a18e157785bd0f294fbcf770d822ca239a9", size = 1970210, upload-time = "2026-09-22T21:24:10.806Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/24/dc/96ce57dad3b1bb3d3c30be936d0f6daea0a02649d8936b878921004b15e1/oracledb-26.0.1-cp313-cp313-win_arm64.whl", hash = "sha256:22bb1e47a01a5259eead9202be8f631682b69be08ee29d8e532ed9b9364bd7bd", size = 1621513, upload-time = "2026-09-22T21:24:12.246Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/3e/e6/7cf147ea1daf0c7c86fcd77acce1a09b0d4ab75539a4677a2034f112c860/oracledb-26.0.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c2201184b46bbec476a7ac29a72f14ee4ed75d692ade3b6c5d672fdbca71e6b2", size = 4961776, upload-time = "2026-09-22T21:24:13.967Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/7e/1ada40fce6ec550718e6eb1b8be9bfcc6e9697f19b6244ed6e0b89aa8da8/oracledb-26.0.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8bddb6bd7156df750045b419ffc400cfe429276f1a17d83913f10adefd0baa9a", size = 2486660, upload-time = "2026-09-22T21:24:16.397Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/65/2a/a4cf6f8081b1ab3498c82e3917b2174843bf22e2fa4c41e98ad1e0b66d47/oracledb-26.0.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b33b22800a9b02ee0ead00e6f3073b810f7dc51c1e82fc9fc5475c09812420df", size = 2664849, upload-time = "2026-09-22T21:24:18.16Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b2/f2/50a32fa2d8c6cbb0e5fb69c262b01e7412ad08e430192404af9d2b9f4bea/oracledb-26.0.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:124f0072e6c7df277c561ec3a4c42cb2c02c30e9ce15e75a460b4abcfee41046", size = 2544330, upload-time = "2026-09-22T21:24:19.627Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/b0/90/9b6dd9bbc1899ca3a982e9012f25be2a70e4795eacc6f8582c340c64fe1c/oracledb-26.0.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4c5094d5ba60b0f67ce7c74eb0fbc738d827103c0ba2db5f8483db405ab4e9c4", size = 2693190, upload-time = "2026-09-22T21:24:21.228Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/42/0df9265f2f562283c88590a6bcd442084cef80c832ff598bdbab994a3cb8/oracledb-26.0.1-cp314-cp314-win32.whl", hash = "sha256:03424ccfbd25c402133c2a23c5c1a630e57effda7b962085d91c41d765af1b25", size = 1602133, upload-time = "2026-09-22T21:24:22.733Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/d4/1b90c5e252a53dbc71833217746f2673e9e2ce0e06115bac1dc42ae347d0/oracledb-26.0.1-cp314-cp314-win_amd64.whl", hash = "sha256:ee8f4736b72f38ade26f9770f7b40e8662bfa825ef48cefcc55cef0c30525d4b", size = 2027835, upload-time = "2026-09-22T21:24:24.105Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d6/4e/9febea61cbf476c2ae5d290fb3f823f1e46668e3ac15b8af5307531e33ad/oracledb-26.0.1-cp314-cp314-win_arm64.whl", hash = "sha256:13b0dfbbc503c6107d6c08b1658df260c2c077bc5574d9545159df0a0301fe7d", size = 1680743, upload-time = "2026-09-22T21:24:25.475Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/77/bb/71f7861bfbf586757873259a858f370030a137b4172865992c3cc9a462b1/oracledb-26.0.1-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:9e77a6b3b01a3a2f4b786939f59d862fbe73fbc4c68cdf87aa45a5398725a16c", size = 4960764, upload-time = "2026-09-22T21:24:27.244Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/c0/86aa68f08e5c1d25b864ac5777c559bf0bc8911e5d8262b70bec5e05cc21/oracledb-26.0.1-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d722f185f9c05ecccb2702a58a1e5ecfd309ae92cad0c38373e0969e83bf2bd", size = 2488466, upload-time = "2026-09-22T21:24:29.177Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/fc/f4/af94230a3c2c6679323f3578a0a5281cfd87f72994f66e6044b5ff6c95e9/oracledb-26.0.1-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfcf6382bc48e21e448ca2b71644cc75626056a638bca50181acdc8adc69dd3c", size = 2685915, upload-time = "2026-09-22T21:24:30.823Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f5/a7/aa99f8e60961e80795ca926ead66c66d98c3aee78d970320eacdfa843614/oracledb-26.0.1-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:d42a2e0caea9bcf7d260d430b225d72cff6dffa0d6729be7a8823db2678e0b74", size = 2544920, upload-time = "2026-09-22T21:24:32.667Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e2/65/b636a18a578cffd5819df08ae80deeb931f1215d2d2b18112a37e9d2bbea/oracledb-26.0.1-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2a073cc8bd74a7ce32d12f4bc898dd143971981b52ecd2f72c37b4474bff76cb", size = 2713492, upload-time = "2026-09-22T21:24:34.22Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0a/4a/e758838b14540a94599487d0d15ce4390edba5a257e53f2cea66f94bd138/oracledb-26.0.1-cp315-cp315-win32.whl", hash = "sha256:f4188bf8eedfc05ef266835e98ce304fda92d6bc87dd776d5f9a8b510d8b54ba", size = 1601872, upload-time = "2026-09-22T21:24:35.591Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/b1/51b2a51abd89ba43dc0f9ee14d456a9b002d0b9c31840c7de55387e555eb/oracledb-26.0.1-cp315-cp315-win_amd64.whl", hash = "sha256:9d508d2df7bb9a4802247ec95d06064e2babf8ae9e1f91d89c349bcd30df44ed", size = 2026829, upload-time = "2026-09-22T21:24:37.15Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/6b/b099ff2c51447cbf0d2f1c33ea7de1abf96d9bf4c3d93b2a5dff48728965/oracledb-26.0.1-cp315-cp315-win_arm64.whl", hash = "sha256:7c9541d7cf2d324e9516c955c033235bbf25b7d96ce41a856b3bff2eab3494ed", size = 1680535, upload-time = "2026-09-22T21:24:38.551Z" },
]
[[package]]
name = "packaging"
version = "26.3"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
]
[[package]]
name = "playwright"
version = "1.62.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "greenlet" },
{ name = "pyee" },
]
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/5b/ca2abcf3aa69f9fb510215e3064f30b57fe57657c8d04ede45bb966d5606/playwright-1.62.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d8da938f3748841a8754f2e1f0216902c1c8f8ae3720de8b32ccf8e6913a7c4f", size = 43732091, upload-time = "2026-07-31T17:00:44.178Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/af/1a/0bfbe9904350961f4dbb713f04342e40d548c5fc26c8157bd13617c81492/playwright-1.62.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:db755ab27db21a04186f1fe8169888e42356086e439b1059b923ef417f0b6034", size = 42510842, upload-time = "2026-07-31T17:00:48.596Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/66/dc/c0486b407ad0699a250f6bbe3066fca95344009a99ca66e88ca175c69dc1/playwright-1.62.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:5108bd5b3e87169ddf269feee097da5893af7f8aea4634dfc840518d64c1f1da", size = 43732093, upload-time = "2026-07-31T17:00:52.218Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/43/6b/b24aebc2b04bffcb342bccf96e287c78b363e1615bed5cea97500cc0393a/playwright-1.62.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ba33bae6a13b3d9d354c751cb618af357d20fe1d57767cbcce52079bbef17ad3", size = 47748926, upload-time = "2026-07-31T17:00:56.438Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/36/43/b4b18bdc87e1949568fffdcde3ff9a0456266b2d0c6d4432cc34d89ea6eb/playwright-1.62.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2d76613a57ad844362ce42f7d0c2fa26b19a4f7a46d4f76b891c631e6e5aff", size = 47441423, upload-time = "2026-07-31T17:01:00.404Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/81/22/af5d926fc2c32a339eec00a443644bc40ab9db1dd2dd9017873c59773c0c/playwright-1.62.0-py3-none-win32.whl", hash = "sha256:e5614fa89355d7081457680324bb219f79f69c423c5cb6fa250e30b0d8aebf1c", size = 38164450, upload-time = "2026-07-31T17:01:04.187Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/a9/4160c1033c07af98bf841ad079457dd78408a5ee0dd56cbfe50b8b6a1c22/playwright-1.62.0-py3-none-win_amd64.whl", hash = "sha256:92c0d98ed04eb35af557b709875edba415b1f548bdb22ddb5bb3e1e6c835c2f1", size = 38164458, upload-time = "2026-07-31T17:01:08.459Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6c/ec/06b55d619a7082a766aa04f2c6bb31435c87f02930087d8a0517119408fa/playwright-1.62.0-py3-none-win_arm64.whl", hash = "sha256:ea8d3055aa9d5a9f1832ac82517bd8b42c78fac7ebcbebb0107116735c8cb6a1", size = 34208868, upload-time = "2026-07-31T17:01:11.818Z" },
]
[[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 = "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 = "pycryptodome"
version = "3.23.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8e/a6/8452177684d5e906854776276ddd34eca30d1b1e15aa1ee9cefc289a33f5/pycryptodome-3.23.0.tar.gz", hash = "sha256:447700a657182d60338bab09fdb27518f8856aecd80ae4c6bdddb67ff5da44ef", size = 4921276, upload-time = "2025-05-17T17:21:45.242Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/5d/bdb09489b63cd34a976cc9e2a8d938114f7a53a74d3dd4f125ffa49dce82/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:0011f7f00cdb74879142011f95133274741778abba114ceca229adbf8e62c3e4", size = 2495152, upload-time = "2025-05-17T17:20:20.833Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a7/ce/7840250ed4cc0039c433cd41715536f926d6e86ce84e904068eb3244b6a6/pycryptodome-3.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:90460fc9e088ce095f9ee8356722d4f10f86e5be06e2354230a9880b9c549aae", size = 1639348, upload-time = "2025-05-17T17:20:23.171Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ee/f0/991da24c55c1f688d6a3b5a11940567353f74590734ee4a64294834ae472/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4764e64b269fc83b00f682c47443c2e6e85b18273712b98aa43bcb77f8570477", size = 2184033, upload-time = "2025-05-17T17:20:25.424Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/16/0e11882deddf00f68b68dd4e8e442ddc30641f31afeb2bc25588124ac8de/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb8f24adb74984aa0e5d07a2368ad95276cf38051fe2dc6605cbcf482e04f2a7", size = 2270142, upload-time = "2025-05-17T17:20:27.808Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d5/fc/4347fea23a3f95ffb931f383ff28b3f7b1fe868739182cb76718c0da86a1/pycryptodome-3.23.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d97618c9c6684a97ef7637ba43bdf6663a2e2e77efe0f863cce97a76af396446", size = 2309384, upload-time = "2025-05-17T17:20:30.765Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/d9/c5261780b69ce66d8cfab25d2797bd6e82ba0241804694cd48be41add5eb/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a53a4fe5cb075075d515797d6ce2f56772ea7e6a1e5e4b96cf78a14bac3d265", size = 2183237, upload-time = "2025-05-17T17:20:33.736Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5a/6f/3af2ffedd5cfa08c631f89452c6648c4d779e7772dfc388c77c920ca6bbf/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:763d1d74f56f031788e5d307029caef067febf890cd1f8bf61183ae142f1a77b", size = 2343898, upload-time = "2025-05-17T17:20:36.086Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9a/dc/9060d807039ee5de6e2f260f72f3d70ac213993a804f5e67e0a73a56dd2f/pycryptodome-3.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:954af0e2bd7cea83ce72243b14e4fb518b18f0c1649b576d114973e2073b273d", size = 2269197, upload-time = "2025-05-17T17:20:38.414Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/34/e6c8ca177cb29dcc4967fef73f5de445912f93bd0343c9c33c8e5bf8cde8/pycryptodome-3.23.0-cp313-cp313t-win32.whl", hash = "sha256:257bb3572c63ad8ba40b89f6fc9d63a2a628e9f9708d31ee26560925ebe0210a", size = 1768600, upload-time = "2025-05-17T17:20:40.688Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e4/1d/89756b8d7ff623ad0160f4539da571d1f594d21ee6d68be130a6eccb39a4/pycryptodome-3.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:6501790c5b62a29fcb227bd6b62012181d886a767ce9ed03b303d1f22eb5c625", size = 1799740, upload-time = "2025-05-17T17:20:42.413Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5d/61/35a64f0feaea9fd07f0d91209e7be91726eb48c0f1bfc6720647194071e4/pycryptodome-3.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:9a77627a330ab23ca43b48b130e202582e91cc69619947840ea4d2d1be21eb39", size = 1703685, upload-time = "2025-05-17T17:20:44.388Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/db/6c/a1f71542c969912bb0e106f64f60a56cc1f0fabecf9396f45accbe63fa68/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:187058ab80b3281b1de11c2e6842a357a1f71b42cb1e15bce373f3d238135c27", size = 2495627, upload-time = "2025-05-17T17:20:47.139Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/6e/4e/a066527e079fc5002390c8acdd3aca431e6ea0a50ffd7201551175b47323/pycryptodome-3.23.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:cfb5cd445280c5b0a4e6187a7ce8de5a07b5f3f897f235caa11f1f435f182843", size = 1640362, upload-time = "2025-05-17T17:20:50.392Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/50/52/adaf4c8c100a8c49d2bd058e5b551f73dfd8cb89eb4911e25a0c469b6b4e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67bd81fcbe34f43ad9422ee8fd4843c8e7198dd88dd3d40e6de42ee65fbe1490", size = 2182625, upload-time = "2025-05-17T17:20:52.866Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/e9/a09476d436d0ff1402ac3867d933c61805ec2326c6ea557aeeac3825604e/pycryptodome-3.23.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c8987bd3307a39bc03df5c8e0e3d8be0c4c3518b7f044b0f4c15d1aa78f52575", size = 2268954, upload-time = "2025-05-17T17:20:55.027Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f9/c5/ffe6474e0c551d54cab931918127c46d70cab8f114e0c2b5a3c071c2f484/pycryptodome-3.23.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aa0698f65e5b570426fc31b8162ed4603b0c2841cbb9088e2b01641e3065915b", size = 2308534, upload-time = "2025-05-17T17:20:57.279Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/28/e199677fc15ecf43010f2463fde4c1a53015d1fe95fb03bca2890836603a/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:53ecbafc2b55353edcebd64bf5da94a2a2cdf5090a6915bcca6eca6cc452585a", size = 2181853, upload-time = "2025-05-17T17:20:59.322Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/ce/ea/4fdb09f2165ce1365c9eaefef36625583371ee514db58dc9b65d3a255c4c/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:156df9667ad9f2ad26255926524e1c136d6664b741547deb0a86a9acf5ea631f", size = 2342465, upload-time = "2025-05-17T17:21:03.83Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/22/82/6edc3fc42fe9284aead511394bac167693fb2b0e0395b28b8bedaa07ef04/pycryptodome-3.23.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:dea827b4d55ee390dc89b2afe5927d4308a8b538ae91d9c6f7a5090f397af1aa", size = 2267414, upload-time = "2025-05-17T17:21:06.72Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/59/fe/aae679b64363eb78326c7fdc9d06ec3de18bac68be4b612fc1fe8902693c/pycryptodome-3.23.0-cp37-abi3-win32.whl", hash = "sha256:507dbead45474b62b2bbe318eb1c4c8ee641077532067fec9c1aa82c31f84886", size = 1768484, upload-time = "2025-05-17T17:21:08.535Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/54/2f/e97a1b8294db0daaa87012c24a7bb714147c7ade7656973fd6c736b484ff/pycryptodome-3.23.0-cp37-abi3-win_amd64.whl", hash = "sha256:c75b52aacc6c0c260f204cbdd834f76edc9fb0d8e0da9fbf8352ef58202564e2", size = 1799636, upload-time = "2025-05-17T17:21:10.393Z" },
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/18/3d/f9441a0d798bf2b1e645adc3265e55706aead1255ccdad3856dbdcffec14/pycryptodome-3.23.0-cp37-abi3-win_arm64.whl", hash = "sha256:11eeeb6917903876f134b56ba11abe95c0b0fd5e3330def218083c7d98bbcb3c", size = 1703675, upload-time = "2025-05-17T17:21:13.146Z" },
]
[[package]]
name = "pyee"
version = "13.0.1"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" },
]
[[package]]
name = "pygments"
version = "2.21.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
]
[[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 = "requests"
version = "2.34.2"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
]
[[package]]
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 = "urllib3"
version = "2.7.0"
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
wheels = [
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
]