Compare commits
No commits in common. "ef9c6f09c6627b55576dad6e5176113cd64e70b0" and "5a3951362a7c53a8ed7292adbe2f2a7c90d66134" have entirely different histories.
ef9c6f09c6
...
5a3951362a
|
|
@ -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` 只有一行」,同一平台拆两仓会撞上。表结构允许多行;联调时若对方拒,再改成同平台合一行(本期不合)。
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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. [自营] 陕西壹号药业有限公司
|
|
||||||
|
|
||||||
|
|
@ -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=
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
3.12
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": ["ms-python.python", "ms-python.debugpy"]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
|
|
@ -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 推送、支付回传、批准文号搜索、商家名模糊匹配。
|
|
||||||
|
|
@ -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()
|
|
||||||
46
选品流程/db.py
46
选品流程/db.py
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
118
选品流程/job.py
118
选品流程/job.py
|
|
@ -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
|
|
||||||
191
选品流程/main.py
191
选品流程/main.py
|
|
@ -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()
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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. 最终结论
|
|
||||||
基于当前实现与回归结果,该双商城查价流程在技术上可行、稳定性可接受、维护成本可控,满足当前业务使用条件。
|
|
||||||
186
选品流程/persist.py
186
选品流程/persist.py
|
|
@ -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] + "…"
|
|
||||||
121
选品流程/pipeline.py
121
选品流程/pipeline.py
|
|
@ -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"
|
|
||||||
|
|
@ -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
|
|
||||||
219
选品流程/repo.py
219
选品流程/repo.py
|
|
@ -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)
|
|
||||||
317
选品流程/rules.py
317
选品流程/rules.py
|
|
@ -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))
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
{
|
|
||||||
"yaoex": {
|
|
||||||
"username": "",
|
|
||||||
"password": ""
|
|
||||||
},
|
|
||||||
"ybm": {
|
|
||||||
"username": "",
|
|
||||||
"password": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 == []
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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}"
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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"] == "无符合规则供应商"
|
|
||||||
|
|
@ -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 == []
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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"
|
|
||||||
33
选品流程/util.py
33
选品流程/util.py
|
|
@ -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
|
|
||||||
604
选品流程/uv.lock
604
选品流程/uv.lock
|
|
@ -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" },
|
|
||||||
]
|
|
||||||
Loading…
Reference in New Issue