初始化项目 v2.0:中国邮政揽收系统自动填单工具
基于 HTTP API + Node.js token bridge 方案,不依赖浏览器自动化。
This commit is contained in:
+27
@@ -0,0 +1,27 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[oc]
|
||||
build/
|
||||
dist/
|
||||
wheels/
|
||||
*.egg-info
|
||||
*.spec
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
|
||||
# Node.js
|
||||
token_bridge/node_modules/
|
||||
|
||||
# HAR captures (large binary data)
|
||||
docs/*.har
|
||||
|
||||
# Runtime data
|
||||
session.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# Claude Code
|
||||
CLAUDE.md
|
||||
@@ -0,0 +1 @@
|
||||
3.10
|
||||
@@ -0,0 +1,155 @@
|
||||
# 中国邮政揽收系统 - 自动填单工具
|
||||
|
||||
通过 HTTP API + Node.js token bridge 实现,**不依赖浏览器自动化**。读取 Excel 运单数据,逐条自动填写产品、客户、运单信息并提交。
|
||||
|
||||
## 运行环境
|
||||
|
||||
- Windows 10/11 64 位
|
||||
- **Node.js 18+**(用于 token bridge,计算 $_ts 安全框架的 c93l5Fya 动态 token)
|
||||
|
||||
## 快速开始
|
||||
|
||||
```bash
|
||||
# 安装 Python 依赖
|
||||
uv sync
|
||||
|
||||
# 安装 token bridge 依赖
|
||||
cd token_bridge && npm install && cd ..
|
||||
|
||||
# 运行
|
||||
uv run python main.py
|
||||
```
|
||||
|
||||
## 打包
|
||||
|
||||
```bash
|
||||
build.bat # Windows
|
||||
bash build.sh # Linux
|
||||
```
|
||||
|
||||
输出 `dist/PickupAuto.exe`,用户端需安装 Node.js 18+。
|
||||
|
||||
## 架构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Python (requests.Session) │
|
||||
│ ├─ engine.py 引擎/表单组装/提交 │
|
||||
│ ├─ pickup_api.py HTTP API 封装(18个) │
|
||||
│ ├─ login_helper.py cookies 导入 │
|
||||
│ ├─ field_mapping.py 中文字段映射 │
|
||||
│ ├─ excel_reader.py Excel 读写 │
|
||||
│ ├─ main.py Tkinter GUI │
|
||||
│ └─ log.py 统一日志 │
|
||||
├─────────────────────────────────────────┤
|
||||
│ Node.js (HTTP server, 同进程启动) │
|
||||
│ └─ token_bridge/server.js │
|
||||
│ jsdom 加载 $_ts 安全 JS │
|
||||
│ 计算 c93l5Fya token │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 数据流
|
||||
|
||||
```
|
||||
Excel 行数据
|
||||
→ 产品搜索 (autocomplete API) → 用户选择
|
||||
→ 客户搜索 (autocomplete API) → 用户选择
|
||||
→ 运单查询 → 获取收寄件人/地址/电话
|
||||
→ 地址匹配 → 获取区划代码
|
||||
→ 组装表单 (5层优先级) → 确认弹窗 → 提交
|
||||
```
|
||||
|
||||
### 表单字段优先级
|
||||
|
||||
```
|
||||
强制固定值 (force=true)
|
||||
> API 获取值 (运单/产品/客户/地址匹配)
|
||||
> 固定值 (force=false)
|
||||
> Excel 表格值
|
||||
> 默认值 (config.json defaults)
|
||||
```
|
||||
|
||||
### token bridge 原理
|
||||
|
||||
揽收系统使用 `$_ts` 安全框架(954KB 混淆 JS),所有 AJAX 请求需附带 `c93l5Fya=TOKEN` 参数。token bridge 在 jsdom 中运行该 JS,通过 HTTP API 对外提供 token 计算服务,Python 拿到 token 后用自己的 `requests.Session` 发请求。
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 1. 登录(获取 cookies)
|
||||
|
||||
点击 `🔑 登录` → 弹出对话框 → 用浏览器 cookies 导出工具导出 `10.4.188.1` 的所有 cookies 为 JSON 数组 → 粘贴到对话框 → 保存。
|
||||
|
||||
cookies 保存到 `session.json`(与 exe 同目录)。
|
||||
|
||||
### 2. 选择 Excel
|
||||
|
||||
点击 `选择文件`,选择要处理的 Excel 文件。
|
||||
|
||||
### 3. 配置固定值(可选)
|
||||
|
||||
在 GUI 固定值表格中设置常用字段,勾选 "强制覆盖" 可覆盖 API 获取的值。
|
||||
|
||||
### 4. 开始处理
|
||||
|
||||
点击 `▶ 开始处理` → 弹出产品/客户选择框 → 确认提交信息 → 自动批量处理后续行。
|
||||
|
||||
## Excel 格式
|
||||
|
||||
| 字段 | 说明 | 来源 |
|
||||
|------|------|------|
|
||||
| 单号 | 运单号 | Excel 必填 |
|
||||
| 重量 | 单位克 | Excel |
|
||||
| 产品名称 | 用于 autocomplete 搜索 | Excel/固定值 |
|
||||
| 客户名称 | 用于 autocomplete 搜索 | Excel/固定值 |
|
||||
| 收件人/手机/地址 | 运单 API 自动填充,Excel 可覆盖 | API/Excel |
|
||||
| 寄件人/手机/地址 | 客户资料 API 自动填充 | API/固定值 |
|
||||
| 寄达地/寄达地区划 | 地址匹配 API 自动获取,固定值可强制覆盖 | API/固定值 |
|
||||
| 电商平台 | 默认 "无"(WU) | 默认值 |
|
||||
| 付款方式 | 默认 "寄付"(1) | 默认值 |
|
||||
| 内件属性 | 产品 API 返回 `innerpartsType` | API |
|
||||
| 运输类型 | 默认 "陆运"(1) | 默认值 |
|
||||
|
||||
## 配置说明 (config.json)
|
||||
|
||||
### system
|
||||
- `url`: 揽收系统地址
|
||||
|
||||
### timing
|
||||
- `delay_between_rows_ms`: 行间延时(毫秒)
|
||||
|
||||
### fixed_values
|
||||
```
|
||||
"字段名": {"value": "值", "force": false}
|
||||
```
|
||||
`force: true` 时覆盖 API 获取值。
|
||||
|
||||
### defaults
|
||||
字段的默认值(中文名,自动翻译为系统编码)。
|
||||
|
||||
### field_mapping
|
||||
Excel 列名 → 表单参数名的自定义映射。内置映射覆盖常见字段,仅需在此配置自定义列名。
|
||||
|
||||
## 文档
|
||||
|
||||
项目级文档见 `../docs/`:
|
||||
|
||||
| 文件 | 说明 |
|
||||
|------|------|
|
||||
| `分析报告.md` | 系统 API 分析 |
|
||||
| `字段覆盖检查报告.md` | 字段来源和覆盖关系 |
|
||||
| `har-successful-submit-fields.json` | 成功提交的完整表单字段模板 |
|
||||
| `portal-to-pickup-full-flow.har` | 门户→揽收页面完整网络流 |
|
||||
| `product-customer-selection-flow.har` | 产品/客户选择 API 流 |
|
||||
| `successful-submit-with-address-change.har` | 改寄达地后成功提交 |
|
||||
|
||||
## 开发
|
||||
|
||||
### 日志等级
|
||||
|
||||
`log.py` 中修改 `LEVEL`:
|
||||
- `0` = 仅用户级消息
|
||||
- `1` = 调试级
|
||||
- `2` = 追踪级(API 请求/响应)
|
||||
|
||||
修改后重新打包生效。
|
||||
@@ -0,0 +1,94 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
chcp 65001 >nul 2>&1
|
||||
|
||||
echo ============================================
|
||||
echo PickupAuto Build Script (HTTP mode)
|
||||
echo ============================================
|
||||
echo.
|
||||
|
||||
:: Check Python
|
||||
python --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] Python not found. Please install Python 3.10+
|
||||
echo https://www.python.org/downloads/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Check Node.js (for token bridge)
|
||||
node --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] Node.js not found. Please install Node.js 18+
|
||||
echo https://nodejs.org/
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Check uv, install if missing
|
||||
uv --version >nul 2>&1
|
||||
if %errorlevel% neq 0 (
|
||||
echo [INFO] Installing uv...
|
||||
pip install uv
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] Failed to install uv
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo [1/4] Installing Python dependencies...
|
||||
uv sync
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] Python dependency install failed
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo [2/4] Installing token bridge dependencies...
|
||||
cd token_bridge
|
||||
call npm install
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] npm install failed in token_bridge
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
cd ..
|
||||
|
||||
echo [3/4] Cleaning old build...
|
||||
if exist dist rmdir /s /q dist
|
||||
if exist build rmdir /s /q build
|
||||
|
||||
echo [4/4] Building exe with PyInstaller...
|
||||
uv run pyinstaller ^
|
||||
--onefile ^
|
||||
--windowed ^
|
||||
--name "PickupAuto" ^
|
||||
--icon="pickup.ico" ^
|
||||
--add-data "config.json;." ^
|
||||
--add-data "pickup.ico;." ^
|
||||
--add-data "token_bridge;token_bridge" ^
|
||||
--hidden-import=openpyxl ^
|
||||
--hidden-import=requests ^
|
||||
main.py
|
||||
|
||||
if %errorlevel% neq 0 (
|
||||
echo [ERROR] PyInstaller build failed
|
||||
pause
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
:: Copy config to dist for reference
|
||||
copy config.json dist\config.json >nul 2>&1
|
||||
|
||||
echo.
|
||||
echo ============================================
|
||||
echo Build complete!
|
||||
echo Output: dist\PickupAuto.exe
|
||||
echo.
|
||||
echo Requirements for end users:
|
||||
echo - Windows 10/11 64-bit
|
||||
echo - Node.js 18+ (for security token computation)
|
||||
echo - Access to internal network (10.4.188.1)
|
||||
echo ============================================
|
||||
pause
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
echo "============================================"
|
||||
echo " PickupAuto Build Script (HTTP mode)"
|
||||
echo "============================================"
|
||||
echo
|
||||
|
||||
# Check dependencies
|
||||
command -v python3 >/dev/null 2>&1 || { echo "[ERROR] Python 3.10+ required"; exit 1; }
|
||||
command -v node >/dev/null 2>&1 || { echo "[ERROR] Node.js 18+ required"; exit 1; }
|
||||
command -v npm >/dev/null 2>&1 || { echo "[ERROR] npm required"; exit 1; }
|
||||
|
||||
# Check uv
|
||||
if ! command -v uv &>/dev/null; then
|
||||
echo "[INFO] Installing uv..."
|
||||
pip install uv
|
||||
fi
|
||||
|
||||
echo "[1/4] Installing Python dependencies..."
|
||||
uv sync
|
||||
|
||||
echo "[2/4] Installing token bridge dependencies..."
|
||||
cd token_bridge
|
||||
npm install
|
||||
cd ..
|
||||
|
||||
echo "[3/4] Cleaning old build..."
|
||||
rm -rf dist build
|
||||
|
||||
echo "[4/4] Building with PyInstaller..."
|
||||
uv run pyinstaller \
|
||||
--onefile \
|
||||
--windowed \
|
||||
--name "PickupAuto" \
|
||||
--add-data "config.json:." \
|
||||
--add-data "pickup.ico:." \
|
||||
--add-data "token_bridge:token_bridge" \
|
||||
--hidden-import=openpyxl \
|
||||
--hidden-import=requests \
|
||||
main.py
|
||||
|
||||
cp config.json dist/config.json 2>/dev/null || true
|
||||
|
||||
echo
|
||||
echo "============================================"
|
||||
echo " Build complete!"
|
||||
echo " Output: dist/PickupAuto"
|
||||
echo
|
||||
echo " Requirements for end users:"
|
||||
echo " - Node.js 18+ (for security token computation)"
|
||||
echo " - Access to internal network (10.4.188.1)"
|
||||
echo "============================================"
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"system": {
|
||||
"url": "https://10.4.188.1/pickup-web/a/postwaybill/main"
|
||||
},
|
||||
"timing": {
|
||||
"delay_between_rows_ms": 3000
|
||||
},
|
||||
"field_mapping": {
|
||||
"_comment": "仅需在此配置与内置映射不同的自定义列名,默认使用内置映射"
|
||||
},
|
||||
"fixed_values": {
|
||||
"客户名称": {"value": "", "force": false},
|
||||
"寄件人": {"value": "", "force": false},
|
||||
"寄件地址": {"value": "", "force": false},
|
||||
"寄达地": {"value": "", "force": false},
|
||||
"寄达地区划": {"value": "", "force": false},
|
||||
"产品名称": {"value": "", "force": false},
|
||||
"内件品名": {"value": "", "force": false}
|
||||
},
|
||||
"defaults": {
|
||||
"电商平台": "无",
|
||||
"付款方式": "寄付",
|
||||
"证件类型": "身份证",
|
||||
"保价金额": "",
|
||||
"代收货款": "",
|
||||
"运输类型": "陆运",
|
||||
"内件属性": "其他",
|
||||
"件数": "1",
|
||||
"保价标志": "不保价",
|
||||
"回单标志": "无回单",
|
||||
"代收标志": "不代收",
|
||||
"投递方式": "普通投递"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
"""Excel 读取模块 — 读取输入 Excel 并写入结果 Excel。"""
|
||||
|
||||
import openpyxl
|
||||
|
||||
|
||||
def read_input_excel(file_path: str) -> tuple[list[dict], list[str]]:
|
||||
"""读取输入 Excel 文件,返回 (行数据列表, 列名列表)。
|
||||
|
||||
每行数据为 {列名: 值} 的字典。跳过全空行。
|
||||
"""
|
||||
wb = openpyxl.load_workbook(file_path, read_only=True)
|
||||
ws = wb.active
|
||||
|
||||
rows = list(ws.iter_rows(min_row=1, values_only=True))
|
||||
if not rows:
|
||||
raise ValueError("Excel 文件为空")
|
||||
|
||||
headers = [str(h).strip() if h is not None else "" for h in rows[0]]
|
||||
if not any(headers):
|
||||
raise ValueError("未找到表头行")
|
||||
|
||||
data = []
|
||||
for row in rows[1:]:
|
||||
values = [str(c).strip() if c is not None else "" for c in row]
|
||||
if not any(values): # 跳过全空行
|
||||
continue
|
||||
row_dict = {}
|
||||
for i, header in enumerate(headers):
|
||||
if i < len(values):
|
||||
row_dict[header] = values[i]
|
||||
else:
|
||||
row_dict[header] = ""
|
||||
data.append(row_dict)
|
||||
|
||||
wb.close()
|
||||
return data, headers
|
||||
|
||||
|
||||
def validate_required_columns(
|
||||
headers: list[str],
|
||||
fixed_values: dict[str, str] = None,
|
||||
defaults: dict[str, str] = None,
|
||||
) -> list[str]:
|
||||
"""验证必要字段是否有数据来源(固定值 > Excel > API > 默认值)。
|
||||
|
||||
数据来源优先级:
|
||||
1. 固定值设置(客户名称、寄件人、寄件地址、产品名称、寄达地)
|
||||
2. Excel 表格(运单号、重量、内件品名)
|
||||
3. API 自动获取(querypostwaybillmanager 返回收件人/电话/地址等全部运单信息)
|
||||
4. 默认值(电商平台、付款方式等系统编码)
|
||||
|
||||
只在所有来源都没有值时警告。
|
||||
"""
|
||||
fixed_values = fixed_values or {}
|
||||
defaults = defaults or {}
|
||||
|
||||
# querypostwaybillmanager API 自动返回的字段,Excel 无需填写
|
||||
api_fields = [
|
||||
"收件人", "收件人手机", "收件地址", "收件邮编", "收件人固话",
|
||||
"寄件人", "寄件人手机", "寄件地址", "寄件邮编",
|
||||
]
|
||||
|
||||
# 其余需要人工提供数据的字段
|
||||
check_fields = [
|
||||
"单号", # Excel 必填(API 不返回)
|
||||
"重量", # Excel 必填(API 返回 orderWeight 但可能为 0)
|
||||
"产品名称", # 固定值
|
||||
"客户名称", # 固定值
|
||||
]
|
||||
|
||||
from field_mapping import get_fixed_value_str
|
||||
|
||||
missing = []
|
||||
for field in check_fields:
|
||||
if get_fixed_value_str(fixed_values, field):
|
||||
continue
|
||||
if field in headers:
|
||||
continue
|
||||
if defaults.get(field, ""):
|
||||
continue
|
||||
missing.append(field)
|
||||
|
||||
return missing
|
||||
@@ -0,0 +1,225 @@
|
||||
"""字段映射模块 — 将 Excel 列名映射到页面 DOM 元素 ID。
|
||||
|
||||
所有中文名→DOM ID 的对应关系写死在 BUILTIN_FIELD_MAP 中。
|
||||
config.json 的 field_mapping 用于追加/覆盖自定义映射。
|
||||
"""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
# 内置字段映射:中文名 → 页面 DOM 元素 ID(写死在代码中)
|
||||
BUILTIN_FIELD_MAP: dict[str, str] = {
|
||||
# ── 业务字段 ──
|
||||
"单号": "waybillNo",
|
||||
"运单号": "waybillNo",
|
||||
"产品名称": "bizProductName",
|
||||
"客户名称": "senders",
|
||||
# ── 收件人 ──
|
||||
"收件人": "receiverLinker",
|
||||
"收件人手机": "receiverMobile",
|
||||
"收件地址": "receiverAddr",
|
||||
"收件邮编": "receiverPostcode",
|
||||
"收件人固话": "receiverFixtel",
|
||||
# ── 寄件人 ──
|
||||
"寄件人": "senderLinker",
|
||||
"寄件人手机": "senderMobile",
|
||||
"寄件地址": "senderAddr",
|
||||
"寄件邮编": "senderPostcode",
|
||||
"寄件人固话": "senderFixtel",
|
||||
# ── 货物 ──
|
||||
"重量": "realWeight",
|
||||
"内件品名": "reserved5",
|
||||
# ── 电商 / 支付 ──
|
||||
"电商平台": "ecommerceNo",
|
||||
"付款方式": "paymentMode",
|
||||
"证件类型": "consignorIdType",
|
||||
"保价金额": "insuranceAmount",
|
||||
"代收货款": "codAmount",
|
||||
# ── 系统保留字段(通常不需要在 Excel 中填写,只用默认值)──
|
||||
"运输类型": "transferType",
|
||||
"内件属性": "contentsAttribute",
|
||||
"件数": "quantity",
|
||||
"保价标志": "insuranceFlag",
|
||||
"回单标志": "receiptFlag",
|
||||
"代收标志": "codFlag",
|
||||
"投递方式": "deliverType",
|
||||
# ── 其他 ──
|
||||
"寄达地": "receiverArriveOrgName",
|
||||
"寄达地区划": "receiverDistrictNo",
|
||||
"备注": "senderNotes",
|
||||
}
|
||||
|
||||
|
||||
def load_config(config_path: str = "config.json") -> dict[str, Any]:
|
||||
"""加载配置文件。"""
|
||||
# 支持开发环境和 PyInstaller 打包后的路径
|
||||
paths_to_try = [
|
||||
Path(config_path),
|
||||
Path(__file__).parent / config_path,
|
||||
Path.cwd() / config_path,
|
||||
]
|
||||
# PyInstaller _MEIPASS
|
||||
import sys
|
||||
if getattr(sys, "frozen", False):
|
||||
paths_to_try.insert(0, Path(sys._MEIPASS) / config_path)
|
||||
|
||||
for p in paths_to_try:
|
||||
if p.exists():
|
||||
with open(p, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
raise FileNotFoundError(f"配置文件未找到: {config_path}")
|
||||
|
||||
|
||||
def get_field_mapping(config: dict) -> dict[str, str]:
|
||||
"""获取 Excel 列名 → DOM ID 的映射(内置 + 配置覆盖)。"""
|
||||
mapping = dict(BUILTIN_FIELD_MAP)
|
||||
mapping.update(config.get("field_mapping", {}))
|
||||
return mapping
|
||||
|
||||
|
||||
def get_defaults(config: dict) -> dict[str, str]:
|
||||
"""获取默认值(中文键 → 值)。"""
|
||||
return config.get("defaults", {})
|
||||
|
||||
|
||||
def get_system_config(config: dict) -> dict[str, Any]:
|
||||
"""获取系统配置。"""
|
||||
return config.get("system", {})
|
||||
|
||||
|
||||
def get_timing_config(config: dict) -> dict[str, Any]:
|
||||
"""获取延时配置。"""
|
||||
return config.get("timing", {})
|
||||
|
||||
|
||||
def get_fixed_values(config: dict) -> dict[str, dict]:
|
||||
"""获取固定值设置,返回 {字段名: {value, force}}。"""
|
||||
return config.get("fixed_values", {})
|
||||
|
||||
|
||||
def get_fixed_value_str(fixed_values: dict[str, dict], field: str) -> str:
|
||||
"""从固定值配置中提取字段的字符串值。"""
|
||||
return fixed_values.get(field, {}).get("value", "")
|
||||
|
||||
|
||||
# 中文显示名 → 系统内部编码(用于默认值/固定值的翻译)
|
||||
# 数据来源:data.har 抓包分析
|
||||
CODE_MAP: dict[str, dict[str, str]] = {
|
||||
"电商平台": {
|
||||
"无": "WU",
|
||||
"菜鸟电子面单": "TAOBAOWAYBILL",
|
||||
"淘宝": "TAOBAO",
|
||||
"拼多多": "PINDUODUO",
|
||||
"电子面单": "DZMD",
|
||||
"大客户订单接口": "DKH",
|
||||
"云打印": "CLOUD",
|
||||
"批量导入": "IMPO",
|
||||
"客户端": "EMSRM",
|
||||
"公安部": "GAB",
|
||||
"电子返单": "DZFD",
|
||||
"仓库配送": "CKPS",
|
||||
"无": "WU",
|
||||
},
|
||||
"付款方式": {
|
||||
"寄付": "1",
|
||||
"到付": "2",
|
||||
},
|
||||
"证件类型": {
|
||||
"身份证": "01",
|
||||
},
|
||||
"运输类型": {
|
||||
"陆运": "1",
|
||||
},
|
||||
"内件属性": {
|
||||
"其他": "00",
|
||||
"身份证": "01",
|
||||
"录取通知书": "02",
|
||||
"法院专递": "03",
|
||||
"护照": "04",
|
||||
"通行证": "05",
|
||||
"签证": "06",
|
||||
"驾驶证": "07",
|
||||
"保险文书": "08",
|
||||
"银行票据": "09",
|
||||
"行驶证": "10",
|
||||
"交通违章通知书": "11",
|
||||
"纳税申报": "12",
|
||||
"到货通知书": "13",
|
||||
"公证书": "14",
|
||||
"学生档案": "15",
|
||||
"准考证": "16",
|
||||
"成绩单": "17",
|
||||
"药监批件": "18",
|
||||
"移动配送": "19",
|
||||
"劳动仲裁": "20",
|
||||
"合格证": "21",
|
||||
"健康证": "22",
|
||||
"火车票": "23",
|
||||
"房证": "24",
|
||||
"企业代码证": "25",
|
||||
"教材": "26",
|
||||
"福利彩票": "27",
|
||||
"病历": "28",
|
||||
"银企对账单": "29",
|
||||
"学位证书": "30",
|
||||
"车辆档案": "31",
|
||||
"交房通知书": "32",
|
||||
"车牌": "36",
|
||||
"信用卡": "41",
|
||||
"检察专递": "53",
|
||||
"法院专递回执": "54",
|
||||
},
|
||||
"保价标志": {
|
||||
"不保价": "1",
|
||||
},
|
||||
"回单标志": {
|
||||
"无回单": "1",
|
||||
},
|
||||
"代收标志": {
|
||||
"不代收": "9",
|
||||
},
|
||||
"投递方式": {
|
||||
"普通投递": "3",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def translate_value(excel_col: str, chinese_value: str) -> str:
|
||||
"""将中文显示名翻译为系统内部编码(如 "寄付" → "1")。
|
||||
|
||||
如果该字段没有翻译表,或值已经是编码格式,直接返回原值。
|
||||
"""
|
||||
if not chinese_value:
|
||||
return chinese_value
|
||||
mapping = CODE_MAP.get(excel_col, {})
|
||||
return mapping.get(chinese_value, chinese_value)
|
||||
|
||||
|
||||
def reverse_translate(excel_col: str, code_value: str) -> str:
|
||||
"""将系统编码翻译回中文显示名(如 "1" → "寄付")。"""
|
||||
if not code_value:
|
||||
return code_value
|
||||
mapping = CODE_MAP.get(excel_col, {})
|
||||
for chinese, code in mapping.items():
|
||||
if code == code_value:
|
||||
return chinese
|
||||
return code_value
|
||||
|
||||
|
||||
def resolve_field_value(
|
||||
row: dict[str, str],
|
||||
excel_col: str,
|
||||
defaults: dict[str, str],
|
||||
fixed_values: dict[str, str] = None,
|
||||
) -> str:
|
||||
"""解析字段值,优先级:固定值 > Excel > 默认值。结果自动翻译为系统编码。"""
|
||||
fixed_values = fixed_values or {}
|
||||
fixed = fixed_values.get(excel_col, "")
|
||||
if fixed:
|
||||
return translate_value(excel_col, fixed)
|
||||
value = row.get(excel_col, "").strip()
|
||||
if value:
|
||||
return translate_value(excel_col, value)
|
||||
return translate_value(excel_col, defaults.get(excel_col, ""))
|
||||
@@ -0,0 +1,44 @@
|
||||
"""统一日志控制 — 编译前修改 LEVEL 控制输出量。
|
||||
|
||||
等级:
|
||||
0 = 用户级:只显示处理结果和错误
|
||||
1 = 调试级:显示所有步骤细节
|
||||
2 = 追踪级:显示 API 请求/响应内容
|
||||
"""
|
||||
|
||||
import sys
|
||||
from datetime import datetime
|
||||
|
||||
LEVEL = 2 # 编译时修改此值
|
||||
|
||||
_gui_callback = None # GUI 注册的回调: (msg: str) -> None
|
||||
|
||||
|
||||
def set_gui_callback(cb):
|
||||
"""注册 GUI 日志回调,所有日志同步输出到 GUI。"""
|
||||
global _gui_callback
|
||||
_gui_callback = cb
|
||||
|
||||
|
||||
def _fmt(msg: str) -> str:
|
||||
return f"[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}] {msg}"
|
||||
|
||||
|
||||
def _emit(msg: str):
|
||||
print(_fmt(msg), file=sys.stderr)
|
||||
if _gui_callback:
|
||||
_gui_callback(msg)
|
||||
|
||||
|
||||
def user(msg: str):
|
||||
_emit(msg)
|
||||
|
||||
|
||||
def debug(msg: str):
|
||||
if LEVEL >= 1:
|
||||
_emit(msg)
|
||||
|
||||
|
||||
def trace(msg: str):
|
||||
if LEVEL >= 2:
|
||||
_emit(f"[trace] {msg}")
|
||||
@@ -0,0 +1,63 @@
|
||||
"""登录助手 — 解析浏览器导出的 cookies JSON,保存为 session.json。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def get_session_path() -> str:
|
||||
if getattr(sys, "frozen", False):
|
||||
base = os.path.dirname(sys.executable)
|
||||
else:
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.join(base, "session.json")
|
||||
|
||||
|
||||
def parse_json_and_save(json_text: str) -> int:
|
||||
"""解析浏览器导出的 cookies JSON 数组,提取目标域名的 cookie 并保存。
|
||||
返回保存的 cookie 数量。
|
||||
"""
|
||||
raw = json.loads(json_text)
|
||||
if not isinstance(raw, list):
|
||||
raise ValueError("JSON 必须是数组格式")
|
||||
|
||||
# 过滤目标域名
|
||||
target = "10.4.188.1"
|
||||
cookies = []
|
||||
seen = set()
|
||||
for c in raw:
|
||||
domain = c.get("domain", "")
|
||||
if target not in domain:
|
||||
continue
|
||||
name = c.get("name", "").strip()
|
||||
path = c.get("path", "/")
|
||||
if not name:
|
||||
continue
|
||||
key = (name, domain, path)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
cookies.append({
|
||||
"name": name,
|
||||
"value": c.get("value", ""),
|
||||
"domain": domain,
|
||||
"path": path,
|
||||
})
|
||||
|
||||
data = {"cookies": cookies, "saved_from": f"https://{target}"}
|
||||
session_path = get_session_path()
|
||||
os.makedirs(os.path.dirname(session_path) or ".", exist_ok=True)
|
||||
with open(session_path, "w", encoding="utf-8") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
return len(cookies)
|
||||
|
||||
|
||||
def load_session_info() -> dict | None:
|
||||
"""读取已保存的 session 信息用于 UI 展示。"""
|
||||
try:
|
||||
with open(get_session_path(), "r") as f:
|
||||
return json.load(f)
|
||||
except (FileNotFoundError, json.JSONDecodeError):
|
||||
return None
|
||||
@@ -0,0 +1,813 @@
|
||||
"""中国邮政揽收系统 - Excel 自动填单工具
|
||||
|
||||
主程序入口,提供 Tkinter GUI 界面。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
from datetime import datetime
|
||||
from tkinter import (
|
||||
BOTH,
|
||||
BOTTOM,
|
||||
DISABLED,
|
||||
END,
|
||||
HORIZONTAL,
|
||||
LEFT,
|
||||
NORMAL,
|
||||
RIGHT,
|
||||
VERTICAL,
|
||||
X,
|
||||
Y,
|
||||
BooleanVar,
|
||||
Button,
|
||||
Entry,
|
||||
Frame,
|
||||
Label,
|
||||
LabelFrame,
|
||||
Listbox,
|
||||
Scrollbar,
|
||||
StringVar,
|
||||
Text,
|
||||
Tk,
|
||||
Toplevel,
|
||||
filedialog,
|
||||
messagebox,
|
||||
ttk,
|
||||
)
|
||||
|
||||
from excel_reader import read_input_excel, validate_required_columns
|
||||
from field_mapping import get_defaults, get_field_mapping, get_fixed_values, get_timing_config, load_config
|
||||
|
||||
# ─── 全局引擎引用 ──────────────────────────────────────────
|
||||
|
||||
_engine = None
|
||||
_loop = None
|
||||
|
||||
def _get_writable_path(name: str) -> str:
|
||||
"""获取可写文件路径(config.json, session.json)。
|
||||
|
||||
开发模式:脚本所在目录。
|
||||
打包模式:exe 所在目录(_MEIPASS 是只读的)。
|
||||
"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
return os.path.join(os.path.dirname(sys.executable), name)
|
||||
else:
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), name)
|
||||
|
||||
def _get_config_path():
|
||||
return _get_writable_path("config.json")
|
||||
def _get_session_path():
|
||||
return _get_writable_path("session.json")
|
||||
|
||||
def _resource_path(relative_path):
|
||||
"""获取只读资源文件路径(pickup.ico 等)。
|
||||
|
||||
开发模式:脚本所在目录。
|
||||
打包模式:_MEIPASS(PyInstaller 解压临时目录)。
|
||||
"""
|
||||
if getattr(sys, 'frozen', False):
|
||||
return os.path.join(sys._MEIPASS, relative_path)
|
||||
else:
|
||||
return os.path.join(os.path.dirname(os.path.abspath(__file__)), relative_path)
|
||||
|
||||
def _get_session_path():
|
||||
return _resource_path("session.json")
|
||||
|
||||
# ─── 异步引擎后台线程 ──────────────────────────────────────
|
||||
|
||||
def _run_async(coro):
|
||||
"""在新线程中运行异步协程。"""
|
||||
global _loop
|
||||
_loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(_loop)
|
||||
_loop.run_until_complete(coro)
|
||||
|
||||
def _run_in_thread(coro):
|
||||
"""在后台线程启动异步任务。"""
|
||||
t = threading.Thread(target=_run_async, args=(coro,), daemon=True)
|
||||
t.start()
|
||||
return t
|
||||
|
||||
# ─── 主窗口 ────────────────────────────────────────────────
|
||||
|
||||
class App:
|
||||
def __init__(self):
|
||||
self.root = Tk()
|
||||
self.root.title("自动填单工具 v2.0")
|
||||
self.root.geometry("900x650")
|
||||
self.root.minsize(800, 550)
|
||||
if sys.platform == "win32":
|
||||
self.root.iconbitmap(_resource_path("pickup.ico"))
|
||||
else:
|
||||
# 在 Linux 上什么都不做,或者用 .xbm 文件
|
||||
# self.root.iconbitmap(_resource_path("pickup.xbm"))
|
||||
pass
|
||||
|
||||
self.config = {}
|
||||
self.excel_path = StringVar()
|
||||
self.delay_var = StringVar(value="3")
|
||||
self.status_var = StringVar(value="就绪")
|
||||
self.engine_status_var = StringVar(value="未启动")
|
||||
self.running = False
|
||||
self.paused = False
|
||||
self.results = []
|
||||
|
||||
self._build_ui()
|
||||
|
||||
# 关闭窗口时清理
|
||||
self.root.protocol("WM_DELETE_WINDOW", self._on_close)
|
||||
|
||||
# 将 log 模块输出重定向到 GUI 日志窗口
|
||||
from log import set_gui_callback
|
||||
set_gui_callback(self._log)
|
||||
|
||||
self._load_config()
|
||||
|
||||
def _build_ui(self):
|
||||
"""构建界面。"""
|
||||
# ── 配置区 ──
|
||||
config_frame = LabelFrame(self.root, text="配置", padx=10, pady=5)
|
||||
config_frame.pack(fill=X, padx=10, pady=5)
|
||||
|
||||
# 引擎状态
|
||||
row_status = Frame(config_frame)
|
||||
row_status.pack(fill=X, pady=2)
|
||||
Label(row_status, text="引擎状态:", width=10, anchor="e").pack(side="left")
|
||||
self.status_label = Label(row_status, textvariable=self.engine_status_var, fg="gray")
|
||||
self.status_label.pack(side="left", padx=5)
|
||||
|
||||
# Excel 文件
|
||||
row2 = Frame(config_frame)
|
||||
row2.pack(fill=X, pady=2)
|
||||
Label(row2, text="Excel文件:", width=10, anchor="e").pack(side="left")
|
||||
self.file_label = Label(row2, text="未选择", fg="gray", anchor="w")
|
||||
self.file_label.pack(side="left", fill=X, expand=True, padx=5)
|
||||
Button(row2, text="选择文件", command=self._pick_file, width=10).pack(
|
||||
side="right"
|
||||
)
|
||||
|
||||
# 延时设置
|
||||
row3 = Frame(config_frame)
|
||||
row3.pack(fill=X, pady=2)
|
||||
Label(row3, text="间隔(秒):", width=10, anchor="e").pack(side="left")
|
||||
Entry(row3, textvariable=self.delay_var, width=8).pack(side="left", padx=5)
|
||||
Label(row3, text="(每单提交之间的等待时间)", fg="gray").pack(side="left")
|
||||
|
||||
# 按钮
|
||||
btn_frame = Frame(config_frame)
|
||||
btn_frame.pack(fill=X, pady=5)
|
||||
self.login_btn = Button(
|
||||
btn_frame, text="🔑 登录", command=self._do_login, width=8
|
||||
)
|
||||
self.login_btn.pack(side="left", padx=2)
|
||||
self.start_btn = Button(
|
||||
btn_frame, text="▶ 开始处理", command=self._start, bg="#4CAF50",
|
||||
fg="white", width=12, height=1
|
||||
)
|
||||
self.start_btn.pack(side="left", padx=2)
|
||||
self.pause_btn = Button(
|
||||
btn_frame, text="⏸ 暂停", command=self._pause, width=10, state=DISABLED
|
||||
)
|
||||
self.pause_btn.pack(side="left", padx=2)
|
||||
self.stop_btn = Button(
|
||||
btn_frame, text="⏹ 停止", command=self._stop, width=10, state=DISABLED
|
||||
)
|
||||
self.stop_btn.pack(side="left", padx=2)
|
||||
self.config_btn = Button(
|
||||
btn_frame, text="编辑配置", command=self._edit_config, width=10
|
||||
)
|
||||
self.config_btn.pack(side="right", padx=2)
|
||||
|
||||
# 状态
|
||||
Label(
|
||||
btn_frame, textvariable=self.status_var, fg="blue"
|
||||
).pack(side="right", padx=10)
|
||||
|
||||
# ── 固定值设置 ──
|
||||
fixed_frame = LabelFrame(self.root, text="固定值设置(优先级:强制覆盖 > API获取值 > 固定值 > 表格 > 默认值)", padx=5, pady=5)
|
||||
fixed_frame.pack(fill=X, padx=10, pady=5)
|
||||
|
||||
tree_f = Frame(fixed_frame)
|
||||
tree_f.pack(fill=X)
|
||||
|
||||
columns = ("field", "value", "force")
|
||||
self.fixed_tree = ttk.Treeview(tree_f, columns=columns, show="headings", height=4)
|
||||
self.fixed_tree.heading("field", text="字段名")
|
||||
self.fixed_tree.heading("value", text="固定值")
|
||||
self.fixed_tree.heading("force", text="强制覆盖")
|
||||
self.fixed_tree.column("field", width=100)
|
||||
self.fixed_tree.column("value", width=260)
|
||||
self.fixed_tree.column("force", width=70, anchor="center")
|
||||
self.fixed_tree.pack(side="left", fill=X, expand=True)
|
||||
|
||||
btn_f = Frame(fixed_frame)
|
||||
btn_f.pack(fill=X, pady=2)
|
||||
Button(btn_f, text="添加", command=self._add_fixed_value, width=6).pack(side="left", padx=2)
|
||||
Button(btn_f, text="删除", command=lambda: [self.fixed_tree.delete(i) for i in self.fixed_tree.selection()], width=6).pack(side="left", padx=2)
|
||||
Button(btn_f, text="保存", command=self._save_fixed_values, width=6, bg="#4CAF50", fg="white").pack(side="left", padx=2)
|
||||
Label(btn_f, text="双击编辑值 | 单击'强制覆盖'列切换 | 留空则不生效", fg="gray").pack(side="left", padx=5)
|
||||
|
||||
self.fixed_tree.bind("<Double-1>", self._edit_fixed_cell)
|
||||
self.fixed_tree.bind("<ButtonRelease-1>", self._toggle_force)
|
||||
|
||||
# ── 进度条 ──
|
||||
progress_frame = Frame(self.root)
|
||||
progress_frame.pack(fill=X, padx=10, pady=2)
|
||||
self.progress = ttk.Progressbar(
|
||||
progress_frame, mode="determinate", length=100
|
||||
)
|
||||
self.progress.pack(fill=X)
|
||||
self.progress_label = Label(progress_frame, text="", fg="gray")
|
||||
self.progress_label.pack()
|
||||
|
||||
# ── 日志区 ──
|
||||
log_frame = LabelFrame(self.root, text="运行日志", padx=10, pady=5)
|
||||
log_frame.pack(fill="both", expand=True, padx=10, pady=5)
|
||||
|
||||
self.log_text = Text(log_frame, wrap="word", state=DISABLED, font=("Consolas", 9))
|
||||
self.log_text.pack(side="left", fill="both", expand=True)
|
||||
|
||||
scroll_y = Scrollbar(log_frame, orient=VERTICAL, command=self.log_text.yview)
|
||||
scroll_y.pack(side=RIGHT, fill=Y)
|
||||
self.log_text.configure(yscrollcommand=scroll_y.set)
|
||||
|
||||
# ─── 配置管理 ───────────────────────────────────────────
|
||||
|
||||
def _load_config(self):
|
||||
"""加载配置文件。首次运行时从 _MEIPASS 复制到 exe 目录。"""
|
||||
try:
|
||||
config_path = _get_config_path()
|
||||
# 打包模式首次运行:从 _MEIPASS 复制 config.json 到可写目录
|
||||
if getattr(sys, 'frozen', False) and not os.path.exists(config_path):
|
||||
bundled = os.path.join(sys._MEIPASS, "config.json")
|
||||
if os.path.exists(bundled):
|
||||
import shutil
|
||||
shutil.copy2(bundled, config_path)
|
||||
self.config = load_config(config_path)
|
||||
timing = get_timing_config(self.config)
|
||||
delay_sec = timing.get("delay_between_rows_ms", 3000) // 1000
|
||||
self.delay_var.set(str(delay_sec))
|
||||
|
||||
# 加载固定值到表格
|
||||
for item in self.fixed_tree.get_children():
|
||||
self.fixed_tree.delete(item)
|
||||
for field, cfg in self.config.get("fixed_values", {}).items():
|
||||
val = cfg.get("value", "")
|
||||
force = "✓" if cfg.get("force") else ""
|
||||
self.fixed_tree.insert("", "end", values=(field, val, force))
|
||||
|
||||
self._log("配置加载成功")
|
||||
|
||||
# 检查已保存的登录会话
|
||||
self._check_session_status()
|
||||
except Exception as e:
|
||||
self._log(f"[error] 配置加载失败: {e}")
|
||||
messagebox.showwarning("配置错误", f"配置文件加载失败:\n{e}")
|
||||
|
||||
def _check_session_status(self):
|
||||
"""检查已保存的登录会话状态并更新 UI。"""
|
||||
try:
|
||||
from login_helper import load_session_info
|
||||
info = load_session_info()
|
||||
if info and len(info.get("cookies", [])) > 0:
|
||||
self.engine_status_var.set(f"已登录 ({len(info['cookies'])} cookies)")
|
||||
self.status_label.configure(fg="green")
|
||||
else:
|
||||
self.engine_status_var.set("未登录")
|
||||
self.status_label.configure(fg="orange")
|
||||
except Exception:
|
||||
self.engine_status_var.set("未登录")
|
||||
self.status_label.configure(fg="orange")
|
||||
|
||||
def _edit_config(self):
|
||||
"""打开配置编辑窗口。"""
|
||||
config_path = _get_config_path()
|
||||
# os.startfile(config_path) if sys.platform == "win32" else None
|
||||
|
||||
win = Toplevel(self.root)
|
||||
win.title("编辑配置")
|
||||
win.geometry("700x500")
|
||||
|
||||
text = Text(win, wrap="none", font=("Consolas", 10))
|
||||
text.pack(fill="both", expand=True, padx=5, pady=5)
|
||||
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
text.insert("1.0", f.read())
|
||||
|
||||
def save():
|
||||
new_config = text.get("1.0", "end-1c")
|
||||
try:
|
||||
json.loads(new_config) # 验证 JSON
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_config)
|
||||
self._load_config()
|
||||
win.destroy()
|
||||
self._log("配置已保存并重新加载")
|
||||
except json.JSONDecodeError as e:
|
||||
messagebox.showerror("JSON 错误", f"配置格式错误:\n{e}")
|
||||
|
||||
Button(win, text="保存", command=save, bg="#4CAF50", fg="white").pack(
|
||||
side="bottom", pady=5
|
||||
)
|
||||
|
||||
# ─── 固定值设置 ────────────────────────────────────────
|
||||
|
||||
def _edit_fixed_cell(self, event):
|
||||
"""双击编辑固定值单元格(跳过强制覆盖列)。"""
|
||||
region = self.fixed_tree.identify_region(event.x, event.y)
|
||||
if region != "cell":
|
||||
return
|
||||
column = self.fixed_tree.identify_column(event.x)
|
||||
if column == "#3": # 强制覆盖列不双击编辑,单击切换
|
||||
return
|
||||
item = self.fixed_tree.identify_row(event.y)
|
||||
if not item:
|
||||
return
|
||||
col_idx = int(column.replace("#", "")) - 1
|
||||
old_val = self.fixed_tree.item(item, "values")[col_idx]
|
||||
|
||||
edit = Entry(self.root)
|
||||
edit.insert(0, old_val)
|
||||
edit.select_range(0, "end")
|
||||
bbox = self.fixed_tree.bbox(item, column)
|
||||
if bbox:
|
||||
edit.place(x=bbox[0] + 185, y=bbox[1] + 310, width=bbox[2], height=bbox[3])
|
||||
|
||||
def save_edit(e=None):
|
||||
vals = list(self.fixed_tree.item(item, "values"))
|
||||
vals[col_idx] = edit.get()
|
||||
self.fixed_tree.item(item, values=vals)
|
||||
edit.destroy()
|
||||
edit.bind("<Return>", save_edit)
|
||||
edit.bind("<FocusOut>", lambda e: edit.destroy())
|
||||
edit.focus_set()
|
||||
|
||||
def _add_fixed_value(self):
|
||||
"""添加新的固定值行(默认不强制覆盖)。"""
|
||||
self.fixed_tree.insert("", "end", values=("新固定值", "", ""))
|
||||
|
||||
def _toggle_force(self, event):
|
||||
"""单击'强制覆盖'列切换开关状态。"""
|
||||
region = self.fixed_tree.identify_region(event.x, event.y)
|
||||
if region != "cell":
|
||||
return
|
||||
column = self.fixed_tree.identify_column(event.x)
|
||||
if column != "#3": # force 是第3列
|
||||
return
|
||||
item = self.fixed_tree.identify_row(event.y)
|
||||
if not item:
|
||||
return
|
||||
vals = list(self.fixed_tree.item(item, "values"))
|
||||
vals[2] = "" if vals[2] == "✓" else "✓"
|
||||
self.fixed_tree.item(item, values=vals)
|
||||
|
||||
def _save_fixed_values(self):
|
||||
"""保存固定值到 config.json(新格式:{value, force})。"""
|
||||
new_fixed = {}
|
||||
for item in self.fixed_tree.get_children():
|
||||
vals = self.fixed_tree.item(item, "values")
|
||||
field_name = vals[0].strip()
|
||||
if field_name:
|
||||
new_fixed[field_name] = {
|
||||
"value": vals[1].strip() if len(vals) > 1 else "",
|
||||
"force": vals[2] == "✓" if len(vals) > 2 else False,
|
||||
}
|
||||
self.config["fixed_values"] = new_fixed
|
||||
|
||||
config_path = _get_config_path()
|
||||
with open(config_path, "r", encoding="utf-8") as f:
|
||||
cfg = json.load(f)
|
||||
cfg["fixed_values"] = new_fixed
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
json.dump(cfg, f, ensure_ascii=False, indent=4)
|
||||
|
||||
self._log(f"固定值已保存: {len(new_fixed)} 项")
|
||||
|
||||
# ─── 文件选择 ───────────────────────────────────────────
|
||||
|
||||
def _pick_file(self):
|
||||
path = filedialog.askopenfilename(
|
||||
title="选择 Excel 文件",
|
||||
filetypes=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
|
||||
)
|
||||
if path:
|
||||
self.excel_path.set(path)
|
||||
self.file_label.configure(text=path, fg="black")
|
||||
|
||||
def _on_close(self):
|
||||
"""关闭窗口;atexit 会强制清理 Node.js 子进程。"""
|
||||
global _engine
|
||||
if _engine:
|
||||
_engine.request_stop()
|
||||
self.root.destroy()
|
||||
|
||||
# ─── 控制按钮 ───────────────────────────────────────────
|
||||
|
||||
def _do_login(self):
|
||||
"""弹窗让用户粘贴一键导出的 cookies JSON。"""
|
||||
from login_helper import parse_json_and_save, load_session_info
|
||||
|
||||
win = Toplevel(self.root)
|
||||
win.title("登录揽收系统")
|
||||
win.geometry("620x360")
|
||||
win.transient(self.root)
|
||||
win.grab_set()
|
||||
|
||||
Label(win, text="登录揽收系统", font=("", 12, "bold"), pady=8).pack()
|
||||
|
||||
Label(win, text="使用 Cookies 导出工具,将 10.4.188.1 的所有 cookies 导出为 JSON,粘贴到下方:").pack(anchor="w", padx=10)
|
||||
|
||||
json_text = Text(win, height=12, wrap="word", font=("Consolas", 9))
|
||||
json_text.pack(fill=BOTH, expand=True, padx=10, pady=5)
|
||||
|
||||
def do_save():
|
||||
try:
|
||||
n = parse_json_and_save(json_text.get("1.0", "end-1c"))
|
||||
self._log(f"已保存 {n} 个 cookies 到 session.json")
|
||||
self.engine_status_var.set("已登录")
|
||||
self.status_label.configure(fg="green")
|
||||
self._check_session_status()
|
||||
win.destroy()
|
||||
except Exception as e:
|
||||
messagebox.showerror("解析失败", f"JSON 解析失败:\n{e}", parent=win)
|
||||
|
||||
btn_f = Frame(win)
|
||||
btn_f.pack(pady=10)
|
||||
Button(btn_f, text="保存并关闭", command=do_save,
|
||||
bg="#4CAF50", fg="white", width=15, height=1).pack()
|
||||
|
||||
info = load_session_info()
|
||||
if info:
|
||||
Label(win, text=f"当前已保存: {len(info.get('cookies', []))} 个 cookies",
|
||||
fg="gray").pack()
|
||||
|
||||
def _start(self):
|
||||
if self.running:
|
||||
return
|
||||
|
||||
if not self.excel_path.get():
|
||||
messagebox.showwarning("提示", "请先选择 Excel 文件")
|
||||
return
|
||||
|
||||
try:
|
||||
rows, headers = read_input_excel(self.excel_path.get())
|
||||
except Exception as e:
|
||||
messagebox.showerror("读取错误", f"读取 Excel 失败:\n{e}")
|
||||
return
|
||||
|
||||
if not rows:
|
||||
messagebox.showwarning("提示", "Excel 中没有数据")
|
||||
return
|
||||
|
||||
# 验证必要字段是否有数据来源(固定值 > Excel > 默认值)
|
||||
fixed_values = get_fixed_values(self.config)
|
||||
defaults = get_defaults(self.config)
|
||||
missing = validate_required_columns(headers, fixed_values, defaults)
|
||||
if missing:
|
||||
msg = (
|
||||
f"以下字段在所有来源中均未找到值:\n"
|
||||
+ "\n".join(f" - {c}(请填入Excel / 固定值 / 默认值任一)" for c in missing)
|
||||
)
|
||||
messagebox.showwarning("缺少必要字段", msg)
|
||||
return
|
||||
|
||||
self.running = True
|
||||
self.paused = False
|
||||
self._set_controls_state("running")
|
||||
|
||||
# 创建结果工作簿
|
||||
self.results = []
|
||||
|
||||
# 更新配置中的延时
|
||||
try:
|
||||
delay_ms = int(self.delay_var.get()) * 1000
|
||||
self.config.setdefault("timing", {})["delay_between_rows_ms"] = delay_ms
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
self._log(f"开始处理 {len(rows)} 条记录...")
|
||||
self.progress["maximum"] = len(rows)
|
||||
self.progress["value"] = 0
|
||||
self.progress_label.configure(text=f"0 / {len(rows)}")
|
||||
|
||||
# 在后台线程启动异步引擎
|
||||
_run_in_thread(self._run_engine(rows, headers))
|
||||
|
||||
async def _run_engine(self, rows, headers):
|
||||
"""后台运行引擎(在 async 线程中执行)。"""
|
||||
global _engine
|
||||
|
||||
try:
|
||||
from engine import PickupEngine
|
||||
|
||||
field_mapping = get_field_mapping(self.config)
|
||||
defaults = get_defaults(self.config)
|
||||
timing = get_timing_config(self.config)
|
||||
fixed_values = get_fixed_values(self.config)
|
||||
|
||||
_engine = PickupEngine(
|
||||
url="https://10.4.188.1/pickup-web/a/postwaybill/main",
|
||||
field_mapping=field_mapping,
|
||||
defaults=defaults,
|
||||
timing=timing,
|
||||
log_callback=self._log_async,
|
||||
fixed_values=fixed_values,
|
||||
choose_callback=self._choose_callback,
|
||||
confirm_callback=self._confirm_callback,
|
||||
)
|
||||
|
||||
self._update_status("启动中...")
|
||||
try:
|
||||
await _engine.start()
|
||||
except Exception as e:
|
||||
self._log_async(f"[error] 启动失败: {e}")
|
||||
self._update_status("启动失败")
|
||||
await _engine.stop()
|
||||
return
|
||||
|
||||
# 验证 engine.start() 中已加载了 session,且包含关键 cookie
|
||||
if not _engine._sess or not _engine._sess._http.cookies:
|
||||
self._log_async("[提示] 未找到登录会话,请点击'登录'按钮完成登录后再开始处理")
|
||||
self._update_status("未登录")
|
||||
await _engine.stop()
|
||||
return
|
||||
cookie_names = {c.name for c in _engine._sess._http.cookies}
|
||||
missing = [n for n in ("JSESSIONID", "jdpt.session.id") if n not in cookie_names]
|
||||
if missing:
|
||||
self._log_async(f"[提示] 缺少关键 cookie: {', '.join(missing)},请重新登录并手动填写")
|
||||
self._update_status("未登录")
|
||||
await _engine.stop()
|
||||
return
|
||||
|
||||
# 初始化页面资源和 token bridge
|
||||
try:
|
||||
await _engine.begin_processing()
|
||||
self._update_status("后台运行中")
|
||||
self._log_async("HTTP API 引擎已就绪")
|
||||
except Exception as e:
|
||||
self._log_async(f"[error] 初始化失败: {e}")
|
||||
self._update_status("启动失败")
|
||||
await _engine.stop()
|
||||
return
|
||||
|
||||
for i, row in enumerate(rows):
|
||||
if _engine._stop_requested:
|
||||
break
|
||||
|
||||
result = await _engine.process_single_row(row)
|
||||
self.results.append(result)
|
||||
|
||||
# 更新进度
|
||||
self.root.after(0, lambda cur=i+1, total=len(rows): self._update_progress(cur, total))
|
||||
|
||||
# 行间延时
|
||||
delay = timing.get("delay_between_rows_ms", 3000) / 1000
|
||||
if i < len(rows) - 1:
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
self._log_async(f"\n处理完成!共处理 {len(self.results)} 条记录")
|
||||
|
||||
except Exception as e:
|
||||
self._log_async(f"[error] 引擎异常: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
finally:
|
||||
if _engine:
|
||||
await _engine.stop()
|
||||
self.root.after(0, self._on_finished)
|
||||
|
||||
def _update_progress(self, current, total):
|
||||
self.progress["value"] = current
|
||||
self.progress_label.configure(text=f"{current} / {total}")
|
||||
self.status_var.set(f"处理中: {current}/{total}")
|
||||
|
||||
def _on_finished(self):
|
||||
global _engine
|
||||
self.running = False
|
||||
self.paused = False
|
||||
_engine = None
|
||||
self._set_controls_state("stopped")
|
||||
self.status_var.set("就绪")
|
||||
self._log("处理结束")
|
||||
|
||||
def _pause(self):
|
||||
global _engine
|
||||
if not self.running:
|
||||
return
|
||||
if self.paused:
|
||||
self.paused = False
|
||||
if _engine:
|
||||
_engine.resume()
|
||||
self.pause_btn.configure(text="⏸ 暂停")
|
||||
self.status_var.set("处理中...")
|
||||
self._log("已恢复")
|
||||
else:
|
||||
self.paused = True
|
||||
if _engine:
|
||||
_engine.request_pause()
|
||||
self.pause_btn.configure(text="▶ 继续")
|
||||
self.status_var.set("已暂停")
|
||||
self._log("已暂停")
|
||||
|
||||
def _stop(self):
|
||||
global _engine
|
||||
self.running = False
|
||||
if _engine:
|
||||
_engine.request_stop()
|
||||
self._set_controls_state("stopped")
|
||||
self.status_var.set("正在停止...")
|
||||
self._log("用户停止")
|
||||
|
||||
def _set_controls_state(self, state):
|
||||
if state == "running":
|
||||
self.start_btn.configure(state=DISABLED)
|
||||
self.pause_btn.configure(state=NORMAL)
|
||||
self.stop_btn.configure(state=NORMAL)
|
||||
else:
|
||||
self.start_btn.configure(state=NORMAL)
|
||||
self.pause_btn.configure(state=DISABLED, text="⏸ 暂停")
|
||||
self.stop_btn.configure(state=DISABLED)
|
||||
|
||||
# ─── 日志 ───────────────────────────────────────────────
|
||||
|
||||
def _log(self, msg):
|
||||
"""线程安全的日志输出。"""
|
||||
self.root.after(0, self._log_sync, msg)
|
||||
|
||||
def _log_async(self, msg):
|
||||
"""从异步线程调用(直接入队到 GUI 线程)。"""
|
||||
self.root.after(0, self._log_sync, msg)
|
||||
|
||||
def _update_status(self, status: str):
|
||||
"""更新引擎状态显示。"""
|
||||
color_map = {"后台运行中": "green", "启动失败": "red", "未登录": "orange"}
|
||||
self.root.after(0, lambda: self.engine_status_var.set(status))
|
||||
self.root.after(0, lambda s=status: self.status_label.configure(
|
||||
fg=color_map.get(s, "gray")
|
||||
))
|
||||
|
||||
|
||||
async def _choose_callback(self, title: str, options: list[str]) -> int | None:
|
||||
"""弹出选择框让用户从 autocomplete 结果中选择。返回索引或 None。"""
|
||||
loop = asyncio.get_event_loop()
|
||||
future = loop.create_future()
|
||||
result = {"index": None}
|
||||
|
||||
def show():
|
||||
win = Toplevel(self.root)
|
||||
win.title(title)
|
||||
win.geometry("400x300")
|
||||
win.transient(self.root)
|
||||
win.grab_set()
|
||||
|
||||
Label(win, text=f"请选择{title},选择后批量复用:", pady=10).pack()
|
||||
|
||||
frame = Frame(win)
|
||||
frame.pack(fill="both", expand=True, padx=10)
|
||||
listbox = Listbox(frame)
|
||||
scroll = Scrollbar(frame, orient=VERTICAL, command=listbox.yview)
|
||||
listbox.configure(yscrollcommand=scroll.set)
|
||||
scroll.pack(side=RIGHT, fill=Y)
|
||||
listbox.pack(side=LEFT, fill=BOTH, expand=True)
|
||||
|
||||
for i, opt in enumerate(options):
|
||||
listbox.insert(END, f"[{i+1}] {opt}")
|
||||
|
||||
def on_select():
|
||||
sel = listbox.curselection()
|
||||
if sel:
|
||||
result["index"] = sel[0]
|
||||
win.destroy()
|
||||
|
||||
listbox.bind("<Double-1>", lambda e: on_select())
|
||||
|
||||
# 关闭窗口 → result["index"] 为 None → engine 停止 batch
|
||||
def on_cancel():
|
||||
result["index"] = None
|
||||
win.destroy()
|
||||
|
||||
btn_frame = Frame(win)
|
||||
btn_frame.pack(pady=10)
|
||||
Button(btn_frame, text="确认选择", command=on_select,
|
||||
bg="#4CAF50", fg="white", width=12).pack(side=LEFT, padx=5)
|
||||
Button(btn_frame, text="取消", command=on_cancel,
|
||||
bg="red", fg="white", width=8).pack(side=LEFT, padx=5)
|
||||
|
||||
win.protocol("WM_DELETE_WINDOW", on_cancel)
|
||||
listbox.focus_set()
|
||||
|
||||
# 等窗口关闭后通知 async 线程
|
||||
def wait_then_resolve():
|
||||
win.wait_window()
|
||||
loop.call_soon_threadsafe(lambda: future.set_result(result["index"]))
|
||||
threading.Thread(target=wait_then_resolve, daemon=True).start()
|
||||
|
||||
self.root.after(0, show)
|
||||
return await future
|
||||
|
||||
async def _confirm_callback(self, fields: dict[str, str]) -> bool:
|
||||
"""弹出确认框显示表单数据,用户确认后返回 True。"""
|
||||
loop = asyncio.get_event_loop()
|
||||
future = loop.create_future()
|
||||
result = {"confirmed": False}
|
||||
|
||||
def show():
|
||||
win = Toplevel(self.root)
|
||||
win.title("确认提交信息 - 确认后自动批量处理")
|
||||
win.geometry("450x400")
|
||||
win.transient(self.root)
|
||||
win.grab_set()
|
||||
|
||||
Label(win, text="请确认以下信息无误后点击确认:", pady=10,
|
||||
font=("", 10, "bold")).pack()
|
||||
|
||||
frame = Frame(win)
|
||||
frame.pack(fill=BOTH, expand=True, padx=20, pady=5)
|
||||
|
||||
text = Text(frame, wrap="word", font=("Consolas", 10), height=16)
|
||||
text.pack(side=LEFT, fill=BOTH, expand=True)
|
||||
scroll = Scrollbar(frame, command=text.yview)
|
||||
scroll.pack(side=RIGHT, fill=Y)
|
||||
text.configure(yscrollcommand=scroll.set)
|
||||
|
||||
# 系统标记(不展示)
|
||||
from engine import _SYSTEM_FLAGS
|
||||
sys_keys = set(_SYSTEM_FLAGS.keys())
|
||||
|
||||
# 关键字段:始终展示,值也翻译为中文
|
||||
from field_mapping import reverse_translate
|
||||
key_fields = [
|
||||
("产品名称", "bizProductName", ""),
|
||||
("客户名称", "senders", ""),
|
||||
("客户代码", "senderNo", ""),
|
||||
("邮件号", "waybillNo", ""),
|
||||
("重量(g)", "realWeight", ""),
|
||||
("寄达地", "receiverArriveOrgName", ""),
|
||||
("收件人", "receiverLinker", ""),
|
||||
("收件人手机", "receiverMobile", ""),
|
||||
("收件人地址", "receiverAddr", ""),
|
||||
("寄件人", "senderLinker", ""),
|
||||
("寄件人手机", "senderMobile", ""),
|
||||
("寄件人地址", "senderAddr", ""),
|
||||
("运输类型", "transferType", "运输类型"),
|
||||
("内件属性", "contentsAttribute", "内件属性"),
|
||||
("电商平台", "ecommerceNo", "电商平台"),
|
||||
("付款方式", "paymentMode", "付款方式"),
|
||||
("内件品名", "reserved5", ""),
|
||||
("保价金额", "insuranceAmount", ""),
|
||||
("代收货款", "codAmount", ""),
|
||||
("备注", "senderNotes", ""),
|
||||
]
|
||||
if not isinstance(fields, dict):
|
||||
text.insert(END, f"错误: 表单数据异常 ({type(fields).__name__})")
|
||||
text.configure(state=DISABLED)
|
||||
return
|
||||
shown = set()
|
||||
for label, key, col_name in key_fields:
|
||||
val = fields.get(key, "") or "空"
|
||||
if col_name and val != "空":
|
||||
val = reverse_translate(col_name, val)
|
||||
text.insert(END, f"{label}: {val}\n")
|
||||
shown.add(key)
|
||||
# 展示其余非系统标记的有值字段
|
||||
for k, v in fields.items():
|
||||
if v and k not in shown and k not in sys_keys:
|
||||
text.insert(END, f"{k}: {v}\n")
|
||||
text.configure(state=DISABLED)
|
||||
|
||||
btn_frame = Frame(win)
|
||||
btn_frame.pack(pady=10)
|
||||
Button(btn_frame, text="确认提交",
|
||||
command=lambda: (result.__setitem__("confirmed", True), win.destroy()),
|
||||
bg="#4CAF50", fg="white", width=12, height=1).pack(side=LEFT, padx=10)
|
||||
Button(btn_frame, text="取消",
|
||||
command=win.destroy,
|
||||
bg="red", fg="white", width=10).pack(side=LEFT, padx=10)
|
||||
|
||||
def wait_then_resolve():
|
||||
win.wait_window()
|
||||
loop.call_soon_threadsafe(lambda: future.set_result(result["confirmed"]))
|
||||
threading.Thread(target=wait_then_resolve, daemon=True).start()
|
||||
|
||||
self.root.after(0, show)
|
||||
return await future
|
||||
|
||||
def _log_sync(self, msg):
|
||||
timestamp = datetime.now().strftime("%H:%M:%S")
|
||||
line = f"[{timestamp}] {msg}\n"
|
||||
self.log_text.configure(state=NORMAL)
|
||||
self.log_text.insert(END, line)
|
||||
self.log_text.see(END)
|
||||
self.log_text.configure(state=DISABLED)
|
||||
|
||||
# ─── 入口 ──────────────────────────────────────────────────
|
||||
|
||||
def main():
|
||||
app = App()
|
||||
app.root.mainloop()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
+604
@@ -0,0 +1,604 @@
|
||||
"""揽收系统 HTTP API 封装 — 直接调用后端接口,不依赖浏览器。
|
||||
|
||||
依赖 Node.js token bridge 计算 $_ts 安全框架的 c93l5Fya 动态 token。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
from typing import Any
|
||||
from urllib.parse import urlparse, urljoin
|
||||
|
||||
from log import debug
|
||||
|
||||
|
||||
# ─── Token Bridge 管理 ────────────────────────────────────────────────
|
||||
|
||||
def _get_bridge_dir() -> str:
|
||||
"""获取 token_bridge 目录路径(支持 PyInstaller 打包)。"""
|
||||
if getattr(sys, "frozen", False):
|
||||
base = sys._MEIPASS
|
||||
else:
|
||||
base = os.path.dirname(os.path.abspath(__file__))
|
||||
return os.path.join(base, "token_bridge")
|
||||
|
||||
|
||||
class TokenBridge:
|
||||
"""管理 Node.js token 计算子进程(HTTP 通信)。"""
|
||||
|
||||
def __init__(self):
|
||||
self._proc: subprocess.Popen | None = None
|
||||
self._port: int = 0
|
||||
self._base_url: str = ""
|
||||
|
||||
def start(self):
|
||||
bridge_dir = _get_bridge_dir()
|
||||
kwargs = dict(
|
||||
cwd=bridge_dir,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True, encoding="utf-8", errors="replace",
|
||||
)
|
||||
if sys.platform == "win32":
|
||||
kwargs["creationflags"] = subprocess.CREATE_NO_WINDOW
|
||||
self._proc = subprocess.Popen(["node", "server.js", "0"], **kwargs)
|
||||
|
||||
# 进程退出时强制杀子进程,防止 _MEIPASS 目录无法删除
|
||||
import atexit as _atexit
|
||||
_atexit.register(self.stop)
|
||||
# 读取就绪信号(包含端口号)
|
||||
line = self._proc.stderr.readline()
|
||||
try:
|
||||
msg = json.loads(line.strip())
|
||||
if not msg.get("ready"):
|
||||
raise RuntimeError(f"Bridge startup failed: {line}")
|
||||
self._port = msg["port"]
|
||||
self._base_url = f"http://127.0.0.1:{self._port}"
|
||||
except json.JSONDecodeError:
|
||||
raise RuntimeError(f"Bridge unexpected stderr: {line}")
|
||||
|
||||
# 后台线程持续读取 stderr 并输出到 log
|
||||
import threading as _threading
|
||||
def _read_stderr():
|
||||
from log import trace
|
||||
for line in self._proc.stderr:
|
||||
trace(f"bridge stderr: {line.strip()}")
|
||||
_threading.Thread(target=_read_stderr, daemon=True).start()
|
||||
|
||||
def stop(self):
|
||||
if self._proc:
|
||||
try:
|
||||
self._proc.kill()
|
||||
self._proc.wait(timeout=3)
|
||||
except Exception:
|
||||
pass
|
||||
self._proc = None
|
||||
self._port = 0
|
||||
|
||||
def _request(self, path: str, body: dict = None) -> dict:
|
||||
"""向 bridge HTTP 服务器发送请求。"""
|
||||
from log import debug, trace
|
||||
import requests as _requests
|
||||
url = f"{self._base_url}{path}"
|
||||
trace(f"bridge -> {path} {str(body)[:100] if body else ''}")
|
||||
try:
|
||||
if body:
|
||||
resp = _requests.post(url, json=body, timeout=30)
|
||||
else:
|
||||
resp = _requests.get(url, timeout=30)
|
||||
resp.raise_for_status()
|
||||
result = resp.json()
|
||||
trace(f"bridge <- {str(result)[:100]}")
|
||||
if "error" in result:
|
||||
raise RuntimeError(f"Bridge error: {result['error']}")
|
||||
return result
|
||||
except _requests.RequestException as e:
|
||||
# 读取 stderr
|
||||
err = ""
|
||||
try:
|
||||
while True:
|
||||
chunk = self._proc.stderr.readline()
|
||||
if not chunk:
|
||||
break
|
||||
err += chunk
|
||||
except Exception:
|
||||
pass
|
||||
if err:
|
||||
raise RuntimeError(f"Bridge request failed: {e}, stderr={err[:300]}")
|
||||
raise
|
||||
|
||||
def initialize(self, html: str, scripts: list[dict], base_url: str):
|
||||
return self._request("/init", {
|
||||
"html": html,
|
||||
"scripts": scripts,
|
||||
"baseUrl": base_url,
|
||||
})
|
||||
|
||||
def compute_token(self, method: str, path: str) -> str:
|
||||
result = self._request("/token", {
|
||||
"method": method,
|
||||
"path": path,
|
||||
})
|
||||
return result["token"]
|
||||
|
||||
def ping(self) -> bool:
|
||||
try:
|
||||
self._request("/ping")
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
# ─── HTTP 会话 & 页面资源管理 ─────────────────────────────────────────
|
||||
|
||||
class PickupSession:
|
||||
"""管理 HTTP 会话(cookies)和页面资源加载。"""
|
||||
|
||||
def __init__(self, base_url: str):
|
||||
self.base_url = base_url.rstrip("/")
|
||||
parsed = urlparse(self.base_url)
|
||||
self._origin = f"{parsed.scheme}://{parsed.netloc}"
|
||||
# 使用 requests 保持 cookie 会话(同步,足够简单可靠)
|
||||
import requests as _requests
|
||||
self._http: _requests.Session = _requests.Session()
|
||||
self._http.verify = False # 内部自签证书
|
||||
import urllib3
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
self._http.headers.update({
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/109.0.0.0 Safari/537.36"
|
||||
),
|
||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||
"Cache-Control": "no-cache",
|
||||
"Pragma": "no-cache",
|
||||
})
|
||||
self._scripts: list[dict] = []
|
||||
self._html: str = ""
|
||||
self._pickup_path: str = ""
|
||||
self._ts_cd: str = ""
|
||||
self._ts_nsd: int = 0
|
||||
|
||||
@property
|
||||
def cookies_str(self) -> str:
|
||||
"""将 requests cookies 转为 Cookie 请求头字符串。"""
|
||||
pairs = []
|
||||
for cookie in self._http.cookies:
|
||||
pairs.append(f"{cookie.name}={cookie.value}")
|
||||
return "; ".join(pairs)
|
||||
|
||||
def load_session(self, session_path: str) -> bool:
|
||||
"""从文件加载之前保存的 cookies。返回是否加载成功。"""
|
||||
import requests as _requests
|
||||
try:
|
||||
with open(session_path, "r") as f:
|
||||
data = json.load(f)
|
||||
seen = set()
|
||||
for cookie_data in data.get("cookies", []):
|
||||
key = (cookie_data["name"], cookie_data.get("domain", ""), cookie_data.get("path", "/"))
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
self._http.cookies.set(
|
||||
cookie_data["name"],
|
||||
cookie_data["value"],
|
||||
domain=cookie_data.get("domain", ""),
|
||||
path=cookie_data.get("path", "/"),
|
||||
)
|
||||
return len(seen) > 0
|
||||
except (FileNotFoundError, json.JSONDecodeError, KeyError):
|
||||
return False
|
||||
|
||||
def save_session(self, session_path: str):
|
||||
"""保存 cookies 到文件。"""
|
||||
data = {
|
||||
"cookies": [
|
||||
{"name": c.name, "value": c.value, "domain": c.domain, "path": c.path}
|
||||
for c in self._http.cookies
|
||||
]
|
||||
}
|
||||
with open(session_path, "w") as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
|
||||
def _abs_url(self, path_or_url: str) -> str:
|
||||
if path_or_url.startswith("http"):
|
||||
return path_or_url
|
||||
return urljoin(self._origin, path_or_url)
|
||||
|
||||
def fetch_page_resources(self) -> dict:
|
||||
"""加载 pickup 页面及所需 JS 文件。返回 {html, scripts, base_url, nsd}。"""
|
||||
import requests as _requests
|
||||
|
||||
pickup_path = urlparse(self.base_url).path
|
||||
self._pickup_path = pickup_path
|
||||
|
||||
# 加载 pickup 页面(需要 Referer 满足安全框架要求)
|
||||
portal_url = f"{self._origin}/portal/a"
|
||||
debug(f"fetch_page: loading {self.base_url}, cookies={list(self._http.cookies.keys())}")
|
||||
resp = self._http.get(
|
||||
self.base_url,
|
||||
headers={
|
||||
"Referer": portal_url,
|
||||
"Sec-Fetch-Dest": "iframe",
|
||||
"Sec-Fetch-Mode": "navigate",
|
||||
"Sec-Fetch-Site": "same-origin",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
debug(f"fetch_page: status={resp.status_code}")
|
||||
if resp.status_code != 200:
|
||||
debug(f"fetch_page: resp_headers={dict(resp.headers)}")
|
||||
debug(f"fetch_page: resp_body={resp.text[:500]}")
|
||||
resp.raise_for_status()
|
||||
self._html = resp.text
|
||||
|
||||
# 从页面 HTML 中提取 $_ts 安全参数
|
||||
self._ts_cd = ""
|
||||
self._ts_nsd = 0
|
||||
m = re.search(r'\$_ts\.cd="([^"]*)"', self._html)
|
||||
if m:
|
||||
self._ts_cd = m.group(1)
|
||||
m = re.search(r'\$_ts\.nsd=(\d+)', self._html)
|
||||
if m:
|
||||
self._ts_nsd = int(m.group(1))
|
||||
if not self._ts_cd or not self._ts_nsd:
|
||||
raise RuntimeError("Failed to extract $_ts.cd/nsd from page")
|
||||
|
||||
# 2. 提取外部 JS 引用,只保留 security JS(cfd114a.js)
|
||||
# 页面业务脚本(jQuery、main.js 等)会导致 jsdom 卡死,不需要
|
||||
script_urls = re.findall(
|
||||
r'<script[^>]+src="([^"]+)"[^>]*>', self._html, re.IGNORECASE
|
||||
)
|
||||
self._scripts = []
|
||||
for url in script_urls:
|
||||
if "cfd114a.js" not in url:
|
||||
continue
|
||||
abs_url = self._abs_url(url)
|
||||
try:
|
||||
script_resp = self._http.get(abs_url, timeout=15)
|
||||
if script_resp.status_code == 200:
|
||||
self._scripts.append({"url": url, "content": script_resp.text})
|
||||
debug(f"fetch_page: loaded security JS {url}")
|
||||
except _requests.RequestException:
|
||||
debug(f"Failed to fetch script: {abs_url}")
|
||||
|
||||
if not self._scripts:
|
||||
raise RuntimeError("Security JS (cfd114a.js) not found on page")
|
||||
|
||||
return {
|
||||
"html": self._html,
|
||||
"scripts": self._scripts,
|
||||
"base_url": self.base_url,
|
||||
"ts_cd": self._ts_cd,
|
||||
"ts_nsd": self._ts_nsd,
|
||||
}
|
||||
|
||||
def request(self, method: str, path: str, token: str,
|
||||
data: dict[str, str] = None) -> dict[str, Any]:
|
||||
"""向 pickup-web 后端发送 API 请求。
|
||||
|
||||
Args:
|
||||
method: HTTP 方法 (GET/POST)
|
||||
path: API 路径 (如 /pickup-web/a/common/customerautocomplete)
|
||||
token: c93l5Fya 安全 token
|
||||
data: POST 表单数据
|
||||
|
||||
Returns:
|
||||
JSON 响应(如果是 JSON),否则返回 {"_text": response_text}
|
||||
"""
|
||||
full_url = self._abs_url(path)
|
||||
if "?" in full_url:
|
||||
full_url += f"&c93l5Fya={token}"
|
||||
else:
|
||||
full_url += f"?c93l5Fya={token}"
|
||||
|
||||
headers = {
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
"Accept": "application/json, text/javascript, */*; q=0.01",
|
||||
"Referer": self.base_url,
|
||||
}
|
||||
if data:
|
||||
headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8"
|
||||
|
||||
debug(f"API {method} {path}")
|
||||
resp = self._http.request(
|
||||
method, full_url,
|
||||
headers=headers,
|
||||
data=data or None,
|
||||
timeout=30,
|
||||
)
|
||||
debug(f"API {method} {path} → {resp.status_code}")
|
||||
if resp.status_code >= 400:
|
||||
debug(f"API error body: {resp.text[:500]}")
|
||||
resp.raise_for_status()
|
||||
|
||||
text = resp.text
|
||||
if text and ("<html" in text[:300].lower() or "<!doctype" in text[:300].lower()):
|
||||
raise RuntimeError("Session 已过期,返回了登录页面,请重新登录并获取 cookies")
|
||||
|
||||
content_type = resp.headers.get("Content-Type", "")
|
||||
if "json" in content_type or text.strip().startswith(("[", "{")):
|
||||
try:
|
||||
return resp.json()
|
||||
except ValueError:
|
||||
pass
|
||||
return {"_text": text}
|
||||
|
||||
|
||||
# ─── 业务 API ─────────────────────────────────────────────────────────
|
||||
|
||||
class PickupAPI:
|
||||
"""揽收系统业务 API 封装。
|
||||
|
||||
每个方法对应页面上的一个 AJAX 操作。
|
||||
所有需要 token 的方法都通过 token_bridge 计算 c93l5Fya 参数。
|
||||
"""
|
||||
|
||||
def __init__(self, session: PickupSession, bridge: TokenBridge):
|
||||
self._sess = session
|
||||
self._bridge = bridge
|
||||
|
||||
def _token(self, method: str, path: str) -> str:
|
||||
return self._bridge.compute_token(method, path)
|
||||
|
||||
def _post(self, path: str, data: dict[str, str] = None) -> dict:
|
||||
token = self._token("POST", path)
|
||||
return self._sess.request("POST", path, token, data)
|
||||
|
||||
def _get(self, path: str) -> dict:
|
||||
token = self._token("GET", path)
|
||||
return self._sess.request("GET", path, token)
|
||||
|
||||
# ── 产品 ──────────────────────────────────────────────────
|
||||
|
||||
def query_product(self, keyword: str) -> list[dict]:
|
||||
"""搜索产品名称,返回候选列表(data=id|bizProductNo, value=显示名)。"""
|
||||
result = self._post(
|
||||
"/pickup-web/a/postwaybilledit/queryproductmanager",
|
||||
data={"no": "on", "query": keyword},
|
||||
)
|
||||
from log import trace
|
||||
trace(f"query_product raw: {str(result)[:300]}")
|
||||
suggestions = result.get("suggestions", []) if isinstance(result, dict) else []
|
||||
# 解析 data 字段: "productId|bizProductNo"
|
||||
parsed = []
|
||||
for s in suggestions:
|
||||
data_parts = s.get("data", "").split("|")
|
||||
parsed.append({
|
||||
"productId": data_parts[0] if len(data_parts) > 0 else "",
|
||||
"bizProductNo": data_parts[1] if len(data_parts) > 1 else "",
|
||||
"productName": s.get("value", ""),
|
||||
})
|
||||
return parsed
|
||||
|
||||
def select_product(self, biz_product_id: str, biz_product_no: str) -> dict:
|
||||
"""选择产品后获取产品详情(模板、材料等)。"""
|
||||
product_info = {"bizProductId": biz_product_id, "bizProductNo": biz_product_no}
|
||||
|
||||
# 获取模板(返回数组,取第一个)
|
||||
template = self._post(
|
||||
"/pickup-web/a/common/templatecodemodel",
|
||||
data={"bizProductNo": biz_product_no},
|
||||
)
|
||||
tmpl = template[0] if isinstance(template, list) and template else template
|
||||
if isinstance(tmpl, dict):
|
||||
product_info.update(tmpl)
|
||||
|
||||
# 查询产品详情
|
||||
detail = self._post(
|
||||
"/pickup-web/a/common/queryproductbymanager",
|
||||
data={
|
||||
"bizProductId": biz_product_id,
|
||||
"bizProductNo": biz_product_no,
|
||||
},
|
||||
)
|
||||
if isinstance(detail, dict):
|
||||
product_info.update(detail)
|
||||
elif isinstance(detail, list) and detail:
|
||||
product_info.update(detail[0])
|
||||
|
||||
# 查询物料信息(返回数组或 dict)
|
||||
material = self._post(
|
||||
"/pickup-web/a/common/findppoductmaterialid",
|
||||
data={"productCode": biz_product_no},
|
||||
)
|
||||
mat = material[0] if isinstance(material, list) and material else material
|
||||
if isinstance(mat, dict):
|
||||
product_info["material"] = mat
|
||||
|
||||
return product_info
|
||||
|
||||
# ── 客户 ──────────────────────────────────────────────────
|
||||
|
||||
def query_customer(self, keyword: str) -> list[dict]:
|
||||
"""搜索客户名称,返回候选列表(data=customerId, valueName=短名, value=全名)。"""
|
||||
result = self._post(
|
||||
"/pickup-web/a/common/customerautocomplete",
|
||||
data={"query": keyword},
|
||||
)
|
||||
from log import trace
|
||||
trace(f"query_customer raw: {str(result)[:300]}")
|
||||
suggestions = result.get("suggestions", []) if isinstance(result, dict) else []
|
||||
parsed = []
|
||||
for s in suggestions:
|
||||
parsed.append({
|
||||
"customerId": s.get("data", ""),
|
||||
"customerName": s.get("valueName", s.get("value", "")),
|
||||
"customerFullName": s.get("value", ""),
|
||||
})
|
||||
return parsed
|
||||
|
||||
def select_customer(self, customer_id: str) -> dict:
|
||||
"""选择客户后获取客户详情。"""
|
||||
result = self._post(
|
||||
"/pickup-web/a/common/customerins",
|
||||
data={"customerId": customer_id},
|
||||
)
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
return {}
|
||||
|
||||
def find_sender_history(self, sender_no: str) -> dict:
|
||||
"""根据寄件人编号查询实名历史。"""
|
||||
result = self._post(
|
||||
"/pickup-web/a/postwaybillargs/findrealnamehistorybysender",
|
||||
data={"senderNo": sender_no},
|
||||
)
|
||||
if isinstance(result, dict):
|
||||
return result
|
||||
return {}
|
||||
|
||||
# ── 运单 ──────────────────────────────────────────────────
|
||||
|
||||
def query_waybill(self, waybill_no: str, ecommerce_no: str,
|
||||
biz_product_no: str, payment_mode: str,
|
||||
biz_product_id: str = "",
|
||||
post_blend: str = "0",
|
||||
lock_customer: str = "0") -> dict:
|
||||
"""查询运单信息。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/postwaybill/querypostwaybillmanager",
|
||||
data={
|
||||
"waybillNo": waybill_no,
|
||||
"ecommerceNo": ecommerce_no,
|
||||
"bizProductNo": biz_product_no,
|
||||
"postBlend": post_blend,
|
||||
"lockCustomer": lock_customer,
|
||||
"paymentMode": payment_mode,
|
||||
"bizProductId": biz_product_id,
|
||||
},
|
||||
)
|
||||
|
||||
# ── 地址 ──────────────────────────────────────────────────
|
||||
|
||||
def address_match_raw(self, receiver_addr: str) -> dict:
|
||||
"""根据地址文本查询区划代码(addressMatch API)。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/postwaybill/addressMatch",
|
||||
data={"receiverAddr": receiver_addr},
|
||||
)
|
||||
|
||||
def query_district(self, dist_code: str) -> dict:
|
||||
"""根据区划代码查询地址基本信息。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/postwaybilledit/addressbasicno",
|
||||
data={"dto": dist_code},
|
||||
)
|
||||
|
||||
def match_address(self, receiver_arrive_org_no: str,
|
||||
base_product_no: str,
|
||||
waybill_no: str,
|
||||
receiver_addr: str,
|
||||
sender_no: str = "",
|
||||
transfer_type: str = "3",
|
||||
contents_attribute: str = "3",
|
||||
single_seal_flag: str = "0",
|
||||
inter_flag: str = "0",
|
||||
reserved29: str = "0",
|
||||
safety_type: str = "0",
|
||||
io_type: str = "10") -> dict:
|
||||
"""匹配收件地址,获取寄达地网格等信息。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/common/findGridPrammer",
|
||||
data={
|
||||
"receiverArriveOrgNo": receiver_arrive_org_no,
|
||||
"baseProductNo": base_product_no,
|
||||
"transferType": transfer_type,
|
||||
"contentsAttribute": contents_attribute,
|
||||
"singleSealFlag": single_seal_flag,
|
||||
"interFlag": inter_flag,
|
||||
"reserved29": reserved29,
|
||||
"waybillNo": waybill_no,
|
||||
"receiverAddr": receiver_addr,
|
||||
"senderNo": sender_no,
|
||||
"safetyType": safety_type,
|
||||
"ioType": io_type,
|
||||
},
|
||||
)
|
||||
|
||||
# ── 校验 ──────────────────────────────────────────────────
|
||||
|
||||
def check_product_material(self, cargo_names: str, waybill_no: str,
|
||||
product_code: str, dist_code: str) -> dict:
|
||||
"""校验产品物料。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/common/checkppoductmaterialidnew",
|
||||
data={
|
||||
"cargoNames": cargo_names,
|
||||
"cargoOptions": "",
|
||||
"waybillNo": waybill_no,
|
||||
"productCode": product_code,
|
||||
"distCode": dist_code,
|
||||
},
|
||||
)
|
||||
|
||||
def check_reach_area(self, product_code: str, dist_code: str,
|
||||
cust_code: str, weight: str, waybill_no: str) -> dict:
|
||||
"""校验可达区域。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/common/reachareaverification",
|
||||
data={
|
||||
"productCode": product_code,
|
||||
"distCode": dist_code,
|
||||
"custCode": cust_code,
|
||||
"weight": weight,
|
||||
"waybillNo": waybill_no,
|
||||
},
|
||||
)
|
||||
|
||||
def check_important_address(self, waybill_no: str, addr: str) -> dict:
|
||||
"""检查重要地址。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/postwaybill/importantaddress",
|
||||
data={"waybillNo": waybill_no, "queryAddr": addr},
|
||||
)
|
||||
|
||||
def check_sensitive_customers(self, mobile_phones: str, waybill_no: str) -> dict:
|
||||
"""检查敏感客户。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/postwaybillargs/findsensitivecustomers",
|
||||
data={"mobilePhone": mobile_phones, "waybillNo": waybill_no},
|
||||
)
|
||||
|
||||
# ── 杂项查询 ──────────────────────────────────────────────
|
||||
|
||||
def query_ecommerce(self, query: str = "B") -> list:
|
||||
"""查询电商平台列表。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/postwaybill/queryecommerce",
|
||||
data={"query": query},
|
||||
)
|
||||
|
||||
def query_package_sort(self) -> list:
|
||||
"""查询包装分类。"""
|
||||
return self._post("/pickup-web/a/common/querypackagesort")
|
||||
|
||||
def query_deliver_date(self) -> dict:
|
||||
"""查询可投递日期。"""
|
||||
return self._post("/pickup-web/a/postwaybillargs/deliverdate")
|
||||
|
||||
def query_reason_temp_storage(self) -> dict:
|
||||
"""查询暂存原因列表。"""
|
||||
return self._post("/pickup-web/a/postwaybill/queryreasonfortemporarystorage")
|
||||
|
||||
def check_customer_warning(self) -> dict:
|
||||
"""检查客户预警。"""
|
||||
return self._post("/pickup-web/a/common/customerwarning")
|
||||
|
||||
def check_sheet_warning(self) -> dict:
|
||||
"""检查面单预警。"""
|
||||
return self._post("/pickup-web/a/postwaybill/sheetWarning")
|
||||
|
||||
# ── 提交 ──────────────────────────────────────────────────
|
||||
|
||||
def submit(self, form_data: dict[str, str]) -> dict:
|
||||
"""提交揽收表单。返回 {"resCode": "", "resMess": "收寄成功", ...}。"""
|
||||
return self._post(
|
||||
"/pickup-web/a/postwaybill/isnotetosave",
|
||||
data=form_data,
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
[project]
|
||||
name = "pickup-auto"
|
||||
version = "2.0.0"
|
||||
description = "Add your description here"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"openpyxl>=3.1.5",
|
||||
"requests>=2.31.0",
|
||||
"pyinstaller>=6.20.0",
|
||||
]
|
||||
Binary file not shown.
Generated
+783
@@ -0,0 +1,783 @@
|
||||
{
|
||||
"name": "pickup-token-bridge",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pickup-token-bridge",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"jsdom": "^24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@asamuzakjp/css-color": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||
"integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/css-calc": "^2.1.3",
|
||||
"@csstools/css-color-parser": "^3.0.9",
|
||||
"@csstools/css-parser-algorithms": "^3.0.4",
|
||||
"@csstools/css-tokenizer": "^3.0.3",
|
||||
"lru-cache": "^10.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/color-helpers": {
|
||||
"version": "5.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
|
||||
"integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-calc": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz",
|
||||
"integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-color-parser": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
|
||||
"integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@csstools/color-helpers": "^5.1.0",
|
||||
"@csstools/css-calc": "^2.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-parser-algorithms": "^3.0.5",
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-parser-algorithms": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
|
||||
"integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@csstools/css-tokenizer": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@csstools/css-tokenizer": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
|
||||
"integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/csstools"
|
||||
},
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/csstools"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/agent-base": {
|
||||
"version": "7.1.4",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz",
|
||||
"integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle": {
|
||||
"version": "4.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz",
|
||||
"integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@asamuzakjp/css-color": "^3.2.0",
|
||||
"rrweb-cssom": "^0.8.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/cssstyle/node_modules/rrweb-cssom": {
|
||||
"version": "0.8.0",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
|
||||
"integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz",
|
||||
"integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.3",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
||||
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ms": "^2.1.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/decimal.js": {
|
||||
"version": "10.6.0",
|
||||
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
|
||||
"integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/dunder-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"gopd": "^1.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
|
||||
"integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/es-define-property": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
|
||||
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-object-atoms": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
|
||||
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/es-set-tostringtag": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
|
||||
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"get-intrinsic": "^1.2.6",
|
||||
"has-tostringtag": "^1.0.2",
|
||||
"hasown": "^2.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
|
||||
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-intrinsic": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
|
||||
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"call-bind-apply-helpers": "^1.0.2",
|
||||
"es-define-property": "^1.0.1",
|
||||
"es-errors": "^1.3.0",
|
||||
"es-object-atoms": "^1.1.1",
|
||||
"function-bind": "^1.1.2",
|
||||
"get-proto": "^1.0.1",
|
||||
"gopd": "^1.2.0",
|
||||
"has-symbols": "^1.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"math-intrinsics": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-proto": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
|
||||
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dunder-proto": "^1.0.1",
|
||||
"es-object-atoms": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-symbols": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
|
||||
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/has-tostringtag": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
|
||||
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"has-symbols": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz",
|
||||
"integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html-encoding-sniffer": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
|
||||
"integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"whatwg-encoding": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/http-proxy-agent": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
|
||||
"integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
|
||||
"integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"agent-base": "^7.1.2",
|
||||
"debug": "4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 14"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-potential-custom-element-name": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
|
||||
"integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/jsdom": {
|
||||
"version": "24.1.3",
|
||||
"resolved": "https://registry.npmjs.org/jsdom/-/jsdom-24.1.3.tgz",
|
||||
"integrity": "sha512-MyL55p3Ut3cXbeBEG7Hcv0mVM8pp8PBNWxRqchZnSfAiES1v1mRnMeFfaHWIPULpwsYfvO+ZmMZz5tGCnjzDUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssstyle": "^4.0.1",
|
||||
"data-urls": "^5.0.0",
|
||||
"decimal.js": "^10.4.3",
|
||||
"form-data": "^4.0.0",
|
||||
"html-encoding-sniffer": "^4.0.0",
|
||||
"http-proxy-agent": "^7.0.2",
|
||||
"https-proxy-agent": "^7.0.5",
|
||||
"is-potential-custom-element-name": "^1.0.1",
|
||||
"nwsapi": "^2.2.12",
|
||||
"parse5": "^7.1.2",
|
||||
"rrweb-cssom": "^0.7.1",
|
||||
"saxes": "^6.0.0",
|
||||
"symbol-tree": "^3.2.4",
|
||||
"tough-cookie": "^4.1.4",
|
||||
"w3c-xmlserializer": "^5.0.0",
|
||||
"webidl-conversions": "^7.0.0",
|
||||
"whatwg-encoding": "^3.1.1",
|
||||
"whatwg-mimetype": "^4.0.0",
|
||||
"whatwg-url": "^14.0.0",
|
||||
"ws": "^8.18.0",
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"canvas": "^2.11.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"canvas": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "10.4.3",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/math-intrinsics": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
|
||||
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nwsapi": {
|
||||
"version": "2.2.23",
|
||||
"resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz",
|
||||
"integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/parse5": {
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
|
||||
"integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"entities": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/inikulin/parse5?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/psl": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
|
||||
"integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/lupomontero"
|
||||
}
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/querystringify": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
|
||||
"integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/requires-port": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rrweb-cssom": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.7.1.tgz",
|
||||
"integrity": "sha512-TrEMa7JGdVm0UThDJSx7ddw5nVm3UJS9o9CCIZ72B1vSyEZoziDqBYP3XIoi/12lKrJR8rE3jeFHMok2F/Mnsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/saxes": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz",
|
||||
"integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"xmlchars": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/symbol-tree": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||
"integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tough-cookie": {
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
|
||||
"integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"psl": "^1.1.33",
|
||||
"punycode": "^2.1.1",
|
||||
"universalify": "^0.2.0",
|
||||
"url-parse": "^1.5.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/tr46": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
|
||||
"integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"punycode": "^2.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/universalify": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
|
||||
"integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/url-parse": {
|
||||
"version": "1.5.10",
|
||||
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
|
||||
"integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"querystringify": "^2.1.1",
|
||||
"requires-port": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/w3c-xmlserializer": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
|
||||
"integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xml-name-validator": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/webidl-conversions": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
|
||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-encoding": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
|
||||
"integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
|
||||
"deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iconv-lite": "0.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-mimetype": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
|
||||
"integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/whatwg-url": {
|
||||
"version": "14.2.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
|
||||
"integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tr46": "^5.1.0",
|
||||
"webidl-conversions": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.20.1",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.20.1.tgz",
|
||||
"integrity": "sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xml-name-validator": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||
"integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "pickup-token-bridge",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"jsdom": "^24.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
/**
|
||||
* Token Bridge — HTTP 服务器模式。
|
||||
* 在 jsdom 中运行 $_ts 安全框架,计算 c93l5Fya token。
|
||||
*
|
||||
* 接口:
|
||||
* GET /ping → {"ok":true}
|
||||
* POST /init → body: {html, scripts, baseUrl} → {"ok":true, "nsd":...}
|
||||
* POST /token → body: {method, path} → {"token":"..."}
|
||||
*/
|
||||
|
||||
const { JSDOM } = require('jsdom');
|
||||
const http = require('http');
|
||||
|
||||
let dom = null;
|
||||
let window = null;
|
||||
let _cdLen = 0;
|
||||
let _nsd = null;
|
||||
|
||||
function initialize(html, scripts, baseUrl) {
|
||||
dom = new JSDOM(html, {
|
||||
url: baseUrl,
|
||||
runScripts: 'dangerously',
|
||||
pretendToBeVisual: true,
|
||||
});
|
||||
window = dom.window;
|
||||
|
||||
_cdLen = (window.$_ts && window.$_ts.cd) ? window.$_ts.cd.length : 0;
|
||||
_nsd = (window.$_ts && window.$_ts.nsd) ? window.$_ts.nsd : null;
|
||||
|
||||
// 拦截 XMLHttpRequest.prototype.open —— security JS 会包装它
|
||||
const OriginalXHR = window.XMLHttpRequest;
|
||||
OriginalXHR.prototype.open = function (method, url) {
|
||||
this.__ts_method = method;
|
||||
this.__ts_url = url;
|
||||
};
|
||||
|
||||
// 注入 security JS
|
||||
for (const script of scripts) {
|
||||
try {
|
||||
const el = window.document.createElement('script');
|
||||
el.textContent = script.content;
|
||||
window.document.head.appendChild(el);
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
// 验证
|
||||
const testXhr = new OriginalXHR();
|
||||
testXhr.open('GET', '/_ts_test_');
|
||||
const captured = testXhr.__ts_url || '';
|
||||
|
||||
if (!captured.includes('c93l5Fya=')) {
|
||||
throw new Error(
|
||||
`$_ts init failed: cd_len=${_cdLen}, nsd=${_nsd}, captured=${captured.substring(0, 80)}`
|
||||
);
|
||||
}
|
||||
|
||||
return { ok: true, nsd: _nsd };
|
||||
}
|
||||
|
||||
function computeToken(method, path) {
|
||||
if (!window) throw new Error('not initialized');
|
||||
process.stderr.write(JSON.stringify({trace: 'computeToken', method, path}) + '\n');
|
||||
const OriginalXHR = window.XMLHttpRequest;
|
||||
process.stderr.write(JSON.stringify({trace: 'XHR class', hasXHR: !!OriginalXHR}) + '\n');
|
||||
const xhr = new OriginalXHR();
|
||||
process.stderr.write(JSON.stringify({trace: 'xhr created'}) + '\n');
|
||||
xhr.open(method || 'GET', path);
|
||||
process.stderr.write(JSON.stringify({trace: 'open called', url: (xhr.__ts_url||'').substring(0,100)}) + '\n');
|
||||
const url = xhr.__ts_url || '';
|
||||
const idx = url.indexOf('c93l5Fya=');
|
||||
if (idx < 0) throw new Error(`Token not found for ${path}, url=${url.substring(0,80)}`);
|
||||
return url.substring(idx + 'c93l5Fya='.length);
|
||||
}
|
||||
|
||||
// ─── HTTP Server ────────────────────────────────────────────
|
||||
|
||||
function readBody(req) {
|
||||
return new Promise((resolve) => {
|
||||
let data = '';
|
||||
req.on('data', c => data += c);
|
||||
req.on('end', () => resolve(data));
|
||||
});
|
||||
}
|
||||
|
||||
function json(res, data, status = 200) {
|
||||
res.writeHead(status, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data));
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
try {
|
||||
if (req.method === 'GET' && req.url === '/ping') {
|
||||
json(res, { ok: true });
|
||||
} else if (req.method === 'POST' && req.url === '/init') {
|
||||
const body = await readBody(req);
|
||||
const { html, scripts, baseUrl } = JSON.parse(body);
|
||||
const result = initialize(html, scripts, baseUrl);
|
||||
json(res, result);
|
||||
} else if (req.method === 'POST' && req.url === '/token') {
|
||||
const body = await readBody(req);
|
||||
const { method, path } = JSON.parse(body);
|
||||
const token = computeToken(method, path);
|
||||
json(res, { token });
|
||||
} else {
|
||||
json(res, { error: 'not found' }, 404);
|
||||
}
|
||||
} catch (e) {
|
||||
process.stderr.write(JSON.stringify({ error: e.message }) + '\n');
|
||||
json(res, { error: e.message }, 500);
|
||||
}
|
||||
});
|
||||
|
||||
const port = process.argv[2] || 0;
|
||||
server.listen(port, '127.0.0.1', () => {
|
||||
const addr = server.address();
|
||||
process.stderr.write(JSON.stringify({ ready: true, port: addr.port }) + '\n');
|
||||
});
|
||||
@@ -0,0 +1,286 @@
|
||||
version = 1
|
||||
revision = 3
|
||||
requires-python = ">=3.10"
|
||||
|
||||
[[package]]
|
||||
name = "altgraph"
|
||||
version = "0.17.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/7e/f8/97fdf103f38fed6792a1601dbc16cc8aac56e7459a9fff08c812d8ae177a/altgraph-0.17.5.tar.gz", hash = "sha256:c87b395dd12fabde9c99573a9749d67da8d29ef9de0125c7f536699b4a9bc9e7", size = 48428, upload-time = "2025-11-21T20:35:50.583Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/ba/000a1996d4308bc65120167c21241a3b205464a2e0b58deda26ae8ac21d1/altgraph-0.17.5-py2.py3-none-any.whl", hash = "sha256:f3a22400bce1b0c701683820ac4f3b159cd301acab067c51c653e06961600597", size = 21228, upload-time = "2025-11-21T20:35:49.444Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2026.4.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/25/ee/6caf7a40c36a1220410afe15a1cc64993a1f864871f698c0f93acb72842a/certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580", size = 137077, upload-time = "2026-04-22T11:26:11.191Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/22/30/7cd8fdcdfbc5b869528b079bfb76dcdf6056b1a2097a662e5e8c04f42965/certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a", size = 135707, upload-time = "2026-04-22T11:26:09.372Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/a1/67fe25fac3c7642725500a3f6cfe5821ad557c3abb11c9d20d12c7008d3e/charset_normalizer-3.4.7.tar.gz", hash = "sha256:ae89db9e5f98a11a4bf50407d4363e7b09b31e55bc117b4f7d80aab97ba009e5", size = 144271, upload-time = "2026-04-02T09:28:39.342Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/26/08/0f303cb0b529e456bb116f2d50565a482694fbb94340bf56d44677e7ed03/charset_normalizer-3.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cdd68a1fb318e290a2077696b7eb7a21a49163c455979c639bf5a5dcdc46617d", size = 315182, upload-time = "2026-04-02T09:25:40.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/47/b192933e94b546f1b1fe4df9cc1f84fcdbf2359f8d1081d46dd029b50207/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e17b8d5d6a8c47c85e68ca8379def1303fd360c3e22093a807cd34a71cd082b8", size = 209329, upload-time = "2026-04-02T09:25:42.354Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/b4/01fa81c5ca6141024d89a8fc15968002b71da7f825dd14113207113fabbd/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:511ef87c8aec0783e08ac18565a16d435372bc1ac25a91e6ac7f5ef2b0bff790", size = 231230, upload-time = "2026-04-02T09:25:44.281Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/f7/7b991776844dfa058017e600e6e55ff01984a063290ca5622c0b63162f68/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:007d05ec7321d12a40227aae9e2bc6dca73f3cb21058999a1df9e193555a9dcc", size = 225890, upload-time = "2026-04-02T09:25:45.475Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/e7/bed0024a0f4ab0c8a9c64d4445f39b30c99bd1acd228291959e3de664247/charset_normalizer-3.4.7-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf29836da5119f3c8a8a70667b0ef5fdca3bb12f80fd06487cfa575b3909b393", size = 216930, upload-time = "2026-04-02T09:25:46.58Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e2/ab/b18f0ab31cdd7b3ddb8bb76c4a414aeb8160c9810fdf1bc62f269a539d87/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:12d8baf840cc7889b37c7c770f478adea7adce3dcb3944d02ec87508e2dcf153", size = 202109, upload-time = "2026-04-02T09:25:48.031Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/82/e5/7e9440768a06dfb3075936490cb82dbf0ee20a133bf0dd8551fa096914ec/charset_normalizer-3.4.7-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d560742f3c0d62afaccf9f41fe485ed69bd7661a241f86a3ef0f0fb8b1a397af", size = 214684, upload-time = "2026-04-02T09:25:49.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/94/8c61d8da9f062fdf457c80acfa25060ec22bf1d34bbeaca4350f13bcfd07/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b14b2d9dac08e28bb8046a1a0434b1750eb221c8f5b87a68f4fa11a6f97b5e34", size = 212785, upload-time = "2026-04-02T09:25:50.671Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/cd/6e9889c648e72c0ab2e5967528bb83508f354d706637bc7097190c874e13/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:bc17a677b21b3502a21f66a8cc64f5bfad4df8a0b8434d661666f8ce90ac3af1", size = 203055, upload-time = "2026-04-02T09:25:51.802Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/92/2e/7a951d6a08aefb7eb8e1b54cdfb580b1365afdd9dd484dc4bee9e5d8f258/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:750e02e074872a3fad7f233b47734166440af3cdea0add3e95163110816d6752", size = 232502, upload-time = "2026-04-02T09:25:53.388Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/d5/abcf2d83bf8e0a1286df55cd0dc1d49af0da4282aa77e986df343e7de124/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:4e5163c14bffd570ef2affbfdd77bba66383890797df43dc8b4cc7d6f500bf53", size = 214295, upload-time = "2026-04-02T09:25:54.765Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/3a/7d4cd7ed54be99973a0dc176032cba5cb1f258082c31fa6df35cff46acfc/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6ed74185b2db44f41ef35fd1617c5888e59792da9bbc9190d6c7300617182616", size = 227145, upload-time = "2026-04-02T09:25:55.904Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/98/3a45bf8247889cf28262ebd3d0872edff11565b2a1e3064ccb132db3fbb0/charset_normalizer-3.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:94e1885b270625a9a828c9793b4d52a64445299baa1fea5a173bf1d3dd9a1a5a", size = 218884, upload-time = "2026-04-02T09:25:57.074Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ad/80/2e8b7f8915ed5c9ef13aa828d82738e33888c485b65ebf744d615040c7ea/charset_normalizer-3.4.7-cp310-cp310-win32.whl", hash = "sha256:6785f414ae0f3c733c437e0f3929197934f526d19dfaa75e18fdb4f94c6fb374", size = 148343, upload-time = "2026-04-02T09:25:58.199Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/1b/3b8c8c77184af465ee9ad88b5aea46ea6b2e1f7b9dc9502891e37af21e30/charset_normalizer-3.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:6696b7688f54f5af4462118f0bfa7c1621eeb87154f77fa04b9295ce7a8f2943", size = 159174, upload-time = "2026-04-02T09:25:59.322Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c1/feb40dca40dbb21e0a908801782d9288c64fc8d8e562c2098e9994c8c21b/charset_normalizer-3.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:66671f93accb62ed07da56613636f3641f1a12c13046ce91ffc923721f23c008", size = 147805, upload-time = "2026-04-02T09:26:00.756Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/d7/b5b7020a0565c2e9fa8c09f4b5fa6232feb326b8c20081ccded47ea368fd/charset_normalizer-3.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7641bb8895e77f921102f72833904dcd9901df5d6d72a2ab8f31d04b7e51e4e7", size = 309705, upload-time = "2026-04-02T09:26:02.191Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/53/58c29116c340e5456724ecd2fff4196d236b98f3da97b404bc5e51ac3493/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:202389074300232baeb53ae2569a60901f7efadd4245cf3a3bf0617d60b439d7", size = 206419, upload-time = "2026-04-02T09:26:03.583Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/02/e8146dc6591a37a00e5144c63f29fb7c97a734ea8a111190783c0e60ab63/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:30b8d1d8c52a48c2c5690e152c169b673487a2a58de1ec7393196753063fcd5e", size = 227901, upload-time = "2026-04-02T09:26:04.738Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/73/77486c4cd58f1267bf17db420e930c9afa1b3be3fe8c8b8ebbebc9624359/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:532bc9bf33a68613fd7d65e4b1c71a6a38d7d42604ecf239c77392e9b4e8998c", size = 222742, upload-time = "2026-04-02T09:26:06.36Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a1/fa/f74eb381a7d94ded44739e9d94de18dc5edc9c17fb8c11f0a6890696c0a9/charset_normalizer-3.4.7-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2fe249cb4651fd12605b7288b24751d8bfd46d35f12a20b1ba33dea122e690df", size = 214061, upload-time = "2026-04-02T09:26:08.347Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/92/42bd3cefcf7687253fb86694b45f37b733c97f59af3724f356fa92b8c344/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:65bcd23054beab4d166035cabbc868a09c1a49d1efe458fe8e4361215df40265", size = 199239, upload-time = "2026-04-02T09:26:09.823Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/3d/069e7184e2aa3b3cddc700e3dd267413dc259854adc3380421c805c6a17d/charset_normalizer-3.4.7-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:08e721811161356f97b4059a9ba7bafb23ea5ee2255402c42881c214e173c6b4", size = 210173, upload-time = "2026-04-02T09:26:10.953Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/51/9d56feb5f2e7074c46f93e0ebdbe61f0848ee246e2f0d89f8e20b89ebb8f/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e060d01aec0a910bdccb8be71faf34e7799ce36950f8294c8bf612cba65a2c9e", size = 209841, upload-time = "2026-04-02T09:26:12.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/59/893d8f99cc4c837dda1fe2f1139079703deb9f321aabcb032355de13b6c7/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:38c0109396c4cfc574d502df99742a45c72c08eff0a36158b6f04000043dbf38", size = 200304, upload-time = "2026-04-02T09:26:13.711Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/1d/ee6f3be3464247578d1ed5c46de545ccc3d3ff933695395c402c21fa6b77/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:1c2a768fdd44ee4a9339a9b0b130049139b8ce3c01d2ce09f67f5a68048d477c", size = 229455, upload-time = "2026-04-02T09:26:14.941Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/bb/8fb0a946296ea96a488928bdce8ef99023998c48e4713af533e9bb98ef07/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:1a87ca9d5df6fe460483d9a5bbf2b18f620cbed41b432e2bddb686228282d10b", size = 210036, upload-time = "2026-04-02T09:26:16.478Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9a/bc/015b2387f913749f82afd4fcba07846d05b6d784dd16123cb66860e0237d/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:d635aab80466bc95771bb78d5370e74d36d1fe31467b6b29b8b57b2a3cd7d22c", size = 224739, upload-time = "2026-04-02T09:26:17.751Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/17/ab/63133691f56baae417493cba6b7c641571a2130eb7bceba6773367ab9ec5/charset_normalizer-3.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ae196f021b5e7c78e918242d217db021ed2a6ace2bc6ae94c0fc596221c7f58d", size = 216277, upload-time = "2026-04-02T09:26:18.981Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/06/6d/3be70e827977f20db77c12a97e6a9f973631a45b8d186c084527e53e77a4/charset_normalizer-3.4.7-cp311-cp311-win32.whl", hash = "sha256:adb2597b428735679446b46c8badf467b4ca5f5056aae4d51a19f9570301b1ad", size = 147819, upload-time = "2026-04-02T09:26:20.295Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/20/d9/5f67790f06b735d7c7637171bbfd89882ad67201891b7275e51116ed8207/charset_normalizer-3.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:8e385e4267ab76874ae30db04c627faaaf0b509e1ccc11a95b3fc3e83f855c00", size = 159281, upload-time = "2026-04-02T09:26:21.74Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/83/6413f36c5a34afead88ce6f66684d943d91f233d76dd083798f9602b75ae/charset_normalizer-3.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:d4a48e5b3c2a489fae013b7589308a40146ee081f6f509e047e0e096084ceca1", size = 147843, upload-time = "2026-04-02T09:26:22.901Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/eb/4fc8d0a7110eb5fc9cc161723a34a8a6c200ce3b4fbf681bc86feee22308/charset_normalizer-3.4.7-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:eca9705049ad3c7345d574e3510665cb2cf844c2f2dcfe675332677f081cbd46", size = 311328, upload-time = "2026-04-02T09:26:24.331Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/e3/0fadc706008ac9d7b9b5be6dc767c05f9d3e5df51744ce4cc9605de7b9f4/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6178f72c5508bfc5fd446a5905e698c6212932f25bcdd4b47a757a50605a90e2", size = 208061, upload-time = "2026-04-02T09:26:25.568Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/42/f0/3dd1045c47f4a4604df85ec18ad093912ae1344ac706993aff91d38773a2/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1421b502d83040e6d7fb2fb18dff63957f720da3d77b2fbd3187ceb63755d7b", size = 229031, upload-time = "2026-04-02T09:26:26.865Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/dc/67/675a46eb016118a2fbde5a277a5d15f4f69d5f3f5f338e5ee2f8948fcf43/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:edac0f1ab77644605be2cbba52e6b7f630731fc42b34cb0f634be1a6eface56a", size = 225239, upload-time = "2026-04-02T09:26:28.044Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4b/f8/d0118a2f5f23b02cd166fa385c60f9b0d4f9194f574e2b31cef350ad7223/charset_normalizer-3.4.7-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5649fd1c7bade02f320a462fdefd0b4bd3ce036065836d4f42e0de958038e116", size = 216589, upload-time = "2026-04-02T09:26:29.239Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b1/f1/6d2b0b261b6c4ceef0fcb0d17a01cc5bc53586c2d4796fa04b5c540bc13d/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:203104ed3e428044fd943bc4bf45fa73c0730391f9621e37fe39ecf477b128cb", size = 202733, upload-time = "2026-04-02T09:26:30.5Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/c0/7b1f943f7e87cc3db9626ba17807d042c38645f0a1d4415c7a14afb5591f/charset_normalizer-3.4.7-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:298930cec56029e05497a76988377cbd7457ba864beeea92ad7e844fe74cd1f1", size = 212652, upload-time = "2026-04-02T09:26:31.709Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/38/dd/5a9ab159fe45c6e72079398f277b7d2b523e7f716acc489726115a910097/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:708838739abf24b2ceb208d0e22403dd018faeef86ddac04319a62ae884c4f15", size = 211229, upload-time = "2026-04-02T09:26:33.282Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d5/ff/531a1cad5ca855d1c1a8b69cb71abfd6d85c0291580146fda7c82857caa1/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f7eb884681e3938906ed0434f20c63046eacd0111c4ba96f27b76084cd679f5", size = 203552, upload-time = "2026-04-02T09:26:34.845Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/4c/a5fb52d528a8ca41f7598cb619409ece30a169fbdf9cdce592e53b46c3a6/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4dc1e73c36828f982bfe79fadf5919923f8a6f4df2860804db9a98c48824ce8d", size = 230806, upload-time = "2026-04-02T09:26:36.152Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/7a/071feed8124111a32b316b33ae4de83d36923039ef8cf48120266844285b/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:aed52fea0513bac0ccde438c188c8a471c4e0f457c2dd20cdbf6ea7a450046c7", size = 212316, upload-time = "2026-04-02T09:26:37.672Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/35/f7dba3994312d7ba508e041eaac39a36b120f32d4c8662b8814dab876431/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:fea24543955a6a729c45a73fe90e08c743f0b3334bbf3201e6c4bc1b0c7fa464", size = 227274, upload-time = "2026-04-02T09:26:38.93Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/2d/a572df5c9204ab7688ec1edc895a73ebded3b023bb07364710b05dd1c9be/charset_normalizer-3.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:bb6d88045545b26da47aa879dd4a89a71d1dce0f0e549b1abcb31dfe4a8eac49", size = 218468, upload-time = "2026-04-02T09:26:40.17Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/eb/890922a8b03a568ca2f336c36585a4713c55d4d67bf0f0c78924be6315ca/charset_normalizer-3.4.7-cp312-cp312-win32.whl", hash = "sha256:2257141f39fe65a3fdf38aeccae4b953e5f3b3324f4ff0daf9f15b8518666a2c", size = 148460, upload-time = "2026-04-02T09:26:41.416Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/d9/0e7dffa06c5ab081f75b1b786f0aefc88365825dfcd0ac544bdb7b2b6853/charset_normalizer-3.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:5ed6ab538499c8644b8a3e18debabcd7ce684f3fa91cf867521a7a0279cab2d6", size = 159330, upload-time = "2026-04-02T09:26:42.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/5d/481bcc2a7c88ea6b0878c299547843b2521ccbc40980cb406267088bc701/charset_normalizer-3.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:56be790f86bfb2c98fb742ce566dfb4816e5a83384616ab59c49e0604d49c51d", size = 147828, upload-time = "2026-04-02T09:26:44.075Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/3b/66777e39d3ae1ddc77ee606be4ec6d8cbd4c801f65e5a1b6f2b11b8346dd/charset_normalizer-3.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f496c9c3cc02230093d8330875c4c3cdfc3b73612a5fd921c65d39cbcef08063", size = 309627, upload-time = "2026-04-02T09:26:45.198Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/4e/b7f84e617b4854ade48a1b7915c8ccfadeba444d2a18c291f696e37f0d3b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ea948db76d31190bf08bd371623927ee1339d5f2a0b4b1b4a4439a65298703c", size = 207008, upload-time = "2026-04-02T09:26:46.824Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c4/bb/ec73c0257c9e11b268f018f068f5d00aa0ef8c8b09f7753ebd5f2880e248/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a277ab8928b9f299723bc1a2dabb1265911b1a76341f90a510368ca44ad9ab66", size = 228303, upload-time = "2026-04-02T09:26:48.397Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/fb/32d1f5033484494619f701e719429c69b766bfc4dbc61aa9e9c8c166528b/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3bec022aec2c514d9cf199522a802bd007cd588ab17ab2525f20f9c34d067c18", size = 224282, upload-time = "2026-04-02T09:26:49.684Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/07/330e3a0dda4c404d6da83b327270906e9654a24f6c546dc886a0eb0ffb23/charset_normalizer-3.4.7-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e044c39e41b92c845bc815e5ae4230804e8e7bc29e399b0437d64222d92809dd", size = 215595, upload-time = "2026-04-02T09:26:50.915Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e3/7c/fc890655786e423f02556e0216d4b8c6bcb6bdfa890160dc66bf52dee468/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:f495a1652cf3fbab2eb0639776dad966c2fb874d79d87ca07f9d5f059b8bd215", size = 201986, upload-time = "2026-04-02T09:26:52.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/97/bfb18b3db2aed3b90cf54dc292ad79fdd5ad65c4eae454099475cbeadd0d/charset_normalizer-3.4.7-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e712b419df8ba5e42b226c510472b37bd57b38e897d3eca5e8cfd410a29fa859", size = 211711, upload-time = "2026-04-02T09:26:53.49Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6f/a5/a581c13798546a7fd557c82614a5c65a13df2157e9ad6373166d2a3e645d/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7804338df6fcc08105c7745f1502ba68d900f45fd770d5bdd5288ddccb8a42d8", size = 210036, upload-time = "2026-04-02T09:26:54.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/bf/b3ab5bcb478e4193d517644b0fb2bf5497fbceeaa7a1bc0f4d5b50953861/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:481551899c856c704d58119b5025793fa6730adda3571971af568f66d2424bb5", size = 202998, upload-time = "2026-04-02T09:26:56.303Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e7/4e/23efd79b65d314fa320ec6017b4b5834d5c12a58ba4610aa353af2e2f577/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f59099f9b66f0d7145115e6f80dd8b1d847176df89b234a5a6b3f00437aa0832", size = 230056, upload-time = "2026-04-02T09:26:57.554Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/9f/1e1941bc3f0e01df116e68dc37a55c4d249df5e6fa77f008841aef68264f/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:f59ad4c0e8f6bba240a9bb85504faa1ab438237199d4cce5f622761507b8f6a6", size = 211537, upload-time = "2026-04-02T09:26:58.843Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/80/0f/088cbb3020d44428964a6c97fe1edfb1b9550396bf6d278330281e8b709c/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:3dedcc22d73ec993f42055eff4fcfed9318d1eeb9a6606c55892a26964964e48", size = 226176, upload-time = "2026-04-02T09:27:00.437Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/9f/130394f9bbe06f4f63e22641d32fc9b202b7e251c9aef4db044324dac493/charset_normalizer-3.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:64f02c6841d7d83f832cd97ccf8eb8a906d06eb95d5276069175c696b024b60a", size = 217723, upload-time = "2026-04-02T09:27:02.021Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/55/c469897448a06e49f8fa03f6caae97074fde823f432a98f979cc42b90e69/charset_normalizer-3.4.7-cp313-cp313-win32.whl", hash = "sha256:4042d5c8f957e15221d423ba781e85d553722fc4113f523f2feb7b188cc34c5e", size = 148085, upload-time = "2026-04-02T09:27:03.192Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/78/1b74c5bbb3f99b77a1715c91b3e0b5bdb6fe302d95ace4f5b1bec37b0167/charset_normalizer-3.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:3946fa46a0cf3e4c8cb1cc52f56bb536310d34f25f01ca9b6c16afa767dab110", size = 158819, upload-time = "2026-04-02T09:27:04.454Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/68/86/46bd42279d323deb8687c4a5a811fd548cb7d1de10cf6535d099877a9a9f/charset_normalizer-3.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:80d04837f55fc81da168b98de4f4b797ef007fc8a79ab71c6ec9bc4dd662b15b", size = 147915, upload-time = "2026-04-02T09:27:05.971Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/97/c8/c67cb8c70e19ef1960b97b22ed2a1567711de46c4ddf19799923adc836c2/charset_normalizer-3.4.7-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c36c333c39be2dbca264d7803333c896ab8fa7d4d6f0ab7edb7dfd7aea6e98c0", size = 309234, upload-time = "2026-04-02T09:27:07.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/85/c091fdee33f20de70d6c8b522743b6f831a2f1cd3ff86de4c6a827c48a76/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1c2aed2e5e41f24ea8ef1590b8e848a79b56f3a5564a65ceec43c9d692dc7d8a", size = 208042, upload-time = "2026-04-02T09:27:08.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/1c/ab2ce611b984d2fd5d86a5a8a19c1ae26acac6bad967da4967562c75114d/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54523e136b8948060c0fa0bc7b1b50c32c186f2fceee897a495406bb6e311d2b", size = 228706, upload-time = "2026-04-02T09:27:09.951Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a8/29/2b1d2cb00bf085f59d29eb773ce58ec2d325430f8c216804a0a5cd83cbca/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:715479b9a2802ecac752a3b0efa2b0b60285cf962ee38414211abdfccc233b41", size = 224727, upload-time = "2026-04-02T09:27:11.175Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/5c/032c2d5a07fe4d4855fea851209cca2b6f03ebeb6d4e3afdb3358386a684/charset_normalizer-3.4.7-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bd6c2a1c7573c64738d716488d2cdd3c00e340e4835707d8fdb8dc1a66ef164e", size = 215882, upload-time = "2026-04-02T09:27:12.446Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/c2/356065d5a8b78ed04499cae5f339f091946a6a74f91e03476c33f0ab7100/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:c45e9440fb78f8ddabcf714b68f936737a121355bf59f3907f4e17721b9d1aae", size = 200860, upload-time = "2026-04-02T09:27:13.721Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/cd/a32a84217ced5039f53b29f460962abb2d4420def55afabe45b1c3c7483d/charset_normalizer-3.4.7-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3534e7dcbdcf757da6b85a0bbf5b6868786d5982dd959b065e65481644817a18", size = 211564, upload-time = "2026-04-02T09:27:15.272Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/86/58e6f13ce26cc3b8f4a36b94a0f22ae2f00a72534520f4ae6857c4b81f89/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e8ac484bf18ce6975760921bb6148041faa8fef0547200386ea0b52b5d27bf7b", size = 211276, upload-time = "2026-04-02T09:27:16.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8f/fe/d17c32dc72e17e155e06883efa84514ca375f8a528ba2546bee73fc4df81/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a5fe03b42827c13cdccd08e6c0247b6a6d4b5e3cdc53fd1749f5896adcdc2356", size = 201238, upload-time = "2026-04-02T09:27:18.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/29/f33daa50b06525a237451cdb6c69da366c381a3dadcd833fa5676bc468b3/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:2d6eb928e13016cea4f1f21d1e10c1cebd5a421bc57ddf5b1142ae3f86824fab", size = 230189, upload-time = "2026-04-02T09:27:19.445Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/6e/52c84015394a6a0bdcd435210a7e944c5f94ea1055f5cc5d56c5fe368e7b/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e74327fb75de8986940def6e8dee4f127cc9752bee7355bb323cc5b2659b6d46", size = 211352, upload-time = "2026-04-02T09:27:20.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8c/d7/4353be581b373033fb9198bf1da3cf8f09c1082561e8e922aa7b39bf9fe8/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d6038d37043bced98a66e68d3aa2b6a35505dc01328cd65217cefe82f25def44", size = 227024, upload-time = "2026-04-02T09:27:22.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/45/99d18aa925bd1740098ccd3060e238e21115fffbfdcb8f3ece837d0ace6c/charset_normalizer-3.4.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7579e913a5339fb8fa133f6bbcfd8e6749696206cf05acdbdca71a1b436d8e72", size = 217869, upload-time = "2026-04-02T09:27:23.486Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/05/5ee478aa53f4bb7996482153d4bfe1b89e0f087f0ab6b294fcf92d595873/charset_normalizer-3.4.7-cp314-cp314-win32.whl", hash = "sha256:5b77459df20e08151cd6f8b9ef8ef1f961ef73d85c21a555c7eed5b79410ec10", size = 148541, upload-time = "2026-04-02T09:27:25.146Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/77/72dcb0921b2ce86420b2d79d454c7022bf5be40202a2a07906b9f2a35c97/charset_normalizer-3.4.7-cp314-cp314-win_amd64.whl", hash = "sha256:92a0a01ead5e668468e952e4238cccd7c537364eb7d851ab144ab6627dbbe12f", size = 159634, upload-time = "2026-04-02T09:27:26.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/a3/c2369911cd72f02386e4e340770f6e158c7980267da16af8f668217abaa0/charset_normalizer-3.4.7-cp314-cp314-win_arm64.whl", hash = "sha256:67f6279d125ca0046a7fd386d01b311c6363844deac3e5b069b514ba3e63c246", size = 148384, upload-time = "2026-04-02T09:27:28.271Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/09/7e8a7f73d24dba1f0035fbbf014d2c36828fc1bf9c88f84093e57d315935/charset_normalizer-3.4.7-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:effc3f449787117233702311a1b7d8f59cba9ced946ba727bdc329ec69028e24", size = 330133, upload-time = "2026-04-02T09:27:29.474Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/da/96975ddb11f8e977f706f45cddd8540fd8242f71ecdb5d18a80723dcf62c/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbccdc05410c9ee21bbf16a35f4c1d16123dcdeb8a1d38f33654fa21d0234f79", size = 216257, upload-time = "2026-04-02T09:27:30.793Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/e8/1d63bf8ef2d388e95c64b2098f45f84758f6d102a087552da1485912637b/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:733784b6d6def852c814bce5f318d25da2ee65dd4839a0718641c696e09a2960", size = 234851, upload-time = "2026-04-02T09:27:32.44Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/40/e5ff04233e70da2681fa43969ad6f66ca5611d7e669be0246c4c7aaf6dc8/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a89c23ef8d2c6b27fd200a42aa4ac72786e7c60d40efdc76e6011260b6e949c4", size = 233393, upload-time = "2026-04-02T09:27:34.03Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/c1/06c6c49d5a5450f76899992f1ee40b41d076aee9279b49cf9974d2f313d5/charset_normalizer-3.4.7-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c114670c45346afedc0d947faf3c7f701051d2518b943679c8ff88befe14f8e", size = 223251, upload-time = "2026-04-02T09:27:35.369Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2b/9f/f2ff16fb050946169e3e1f82134d107e5d4ae72647ec8a1b1446c148480f/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:a180c5e59792af262bf263b21a3c49353f25945d8d9f70628e73de370d55e1e1", size = 206609, upload-time = "2026-04-02T09:27:36.661Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/69/d5/a527c0cd8d64d2eab7459784fb4169a0ac76e5a6fc5237337982fd61347e/charset_normalizer-3.4.7-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:3c9a494bc5ec77d43cea229c4f6db1e4d8fe7e1bbffa8b6f0f0032430ff8ab44", size = 220014, upload-time = "2026-04-02T09:27:38.019Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/80/8a7b8104a3e203074dc9aa2c613d4b726c0e136bad1cc734594b02867972/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d828b6667a32a728a1ad1d93957cdf37489c57b97ae6c4de2860fa749b8fc1e", size = 218979, upload-time = "2026-04-02T09:27:39.37Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/9a/b759b503d507f375b2b5c153e4d2ee0a75aa215b7f2489cf314f4541f2c0/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:cf1493cd8607bec4d8a7b9b004e699fcf8f9103a9284cc94962cb73d20f9d4a3", size = 209238, upload-time = "2026-04-02T09:27:40.722Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/4e/0f3f5d47b86bdb79256e7290b26ac847a2832d9a4033f7eb2cd4bcf4bb5b/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0c96c3b819b5c3e9e165495db84d41914d6894d55181d2d108cc1a69bfc9cce0", size = 236110, upload-time = "2026-04-02T09:27:42.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/23/bce28734eb3ed2c91dcf93abeb8a5cf393a7b2749725030bb630e554fdd8/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:752a45dc4a6934060b3b0dab47e04edc3326575f82be64bc4fc293914566503e", size = 219824, upload-time = "2026-04-02T09:27:43.924Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/6f/6e897c6984cc4d41af319b077f2f600fc8214eb2fe2d6bcb79141b882400/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:8778f0c7a52e56f75d12dae53ae320fae900a8b9b4164b981b9c5ce059cd1fcb", size = 233103, upload-time = "2026-04-02T09:27:45.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/22/ef7bd0fe480a0ae9b656189ec00744b60933f68b4f42a7bb06589f6f576a/charset_normalizer-3.4.7-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ce3412fbe1e31eb81ea42f4169ed94861c56e643189e1e75f0041f3fe7020abe", size = 225194, upload-time = "2026-04-02T09:27:46.706Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/a7/0e0ab3e0b5bc1219bd80a6a0d4d72ca74d9250cb2382b7c699c147e06017/charset_normalizer-3.4.7-cp314-cp314t-win32.whl", hash = "sha256:c03a41a8784091e67a39648f70c5f97b5b6a37f216896d44d2cdcb82615339a0", size = 159827, upload-time = "2026-04-02T09:27:48.053Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7a/1d/29d32e0fb40864b1f878c7f5a0b343ae676c6e2b271a2d55cc3a152391da/charset_normalizer-3.4.7-cp314-cp314t-win_amd64.whl", hash = "sha256:03853ed82eeebbce3c2abfdbc98c96dc205f32a79627688ac9a27370ea61a49c", size = 174168, upload-time = "2026-04-02T09:27:49.795Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/32/d92444ad05c7a6e41fb2036749777c163baf7a0301a040cb672d6b2b1ae9/charset_normalizer-3.4.7-cp314-cp314t-win_arm64.whl", hash = "sha256:c35abb8bfff0185efac5878da64c45dafd2b37fb0383add1be155a763c1f083d", size = 153018, upload-time = "2026-04-02T09:27:51.116Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "et-xmlfile"
|
||||
version = "2.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "macholib"
|
||||
version = "1.16.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/10/2f/97589876ea967487978071c9042518d28b958d87b17dceb7cdc1d881f963/macholib-1.16.4.tar.gz", hash = "sha256:f408c93ab2e995cd2c46e34fe328b130404be143469e41bc366c807448979362", size = 59427, upload-time = "2025-11-22T08:28:38.373Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/d1/a9f36f8ecdf0fb7c9b1e78c8d7af12b8c8754e74851ac7b94a8305540fc7/macholib-1.16.4-py2.py3-none-any.whl", hash = "sha256:da1a3fa8266e30f0ce7e97c6a54eefaae8edd1e5f86f3eb8b95457cae90265ea", size = 38117, upload-time = "2025-11-22T08:28:36.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "openpyxl"
|
||||
version = "3.1.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "et-xmlfile" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pefile"
|
||||
version = "2024.8.26"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/03/4f/2750f7f6f025a1507cd3b7218691671eecfd0bbebebe8b39aa0fe1d360b8/pefile-2024.8.26.tar.gz", hash = "sha256:3ff6c5d8b43e8c37bb6e6dd5085658d658a7a0bdcd20b6a07b1fcfc1c4e9d632", size = 76008, upload-time = "2024-08-26T20:58:38.155Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/16/12b82f791c7f50ddec566873d5bdd245baa1491bac11d15ffb98aecc8f8b/pefile-2024.8.26-py3-none-any.whl", hash = "sha256:76f8b485dcd3b1bb8166f1128d395fa3d87af26360c2358fb75b80019b957c6f", size = 74766, upload-time = "2024-08-26T21:01:02.632Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pickup-auto"
|
||||
version = "0.1.0"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "openpyxl" },
|
||||
{ name = "pyinstaller" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "openpyxl", specifier = ">=3.1.5" },
|
||||
{ name = "pyinstaller", specifier = ">=6.20.0" },
|
||||
{ name = "requests", specifier = ">=2.31.0" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller"
|
||||
version = "6.20.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "altgraph" },
|
||||
{ name = "macholib", marker = "sys_platform == 'darwin'" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pefile", marker = "sys_platform == 'win32'" },
|
||||
{ name = "pyinstaller-hooks-contrib" },
|
||||
{ name = "pywin32-ctypes", marker = "sys_platform == 'win32'" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/60/d03d52e6690d4e9caf333dcd14550cde634ce6c118b3bc8fa3112c3186fd/pyinstaller-6.20.0.tar.gz", hash = "sha256:95c5c7e03d5d61e9dfb8ef259c699cf492bb1041beb6dbe83696608cec07347a", size = 4048728, upload-time = "2026-04-22T20:59:36.96Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d0/e4/e228d6d1bbb7fd62dc660a8fb202a583b023d3a3624ca95d1a9290ee4d6a/pyinstaller-6.20.0-py3-none-macosx_10_13_universal2.whl", hash = "sha256:bf3be4e1284ee78ddccba5e29f99443a12a7b4673168288ffc4c9d38c6f7b90e", size = 1047642, upload-time = "2026-04-22T20:58:32.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/bd/afb631bcb3f9040efebd4f6d067f0828b51710818f69fb41a2d4b7787f52/pyinstaller-6.20.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:72ae9c1fdea134afa791f58bdc9a1934d5c7609753c111e0026bfc272b32b712", size = 742494, upload-time = "2026-04-22T20:58:36.285Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/08/0729a5bac14754150e5d83b39d87d842eb42b0bffcaa03dbad6252e23a39/pyinstaller-6.20.0-py3-none-manylinux2014_i686.whl", hash = "sha256:1031bcc307f3fbeffd4e162723e64d46dbf591c82dd0997413afb2a07328b941", size = 754191, upload-time = "2026-04-22T20:58:40.603Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/82/bc0ee4c7b97db1958eb651e0da9fb1e672e5ae53ca8867fd97701de52906/pyinstaller-6.20.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:8df3b3f347659fa2562d8d193a98ad4600133b8b8d07c268df89e4154376750e", size = 751902, upload-time = "2026-04-22T20:58:44.7Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/e7/770002d6aaa54173881cb2c49bb195ba67b97bf39bac1cdf320f28401629/pyinstaller-6.20.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:b0d3cc9dd8120d448459bd3880a12e2f9774c51443af49047801446377999a59", size = 748634, upload-time = "2026-04-22T20:58:48.579Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fe/db/68ba1fccb71278b2124fb90b37b7c8c0bc4c1173fba45b94466df3d9cb7f/pyinstaller-6.20.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:03696bb6350177c6bc23bcaf78e71a33c4a89b6754dd90d1be2f318e978c918b", size = 748490, upload-time = "2026-04-22T20:58:52.749Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/0f/ac77ffa996a56be3d5c8f85734a007f8347240691657f9704e7de2527fa3/pyinstaller-6.20.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:6357f1699f6af84f37e7367f031d4f68abdba65543b83990c9e8f5a4cebed0b7", size = 747650, upload-time = "2026-04-22T20:58:57.093Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/56/1ee91c3a2bc10ca1f36da10a6fd55ff7efc4dec367171eb25992a827874f/pyinstaller-6.20.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:0ab39c690abad26ba148e8f664f0478acc82a733997f4f22e757774832802da9", size = 747413, upload-time = "2026-04-22T20:59:01.174Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/55/ae264339996953c4cdf9d89d916a0a8fa26a83cf917a742fff8b9d5f3fe8/pyinstaller-6.20.0-py3-none-win32.whl", hash = "sha256:9a7637e8e44b4387b13667fdcaac86ab6b29c446c16d34d8401539b81838759c", size = 1331584, upload-time = "2026-04-22T20:59:07.201Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/76/8c/300f57578882cce259bfb5ae56fda3b69caa3fe9df40a176c719920ea6e2/pyinstaller-6.20.0-py3-none-win_amd64.whl", hash = "sha256:d588844e890ee80c4365867f98146636e1849bbca8e4284bbf0c809aff0f161a", size = 1391851, upload-time = "2026-04-22T20:59:14.024Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8a/ea/b2f8e1642aecda78c0b75c7321f708e49e10bb3c00dd4f148c40761a1527/pyinstaller-6.20.0-py3-none-win_arm64.whl", hash = "sha256:bd53282c0a73e5c95573e1ddc8e5d564d4932bec91efbaed4dc5fdff9c2ae7f2", size = 1332259, upload-time = "2026-04-22T20:59:20.509Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyinstaller-hooks-contrib"
|
||||
version = "2026.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "packaging" },
|
||||
{ name = "setuptools" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1a/67/f4452d68793fb15beba4f19ef39a38a8822f0da7452b503c400d5a21f5c1/pyinstaller_hooks_contrib-2026.5.tar.gz", hash = "sha256:f066dfca8f7c45ff6336c9cf9fe25b4e48bfeb322a1aa24faaedfb8a8d1b0b08", size = 173689, upload-time = "2026-05-04T22:36:55.124Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f6/5c/fd465d11da4d12b50d7eb5d2ee2ceb780d8d049dbb489f3828d131e387af/pyinstaller_hooks_contrib-2026.5-py3-none-any.whl", hash = "sha256:ea1535783fbdac4626351709e83f3ea80b681d3a4745763ebb407b5e27342eb9", size = 457314, upload-time = "2026-05-04T22:36:53.598Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pywin32-ctypes"
|
||||
version = "0.2.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "requests"
|
||||
version = "2.34.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "charset-normalizer" },
|
||||
{ name = "idna" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/43/b8/7a707d60fea4c49094e40262cc0e2ca6c768cca21587e34d3f705afec47e/requests-2.34.0.tar.gz", hash = "sha256:7d62fe92f50eb82c529b0916bb445afa1531a566fc8f35ffdc64446e771b856a", size = 142436, upload-time = "2026-05-11T19:29:51.717Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ef/e6/e300fce5fe83c30520607a015dabd985df3251e188d234bfe9492e17a389/requests-2.34.0-py3-none-any.whl", hash = "sha256:917520a21b767485ce7c588f4ebb917c436b24a31231b44228715eaeb5a52c60", size = 73021, upload-time = "2026-05-11T19:29:49.923Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "setuptools"
|
||||
version = "82.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4f/db/cfac1baf10650ab4d1c111714410d2fbb77ac5a616db26775db562c8fab2/setuptools-82.0.1.tar.gz", hash = "sha256:7d872682c5d01cfde07da7bccc7b65469d3dca203318515ada1de5eda35efbf9", size = 1152316, upload-time = "2026-03-09T12:47:17.221Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/76/f789f7a86709c6b087c5a2f52f911838cad707cc613162401badc665acfe/setuptools-82.0.1-py3-none-any.whl", hash = "sha256:a59e362652f08dcd477c78bb6e7bd9d80a7995bc73ce773050228a348ce2e5bb", size = 1006223, upload-time = "2026-03-09T12:47:15.026Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "urllib3"
|
||||
version = "2.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/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://files.pythonhosted.org/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" },
|
||||
]
|
||||
Reference in New Issue
Block a user