feat(scripts): 添加批量生成 YouTube Studio 内容管理器 URL 的脚本
This commit is contained in:
5
.gitignore
vendored
Normal file
5
.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
.trae/
|
||||||
|
.venv/
|
||||||
|
adr/
|
||||||
|
__pycache__/
|
||||||
|
.pytest_cache/
|
||||||
32
CONTEXT.md
Normal file
32
CONTEXT.md
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
# YouTube Studio URL 拼接工具
|
||||||
|
|
||||||
|
根据需求清单(CSV/Excel)批量生成 YouTube Studio 内容管理器(Content Manager)高级模式 explore 报告 URL 的小工具。领域词汇统一如下。
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
**所有者(Content Owner)**:
|
||||||
|
拥有内容管理器的账号实体,URL 中通过 `o` 参数与路径 `/owner/<owner_id>/` 标识。
|
||||||
|
_Avoid_: 账号、客户、商户
|
||||||
|
|
||||||
|
**实体(Entity)**:
|
||||||
|
报告统计的对象,由 `entity_type` + `entity_id` 标识,包括所有者(CONTENT_OWNER)、群组(GROUP)、频道(CHANNEL)、节目(VIDEO)。
|
||||||
|
_Avoid_: 对象、目标
|
||||||
|
|
||||||
|
**群组(Group)**:
|
||||||
|
内容所有者名下用于组织一批频道/资产的实体,`entity_id` 为其 groupId(如 `NCy9C2QPQ1E`)。
|
||||||
|
|
||||||
|
**需求(Requirement)**:
|
||||||
|
输入清单中的一行,描述一条待生成 URL 的完整条件:所有者名称、所有者ID、实体类型、实体名称、实体ID、数据周期、国家筛选。
|
||||||
|
|
||||||
|
**实体ID(entity_id)**:
|
||||||
|
URL 中 `entity_id` 参数的值;按实体类型不同取群组ID / 频道ID / 节目ID;所有者场景下即所有者ID。
|
||||||
|
|
||||||
|
**数据周期(Period)**:
|
||||||
|
需求中形如 `yyyy.mm.dd-yyyy.mm.dd` 或 `yyyy.m.d-yyyy.m.d` 的日期区间,起始日与结束日均包含在数据范围内。
|
||||||
|
|
||||||
|
**日界线(Day Boundary)**:
|
||||||
|
周期中某日期对应的 Unix 毫秒时刻,采用「锚点 2026-06-15 = 1781506800000 + 整日偏移」计算,不做时区数学。
|
||||||
|
_Avoid_: 时间戳换算、时区转换
|
||||||
|
|
||||||
|
**国家筛选(Country Filter)**:
|
||||||
|
`ur_dimensions=COUNTRY` + `ur_values` 的筛选参数;多国以 `'`(`%27`)包裹、`|`(`%7C`)连接,如 `%27US%27%7C%27JP%27`。国家以 ISO 3166-1 alpha-2 代码表示。
|
||||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Copyright 2026 SIDNEYZHANG
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
127
README.md
Normal file
127
README.md
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
# YouTube Studio 工具集
|
||||||
|
|
||||||
|
本项目包含三个 Trae CN 技能(skills)及配套 Python 脚本,覆盖 YouTube Studio 内容管理器(Content Manager)分析的完整工作流:**环境准备 → 批量生成报告 URL → 脚本化下载分析数据**。
|
||||||
|
|
||||||
|
仅支持 Windows。
|
||||||
|
|
||||||
|
## 技能清单
|
||||||
|
|
||||||
|
| 技能 | 用途 | 触发示例 |
|
||||||
|
|---|---|---|
|
||||||
|
| `uv-env-setup` | 用 uv 准备并维护 Python 运行环境(`pyproject.toml` → `uv sync` → `uv run`) | 「帮我准备环境」「装依赖」 |
|
||||||
|
| `yt-studio-url-builder` | 根据需求清单(CSV/Excel)批量拼接 explore 报告 URL | 「根据这份清单生成 Studio URL」 |
|
||||||
|
| `youtube-studio-csv-download` | 复用已登录浏览器会话,脚本化下载分析 CSV(zip) | 「用我的 Chrome 会话下载这份 CSV」 |
|
||||||
|
|
||||||
|
## 项目结构
|
||||||
|
|
||||||
|
```
|
||||||
|
├── skills/ # 技能源目录(安装脚本从这里复制)
|
||||||
|
│ ├── uv-env-setup/ # 环境准备技能
|
||||||
|
│ │ ├── SKILL.md
|
||||||
|
│ │ └── references/troubleshooting.md
|
||||||
|
│ ├── yt-studio-url-builder/ # URL 拼接技能
|
||||||
|
│ │ ├── SKILL.md
|
||||||
|
│ │ ├── references/ # input-guide.md(输入清单指南)、troubleshooting.md
|
||||||
|
│ │ └── scripts/ # build_studio_urls.py、countries.json
|
||||||
|
│ └── youtube-studio-csv-download/ # CSV 下载技能
|
||||||
|
│ ├── SKILL.md
|
||||||
|
│ ├── references/troubleshooting.md
|
||||||
|
│ └── scripts/youtube_export_download.py
|
||||||
|
├── scripts/
|
||||||
|
│ ├── install-skills.bat # 傻瓜安装入口(双击运行)
|
||||||
|
│ └── install-skills.ps1 # 安装逻辑
|
||||||
|
├── docs/
|
||||||
|
│ ├── install-skills.md # 安装详细说明
|
||||||
|
│ └── adr/ # 架构决策记录
|
||||||
|
├── assets/ # 需求输入示例文件
|
||||||
|
├── pyproject.toml # Python 依赖唯一事实来源(pandas/openpyxl/playwright)
|
||||||
|
└── uv.lock # uv 依赖锁定(可复现环境)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
### 前置条件
|
||||||
|
|
||||||
|
- Windows
|
||||||
|
- [Trae CN](https://www.trae.cn/) 已安装
|
||||||
|
- [uv](https://docs.astral.sh/uv/) 已安装(未装可 `winget install astral-sh.uv`,装完重开终端)
|
||||||
|
|
||||||
|
### 第 1 步:安装技能到 Trae CN
|
||||||
|
|
||||||
|
双击运行:
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts\install-skills.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
看到 `全部安装成功` 即完成。脚本会把 `skills\` 下全部技能复制到 `%USERPROFILE%\.trae-cn\skills\`;同名旧版自动备份到 `~\.trae-cn\skills-backup\`,不会丢数据。详细说明见 [docs/install-skills.md](docs/install-skills.md)。
|
||||||
|
|
||||||
|
### 第 2 步:准备 Python 运行环境
|
||||||
|
|
||||||
|
项目根目录执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
uv 自动创建 `.venv\` 并安装全部依赖(pandas、openpyxl、playwright)。本机无兼容 Python 时 uv 会自动下载托管 Python(要求 ≥3.10)。
|
||||||
|
|
||||||
|
### 第 3 步:开始使用
|
||||||
|
|
||||||
|
重启 Trae CN(或新建会话)后,在对话中直接说需求即可触发对应技能,例如:
|
||||||
|
|
||||||
|
> 根据这份清单批量生成 Studio URL
|
||||||
|
|
||||||
|
把需求整理成 CSV/Excel(格式见 `skills/yt-studio-url-builder/references/input-guide.md`,示例见 `assets/需求输入示例.xlsx`),技能会引导生成 URL 清单。
|
||||||
|
|
||||||
|
> 用我的 Chrome 会话下载这份分析 CSV
|
||||||
|
|
||||||
|
技能会用 Playwright 复用已登录的浏览器会话,自动点击导出并保存 zip。
|
||||||
|
|
||||||
|
> 帮我准备/重建环境
|
||||||
|
|
||||||
|
技能会引导走 `uv sync` 与验证流程。
|
||||||
|
|
||||||
|
所有脚本统一用 `uv run python <脚本>` 执行(自动使用项目环境,免激活、免手动装依赖)。
|
||||||
|
|
||||||
|
## 后续优化与修改
|
||||||
|
|
||||||
|
### 改 URL 固定参数
|
||||||
|
|
||||||
|
指标、粒度、维度、排序等固定参数集中在 `skills/yt-studio-url-builder/scripts/build_studio_urls.py` 顶部的 `CONFIG` 字典中,**单点维护**——改完重跑脚本即生效,需求清单无需变动。
|
||||||
|
|
||||||
|
### 扩充国家映射
|
||||||
|
|
||||||
|
中文国家名 → ISO 代码的映射在 `skills/yt-studio-url-builder/scripts/countries.json`,直接追加条目即可;映射外的两位 ISO 代码(如 `US`)会原样透传。
|
||||||
|
|
||||||
|
### 新增/修改依赖
|
||||||
|
|
||||||
|
只改根目录 `pyproject.toml` 的 `dependencies`,然后 `uv sync` 更新 `uv.lock`。不要 `pip install` 直装(绕过 lock,环境不可复现)。
|
||||||
|
|
||||||
|
### 修改技能并重新安装
|
||||||
|
|
||||||
|
技能就是在 `skills/` 下编辑的 Markdown + 脚本。改完后重新双击 `scripts\install-skills.bat` 即可同步到 Trae CN(旧版自动备份)。建议遵循的写法:
|
||||||
|
|
||||||
|
- 每个技能一个文件夹,必须含 `SKILL.md`(frontmatter 的 `name` + `description`,description 写清触发场景)。
|
||||||
|
- 大块参考内容放 `references/` 子目录,`SKILL.md` 中按需引用(渐进披露)。
|
||||||
|
- 每个步骤给出可检查的完成判据,避免半途而废。
|
||||||
|
- 同一信息只在一处维护(单一事实来源),避免重复表述。
|
||||||
|
|
||||||
|
### 回滚 / 卸载
|
||||||
|
|
||||||
|
- 回滚:把 `~\.trae-cn\skills-backup\<技能名>-<时间戳>\` 复制回 `~\.trae-cn\skills\<技能名>\`。
|
||||||
|
- 卸载:删除 `~\.trae-cn\skills\` 下对应技能文件夹。
|
||||||
|
- 重置环境:删除项目根 `.venv\` 后重新 `uv sync`。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
- **双击 bat 闪退**:在 PowerShell 中手动运行 `scripts\install-skills.bat` 查看报错。
|
||||||
|
- **装了技能不触发**:需重启 Trae CN 或新建会话。
|
||||||
|
- **`uv sync` 拉包慢**:国内镜像 `$env:UV_DEFAULT_INDEX = "https://pypi.tuna.tsinghua.edu.cn/simple"` 后重试。
|
||||||
|
- **技能脚本报 `ModuleNotFoundError`**:项目根执行 `uv sync`,或确认命令用了 `uv run` 前缀。
|
||||||
|
|
||||||
|
更多排查见各技能的 `references/troubleshooting.md`。
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
UPDATE: 2026-08-21
|
||||||
BIN
assets/YouTube_Studio_细分维度_时间颗粒度_完整清单.xlsx
Normal file
BIN
assets/YouTube_Studio_细分维度_时间颗粒度_完整清单.xlsx
Normal file
Binary file not shown.
BIN
assets/需求输入示例.xlsx
Normal file
BIN
assets/需求输入示例.xlsx
Normal file
Binary file not shown.
63
docs/install-skills.md
Normal file
63
docs/install-skills.md
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
# 技能安装说明(Windows)
|
||||||
|
|
||||||
|
把本项目 `skills\` 下的技能一键安装到 Trae CN 的用户级技能目录 `%USERPROFILE%\.trae-cn\skills\`,安装后在任意项目的对话中都可触发。
|
||||||
|
|
||||||
|
## 技能清单
|
||||||
|
|
||||||
|
| 技能 | 用途 |
|
||||||
|
|---|---|
|
||||||
|
| `uv-env-setup` | 用 uv 准备并维护本项目 Python 运行环境(`uv sync` / `uv run`) |
|
||||||
|
| `yt-studio-url-builder` | 根据需求清单(CSV/Excel)批量生成 YouTube Studio 内容管理器 explore URL |
|
||||||
|
| `youtube-studio-csv-download` | 复用已登录浏览器会话,脚本化下载 YouTube Studio 分析 CSV(zip) |
|
||||||
|
|
||||||
|
## 安装(傻瓜式)
|
||||||
|
|
||||||
|
双击运行:
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts\install-skills.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
看到 `全部安装成功` 及逐项 `[OK]` 即完成,按任意键关闭窗口。
|
||||||
|
|
||||||
|
命令行等价方式(PowerShell / cmd 均可):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File scripts\install-skills.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
## 脚本做了什么
|
||||||
|
|
||||||
|
1. 扫描项目 `skills\` 下每个包含 `SKILL.md` 的子目录(即可安装技能)。
|
||||||
|
2. 逐个复制到 `%USERPROFILE%\.trae-cn\skills\`。
|
||||||
|
3. 目标已存在同名技能时,先把旧版移动到 `%USERPROFILE%\.trae-cn\skills-backup\<技能名>-<时间戳>\` 再装新版——不直接覆盖,旧版不丢。
|
||||||
|
4. 逐个校验并打印 `[OK]` / `[FAIL]`;全部成功时退出码为 0。
|
||||||
|
|
||||||
|
安全性:脚本只处理本项目 `skills\` 中出现的技能,不会删除或改动 `.trae-cn\skills\` 下的其他技能;不修改任何 Trae CN 配置文件。
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
1. 重启 Trae CN(或新建会话)。
|
||||||
|
2. 对话中直接提及技能名或相关意图,例如:
|
||||||
|
- 「帮我准备环境」→ `uv-env-setup`
|
||||||
|
- 「根据这份清单批量生成 Studio URL」→ `yt-studio-url-builder`
|
||||||
|
- 「用我的 Chrome 会话下载这份分析 CSV」→ `youtube-studio-csv-download`
|
||||||
|
|
||||||
|
## 更新与卸载
|
||||||
|
|
||||||
|
- **更新**:项目技能有改动后,重新双击 `install-skills.bat`(旧版自动备份)。
|
||||||
|
- **回滚**:把 `%USERPROFILE%\.trae-cn\skills-backup\<技能名>-<时间戳>\` 整个文件夹复制回 `%USERPROFILE%\.trae-cn\skills\<技能名>\`。
|
||||||
|
- **卸载**:删除 `%USERPROFILE%\.trae-cn\skills\` 下对应技能文件夹。
|
||||||
|
- **清理备份**:确认新版可用后,删除 `%USERPROFILE%\.trae-cn\skills-backup\` 整个文件夹。
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
- **双击 bat 闪退**:在 PowerShell 中手动运行 `scripts\install-skills.bat` 查看报错;最常见原因是 `install-skills.ps1` 没有和 bat 放在同一目录。
|
||||||
|
- **PowerShell 执行策略受限**:bat 已带 `-ExecutionPolicy Bypass`;若组策略仍拦截,以管理员身份运行。
|
||||||
|
- **技能装了但不触发**:需重启 Trae CN 或新建会话后生效;确认对话窗口正常加载。
|
||||||
|
- **中文输出乱码**:`install-skills.ps1` 必须保持 UTF-8 with BOM 编码(重新编辑保存时注意)。
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 运行时依赖不随技能安装:两个脚本型技能的 Python 依赖(pandas、openpyxl、playwright)由项目根 `pyproject.toml` 统一管理,首次使用按 `uv-env-setup` 技能执行 `uv sync` 即可。
|
||||||
|
- 安装脚本仅支持 Windows(路径与命令均按 Windows 编写)。
|
||||||
231
docs/yt-studio-export-principle.md
Normal file
231
docs/yt-studio-export-principle.md
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
# YouTube Studio 高级分析「导出当前视图」原理
|
||||||
|
|
||||||
|
> 技术原理说明 · 实测分析
|
||||||
|
>
|
||||||
|
> 从点击「导出当前视图 → 逗号分隔值 (.csv)」到 ZIP 落盘,拆解一次完整的请求-响应机制:后端并不生成可下载的 URL,而是把打包好的 ZIP 以 base64 内联在 API 响应里,由前端解码成 Blob 后触发浏览器下载。
|
||||||
|
>
|
||||||
|
> 抓包日期:2026-08-21 · 方法:浏览器抓包 + 脚本复现
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
1. [概述与核心结论](#01-概述与核心结论)
|
||||||
|
2. [用户操作入口](#02-用户操作入口)
|
||||||
|
3. [网络请求机制](#03-网络请求机制)
|
||||||
|
4. [exportQuery 请求体结构](#04-exportquery-请求体结构)
|
||||||
|
5. [服务端响应与编码](#05-服务端响应与编码)
|
||||||
|
6. [ZIP 内容结构](#06-zip-内容结构)
|
||||||
|
7. [客户端解码与下载](#07-客户端解码与下载)
|
||||||
|
8. [文件命名规则](#08-文件命名规则)
|
||||||
|
9. [落盘与重名去重](#09-落盘与重名去重)
|
||||||
|
10. [拦截与自动化可行性](#10-拦截与自动化可行性)
|
||||||
|
11. [端到端流程总览](#11-端到端流程总览)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 01 概述与核心结论
|
||||||
|
|
||||||
|
「高级分析」是 YouTube Studio(内容管理器)面向版权所有者与频道的一份数据分析视图。页面上方有一个「导出当前视图」按钮,下拉后选择 `逗号分隔值 (.csv)`,即可把当前可见的表格导出为一份本地文件 —— 用户拿到手的并非多个散落的 CSV,而是一个 ZIP 压缩包。
|
||||||
|
|
||||||
|
这份导出的关键在于:**整个文件并没有走传统的「服务端生成下载链接 → 浏览器 GET 下载」路径**。在 2026-08-21 的抓包中,页面向后端发了一次 `POST` 请求,响应体里直接内联了一段 base64 字符串,其解码结果是完整的 ZIP 字节流。前端拿到这段字符串后自行解码、构造 `Blob`,再唤起浏览器的下载动作。整个过程没有出现任何指向下载文件的 GET 请求或页面跳转。
|
||||||
|
|
||||||
|
> **一句话结论**
|
||||||
|
>
|
||||||
|
> 导出 = **前端发起 POST** → **后端打包 ZIP 并 base64 内联返回** → **前端解码为 Blob 触发下载**。因为没有独立下载 URL,反而使「拦截响应、自行解码落盘」成为完全可行的自动化方式。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 02 用户操作入口
|
||||||
|
|
||||||
|
操作发生在内容管理器的「高级分析 / Explore」页面(URL 以 `studio.youtube.com/owner/…/analytics` 形态出现)。页面通过「维度」按钮切换统计口径(如**内容**、**频道**等),再通过时间选择器确定日期范围。
|
||||||
|
|
||||||
|
页面的「导出当前视图」下拉提供多种格式,本主题只涉及 `逗号分隔值 (.csv)`。点击该选项后,前端读取当前维度和日期范围,把它转成一份结构化的命名查询,随后发起网络请求。用户视角只有一次点击,但背后是下述完整链路。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 03 网络请求机制
|
||||||
|
|
||||||
|
导出动作返回的是一个内嵌压缩包的 JSON API 调用,而不是文件下载请求。抓包得到的确切端点如下:
|
||||||
|
|
||||||
|
```
|
||||||
|
POST https://studio.youtube.com/youtubei/v1/yta_web/csv_export?alt=json
|
||||||
|
```
|
||||||
|
|
||||||
|
这是 YouTube 内部 `youtubei` 风格 v1 接口的一个方法,`yta_web` 对应 YouTube Analytics 的 Web 端命名空间,`csv_export` 即「导出 CSV(实为 ZIP)」的动作。`?alt=json` 表示要求返回 JSON 而非其他编码。请求体承载一份 `exportQuery`,描述「要导出哪些统计维度和哪段日期」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 04 exportQuery 请求体结构
|
||||||
|
|
||||||
|
请求体(POST body)的根字段是 `exportQuery`。它内部通过一个 `joinRequest` 描述导出内容,核心是 `nodes` 数组 —— 每个节点对应导出一张表格。实测一次导出包含三张表:**表格数据**、**图表数据**、**总计**。每个节点内部的 `value.query` 声明了维度类型和日期范围。
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"exportQuery": {
|
||||||
|
"joinRequest": {
|
||||||
|
"nodes": [
|
||||||
|
{ "value": { "query": {
|
||||||
|
"dimensions": [{ "type": "VIDEO" }], // 表格数据
|
||||||
|
"timeRange": { "dateIdRange": {
|
||||||
|
"inclusiveStart": 20260723,
|
||||||
|
"exclusiveEnd": 20260820
|
||||||
|
} }
|
||||||
|
} } },
|
||||||
|
{ "value": { "query": { /* 图表数据 */ } } },
|
||||||
|
{ "value": { "query": { /* 总计 */ } } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
其中两处日期字段最值得留意:`inclusiveStart` 表述导出区间的起始日(含),`exclusiveEnd` 表述结束日(不含,即「到这一天为止」)。两者都是 `YYYYMMDD` 格式的整数,例如 `20260723` 表示 2026-07-23。它们同时决定了导出内容和最终文件名。
|
||||||
|
|
||||||
|
> **关键词义**
|
||||||
|
>
|
||||||
|
> `exclusiveEnd` 是「开区间上界」:导出数据覆盖到 exclusiveEnd 的前一天。文件名里二者的下划线形式正是从这两个字段直接格式化而来。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 05 服务端响应与编码
|
||||||
|
|
||||||
|
后端收到 `exportQuery` 后,把请求的三张表各自输出成 CSV,再打包成一个 ZIP,然后把 ZIP 的**二进制字节流做 base64 编码**,塞进 JSON 响应的一个重要字段里返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"responseContext": { /* 服务元信息 */ },
|
||||||
|
"zippedData": "UEsDBBQAAAAA..." // base64 编码的 ZIP 字节流
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`zippedData` 是这条响应的精华。它开头的 `UEsDBBQ` 是 base64 对 ZIP 文件头魔数 `PK\x03\x04`(即 ASCII 的 `PK` 两个字节)的编码 —— 这是识别「这是一个 ZIP」的最直接证据。JSON 只能承载文本,二进制 ZIP 无法原样嵌入,因此后端先 base64 编码,让整个压缩包变成一段纯文本字符串随响应返回。
|
||||||
|
|
||||||
|
这里没有生成任何 `/download/…` 之类的文件地址,也没有 `Content-Disposition: attachment` 的响应头来驱动浏览器另存 —— 下载的编排完全交还给了前端。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 06 ZIP 内容结构
|
||||||
|
|
||||||
|
把 `zippedData` 做一次 base64 解码,得到的是一个标准 ZIP 压缩包,内含三个 CSV 文件,正好与请求体 `joinRequest.nodes` 的三个节点一一对应:
|
||||||
|
|
||||||
|
| ZIP 内文件名 | 对应请求节点 | 说明 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `表格数据.csv` | nodes[0] | 当前维度下的逐条明细(如每条内容/频道的指标) |
|
||||||
|
| `图表数据.csv` | nodes[1] | 供图表渲染的时间序列 / 汇总数据 |
|
||||||
|
| `总计.csv` | nodes[2] | 全表合计行的汇总值 |
|
||||||
|
|
||||||
|
三张表的拆分说明导出本质上是一次「多表联查」:前端页面上同时呈现的明细表、图表和合计,被后端一次性打包进同一个压缩包交付,而不是三次独立下载。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 07 客户端解码与下载
|
||||||
|
|
||||||
|
前端收到 JSON 后,做的是标准的「base64 → 二进制 → Blob → 触发下载」四步。由于抓包时没有观察到任何指向下载文件的 GET 或跳转,可以判定这是**客户端 Blob 下载**而非服务端重定向下载。下面这段是这一机制的通用实现示意(并非从 YouTube 混淆后的前端源码中直接提取):
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// base64 → 字节数组
|
||||||
|
const bytes = atob(json.zippedData);
|
||||||
|
const buf = new Uint8Array(bytes.length);
|
||||||
|
for (let i = 0; i < bytes.length; i++) buf[i] = bytes.charCodeAt(i);
|
||||||
|
|
||||||
|
// 字节 → Blob → 对象 URL
|
||||||
|
const blob = new Blob([buf], { type: "application/zip" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// 挂到 <a> 上并触发点击,交由浏览器下载管理器接管
|
||||||
|
const a = document.createElement("a");
|
||||||
|
a.href = url;
|
||||||
|
a.download = "内容 2026-07-23_2026-08-20 WL Media.zip";
|
||||||
|
a.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
```
|
||||||
|
|
||||||
|
`a.download` 里填写的文件名,就是用户在下载管理器里看到的名字。而真正的写盘动作,由浏览器自身的下载管理器执行。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 08 文件命名规则
|
||||||
|
|
||||||
|
与实测下载目录 `D:\Downloads` 中的产物一致,最终文件名遵循固定模板:
|
||||||
|
|
||||||
|
```
|
||||||
|
<维度标签> <inclusiveStart>_<exclusiveEnd> <账号名>.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `inclusiveStart` 与 `exclusiveEnd` 取自请求体,由 `YYYYMMDD` 整数格式化为 `YYYY-MM-DD`。实测的一个完整例子是:
|
||||||
|
|
||||||
|
```
|
||||||
|
内容 2026-07-23_2026-08-20 WL Media.zip
|
||||||
|
```
|
||||||
|
|
||||||
|
维度标签来自「维度」选择器:类型 `VIDEO` 对应「内容」、`USER` 对应「频道」。账号名取自右上角账号选择器按钮的文本(示例为 `WL Media`)。
|
||||||
|
|
||||||
|
| 构成部分 | 来源 | 示例值 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 维度标签 | 页面「维度」按钮(VIDEO→内容 / USER→频道) | 内容 |
|
||||||
|
| 起始日 | `inclusiveStart` 格式化为 YYYY-MM-DD | 2026-07-23 |
|
||||||
|
| 结束日 | `exclusiveEnd` 格式化为 YYYY-MM-DD | 2026-08-20 |
|
||||||
|
| 账号名 | 右上角账号按钮文本 | WL Media |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 09 落盘与重名去重
|
||||||
|
|
||||||
|
因为下载由浏览器下载管理器接管,落盘位置默认是浏览器的下载目录(本机为 `D:\Downloads`),且无需「另存为」确认。Chromium 内核对重名文件会自动追加序号后缀,这正是「需求文件.zip → 需求文件 (1).zip → 需求文件 (2).zip」这一规则的由来。
|
||||||
|
|
||||||
|
去重逻辑是:若目标名已存在,则在扩展名前追加 ` (n)`,`n` 从 1 开始递增,每次取「最小不冲突的序号」—— 即便 `(1)` 已被占用,也会继续向后找 `(2)`,而不是覆盖或失败。这一规则在后续的自动化脚本中被原样复现,从而做到完全不依赖浏览器也能自管文件名。
|
||||||
|
|
||||||
|
> **去重规则**
|
||||||
|
>
|
||||||
|
> 同名文件依次保存为 `需求文件.zip`、`需求文件 (1).zip`、`需求文件 (2).zip`……若某个 `(n)` 已存在则跳到下一个可用 n。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10 拦截与自动化可行性
|
||||||
|
|
||||||
|
正因为导出所需的全部数据(ZIP 字节流)都内联在一次 API 响应里,且不依赖额外的文件下载地址,**在响应层拦截即可完整掌控落盘行为** —— 保存目录、文件名、重名去重全由脚本决定,绕开浏览器的下载管理器和确认弹窗。
|
||||||
|
|
||||||
|
实现上有两条路径,落地为一套脚本与一个可复用 skill:
|
||||||
|
|
||||||
|
- **响应拦截 + 自行解码**:命中 `csv_export` 响应后读取 `zippedData`,base64 解码、反推文件名、去重后写盘,完全可控。
|
||||||
|
- **依赖 Chromium 下载偏好**:开启自动下载与内置去重,让浏览器原样落盘,仅省去人工点击。
|
||||||
|
|
||||||
|
无论哪条路径,前提都是复用**已登录 YouTube Studio** 的浏览器会话:要么以已登录的用户数据目录启动浏览器,要么通过调试端口附加到已打开的浏览器。否则页面会跳转到 Google 登录页,导出无法触发。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11 端到端流程总览
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
A[1. 操作入口<br/>点击「导出当前视图 → 逗号分隔值 (.csv)」] --> B
|
||||||
|
B[2. 组装请求<br/>前端生成 exportQuery:joinRequest 节点 + 日期范围] --> C
|
||||||
|
C[3. 发送请求<br/>POST /youtubei/v1/yta_web/csv_export?alt=json] --> D
|
||||||
|
D[4. 服务端打包<br/>三张 CSV 压缩为 ZIP,再 base64 编码] --> E
|
||||||
|
E[5. 内联返回<br/>响应体 zippedData 承载 base64(PK 魔数开头)] --> F
|
||||||
|
F[6. 前端解码<br/>base64 → Blob,触发浏览器下载] --> G
|
||||||
|
G[7. 落盘去重<br/>下载管理器写入默认目录,重名自动加 n]
|
||||||
|
|
||||||
|
style A fill:#ff5c5c22,stroke:#ff5c5c,color:#e8ecf1
|
||||||
|
style B fill:#ff5c5c22,stroke:#ff5c5c,color:#e8ecf1
|
||||||
|
style C fill:#58a6ff22,stroke:#58a6ff,color:#e8ecf1
|
||||||
|
style D fill:#58a6ff22,stroke:#58a6ff,color:#e8ecf1
|
||||||
|
style E fill:#58a6ff22,stroke:#58a6ff,color:#e8ecf1
|
||||||
|
style F fill:#ff5c5c22,stroke:#ff5c5c,color:#e8ecf1
|
||||||
|
style G fill:#ff5c5c22,stroke:#ff5c5c,color:#e8ecf1
|
||||||
|
```
|
||||||
|
|
||||||
|
图 1 · 从点击导出到 ZIP 落盘的端到端流程(红色 = 客户端/前端,蓝色 = YouTube 后端)
|
||||||
|
|
||||||
|
**请求 / 响应结构映射**
|
||||||
|
|
||||||
|
| 请求 · exportQuery | 响应 · zippedData |
|
||||||
|
| --- | --- |
|
||||||
|
| `joinRequest.nodes`(3 张表) | `responseContext`(元信息) |
|
||||||
|
| `dimensions` → 维度类型 | `zippedData` = base64(ZIP) |
|
||||||
|
| `dateIdRange.inclusiveStart` | 开头 `UEsDBBQ` = PK 魔数 |
|
||||||
|
| `dateIdRange.exclusiveEnd` | 解码得 表格/图表/总计.csv |
|
||||||
|
| 日期均为 YYYYMMDD 整数 | — |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*分析基于 2026-08-21 的浏览器抓包与脚本复现实证。配套脚本:`youtube-studio-csv-download`*
|
||||||
22
pyproject.toml
Normal file
22
pyproject.toml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
[project]
|
||||||
|
name = "StudioLift"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "StudioLift :一个 YouTube Studio 工具集,提供 URL 批量拼接、分析 CSV 导出下载等功能。"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"pandas>=2.0",
|
||||||
|
"openpyxl>=3.1",
|
||||||
|
"playwright>=1.40",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
|
||||||
|
[tool.uv]
|
||||||
|
# 脚本在 scripts/ 下,非 Python 包,无需打包构建
|
||||||
|
package = false
|
||||||
297
scripts/build_studio_urls.py
Normal file
297
scripts/build_studio_urls.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
根据需求清单(CSV/Excel)批量拼接 YouTube Studio 内容管理器 explore URL。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python build_studio_urls.py -i 需求清单.csv -o 输出.csv
|
||||||
|
python build_studio_urls.py -i 需求清单.xlsx -o 输出.csv
|
||||||
|
|
||||||
|
输入列(支持中英文别名,未填可留空):
|
||||||
|
所有者名称 : 所有者名称 / owner_name
|
||||||
|
所有者ID : 所有者ID / owner_id / o
|
||||||
|
实体名称 : 实体名称 / 群组名称 / 频道名称 / 节目名称 / entity_name / group_name
|
||||||
|
实体ID : 实体ID / 群组ID / group_id / entity_id / id
|
||||||
|
实体类型 : 实体类型 / 类型 / entity_type (群组/所有者/频道/节目,或 GROUP/CONTENT_OWNER/CHANNEL/VIDEO)
|
||||||
|
数据周期 : 数据周期 / 周期 / period / time_period (yyyy.mm.dd-yyyy.mm.dd 或 yyyy.m.d-yyyy.m.d)
|
||||||
|
国家 : 国家 / 国家/地区 / country / countries (一个或多个,中文名或 ISO 两位代码)
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 数据周期起止日期均包含在数据范围内,time_period 结束值取结束日后一天的日界线。
|
||||||
|
- 国家为多个时,ur_values 以 '%27' 包裹、'%7C' 连接(如 美国,日本 -> %27US%27%7C%27JP%27)。
|
||||||
|
- 中文国家名 -> ISO 代码的映射放在同目录 countries.json(可自行扩充);已是两位代码的原样透传。
|
||||||
|
- 固定参数(metric/granularity/dimension/t_metrics 等)在本文件 CONFIG 中统一配置。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 固定参数(所有需求共用,按需修改)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
CONFIG = {
|
||||||
|
"explore_type": "TABLE_AND_CHART",
|
||||||
|
"metric": "SUBSCRIBERS_NET_CHANGE", # 主指标
|
||||||
|
"granularity": "DAY", # DAY / WEEK / MONTH / YEAR
|
||||||
|
"dimension": "USER", # 细分维度
|
||||||
|
"t_metrics": [ # 表格列指标(可多个)
|
||||||
|
"SUBSCRIBERS_NET_CHANGE",
|
||||||
|
"VIDEO_COUNT_FIRST_PUBLISHED",
|
||||||
|
"ENGAGED_VIEWS",
|
||||||
|
"EXTERNAL_VIEWS",
|
||||||
|
"EXTERNAL_WATCH_TIME",
|
||||||
|
"AVERAGE_WATCH_TIME",
|
||||||
|
"TOTAL_ESTIMATED_EARNINGS",
|
||||||
|
],
|
||||||
|
"o_column": "SUBSCRIBERS_NET_CHANGE", # 排序字段
|
||||||
|
"o_direction": "ANALYTICS_ORDER_DIRECTION_DESC", # DESC / ASC
|
||||||
|
"comparison_type": "NONE",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 实体类型 中文/代码 -> URL 参数值(可扩充)
|
||||||
|
ENTITY_TYPE_MAP = {
|
||||||
|
"群组": "GROUP", "GROUP": "GROUP",
|
||||||
|
"所有者": "CONTENT_OWNER", "账号": "CONTENT_OWNER", "CONTENT_OWNER": "CONTENT_OWNER",
|
||||||
|
"频道": "CHANNEL", "CHANNEL": "CHANNEL",
|
||||||
|
"节目": "VIDEO", "视频": "VIDEO", "VIDEO": "VIDEO",
|
||||||
|
}
|
||||||
|
DEFAULT_ENTITY_TYPE = "GROUP"
|
||||||
|
|
||||||
|
# 日界线锚点 + 整日偏移(见 docs/adr/0001)
|
||||||
|
ANCHOR_DATE = date(2026, 6, 15)
|
||||||
|
ANCHOR_MS = 1781506800000
|
||||||
|
MS_PER_DAY = 86400000
|
||||||
|
|
||||||
|
# 数据周期正则:支持 2026.07.01-2026.08.01 / 2026.7.1-2026.8.1 / 2026-07-01~2026-08-01
|
||||||
|
PERIOD_RE = re.compile(
|
||||||
|
r"(\d{4})[.\-/](\d{1,2})[.\-/](\d{1,2})\s*[-~~]\s*(\d{4})[.\-/](\d{1,2})[.\-/](\d{1,2})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 输入列名 -> 规范字段(键为去空白、转小写后的列名)
|
||||||
|
COLUMN_ALIASES = {
|
||||||
|
# 所有者
|
||||||
|
"所有者名称": "owner_name", "ownername": "owner_name", "owner_name": "owner_name",
|
||||||
|
"所有者": "owner_name",
|
||||||
|
"所有者id": "owner_id", "ownerid": "owner_id", "owner_id": "owner_id", "o": "owner_id",
|
||||||
|
# 实体
|
||||||
|
"实体名称": "entity_name", "entity_name": "entity_name",
|
||||||
|
"群组名称": "entity_name", "group_name": "entity_name", "groupname": "entity_name",
|
||||||
|
"频道名称": "entity_name", "节目名称": "entity_name",
|
||||||
|
"实体id": "entity_id", "entity_id": "entity_id", "entityid": "entity_id",
|
||||||
|
"群组id": "entity_id", "group_id": "entity_id", "groupid": "entity_id",
|
||||||
|
"群组": "entity_id", "id": "entity_id",
|
||||||
|
"实体类型": "entity_type", "entity_type": "entity_type", "entitytype": "entity_type",
|
||||||
|
"类型": "entity_type", "type": "entity_type",
|
||||||
|
# 数据周期
|
||||||
|
"数据周期": "period", "周期": "period", "period": "period",
|
||||||
|
"time_period": "period", "timeperiod": "period", "日期范围": "period",
|
||||||
|
# 国家
|
||||||
|
"国家": "countries", "国家/地区": "countries", "国家地区": "countries",
|
||||||
|
"countries": "countries", "country": "countries", "筛选国家": "countries", "地区": "countries",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ts(y, m, d):
|
||||||
|
"""日期 -> 日界线 Unix 毫秒(锚点 + 整日偏移)。"""
|
||||||
|
return ANCHOR_MS + (date(y, m, d) - ANCHOR_DATE).days * MS_PER_DAY
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_columns(df):
|
||||||
|
"""按别名把输入列映射到规范字段。返回 (field->column_index, 未识别列名列表)。"""
|
||||||
|
mapping = {}
|
||||||
|
unknown = []
|
||||||
|
for col in df.columns:
|
||||||
|
key = str(col).strip().lower()
|
||||||
|
field = COLUMN_ALIASES.get(key)
|
||||||
|
if field:
|
||||||
|
mapping[field] = col
|
||||||
|
else:
|
||||||
|
unknown.append(str(col))
|
||||||
|
return mapping, unknown
|
||||||
|
|
||||||
|
|
||||||
|
def parse_period(text):
|
||||||
|
"""解析数据周期,返回 (start_ms, end_ms),起止日期均含。"""
|
||||||
|
text = str(text).strip()
|
||||||
|
m = PERIOD_RE.search(text)
|
||||||
|
if not m:
|
||||||
|
raise ValueError("无法解析数据周期: %r(应为 yyyy.mm.dd-yyyy.mm.dd)" % text)
|
||||||
|
y1, m1, d1, y2, m2, d2 = (int(g) for g in m.groups())
|
||||||
|
start_ms = ts(y1, m1, d1)
|
||||||
|
end_ms = ts(y2, m2, d2) + MS_PER_DAY # 结束日包含
|
||||||
|
return start_ms, end_ms
|
||||||
|
|
||||||
|
|
||||||
|
def load_country_map(path):
|
||||||
|
"""加载 中文国家名 -> ISO 代码 映射。文件缺失则仅支持两位代码透传。"""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print("[提示] 未找到国家映射文件 %s,仅支持直接填写两位 ISO 代码" % path, file=sys.stderr)
|
||||||
|
return {}
|
||||||
|
with open(path, "r", encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return {str(k).strip(): str(v).strip().upper() for k, v in data.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_countries(text, country_map):
|
||||||
|
"""解析国家列(一个或多个,中文名或 ISO 代码),返回 ISO 代码列表。"""
|
||||||
|
if text is None or (isinstance(text, float) and str(text) == "nan"):
|
||||||
|
return []
|
||||||
|
raw = str(text)
|
||||||
|
parts = re.split(r"[,,、;;\s|]+", raw)
|
||||||
|
codes = []
|
||||||
|
for part in parts:
|
||||||
|
p = part.strip().strip("'\"").strip()
|
||||||
|
if not p:
|
||||||
|
continue
|
||||||
|
upper = p.upper()
|
||||||
|
if re.fullmatch(r"[A-Z]{2}", upper): # 已是两位 ISO 代码
|
||||||
|
codes.append(upper)
|
||||||
|
elif p in country_map:
|
||||||
|
codes.append(country_map[p])
|
||||||
|
else:
|
||||||
|
raise ValueError("未识别的国家: %r(不在映射文件中,也不是两位 ISO 代码)" % p)
|
||||||
|
return codes
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_entity_type(text):
|
||||||
|
"""实体类型 -> URL 参数值。空则用默认群组。"""
|
||||||
|
if text is None or (isinstance(text, float) and str(text) == "nan") or str(text).strip() == "":
|
||||||
|
return DEFAULT_ENTITY_TYPE
|
||||||
|
key = str(text).strip()
|
||||||
|
if key in ENTITY_TYPE_MAP:
|
||||||
|
return ENTITY_TYPE_MAP[key]
|
||||||
|
raise ValueError("未识别的实体类型: %r(应为 群组/所有者/频道/节目 或 GROUP/CONTENT_OWNER/CHANNEL/VIDEO)" % key)
|
||||||
|
|
||||||
|
|
||||||
|
def build_url(row, country_map):
|
||||||
|
"""根据一行需求生成 URL。返回 (url, 状态, 错误信息)。"""
|
||||||
|
owner_id = row.get("owner_id", "").strip()
|
||||||
|
entity_type = resolve_entity_type(row.get("entity_type", ""))
|
||||||
|
entity_id = row.get("entity_id", "").strip()
|
||||||
|
|
||||||
|
if not owner_id:
|
||||||
|
return None, "error", "缺少所有者ID"
|
||||||
|
if not entity_id:
|
||||||
|
if entity_type == "CONTENT_OWNER":
|
||||||
|
entity_id = owner_id # 账号整体场景回退
|
||||||
|
else:
|
||||||
|
return None, "error", "缺少实体ID"
|
||||||
|
|
||||||
|
period = row.get("period", "").strip()
|
||||||
|
if not period:
|
||||||
|
return None, "error", "缺少数据周期"
|
||||||
|
start_ms, end_ms = parse_period(period)
|
||||||
|
|
||||||
|
codes = parse_countries(row.get("countries", ""), country_map)
|
||||||
|
|
||||||
|
base = "https://studio.youtube.com/owner/%s/analytics/tab-overview/period-default/explore" % owner_id
|
||||||
|
params = []
|
||||||
|
params.append("o=%s" % owner_id)
|
||||||
|
params.append("entity_type=%s" % entity_type)
|
||||||
|
params.append("entity_id=%s" % entity_id)
|
||||||
|
if codes:
|
||||||
|
ur_values = quote("|".join("'%s'" % c for c in codes), safe="") # 'US'|'JP' -> %27US%27%7C%27JP%27
|
||||||
|
params.append("ur_dimensions=COUNTRY")
|
||||||
|
params.append("ur_values=%s" % ur_values)
|
||||||
|
params.append("ur_inclusive_starts=")
|
||||||
|
params.append("ur_exclusive_ends=")
|
||||||
|
params.append("time_period=%d%%2C%d" % (start_ms, end_ms))
|
||||||
|
params.append("explore_type=%s" % CONFIG["explore_type"])
|
||||||
|
params.append("metric=%s" % CONFIG["metric"])
|
||||||
|
params.append("granularity=%s" % CONFIG["granularity"])
|
||||||
|
for m in CONFIG["t_metrics"]:
|
||||||
|
params.append("t_metrics=%s" % m)
|
||||||
|
params.append("dimension=%s" % CONFIG["dimension"])
|
||||||
|
params.append("o_column=%s" % CONFIG["o_column"])
|
||||||
|
params.append("o_direction=%s" % CONFIG["o_direction"])
|
||||||
|
params.append("comparison_type=%s" % CONFIG["comparison_type"])
|
||||||
|
|
||||||
|
url = base + "?" + "&".join(params)
|
||||||
|
return url, "ok", ""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="批量拼接 YouTube Studio explore URL")
|
||||||
|
parser.add_argument("-i", "--input", required=True, help="需求清单文件(.csv / .xlsx / .xls)")
|
||||||
|
parser.add_argument("-o", "--output", default=None, help="输出 CSV 路径(默认:输入同目录 studio_urls_output.csv)")
|
||||||
|
parser.add_argument("--countries", default=None, help="国家映射 JSON 路径(默认:脚本同目录 countries.json)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.exists(args.input):
|
||||||
|
print("错误:输入文件不存在: %s" % args.input, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
ext = os.path.splitext(args.input)[1].lower()
|
||||||
|
if ext == ".csv":
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(args.input, encoding="utf-8-sig")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
df = pd.read_csv(args.input, encoding="gbk") # Excel 另存的 ANSI/GBK
|
||||||
|
elif ext in (".xlsx", ".xls"):
|
||||||
|
df = pd.read_excel(args.input)
|
||||||
|
else:
|
||||||
|
print("错误:不支持的输入格式: %s(支持 .csv/.xlsx/.xls)" % ext, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
df = df.dropna(how="all") # 去掉全空行
|
||||||
|
|
||||||
|
mapping, unknown = normalize_columns(df)
|
||||||
|
if unknown:
|
||||||
|
print("[提示] 未识别的列(忽略): %s" % ", ".join(unknown), file=sys.stderr)
|
||||||
|
missing = [f for f in ("owner_id", "period") if f not in mapping]
|
||||||
|
if missing:
|
||||||
|
print("错误:输入缺少必要列: %s" % ", ".join(missing), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
countries_file = args.countries or os.path.join(os.path.dirname(os.path.abspath(__file__)), "countries.json")
|
||||||
|
country_map = load_country_map(countries_file)
|
||||||
|
|
||||||
|
records = []
|
||||||
|
errors = []
|
||||||
|
for idx, raw in df.iterrows():
|
||||||
|
row = {f: ("" if pd.isna(raw[mapping[f]]) else str(raw[mapping[f]])) for f in mapping}
|
||||||
|
try:
|
||||||
|
url, status, msg = build_url(row, country_map)
|
||||||
|
except ValueError as e:
|
||||||
|
url, status, msg = None, "error", str(e)
|
||||||
|
if status == "ok":
|
||||||
|
start_ms, end_ms = parse_period(row.get("period", ""))
|
||||||
|
codes = parse_countries(row.get("countries", ""), country_map)
|
||||||
|
records.append({
|
||||||
|
"所有者名称": row.get("owner_name", ""),
|
||||||
|
"所有者ID": row.get("owner_id", ""),
|
||||||
|
"实体类型": row.get("entity_type", ""),
|
||||||
|
"实体名称": row.get("entity_name", ""),
|
||||||
|
"实体ID": row.get("entity_id", ""),
|
||||||
|
"数据周期": row.get("period", ""),
|
||||||
|
"国家": row.get("countries", ""),
|
||||||
|
"国家代码": ",".join(codes),
|
||||||
|
"开始时间戳": start_ms,
|
||||||
|
"结束时间戳": end_ms,
|
||||||
|
"URL": url,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
errors.append((idx + 2, row.get("owner_name", ""), row.get("entity_name", ""), msg))
|
||||||
|
|
||||||
|
out_path = args.output or os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(args.input)), "studio_urls_output.csv")
|
||||||
|
out_df = pd.DataFrame(records)
|
||||||
|
out_df.to_csv(out_path, index=False, encoding="utf-8-sig")
|
||||||
|
print("已生成 %d 条 URL -> %s" % (len(records), out_path))
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("\n以下 %d 行生成失败:" % len(errors), file=sys.stderr)
|
||||||
|
for r in errors:
|
||||||
|
print(" 第%d行 所有者=%s 实体=%s:%s" % r, file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
24
scripts/countries.json
Normal file
24
scripts/countries.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"美国": "US", "日本": "JP", "英国": "GB", "德国": "DE", "法国": "FR",
|
||||||
|
"意大利": "IT", "西班牙": "ES", "加拿大": "CA", "澳大利亚": "AU", "韩国": "KR",
|
||||||
|
"巴西": "BR", "墨西哥": "MX", "印度": "IN", "俄罗斯": "RU", "荷兰": "NL",
|
||||||
|
"瑞典": "SE", "挪威": "NO", "芬兰": "FI", "丹麦": "DK", "比利时": "BE",
|
||||||
|
"瑞士": "CH", "奥地利": "AT", "波兰": "PL", "葡萄牙": "PT", "爱尔兰": "IE",
|
||||||
|
"新西兰": "NZ", "新加坡": "SG", "马来西亚": "MY", "泰国": "TH", "越南": "VN",
|
||||||
|
"印度尼西亚": "ID", "菲律宾": "PH", "土耳其": "TR", "沙特阿拉伯": "SA",
|
||||||
|
"阿联酋": "AE", "以色列": "IL", "南非": "ZA", "阿根廷": "AR", "智利": "CL",
|
||||||
|
"哥伦比亚": "CO", "秘鲁": "PE", "埃及": "EG", "尼日利亚": "NG", "中国": "CN",
|
||||||
|
"中国台湾": "TW", "台湾": "TW", "中国香港": "HK", "香港": "HK", "中国澳门": "MO", "澳门": "MO",
|
||||||
|
"乌克兰": "UA", "希腊": "GR", "捷克": "CZ", "匈牙利": "HU", "罗马尼亚": "RO",
|
||||||
|
"保加利亚": "BG", "克罗地亚": "HR", "斯洛伐克": "SK", "斯洛文尼亚": "SI",
|
||||||
|
"立陶宛": "LT", "拉脱维亚": "LV", "爱沙尼亚": "EE", "塞尔维亚": "RS", "冰岛": "IS",
|
||||||
|
"卢森堡": "LU", "马耳他": "MT", "塞浦路斯": "CY", "巴基斯坦": "PK",
|
||||||
|
"孟加拉国": "BD", "斯里兰卡": "LK", "哈萨克斯坦": "KZ", "卡塔尔": "QA",
|
||||||
|
"科威特": "KW", "巴林": "BH", "阿曼": "OM", "约旦": "JO", "黎巴嫩": "LB",
|
||||||
|
"摩洛哥": "MA", "阿尔及利亚": "DZ", "突尼斯": "TN", "肯尼亚": "KE",
|
||||||
|
"加纳": "GH", "坦桑尼亚": "TZ", "埃塞俄比亚": "ET", "玻利维亚": "BO",
|
||||||
|
"厄瓜多尔": "EC", "乌拉圭": "UY", "巴拉圭": "PY", "委内瑞拉": "VE",
|
||||||
|
"巴拿马": "PA", "哥斯达黎加": "CR", "古巴": "CU", "多米尼加": "DO",
|
||||||
|
"波多黎各": "PR", "危地马拉": "GT", "洪都拉斯": "HN", "萨尔瓦多": "SV",
|
||||||
|
"尼加拉瓜": "NI"
|
||||||
|
}
|
||||||
12
scripts/install-skills.bat
Normal file
12
scripts/install-skills.bat
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
@echo off
|
||||||
|
rem Trae CN skill installer launcher - double click to run
|
||||||
|
rem Actual logic lives in install-skills.ps1 (same folder)
|
||||||
|
cd /d "%~dp0"
|
||||||
|
if not exist "install-skills.ps1" (
|
||||||
|
echo [ERROR] install-skills.ps1 not found in %~dp0
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
powershell -NoProfile -ExecutionPolicy Bypass -File "install-skills.ps1"
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
92
scripts/install-skills.ps1
Normal file
92
scripts/install-skills.ps1
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# =====================================================================
|
||||||
|
# Trae CN 技能安装器(Windows)
|
||||||
|
# 作用:把本项目 skills\ 下全部技能安装到用户级技能目录
|
||||||
|
# %USERPROFILE%\.trae-cn\skills\
|
||||||
|
# 用法:双击同目录下的 install-skills.bat(推荐),或
|
||||||
|
# powershell -NoProfile -ExecutionPolicy Bypass -File install-skills.ps1
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
# --- 1. 定位源目录与目标目录 ---
|
||||||
|
$ScriptDir = $PSScriptRoot
|
||||||
|
$ProjectRoot = Split-Path -Parent $ScriptDir
|
||||||
|
$SkillsSource = Join-Path $ProjectRoot "skills"
|
||||||
|
$TraeSkills = Join-Path $env:USERPROFILE ".trae-cn\skills"
|
||||||
|
# 备份放在技能目录外,避免被 Trae 当成技能重复扫描
|
||||||
|
$BackupRoot = Join-Path $env:USERPROFILE ".trae-cn\skills-backup"
|
||||||
|
|
||||||
|
Write-Host "=== Trae CN 技能安装器 ===" -ForegroundColor Cyan
|
||||||
|
Write-Host "技能源目录: $SkillsSource"
|
||||||
|
Write-Host "安装目标: $TraeSkills"
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# --- 2. 校验源目录并收集技能(含 SKILL.md 的子目录才算) ---
|
||||||
|
if (-not (Test-Path $SkillsSource)) {
|
||||||
|
Write-Host "[错误] 找不到技能源目录: $SkillsSource" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$skills = @(Get-ChildItem -Path $SkillsSource -Directory | Where-Object {
|
||||||
|
Test-Path (Join-Path $_.FullName "SKILL.md")
|
||||||
|
})
|
||||||
|
|
||||||
|
if ($skills.Count -eq 0) {
|
||||||
|
Write-Host "[错误] 源目录下没有可用技能(每个技能文件夹需包含 SKILL.md)。" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ("发现 {0} 个技能: {1}" -f $skills.Count, ($skills.Name -join ", "))
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# --- 3. 逐个安装:旧版先备份,再复制新版 ---
|
||||||
|
New-Item -ItemType Directory -Force -Path $TraeSkills | Out-Null
|
||||||
|
|
||||||
|
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
|
||||||
|
$backupCount = 0
|
||||||
|
|
||||||
|
foreach ($skill in $skills) {
|
||||||
|
$dest = Join-Path $TraeSkills $skill.Name
|
||||||
|
|
||||||
|
if (Test-Path $dest) {
|
||||||
|
New-Item -ItemType Directory -Force -Path $BackupRoot | Out-Null
|
||||||
|
$backupPath = Join-Path $BackupRoot ("{0}-{1}" -f $skill.Name, $timestamp)
|
||||||
|
Move-Item -Path $dest -Destination $backupPath
|
||||||
|
$backupCount++
|
||||||
|
Write-Host "[备份] $($skill.Name) 旧版 -> $backupPath" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Copy-Item -Path $skill.FullName -Destination $dest -Recurse -Force
|
||||||
|
Write-Host "[安装] $($skill.Name)" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 4. 校验安装结果 ---
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "=== 校验结果 ==="
|
||||||
|
$failed = @()
|
||||||
|
foreach ($skill in $skills) {
|
||||||
|
if (Test-Path (Join-Path $TraeSkills "$($skill.Name)\SKILL.md")) {
|
||||||
|
Write-Host " [OK] $($skill.Name)" -ForegroundColor Green
|
||||||
|
} else {
|
||||||
|
Write-Host " [FAIL] $($skill.Name)" -ForegroundColor Red
|
||||||
|
$failed += $skill.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# --- 5. 汇总与后续提示 ---
|
||||||
|
Write-Host ""
|
||||||
|
if ($failed.Count -gt 0) {
|
||||||
|
Write-Host ("安装失败: {0}" -f ($failed -join ", ")) -ForegroundColor Red
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host "全部安装成功。" -ForegroundColor Green
|
||||||
|
if ($backupCount -gt 0) {
|
||||||
|
Write-Host "旧版本备份于: $BackupRoot(确认新版可用后可手动删除)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "下一步: 重启 Trae CN 或新建会话后,在对话中提及技能名或相关意图即可触发:"
|
||||||
|
foreach ($s in $skills) {
|
||||||
|
Write-Host " - $($s.Name)"
|
||||||
|
}
|
||||||
306
scripts/youtube_export_interceptor.py
Normal file
306
scripts/youtube_export_interceptor.py
Normal file
@@ -0,0 +1,306 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r"""
|
||||||
|
YouTube Studio 内容管理器「导出当前视图 → 逗号分隔值 (.csv)」下载流程 —— 拦截/解码/自动保存/重名去重 参考实现。
|
||||||
|
|
||||||
|
## 已实证的下载生成机制(2026-08-21 抓包确认)
|
||||||
|
|
||||||
|
1. 前端点击「导出当前视图 → 逗号分隔值 (.csv)」后,向后端发起:
|
||||||
|
POST https://studio.youtube.com/youtubei/v1/yta_web/csv_export?alt=json
|
||||||
|
请求体 `exportQuery` 内含 joinRequest 各节点(表格数据/图表数据/总计),以及
|
||||||
|
日期范围 `dateIdRange.inclusiveStart` / `dateIdRange.exclusiveEnd`(都是 YYYYMMDD)。
|
||||||
|
|
||||||
|
2. 后端**不在服务器上生成一个可下载的 URL**,而是直接把打好的 zip 以
|
||||||
|
**base64 字符串**内联在响应里:
|
||||||
|
{ "responseContext": {...}, "zippedData": "<base64>" }
|
||||||
|
实测 `zippedData` 以 `UEsDBBQ...` 开头(即 `PK\\x03\\x04`,ZIP 魔数),
|
||||||
|
base64 解码后得到 zip,内含 `表格数据.csv`、`图表数据.csv`、`总计.csv`。
|
||||||
|
|
||||||
|
3. 前端把 `zippedData` base64 解码 → Blob → 触发浏览器下载。
|
||||||
|
由于没有出现指向下载文件的 GET/跳转,判定为「客户端 Blob 下载」而非服务端重定向。
|
||||||
|
|
||||||
|
4. 浏览器自身已自动保存到本机默认下载目录(本机为 `D:\\Downloads`),无需"另存为"确认;
|
||||||
|
且 Chromium 对重名文件会自动追加 ` (1)`、` (2)` …后缀。
|
||||||
|
|
||||||
|
## 结论:可以跨越下载流程
|
||||||
|
- 在响应层拦截 `csv_export`,拿到 `zippedData`,自行 base64 解码并写盘,
|
||||||
|
即可完全掌控「保存目录 + 文件名 + 重名去重」,不依赖浏览器的下载管理器和弹窗。
|
||||||
|
- 或者只用 Chromium 的下载偏好(auto-download + 内置去重)让它自动落盘。
|
||||||
|
|
||||||
|
## 文件名约定(与实测 D:\\Downloads 中产物一致)
|
||||||
|
<维度标签> <inclusiveStart>_<exclusiveEnd> <账号名>.zip
|
||||||
|
例:内容 2026-07-23_2026-08-20 WL Media.zip
|
||||||
|
- 维度标签:VIDEO -> 内容;USER -> 频道(生产中建议从页面"维度"按钮文本读取)
|
||||||
|
- 日期格式 YYYY-MM-DD:inclusiveStart 与 exclusiveEnd 各取 YYYYMMDD 转 YYYY-MM-DD
|
||||||
|
- 账号名:右上角账号按钮文本
|
||||||
|
|
||||||
|
自测(无需 Playwright): python youtube_export_interceptor.py --selftest
|
||||||
|
|
||||||
|
实际运行(需 Playwright;必须复用已登录 YouTube Studio 的浏览器会话,否则跳登录页):
|
||||||
|
方式 A(用已登录的用户数据目录启动,需先关闭 Chrome/Edge):
|
||||||
|
python youtube_export_interceptor.py --url "<explore URL>" --channel chrome ^
|
||||||
|
--user-data-dir "%LOCALAPPDATA%\Google\Chrome\User Data"
|
||||||
|
方式 B(附加到已在调试端口运行的浏览器,无需关闭):
|
||||||
|
python youtube_export_interceptor.py --url "<explore URL>" --connect http://localhost:9222
|
||||||
|
# 先启动: chrome.exe --remote-debugging-port=9222 或 msedge.exe --remote-debugging-port=9222
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# 本机 Windows 下载目录。可改为 os.path.expanduser("~") / "Downloads"。
|
||||||
|
DOWNLOAD_DIR = r"D:\Downloads"
|
||||||
|
|
||||||
|
# 维度类型 -> 文件名前缀标签(生产环境请从页面「维度」按钮文本读取,这里兜底映射)。
|
||||||
|
DIMENSION_LABEL = {
|
||||||
|
"VIDEO": "内容",
|
||||||
|
"USER": "频道",
|
||||||
|
"CONTENT_OWNER": "内容",
|
||||||
|
}
|
||||||
|
|
||||||
|
CSV_EXPORT_PATH = "/youtubei/v1/yta_web/csv_export"
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 重名去重:需求文件.zip -> 需求文件 (1).zip -> 需求文件 (2).zip ... #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def dedup_path(directory, filename):
|
||||||
|
"""返回不冲突的落盘路径。重名时按 `名称 (n).后缀` 递增,n 从 1 开始。
|
||||||
|
|
||||||
|
规则与用户要求一致:第二次同名保存 `xx (1).zip`,第三次 `xx (2).zip`,以此类推;
|
||||||
|
若 `xx (1).zip` 也已存在,则继续找 `xx (2).zip`(即取最小无冲突的 n)。
|
||||||
|
"""
|
||||||
|
directory = os.path.abspath(directory)
|
||||||
|
base, ext = os.path.splitext(filename)
|
||||||
|
candidate = os.path.join(directory, filename)
|
||||||
|
n = 1
|
||||||
|
while os.path.exists(candidate):
|
||||||
|
candidate = os.path.join(directory, f"{base} ({n}){ext}")
|
||||||
|
n += 1
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
def build_export_filename(export_query, account_name, dimension_label=None):
|
||||||
|
"""从 csv_export 请求体 `exportQuery` 反推导出文件名。
|
||||||
|
|
||||||
|
与实测产物命名一致:`<维度标签> <inclusiveStart>_<exclusiveEnd> <账号名>.zip`
|
||||||
|
"""
|
||||||
|
def fmt_dateid(yyyymmdd):
|
||||||
|
s = str(yyyymmdd)
|
||||||
|
return f"{s[0:4]}-{s[4:6]}-{s[6:8]}"
|
||||||
|
|
||||||
|
# 从任意一个 joinRequest 节点取 dateIdRange
|
||||||
|
date_range = None
|
||||||
|
dimension = None
|
||||||
|
nodes = (export_query.get("joinRequest", {}).get("nodes") or [])
|
||||||
|
for node in nodes:
|
||||||
|
q = (node.get("value", {}).get("query") or {})
|
||||||
|
if not date_range:
|
||||||
|
tr = q.get("timeRange", {}).get("dateIdRange")
|
||||||
|
if tr and tr.get("inclusiveStart"):
|
||||||
|
date_range = (tr["inclusiveStart"], tr.get("exclusiveEnd"))
|
||||||
|
dims = q.get("dimensions") or []
|
||||||
|
if dimension is None and dims:
|
||||||
|
dimension = dims[0].get("type")
|
||||||
|
|
||||||
|
if not date_range:
|
||||||
|
raise ValueError("无法从 exportQuery 解析日期范围")
|
||||||
|
|
||||||
|
if dimension_label is None:
|
||||||
|
dimension_label = DIMENSION_LABEL.get(dimension or "", "")
|
||||||
|
|
||||||
|
start, end = date_range
|
||||||
|
return f"{dimension_label} {fmt_dateid(start)}_{fmt_dateid(end)} {account_name}.zip"
|
||||||
|
|
||||||
|
|
||||||
|
def decode_zipped_data(payload):
|
||||||
|
"""把 csv_export 响应 payload 里的 zippedData 解码为 zip 字节流。"""
|
||||||
|
zipped = payload.get("zippedData")
|
||||||
|
if not zipped:
|
||||||
|
raise ValueError("响应中缺少 zippedData 字段")
|
||||||
|
return base64.b64decode(zipped)
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Playwright 主流程(拦截响应 -> 解码 -> 去重 -> 落盘) #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def intercept_and_save(page, account_name):
|
||||||
|
"""给 page 绑定 response 拦截器:命中 csv_export 就把 zip 保存到下载目录。"""
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
saved = []
|
||||||
|
|
||||||
|
def on_response(response):
|
||||||
|
if CSV_EXPORT_PATH not in response.url:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
payload = response.json()
|
||||||
|
data = decode_zipped_data(payload)
|
||||||
|
|
||||||
|
# 反推文件名:请求体在 response.request.post_data 里不总是可读,
|
||||||
|
# 这里从已捕获的 body 兜底;找不到就用时间戳命名,保证不误覆盖。
|
||||||
|
filename = None
|
||||||
|
try:
|
||||||
|
body = json.loads(response.request.post_data or "{}")
|
||||||
|
filename = build_export_filename(body.get("exportQuery", {}), account_name)
|
||||||
|
except Exception:
|
||||||
|
filename = f"export-{response.request.headers.get('date', '')}.zip"
|
||||||
|
|
||||||
|
filename = re.sub(r"[\\/:*?\"<>|]", "_", filename) # Windows 非法字符
|
||||||
|
path = dedup_path(DOWNLOAD_DIR, filename)
|
||||||
|
pathlib.Path(path).write_bytes(data)
|
||||||
|
saved.append((filename, len(data), path))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[interceptor] 处理 csv_export 响应失败: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
page.on("response", on_response)
|
||||||
|
return saved
|
||||||
|
|
||||||
|
|
||||||
|
def _default_user_data_dir(channel):
|
||||||
|
"""返回指定浏览器的默认用户数据目录(Windows),用于复用已登录会话。"""
|
||||||
|
_local = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
|
||||||
|
if channel == "msedge":
|
||||||
|
return os.path.join(_local, "Microsoft", "Edge", "User Data")
|
||||||
|
return os.path.join(_local, "Google", "Chrome", "User Data")
|
||||||
|
|
||||||
|
|
||||||
|
def run(url, user_data_dir=None, channel=None, cdp_url=None):
|
||||||
|
"""启动/连接浏览器并触发导出。
|
||||||
|
|
||||||
|
关键:必须复用「已登录 YouTube Studio」的浏览器会话,否则会跳 Google 登录页。
|
||||||
|
- cdp_url: 附加到已在调试端口运行的浏览器(推荐,无需关闭浏览器)
|
||||||
|
- user_data_dir:用已登录的用户数据目录启动持久化上下文(需先关闭该浏览器)
|
||||||
|
- 两者都不传: 新建空白会话(大概率未登录,仅作占位/调试)
|
||||||
|
"""
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = None
|
||||||
|
context = None
|
||||||
|
|
||||||
|
if cdp_url:
|
||||||
|
# 方式 B:附加到已打开、已登录的浏览器(先以调试端口启动浏览器)
|
||||||
|
browser = p.chromium.connect_over_cdp(cdp_url)
|
||||||
|
context = browser.contexts[0] if browser.contexts else \
|
||||||
|
browser.new_context(accept_downloads=True)
|
||||||
|
page = context.new_page()
|
||||||
|
page.goto(url, wait_until="domcontentloaded")
|
||||||
|
elif user_data_dir:
|
||||||
|
# 方式 A:用已登录的用户数据目录启动(cookies 复用;必须先关闭同名浏览器)
|
||||||
|
context = p.chromium.launch_persistent_context(
|
||||||
|
user_data_dir=user_data_dir or _default_user_data_dir(channel),
|
||||||
|
channel=channel,
|
||||||
|
headless=False,
|
||||||
|
accept_downloads=True,
|
||||||
|
args=["--disable-blink-features=AutomationControlled"],
|
||||||
|
)
|
||||||
|
page = context.new_page()
|
||||||
|
page.goto(url, wait_until="domcontentloaded")
|
||||||
|
else:
|
||||||
|
browser = p.chromium.launch(headless=False, channel=channel)
|
||||||
|
context = browser.new_context(accept_downloads=True)
|
||||||
|
page = context.new_page()
|
||||||
|
page.goto(url, wait_until="domcontentloaded")
|
||||||
|
|
||||||
|
if "studio.youtube.com" not in page.url and "accounts.google" in page.url:
|
||||||
|
print("[!] 当前会话未登录,已跳转到 Google 登录页。", file=sys.stderr)
|
||||||
|
print(" 请用 --user-data-dir 复用已登录浏览器,或先手动登录后再试。",
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
# 账号名:右上角账号按钮(示例选择器,按实际页面微调)
|
||||||
|
account_name = "WL Media"
|
||||||
|
try:
|
||||||
|
account_name = page.locator(
|
||||||
|
"ytcp-account-item button, .account-switcher button"
|
||||||
|
).first.inner_text(timeout=5000).strip() or account_name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
saved = intercept_and_save(page, account_name)
|
||||||
|
|
||||||
|
# 触发导出:点「导出当前视图」→「逗号分隔值 (.csv)」
|
||||||
|
page.get_by_text("导出当前视图").click()
|
||||||
|
page.get_by_text("逗号分隔值 (.csv)").click()
|
||||||
|
|
||||||
|
page.wait_for_timeout(3000)
|
||||||
|
if not saved:
|
||||||
|
print("未捕获到 csv_export 响应,请确认已点击导出且登录态有效。")
|
||||||
|
else:
|
||||||
|
for name, size, path in saved:
|
||||||
|
print(f"已保存: {path} ({size} bytes)")
|
||||||
|
|
||||||
|
if browser is not None:
|
||||||
|
browser.close()
|
||||||
|
elif context is not None:
|
||||||
|
context.close()
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# 自测 #
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
def selftest():
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
# 1) 用户示例:需求文件.zip 第二次 -> 需求文件 (1).zip,第三次 -> (2).zip
|
||||||
|
p0 = Path(dedup_path(td, "需求文件.zip"))
|
||||||
|
assert p0.name == "需求文件.zip", p0.name
|
||||||
|
p0.write_bytes(b"a")
|
||||||
|
|
||||||
|
p1 = Path(dedup_path(td, "需求文件.zip"))
|
||||||
|
assert p1.name == "需求文件 (1).zip", p1.name
|
||||||
|
p1.write_bytes(b"b")
|
||||||
|
|
||||||
|
p2 = Path(dedup_path(td, "需求文件.zip"))
|
||||||
|
assert p2.name == "需求文件 (2).zip", p2.name
|
||||||
|
p2.write_bytes(b"c")
|
||||||
|
|
||||||
|
# 2) 若 (1) 已存在,也应跳到 (2)
|
||||||
|
assert Path(dedup_path(td, "需求文件.zip")).name == "需求文件 (3).zip"
|
||||||
|
|
||||||
|
# 3) 无冲突时不加后缀
|
||||||
|
p3 = Path(dedup_path(td, "其他.zip"))
|
||||||
|
assert p3.name == "其他.zip", p3.name
|
||||||
|
|
||||||
|
# 4) 文件名反推(对应实测产物「内容 2026-07-23_2026-08-20 WL Media.zip」)
|
||||||
|
export_query = {
|
||||||
|
"joinRequest": {"nodes": [{"value": {"query": {
|
||||||
|
"dimensions": [{"type": "VIDEO"}],
|
||||||
|
"timeRange": {"dateIdRange": {
|
||||||
|
"inclusiveStart": 20260723, "exclusiveEnd": 20260820}},
|
||||||
|
}}}]},
|
||||||
|
}
|
||||||
|
name = build_export_filename(export_query, "WL Media")
|
||||||
|
assert name == "内容 2026-07-23_2026-08-20 WL Media.zip", name
|
||||||
|
|
||||||
|
print("selftest OK:去重与文件名反推逻辑全部通过")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--selftest", action="store_true", help="仅跑去重/命名自测")
|
||||||
|
ap.add_argument("--url", help="explore URL")
|
||||||
|
ap.add_argument("--user-data-dir", help="浏览器用户数据目录(复用登录态,需先关闭该浏览器)")
|
||||||
|
ap.add_argument("--channel", choices=["chrome", "msedge"],
|
||||||
|
help="浏览器品牌:chrome / msedge(复用登录态时必填其一)")
|
||||||
|
ap.add_argument("--connect", help="通过 CDP 附加到已打开的浏览器,如 http://localhost:9222")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.selftest:
|
||||||
|
selftest()
|
||||||
|
elif args.url:
|
||||||
|
run(args.url, user_data_dir=args.user_data_dir,
|
||||||
|
channel=args.channel, cdp_url=args.connect)
|
||||||
|
else:
|
||||||
|
selftest()
|
||||||
|
print("\n实际运行请先: pip install playwright\n"
|
||||||
|
"复用登录态(必选其一,详见脚本顶部 docstring):\n"
|
||||||
|
" 方式 A: python youtube_export_interceptor.py --url \"<explore URL>\" "
|
||||||
|
"--channel chrome --user-data-dir \"%LOCALAPPDATA%\\Google\\Chrome\\User Data\"\n"
|
||||||
|
" 方式 B: python youtube_export_interceptor.py --url \"<explore URL>\" "
|
||||||
|
"--connect http://localhost:9222")
|
||||||
61
skills/uv-env-setup/SKILL.md
Normal file
61
skills/uv-env-setup/SKILL.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: "uv-env-setup"
|
||||||
|
description: "用 uv 准备并维护本项目 Python 运行环境(pyproject.toml → uv sync → uv run)。当用户提到环境准备/环境初始化/装依赖/重建 venv,脚本报 ModuleNotFoundError(pandas/openpyxl/playwright),.venv 缺失或损坏,或首次运行 scripts/、skills/*/scripts/ 下的 Python 脚本时使用。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# uv 运行环境准备
|
||||||
|
|
||||||
|
本项目所有 Python 脚本共用一套 uv 管理的环境:根目录 `pyproject.toml` 是依赖的**唯一事实来源**,`uv sync` 落地 `.venv\`,`uv run` 执行脚本(自动使用该环境,免激活、免手装依赖)。
|
||||||
|
|
||||||
|
## 步骤 1:确认 uv 可用
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv --version
|
||||||
|
```
|
||||||
|
|
||||||
|
未安装时任选其一(Windows):
|
||||||
|
|
||||||
|
- `winget install astral-sh.uv`
|
||||||
|
- `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`
|
||||||
|
- `pip install uv`
|
||||||
|
|
||||||
|
装完重开终端使 PATH 生效。
|
||||||
|
|
||||||
|
**完成判据**:`uv --version` 打印出版本号。
|
||||||
|
|
||||||
|
## 步骤 2:同步依赖
|
||||||
|
|
||||||
|
项目根目录(`pyproject.toml` 所在处)执行:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
- 首次运行自动创建 `.venv\` 并安装全部依赖(pandas、openpyxl、playwright)。
|
||||||
|
- 本机无兼容 Python 时,uv 自动下载托管 Python(`requires-python = ">=3.10"`)。
|
||||||
|
- `uv run` 本身也会隐式同步;显式 `uv sync` 是为了在跑脚本前把依赖错误一次性暴露出来。
|
||||||
|
|
||||||
|
**完成判据**:命令退出码 0,且项目根出现 `.venv\` 目录。
|
||||||
|
|
||||||
|
## 步骤 3:验证
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run python -c "import pandas, openpyxl, playwright; print('env OK')"
|
||||||
|
uv run python skills\youtube-studio-csv-download\scripts\youtube_export_download.py --selftest
|
||||||
|
uv run python skills\yt-studio-url-builder\scripts\build_studio_urls.py --help
|
||||||
|
```
|
||||||
|
|
||||||
|
**完成判据**:三条命令均成功——打印 `env OK`、`selftest OK`、脚本的用法帮助。
|
||||||
|
|
||||||
|
## 其他技能如何使用本环境
|
||||||
|
|
||||||
|
- 统一在项目根目录用 `uv run python <脚本> [参数]` 执行,uv 自动向上定位 `pyproject.toml` 并复用其环境。
|
||||||
|
- 不用裸 `python`(依赖 PATH 碰运气),不手动激活 venv,不 `pip install` 到全局。
|
||||||
|
|
||||||
|
## 新增依赖
|
||||||
|
|
||||||
|
改 `pyproject.toml` 的 `dependencies`(唯一入口),再 `uv sync` 更新 `uv.lock`。禁止 `pip install` / `uv pip install` 直装——绕过 lock,环境不可复现。
|
||||||
|
|
||||||
|
## 排查
|
||||||
|
|
||||||
|
网络慢/超时、uv 安装失败、`.venv` 损坏重建、多版本 Python 冲突等,查 [references/troubleshooting.md](references/troubleshooting.md)。
|
||||||
49
skills/uv-env-setup/references/troubleshooting.md
Normal file
49
skills/uv-env-setup/references/troubleshooting.md
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
# 环境排查
|
||||||
|
|
||||||
|
## uv 安装与 PATH
|
||||||
|
|
||||||
|
- **`uv: command not found` / 无法识别**:装完未重开终端,PATH 未生效;重开终端或手动刷新 `$env:Path`。
|
||||||
|
- **winget 安装失败**:改用官方脚本 `powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"`,或 `pip install uv`。
|
||||||
|
|
||||||
|
## 网络与镜像(国内环境)
|
||||||
|
|
||||||
|
`uv sync` / `uv run` 拉包慢或超时时,临时切换清华镜像:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:UV_DEFAULT_INDEX = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
要持久化则写入 `pyproject.toml`:
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[[tool.uv.index]]
|
||||||
|
url = "https://pypi.tuna.tsinghua.edu.cn/simple"
|
||||||
|
default = true
|
||||||
|
```
|
||||||
|
|
||||||
|
## sync 相关
|
||||||
|
|
||||||
|
- **`No solution found` / 依赖解析冲突**:`dependencies` 里的版本约束互相矛盾。放宽或修正 `pyproject.toml` 后重试。
|
||||||
|
- **lock 与 pyproject 不一致**:`uv.lock` 过期,`uv sync` 会自动更新;若报 lock 损坏,删除 `uv.lock` 后重新 `uv sync`。
|
||||||
|
- **`requires-python` 不满足**:本机 Python 全部低于 3.10。让 uv 自动下载即可:`uv sync -p 3.12`(或省略,uv 自选)。
|
||||||
|
|
||||||
|
## .venv 损坏 / 重建
|
||||||
|
|
||||||
|
症状:`uv run` 报奇怪的导入错误、DLL 加载失败,或 `.venv` 被移动过。
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Remove-Item -Recurse -Force .venv
|
||||||
|
uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
## playwright 相关
|
||||||
|
|
||||||
|
- **`ModuleNotFoundError: playwright`**:环境未同步。项目根执行 `uv sync`,或直接用 `uv run python <脚本>`(隐式同步)。
|
||||||
|
- **复用系统 Chrome/Edge 无需 `playwright install`**:只有用 uv 环境内置 Chromium 时才需要 `uv run playwright install chromium`。
|
||||||
|
- **`playwright install` 下载浏览器慢**:`$env:PLAYWRIGHT_DOWNLOAD_HOST = "https://npmmirror.com/mirrors/playwright"` 后重试。
|
||||||
|
|
||||||
|
## 与全局环境隔离
|
||||||
|
|
||||||
|
- 本项目所有命令都经 `uv run`,不会污染全局 site-packages。
|
||||||
|
- 若此前 `pip install` 过 playwright/pandas 到全局,与本 `.venv` 无冲突,但项目内一律走 `uv run`。
|
||||||
61
skills/youtube-studio-csv-download/SKILL.md
Normal file
61
skills/youtube-studio-csv-download/SKILL.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
---
|
||||||
|
name: "youtube-studio-csv-download"
|
||||||
|
description: "Download YouTube Studio analytics CSV export (zip) via a Playwright script that reuses a logged-in browser user-data dir or debug port, auto-saving to the Downloads folder with (n) dedup. Use when the user wants to download/save YT Studio CSV, or supplies a user-data path / remote-debugging-port."
|
||||||
|
---
|
||||||
|
|
||||||
|
# 下载 YouTube Studio 分析 CSV(脚本版)
|
||||||
|
|
||||||
|
把 YouTube Studio 内容管理器「导出当前视图 → 逗号分隔值 (.csv)」的下载,改由 Playwright 拦截脚本完成:复用已登录的 Chrome/Edge 会话,自动保存 zip 到下载文件夹,重名自动追加 ` (n)` 后缀。
|
||||||
|
|
||||||
|
## 与 MCP 浏览器版技能的区别
|
||||||
|
|
||||||
|
- 本技能:用户**主动给了浏览器 user-data 目录或调试端口**、或明确要"跑脚本下载"时使用。
|
||||||
|
- `youtube-studio-analytics-export`:走 MCP `integrated_browser`、界面点击兜底,适合交互式/按 URL 直连。
|
||||||
|
两者产物相同(zip 内含 `表格数据.csv` / `图表数据.csv` / `总计.csv`),只选一条路径即可。
|
||||||
|
|
||||||
|
## 脚本与依赖
|
||||||
|
|
||||||
|
- 脚本:`scripts/youtube_export_download.py`(本技能目录下;若缺失,用 SearchCodebase 按 `youtube_export_download.py` 定位)
|
||||||
|
- 依赖:playwright 已在根 `pyproject.toml` 统一声明,环境准备见 `uv-env-setup` 技能(复用系统 Chrome/Edge 时无需 `playwright install chromium`)
|
||||||
|
- 登录态必须已存在:脚本不登录,只复用已登录会话。
|
||||||
|
|
||||||
|
## 步骤 1:确认登录态来源(二选一)
|
||||||
|
|
||||||
|
复用已登录 YouTube Studio 的浏览器会话,否则会跳到 Google 登录页。
|
||||||
|
|
||||||
|
- 方式 A(能关浏览器):用已登录的**用户数据目录**。
|
||||||
|
- 方式 B(浏览器不能关):用**调试端口**附加。
|
||||||
|
|
||||||
|
**完成判据**:得到下面两者之一——
|
||||||
|
- user-data 目录路径(Chrome:`<LOCALAPPDATA>\Google\Chrome\User Data`;Edge:`<LOCALAPPDATA>\Microsoft\Edge\User Data`),或
|
||||||
|
- 调试端口号(如 `9222`)。
|
||||||
|
|
||||||
|
## 步骤 2:运行脚本
|
||||||
|
|
||||||
|
方式 A:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run python scripts\youtube_export_download.py --url "<explore URL>" --channel chrome --user-data-dir "$env:LOCALAPPDATA\Google\Chrome\User Data"
|
||||||
|
```
|
||||||
|
|
||||||
|
方式 B(先 `chrome.exe --remote-debugging-port=9222` 或 `msedge.exe --remote-debugging-port=9222` 打开已登录浏览器):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
uv run python scripts\youtube_export_download.py --url "<explore URL>" --connect http://localhost:9222
|
||||||
|
```
|
||||||
|
|
||||||
|
`--channel` 只允许 `chrome` / `msedge`,且必须与 `--user-data-dir` 指向的浏览器**同一品牌**。
|
||||||
|
|
||||||
|
**完成判据**:终端打印 `已保存: <完整路径> (<字节数>)`,退出码 0。
|
||||||
|
|
||||||
|
## 步骤 3:校验产物
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Get-ChildItem -Path "D:\Downloads" -Filter "*.zip" | Sort-Object LastWriteTime -Descending | Select-Object -First 3 Name, Length, LastWriteTime
|
||||||
|
```
|
||||||
|
|
||||||
|
**完成判据**:存在最新 `<维度标签> <起始日>_<结束日> <账号名>.zip`(如 `内容 2026-07-23_2026-08-20 WL Media.zip`);同名重复时后缀为 ` (1)`、` (2)`。
|
||||||
|
|
||||||
|
## 遇到问题
|
||||||
|
|
||||||
|
查 `references/troubleshooting.md`(未登录跳转、目录被占用、CDP 连不上、channel 不匹配、未捕获响应、文件名回退等)。
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# 故障排查
|
||||||
|
|
||||||
|
## 登录态相关问题
|
||||||
|
|
||||||
|
- **运行后跳到 Google 登录页**(URL 含 `accounts.google.com`):未复用登录态。改用 `--user-data-dir`(方式 A)或 `--connect`(方式 B)。
|
||||||
|
- **报错 "user data directory ... is already in use" / "ProcessSingleton"**:该浏览器还没关。完全退出 Chrome/Edge 后再用方式 A。
|
||||||
|
- **`connect ECONNREFUSED 127.0.0.1:9222`**:调试端口没开。先 `chrome.exe --remote-debugging-port=9222`(Edge 同理)再 `--connect`。
|
||||||
|
- **连上了但仍未登录**:`--channel` 与 `--user-data-dir` 品牌不匹配(例如 chrome 的目录配了 `--channel msedge`),或指向了没登录过的 profile。换正确的浏览器/目录。
|
||||||
|
|
||||||
|
## 环境与依赖
|
||||||
|
|
||||||
|
- **`playwright` / `sync_playwright` 找不到**:环境未同步。项目根执行 `uv sync`,或直接用 `uv run python <脚本>`;详见 `uv-env-setup` 技能。复用系统 Chrome/Edge 时无需 `playwright install chromium`。
|
||||||
|
- **系统找不到浏览器**:确认本机装了 Chrome 或 Edge;用 `--channel` 显式指定 `chrome` 或 `msedge`。
|
||||||
|
|
||||||
|
## 运行过程
|
||||||
|
|
||||||
|
- **找不到「导出当前视图」按钮 / 点击超时**:URL 不是 explore 页,或页面尚未加载完。务必用用户提供的 explore URL,别用 overview URL。
|
||||||
|
- **没打印「已保存」(未捕获 csv_export 响应)**:导出未被触发,或登录态已失效。重试步骤 2,必要时重新登录后重跑。
|
||||||
|
- **文件名变成 `export.zip` 而非标准命名**:请求体解析失败触发兜底,内容仍完整;检查日期范围与账号名是否正常。
|
||||||
|
- **中文/特殊字符文件名**:已把 Windows 非法字符 `\/:*?"<>|` 自动替换为 `_`,不会因文件名失败。
|
||||||
|
|
||||||
|
## 产物与去重
|
||||||
|
|
||||||
|
- **下载目录不存在导致写盘失败**:脚本不自动建目录。先建 `D:\Downloads`,或用 `--download-dir` 指定已存在目录。
|
||||||
|
- **重名文件**:自动按 `名称 (1).zip`、`名称 (2).zip` 递增;这是脚本自己的 dedup_path 逻辑,不依赖浏览器。
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
r"""
|
||||||
|
YouTube Studio 内容管理器「导出当前视图 → 逗号分隔值 (.csv)」下载脚本
|
||||||
|
(拦截响应 + 解码 zip + 自动保存到下载目录 + 重名去重)。
|
||||||
|
|
||||||
|
机制(已实证):前端点击导出后向后端
|
||||||
|
POST https://studio.youtube.com/youtubei/v1/yta_web/csv_export?alt=json
|
||||||
|
后端把打好的 zip 以 base64 内联在响应 `zippedData` 字段里(开头 `UEsDBBQ` 即 ZIP 文件头 `PK`)。
|
||||||
|
本脚本拦截该响应,base64 解码后按 `<维度标签> <起始日>_<结束日> <账号名>.zip` 写盘,
|
||||||
|
重名自动加 ` (n)` 后缀,n 从 1 起。
|
||||||
|
|
||||||
|
登录态:脚本不负责登录,必须复用已登录 YouTube Studio 的浏览器会话,二选一:
|
||||||
|
- --user-data-dir + --channel chrome|msedge :用已登录的用户数据目录启动(需先关闭该浏览器)
|
||||||
|
- --connect http://localhost:9222 :附加到已在调试端口运行的浏览器
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python youtube_export_download.py --selftest
|
||||||
|
python youtube_export_download.py --url "<explore URL>" --channel chrome --user-data-dir "C:\Users\<you>\AppData\Local\Google\Chrome\User Data"
|
||||||
|
python youtube_export_download.py --url "<explore URL>" --connect http://localhost:9222
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
|
||||||
|
DOWNLOAD_DIR = r"D:\Downloads" # 默认下载目录,可用 --download-dir 覆盖
|
||||||
|
|
||||||
|
# 维度类型 -> 文件名前缀标签(生产环境建议从页面「维度」按钮文本读取,这里兜底映射)。
|
||||||
|
DIMENSION_LABEL = {
|
||||||
|
"VIDEO": "内容",
|
||||||
|
"USER": "频道",
|
||||||
|
"CONTENT_OWNER": "内容",
|
||||||
|
}
|
||||||
|
|
||||||
|
CSV_EXPORT_PATH = "/youtubei/v1/yta_web/csv_export"
|
||||||
|
|
||||||
|
|
||||||
|
def dedup_path(directory, filename):
|
||||||
|
"""返回不冲突的落盘路径;重名按 `名称 (n).后缀` 递增,n 从 1 起。"""
|
||||||
|
directory = os.path.abspath(directory)
|
||||||
|
base, ext = os.path.splitext(filename)
|
||||||
|
candidate = os.path.join(directory, filename)
|
||||||
|
n = 1
|
||||||
|
while os.path.exists(candidate):
|
||||||
|
candidate = os.path.join(directory, f"{base} ({n}){ext}")
|
||||||
|
n += 1
|
||||||
|
return candidate
|
||||||
|
|
||||||
|
|
||||||
|
def build_export_filename(export_query, account_name, dimension_label=None):
|
||||||
|
"""从 csv_export 请求体 `exportQuery` 反推文件名。"""
|
||||||
|
def fmt_dateid(yyyymmdd):
|
||||||
|
s = str(yyyymmdd)
|
||||||
|
return f"{s[0:4]}-{s[4:6]}-{s[6:8]}"
|
||||||
|
|
||||||
|
date_range = None
|
||||||
|
dimension = None
|
||||||
|
nodes = export_query.get("joinRequest", {}).get("nodes") or []
|
||||||
|
for node in nodes:
|
||||||
|
q = node.get("value", {}).get("query") or {}
|
||||||
|
if not date_range:
|
||||||
|
tr = q.get("timeRange", {}).get("dateIdRange")
|
||||||
|
if tr and tr.get("inclusiveStart"):
|
||||||
|
date_range = (tr["inclusiveStart"], tr.get("exclusiveEnd"))
|
||||||
|
dims = q.get("dimensions") or []
|
||||||
|
if dimension is None and dims:
|
||||||
|
dimension = dims[0].get("type")
|
||||||
|
|
||||||
|
if not date_range:
|
||||||
|
raise ValueError("无法从 exportQuery 解析日期范围")
|
||||||
|
|
||||||
|
if dimension_label is None:
|
||||||
|
dimension_label = DIMENSION_LABEL.get(dimension or "", "")
|
||||||
|
|
||||||
|
start, end = date_range
|
||||||
|
return f"{dimension_label} {fmt_dateid(start)}_{fmt_dateid(end)} {account_name}.zip"
|
||||||
|
|
||||||
|
|
||||||
|
def decode_zipped_data(payload):
|
||||||
|
"""把 csv_export 响应 payload 里的 zippedData 解码为 zip 字节流。"""
|
||||||
|
zipped = payload.get("zippedData")
|
||||||
|
if not zipped:
|
||||||
|
raise ValueError("响应中缺少 zippedData 字段")
|
||||||
|
return base64.b64decode(zipped)
|
||||||
|
|
||||||
|
|
||||||
|
def intercept_and_save(page, account_name, download_dir):
|
||||||
|
"""给 page 绑定 response 拦截器:命中 csv_export 就把 zip 保存到下载目录。"""
|
||||||
|
import pathlib
|
||||||
|
|
||||||
|
saved = []
|
||||||
|
|
||||||
|
def on_response(response):
|
||||||
|
if CSV_EXPORT_PATH not in response.url:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
payload = response.json()
|
||||||
|
data = decode_zipped_data(payload)
|
||||||
|
|
||||||
|
filename = None
|
||||||
|
try:
|
||||||
|
body = json.loads(response.request.post_data or "{}")
|
||||||
|
filename = build_export_filename(body.get("exportQuery", {}), account_name)
|
||||||
|
except Exception:
|
||||||
|
filename = "export.zip" # 反推失败兜底,避免丢内容
|
||||||
|
|
||||||
|
filename = re.sub(r"[\\/:*?\"<>|]", "_", filename) # Windows 非法字符
|
||||||
|
path = dedup_path(download_dir, filename)
|
||||||
|
pathlib.Path(path).write_bytes(data)
|
||||||
|
saved.append((filename, len(data), path))
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f"[interceptor] 处理 csv_export 响应失败: {e}", file=sys.stderr)
|
||||||
|
|
||||||
|
page.on("response", on_response)
|
||||||
|
return saved
|
||||||
|
|
||||||
|
|
||||||
|
def _default_user_data_dir(channel):
|
||||||
|
"""返回指定浏览器的默认用户数据目录(Windows),用于复用已登录会话。"""
|
||||||
|
_local = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
|
||||||
|
if channel == "msedge":
|
||||||
|
return os.path.join(_local, "Microsoft", "Edge", "User Data")
|
||||||
|
return os.path.join(_local, "Google", "Chrome", "User Data")
|
||||||
|
|
||||||
|
|
||||||
|
def run(url, user_data_dir=None, channel=None, cdp_url=None,
|
||||||
|
download_dir=None, account_name=None):
|
||||||
|
"""启动/连接浏览器并触发导出。必须复用已登录会话,否则跳 Google 登录页。"""
|
||||||
|
from playwright.sync_api import sync_playwright
|
||||||
|
|
||||||
|
download_dir = download_dir or DOWNLOAD_DIR
|
||||||
|
|
||||||
|
with sync_playwright() as p:
|
||||||
|
browser = None
|
||||||
|
context = None
|
||||||
|
|
||||||
|
if cdp_url:
|
||||||
|
browser = p.chromium.connect_over_cdp(cdp_url)
|
||||||
|
context = browser.contexts[0] if browser.contexts else \
|
||||||
|
browser.new_context(accept_downloads=True)
|
||||||
|
page = context.new_page()
|
||||||
|
page.goto(url, wait_until="domcontentloaded")
|
||||||
|
elif user_data_dir:
|
||||||
|
context = p.chromium.launch_persistent_context(
|
||||||
|
user_data_dir=user_data_dir or _default_user_data_dir(channel),
|
||||||
|
channel=channel,
|
||||||
|
headless=False,
|
||||||
|
accept_downloads=True,
|
||||||
|
args=["--disable-blink-features=AutomationControlled"],
|
||||||
|
)
|
||||||
|
page = context.new_page()
|
||||||
|
page.goto(url, wait_until="domcontentloaded")
|
||||||
|
else:
|
||||||
|
browser = p.chromium.launch(headless=False, channel=channel)
|
||||||
|
context = browser.new_context(accept_downloads=True)
|
||||||
|
page = context.new_page()
|
||||||
|
page.goto(url, wait_until="domcontentloaded")
|
||||||
|
|
||||||
|
if "accounts.google" in page.url:
|
||||||
|
print("[!] 当前会话未登录,已跳转到 Google 登录页。", file=sys.stderr)
|
||||||
|
print(" 请用 --user-data-dir 或 --connect 复用已登录浏览器后重试。",
|
||||||
|
file=sys.stderr)
|
||||||
|
|
||||||
|
if not account_name:
|
||||||
|
account_name = "WL Media"
|
||||||
|
try:
|
||||||
|
account_name = page.locator(
|
||||||
|
"ytcp-account-item button, .account-switcher button"
|
||||||
|
).first.inner_text(timeout=5000).strip() or account_name
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
saved = intercept_and_save(page, account_name, download_dir)
|
||||||
|
|
||||||
|
# 触发导出:点「导出当前视图」→「逗号分隔值 (.csv)」
|
||||||
|
page.get_by_text("导出当前视图").click()
|
||||||
|
page.get_by_text("逗号分隔值 (.csv)").click()
|
||||||
|
page.wait_for_timeout(3000)
|
||||||
|
|
||||||
|
if not saved:
|
||||||
|
print("未捕获到 csv_export 响应,请确认已点击导出且登录态有效。")
|
||||||
|
else:
|
||||||
|
for name, size, path in saved:
|
||||||
|
print(f"已保存: {path} ({size} bytes)")
|
||||||
|
|
||||||
|
if browser is not None:
|
||||||
|
browser.close()
|
||||||
|
elif context is not None:
|
||||||
|
context.close()
|
||||||
|
|
||||||
|
|
||||||
|
def selftest():
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
with tempfile.TemporaryDirectory() as td:
|
||||||
|
p0 = Path(dedup_path(td, "需求文件.zip"))
|
||||||
|
assert p0.name == "需求文件.zip", p0.name
|
||||||
|
p0.write_bytes(b"a")
|
||||||
|
|
||||||
|
p1 = Path(dedup_path(td, "需求文件.zip"))
|
||||||
|
assert p1.name == "需求文件 (1).zip", p1.name
|
||||||
|
p1.write_bytes(b"b")
|
||||||
|
|
||||||
|
p2 = Path(dedup_path(td, "需求文件.zip"))
|
||||||
|
assert p2.name == "需求文件 (2).zip", p2.name
|
||||||
|
p2.write_bytes(b"c")
|
||||||
|
|
||||||
|
assert Path(dedup_path(td, "需求文件.zip")).name == "需求文件 (3).zip"
|
||||||
|
assert Path(dedup_path(td, "其他.zip")).name == "其他.zip"
|
||||||
|
|
||||||
|
export_query = {
|
||||||
|
"joinRequest": {"nodes": [{"value": {"query": {
|
||||||
|
"dimensions": [{"type": "VIDEO"}],
|
||||||
|
"timeRange": {"dateIdRange": {
|
||||||
|
"inclusiveStart": 20260723, "exclusiveEnd": 20260820}},
|
||||||
|
}}}]},
|
||||||
|
}
|
||||||
|
name = build_export_filename(export_query, "WL Media")
|
||||||
|
assert name == "内容 2026-07-23_2026-08-20 WL Media.zip", name
|
||||||
|
|
||||||
|
print("selftest OK:去重与文件名反推逻辑全部通过")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
ap = argparse.ArgumentParser()
|
||||||
|
ap.add_argument("--selftest", action="store_true", help="仅跑去重/命名自测")
|
||||||
|
ap.add_argument("--url", help="explore URL")
|
||||||
|
ap.add_argument("--user-data-dir", help="浏览器用户数据目录(复用登录态,需先关闭该浏览器)")
|
||||||
|
ap.add_argument("--channel", choices=["chrome", "msedge"],
|
||||||
|
help="浏览器品牌,复用登录态时必填其一")
|
||||||
|
ap.add_argument("--connect", help="通过 CDP 附加到已打开浏览器,如 http://localhost:9222")
|
||||||
|
ap.add_argument("--download-dir", help=f"下载目录,默认 {DOWNLOAD_DIR}")
|
||||||
|
ap.add_argument("--account-name", help="账号名(用于文件名),默认从页面读取")
|
||||||
|
args = ap.parse_args()
|
||||||
|
|
||||||
|
if args.selftest:
|
||||||
|
selftest()
|
||||||
|
elif args.url:
|
||||||
|
run(args.url, user_data_dir=args.user_data_dir, channel=args.channel,
|
||||||
|
cdp_url=args.connect, download_dir=args.download_dir,
|
||||||
|
account_name=args.account_name)
|
||||||
|
else:
|
||||||
|
selftest()
|
||||||
|
print("\n实际运行需先 pip install playwright,再复用登录态(二选一):\n"
|
||||||
|
" 方式 A: python youtube_export_download.py --url \"<explore URL>\" "
|
||||||
|
"--channel chrome --user-data-dir <你的 Chrome User Data 目录>\n"
|
||||||
|
" 方式 B: python youtube_export_download.py --url \"<explore URL>\" "
|
||||||
|
"--connect http://localhost:9222")
|
||||||
84
skills/yt-studio-url-builder/SKILL.md
Normal file
84
skills/yt-studio-url-builder/SKILL.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
---
|
||||||
|
name: "yt-studio-url-builder"
|
||||||
|
description: "根据需求清单(CSV/Excel)批量生成 YouTube Studio 内容管理器 explore URL。当用户要求拼接/生成 YouTube Studio URL、批量产出报告链接、制作或校验需求输入清单、排查 URL 生成失败问题时使用。"
|
||||||
|
---
|
||||||
|
|
||||||
|
# YouTube Studio URL 拼接器
|
||||||
|
|
||||||
|
基于需求清单(CSV/Excel)批量生成 YouTube Studio 内容管理器(Content Manager)高级模式 explore 报告 URL。底层执行脚本 `scripts/build_studio_urls.py`,需求行只提供差异化条件,固定参数由脚本顶部 `CONFIG` 统一维护。
|
||||||
|
|
||||||
|
## 何时使用
|
||||||
|
|
||||||
|
- **生成 URL**:用户给出所有者/群组/频道/节目清单,要求批量产出 explore URL。
|
||||||
|
- **制作输入清单**:用户有零散需求(所有者ID、实体、日期范围、国家),需先整理成脚本可读的输入文件。
|
||||||
|
- **排查问题**:运行后出现失败行、空输出、解析错误,需要定位并修复。
|
||||||
|
|
||||||
|
## 领域词汇
|
||||||
|
|
||||||
|
- **所有者(Content Owner)**:拥有内容管理器的账号实体,URL 中由 `o` 参数 + `/owner/<id>/` 路径标识。
|
||||||
|
- **实体(Entity)**:报告统计的对象,由 `entity_type` + `entity_id` 标识(CONTENT_OWNER / GROUP / CHANNEL / VIDEO)。
|
||||||
|
- **群组(Group)**:所有者名下用于组织一批频道/资产的实体,`entity_id` 为其 groupId(如 `NCy9C2QPQ1E`)。
|
||||||
|
- **需求(Requirement)**:输入清单中的一行,描述一条待生成 URL 的完整条件(所有者、实体、数据周期、国家筛选)。
|
||||||
|
- **数据周期(Period)**:`yyyy.mm.dd-yyyy.mm.dd` 或 `yyyy.m.d-yyyy.m.d` 的日期区间,起始日与结束日均包含。
|
||||||
|
- **日界线(Day Boundary)**:周期中日期对应的 Unix 毫秒,采用「锚点 2026-06-15 = 1781506800000 + 整日偏移」计算,不做时区换算。
|
||||||
|
- **国家筛选(Country Filter)**:`ur_dimensions=COUNTRY` + `ur_values`;多国以 `'`(`%27`)包裹、`|`(`%7C`)连接;国家用 ISO 3166-1 alpha-2 代码。
|
||||||
|
|
||||||
|
## 工作流
|
||||||
|
|
||||||
|
### 1. 定位脚本与依赖
|
||||||
|
|
||||||
|
- 脚本默认位于 `scripts/build_studio_urls.py`;若缺失,用 SearchCodebase 按 `build_studio_urls.py` 定位。
|
||||||
|
- 运行环境由 `uv-env-setup` 技能统一管理:依赖(pandas、openpyxl)在根 `pyproject.toml` 声明,`uv run` 自动同步。首次运行前先按该技能准备环境。
|
||||||
|
- 中文国家名 → ISO 代码的映射在 `scripts/countries.json`,可自行扩充。
|
||||||
|
|
||||||
|
完成标准:脚本路径已确定,且第 4 步的运行命令可直接执行。
|
||||||
|
|
||||||
|
### 2. 收集每条需求
|
||||||
|
|
||||||
|
对每条待生成 URL 的需求,明确以下要素(留空表示走默认):
|
||||||
|
|
||||||
|
| 要素 | 必填 | 说明 |
|
||||||
|
|---|---|---|
|
||||||
|
| 所有者ID | 是 | URL 中的 `o` 参数与 `/owner/<id>/` 路径 |
|
||||||
|
| 实体类型 | 可默认 | 群组/所有者/频道/节目;缺省按群组 |
|
||||||
|
| 实体ID | 视类型 | 群组/频道/节目必填;所有者场景可留空(回退用所有者ID) |
|
||||||
|
| 数据周期 | 是 | 见领域词汇「数据周期」 |
|
||||||
|
| 国家 | 可空 | 一个或多个,中文名或两位 ISO 代码 |
|
||||||
|
|
||||||
|
完成标准:对每一条需求,上表要素已确定,或明确「留空走默认」。
|
||||||
|
|
||||||
|
### 3. 制作输入文件
|
||||||
|
|
||||||
|
按 [references/input-guide.md](references/input-guide.md) 生成 CSV/Excel 需求清单(文件格式、列名别名、日期/国家/实体类型写法、完整示例、核对清单)。
|
||||||
|
|
||||||
|
完成标准:输入文件包含必要列(所有者ID、数据周期),且列名能被脚本识别(无「缺必要列」错误)。
|
||||||
|
|
||||||
|
### 4. 运行脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 项目根目录下
|
||||||
|
uv run python scripts\build_studio_urls.py -i <输入文件> [-o <输出csv>]
|
||||||
|
```
|
||||||
|
|
||||||
|
- 输入支持 `.csv`(UTF-8 或 GBK,自动尝试)与 `.xlsx` / `.xls`。
|
||||||
|
- 未指定 `-o` 时,输出到输入文件同目录的 `studio_urls_output.csv`。
|
||||||
|
- 可用 `--countries <json>` 覆盖国家映射文件路径(默认脚本同目录 `countries.json`)。
|
||||||
|
|
||||||
|
完成标准:脚本退出码为 0,且输出 CSV 行数与成功需求数一致。
|
||||||
|
|
||||||
|
### 5. 校验输出并处理失败行
|
||||||
|
|
||||||
|
- 检查输出列:所有者名称、所有者ID、实体类型、实体名称、实体ID、数据周期、国家、国家代码、开始时间戳、结束时间戳、URL。
|
||||||
|
- 抽查 URL 是否包含正确的 `entity_type` / `entity_id` / `time_period` / `ur_values`(国家筛选)。
|
||||||
|
- 有失败行时脚本以退出码 2 结束,并在 stderr 逐行打印「第N行 …:原因」;逐条按 [references/troubleshooting.md](references/troubleshooting.md) 修复后重跑。
|
||||||
|
|
||||||
|
完成标准:所有需求行均生成 URL,或失败行已定位原因并修复。
|
||||||
|
|
||||||
|
## 固定参数(CONFIG)
|
||||||
|
|
||||||
|
metric / granularity / dimension / t_metrics / o_column / o_direction / explore_type / comparison_type 等固定参数集中在脚本顶部 `CONFIG` 一处维护。**修改固定参数只需改 `CONFIG`(单点维护)**,不要逐行携带到需求清单中。
|
||||||
|
|
||||||
|
## 参考
|
||||||
|
|
||||||
|
- [需求输入制作指南](references/input-guide.md):列名别名、日期/国家/实体类型写法、完整示例、制作核对清单。
|
||||||
|
- [常见问题排查](references/troubleshooting.md):按 现象 → 原因 → 解决 逐条排查。
|
||||||
83
skills/yt-studio-url-builder/references/input-guide.md
Normal file
83
skills/yt-studio-url-builder/references/input-guide.md
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
# 需求输入清单制作指南
|
||||||
|
|
||||||
|
输入文件是脚本的唯一数据来源。本指南回答:怎么组织列、怎么写日期和国家、用什么实体类型、以及怎么核对。
|
||||||
|
|
||||||
|
## 1. 文件格式
|
||||||
|
|
||||||
|
- **CSV**:UTF-8(带/不带 BOM)或 GBK/ANSI(Excel 另存)均可,脚本自动尝试编码。
|
||||||
|
- **Excel**:`.xlsx` / `.xls`,读首个工作表。
|
||||||
|
- 全空行会被忽略。
|
||||||
|
|
||||||
|
## 2. 列与别名
|
||||||
|
|
||||||
|
脚本按「去空白、转小写」后的列名匹配,**中英文别名均可**。未识别列会被忽略(stderr 有提示),但不中断生成。
|
||||||
|
|
||||||
|
| 规范字段 | 必填 | 可用列名(别名) | 示例 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| owner_name 所有者名称 | 否 | 所有者名称 / owner_name / 所有者 | 示例内容所有者 |
|
||||||
|
| owner_id 所有者ID | **是** | 所有者ID / owner_id / o | bqSUnNpU67xJ51TxH4PKpQ |
|
||||||
|
| entity_name 实体名称 | 否 | 实体名称 / 群组名称 / 频道名称 / 节目名称 / entity_name | 示例群组-1 |
|
||||||
|
| entity_id 实体ID | 视类型 | 实体ID / 群组ID / group_id / entity_id / id / 群组 | NCy9C2QPQ1E |
|
||||||
|
| entity_type 实体类型 | 否 | 实体类型 / 类型 / entity_type / type | 群组 |
|
||||||
|
| period 数据周期 | **是** | 数据周期 / 周期 / period / time_period / 日期范围 | 2026.08.01-2026.08.31 |
|
||||||
|
| countries 国家 | 否 | 国家 / 国家/地区 / countries / country / 筛选国家 / 地区 | 美国,日本 |
|
||||||
|
|
||||||
|
> 注意:
|
||||||
|
> - 「所有者名称 / 实体名称」只进输出回显,不参与 URL 拼装,缺列不影响生成。
|
||||||
|
> - **必要列只有两列:所有者ID、数据周期**。
|
||||||
|
|
||||||
|
## 3. 数据周期格式
|
||||||
|
|
||||||
|
- 形式:`yyyy.mm.dd-yyyy.mm.dd` 或 `yyyy.m.d-yyyy.m.d`(前导零可省)。
|
||||||
|
- 起止日期之间可用 `-`、`~`、`~`;日期内部用 `.`、`/`、`-`。
|
||||||
|
- **起止日期均包含在数据范围内**(结束日取次日的日界线)。
|
||||||
|
- 例子:`2026.08.01-2026.08.31`、`2026.8.1-2026.8.31`、`2026-07-01~2026-08-01` 均可。
|
||||||
|
- 时间换算为日界线毫秒:`ts(X) = 1781506800000 + (X − 2026-06-15) × 86400000`。
|
||||||
|
|
||||||
|
## 4. 国家格式
|
||||||
|
|
||||||
|
- 中文国家名:须在 `scripts/countries.json` 映射内(可自行扩充)。
|
||||||
|
- 两位 ISO 代码(如 `US`、`JP`):原样透传,大小写不敏感。
|
||||||
|
- 多个国家:用 `,`、`、`、`;`、空格或 `|` 分隔均可。
|
||||||
|
- 留空 = 不筛选国家(URL 中不带 `ur_dimensions` / `ur_values`)。
|
||||||
|
- 输出中的「国家代码」列展示解析后的 ISO 代码,便于核对。
|
||||||
|
|
||||||
|
## 5. 实体类型
|
||||||
|
|
||||||
|
| 输入(中文/代码) | URL 参数值 |
|
||||||
|
|---|---|
|
||||||
|
| 群组 / GROUP | GROUP |
|
||||||
|
| 所有者 / 账号 / CONTENT_OWNER | CONTENT_OWNER |
|
||||||
|
| 频道 / CHANNEL | CHANNEL |
|
||||||
|
| 节目 / 视频 / VIDEO | VIDEO |
|
||||||
|
|
||||||
|
- 缺省按「群组」处理。
|
||||||
|
- 实体类型为**所有者**且实体ID留空时,自动回退用所有者ID作为 entity_id。
|
||||||
|
|
||||||
|
## 6. 完整示例(对应 assets/需求输入示例.xlsx)
|
||||||
|
|
||||||
|
| 所有者名称 | 所有者ID | 实体类型 | 实体名称 | 实体ID | 数据周期 | 国家 |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| 示例内容所有者 | bqSUnNpU67xJ51TxH4PKpQ | 群组 | 示例群组-1 | NCy9C2QPQ1E | 2026.08.01-2026.08.31 | 美国 |
|
||||||
|
| 示例内容所有者 | bqSUnNpU67xJ51TxH4PKpQ | 群组 | 示例群组-2 | NCyxxxxxxxxx | 2026.8.1-2026.8.31 | 美国,日本 |
|
||||||
|
| 示例内容所有者 | bqSUnNpU67xJ51TxH4PKpQ | 群组 | 示例群组-3 | NCyYYYYYYYYY | 2026.07.01-2026.07.31 | GB,DE,FR |
|
||||||
|
| 示例内容所有者 | bqSUnNpU67xJ51TxH4PKpQ | 所有者 | 账号整体 | (留空) | 2026.06.01-2026.06.30 | US |
|
||||||
|
| 示例内容所有者 | bqSUnNpU67xJ51TxH4PKpQ | 频道 | 示例频道 | UCxxxxxUCxxxxx | 2026.08.01-2026.08.15 | 日本 |
|
||||||
|
| 示例内容所有者 | bqSUnNpU67xJ51TxH4PKpQ | 节目 | 示例节目 | video123456 | 2026.08.01-2026.08.31 | 韩国,日本 |
|
||||||
|
|
||||||
|
覆盖场景:单/多国家、中文名与 ISO 代码混用、四种实体类型、带/不带前导零的日期、所有者场景留空实体ID。
|
||||||
|
|
||||||
|
## 7. 制作核对清单
|
||||||
|
|
||||||
|
- [ ] 至少含「所有者ID」「数据周期」两列(列名可用中英文别名)。
|
||||||
|
- [ ] 每行数据周期格式正确,起止日期均含。
|
||||||
|
- [ ] 群组/频道/节目行已填实体ID;所有者行可留空实体ID。
|
||||||
|
- [ ] 中文国家名已在 countries.json 中,否则改用两位 ISO 代码。
|
||||||
|
- [ ] 多国分隔符正确。
|
||||||
|
- [ ] 试跑无失败行、无「缺必要列」错误。
|
||||||
|
|
||||||
|
试跑命令(项目根目录):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv run python scripts\build_studio_urls.py -i <输入文件> -o <输出csv>
|
||||||
|
```
|
||||||
71
skills/yt-studio-url-builder/references/troubleshooting.md
Normal file
71
skills/yt-studio-url-builder/references/troubleshooting.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# 常见问题排查
|
||||||
|
|
||||||
|
脚本的错误分三类,先分清类型再定位:
|
||||||
|
|
||||||
|
- **提示(stderr,不中断)**:未识别的列、未找到国家映射文件等,仅警告。
|
||||||
|
- **致命错误(退出码 1)**:输入文件不存在、格式不支持、缺必要列。
|
||||||
|
- **失败行(退出码 2)**:个别需求行生成失败,成功行仍会写出;stderr 逐行打印「第N行 …:原因」。
|
||||||
|
|
||||||
|
## 输入相关
|
||||||
|
|
||||||
|
### 输入文件不存在
|
||||||
|
- 原因:`-i` 路径错误,或文件名大小写/中文名不符。
|
||||||
|
- 解决:确认路径存在;PowerShell 中路径含空格时用引号包裹。
|
||||||
|
|
||||||
|
### 不支持的输入格式
|
||||||
|
- 原因:扩展名不是 `.csv` / `.xlsx` / `.xls`。
|
||||||
|
- 解决:另存为支持格式后再跑。
|
||||||
|
|
||||||
|
### 输入缺少必要列
|
||||||
|
- 原因:清单中没有「所有者ID」或「数据周期」(或其别名)。
|
||||||
|
- 解决:补齐列;列名支持别名,见 input-guide.md 第 2 节。
|
||||||
|
|
||||||
|
### 未识别的列(提示)
|
||||||
|
- 原因:列名不在别名表。
|
||||||
|
- 解决:不影响生成;如需在输出中回显,改用别名表中列名。
|
||||||
|
|
||||||
|
## 行级失败(第 N 行)
|
||||||
|
|
||||||
|
### 缺少所有者ID
|
||||||
|
- 原因:该行 `owner_id` 为空。
|
||||||
|
- 解决:补充所有者ID。
|
||||||
|
|
||||||
|
### 缺少实体ID
|
||||||
|
- 原因:实体类型不是所有者(如群组/频道/节目)且 `entity_id` 为空。
|
||||||
|
- 解决:补充实体ID;若确为所有者整体场景,把实体类型改为「所有者/账号」并留空实体ID。
|
||||||
|
|
||||||
|
### 无法解析数据周期
|
||||||
|
- 原因:日期不符合 `yyyy.m.d-yyyy.m.d`(或 `~`/`~` 分隔)的写法。
|
||||||
|
- 解决:按 input-guide.md 第 3 节修正。
|
||||||
|
|
||||||
|
### 未识别的国家
|
||||||
|
- 原因:中文国家名不在 `countries.json`,也不是两位 ISO 代码。
|
||||||
|
- 解决:改用两位 ISO 代码,或向 `countries.json` 追加映射。
|
||||||
|
|
||||||
|
### 未识别的实体类型
|
||||||
|
- 原因:`entity_type` 不在映射表。
|
||||||
|
- 解决:使用 群组/所有者/频道/节目 或 GROUP/CONTENT_OWNER/CHANNEL/VIDEO。
|
||||||
|
|
||||||
|
## 输出问题
|
||||||
|
|
||||||
|
### 输出行数 < 输入行数
|
||||||
|
- 原因:存在失败行(退出码 2)。
|
||||||
|
- 解决:查看 stderr 失败列表,逐条修复后重跑。
|
||||||
|
|
||||||
|
### 输出为空 / Excel 打开乱码
|
||||||
|
- 原因:成功记录为 0;或查看方式不对。
|
||||||
|
- 解决:确认输入有合法行;脚本以 UTF-8-SIG 写出,Excel 可直接打开正常显示。
|
||||||
|
|
||||||
|
### 找不到输出文件
|
||||||
|
- 原因:未指定 `-o`,默认输出在**输入文件同目录** `studio_urls_output.csv`。
|
||||||
|
- 解决:显式用 `-o` 指定输出路径。
|
||||||
|
|
||||||
|
## 编码相关
|
||||||
|
|
||||||
|
- CSV 输入乱码/解析异常:脚本先按 UTF-8-SIG 读,失败回退 GBK;仍异常时,把文件另存为 UTF-8 或 GBK 后重试。
|
||||||
|
- `countries.json` 建议 UTF-8;脚本按 UTF-8-SIG 读取,带 BOM 无影响。
|
||||||
|
|
||||||
|
## 改了固定参数不生效
|
||||||
|
|
||||||
|
- 原因:固定参数集中在脚本顶部 `CONFIG`,改后需重跑脚本才会生效。
|
||||||
|
- 提示:改 `CONFIG` 只影响 URL 参数,不需要改需求清单。
|
||||||
297
skills/yt-studio-url-builder/scripts/build_studio_urls.py
Normal file
297
skills/yt-studio-url-builder/scripts/build_studio_urls.py
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
根据需求清单(CSV/Excel)批量拼接 YouTube Studio 内容管理器 explore URL。
|
||||||
|
|
||||||
|
用法:
|
||||||
|
python build_studio_urls.py -i 需求清单.csv -o 输出.csv
|
||||||
|
python build_studio_urls.py -i 需求清单.xlsx -o 输出.csv
|
||||||
|
|
||||||
|
输入列(支持中英文别名,未填可留空):
|
||||||
|
所有者名称 : 所有者名称 / owner_name
|
||||||
|
所有者ID : 所有者ID / owner_id / o
|
||||||
|
实体名称 : 实体名称 / 群组名称 / 频道名称 / 节目名称 / entity_name / group_name
|
||||||
|
实体ID : 实体ID / 群组ID / group_id / entity_id / id
|
||||||
|
实体类型 : 实体类型 / 类型 / entity_type (群组/所有者/频道/节目,或 GROUP/CONTENT_OWNER/CHANNEL/VIDEO)
|
||||||
|
数据周期 : 数据周期 / 周期 / period / time_period (yyyy.mm.dd-yyyy.mm.dd 或 yyyy.m.d-yyyy.m.d)
|
||||||
|
国家 : 国家 / 国家/地区 / country / countries (一个或多个,中文名或 ISO 两位代码)
|
||||||
|
|
||||||
|
说明:
|
||||||
|
- 数据周期起止日期均包含在数据范围内,time_period 结束值取结束日后一天的日界线。
|
||||||
|
- 国家为多个时,ur_values 以 '%27' 包裹、'%7C' 连接(如 美国,日本 -> %27US%27%7C%27JP%27)。
|
||||||
|
- 中文国家名 -> ISO 代码的映射放在同目录 countries.json(可自行扩充);已是两位代码的原样透传。
|
||||||
|
- 固定参数(metric/granularity/dimension/t_metrics 等)在本文件 CONFIG 中统一配置。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from urllib.parse import quote
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 固定参数(所有需求共用,按需修改)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
CONFIG = {
|
||||||
|
"explore_type": "TABLE_AND_CHART",
|
||||||
|
"metric": "SUBSCRIBERS_NET_CHANGE", # 主指标
|
||||||
|
"granularity": "DAY", # DAY / WEEK / MONTH / YEAR
|
||||||
|
"dimension": "USER", # 细分维度
|
||||||
|
"t_metrics": [ # 表格列指标(可多个)
|
||||||
|
"SUBSCRIBERS_NET_CHANGE",
|
||||||
|
"VIDEO_COUNT_FIRST_PUBLISHED",
|
||||||
|
"ENGAGED_VIEWS",
|
||||||
|
"EXTERNAL_VIEWS",
|
||||||
|
"EXTERNAL_WATCH_TIME",
|
||||||
|
"AVERAGE_WATCH_TIME",
|
||||||
|
"TOTAL_ESTIMATED_EARNINGS",
|
||||||
|
],
|
||||||
|
"o_column": "SUBSCRIBERS_NET_CHANGE", # 排序字段
|
||||||
|
"o_direction": "ANALYTICS_ORDER_DIRECTION_DESC", # DESC / ASC
|
||||||
|
"comparison_type": "NONE",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 实体类型 中文/代码 -> URL 参数值(可扩充)
|
||||||
|
ENTITY_TYPE_MAP = {
|
||||||
|
"群组": "GROUP", "GROUP": "GROUP",
|
||||||
|
"所有者": "CONTENT_OWNER", "账号": "CONTENT_OWNER", "CONTENT_OWNER": "CONTENT_OWNER",
|
||||||
|
"频道": "CHANNEL", "CHANNEL": "CHANNEL",
|
||||||
|
"节目": "VIDEO", "视频": "VIDEO", "VIDEO": "VIDEO",
|
||||||
|
}
|
||||||
|
DEFAULT_ENTITY_TYPE = "GROUP"
|
||||||
|
|
||||||
|
# 日界线锚点 + 整日偏移(见 docs/adr/0001)
|
||||||
|
ANCHOR_DATE = date(2026, 6, 15)
|
||||||
|
ANCHOR_MS = 1781506800000
|
||||||
|
MS_PER_DAY = 86400000
|
||||||
|
|
||||||
|
# 数据周期正则:支持 2026.07.01-2026.08.01 / 2026.7.1-2026.8.1 / 2026-07-01~2026-08-01
|
||||||
|
PERIOD_RE = re.compile(
|
||||||
|
r"(\d{4})[.\-/](\d{1,2})[.\-/](\d{1,2})\s*[-~~]\s*(\d{4})[.\-/](\d{1,2})[.\-/](\d{1,2})"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 输入列名 -> 规范字段(键为去空白、转小写后的列名)
|
||||||
|
COLUMN_ALIASES = {
|
||||||
|
# 所有者
|
||||||
|
"所有者名称": "owner_name", "ownername": "owner_name", "owner_name": "owner_name",
|
||||||
|
"所有者": "owner_name",
|
||||||
|
"所有者id": "owner_id", "ownerid": "owner_id", "owner_id": "owner_id", "o": "owner_id",
|
||||||
|
# 实体
|
||||||
|
"实体名称": "entity_name", "entity_name": "entity_name",
|
||||||
|
"群组名称": "entity_name", "group_name": "entity_name", "groupname": "entity_name",
|
||||||
|
"频道名称": "entity_name", "节目名称": "entity_name",
|
||||||
|
"实体id": "entity_id", "entity_id": "entity_id", "entityid": "entity_id",
|
||||||
|
"群组id": "entity_id", "group_id": "entity_id", "groupid": "entity_id",
|
||||||
|
"群组": "entity_id", "id": "entity_id",
|
||||||
|
"实体类型": "entity_type", "entity_type": "entity_type", "entitytype": "entity_type",
|
||||||
|
"类型": "entity_type", "type": "entity_type",
|
||||||
|
# 数据周期
|
||||||
|
"数据周期": "period", "周期": "period", "period": "period",
|
||||||
|
"time_period": "period", "timeperiod": "period", "日期范围": "period",
|
||||||
|
# 国家
|
||||||
|
"国家": "countries", "国家/地区": "countries", "国家地区": "countries",
|
||||||
|
"countries": "countries", "country": "countries", "筛选国家": "countries", "地区": "countries",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def ts(y, m, d):
|
||||||
|
"""日期 -> 日界线 Unix 毫秒(锚点 + 整日偏移)。"""
|
||||||
|
return ANCHOR_MS + (date(y, m, d) - ANCHOR_DATE).days * MS_PER_DAY
|
||||||
|
|
||||||
|
|
||||||
|
def normalize_columns(df):
|
||||||
|
"""按别名把输入列映射到规范字段。返回 (field->column_index, 未识别列名列表)。"""
|
||||||
|
mapping = {}
|
||||||
|
unknown = []
|
||||||
|
for col in df.columns:
|
||||||
|
key = str(col).strip().lower()
|
||||||
|
field = COLUMN_ALIASES.get(key)
|
||||||
|
if field:
|
||||||
|
mapping[field] = col
|
||||||
|
else:
|
||||||
|
unknown.append(str(col))
|
||||||
|
return mapping, unknown
|
||||||
|
|
||||||
|
|
||||||
|
def parse_period(text):
|
||||||
|
"""解析数据周期,返回 (start_ms, end_ms),起止日期均含。"""
|
||||||
|
text = str(text).strip()
|
||||||
|
m = PERIOD_RE.search(text)
|
||||||
|
if not m:
|
||||||
|
raise ValueError("无法解析数据周期: %r(应为 yyyy.mm.dd-yyyy.mm.dd)" % text)
|
||||||
|
y1, m1, d1, y2, m2, d2 = (int(g) for g in m.groups())
|
||||||
|
start_ms = ts(y1, m1, d1)
|
||||||
|
end_ms = ts(y2, m2, d2) + MS_PER_DAY # 结束日包含
|
||||||
|
return start_ms, end_ms
|
||||||
|
|
||||||
|
|
||||||
|
def load_country_map(path):
|
||||||
|
"""加载 中文国家名 -> ISO 代码 映射。文件缺失则仅支持两位代码透传。"""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
print("[提示] 未找到国家映射文件 %s,仅支持直接填写两位 ISO 代码" % path, file=sys.stderr)
|
||||||
|
return {}
|
||||||
|
with open(path, "r", encoding="utf-8-sig") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return {str(k).strip(): str(v).strip().upper() for k, v in data.items()}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_countries(text, country_map):
|
||||||
|
"""解析国家列(一个或多个,中文名或 ISO 代码),返回 ISO 代码列表。"""
|
||||||
|
if text is None or (isinstance(text, float) and str(text) == "nan"):
|
||||||
|
return []
|
||||||
|
raw = str(text)
|
||||||
|
parts = re.split(r"[,,、;;\s|]+", raw)
|
||||||
|
codes = []
|
||||||
|
for part in parts:
|
||||||
|
p = part.strip().strip("'\"").strip()
|
||||||
|
if not p:
|
||||||
|
continue
|
||||||
|
upper = p.upper()
|
||||||
|
if re.fullmatch(r"[A-Z]{2}", upper): # 已是两位 ISO 代码
|
||||||
|
codes.append(upper)
|
||||||
|
elif p in country_map:
|
||||||
|
codes.append(country_map[p])
|
||||||
|
else:
|
||||||
|
raise ValueError("未识别的国家: %r(不在映射文件中,也不是两位 ISO 代码)" % p)
|
||||||
|
return codes
|
||||||
|
|
||||||
|
|
||||||
|
def resolve_entity_type(text):
|
||||||
|
"""实体类型 -> URL 参数值。空则用默认群组。"""
|
||||||
|
if text is None or (isinstance(text, float) and str(text) == "nan") or str(text).strip() == "":
|
||||||
|
return DEFAULT_ENTITY_TYPE
|
||||||
|
key = str(text).strip()
|
||||||
|
if key in ENTITY_TYPE_MAP:
|
||||||
|
return ENTITY_TYPE_MAP[key]
|
||||||
|
raise ValueError("未识别的实体类型: %r(应为 群组/所有者/频道/节目 或 GROUP/CONTENT_OWNER/CHANNEL/VIDEO)" % key)
|
||||||
|
|
||||||
|
|
||||||
|
def build_url(row, country_map):
|
||||||
|
"""根据一行需求生成 URL。返回 (url, 状态, 错误信息)。"""
|
||||||
|
owner_id = row.get("owner_id", "").strip()
|
||||||
|
entity_type = resolve_entity_type(row.get("entity_type", ""))
|
||||||
|
entity_id = row.get("entity_id", "").strip()
|
||||||
|
|
||||||
|
if not owner_id:
|
||||||
|
return None, "error", "缺少所有者ID"
|
||||||
|
if not entity_id:
|
||||||
|
if entity_type == "CONTENT_OWNER":
|
||||||
|
entity_id = owner_id # 账号整体场景回退
|
||||||
|
else:
|
||||||
|
return None, "error", "缺少实体ID"
|
||||||
|
|
||||||
|
period = row.get("period", "").strip()
|
||||||
|
if not period:
|
||||||
|
return None, "error", "缺少数据周期"
|
||||||
|
start_ms, end_ms = parse_period(period)
|
||||||
|
|
||||||
|
codes = parse_countries(row.get("countries", ""), country_map)
|
||||||
|
|
||||||
|
base = "https://studio.youtube.com/owner/%s/analytics/tab-overview/period-default/explore" % owner_id
|
||||||
|
params = []
|
||||||
|
params.append("o=%s" % owner_id)
|
||||||
|
params.append("entity_type=%s" % entity_type)
|
||||||
|
params.append("entity_id=%s" % entity_id)
|
||||||
|
if codes:
|
||||||
|
ur_values = quote("|".join("'%s'" % c for c in codes), safe="") # 'US'|'JP' -> %27US%27%7C%27JP%27
|
||||||
|
params.append("ur_dimensions=COUNTRY")
|
||||||
|
params.append("ur_values=%s" % ur_values)
|
||||||
|
params.append("ur_inclusive_starts=")
|
||||||
|
params.append("ur_exclusive_ends=")
|
||||||
|
params.append("time_period=%d%%2C%d" % (start_ms, end_ms))
|
||||||
|
params.append("explore_type=%s" % CONFIG["explore_type"])
|
||||||
|
params.append("metric=%s" % CONFIG["metric"])
|
||||||
|
params.append("granularity=%s" % CONFIG["granularity"])
|
||||||
|
for m in CONFIG["t_metrics"]:
|
||||||
|
params.append("t_metrics=%s" % m)
|
||||||
|
params.append("dimension=%s" % CONFIG["dimension"])
|
||||||
|
params.append("o_column=%s" % CONFIG["o_column"])
|
||||||
|
params.append("o_direction=%s" % CONFIG["o_direction"])
|
||||||
|
params.append("comparison_type=%s" % CONFIG["comparison_type"])
|
||||||
|
|
||||||
|
url = base + "?" + "&".join(params)
|
||||||
|
return url, "ok", ""
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(description="批量拼接 YouTube Studio explore URL")
|
||||||
|
parser.add_argument("-i", "--input", required=True, help="需求清单文件(.csv / .xlsx / .xls)")
|
||||||
|
parser.add_argument("-o", "--output", default=None, help="输出 CSV 路径(默认:输入同目录 studio_urls_output.csv)")
|
||||||
|
parser.add_argument("--countries", default=None, help="国家映射 JSON 路径(默认:脚本同目录 countries.json)")
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if not os.path.exists(args.input):
|
||||||
|
print("错误:输入文件不存在: %s" % args.input, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
ext = os.path.splitext(args.input)[1].lower()
|
||||||
|
if ext == ".csv":
|
||||||
|
try:
|
||||||
|
df = pd.read_csv(args.input, encoding="utf-8-sig")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
df = pd.read_csv(args.input, encoding="gbk") # Excel 另存的 ANSI/GBK
|
||||||
|
elif ext in (".xlsx", ".xls"):
|
||||||
|
df = pd.read_excel(args.input)
|
||||||
|
else:
|
||||||
|
print("错误:不支持的输入格式: %s(支持 .csv/.xlsx/.xls)" % ext, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
df = df.dropna(how="all") # 去掉全空行
|
||||||
|
|
||||||
|
mapping, unknown = normalize_columns(df)
|
||||||
|
if unknown:
|
||||||
|
print("[提示] 未识别的列(忽略): %s" % ", ".join(unknown), file=sys.stderr)
|
||||||
|
missing = [f for f in ("owner_id", "period") if f not in mapping]
|
||||||
|
if missing:
|
||||||
|
print("错误:输入缺少必要列: %s" % ", ".join(missing), file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
countries_file = args.countries or os.path.join(os.path.dirname(os.path.abspath(__file__)), "countries.json")
|
||||||
|
country_map = load_country_map(countries_file)
|
||||||
|
|
||||||
|
records = []
|
||||||
|
errors = []
|
||||||
|
for idx, raw in df.iterrows():
|
||||||
|
row = {f: ("" if pd.isna(raw[mapping[f]]) else str(raw[mapping[f]])) for f in mapping}
|
||||||
|
try:
|
||||||
|
url, status, msg = build_url(row, country_map)
|
||||||
|
except ValueError as e:
|
||||||
|
url, status, msg = None, "error", str(e)
|
||||||
|
if status == "ok":
|
||||||
|
start_ms, end_ms = parse_period(row.get("period", ""))
|
||||||
|
codes = parse_countries(row.get("countries", ""), country_map)
|
||||||
|
records.append({
|
||||||
|
"所有者名称": row.get("owner_name", ""),
|
||||||
|
"所有者ID": row.get("owner_id", ""),
|
||||||
|
"实体类型": row.get("entity_type", ""),
|
||||||
|
"实体名称": row.get("entity_name", ""),
|
||||||
|
"实体ID": row.get("entity_id", ""),
|
||||||
|
"数据周期": row.get("period", ""),
|
||||||
|
"国家": row.get("countries", ""),
|
||||||
|
"国家代码": ",".join(codes),
|
||||||
|
"开始时间戳": start_ms,
|
||||||
|
"结束时间戳": end_ms,
|
||||||
|
"URL": url,
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
errors.append((idx + 2, row.get("owner_name", ""), row.get("entity_name", ""), msg))
|
||||||
|
|
||||||
|
out_path = args.output or os.path.join(
|
||||||
|
os.path.dirname(os.path.abspath(args.input)), "studio_urls_output.csv")
|
||||||
|
out_df = pd.DataFrame(records)
|
||||||
|
out_df.to_csv(out_path, index=False, encoding="utf-8-sig")
|
||||||
|
print("已生成 %d 条 URL -> %s" % (len(records), out_path))
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("\n以下 %d 行生成失败:" % len(errors), file=sys.stderr)
|
||||||
|
for r in errors:
|
||||||
|
print(" 第%d行 所有者=%s 实体=%s:%s" % r, file=sys.stderr)
|
||||||
|
sys.exit(2)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
24
skills/yt-studio-url-builder/scripts/countries.json
Normal file
24
skills/yt-studio-url-builder/scripts/countries.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"美国": "US", "日本": "JP", "英国": "GB", "德国": "DE", "法国": "FR",
|
||||||
|
"意大利": "IT", "西班牙": "ES", "加拿大": "CA", "澳大利亚": "AU", "韩国": "KR",
|
||||||
|
"巴西": "BR", "墨西哥": "MX", "印度": "IN", "俄罗斯": "RU", "荷兰": "NL",
|
||||||
|
"瑞典": "SE", "挪威": "NO", "芬兰": "FI", "丹麦": "DK", "比利时": "BE",
|
||||||
|
"瑞士": "CH", "奥地利": "AT", "波兰": "PL", "葡萄牙": "PT", "爱尔兰": "IE",
|
||||||
|
"新西兰": "NZ", "新加坡": "SG", "马来西亚": "MY", "泰国": "TH", "越南": "VN",
|
||||||
|
"印度尼西亚": "ID", "菲律宾": "PH", "土耳其": "TR", "沙特阿拉伯": "SA",
|
||||||
|
"阿联酋": "AE", "以色列": "IL", "南非": "ZA", "阿根廷": "AR", "智利": "CL",
|
||||||
|
"哥伦比亚": "CO", "秘鲁": "PE", "埃及": "EG", "尼日利亚": "NG", "中国": "CN",
|
||||||
|
"中国台湾": "TW", "台湾": "TW", "中国香港": "HK", "香港": "HK", "中国澳门": "MO", "澳门": "MO",
|
||||||
|
"乌克兰": "UA", "希腊": "GR", "捷克": "CZ", "匈牙利": "HU", "罗马尼亚": "RO",
|
||||||
|
"保加利亚": "BG", "克罗地亚": "HR", "斯洛伐克": "SK", "斯洛文尼亚": "SI",
|
||||||
|
"立陶宛": "LT", "拉脱维亚": "LV", "爱沙尼亚": "EE", "塞尔维亚": "RS", "冰岛": "IS",
|
||||||
|
"卢森堡": "LU", "马耳他": "MT", "塞浦路斯": "CY", "巴基斯坦": "PK",
|
||||||
|
"孟加拉国": "BD", "斯里兰卡": "LK", "哈萨克斯坦": "KZ", "卡塔尔": "QA",
|
||||||
|
"科威特": "KW", "巴林": "BH", "阿曼": "OM", "约旦": "JO", "黎巴嫩": "LB",
|
||||||
|
"摩洛哥": "MA", "阿尔及利亚": "DZ", "突尼斯": "TN", "肯尼亚": "KE",
|
||||||
|
"加纳": "GH", "坦桑尼亚": "TZ", "埃塞俄比亚": "ET", "玻利维亚": "BO",
|
||||||
|
"厄瓜多尔": "EC", "乌拉圭": "UY", "巴拉圭": "PY", "委内瑞拉": "VE",
|
||||||
|
"巴拿马": "PA", "哥斯达黎加": "CR", "古巴": "CU", "多米尼加": "DO",
|
||||||
|
"波多黎各": "PR", "危地马拉": "GT", "洪都拉斯": "HN", "萨尔瓦多": "SV",
|
||||||
|
"尼加拉瓜": "NI"
|
||||||
|
}
|
||||||
67
tests/conftest.py
Normal file
67
tests/conftest.py
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""测试公共设施:把两个 skill 目录下的独立脚本按路径加载为模块,供单元测试直接 import。
|
||||||
|
|
||||||
|
两个被测脚本均为独立脚本(非 Python 包),用 importlib 按文件路径加载:
|
||||||
|
- skills/yt-studio-url-builder/scripts/build_studio_urls.py
|
||||||
|
- skills/youtube-studio-csv-download/scripts/youtube_export_download.py
|
||||||
|
|
||||||
|
CLI 端到端测试则通过 subprocess 以 sys.executable 运行脚本(见 test_*_cli.py)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
URL_BUILDER_SCRIPT = ROOT / "skills" / "yt-studio-url-builder" / "scripts" / "build_studio_urls.py"
|
||||||
|
COUNTRIES_JSON = URL_BUILDER_SCRIPT.parent / "countries.json"
|
||||||
|
DOWNLOADER_SCRIPT = ROOT / "skills" / "youtube-studio-csv-download" / "scripts" / "youtube_export_download.py"
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module(name, path):
|
||||||
|
"""按文件路径加载独立脚本为 Python 模块。"""
|
||||||
|
spec = importlib.util.spec_from_file_location(name, path)
|
||||||
|
assert spec is not None and spec.loader is not None
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def url_builder():
|
||||||
|
"""build_studio_urls.py 模块(会话级,加载一次)。"""
|
||||||
|
return _load_module("build_studio_urls_under_test", URL_BUILDER_SCRIPT)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def downloader():
|
||||||
|
"""youtube_export_download.py 模块(会话级,加载一次)。"""
|
||||||
|
return _load_module("youtube_export_download_under_test", DOWNLOADER_SCRIPT)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def real_countries(url_builder):
|
||||||
|
"""脚本自带 countries.json 加载出的国家映射。"""
|
||||||
|
return url_builder.load_country_map(str(COUNTRIES_JSON))
|
||||||
|
|
||||||
|
|
||||||
|
def run_script(script, args, cwd=None):
|
||||||
|
"""以子进程运行被测脚本(uv run pytest 下 sys.executable 即 venv python)。
|
||||||
|
|
||||||
|
强制子进程 PYTHONUTF8=1,避免 Windows 管道输出按 GBK 解码导致中文乱码。
|
||||||
|
"""
|
||||||
|
env = {**os.environ, "PYTHONUTF8": "1", "PYTHONIOENCODING": "utf-8"}
|
||||||
|
return subprocess.run(
|
||||||
|
[sys.executable, str(script), *args],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
errors="replace",
|
||||||
|
cwd=cwd,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
400
tests/test_build_studio_urls.py
Normal file
400
tests/test_build_studio_urls.py
Normal file
@@ -0,0 +1,400 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""build_studio_urls.py 单元测试。
|
||||||
|
|
||||||
|
覆盖纯逻辑函数(不落盘、不起子进程):
|
||||||
|
ts() 日期 -> 日界线毫秒(锚点 + 整日偏移)
|
||||||
|
parse_period() 数据周期解析(多种写法、含首尾日)
|
||||||
|
normalize_columns() 输入列名别名映射
|
||||||
|
load_country_map() 国家映射加载
|
||||||
|
parse_countries() 国家列解析(中文名/ISO 码/多分隔符)
|
||||||
|
resolve_entity_type() 实体类型解析
|
||||||
|
build_url() URL 拼接(参数完整性、编码、缺字段错误)
|
||||||
|
|
||||||
|
CLI 端到端流程见 test_build_studio_urls_cli.py。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from conftest import COUNTRIES_JSON
|
||||||
|
|
||||||
|
# 文档化锚点:2026-06-15 = 1781506800000(见 docs/adr/0001,脚本内 ANCHOR_MS)
|
||||||
|
ANCHOR_MS = 1781506800000
|
||||||
|
MS_PER_DAY = 86400000
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# ts:日界线毫秒
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestTs:
|
||||||
|
def test_anchor_date(self, url_builder):
|
||||||
|
assert url_builder.ts(2026, 6, 15) == ANCHOR_MS
|
||||||
|
|
||||||
|
def test_next_day(self, url_builder):
|
||||||
|
assert url_builder.ts(2026, 6, 16) == ANCHOR_MS + MS_PER_DAY
|
||||||
|
|
||||||
|
def test_prev_day(self, url_builder):
|
||||||
|
assert url_builder.ts(2026, 6, 14) == ANCHOR_MS - MS_PER_DAY
|
||||||
|
|
||||||
|
def test_month_boundary(self, url_builder):
|
||||||
|
assert url_builder.ts(2026, 7, 1) == url_builder.ts(2026, 6, 30) + MS_PER_DAY
|
||||||
|
|
||||||
|
def test_year_boundary(self, url_builder):
|
||||||
|
assert url_builder.ts(2027, 1, 1) == url_builder.ts(2026, 12, 31) + MS_PER_DAY
|
||||||
|
|
||||||
|
def test_anchor_matches_module_constant(self, url_builder):
|
||||||
|
assert url_builder.ts(2026, 6, 15) == url_builder.ANCHOR_MS
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# parse_period:数据周期
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestParsePeriod:
|
||||||
|
def test_standard_dot_format(self, url_builder):
|
||||||
|
start, end = url_builder.parse_period("2026.07.01-2026.08.01")
|
||||||
|
assert start == url_builder.ts(2026, 7, 1)
|
||||||
|
assert end == url_builder.ts(2026, 8, 1) + MS_PER_DAY # 结束日包含
|
||||||
|
|
||||||
|
def test_single_digit_month_day(self, url_builder):
|
||||||
|
assert url_builder.parse_period("2026.7.1-2026.8.1") == \
|
||||||
|
url_builder.parse_period("2026.07.01-2026.08.01")
|
||||||
|
|
||||||
|
def test_dash_date_with_tilde(self, url_builder):
|
||||||
|
start, end = url_builder.parse_period("2026-07-01~2026-08-01")
|
||||||
|
assert start == url_builder.ts(2026, 7, 1)
|
||||||
|
assert end == url_builder.ts(2026, 8, 1) + MS_PER_DAY
|
||||||
|
|
||||||
|
def test_full_width_tilde(self, url_builder):
|
||||||
|
start, _ = url_builder.parse_period("2026.07.01~2026.08.01")
|
||||||
|
assert start == url_builder.ts(2026, 7, 1)
|
||||||
|
|
||||||
|
def test_spaces_around_separator(self, url_builder):
|
||||||
|
start, _ = url_builder.parse_period("2026.07.01 - 2026.08.01")
|
||||||
|
assert start == url_builder.ts(2026, 7, 1)
|
||||||
|
|
||||||
|
def test_same_day_period(self, url_builder):
|
||||||
|
start, end = url_builder.parse_period("2026.07.01-2026.07.01")
|
||||||
|
assert start == url_builder.ts(2026, 7, 1)
|
||||||
|
assert end == start + MS_PER_DAY # 单日区间跨度正好一天
|
||||||
|
|
||||||
|
def test_end_date_inclusive(self, url_builder):
|
||||||
|
"""结束日包含在范围内:time_period 结束值 = 结束日次日日界线。"""
|
||||||
|
_, end = url_builder.parse_period("2026.07.01-2026.07.31")
|
||||||
|
assert end == url_builder.ts(2026, 8, 1)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("bad", [
|
||||||
|
"", # 空
|
||||||
|
"2026.07.01", # 只有起始日
|
||||||
|
"20260701-20260801", # 无分隔符
|
||||||
|
"abc-def", # 非日期
|
||||||
|
"2026.07-2026.08", # 缺日
|
||||||
|
"2026.07.01 至 2026.08.01", # 不支持的连接词
|
||||||
|
])
|
||||||
|
def test_invalid_format_raises(self, url_builder, bad):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
url_builder.parse_period(bad)
|
||||||
|
|
||||||
|
def test_invalid_month_raises(self, url_builder):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
url_builder.parse_period("2026.13.01-2026.08.01")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# normalize_columns:列名别名映射
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestNormalizeColumns:
|
||||||
|
def test_chinese_aliases(self, url_builder):
|
||||||
|
df = pd.DataFrame(columns=["所有者名称", "所有者ID", "群组名称", "群组ID",
|
||||||
|
"实体类型", "数据周期", "国家"])
|
||||||
|
mapping, unknown = url_builder.normalize_columns(df)
|
||||||
|
assert mapping == {
|
||||||
|
"owner_name": "所有者名称", "owner_id": "所有者ID",
|
||||||
|
"entity_name": "群组名称", "entity_id": "群组ID",
|
||||||
|
"entity_type": "实体类型", "period": "数据周期", "countries": "国家",
|
||||||
|
}
|
||||||
|
assert unknown == []
|
||||||
|
|
||||||
|
def test_english_aliases(self, url_builder):
|
||||||
|
df = pd.DataFrame(columns=["owner_name", "owner_id", "group_name", "group_id",
|
||||||
|
"entity_type", "period", "country"])
|
||||||
|
mapping, unknown = url_builder.normalize_columns(df)
|
||||||
|
assert mapping == {
|
||||||
|
"owner_name": "owner_name", "owner_id": "owner_id",
|
||||||
|
"entity_name": "group_name", "entity_id": "group_id",
|
||||||
|
"entity_type": "entity_type", "period": "period", "countries": "country",
|
||||||
|
}
|
||||||
|
assert unknown == []
|
||||||
|
|
||||||
|
def test_short_aliases(self, url_builder):
|
||||||
|
"""所有者ID 的超短别名 o、实体ID 的别名 id。"""
|
||||||
|
df = pd.DataFrame(columns=["o", "id", "period"])
|
||||||
|
mapping, _ = url_builder.normalize_columns(df)
|
||||||
|
assert mapping["owner_id"] == "o"
|
||||||
|
assert mapping["entity_id"] == "id"
|
||||||
|
|
||||||
|
def test_column_name_normalized_before_lookup(self, url_builder):
|
||||||
|
"""列名先去空白、转小写再匹配:' Owner_ID ' 可识别。"""
|
||||||
|
df = pd.DataFrame(columns=[" Owner_ID ", "Period"])
|
||||||
|
mapping, _ = url_builder.normalize_columns(df)
|
||||||
|
assert mapping["owner_id"] == " Owner_ID "
|
||||||
|
assert mapping["period"] == "Period"
|
||||||
|
|
||||||
|
def test_unknown_columns_reported(self, url_builder):
|
||||||
|
df = pd.DataFrame(columns=["所有者ID", "数据周期", "备注", "extra"])
|
||||||
|
mapping, unknown = url_builder.normalize_columns(df)
|
||||||
|
assert "owner_id" in mapping and "period" in mapping
|
||||||
|
assert unknown == ["备注", "extra"]
|
||||||
|
|
||||||
|
def test_country_slash_alias(self, url_builder):
|
||||||
|
df = pd.DataFrame(columns=["国家/地区"])
|
||||||
|
mapping, _ = url_builder.normalize_columns(df)
|
||||||
|
assert mapping["countries"] == "国家/地区"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# load_country_map:国家映射
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestLoadCountryMap:
|
||||||
|
def test_missing_file_returns_empty_and_warns(self, url_builder, tmp_path, capsys):
|
||||||
|
path = tmp_path / "not_exists.json"
|
||||||
|
assert url_builder.load_country_map(str(path)) == {}
|
||||||
|
assert "未找到国家映射文件" in capsys.readouterr().err
|
||||||
|
|
||||||
|
def test_bom_file_loads(self, url_builder, tmp_path):
|
||||||
|
"""utf-8-sig(Excel 另存带 BOM)可正常加载。"""
|
||||||
|
p = tmp_path / "c.json"
|
||||||
|
p.write_bytes('{"美国": "us"}'.encode("utf-8-sig"))
|
||||||
|
assert url_builder.load_country_map(str(p)) == {"美国": "US"}
|
||||||
|
|
||||||
|
def test_values_trimmed_and_uppercased(self, url_builder, tmp_path):
|
||||||
|
p = tmp_path / "c.json"
|
||||||
|
p.write_text('{ "美国" : " us " , "日本" : "jp" }', encoding="utf-8")
|
||||||
|
assert url_builder.load_country_map(str(p)) == {"美国": "US", "日本": "JP"}
|
||||||
|
|
||||||
|
def test_real_countries_json(self, real_countries):
|
||||||
|
assert real_countries["美国"] == "US"
|
||||||
|
assert real_countries["日本"] == "JP"
|
||||||
|
assert real_countries["中国台湾"] == "TW"
|
||||||
|
assert real_countries["香港"] == "HK"
|
||||||
|
# 真实映射规模足够覆盖常用地区
|
||||||
|
assert len(real_countries) > 80
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# parse_countries:国家列解析
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestParseCountries:
|
||||||
|
def test_none_returns_empty(self, url_builder):
|
||||||
|
assert url_builder.parse_countries(None, {}) == []
|
||||||
|
|
||||||
|
def test_nan_returns_empty(self, url_builder):
|
||||||
|
assert url_builder.parse_countries(float("nan"), {}) == []
|
||||||
|
|
||||||
|
def test_empty_string_returns_empty(self, url_builder):
|
||||||
|
assert url_builder.parse_countries("", {}) == []
|
||||||
|
|
||||||
|
def test_single_iso_code(self, url_builder):
|
||||||
|
assert url_builder.parse_countries("US", {}) == ["US"]
|
||||||
|
|
||||||
|
def test_lowercase_iso_uppercased(self, url_builder):
|
||||||
|
assert url_builder.parse_countries("us", {}) == ["US"]
|
||||||
|
|
||||||
|
def test_chinese_name_mapped(self, url_builder):
|
||||||
|
assert url_builder.parse_countries("美国", {"美国": "US"}) == ["US"]
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("sep", [",", ",", "、", ";", ";", " ", "|"])
|
||||||
|
def test_multiple_separators(self, url_builder, sep):
|
||||||
|
text = sep.join(["美国", "日本"])
|
||||||
|
assert url_builder.parse_countries(text, {"美国": "US", "日本": "JP"}) == ["US", "JP"]
|
||||||
|
|
||||||
|
def test_mixed_code_and_chinese(self, url_builder):
|
||||||
|
assert url_builder.parse_countries("US,日本", {"日本": "JP"}) == ["US", "JP"]
|
||||||
|
|
||||||
|
def test_quotes_stripped(self, url_builder):
|
||||||
|
assert url_builder.parse_countries("'US' \"JP\"", {}) == ["US", "JP"]
|
||||||
|
|
||||||
|
def test_unknown_name_raises(self, url_builder):
|
||||||
|
with pytest.raises(ValueError, match="未识别的国家"):
|
||||||
|
url_builder.parse_countries("亚特兰蒂斯", {"美国": "US"})
|
||||||
|
|
||||||
|
def test_chinese_name_without_map_raises(self, url_builder):
|
||||||
|
with pytest.raises(ValueError, match="未识别的国家"):
|
||||||
|
url_builder.parse_countries("美国", {})
|
||||||
|
|
||||||
|
def test_real_map_common_countries(self, url_builder, real_countries):
|
||||||
|
assert url_builder.parse_countries("美国,日本,英国", real_countries) == ["US", "JP", "GB"]
|
||||||
|
assert url_builder.parse_countries("中国台湾、香港", real_countries) == ["TW", "HK"]
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# resolve_entity_type:实体类型
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestResolveEntityType:
|
||||||
|
@pytest.mark.parametrize("text,expected", [
|
||||||
|
("群组", "GROUP"), ("GROUP", "GROUP"),
|
||||||
|
("所有者", "CONTENT_OWNER"), ("账号", "CONTENT_OWNER"), ("CONTENT_OWNER", "CONTENT_OWNER"),
|
||||||
|
("频道", "CHANNEL"), ("CHANNEL", "CHANNEL"),
|
||||||
|
("节目", "VIDEO"), ("视频", "VIDEO"), ("VIDEO", "VIDEO"),
|
||||||
|
])
|
||||||
|
def test_known_types(self, url_builder, text, expected):
|
||||||
|
assert url_builder.resolve_entity_type(text) == expected
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("empty", ["", None, float("nan")])
|
||||||
|
def test_empty_defaults_to_group(self, url_builder, empty):
|
||||||
|
assert url_builder.resolve_entity_type(empty) == "GROUP"
|
||||||
|
|
||||||
|
def test_whitespace_only_defaults_to_group(self, url_builder):
|
||||||
|
assert url_builder.resolve_entity_type(" ") == "GROUP"
|
||||||
|
|
||||||
|
def test_unknown_raises(self, url_builder):
|
||||||
|
with pytest.raises(ValueError, match="未识别的实体类型"):
|
||||||
|
url_builder.resolve_entity_type("星球")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# build_url:URL 拼接
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def make_row(**overrides):
|
||||||
|
row = {
|
||||||
|
"owner_name": "示例所有者",
|
||||||
|
"owner_id": "MC123",
|
||||||
|
"entity_name": "示例群组",
|
||||||
|
"entity_id": "NCy9C2QPQ1E",
|
||||||
|
"entity_type": "",
|
||||||
|
"period": "2026.07.01-2026.08.01",
|
||||||
|
"countries": "",
|
||||||
|
}
|
||||||
|
row.update(overrides)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildUrl:
|
||||||
|
def test_full_url_exact(self, url_builder):
|
||||||
|
"""完整 URL 精确匹配(含参数顺序与编码)。"""
|
||||||
|
row = make_row(countries="美国,日本")
|
||||||
|
url, status, msg = url_builder.build_url(row, {"美国": "US", "日本": "JP"})
|
||||||
|
assert (status, msg) == ("ok", "")
|
||||||
|
|
||||||
|
c = url_builder.CONFIG
|
||||||
|
start = url_builder.ts(2026, 7, 1)
|
||||||
|
end = url_builder.ts(2026, 8, 1) + MS_PER_DAY
|
||||||
|
expected = (
|
||||||
|
"https://studio.youtube.com/owner/MC123/analytics/tab-overview/period-default/explore"
|
||||||
|
f"?o=MC123&entity_type=GROUP&entity_id=NCy9C2QPQ1E"
|
||||||
|
f"&ur_dimensions=COUNTRY&ur_values=%27US%27%7C%27JP%27"
|
||||||
|
f"&ur_inclusive_starts=&ur_exclusive_ends="
|
||||||
|
f"&time_period={start}%2C{end}"
|
||||||
|
f"&explore_type={c['explore_type']}&metric={c['metric']}&granularity={c['granularity']}"
|
||||||
|
+ "".join(f"&t_metrics={m}" for m in c["t_metrics"])
|
||||||
|
+ f"&dimension={c['dimension']}&o_column={c['o_column']}"
|
||||||
|
f"&o_direction={c['o_direction']}&comparison_type={c['comparison_type']}"
|
||||||
|
)
|
||||||
|
assert url == expected
|
||||||
|
|
||||||
|
def test_url_parts(self, url_builder):
|
||||||
|
url, status, _ = url_builder.build_url(make_row(countries="US"), {})
|
||||||
|
assert status == "ok"
|
||||||
|
assert url.startswith(
|
||||||
|
"https://studio.youtube.com/owner/MC123/analytics/tab-overview/period-default/explore?")
|
||||||
|
assert "o=MC123" in url
|
||||||
|
assert "entity_type=GROUP" in url # 空实体类型默认 GROUP
|
||||||
|
assert "entity_id=NCy9C2QPQ1E" in url
|
||||||
|
assert "ur_dimensions=COUNTRY" in url
|
||||||
|
assert "ur_values=%27US%27" in url # 单国同样用 %27 包裹
|
||||||
|
assert "granularity=DAY" in url
|
||||||
|
assert "dimension=USER" in url
|
||||||
|
assert "o_direction=ANALYTICS_ORDER_DIRECTION_DESC" in url
|
||||||
|
|
||||||
|
def test_time_period_values(self, url_builder):
|
||||||
|
url, _, _ = url_builder.build_url(make_row(), {})
|
||||||
|
start = url_builder.ts(2026, 7, 1)
|
||||||
|
end = url_builder.ts(2026, 8, 1) + MS_PER_DAY
|
||||||
|
assert "time_period=%d%%2C%d" % (start, end) in url # 逗号编码为 %2C
|
||||||
|
|
||||||
|
def test_t_metrics_repeat_per_config(self, url_builder):
|
||||||
|
url, _, _ = url_builder.build_url(make_row(), {})
|
||||||
|
assert url.count("t_metrics=") == len(url_builder.CONFIG["t_metrics"])
|
||||||
|
for m in url_builder.CONFIG["t_metrics"]:
|
||||||
|
assert "t_metrics=%s" % m in url
|
||||||
|
|
||||||
|
def test_no_country_omits_country_params(self, url_builder):
|
||||||
|
url, _, _ = url_builder.build_url(make_row(), {})
|
||||||
|
assert "ur_dimensions" not in url
|
||||||
|
assert "ur_values" not in url
|
||||||
|
assert "ur_inclusive_starts" not in url
|
||||||
|
|
||||||
|
def test_entity_type_channel(self, url_builder):
|
||||||
|
url, _, _ = url_builder.build_url(
|
||||||
|
make_row(entity_type="频道", entity_id="UCxyz"), {})
|
||||||
|
assert "entity_type=CHANNEL" in url
|
||||||
|
assert "entity_id=UCxyz" in url
|
||||||
|
|
||||||
|
def test_content_owner_falls_back_to_owner_id(self, url_builder):
|
||||||
|
"""所有者整体场景:实体ID留空时回退用所有者ID。"""
|
||||||
|
row = make_row(entity_type="CONTENT_OWNER", entity_id="")
|
||||||
|
url, status, msg = url_builder.build_url(row, {})
|
||||||
|
assert (status, msg) == ("ok", "")
|
||||||
|
assert "entity_type=CONTENT_OWNER" in url
|
||||||
|
assert "entity_id=MC123" in url
|
||||||
|
|
||||||
|
def test_missing_owner_id(self, url_builder):
|
||||||
|
url, status, msg = url_builder.build_url(make_row(owner_id=""), {})
|
||||||
|
assert url is None
|
||||||
|
assert status == "error"
|
||||||
|
assert msg == "缺少所有者ID"
|
||||||
|
|
||||||
|
def test_missing_entity_id_for_group(self, url_builder):
|
||||||
|
url, status, msg = url_builder.build_url(make_row(entity_id=""), {})
|
||||||
|
assert url is None
|
||||||
|
assert status == "error"
|
||||||
|
assert msg == "缺少实体ID"
|
||||||
|
|
||||||
|
def test_missing_entity_id_for_channel(self, url_builder):
|
||||||
|
url, status, msg = url_builder.build_url(
|
||||||
|
make_row(entity_type="频道", entity_id=""), {})
|
||||||
|
assert url is None
|
||||||
|
assert status == "error"
|
||||||
|
assert msg == "缺少实体ID"
|
||||||
|
|
||||||
|
def test_missing_period(self, url_builder):
|
||||||
|
url, status, msg = url_builder.build_url(make_row(period=""), {})
|
||||||
|
assert url is None
|
||||||
|
assert status == "error"
|
||||||
|
assert msg == "缺少数据周期"
|
||||||
|
|
||||||
|
def test_bad_period_raises_value_error(self, url_builder):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
url_builder.build_url(make_row(period="not-a-period"), {})
|
||||||
|
|
||||||
|
def test_unknown_country_raises_value_error(self, url_builder):
|
||||||
|
with pytest.raises(ValueError, match="未识别的国家"):
|
||||||
|
url_builder.build_url(make_row(countries="火星"), {"美国": "US"})
|
||||||
|
|
||||||
|
def test_unknown_entity_type_raises_value_error(self, url_builder):
|
||||||
|
with pytest.raises(ValueError, match="未识别的实体类型"):
|
||||||
|
url_builder.build_url(make_row(entity_type="星球"), {})
|
||||||
|
|
||||||
|
def test_owner_id_checked_before_entity(self, url_builder):
|
||||||
|
"""所有者ID 缺失时报错优先于实体ID。"""
|
||||||
|
_, status, msg = url_builder.build_url(make_row(owner_id="", entity_id=""), {})
|
||||||
|
assert (status, msg) == ("error", "缺少所有者ID")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 其他:nan 判断辅助行为(build_url 入参可能来自 pandas)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestNanHandling:
|
||||||
|
def test_period_nan_string_is_error(self, url_builder):
|
||||||
|
"""main() 会把 NaN 转 '',这里验证空串路径报「缺少数据周期」。"""
|
||||||
|
_, status, msg = url_builder.build_url(make_row(period=""), {})
|
||||||
|
assert msg == "缺少数据周期"
|
||||||
|
|
||||||
|
def test_parse_countries_nan(self, url_builder):
|
||||||
|
assert url_builder.parse_countries(float("nan"), {}) == []
|
||||||
|
|
||||||
|
def test_plain_float_treated_as_text(self, url_builder):
|
||||||
|
"""普通 float(如 3.14)不是 NaN:走字符串解析分支,非两位代码则报未识别。"""
|
||||||
|
with pytest.raises(ValueError, match="未识别的国家"):
|
||||||
|
url_builder.parse_countries(3.14, {})
|
||||||
268
tests/test_build_studio_urls_cli.py
Normal file
268
tests/test_build_studio_urls_cli.py
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""build_studio_urls.py CLI 端到端测试。
|
||||||
|
|
||||||
|
以子进程运行脚本(与真实使用方式一致:uv run python build_studio_urls.py -i ... -o ...),
|
||||||
|
覆盖输入格式(CSV UTF-8-BOM / GBK / XLSX)、成功输出、失败行与退出码约定:
|
||||||
|
|
||||||
|
退出码 0 :全部行生成成功
|
||||||
|
退出码 1 :输入文件不存在 / 格式不支持 / 缺必要列
|
||||||
|
退出码 2 :存在失败行(成功行仍写入输出,失败行逐行打印在 stderr)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from conftest import ROOT, URL_BUILDER_SCRIPT, run_script
|
||||||
|
|
||||||
|
ASSET_XLSX = ROOT / "assets" / "需求输入示例.xlsx"
|
||||||
|
|
||||||
|
HEADERS = ["所有者名称", "所有者ID", "实体类型", "实体名称", "实体ID", "数据周期", "国家"]
|
||||||
|
|
||||||
|
OUTPUT_COLUMNS = ["所有者名称", "所有者ID", "实体类型", "实体名称", "实体ID",
|
||||||
|
"数据周期", "国家", "国家代码", "开始时间戳", "结束时间戳", "URL"]
|
||||||
|
|
||||||
|
|
||||||
|
def run_builder(args):
|
||||||
|
return run_script(URL_BUILDER_SCRIPT, args)
|
||||||
|
|
||||||
|
|
||||||
|
def read_output(path):
|
||||||
|
return pd.read_csv(path, encoding="utf-8-sig")
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 输入格式
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestInputFormats:
|
||||||
|
def test_csv_utf8_bom(self, tmp_path):
|
||||||
|
src = tmp_path / "需求.csv"
|
||||||
|
src.write_text(
|
||||||
|
",".join(HEADERS) + "\n"
|
||||||
|
"示例所有者,MC123,群组,示例群组,G001,2026.07.01-2026.08.01,美国\n",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
)
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(out)])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
assert "已生成 1 条 URL" in r.stdout
|
||||||
|
df = read_output(out)
|
||||||
|
assert list(df.columns) == OUTPUT_COLUMNS
|
||||||
|
assert len(df) == 1
|
||||||
|
assert df.loc[0, "国家代码"] == "US"
|
||||||
|
assert df.loc[0, "URL"].startswith("https://studio.youtube.com/owner/MC123/")
|
||||||
|
|
||||||
|
def test_csv_gbk_fallback(self, tmp_path):
|
||||||
|
"""Excel 另存 ANSI/GBK 编码 CSV 也能读。"""
|
||||||
|
src = tmp_path / "需求_gbk.csv"
|
||||||
|
src.write_text(
|
||||||
|
",".join(HEADERS) + "\n"
|
||||||
|
"示例所有者,MC123,群组,示例群组,G001,2026.07.01-2026.08.01,美国\n",
|
||||||
|
encoding="gbk",
|
||||||
|
)
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(out)])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
df = read_output(out)
|
||||||
|
assert len(df) == 1
|
||||||
|
assert df.loc[0, "国家代码"] == "US"
|
||||||
|
|
||||||
|
def test_xlsx_input(self, tmp_path):
|
||||||
|
src = tmp_path / "需求.xlsx"
|
||||||
|
pd.DataFrame([
|
||||||
|
{"所有者名称": "示例所有者", "所有者ID": "MC123", "实体类型": "群组",
|
||||||
|
"实体名称": "示例群组", "实体ID": "G001",
|
||||||
|
"数据周期": "2026.07.01-2026.08.01", "国家": "US"},
|
||||||
|
]).to_excel(src, index=False)
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(out)])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
df = read_output(out)
|
||||||
|
assert len(df) == 1
|
||||||
|
assert "ur_values=%27US%27" in df.loc[0, "URL"]
|
||||||
|
|
||||||
|
def test_unsupported_extension_exits_1(self, tmp_path):
|
||||||
|
src = tmp_path / "需求.txt"
|
||||||
|
src.write_text("所有者ID,数据周期\nMC123,2026.07.01-2026.08.01", encoding="utf-8")
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(tmp_path / "out.csv")])
|
||||||
|
assert r.returncode == 1
|
||||||
|
assert "不支持的输入格式" in r.stderr
|
||||||
|
|
||||||
|
def test_missing_file_exits_1(self, tmp_path):
|
||||||
|
r = run_builder(["-i", str(tmp_path / "不存在.csv"), "-o", str(tmp_path / "out.csv")])
|
||||||
|
assert r.returncode == 1
|
||||||
|
assert "输入文件不存在" in r.stderr
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 成功路径与输出内容
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestHappyPath:
|
||||||
|
def test_output_content(self, tmp_path):
|
||||||
|
src = tmp_path / "需求.csv"
|
||||||
|
pd.DataFrame([
|
||||||
|
{"所有者名称": "所有者A", "所有者ID": "MC123", "实体类型": "群组",
|
||||||
|
"实体名称": "群组1", "实体ID": "G001",
|
||||||
|
"数据周期": "2026.07.01-2026.08.01", "国家": "美国,日本"},
|
||||||
|
{"所有者名称": "所有者B", "所有者ID": "MC456", "实体类型": "频道",
|
||||||
|
"实体名称": "频道1", "实体ID": "UCxyz",
|
||||||
|
"数据周期": "2026.8.1-2026.8.31", "国家": ""},
|
||||||
|
]).to_csv(src, index=False, encoding="utf-8-sig")
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(out)])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
df = read_output(out)
|
||||||
|
assert len(df) == 2
|
||||||
|
|
||||||
|
row0, row1 = df.iloc[0], df.iloc[1]
|
||||||
|
assert row0["国家代码"] == "US,JP"
|
||||||
|
assert "ur_values=%27US%27%7C%27JP%27" in row0["URL"]
|
||||||
|
# 时间戳 = 日界线毫秒(独立于模块重算:锚点 2026-06-15 + 整日偏移)
|
||||||
|
anchor = 1781506800000
|
||||||
|
assert row0["开始时间戳"] == anchor + 16 * 86400000 # 2026-07-01
|
||||||
|
assert row0["结束时间戳"] == anchor + 48 * 86400000 # 2026-08-01 次日
|
||||||
|
# 无国家行:不含国家筛选参数,实体类型为频道
|
||||||
|
assert "ur_values" not in row1["URL"]
|
||||||
|
assert "entity_type=CHANNEL" in row1["URL"]
|
||||||
|
assert "entity_id=UCxyz" in row1["URL"]
|
||||||
|
assert row1["国家代码"] == "" or pd.isna(row1["国家代码"])
|
||||||
|
|
||||||
|
def test_default_output_next_to_input(self, tmp_path):
|
||||||
|
"""不传 -o 时输出到输入同目录 studio_urls_output.csv。"""
|
||||||
|
src = tmp_path / "sub"
|
||||||
|
src.mkdir()
|
||||||
|
inp = src / "需求.csv"
|
||||||
|
inp.write_text(
|
||||||
|
"所有者ID,实体ID,数据周期\nMC123,G001,2026.07.01-2026.08.01",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
)
|
||||||
|
r = run_builder(["-i", str(inp)])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
assert (src / "studio_urls_output.csv").exists()
|
||||||
|
assert "已生成 1 条 URL" in r.stdout
|
||||||
|
|
||||||
|
def test_blank_rows_dropped(self, tmp_path):
|
||||||
|
"""全空行不产出 URL、不算失败。"""
|
||||||
|
src = tmp_path / "需求.csv"
|
||||||
|
src.write_text(
|
||||||
|
"所有者ID,实体ID,数据周期\n"
|
||||||
|
"MC123,G001,2026.07.01-2026.08.01\n"
|
||||||
|
",,\n"
|
||||||
|
"MC456,G002,2026.07.01-2026.08.01\n",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
)
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(out)])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
assert "已生成 2 条 URL" in r.stdout
|
||||||
|
|
||||||
|
def test_custom_countries_json(self, tmp_path):
|
||||||
|
cj = tmp_path / "my_countries.json"
|
||||||
|
cj.write_text('{"梦幻国": "ZZ"}', encoding="utf-8")
|
||||||
|
src = tmp_path / "需求.csv"
|
||||||
|
src.write_text(
|
||||||
|
"所有者ID,实体ID,数据周期,国家\nMC123,G001,2026.07.01-2026.08.01,梦幻国",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
)
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(out), "--countries", str(cj)])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
df = read_output(out)
|
||||||
|
assert df.loc[0, "国家代码"] == "ZZ"
|
||||||
|
assert "ur_values=%27ZZ%27" in df.loc[0, "URL"]
|
||||||
|
|
||||||
|
def test_unknown_columns_warned_but_ok(self, tmp_path):
|
||||||
|
src = tmp_path / "需求.csv"
|
||||||
|
src.write_text(
|
||||||
|
"所有者ID,实体ID,数据周期,备注\nMC123,G001,2026.07.01-2026.08.01,随便写",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
)
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(out)])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
assert "未识别的列" in r.stderr and "备注" in r.stderr
|
||||||
|
assert len(read_output(out)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 失败行与退出码
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestErrorRows:
|
||||||
|
def test_error_row_exits_2_and_reports_line(self, tmp_path):
|
||||||
|
"""失败行:退出码 2,stderr 报「第N行」(N 按文件行号,含表头);成功行照常输出。"""
|
||||||
|
src = tmp_path / "需求.csv"
|
||||||
|
src.write_text(
|
||||||
|
"所有者ID,实体ID,数据周期\n"
|
||||||
|
"MC123,G001,2026.07.01-2026.08.01\n" # 第2行 成功
|
||||||
|
"MC123,,2026.07.01-2026.08.01\n" # 第3行 缺实体ID -> 失败
|
||||||
|
"MC456,G002,2026.07.01-2026.08.01\n", # 第4行 成功
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
)
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(out)])
|
||||||
|
assert r.returncode == 2
|
||||||
|
assert "已生成 2 条 URL" in r.stdout
|
||||||
|
assert "以下 1 行生成失败" in r.stderr
|
||||||
|
assert "第3行" in r.stderr
|
||||||
|
assert "缺少实体ID" in r.stderr
|
||||||
|
assert len(read_output(out)) == 2
|
||||||
|
|
||||||
|
def test_missing_required_column_exits_1(self, tmp_path):
|
||||||
|
src = tmp_path / "需求.csv"
|
||||||
|
src.write_text("所有者名称,实体名称\nA,B\n", encoding="utf-8-sig")
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(tmp_path / "out.csv")])
|
||||||
|
assert r.returncode == 1
|
||||||
|
# 提示缺失的规范字段名(owner_id 与 period 同时缺失时都列出)
|
||||||
|
assert "缺少必要列" in r.stderr
|
||||||
|
assert "owner_id" in r.stderr
|
||||||
|
assert "period" in r.stderr
|
||||||
|
|
||||||
|
def test_all_rows_fail_still_writes_empty_output(self, tmp_path):
|
||||||
|
src = tmp_path / "需求.csv"
|
||||||
|
src.write_text(
|
||||||
|
"所有者ID,实体ID,数据周期\nMC123,,2026.07.01-2026.08.01\n",
|
||||||
|
encoding="utf-8-sig",
|
||||||
|
)
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(src), "-o", str(out)])
|
||||||
|
assert r.returncode == 2
|
||||||
|
assert "已生成 0 条 URL" in r.stdout
|
||||||
|
assert out.exists() # 空结果也落盘(仅表头)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 集成:真实资产文件
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestRealAsset:
|
||||||
|
def test_example_asset_generates_all_urls(self, tmp_path):
|
||||||
|
"""assets/需求输入示例.xlsx:6 条示例(群组/所有者/频道/节目)全部成功。"""
|
||||||
|
out = tmp_path / "out.csv"
|
||||||
|
r = run_builder(["-i", str(ASSET_XLSX), "-o", str(out)])
|
||||||
|
assert r.returncode == 0, r.stderr
|
||||||
|
df = read_output(out)
|
||||||
|
assert len(df) == 6
|
||||||
|
# 所有者整体行:实体ID 为空时回退用所有者ID
|
||||||
|
owner_row = df[df["实体类型"] == "所有者"].iloc[0]
|
||||||
|
assert "entity_type=CONTENT_OWNER" in owner_row["URL"]
|
||||||
|
assert "entity_id=bqSUnNpU67xJ51TxH4PKpQ" in owner_row["URL"]
|
||||||
|
# 实体类型枚举全覆盖
|
||||||
|
assert set(df["实体类型"]) == {"群组", "所有者", "频道", "节目"}
|
||||||
|
# 每行 URL 均含 owner 路径与 time_period
|
||||||
|
for url in df["URL"]:
|
||||||
|
assert url.startswith(
|
||||||
|
"https://studio.youtube.com/owner/bqSUnNpU67xJ51TxH4PKpQ/analytics/")
|
||||||
|
assert "time_period=" in url
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# CLI 参数
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestCliArgs:
|
||||||
|
def test_help(self):
|
||||||
|
r = run_builder(["--help"])
|
||||||
|
assert r.returncode == 0
|
||||||
|
assert "批量拼接 YouTube Studio explore URL" in r.stdout
|
||||||
|
|
||||||
|
def test_input_required(self):
|
||||||
|
r = run_builder([])
|
||||||
|
assert r.returncode != 0 # argparse 缺 -i 报错退出码 2
|
||||||
|
assert "required" in r.stderr or "-i" in r.stderr
|
||||||
344
tests/test_youtube_export_download.py
Normal file
344
tests/test_youtube_export_download.py
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""youtube_export_download.py 测试。
|
||||||
|
|
||||||
|
覆盖不依赖真实浏览器/登录态的全部逻辑:
|
||||||
|
dedup_path() 重名 ` (n)` 去重
|
||||||
|
build_export_filename() 从 exportQuery 反推 zip 文件名
|
||||||
|
decode_zipped_data() zippedData base64 解码
|
||||||
|
intercept_and_save() 响应拦截落盘(用 FakePage/FakeResponse 模拟 Playwright)
|
||||||
|
_default_user_data_dir() 浏览器用户数据目录推导
|
||||||
|
selftest() / --selftest 内置自测
|
||||||
|
|
||||||
|
run() 需要已登录 YouTube Studio 的真实浏览器会话,不在自动化测试范围(见 SKILL.md)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import zipfile
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from conftest import DOWNLOADER_SCRIPT, run_script
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 测试替身:模拟 playwright Page / Response / Request
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class FakeRequest:
|
||||||
|
def __init__(self, post_data=None):
|
||||||
|
self.post_data = post_data
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResponse:
|
||||||
|
def __init__(self, url, payload, post_data=None):
|
||||||
|
self.url = url
|
||||||
|
self._payload = payload
|
||||||
|
self.request = FakeRequest(post_data)
|
||||||
|
|
||||||
|
def json(self):
|
||||||
|
return self._payload
|
||||||
|
|
||||||
|
|
||||||
|
class FakePage:
|
||||||
|
"""记录 page.on() 注册的回调,测试中手动触发。"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self.handlers = {}
|
||||||
|
|
||||||
|
def on(self, event, handler):
|
||||||
|
self.handlers[event] = handler
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 工具
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
def make_zip_bytes(files):
|
||||||
|
"""构造内存 zip:{成员名: 内容(bytes/str)}。"""
|
||||||
|
buf = io.BytesIO()
|
||||||
|
with zipfile.ZipFile(buf, "w", zipfile.ZIP_DEFLATED) as zf:
|
||||||
|
for name, data in files.items():
|
||||||
|
zf.writestr(name, data)
|
||||||
|
return buf.getvalue()
|
||||||
|
|
||||||
|
|
||||||
|
def make_export_query(dimension="USER", start=20260723, end=20260820, with_time=True):
|
||||||
|
query = {"dimensions": [{"type": dimension}]}
|
||||||
|
if with_time:
|
||||||
|
query["timeRange"] = {"dateIdRange": {
|
||||||
|
"inclusiveStart": start, "exclusiveEnd": end}}
|
||||||
|
return {"joinRequest": {"nodes": [{"value": {"query": query}}]}}
|
||||||
|
|
||||||
|
|
||||||
|
def make_export_response(tmp_dir, files=None, account="WL Media",
|
||||||
|
dimension="USER", post_data=None):
|
||||||
|
"""构造一个命中 csv_export 的 FakeResponse(zip 为真实可解压内容)。"""
|
||||||
|
data = make_zip_bytes(files or {"表格数据.csv": "a,b\n1,2"})
|
||||||
|
payload = {"zippedData": base64.b64encode(data).decode("ascii")}
|
||||||
|
if post_data is None:
|
||||||
|
post_data = json.dumps({"exportQuery": make_export_query(dimension=dimension)})
|
||||||
|
url = "https://studio.youtube.com/youtubei/v1/yta_web/csv_export?alt=json"
|
||||||
|
return FakeResponse(url, payload, post_data), data
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# dedup_path:重名去重
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestDedupPath:
|
||||||
|
def test_no_conflict_returns_original(self, downloader, tmp_path):
|
||||||
|
assert Path(downloader.dedup_path(str(tmp_path), "需求文件.zip")).name == "需求文件.zip"
|
||||||
|
|
||||||
|
def test_conflict_appends_suffix_in_order(self, downloader, tmp_path):
|
||||||
|
d = str(tmp_path)
|
||||||
|
Path(downloader.dedup_path(d, "需求文件.zip")).write_bytes(b"a") # 需求文件.zip
|
||||||
|
p1 = Path(downloader.dedup_path(d, "需求文件.zip"))
|
||||||
|
assert p1.name == "需求文件 (1).zip"
|
||||||
|
p1.write_bytes(b"b")
|
||||||
|
p2 = Path(downloader.dedup_path(d, "需求文件.zip"))
|
||||||
|
assert p2.name == "需求文件 (2).zip"
|
||||||
|
p2.write_bytes(b"c")
|
||||||
|
assert Path(downloader.dedup_path(d, "需求文件.zip")).name == "需求文件 (3).zip"
|
||||||
|
|
||||||
|
def test_suffix_before_extension(self, downloader, tmp_path):
|
||||||
|
"""后缀插在扩展名之前:`名称 (1).zip` 而非 `名称.zip (1)`。"""
|
||||||
|
d = str(tmp_path)
|
||||||
|
Path(downloader.dedup_path(d, "report.2026.zip")).write_bytes(b"x")
|
||||||
|
assert Path(downloader.dedup_path(d, "report.2026.zip")).name == "report.2026 (1).zip"
|
||||||
|
|
||||||
|
def test_other_names_unaffected(self, downloader, tmp_path):
|
||||||
|
d = str(tmp_path)
|
||||||
|
Path(downloader.dedup_path(d, "A.zip")).write_bytes(b"x")
|
||||||
|
assert Path(downloader.dedup_path(d, "B.zip")).name == "B.zip"
|
||||||
|
|
||||||
|
def test_gap_filling(self, downloader, tmp_path):
|
||||||
|
"""(1) 被删后再次落盘优先复用空位。"""
|
||||||
|
d = str(tmp_path)
|
||||||
|
p0 = Path(downloader.dedup_path(d, "f.zip")); p0.write_bytes(b"0")
|
||||||
|
p1 = Path(downloader.dedup_path(d, "f.zip")); p1.write_bytes(b"1")
|
||||||
|
p1.unlink() # 删掉 (1)
|
||||||
|
assert Path(downloader.dedup_path(d, "f.zip")).name == "f (1).zip"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# build_export_filename:文件名反推
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestBuildExportFilename:
|
||||||
|
def test_user_dimension(self, downloader):
|
||||||
|
name = downloader.build_export_filename(make_export_query(), "WL Media")
|
||||||
|
assert name == "频道 2026-07-23_2026-08-20 WL Media.zip"
|
||||||
|
|
||||||
|
def test_video_dimension(self, downloader):
|
||||||
|
q = make_export_query(dimension="VIDEO")
|
||||||
|
assert downloader.build_export_filename(q, "acct") == "内容 2026-07-23_2026-08-20 acct.zip"
|
||||||
|
|
||||||
|
def test_content_owner_dimension(self, downloader):
|
||||||
|
q = make_export_query(dimension="CONTENT_OWNER")
|
||||||
|
assert downloader.build_export_filename(q, "acct") == "内容 2026-07-23_2026-08-20 acct.zip"
|
||||||
|
|
||||||
|
def test_custom_dimension_label_overrides(self, downloader):
|
||||||
|
q = make_export_query(dimension="VIDEO")
|
||||||
|
name = downloader.build_export_filename(q, "acct", dimension_label="地区")
|
||||||
|
assert name == "地区 2026-07-23_2026-08-20 acct.zip"
|
||||||
|
|
||||||
|
def test_unknown_dimension_empty_label(self, downloader):
|
||||||
|
q = make_export_query(dimension="SOMETHING")
|
||||||
|
assert downloader.build_export_filename(q, "acct") == " 2026-07-23_2026-08-20 acct.zip"
|
||||||
|
|
||||||
|
def test_date_id_formatting(self, downloader):
|
||||||
|
"""dateId 20260101 -> 2026-01-01(8 位定长切分)。"""
|
||||||
|
q = make_export_query(start=20260101, end=20260102)
|
||||||
|
assert "2026-01-01_2026-01-02" in downloader.build_export_filename(q, "acct")
|
||||||
|
|
||||||
|
def test_missing_date_range_raises(self, downloader):
|
||||||
|
q = make_export_query(with_time=False)
|
||||||
|
with pytest.raises(ValueError, match="日期范围"):
|
||||||
|
downloader.build_export_filename(q, "acct")
|
||||||
|
|
||||||
|
def test_empty_query_raises(self, downloader):
|
||||||
|
with pytest.raises(ValueError, match="日期范围"):
|
||||||
|
downloader.build_export_filename({}, "acct")
|
||||||
|
|
||||||
|
def test_multi_node_query(self, downloader):
|
||||||
|
"""维度与日期分布在不同 node 时也能各取所需。"""
|
||||||
|
q = {"joinRequest": {"nodes": [
|
||||||
|
{"value": {"query": {"dimensions": [{"type": "VIDEO"}]}}},
|
||||||
|
{"value": {"query": {"timeRange": {"dateIdRange": {
|
||||||
|
"inclusiveStart": 20260723, "exclusiveEnd": 20260820}}}}},
|
||||||
|
]}}
|
||||||
|
assert downloader.build_export_filename(q, "acct") == "内容 2026-07-23_2026-08-20 acct.zip"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# decode_zipped_data:zippedData 解码
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestDecodeZippedData:
|
||||||
|
def test_roundtrip(self, downloader):
|
||||||
|
data = make_zip_bytes({"表格数据.csv": "a,b\n1,2"})
|
||||||
|
payload = {"zippedData": base64.b64encode(data).decode("ascii")}
|
||||||
|
assert downloader.decode_zipped_data(payload) == data
|
||||||
|
|
||||||
|
def test_decoded_bytes_are_valid_zip(self, downloader):
|
||||||
|
data = make_zip_bytes({"x.csv": "1,2"})
|
||||||
|
payload = {"zippedData": base64.b64encode(data).decode("ascii")}
|
||||||
|
out = downloader.decode_zipped_data(payload)
|
||||||
|
with zipfile.ZipFile(io.BytesIO(out)) as zf:
|
||||||
|
assert zf.read("x.csv") == b"1,2"
|
||||||
|
|
||||||
|
def test_missing_field_raises(self, downloader):
|
||||||
|
with pytest.raises(ValueError, match="zippedData"):
|
||||||
|
downloader.decode_zipped_data({"foo": "bar"})
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("empty", ["", None])
|
||||||
|
def test_empty_field_raises(self, downloader, empty):
|
||||||
|
with pytest.raises(ValueError, match="zippedData"):
|
||||||
|
downloader.decode_zipped_data({"zippedData": empty})
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# intercept_and_save:拦截器(FakePage 模拟)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestInterceptAndSave:
|
||||||
|
def test_saves_zip_and_reports(self, downloader, tmp_path):
|
||||||
|
page = FakePage()
|
||||||
|
saved = downloader.intercept_and_save(page, "WL Media", str(tmp_path))
|
||||||
|
resp, data = make_export_response(tmp_path, {"表格数据.csv": "a,b\n1,2"})
|
||||||
|
|
||||||
|
page.handlers["response"](resp)
|
||||||
|
|
||||||
|
assert len(saved) == 1
|
||||||
|
filename, size, path = saved[0]
|
||||||
|
assert filename == "频道 2026-07-23_2026-08-20 WL Media.zip"
|
||||||
|
assert size == len(data)
|
||||||
|
assert Path(path).read_bytes() == data # 落盘内容与响应一致
|
||||||
|
with zipfile.ZipFile(path) as zf: # 且是可解压的有效 zip
|
||||||
|
assert zf.read("表格数据.csv") == b"a,b\n1,2"
|
||||||
|
|
||||||
|
def test_second_export_deduped(self, downloader, tmp_path):
|
||||||
|
page = FakePage()
|
||||||
|
saved = downloader.intercept_and_save(page, "WL Media", str(tmp_path))
|
||||||
|
resp, _ = make_export_response(tmp_path)
|
||||||
|
|
||||||
|
page.handlers["response"](resp)
|
||||||
|
page.handlers["response"](resp)
|
||||||
|
|
||||||
|
# saved 记录的是请求体反推出的原始文件名;实际落盘路径(s[2])带去重后缀
|
||||||
|
assert [s[0] for s in saved] == ["频道 2026-07-23_2026-08-20 WL Media.zip"] * 2
|
||||||
|
assert [Path(s[2]).name for s in saved] == [
|
||||||
|
"频道 2026-07-23_2026-08-20 WL Media.zip",
|
||||||
|
"频道 2026-07-23_2026-08-20 WL Media (1).zip",
|
||||||
|
]
|
||||||
|
|
||||||
|
def test_ignores_non_export_responses(self, downloader, tmp_path):
|
||||||
|
page = FakePage()
|
||||||
|
saved = downloader.intercept_and_save(page, "WL Media", str(tmp_path))
|
||||||
|
other = FakeResponse("https://studio.youtube.com/youtubei/v1/other", {})
|
||||||
|
page.handlers["response"](other)
|
||||||
|
assert saved == []
|
||||||
|
assert list(tmp_path.iterdir()) == [] # 无任何落盘
|
||||||
|
|
||||||
|
def test_illegal_chars_sanitized(self, downloader, tmp_path):
|
||||||
|
"""Windows 非法字符 \\ / : * ? " < > | 替换为 _。"""
|
||||||
|
page = FakePage()
|
||||||
|
saved = downloader.intercept_and_save(page, r'A/B:C*D?E"F<G>H|I', str(tmp_path))
|
||||||
|
resp, _ = make_export_response(tmp_path)
|
||||||
|
page.handlers["response"](resp)
|
||||||
|
filename = saved[0][0]
|
||||||
|
assert filename == "频道 2026-07-23_2026-08-20 A_B_C_D_E_F_G_H_I.zip"
|
||||||
|
assert Path(saved[0][2]).exists()
|
||||||
|
|
||||||
|
def test_bad_post_data_falls_back_to_export_zip(self, downloader, tmp_path):
|
||||||
|
"""exportQuery 反推失败时兜底 export.zip,内容不丢。"""
|
||||||
|
page = FakePage()
|
||||||
|
saved = downloader.intercept_and_save(page, "WL Media", str(tmp_path))
|
||||||
|
resp, data = make_export_response(tmp_path, post_data="not-json-{{{")
|
||||||
|
page.handlers["response"](resp)
|
||||||
|
assert saved[0][0] == "export.zip"
|
||||||
|
assert Path(saved[0][2]).read_bytes() == data
|
||||||
|
|
||||||
|
def test_empty_post_data_falls_back(self, downloader, tmp_path):
|
||||||
|
"""request.post_data 为 None(或 "{}"):反推失败兜底 export.zip。"""
|
||||||
|
page = FakePage()
|
||||||
|
saved = downloader.intercept_and_save(page, "acct", str(tmp_path))
|
||||||
|
data = make_zip_bytes({"x.csv": "1"})
|
||||||
|
resp = FakeResponse(
|
||||||
|
"https://studio.youtube.com/youtubei/v1/yta_web/csv_export?alt=json",
|
||||||
|
{"zippedData": base64.b64encode(data).decode("ascii")},
|
||||||
|
post_data=None,
|
||||||
|
)
|
||||||
|
page.handlers["response"](resp)
|
||||||
|
assert saved[0][0] == "export.zip"
|
||||||
|
assert Path(saved[0][2]).read_bytes() == data
|
||||||
|
|
||||||
|
def test_missing_zipped_data_no_save_with_stderr(self, downloader, tmp_path, capsys):
|
||||||
|
page = FakePage()
|
||||||
|
saved = downloader.intercept_and_save(page, "WL Media", str(tmp_path))
|
||||||
|
resp = FakeResponse(
|
||||||
|
"https://studio.youtube.com/youtubei/v1/yta_web/csv_export?alt=json",
|
||||||
|
{"foo": "bar"},
|
||||||
|
post_data=json.dumps({"exportQuery": make_export_query()}),
|
||||||
|
)
|
||||||
|
page.handlers["response"](resp)
|
||||||
|
assert saved == []
|
||||||
|
assert list(tmp_path.iterdir()) == []
|
||||||
|
assert "处理 csv_export 响应失败" in capsys.readouterr().err
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# _default_user_data_dir:用户数据目录推导
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestDefaultUserDataDir:
|
||||||
|
def test_chrome(self, downloader, monkeypatch):
|
||||||
|
monkeypatch.setenv("LOCALAPPDATA", r"C:\fake\Local")
|
||||||
|
assert downloader._default_user_data_dir("chrome") == \
|
||||||
|
r"C:\fake\Local\Google\Chrome\User Data"
|
||||||
|
|
||||||
|
def test_msedge(self, downloader, monkeypatch):
|
||||||
|
monkeypatch.setenv("LOCALAPPDATA", r"C:\fake\Local")
|
||||||
|
assert downloader._default_user_data_dir("msedge") == \
|
||||||
|
r"C:\fake\Local\Microsoft\Edge\User Data"
|
||||||
|
|
||||||
|
def test_default_is_chrome(self, downloader, monkeypatch):
|
||||||
|
monkeypatch.setenv("LOCALAPPDATA", r"C:\fake\Local")
|
||||||
|
assert downloader._default_user_data_dir(None) == \
|
||||||
|
r"C:\fake\Local\Google\Chrome\User Data"
|
||||||
|
|
||||||
|
def test_fallback_without_env(self, downloader, monkeypatch):
|
||||||
|
monkeypatch.delenv("LOCALAPPDATA", raising=False)
|
||||||
|
expected = str(Path.home() / "AppData" / "Local" / "Google" / "Chrome" / "User Data")
|
||||||
|
assert downloader._default_user_data_dir("chrome") == expected
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 内置自测与 CLI
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestSelftest:
|
||||||
|
def test_module_selftest(self, downloader, capsys):
|
||||||
|
downloader.selftest()
|
||||||
|
assert "selftest OK" in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_cli_selftest(self):
|
||||||
|
r = run_script(DOWNLOADER_SCRIPT, ["--selftest"])
|
||||||
|
assert r.returncode == 0
|
||||||
|
assert "selftest OK" in r.stdout
|
||||||
|
|
||||||
|
def test_cli_no_args_runs_selftest_and_prints_usage(self):
|
||||||
|
"""无参数运行:先自测,再提示实际运行方式。"""
|
||||||
|
r = run_script(DOWNLOADER_SCRIPT, [])
|
||||||
|
assert r.returncode == 0
|
||||||
|
assert "selftest OK" in r.stdout
|
||||||
|
assert "--user-data-dir" in r.stdout
|
||||||
|
assert "--connect" in r.stdout
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# 模块常量契约
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
class TestConstants:
|
||||||
|
def test_csv_export_path(self, downloader):
|
||||||
|
assert downloader.CSV_EXPORT_PATH == "/youtubei/v1/yta_web/csv_export"
|
||||||
|
|
||||||
|
def test_dimension_labels(self, downloader):
|
||||||
|
assert downloader.DIMENSION_LABEL["VIDEO"] == "内容"
|
||||||
|
assert downloader.DIMENSION_LABEL["USER"] == "频道"
|
||||||
|
assert downloader.DIMENSION_LABEL["CONTENT_OWNER"] == "内容"
|
||||||
716
uv.lock
generated
Normal file
716
uv.lock
generated
Normal file
@@ -0,0 +1,716 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||||
|
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
|
||||||
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
"python_full_version == '3.11.*' and sys_platform == 'win32'",
|
||||||
|
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
"python_full_version < '3.11'",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "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 = "exceptiongroup"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "greenlet"
|
||||||
|
version = "3.5.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/0b/d8/7cc97c142388aef03f622e001c572c4f84e9252a439549d483f555771970/greenlet-3.5.5.tar.gz", hash = "sha256:adb4bae02e91a8e863e48b177e4014bdcac8a6b5e047ea1df687a61534b85e6c", size = 207585, upload-time = "2026-08-10T15:09:36.136Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/67/07ecd6d85c0f253363cbc4ac9e7dd048ca571a267fbccfea084d6009ac3b/greenlet-3.5.5-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:816230f469381ad0a43abc9fa8dda5a699e32fb78958dde32ded93213b70a667", size = 292971, upload-time = "2026-08-10T13:28:09.837Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/35/426733bc24247ee17bb76df90f550c299ff5f8572b2bdd7cacb5c53fd994/greenlet-3.5.5-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5433cf291e0ef9114bd14d0d824db6e5e4a43033234bca48181a9597acca07b", size = 609290, upload-time = "2026-08-10T14:14:32.273Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/dc/17d3a5acceb2fd0bbc9682a228117b719cdfb68085e6dbb33fa325755f27/greenlet-3.5.5-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:19d59f068887d8c5907fc177f27683413ace3011b6ed646c0b309266e74a6502", size = 622650, upload-time = "2026-08-10T14:27:22.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/3f/b31e6bd6adb8f80492efb6a47e8f9cd0e7335db5aac2795b1876d5afcfee/greenlet-3.5.5-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:86c5113d698cb8d927b2750bb1f1d59eefe3a37e0e0217491aee29a7f84ef52c", size = 629560, upload-time = "2026-08-10T14:30:04.733Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/91/00b3c0566316c6f383ea16ee05388ec4f57f6e0afa49e72ae471eda8c47f/greenlet-3.5.5-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ff00e12102358292087274dfb1669132387ff6e7920ebf9d85f4826ce0d3a56", size = 622819, upload-time = "2026-08-10T13:40:46.562Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/5e/58c561efde575c4ca070030e2e0513e8d1b07b76d8a2d84a9bcef1becd42/greenlet-3.5.5-cp310-cp310-manylinux_2_39_riscv64.whl", hash = "sha256:c69bed34470abfcd456984fdadaa18e62169af4480335c45f3c32d1d9c12e638", size = 425489, upload-time = "2026-08-10T14:29:59.768Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/78/52de4f7ac9152ad1dbc8f437895c1e573d30ee1e1427e0f91a280e2417b6/greenlet-3.5.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:523bb8e27614d77101ea7a8cf59f8d91219b72d5c29f6a038c92b50828bfa8d0", size = 1582164, upload-time = "2026-08-10T14:15:02.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/ff/c3855a00c2417e8f61c7b9f0bc4f8f599c6928f5c15681ad251e78a91e05/greenlet-3.5.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f1e2db190db51c17433eee424803818cf0670bf049d9cfe0dd07be111d1aa7c4", size = 1648807, upload-time = "2026-08-10T13:40:27.471Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/21/d29ff6a79dbd1ec1fe74c155d25d4c5a85e221d6b9ffc2cc7a709e7c8c39/greenlet-3.5.5-cp310-cp310-win_amd64.whl", hash = "sha256:740e544169527b82695ce76af2f7ad6f030904658f2f3921a1d245771fb88cfc", size = 322832, upload-time = "2026-08-10T13:28:10.85Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/a3/07297917485ee2ca85bc3c8dc6ed85ad3fffcf424047fba62671dba68e97/greenlet-3.5.5-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:be63afcbbccfad3dd95a1ba12ada84dab2ef32031973d80b5b92df67fa763a61", size = 294165, upload-time = "2026-08-10T13:25:17.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/db/51/6f732f9314cda54c5fd48a7620c7160f4f286967e8045ad94b9d66ce80b7/greenlet-3.5.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a268024ce2d7d2b04694bf1594058981a9fa663d1df4b762dee499211ed7c1c", size = 613610, upload-time = "2026-08-10T14:14:33.829Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/c0/b27589e25d220289edcd4d582b2b17b83058d1a56d53d971b6ea1a34f10d/greenlet-3.5.5-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:35cbb8bf55ace57fbccb4fb8622c4521713acd8691e77f4696d416ea7ca527da", size = 625481, upload-time = "2026-08-10T14:27:23.647Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/82/5c873dbb4fb001d22fbbd50e80d4c1b0181ddae106856132160f84b94e88/greenlet-3.5.5-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:abc8bc8d9f935cd685457545b6a53863a877fdc12c2c0f5ee9beee18d9db139c", size = 633329, upload-time = "2026-08-10T14:30:06.062Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/2d/f2c928218ac52f26d7a2c188c171d1b7e728b23782cb3347e7b4fce1493a/greenlet-3.5.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cc6df89ec5302337adc9cf096221cbed2510fd444b0e0f1586cf0470740864", size = 624562, upload-time = "2026-08-10T13:40:48.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/12/f7df98e72a8eb4a7edfec5a08d6d1a4ab53a52c95ba0b2ea6c10b8dd9bd0/greenlet-3.5.5-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:3134291427bb0f3526e9d90311988caf336eb43730e95244997a4fb15f45144f", size = 428145, upload-time = "2026-08-10T14:30:01.071Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/4a/92fc51d5d35912f4f06eec037ba347985defd0be47463a010a325634d9d2/greenlet-3.5.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6d9b454c5fc48aeaa7c4337813dbf513a6870468e426438a04d922c6d0fe63db", size = 1584909, upload-time = "2026-08-10T14:15:04.343Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/58/ed98b80ac5738c149a5258544843c45601ade1fd70f61740cdaead6351b3/greenlet-3.5.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:03551ed792cb1b4fc0277a0c60dfd8c343894a0ba06fe60dcd22f568b433da39", size = 1651184, upload-time = "2026-08-10T13:40:28.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d8/be/b582ceb80cefdf9d8da34078714e4b12b3d16f509dee0f65e40a5cc8fc7d/greenlet-3.5.5-cp311-cp311-win_amd64.whl", hash = "sha256:ab3df3dffb58bf70564e93a5cec7941e4d9faa5a36cc4234a10d3131afe04f53", size = 323280, upload-time = "2026-08-10T13:26:07.495Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4d/18/5313c4c58598c38b0373c013e4ff2b3e6d258aaaa338f373335ebecdaddd/greenlet-3.5.5-cp311-cp311-win_arm64.whl", hash = "sha256:2b70a766135540c472ac1393d57c2e1b4a2eb85bf526a1e41e6d096173a8cee5", size = 307785, upload-time = "2026-08-10T13:28:34.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/7e/9ecd0285e3153532ae07aeb88063c43c72b4221cf0d4d123b02f3682e3ff/greenlet-3.5.5-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:49520f0c95a48b42cf55414b8e8479beb274ea70431afc33e3f79903c71f4380", size = 295809, upload-time = "2026-08-10T13:25:34.023Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/35/73/60e4bbcc89252037b18087f2ec16405d5b2d5be42dde191bbf3667e96102/greenlet-3.5.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55272212cbc5f43d1d723725ab931f1939969b7e9523882ca58b55061769d053", size = 611910, upload-time = "2026-08-10T14:14:35.18Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/17/cd5134be659cd4a443e7a61ae670dabec165a814c51162916d637b6dd38e/greenlet-3.5.5-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655bca754a2ef4efcb0eb48a94d3f4593536d0f3d48f8ed44343c01d16a92f95", size = 624198, upload-time = "2026-08-10T14:27:25.229Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/30/87c212b5c684d0e72974f1063b7a9687631e8985902c06e1016542c874e7/greenlet-3.5.5-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ca5d6ae0739e5764f2cfcfaa562ac5a990cbdaedca93251c5e3cf07c362371f", size = 629504, upload-time = "2026-08-10T14:30:07.967Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/ac/5c5b959999b6f09c3026b5dfe171575bc3121c5236ce74f495096f25b203/greenlet-3.5.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:147b25a42e5ca5be3d42356e8f608b37af715a1c196e9bf9d1627f3341adfe1d", size = 621439, upload-time = "2026-08-10T13:40:49.391Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/2c/eb487fafc9f50ffff2b1e0b697f70fb34bf150821c08ab225aacf5583a7e/greenlet-3.5.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1b5ed9162c0c098e0bbc2cf88a94f433c1b8926f831745252e099e5d83e17759", size = 432462, upload-time = "2026-08-10T14:30:02.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/8b/6acf112ed8aee499f25b4d6949820fb02ac950ff9c1f3d793bd5be0599f2/greenlet-3.5.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:27493374cff1d1b7919dc8126547f2aea582737e3046147b434b1e12de56389b", size = 1581342, upload-time = "2026-08-10T14:15:05.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/d7/734e5f198888876b42d7616ff6644c075baf6b8a2412deadd6b0e1b8b20c/greenlet-3.5.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12e2ee66c2aba86133f10fd99d6a8856c6d351ffb7be0e4d52ef2cc5fbb705b2", size = 1645744, upload-time = "2026-08-10T13:40:30.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/30/1f42b88dc587b5899ee50616ad56ee40cafaf225df4fb829f10183c62a5c/greenlet-3.5.5-cp312-cp312-win_amd64.whl", hash = "sha256:49ddacd36af37735fab103846f4ee4d18a492dde72730d1699c0c8ebe30d9f18", size = 324171, upload-time = "2026-08-10T13:28:44.472Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/e5/4dee4d8d2e603fe5fdd7b444e63219f7b9bd852c60c6214511c7157cbe88/greenlet-3.5.5-cp312-cp312-win_arm64.whl", hash = "sha256:5f1b1ff4828cdc1aba4266aff814085d04a1d07959287219af021b838b265d52", size = 308362, upload-time = "2026-08-10T13:26:46.839Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/3d/8cef5f724ec0d4add2af8961d504535ec60c3cca9e464f6d03bdba29d85b/greenlet-3.5.5-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:b79fd2a5bc099b5e744f34c4c9a58954a5f4cb7529fb4b6e8446057d61b6edaa", size = 294730, upload-time = "2026-08-10T13:27:51.206Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/4b/8e7aa3f514273aecff30a16ab1bac09ff54cfc7e6860fdd8058c37ff2499/greenlet-3.5.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:634cf15a233a949136879dd388e25d3296e16f3f1e217d2456797b8579ebc6ed", size = 614536, upload-time = "2026-08-10T14:14:36.589Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/48/4e95e9dd5a8a397dc6a6345dd7f1935113d0fca4f85e89d3976da9cd988d/greenlet-3.5.5-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:499adea519f748407fc6806d20eedabac2884fd73b9f38d81236e190ba20dfef", size = 626924, upload-time = "2026-08-10T14:27:27.048Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/84/eaa476d6bf3816828d0d70e80dcc36bf30a058233bd889e707e693f6e860/greenlet-3.5.5-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7278591501941bb2456af102bb9cd59aab48c6cfd6e2dd68fa1290bb0c49a42", size = 632726, upload-time = "2026-08-10T14:30:09.874Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/5d/398a1c71fa7a277deeb376c999979de6786f08fc2d5747a0b9d6e11738dd/greenlet-3.5.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2eabb980975cba5b93a95f6f69287d05fc05ac955bfd6a320a7c083eeb52c0b0", size = 623906, upload-time = "2026-08-10T13:40:50.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/f2/0cc2849ede68579291e9c59b3ab6ec1958f98681cca5b14d8fc75bf674a4/greenlet-3.5.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:4dfc7c4470354e7b09184d1a3a985761053a2fd694ddb5b5c80242afc2c8c90b", size = 434966, upload-time = "2026-08-10T14:30:03.729Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/1b/745450fc5ea9e0cb17d840d248f284db3363de736d362c7d2d883e3eadba/greenlet-3.5.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:03115c2e0a371999bf8ae616aa8d653f96641d4705c457aebaa187276e9f7537", size = 1581430, upload-time = "2026-08-10T14:15:06.853Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/29/d51b296e3191bb15d3d81ec375af1909e4466c0f395d744ed475801798a9/greenlet-3.5.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4441153ffba21b90d3ca89fe3d31f5c093ae6c0bf0cfdfc98f54cde22f95b62e", size = 1645684, upload-time = "2026-08-10T13:40:32.133Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/63/369f1a1625e64e9e31df3963c6044056e3fdfa3fa3fdba3c54ffefa6e987/greenlet-3.5.5-cp313-cp313-win_amd64.whl", hash = "sha256:95c5b1f4b3a193f8a0c2de4bfdcb48d119f7f1063941f1de1f2168051b3e52dd", size = 324075, upload-time = "2026-08-10T13:26:58.974Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/78/649cb5c09d4d81f6dd1444e75474a7206784743283a21d24171562ac4899/greenlet-3.5.5-cp313-cp313-win_arm64.whl", hash = "sha256:1af90aa4bc129883b340cdd6957a3bc74f60528a4993bbd1f53aaebe1d9981cc", size = 308260, upload-time = "2026-08-10T13:27:50.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/8c/080e881fa2be95ff1ddbd6994b2bab3b1a78df3b3fcab39306011764fcc7/greenlet-3.5.5-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:d4a389a852e392a6366058651a20fa5ba40d979865aa81bea2ccbdc44805070d", size = 295309, upload-time = "2026-08-10T13:26:03.032Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/cc/0ac614e6586c0e42d4cc281a5819150f4f43685744a4c5ff77139286409d/greenlet-3.5.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70b157cd319873e8b544ddc2de158f55bbd0a9b0218c8ce9332039801518e328", size = 661185, upload-time = "2026-08-10T14:14:37.867Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5e/b9/6808725354be8ad305dfe5172377664fc9642d4fc043be246b3314cf4482/greenlet-3.5.5-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8bdfd1424abcf26832961e766570cae79efdb9599d709088c9cb6ef82b194926", size = 673419, upload-time = "2026-08-10T14:27:28.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/52/f005d579acde46c3d1cc3cab1c9f3d5708c8a3006a4120e8cf5da801afe9/greenlet-3.5.5-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d98ef6f92e67c6dbf299dbfd8facc1b0d2d9cedf91e325e73b3d0373fe4309d8", size = 677863, upload-time = "2026-08-10T14:30:11.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/2e/40c509967da7f254680826a2fa0dd22138ec79946c70b97542d74cde8b43/greenlet-3.5.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:182de51c6b572a705f2fafaab2e783bcf7d2760940229dfe73086cbae037af3e", size = 670822, upload-time = "2026-08-10T13:40:51.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/8a/a75f8a2bdcef3c358a3147cdc9db3aa83755f0a038f766ab0bedb66f512c/greenlet-3.5.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:159df1942d88e8f784cbb38d6f18bdb365cd11319cfbb3e89623de2b97892d53", size = 480554, upload-time = "2026-08-10T14:30:05.171Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/22/c3c2eee4a8fe191d6d1d183086c56133d646024e3d70bfd414829f64560b/greenlet-3.5.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8fec3f165dfe332e490c3247c0f6c23b0bfc45f06496ad7f00ddb00e3d35e4dc", size = 1628469, upload-time = "2026-08-10T14:15:08.11Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/87/25babd09b94cb1f03e71db815fde463f0262e40cfbd953d58a8d77311351/greenlet-3.5.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c6ce25fee6cabc8bf22cb8b52e642cbb821be5b9aec8094d07ff03378141b8e9", size = 1691952, upload-time = "2026-08-10T13:40:33.502Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2e/3d/5cc9701117ea4dc0eb7bf1f4f9b7888a6e2e5277ddfae095805ace50f2b6/greenlet-3.5.5-cp314-cp314-win_amd64.whl", hash = "sha256:7dffc5c859fe6059974df1e37d7923d654a83e2ae18fdd616994270e001115e1", size = 327458, upload-time = "2026-08-10T13:27:02.868Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a7/6b/594fa2de7fae7629168a404a4305d7d7e31a5742c50a801b1839543cb93d/greenlet-3.5.5-cp314-cp314-win_arm64.whl", hash = "sha256:5e2afcfc4d4305dd715809b03da5cbe437c8984f61d8917751eb5fe4aefa3e07", size = 311146, upload-time = "2026-08-10T13:27:25.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/e0/50cd600b469e5734c72709b6b1838b6bc63f307b573c772c3132d6ecfe92/greenlet-3.5.5-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:0e5a7de979d764aea1f5b6e95cf92b5b37741b9823702041f34b126e7f690277", size = 305471, upload-time = "2026-08-10T13:26:20.568Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/a3/77acd66dfc6387b5219b2080806c0cabb73c10eb1bb44b413c40a62015ba/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fef01bd457f11fc158b130ca0027a3c365693280e8e231b65bdaf57999f39f5b", size = 672470, upload-time = "2026-08-10T14:14:39.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/71/0d178142dca3ec19f46fb2212ae73d30ad53b9d548dc64804086033a7089/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5173a72310725a74afc82c164f0e52cb8ad0de62f2bb623f24f6c0cc07d80272", size = 679973, upload-time = "2026-08-10T14:27:30.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/ac/0d7887aa4bbfc9eba075cc428244dfc96f623478454d5ec81180d0d6bd5a/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5e9ec2e7c98e895fcea0c5cc57b2606cf86ece6d0a56578f3eb225e2af4f0387", size = 681587, upload-time = "2026-08-10T14:30:13.519Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6e/31/46eb8567302eaf787abf88d09df014e14ae3baf460af1b8b0efdbd3efcd5/greenlet-3.5.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44f08341873200ba8a60a8bc14ace3d91f1754f7fa7bc66157714a8cd420a476", size = 676634, upload-time = "2026-08-10T13:40:53.004Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/18/8d58ba1c429b0383e3219a3d0e0bba241d0444d8ed05b73349953c7d7c7b/greenlet-3.5.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:102817506f6090b5176c746a82603341a549b40e5c3d5b72a4c672228a918c41", size = 510175, upload-time = "2026-08-10T14:30:07.047Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/e9/b88bbf5b29970cb84172dc2c32aa3e5e579ceb94c808e81c826454138850/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d246c0db9a2513cd45f019ba178ea4d4d4705bd210ee465e2c15d76a1ab13874", size = 1637320, upload-time = "2026-08-10T14:15:09.317Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/8c/7631ed29cc6f0392f11830076e172ce4885e70b0bc2c1bce1731176d4b4e/greenlet-3.5.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:72507285b5caa1d17904a3f7c322ca780823a54170a0e04ec3f37bcc60d4db71", size = 1697412, upload-time = "2026-08-10T13:40:34.924Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/0f/f7dd935f9c4cb1be49098770587f54d8a78518e55c89bce86c4fb4109057/greenlet-3.5.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7805655781fb8f28a55d05fe57ed61f5f10f1892fb587673e3bb5264f28041f0", size = 331514, upload-time = "2026-08-10T13:29:20.611Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/e5/681b01f8fbc1b55232822f99e8f8afeb78a55a7c76a7bf9dbdc7ccb03a6d/greenlet-3.5.5-cp315-cp315-macosx_11_0_universal2.whl", hash = "sha256:c0db80fcd5b8aece93f66c64f78a786bbb6b96c5fe63ef5a5a4581ecf8bab206", size = 295975, upload-time = "2026-08-10T13:28:45.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/f2/69b488cd9e7267bf4b0fe8cdebf25d8d6df680d21bdf41150d23e23d6652/greenlet-3.5.5-cp315-cp315-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b241c32f912ada659808d68e308c568baf577eebf757d15471472de0c18cfad", size = 666823, upload-time = "2026-08-10T14:14:40.222Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/84/d4/d5bc2fdebbdda0c94555925ba79948b8395d75a7f6a36cc85dce5bab9f11/greenlet-3.5.5-cp315-cp315-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ef6a08349401d8eaf3cb12688ac8557de95788556b8631ef17555a4a173022c0", size = 677613, upload-time = "2026-08-10T14:27:31.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/53/4e13642efc4d7ad6554ecb2242a5be42666b2e1a067323e88dfc0124a04b/greenlet-3.5.5-cp315-cp315-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:37faa97daccb6d9f4c2141ce3118d023c3c5506864a7d8bdf726f665018c1f76", size = 681436, upload-time = "2026-08-10T14:30:14.839Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/93/542d8a3a90f3b35c6ad8bf7e56a03010287f2cafa289a5b7985b5207db39/greenlet-3.5.5-cp315-cp315-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f2e3d061b8e13aec2f0441689b3c71b244a20e5d274a52cb0f7e31bd1d139552", size = 675930, upload-time = "2026-08-10T13:40:54.205Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/32/188447c9a468d6977d2989397226b0c6b65ab6f4cf943f931643328512fc/greenlet-3.5.5-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:b18007dc2473a7942fd157366b55f01da6fed7ce85318591005b419e0a439474", size = 487404, upload-time = "2026-08-10T14:30:08.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/b5/89c9f2e8460d71101037d47a1feed11928615a5edd42370be290e0657eeb/greenlet-3.5.5-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:9ab5f5b93655e77fe0d6c2dfd22b5eac751bb1f876d8ec21761b7c1fb9266007", size = 1633878, upload-time = "2026-08-10T14:15:10.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/60/297de93f3b02ac78a5e04d32bb8bbe3080f4a73d8ed95016561463b70618/greenlet-3.5.5-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f0e5a21bd4452a88cf032fc43c4a5b307ab1380eacb63b5988f9c0317885e773", size = 1696597, upload-time = "2026-08-10T13:40:36.252Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/25/54c6eaff4f337fb670215e89eb2d00d9499487b658e709d4b477be4a342e/greenlet-3.5.5-cp315-cp315-win_amd64.whl", hash = "sha256:469dbb0a78625642f4a626cfd0c6e8bccc0385b5e49189b6308bbe849ec88a8e", size = 327700, upload-time = "2026-08-10T13:28:06.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/67/857e88a36301caa0e029870132c2478bd55d896630321432afab03a3115f/greenlet-3.5.5-cp315-cp315-win_arm64.whl", hash = "sha256:2d57406c3efd32d7a81e17a674314e8bd00792cdab49ea3228a49aa1bfb2e769", size = 311750, upload-time = "2026-08-10T13:34:08.815Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/e2/3144c0a116067ac1e30457b0139a94d60d1d36a86e015de68e9ac87cb3bc/greenlet-3.5.5-cp315-cp315t-macosx_11_0_universal2.whl", hash = "sha256:68184dfcf50ccaa8e864770fe0633a7e27250ea9329f8192ef47ee9ecfd78e1c", size = 306387, upload-time = "2026-08-10T13:27:00.897Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/a1/cb4223a7e9b9f43b8807e8eb212358bfe2dfaa174a9ea2889eb1714dcba2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ec0dc0e59dc9c61af5c47348365ccbbd7addfafe0a93b00336ff3da2907bdc6", size = 676472, upload-time = "2026-08-10T14:14:41.417Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/cd/a154b4498e5d8f12ada291cfb3b8d596eadde2177f5bf09a9be699d2a446/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e604f58e35833fc46ef20302bcb314dddbfd3fcf33a4f936216d51dd678d63ae", size = 684238, upload-time = "2026-08-10T14:27:32.946Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/f4/e450a68a152f819491d8c7df6a8254e761d87e6a78759268961f8c5bd4dd/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2888a3a38bc5ee5bb6c438372197152e815837e4fab7ed7a1f86ef18ffd58ad1", size = 686022, upload-time = "2026-08-10T14:30:15.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/bb/b0031d260c2968a3c87deebc51d80c64e499377f993aafe06ee3b7488cc2/greenlet-3.5.5-cp315-cp315t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40239b5384f96da3963585cc6d7eaa9b56f8ae67e8d92cc82dd9e202fc847de3", size = 681246, upload-time = "2026-08-10T13:40:55.402Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/23/17e63d6bf3b9c9b9dbea981b7f643a71f79603bdfb4f1c3a9cf353e22aed/greenlet-3.5.5-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:1e8d9391fe77f15649589a907cef972dbbd6352ef7ff7dc0492f658c0c26495f", size = 516951, upload-time = "2026-08-10T14:30:10.907Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/07/da554b71ab88e649da146e1065d86a48a5c5d92e50ab74ef41b504aa7f56/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:a1eaccf5c3a1d3e46dead602c72e6836731e8e245c9de6a27764567b6b62d4c0", size = 1642735, upload-time = "2026-08-10T14:15:11.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/76/26a3782a051677668af9d92beaa47cd87ba9dd5072f762961144a03dd4c6/greenlet-3.5.5-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:19e4e026fe20691f333b8eb1a3bc9625eceba8c3f9d62ec5a6f8581afbc6b5a5", size = 1700925, upload-time = "2026-08-10T13:40:37.656Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/28/d9/fe7baf4190c2ae71f267efb9de21b3172bb35bc0ed1ef53dd6027d658e33/greenlet-3.5.5-cp315-cp315t-win_amd64.whl", hash = "sha256:712aee154f648bde84634654bb38bb78c69ac640c37a45c9effed800735049d8", size = 331829, upload-time = "2026-08-10T13:26:48.851Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/af/419a4e383bd600858a9b67e9b280a60fdc383ee3f2fe5b6c0c1ef04e74d1/greenlet-3.5.5-cp315-cp315t-win_arm64.whl", hash = "sha256:7f049911ee81a16a03c33d5450d8d5867d27f596ca5fb201b86f4524e874468b", size = 315093, upload-time = "2026-08-10T13:29:34.949Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.2.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version < '3.11'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version == '3.11.*' and sys_platform == 'win32'",
|
||||||
|
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/49/ec46835a70be8fa6446c495126ac84fdb28cb2558e1620ffb87a10c8b64c/numpy-2.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0280e0356c0829a18d9de1cb7eee50ec22ca639878d7240307ca0943d73cd2c4", size = 16969194, upload-time = "2026-05-18T23:33:13.503Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/0d/f5957185c0ee2f3e12f78715aa9e3b353fd83633316c8532b38faa37e3f6/numpy-2.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:110f8b71aacb688ec69062bb7f6938a0f8acb01b7c1c4beb453c65b6d234584d", size = 14964111, upload-time = "2026-05-18T23:33:17.795Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/40/40a40ee0ddf7ceb782c49af278894b686e586d65d8c1889c8b5da01a3d7d/numpy-2.4.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4cfe66903cc32a9921a6733d96b19bb6abf310397581bbad89c228f5abaf0ee8", size = 5469159, upload-time = "2026-05-18T23:33:20.654Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/13/f9a8046535cb21deae82f8d03de9617e08882d274fad2539630761888228/numpy-2.4.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:8155154c7c691289fe18f510b5d4657c68c67989f293f0535a91360392ff6538", size = 6798936, upload-time = "2026-05-18T23:33:22.987Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/a8/6fa8c1a345a8c85dbb21932c447bee07c30a2c2a3f31e369c0a84b300147/numpy-2.4.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ab0a9c4ffb1a6d95ef519fe4247dba8eb6b18ad93999f76b7f657039acabd47", size = 15966692, upload-time = "2026-05-18T23:33:26.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/03/74fe2a4cb3817d94d86402f2506554130a2f01414e299b5a843e5a8a957f/numpy-2.4.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89cd468399cfd2504718f0ba50e410dca55a170b61a02ad92bb18c8a65186e93", size = 16918164, upload-time = "2026-05-18T23:33:29.955Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/80/3615be3313f7e7696609bc194b9f0101da809df79e859bdb84e0cd043f46/numpy-2.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c2d37ab77531417474168eb79d6d80b14f821a966818505d03013d0833edb7a8", size = 17322877, upload-time = "2026-05-18T23:33:34.724Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/ac/a691e0fe2675e370d0e08ff905adc49a1c8830e8cae03efe4477e92cd55d/numpy-2.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f407cb6b8e9d6d8c626bc73c945db1706035af8fd632295547bf1c9e46d092d6", size = 18651487, upload-time = "2026-05-18T23:33:38.217Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/a7/9bc1cd626d7bf6869bfedf27b91b6ab5dd607758bf8e959d6fa80c6a59cb/numpy-2.4.6-cp311-cp311-win32.whl", hash = "sha256:ddea102b48f9e339f3948bf22040944184627a30fdf7f858667673b9c5f033c8", size = 6233945, upload-time = "2026-05-18T23:33:41.331Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/31/7fc6239c12bce7e931463251cca4426c465e1876ba3cc785402ef4dd8f4e/numpy-2.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:1e254a00cdf42b1e4d5b3d68d33af63268d41340d8885df2ab6470f2e1500147", size = 12608406, upload-time = "2026-05-18T23:33:44.131Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/83/140f85a466595a16382996a1bf06b2b54bcd597488921b0c9daaeeda72af/numpy-2.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:ed9749eef4cbd126da3dc1d6bcb3a57f5eb7ac6a6484146bdbf743f552dfc577", size = 10479528, upload-time = "2026-05-18T23:33:50.725Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/2a/3d7b5ac8aac24feaf9ad7ed58f45b0bbc06d37e4338ae84c9f2298b570f9/numpy-2.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:001fbb8e08d942dd57599e781f2472269ee7f2755fae407b4f67b2f0b17da3f1", size = 16689119, upload-time = "2026-05-18T23:33:54.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/12/92c4c131527599e8288d6918e888d88726f84d805d784b771f32408aeaef/numpy-2.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ebfb099f8dcf083deef3ac1ca4c1503f387cf76296fcb3816b66f5ecb5f54fdb", size = 14699246, upload-time = "2026-05-18T23:33:57.621Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/fe/c0a6b7b2ca128a8fb228575147073b660656734b8ebe4d76c8fd748dcc79/numpy-2.4.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:3213d622a0283a39a93d188f3cf72b26862df52fbb4ca3697f51705016523d41", size = 5204410, upload-time = "2026-05-18T23:34:00.302Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/d4/9770d14ba719432bb90a421bfd443872ed0f70f7264b64bec12ea363d5fd/numpy-2.4.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:357cc07a6d7b0b182ff02249616a03742827ebb1277546b5c7cd7f7620a45698", size = 6551240, upload-time = "2026-05-18T23:34:02.852Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/c6/50a46a6205feba2343f1d6d17438107c5dc491ed1c736e6ea68689fd906b/numpy-2.4.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f9fb9157b4ce2971008323afe46053787b526ef624fea915b261468a8421a0f", size = 15671012, upload-time = "2026-05-18T23:34:05.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/60/14115e6364fa676c5397c2ad3004e527e9aa487abf5d0706ec81bbd08529/numpy-2.4.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:90f9849678c75fe7afa2d348ac842c168b0a4d3d61919687216dfc547976d853", size = 16645538, upload-time = "2026-05-18T23:34:09.265Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/c5/693cbe59e57db94d2231fa519ca3978dc9e19da5a8f088588f5c6e947ff2/numpy-2.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c1a2af6c6ef86344a6b0db6b97834208bf598db514f2b155042439b62605601a", size = 17020706, upload-time = "2026-05-18T23:34:13.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/fc/85b7c4eff9b4966ade25c2273cf7e7012e92366c032058653934b37de044/numpy-2.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e5805d5a22fd19c8ccff10a9561f9df94436b0545619ea579db2d3c35294bce2", size = 18368541, upload-time = "2026-05-18T23:34:17.024Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/81/e1b27545deedce7f4a0b348618c6b62d74e36a4dc9ccd42f3eb2f85eee32/numpy-2.4.6-cp312-cp312-win32.whl", hash = "sha256:e3eeb0aabd6bd5ce64faae67e9935203a6991b4bc2a485a767fbafb2c5125f45", size = 5962825, upload-time = "2026-05-18T23:34:20.3Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ab/ca/feab00bd44aa5fe1ad2c18f08b4d3bb92e26484b0b1d1443897809ed528c/numpy-2.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:d8e8286dd7cea7895157318d1b91cdacac64c479f3cbc8dce548331728484751", size = 12321687, upload-time = "2026-05-18T23:34:23.095Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/cf/5a6d34850a39d1093558564f77ee8e8e0bee5061151b8f05a55711001ec7/numpy-2.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:4081eb135ac24158bd51cdfbef16f1c64df7063b1143f24731387137c092bec8", size = 10221482, upload-time = "2026-05-18T23:34:25.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/12/b422cc84439adc0d00de605bf4a308890ae5c26f2c71fbd73e5d08fbb0dd/numpy-2.4.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:55cced7c52e981362f708ad635198e97a752dfba412cc03c23bbf3bd8d5cd662", size = 16847511, upload-time = "2026-05-18T23:36:50.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/53/f481bef68011740f8849418d82db07230e825013f31f4eef5ba5b805316a/numpy-2.4.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d6da64deb6b8ed903e7560180a92f2d804ee1ba5eeb849ac2748b8c1aba1f6d7", size = 14889064, upload-time = "2026-05-18T23:36:53.879Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/57/42ed575c10ced8af951d426bc4e1f8aff16fd851db33f067036215a7f860/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:68a5124b13fa6cc2086764a20005d30bc0548146f7f5322f02fce212ca14317f", size = 5394157, upload-time = "2026-05-18T23:36:57.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/ef/f66cc724fcc36c1e364c67f51ae9146090b8b584f27d58b97fdae3edd737/numpy-2.4.6-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:948424b06129ce883307e8cff868c31396d8dc7630a59c61d70d98dbe70f222c", size = 6708728, upload-time = "2026-05-18T23:36:59.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/9c/c531f2293b91265d8b48e9b329f54fdd7ffae73cb4134ea10cca4237e9cc/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbbdb29840ca3d91ee0fece42fc29278886d908280bfec0a5846c6f901a3eb0", size = 15798374, upload-time = "2026-05-18T23:37:02.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/b0/413077f6b1153ed3cba361401c6783bbad6114804a000cc22eb71c13e190/numpy-2.4.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ad03c0965fb3c692200e74d458ca28c1dbb4ce96f9a479a8aa041ad5fabca02", size = 16747286, upload-time = "2026-05-18T23:37:06.327Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/ce/e5ec180bc41812edcd8daeb8639d205622c0e8c02259d8ab25a0201b3c2a/numpy-2.4.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:2803abfebfc990042cd494d8ce2d5f82e9d847af6d35ec486923aa19dbad5e73", size = 12504263, upload-time = "2026-05-18T23:37:09.715Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "numpy"
|
||||||
|
version = "2.5.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||||
|
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
|
||||||
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9a/80/db0b4559e57ec36362bedbb05530a87fafbcb6067708c946967a41d449e7/numpy-2.5.2.tar.gz", hash = "sha256:d482d171c406ae88c5b19cad3b6a1c4c5209f886ab74bc44c2c865c23f52d860", size = 20773161, upload-time = "2026-08-09T13:48:27.962Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/72/dccb0aaf40972777283303919f613964227266d0c13adebb79ac124f1c3e/numpy-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14e373cfc6387177e8409dac3c7159be8eb05cd77096cd7c950268b86f62831c", size = 16891693, upload-time = "2026-08-09T13:44:51.702Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/2e/b5aee50a1f74ac815cf8331812cb8251e29024025de462e0c047641c614c/numpy-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbd96c833ecc8cc069ce518078fc8c60cb9cbfb0fea5b7a803ad65035596d03", size = 11903109, upload-time = "2026-08-09T13:44:55.501Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/f4/29e78102a80601cf034d4e9767022cffeca2c3b4c926e1754572ca95593d/numpy-2.5.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:6e8172ddfcf5cf74b811d372b570b83c60bd2de87a6fbfbebdadb4a9bd9c6cbb", size = 5350202, upload-time = "2026-08-09T13:44:58.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/4b/dcd3b7eadaf4035d2c7a4289d232523a6964f602598ef7674e4bd7291f93/numpy-2.5.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:65f188481f1669e26f62b701e8205d19e460fa4a9b52a1414ba382330e4a3414", size = 6687736, upload-time = "2026-08-09T13:45:00.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/21/4947e0e9d6c9fc2e2ff15b8949049ee44f63adb9cacc729ab8793f97e712/numpy-2.5.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8ee9c4eeb8454b3660a8b53493563c3e121c2fc94fbd72b848ef814ed7b676a9", size = 15612696, upload-time = "2026-08-09T13:45:04.151Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/5f/62d28cf019460c7f1394105b4d49d9911a9c444cb77ab0bd95a204c5a6de/numpy-2.5.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3cdec01fa790a186d430433fdd4d4ffb70eed6f0eeb4bf05c8dbe2dce0a9bcb8", size = 16722264, upload-time = "2026-08-09T13:45:07.714Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/25/3f0be4c1b9fdf5dd5e708a6806978564d7c46a055c000496309ff2a2f8af/numpy-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7999d4ddb0c4025018373fd787510d46e04c769467af22869707b3c1cfd459ab", size = 16974396, upload-time = "2026-08-09T13:45:11.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/72/6262cbdeeb45da9d971e40715f579d791603ba8ec0b5e2db1ac55454421d/numpy-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c1f017dc0875c9209d219f97feceb7d54c2661bb243deb4114478e1295808af7", size = 18476044, upload-time = "2026-08-09T13:45:14.869Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/33/29208b8b075bde62d26a81d14b358c42b0f69b6cabd98d4ff97f37f22b05/numpy-2.5.2-cp312-cp312-win32.whl", hash = "sha256:d6a48072864e3324e194a8fbb3c657bcc5b5c869dbc64c9537b1d5c862572c0a", size = 6072817, upload-time = "2026-08-09T13:45:17.867Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7f/b9/87fea2769fe1c47c1b5b01d8310772c9d1a85d485de7cf386ef7a3332b02/numpy-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:28ac63476ec7651484215ee7fa15a1f78b57c14621f01e392afe17b9a1390ce4", size = 12464674, upload-time = "2026-08-09T13:45:20.734Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/52/032b97e00461ab0809bbe4c588b035620e5a14b8cdee47ecddefc7b17d33/numpy-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:27650bb0e7140fa3d37b9923b4803645e0b125d190f326eecfd3f4dad8e8ade1", size = 10397131, upload-time = "2026-08-09T13:45:23.73Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f5/d2/6b24738a0ef4557d189b150046cd07823c50e4273e8aebd651222e24306f/numpy-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8e4cb9a754c8a0c62eaa88273a5fba3391f4a610d1dee893c0755da31c083f15", size = 16886595, upload-time = "2026-08-09T13:45:27.323Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/60/f2d208d366f263f39c6e69ed309290717aab41078b6d04c9be2a84fa2a07/numpy-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:52c808f96484f5571a5cc863775ce50247c17dfb3b0361f8ed6b4b0456f80080", size = 11896845, upload-time = "2026-08-09T13:45:31.638Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/79/81e0bf24f4d020a2b1d5cd297a9f60c3f24eeb116f9bba5870443f7b6a4a/numpy-2.5.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:29d81e97f668489cba8ebfd796b9bdd453525d35dd9e162e2daec94bf3fc7740", size = 5343880, upload-time = "2026-08-09T13:45:34.373Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/cc/e3141cf06d1a8a2c7e107543fe1269c1d1af760d4d683c0794a4ee1127c2/numpy-2.5.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afb3f0632d6b2e3ba04dbce8d1e48d321b369138b73830b5ca371a0e8d479d56", size = 6682264, upload-time = "2026-08-09T13:45:36.7Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/f1/2a64a307d92c5d98f5255a4014eb43bb6103ee477087b61ecae44a3aa9b9/numpy-2.5.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0aadf13b60048d501e05fa699efaf7734e2494f3498a4c2a5521d822640324f3", size = 15609566, upload-time = "2026-08-09T13:45:39.518Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/44/59a1eb68e773c4098d107ef34a0dbdeca501d72ffcfbff9a7707343921ce/numpy-2.5.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29b86ff8a6cc556b47ec6b64b194815cc80e6bf5eedcc6cddfd65318cb0b4eee", size = 16709995, upload-time = "2026-08-09T13:45:43.661Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/4c/3e54d4ddbc359a1295f8b633e8106bcd4d7d4a206e82df051bdfb3058755/numpy-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6950c4b7dd562453090548ba7f5da7e59f57f85663f15d5dcc60e249192f7e59", size = 16972511, upload-time = "2026-08-09T13:45:47.094Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/9f/02e371638ebf19b66d46231e4be52999e87f32d1961b113bc45656608b22/numpy-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b9727f472d2f3888053b8a75ab0cb94745a9de224bb5846dbadc0092101bc71d", size = 18465609, upload-time = "2026-08-09T13:45:50.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/eb/ae/ad6645abc7a3510fe48e8ea1ab4598166f500057ef4ebf38bfad4f1577de/numpy-2.5.2-cp313-cp313-win32.whl", hash = "sha256:4f9744f9fbdcea0bc552e8f19e1f141f811a3f9bc2be2cc6e86d982cab23e3f4", size = 6070204, upload-time = "2026-08-09T13:45:54.111Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/20/f3489f86d81ea460b2bcdceaed094142ca6579f6be0ec527b781d39afe68/numpy-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:85aaccb24182c25df891ad0ec333585967e115269d5f1b17f2c9ae005bc96657", size = 12460532, upload-time = "2026-08-09T13:45:57.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/21/35b31dde1b283b79de828b80f876afd8c94e28fe1e9c375f89e261cc4c0d/numpy-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:bd68ece1553d2023c09a4226d9e41c586ad2d20594d1a456186c33513d2cb3f2", size = 10396725, upload-time = "2026-08-09T13:46:00.478Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/f8/c3b222bf075b50afd8e949a07a15c4b312a4a84bd8102a332bcd953cbbb4/numpy-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:d787cf769c3baeb5f6235e778edb52c08dfa923789b5958f28e6450f96107cb1", size = 16885180, upload-time = "2026-08-09T13:46:03.939Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/17/e1/2c1d4b1987795a92b5bbf7c24fe249ab96aa2573ab0d7604802c189d7b86/numpy-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:24b9dc2e3d84aa58523798805194e23e736f3f6ce2d1a5b92583ae734e6dbda8", size = 11907878, upload-time = "2026-08-09T13:46:07.045Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b9/ee/d08226fc858044355983a6e5b94f08ff6f3969e0a2b160a4a89f0ddb3445/numpy-2.5.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:9e9413326d726c2545bfa65d2c0876871e8d8386e77f992c1d426e180bbd4323", size = 5354922, upload-time = "2026-08-09T13:46:10.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/f0/6d3d933056440ebbc5e6bad92065fc6c26a48a84a36b1208580e94eea76c/numpy-2.5.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:60e902ac295855348a5ca2ea4c89108989a9f5fddfad3dfc0a8f36b10358567e", size = 6679168, upload-time = "2026-08-09T13:46:12.275Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/3b/ecd49dd90033cceb2704d88ca905d4d7d89b0e8c739608754ffd325fa820/numpy-2.5.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:50e500dc868e9313530ce12ba470fe50ff3afe3d62993ed6eff652dacd555b65", size = 15624501, upload-time = "2026-08-09T13:46:15.322Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/99/461bd36dbdfac6c1c53efa370bd55a83227542d0d118f1677dbf1a3dacd5/numpy-2.5.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318b9a4c845dbea06708a29c84ee429cc3065048db34cdb799047643492050ee", size = 16713701, upload-time = "2026-08-09T13:46:18.949Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/9c/2b251df9e8a5d647b62b0cbc1b90a91850c1cf4859ecb532fd0b4eacff6c/numpy-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:34c319e2963be042673fb46570501b2f06c41924e17e3563d58646b4380dfb68", size = 16986065, upload-time = "2026-08-09T13:46:23.006Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8f/25/20de43f53ff1390534a124475055a19f01fe10c920a0fd11b8e18d6d6052/numpy-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f06571a052127dc1b4e8b83029b4d1b20daa2b64a31cdd181fc6bc774e9000eb", size = 18470031, upload-time = "2026-08-09T13:46:27.102Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/5e/0c577ca308d6da5eb79b546ba10bbe5b60148192194e2da060913b1de4f1/numpy-2.5.2-cp314-cp314-win32.whl", hash = "sha256:2cc779226e476d1e1f08c74068c419e60f41a9e0e069c92f6671d31d5c985e98", size = 6121028, upload-time = "2026-08-09T13:46:30.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/5c/7bcbd5b11f94199073320410cddcbb80cee62415bfeb540874b265c2d922/numpy-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:7587f53dfbd5edc0f7b87c6217b4c6d2d1f2ef9c3da70bc1315e7db5f8d7ec9d", size = 12597627, upload-time = "2026-08-09T13:46:32.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/bc/4d0b06fba0da90ccc75af62823cb9dcedb6c9ea0cffa058cb2c9ee773a77/numpy-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:3e4c367352d3747784248a227fbec218e193b56f7e6692e3b64fc805478ecfdf", size = 10680414, upload-time = "2026-08-09T13:46:36.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/17/f429aac9dc08833a0d0f188eba38c532a751b1a1f2ca6018a37b455cb321/numpy-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b879fb674276e331513fb136b78dbc6bd3c848309e0d841cfd63be3896c4cfc1", size = 12026967, upload-time = "2026-08-09T13:46:39.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/9f/d0849de96a2a4ceaa16662f18ee13eaa9c0aa418269fdc8c4857c56b11da/numpy-2.5.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:fd0d703772bba096843785bd38371e31bb4a0c1151497ad5739d182114a73f7f", size = 5473874, upload-time = "2026-08-09T13:46:42.075Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/3c/8df216d4a4a5422a3de045301cf7df8ea47286d76f5cb7160b0128ac26b7/numpy-2.5.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:3a2f061cebd9e3d23bdcfaaded5e2293a4c6a5b60fa42df85d410a725ce621bf", size = 6789276, upload-time = "2026-08-09T13:46:44.387Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e6/3a/20d7e9891c4ddfadd6ff8d95bf4b29f353d8e1770553de2099880551dfb9/numpy-2.5.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6df895598c0edcb41030126c89e0f353b07d93238116143b7405e937359736c4", size = 15659154, upload-time = "2026-08-09T13:46:47.538Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/d6/f3aa3d2688bf501b858835c6bd087ae9b51a56ae6fca8e2b0990abd177af/numpy-2.5.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1ab3d4a901f844ea836c3e80bf463c6a27d7f3c14e8e292fcf28d348b25b9bce", size = 16748909, upload-time = "2026-08-09T13:46:51.442Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7d/8f/1c5cae8d2baf86ab802ae97a00be55bc7e21ebc11b12bbc33376c5f05342/numpy-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:cebc2d6dbb605a7703d59751dea4bd6b0ab127a5a4338a6f432df1936fef8b26", size = 17027685, upload-time = "2026-08-09T13:46:55.095Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/27/71d3467404aedc1c24ce79610f91b52b0b0f466c43a701aa56fc75c145ab/numpy-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:eaca7ff36f0f52e2111ec71f169d8fd3e889e7ddc0d2592e0d703fd8d3ce8fac", size = 18501181, upload-time = "2026-08-09T13:46:59.09Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/2f/42921d27c40aea7e077f4a423ae509fd9220b028cd787bafefd8ab2b3a5f/numpy-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:ddf47472af2e4280d79bac82304f5e80150211f1b9e614b760061d5fdfbb6eba", size = 6271085, upload-time = "2026-08-09T13:47:01.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/75/e6/bad5f5d56de9b1971bac959963dda276d35c40f1854475005434bbe08692/numpy-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:44ef9675d908e65f9953063837c3277730f3f4437615a4cdab67b366cabaf884", size = 12787971, upload-time = "2026-08-09T13:47:04.963Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/05/f608795cb34391acd67e38d94a3c36abd8d8576293a3a80727d7595c372c/numpy-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:eaa088384c46f519dacb93b7ec483a6d6b19a4a2085ae4f25ab9b1c43d387d1e", size = 10750306, upload-time = "2026-08-09T13:47:07.976Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/c6/28de0191c5f82b7d42a0a51390ba98587048aa93a39fafb05bdbe6e8d00c/numpy-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:078f9b027b478c9379b9677babbf0f8b8f1ecfada27636d7b9a93990c638739f", size = 16885274, upload-time = "2026-08-09T13:47:11.439Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/d1/973ca116000d244897e468ea1aff30b589e5022e3c8744b71706fe33bd57/numpy-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:50a68f4bacd8a2b33d8da3d2269d0d78500f86ea582e4786dc10f5ef2c2c6842", size = 11907846, upload-time = "2026-08-09T13:47:15.128Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/d9/8c4b3937ef204cb2fd88d389ccd0f265a2ffb11f35a01d2064cf46714bd6/numpy-2.5.2-cp315-cp315-macosx_14_0_arm64.whl", hash = "sha256:e79aba74ffaf5f78a050d777c184cddf8fdffabab38acf5f3ef1fecbc17895d6", size = 5354892, upload-time = "2026-08-09T13:47:18.07Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/9b/b6ee65ea2999fdb7023935e108e6fb776ee4082aa15f159acfa857e578c8/numpy-2.5.2-cp315-cp315-macosx_14_0_x86_64.whl", hash = "sha256:9a0731745a72a184490a582fb4af2533512bd071ace67785b5fdffc0ae58dce8", size = 6679309, upload-time = "2026-08-09T13:47:20.456Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/f3/acb18d8b137a393c8e7803a8c994c9e64bde3930692a69d826993113a159/numpy-2.5.2-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4ec954036759bcee3aa484f8603bd9c14f3e776293b85578b8734c2d72777c69", size = 15625850, upload-time = "2026-08-09T13:47:24.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/bf/a8e9bb0db815a0e265b5744ebedd3af0bd5faad8604e5b50a1cd012f3c91/numpy-2.5.2-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc649493697006bc90614a5f0bbc8cb3cb1866715c474e473694968d7e6b99ab", size = 16713664, upload-time = "2026-08-09T13:47:27.965Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/c3/6e913736b3dd6582344af32418b5fb9dab34282e8a8174ae1d54ceb0fc13/numpy-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:cf7de32f486e4ac9e2d93b810f9e9ac72a728dd46a32a0bb403222f27f653514", size = 16986749, upload-time = "2026-08-09T13:47:31.541Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/09/7d3b23eff5c7428ef6c01e6f7052bb60d504c4d33e317b36b8959c24ad97/numpy-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2ffa7bacab3e2ee1b19ed31766bb60bb380b68c23f051e199c5cc598afd68710", size = 18470495, upload-time = "2026-08-09T13:47:35.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/a4/68a321d825374f6eb677ffe8ef8c6b9a328304e6fd2e39d9530822776607/numpy-2.5.2-cp315-cp315-win32.whl", hash = "sha256:6b588cc8f902d6bff201c19fd00c43ab8545671e3554d014e12e14139e5e8617", size = 6120696, upload-time = "2026-08-09T13:47:38.561Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c8/23/deafbb1700f79fae9cd1e91220f133d124cc267de1b584da3fbf6db2f6cd/numpy-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:07d4e89f3a9ab0a9ba24264ccdb642b3dd951b2281e8883a5481a4aa79cc31a7", size = 12597324, upload-time = "2026-08-09T13:47:41.401Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/cd/3272ba105e3bbbdaeb11357eda31e7a6825ffe159e8171665660299a948f/numpy-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:a610dc7e3c52edd39c2bc2375ff9c3fd59cb3ad00e4472d36f83bc1457145788", size = 10680466, upload-time = "2026-08-09T13:47:44.873Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/0e/58370637b1bb70a5c9ce2b43f4b521ccb224e36ccb76a6596b17ae4b447c/numpy-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:40f4d451aed46a8046a1aae41c4e55fb3612273df9c502480135e1501576a34b", size = 16993947, upload-time = "2026-08-09T13:47:48.97Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/93/2abcb807712b289d6d60fe4cf30532f98974a8396d885650f3ba5a13026e/numpy-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:c081cbe16ba1ab53078e5ff29013621e33c509eedab055775d956427712c236e", size = 12025331, upload-time = "2026-08-09T13:47:52.646Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/3a/2898e003a5fbaf87e76c039b4ee1f5eb390471b4ffe74887c1f34c4e791e/numpy-2.5.2-cp315-cp315t-macosx_14_0_arm64.whl", hash = "sha256:0090ccdd57ec2703e9b49d0bf554767370581c1dd0a6b2bb2b2d9def317d042a", size = 5472336, upload-time = "2026-08-09T13:47:55.403Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/a5/23f69d07c544597b29758b31b55c27dc9d541012a2c1496189fef702aec2/numpy-2.5.2-cp315-cp315t-macosx_14_0_x86_64.whl", hash = "sha256:6a9bb119fb8dd21ba30b3f0e555b7e2b081bd9883af21ec9c1c633d161cda3a8", size = 6788387, upload-time = "2026-08-09T13:47:58.192Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/ea/c0dbdbcf22f43782510a3e492dd3da73c6112b69cac8929d16d127536fc4/numpy-2.5.2-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a839318485284a6fb31be4f8f2c91c8f2cb22f4543c4a8903f12b0671ffe07cc", size = 15667096, upload-time = "2026-08-09T13:48:01.562Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/5e/29c73c31748cdb0f7566642125ba17fd5b56780cddf891b085dab27e4466/numpy-2.5.2-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba0a474801b8dc67b66bf465548abc90e82b44d2611b5770f33008dcabffe8ec", size = 16751730, upload-time = "2026-08-09T13:48:05.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/95/02501e8454796bb58dadf7a99d3181e0b464bf264e1003039572f9779fac/numpy-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:0a4035ae1129ff8777f08bfbd44f1e5d8e9c049ce0c2dd78fc0d92c13e7251c0", size = 17038686, upload-time = "2026-08-09T13:48:09.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/b5/53a681d91b5c82687067d8ea5035e02d917b5509d6f334cb06484a954714/numpy-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:77843ca236b777e67f8d6b3660ea116e499612703a0ecd7093f316201eb9d8e2", size = 18507727, upload-time = "2026-08-09T13:48:13.744Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/06/6e11443f7b64ee376c860506091103bf68f92d2cab9e8d96d4501babf07c/numpy-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:7354826bc6f8f69402e9b7fe28d15fcd34feebd74f856f111585c5b0c9fb0251", size = 6269775, upload-time = "2026-08-09T13:48:17.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/18/195d6b86cd72dbbc501edfa778005fa6b87afd34c153e46028cd3a0938f4/numpy-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:e5651f3f87add730ee6608d915009e19c911fba0cb000c7e3ea994b7d768eb12", size = 12782559, upload-time = "2026-08-09T13:48:21.023Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b4/07/458c344f0f0c178f4481dad5cca790626ffe4c34eabf9467069d06ee4999/numpy-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:5f8e00be2ec6f45f4e8a41a527f68d44a7d96fee92a650e4d8b1326f77f61e6e", size = 10748103, upload-time = "2026-08-09T13:48:24.21Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[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.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "2.3.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version < '3.11'",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" } },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "pytz" },
|
||||||
|
{ name = "tzdata" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/33/01/d40b85317f86cf08d853a4f495195c73815fdf205eef3993821720274518/pandas-2.3.3.tar.gz", hash = "sha256:e05e1af93b977f7eafa636d043f9f94c7ee3ac81af99c13508215942e64c993b", size = 4495223, upload-time = "2025-09-29T23:34:51.853Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/f7/f425a00df4fcc22b292c6895c6831c0c8ae1d9fac1e024d16f98a9ce8749/pandas-2.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:376c6446ae31770764215a6c937f72d917f214b43560603cd60da6408f183b6c", size = 11555763, upload-time = "2025-09-29T23:16:53.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/13/4f/66d99628ff8ce7857aca52fed8f0066ce209f96be2fede6cef9f84e8d04f/pandas-2.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e19d192383eab2f4ceb30b412b22ea30690c9e618f78870357ae1d682912015a", size = 10801217, upload-time = "2025-09-29T23:17:04.522Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/03/3fc4a529a7710f890a239cc496fc6d50ad4a0995657dccc1d64695adb9f4/pandas-2.3.3-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5caf26f64126b6c7aec964f74266f435afef1c1b13da3b0636c7518a1fa3e2b1", size = 12148791, upload-time = "2025-09-29T23:17:18.444Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/a8/4dac1f8f8235e5d25b9955d02ff6f29396191d4e665d71122c3722ca83c5/pandas-2.3.3-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd7478f1463441ae4ca7308a70e90b33470fa593429f9d4c578dd00d1fa78838", size = 12769373, upload-time = "2025-09-29T23:17:35.846Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/91/82cc5169b6b25440a7fc0ef3a694582418d875c8e3ebf796a6d6470aa578/pandas-2.3.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4793891684806ae50d1288c9bae9330293ab4e083ccd1c5e383c34549c6e4250", size = 13200444, upload-time = "2025-09-29T23:17:49.341Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/ae/89b3283800ab58f7af2952704078555fa60c807fff764395bb57ea0b0dbd/pandas-2.3.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28083c648d9a99a5dd035ec125d42439c6c1c525098c58af0fc38dd1a7a1b3d4", size = 13858459, upload-time = "2025-09-29T23:18:03.722Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/85/72/530900610650f54a35a19476eca5104f38555afccda1aa11a92ee14cb21d/pandas-2.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:503cf027cf9940d2ceaa1a93cfb5f8c8c7e6e90720a2850378f0b3f3b1e06826", size = 11346086, upload-time = "2025-09-29T23:18:18.505Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/fa/7ac648108144a095b4fb6aa3de1954689f7af60a14cf25583f4960ecb878/pandas-2.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:602b8615ebcc4a0c1751e71840428ddebeb142ec02c786e8ad6b1ce3c8dec523", size = 11578790, upload-time = "2025-09-29T23:18:30.065Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9b/35/74442388c6cf008882d4d4bdfc4109be87e9b8b7ccd097ad1e7f006e2e95/pandas-2.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8fe25fc7b623b0ef6b5009149627e34d2a4657e880948ec3c840e9402e5c1b45", size = 10833831, upload-time = "2025-09-29T23:38:56.071Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/e4/de154cbfeee13383ad58d23017da99390b91d73f8c11856f2095e813201b/pandas-2.3.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b468d3dad6ff947df92dcb32ede5b7bd41a9b3cceef0a30ed925f6d01fb8fa66", size = 12199267, upload-time = "2025-09-29T23:18:41.627Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/c9/63f8d545568d9ab91476b1818b4741f521646cbdd151c6efebf40d6de6f7/pandas-2.3.3-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b98560e98cb334799c0b07ca7967ac361a47326e9b4e5a7dfb5ab2b1c9d35a1b", size = 12789281, upload-time = "2025-09-29T23:18:56.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cd/4b/18b035ee18f97c1040d94debd8f2e737000ad70ccc8f5513f4eefad75f4b/pandas-2.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:56851a737e3470de7fa88e6131f41281ed440d29a9268dcbf0002da5ac366713", size = 11544671, upload-time = "2025-09-29T23:21:05.024Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/94/72fac03573102779920099bcac1c3b05975c2cb5f01eac609faf34bed1ca/pandas-2.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdcd9d1167f4885211e401b3036c0c8d9e274eee67ea8d0758a256d60704cfe8", size = 10680807, upload-time = "2025-09-29T23:21:15.979Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/87/9472cf4a487d848476865321de18cc8c920b8cab98453ab79dbbc98db63a/pandas-2.3.3-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e32e7cc9af0f1cc15548288a51a3b681cc2a219faa838e995f7dc53dbab1062d", size = 11709872, upload-time = "2025-09-29T23:21:27.165Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/07/284f757f63f8a8d69ed4472bfd85122bd086e637bf4ed09de572d575a693/pandas-2.3.3-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:318d77e0e42a628c04dc56bcef4b40de67918f7041c2b061af1da41dcff670ac", size = 12306371, upload-time = "2025-09-29T23:21:40.532Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/33/81/a3afc88fca4aa925804a27d2676d22dcd2031c2ebe08aabd0ae55b9ff282/pandas-2.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4e0a175408804d566144e170d0476b15d78458795bb18f1304fb94160cabf40c", size = 12765333, upload-time = "2025-09-29T23:21:55.77Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/0f/b4d4ae743a83742f1153464cf1a8ecfafc3ac59722a0b5c8602310cb7158/pandas-2.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:93c2d9ab0fc11822b5eece72ec9587e172f63cff87c00b062f6e37448ced4493", size = 13418120, upload-time = "2025-09-29T23:22:10.109Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/c7/e54682c96a895d0c808453269e0b5928a07a127a15704fedb643e9b0a4c8/pandas-2.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:f8bfc0e12dc78f777f323f55c58649591b2cd0c43534e8355c51d3fede5f4dee", size = 10993991, upload-time = "2025-09-29T23:25:04.889Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/ca/3f8d4f49740799189e1395812f3bf23b5e8fc7c190827d55a610da72ce55/pandas-2.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:75ea25f9529fdec2d2e93a42c523962261e567d250b0013b16210e1d40d7c2e5", size = 12048227, upload-time = "2025-09-29T23:22:24.343Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0e/5a/f43efec3e8c0cc92c4663ccad372dbdff72b60bdb56b2749f04aa1d07d7e/pandas-2.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74ecdf1d301e812db96a465a525952f4dde225fdb6d8e5a521d47e1f42041e21", size = 11411056, upload-time = "2025-09-29T23:22:37.762Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/46/b1/85331edfc591208c9d1a63a06baa67b21d332e63b7a591a5ba42a10bb507/pandas-2.3.3-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6435cb949cb34ec11cc9860246ccb2fdc9ecd742c12d3304989017d53f039a78", size = 11645189, upload-time = "2025-09-29T23:22:51.688Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/23/78d645adc35d94d1ac4f2a3c4112ab6f5b8999f4898b8cdf01252f8df4a9/pandas-2.3.3-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:900f47d8f20860de523a1ac881c4c36d65efcb2eb850e6948140fa781736e110", size = 12121912, upload-time = "2025-09-29T23:23:05.042Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/53/da/d10013df5e6aaef6b425aa0c32e1fc1f3e431e4bcabd420517dceadce354/pandas-2.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a45c765238e2ed7d7c608fc5bc4a6f88b642f2f01e70c0c23d2224dd21829d86", size = 12712160, upload-time = "2025-09-29T23:23:28.57Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/17/e756653095a083d8a37cbd816cb87148debcfcd920129b25f99dd8d04271/pandas-2.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c4fc4c21971a1a9f4bdb4c73978c7f7256caa3e62b323f70d6cb80db583350bc", size = 13199233, upload-time = "2025-09-29T23:24:24.876Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/04/fd/74903979833db8390b73b3a8a7d30d146d710bd32703724dd9083950386f/pandas-2.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:ee15f284898e7b246df8087fc82b87b01686f98ee67d85a17b7ab44143a3a9a0", size = 11540635, upload-time = "2025-09-29T23:25:52.486Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/00/266d6b357ad5e6d3ad55093a7e8efc7dd245f5a842b584db9f30b0f0a287/pandas-2.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1611aedd912e1ff81ff41c745822980c49ce4a7907537be8692c8dbc31924593", size = 10759079, upload-time = "2025-09-29T23:26:33.204Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ca/05/d01ef80a7a3a12b2f8bbf16daba1e17c98a2f039cbc8e2f77a2c5a63d382/pandas-2.3.3-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d2cefc361461662ac48810cb14365a365ce864afe85ef1f447ff5a1e99ea81c", size = 11814049, upload-time = "2025-09-29T23:27:15.384Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/b2/0e62f78c0c5ba7e3d2c5945a82456f4fac76c480940f805e0b97fcbc2f65/pandas-2.3.3-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ee67acbbf05014ea6c763beb097e03cd629961c8a632075eeb34247120abcb4b", size = 12332638, upload-time = "2025-09-29T23:27:51.625Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c5/33/dd70400631b62b9b29c3c93d2feee1d0964dc2bae2e5ad7a6c73a7f25325/pandas-2.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c46467899aaa4da076d5abc11084634e2d197e9460643dd455ac3db5856b24d6", size = 12886834, upload-time = "2025-09-29T23:28:21.289Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/18/b5d48f55821228d0d2692b34fd5034bb185e854bdb592e9c640f6290e012/pandas-2.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6253c72c6a1d990a410bc7de641d34053364ef8bcd3126f7e7450125887dffe3", size = 13409925, upload-time = "2025-09-29T23:28:58.261Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a6/3d/124ac75fcd0ecc09b8fdccb0246ef65e35b012030defb0e0eba2cbbbe948/pandas-2.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:1b07204a219b3b7350abaae088f451860223a52cfb8a6c53358e7948735158e5", size = 11109071, upload-time = "2025-09-29T23:32:27.484Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/9c/0e21c895c38a157e0faa1fb64587a9226d6dd46452cac4532d80c3c4a244/pandas-2.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2462b1a365b6109d275250baaae7b760fd25c726aaca0054649286bcfbb3e8ec", size = 12048504, upload-time = "2025-09-29T23:29:31.47Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/82/b69a1c95df796858777b68fbe6a81d37443a33319761d7c652ce77797475/pandas-2.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:0242fe9a49aa8b4d78a4fa03acb397a58833ef6199e9aa40a95f027bb3a1b6e7", size = 11410702, upload-time = "2025-09-29T23:29:54.591Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/88/702bde3ba0a94b8c73a0181e05144b10f13f29ebfc2150c3a79062a8195d/pandas-2.3.3-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a21d830e78df0a515db2b3d2f5570610f5e6bd2e27749770e8bb7b524b89b450", size = 11634535, upload-time = "2025-09-29T23:30:21.003Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/1e/1bac1a839d12e6a82ec6cb40cda2edde64a2013a66963293696bbf31fbbb/pandas-2.3.3-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e3ebdb170b5ef78f19bfb71b0dc5dc58775032361fa188e814959b74d726dd5", size = 12121582, upload-time = "2025-09-29T23:30:43.391Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/44/91/483de934193e12a3b1d6ae7c8645d083ff88dec75f46e827562f1e4b4da6/pandas-2.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d051c0e065b94b7a3cea50eb1ec32e912cd96dba41647eb24104b6c6c14c5788", size = 12699963, upload-time = "2025-09-29T23:31:10.009Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/44/5191d2e4026f86a2a109053e194d3ba7a31a2d10a9c2348368c63ed4e85a/pandas-2.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3869faf4bd07b3b66a9f462417d0ca3a9df29a9f6abd5d0d0dbab15dac7abe87", size = 13202175, upload-time = "2025-09-29T23:31:59.173Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pandas"
|
||||||
|
version = "3.0.5"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
resolution-markers = [
|
||||||
|
"python_full_version >= '3.14' and sys_platform == 'win32'",
|
||||||
|
"python_full_version >= '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'win32'",
|
||||||
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version >= '3.12' and python_full_version < '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
"python_full_version == '3.11.*' and sys_platform == 'win32'",
|
||||||
|
"python_full_version == '3.11.*' and sys_platform == 'emscripten'",
|
||||||
|
"python_full_version == '3.11.*' and sys_platform != 'emscripten' and sys_platform != 'win32'",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
{ name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
|
||||||
|
{ name = "numpy", version = "2.5.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
|
||||||
|
{ name = "python-dateutil" },
|
||||||
|
{ name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/be/4f/5f3422a2afec5ffc46308b79e53291365a93748b498ac2e58bead0197916/pandas-3.0.5.tar.gz", hash = "sha256:dca3734d6ab7c906e6730f0788b0a1dbb9f2467731f9711f77995c8e9d62d712", size = 4658219, upload-time = "2026-07-22T22:19:28.819Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/ef/f1fd7431d635bf20015489bf0bd69c17fff1018de773540f651455a3916b/pandas-3.0.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2946e77e4a53cd248cbde631a12f0e51c8324ce354c3eba4d20147c1ad6f4282", size = 10397178, upload-time = "2026-07-22T22:17:48.274Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/31/b4/0eafac990a431561187694126de01f9b12559549b4d86360c0c4bd870fde/pandas-3.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71ecc8fb7ed1a7aa4392316b5309a6347e8e7f832f38fd897846b3a1457a9298", size = 9990736, upload-time = "2026-07-22T22:17:52.388Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/21/359880af3ea9b7cb23bea5b51e8e70ef3866c03be09da9a2787e18e330a8/pandas-3.0.5-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b173f5951ff6b8b0ec7675e20dff3c97b7e7a57dfcce387c2d7c5afe87cb7899", size = 10814438, upload-time = "2026-07-22T22:17:54.708Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/50/d6cc4d7e508bbccf5d6027314a8312bc7ac73d0ec7f195f53838daafab40/pandas-3.0.5-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2c0cf1dd9b55a22d105fc46c1b489af3bd42264fcba7c66297bf47a9a1d9c78a", size = 11323634, upload-time = "2026-07-22T22:17:56.858Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/2b/d5f0a8c90dd0ae04e64ba53b871afb796ec026b615086d382ddc2ade729b/pandas-3.0.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0fac0010c75e4efb6b99e249c183a8993ce0dc95c240f9b120a5e67c727b7928", size = 11850860, upload-time = "2026-07-22T22:17:59.1Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/30/183aec2e19adf778a98d29b5729a0a68f4cc4ebf9b9c3b70d0297355bcb1/pandas-3.0.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:08d24fe11a17dc33bd6e937dc9c665f9cba08fbdc9f657f405713515febe300d", size = 12411100, upload-time = "2026-07-22T22:18:01.485Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/9a/31f4983f191af51ab2a8f2d0c7b33dff3a84da26533f982fff02c2f9e28b/pandas-3.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b1261758dfb6cf12c3cff8300e21cefad30e7ec709abb4c24ac7318e6a52462a", size = 9968804, upload-time = "2026-07-22T22:18:03.903Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/97/7886c89a39045c69ad82cbceaf3343810480c8ef49a216319ce8183860a6/pandas-3.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:679f4e85b30ddb1515458ab1e788d3e260eae369b1f78da7a3aa4cac8ebf4a2a", size = 9205447, upload-time = "2026-07-22T22:18:06.134Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1c/54/1dc810ea558d1320b597aa140a514f2fdf1d2ea09c38cf556f13ea712ec9/pandas-3.0.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fa290c16964d4963fbfbc358928239cf3bd755b20e988ce944877def2f44471d", size = 10411717, upload-time = "2026-07-22T22:18:08.307Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/56/fbe81c09195924d8b7b8d4461a20458fe80a6a5ed6b24f0314da684277e1/pandas-3.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2e26bb46934b8a2ca0c3de1d3d606fc5f6746584791b2db264d58cf370e08dc", size = 9957095, upload-time = "2026-07-22T22:18:10.6Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/51/fac252f4a913ed5eabf3c11b880a9e8d5a6c10f0b2129d0462212d238b4d/pandas-3.0.5-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:73fa87b08a7ef706f8aafda39ddaccf2a99047bea62d8c88a0361bcafb2237bc", size = 10485458, upload-time = "2026-07-22T22:18:12.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/98/e976540c1addf70442be7842a18cf70884a964abbf69442504f4d2939989/pandas-3.0.5-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d373ce03ffd84010ed9839fa73672a9c8256990532e158440c0085db7d914b34", size = 10998091, upload-time = "2026-07-22T22:18:15.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a4/8c/1f29b5be8d3fc47dd7567eb167fabba2085879b31e0287ce7cba6d3d2ff4/pandas-3.0.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2a29c53d85ea98c5e792c59ef82ee9fbe6ca902c0d0adb6b23f45ef894cd7bf6", size = 11499501, upload-time = "2026-07-22T22:18:17.689Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/e2/bd9c98ad2df7b38bde002adde4cdf353519da51881634323b126c55997f9/pandas-3.0.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ad3b02ed6bc7d7ae9b70804b2c6aa31827489d150f8e623ce82491b82085d7", size = 12060559, upload-time = "2026-07-22T22:18:20.147Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/9a/ffbd852d58bd74a617fe2f8ee6a58a96982271ce41cf981eab22190b4a4b/pandas-3.0.5-cp312-cp312-pyemscripten_2024_0_wasm32.whl", hash = "sha256:b2acb4650527eec6822c3dadb2b771277b65e7dae7a267d4bccf65fd1bb3fbce", size = 7197652, upload-time = "2026-07-22T22:18:22.502Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/70/b5/d2d3e9ae73362ba4229651b0ee1455cf78073a1ce585f6ff693782ce263e/pandas-3.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:80a611068e8a3ac23f7398c6c14eb46dc974e5cc9997f653e2dcfd1da74edd41", size = 9831691, upload-time = "2026-07-22T22:18:24.534Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/52/51/dea1e89d6a6796b9c43f85a09b484ee03edb8a4c4842e73e200a8c11301c/pandas-3.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:25ff585b972a18ef1fe9ffa3ac6544d9950508aa76832e5147640b6022821e49", size = 9105796, upload-time = "2026-07-22T22:18:27.064Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bf/09/7b95c4a0025227d6f118c4039b423412ac6a982db02864166185d812fbc7/pandas-3.0.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c1c05a767fe8e5b4fe9e1c29806829c582052eaedb9120a3da83ba3f69e24a5b", size = 10385742, upload-time = "2026-07-22T22:18:29.346Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/0c/dc78fd8c4da477b4b5e8ad37295af352190d21ef63a9ee1bc071753074cc/pandas-3.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b86765f268b56f7e665b93bce9d5df69dee7f99e595cf8fb839483ab315942a3", size = 9932067, upload-time = "2026-07-22T22:18:31.833Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3e/71/3592c055cf44df9808550f9368ceda80ff2b224d355ef73fe251dcda1802/pandas-3.0.5-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c597ecf5616b5c420372c1d4d4c00dbbfba7398bea857dcc984347e1ea48417b", size = 10466756, upload-time = "2026-07-22T22:18:34.195Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/70/4363150359f95b4cb4bcbb34ca23572bb5495749a621a8f3d5a1ddfd293c/pandas-3.0.5-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4b11c36e218331d0387cbe3a0a5f75162357a1d92d57b2b08a336ff94b19b2be", size = 10938525, upload-time = "2026-07-22T22:18:36.81Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/d0/317e7a0c67c0e69fa905a0161409397a7dc2d46ff611f6ca4803352c042b/pandas-3.0.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cf52e1f61d229496da17dc7ab54acdee627357e7008fd4fecba3d0ba2937fa58", size = 11489303, upload-time = "2026-07-22T22:18:39.287Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/8d/36dade89b49e4f9d5cbdbe863772581f98c0c6d78fc39ad4c557f6f2e17e/pandas-3.0.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:db172144bb56422bd157812f3b021eacc255451470b31e2c633c349490a1cfee", size = 11989004, upload-time = "2026-07-22T22:18:42.208Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/ba/18c4ec8a746e177da05a9e7a7963781d8ea195780724f854601b6ebd6b78/pandas-3.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:0d298e951f23016ce4699951d044ae6418dbc91bf68cefca0f77666fcbb4e5c6", size = 9826896, upload-time = "2026-07-22T22:18:44.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/ec/28a57266b753799a87b8bc79e7887ac6fd981b8c6d2978a0b7e7b6bd708c/pandas-3.0.5-cp313-cp313-win_arm64.whl", hash = "sha256:66266d3442a5e8b3c90274c2b8b230bee42dd1c286bc822cc2f9f2c7e12b883e", size = 9094790, upload-time = "2026-07-22T22:18:47.468Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/2f/cf6aae281264f4463f0875bcbb15fd2bb6d291cc535187dad1732475e4a9/pandas-3.0.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2f264fc46911cc8131a7322a16199bbf8e353d27c10bb211f5bd0c814324dc36", size = 10390034, upload-time = "2026-07-22T22:18:49.818Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/ec/5189518c7a7659c4bdcc6b1eb32c46c6f3c86b0661ffd84143d1112c7732/pandas-3.0.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:53730687fcd161883b24e10411c06d6a4c0f2275d2faf3bb2bc25deb4ba8007c", size = 9980065, upload-time = "2026-07-22T22:18:52.249Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ea/f1/598503ce8d7e3c35601e0747ba288c7864baae66380725bc12f13f884dfe/pandas-3.0.5-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:960d3ebcf249f75206899fcd2c6de53f736b7265759ced0d3e559df0b8b709b0", size = 10545532, upload-time = "2026-07-22T22:18:54.813Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/de/ceae2adf7034e07e9910299fe412e1819c4f0dd520700a888bcb03625448/pandas-3.0.5-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e94c2c5ca43bd3ca32bf64d32308887b65e5f9bfd8023ea52755107a999f93b", size = 10963120, upload-time = "2026-07-22T22:18:57.42Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/25/86e0f4451874eb79e688deeebe3c451fec4557f8952005818d800ee8ac7e/pandas-3.0.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e819dd5f62966b481a8cb649d3299ebd886a1ea91ed5a99bf7ce77c98d18ab94", size = 11563178, upload-time = "2026-07-22T22:18:59.729Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f3/45/8643daa3b4147e433adfcccefdd0380d3aad79d86b15d8999730fe1944d5/pandas-3.0.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3c5ed2e7c06e91d340dfd091d7934f9bc82e4a36b95f647f090b9d1c9ac649da", size = 12028708, upload-time = "2026-07-22T22:19:02.164Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/96/58/ad979ae617615576e8aafd569c9d4b62f1191d896e38f51d66ba06f3b89a/pandas-3.0.5-cp314-cp314-win_amd64.whl", hash = "sha256:cd8f7c6dc98527058ee6264219343f5392240a6f1bfa654fc5d79023020d0c92", size = 9951806, upload-time = "2026-07-22T22:19:04.596Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/69/32/7ac03886b304049a9d2625ee88f59af760d8a93bd30ed9239bce7b9869a8/pandas-3.0.5-cp314-cp314-win_arm64.whl", hash = "sha256:5183427f5a8156d480f30333777bc978be93650a49a7c01db26adffe95b31e85", size = 9238297, upload-time = "2026-07-22T22:19:06.836Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/ed/1d1f2ee5547d5167face2376d11c8b2a4c7bfff5a416ee7a9046891fab1e/pandas-3.0.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:303da736987d481074ca720ada325f8bd80c64ebc2d45ed79b29df3aaa4a26ca", size = 10849690, upload-time = "2026-07-22T22:19:09.391Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/55/17e17152e98fbb0c4b1e562bc65387a2f20a80db0f4a86bf8d3a0e4248d4/pandas-3.0.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3b2801bbb049d0136f6c213eae02b5fca969384fc2064dd728d8620552aa49da", size = 10509945, upload-time = "2026-07-22T22:19:11.773Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/90/817d44dbf83facf9556f33576d9af0a241981e7bb5c00606c0bcb5df8dda/pandas-3.0.5-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cce3a9d11d2b1f82c69a27ec1f4948a170e2c403c4bbfa8cca62e3fdebe2ef3a", size = 10392197, upload-time = "2026-07-22T22:19:14.024Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/da/889f00c0a6f5aa1545add70abbf01502dff87ab577adb855bd631c54d2f2/pandas-3.0.5-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef01af4d8dc6cd2c8d6c7736f149574ef93fe043811eeb5e445f2647154b5040", size = 10862726, upload-time = "2026-07-22T22:19:16.351Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/98/f1e934fb3c98fce859c6147c6785816c7b5b9ab7821115c5d8c4de9842b9/pandas-3.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e2759e890db96dfcffdbd9b86c3c2cb6afaf58def482820317e06163ec1066cd", size = 11414864, upload-time = "2026-07-22T22:19:18.981Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fe/be/d448af7d657d82e1888dd8551f79c6d6fb161080b5b9752d84d910ec2319/pandas-3.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b58b1b39d46a5862e3fb18f50d1a201398619d16a0f9f73f57eea5583cf0e63c", size = 11925105, upload-time = "2026-07-22T22:19:21.515Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/29/c1/ccb4238212c8c4f496c584f3044d94e0c030ed8e1d68999db46c91c2242f/pandas-3.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:1c10461f6eeb35d8f05b6184c65c8b9991663b66c46b1d559b682cb34ae7c6ea", size = 10387612, upload-time = "2026-07-22T22:19:24.257Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/cf/6a51b2c38980e04c279fd2fa908a1b0982064e860444acfca4ec2e2c8359/pandas-3.0.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3c5015fd1730fbf883647e88068176c839c102cea883ba1769a6f4593bfc1f8c", size = 9509776, upload-time = "2026-07-22T22:19:26.694Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "playwright"
|
||||||
|
version = "1.62.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "greenlet" },
|
||||||
|
{ name = "pyee" },
|
||||||
|
]
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/5b/ca2abcf3aa69f9fb510215e3064f30b57fe57657c8d04ede45bb966d5606/playwright-1.62.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:d8da938f3748841a8754f2e1f0216902c1c8f8ae3720de8b32ccf8e6913a7c4f", size = 43732091, upload-time = "2026-07-31T17:00:44.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/af/1a/0bfbe9904350961f4dbb713f04342e40d548c5fc26c8157bd13617c81492/playwright-1.62.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:db755ab27db21a04186f1fe8169888e42356086e439b1059b923ef417f0b6034", size = 42510842, upload-time = "2026-07-31T17:00:48.596Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/66/dc/c0486b407ad0699a250f6bbe3066fca95344009a99ca66e88ca175c69dc1/playwright-1.62.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:5108bd5b3e87169ddf269feee097da5893af7f8aea4634dfc840518d64c1f1da", size = 43732093, upload-time = "2026-07-31T17:00:52.218Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/6b/b24aebc2b04bffcb342bccf96e287c78b363e1615bed5cea97500cc0393a/playwright-1.62.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:ba33bae6a13b3d9d354c751cb618af357d20fe1d57767cbcce52079bbef17ad3", size = 47748926, upload-time = "2026-07-31T17:00:56.438Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/36/43/b4b18bdc87e1949568fffdcde3ff9a0456266b2d0c6d4432cc34d89ea6eb/playwright-1.62.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db2d76613a57ad844362ce42f7d0c2fa26b19a4f7a46d4f76b891c631e6e5aff", size = 47441423, upload-time = "2026-07-31T17:01:00.404Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/22/af5d926fc2c32a339eec00a443644bc40ab9db1dd2dd9017873c59773c0c/playwright-1.62.0-py3-none-win32.whl", hash = "sha256:e5614fa89355d7081457680324bb219f79f69c423c5cb6fa250e30b0d8aebf1c", size = 38164450, upload-time = "2026-07-31T17:01:04.187Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/a9/4160c1033c07af98bf841ad079457dd78408a5ee0dd56cbfe50b8b6a1c22/playwright-1.62.0-py3-none-win_amd64.whl", hash = "sha256:92c0d98ed04eb35af557b709875edba415b1f548bdb22ddb5bb3e1e6c835c2f1", size = 38164458, upload-time = "2026-07-31T17:01:08.459Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6c/ec/06b55d619a7082a766aa04f2c6bb31435c87f02930087d8a0517119408fa/playwright-1.62.0-py3-none-win_arm64.whl", hash = "sha256:ea8d3055aa9d5a9f1832ac82517bd8b42c78fac7ebcbebb0107116735c8cb6a1", size = 34208868, upload-time = "2026-07-31T17:01:11.818Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyee"
|
||||||
|
version = "13.0.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/8b/04/e7c1fe4dc78a6fdbfd6c337b1c3732ff543b8a397683ab38378447baa331/pyee-13.0.1.tar.gz", hash = "sha256:0b931f7c14535667ed4c7e0d531716368715e860b988770fc7eb8578d1f67fc8", size = 31655, upload-time = "2026-02-14T21:12:28.044Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/c4/b4d4827c93ef43c01f599ef31453ccc1c132b353284fc6c87d535c233129/pyee-13.0.1-py3-none-any.whl", hash = "sha256:af2f8fede4171ef667dfded53f96e2ed0d6e6bd7ee3bb46437f77e3b57689228", size = 15659, upload-time = "2026-02-14T21:12:26.263Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.21.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "9.1.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "python-dateutil"
|
||||||
|
version = "2.9.0.post0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "six" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytz"
|
||||||
|
version = "2026.3.post1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/fb/48/fb042503b6ca6cd271261dc559fd6432f7d8c713153e9ec5c591af4dfc1c/pytz-2026.3.post1.tar.gz", hash = "sha256:2211d3fcf9a797d3405cac96ac7f61d80e6a644f72a3309607282fe8a2010c5d", size = 319745, upload-time = "2026-07-25T15:12:07.385Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/7b/39c34ca613b0b198cb866466651b26b045e2009864c5183c979a3b83f383/pytz-2026.3.post1-py2.py3-none-any.whl", hash = "sha256:dd95840dd199baea12d9cc096a1d452caa6596a1c1e4b5f3dbd1541855d5e815", size = 508283, upload-time = "2026-07-25T15:12:05.782Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.17.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f6/cc/6253133b5bb138fc3306cebfbda2c520f545d36b5be2c7255cc528bb45d6/typing_extensions-4.16.0.tar.gz", hash = "sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5", size = 113555, upload-time = "2026-07-02T08:40:05.92Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tzdata"
|
||||||
|
version = "2026.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/92/ff/5a28bdfd8c3ebec42564ac7d0e54ca3db65044a9314a97f9564fa7a1e926/tzdata-2026.3.tar.gz", hash = "sha256:4a1518b8993086a7982523e071643f3c0e5f213e75b21318e78bcabfff9d1415", size = 198674, upload-time = "2026-07-10T08:50:37.887Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e5/6d/b53b99a9f2766d095985947a5782f1702cabb129a34f7a802d7197af832f/tzdata-2026.3-py2.py3-none-any.whl", hash = "sha256:dc096730c87af6cab1b171c9d532be840741ff5d459015e7f6947bd7d7e54931", size = 348168, upload-time = "2026-07-10T08:50:36.46Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "yt-studio-url-builder"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "openpyxl" },
|
||||||
|
{ name = "pandas", version = "2.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "pandas", version = "3.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
|
||||||
|
{ name = "playwright" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "openpyxl", specifier = ">=3.1" },
|
||||||
|
{ name = "pandas", specifier = ">=2.0" },
|
||||||
|
{ name = "playwright", specifier = ">=1.40" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [{ name = "pytest", specifier = ">=8.0" }]
|
||||||
Reference in New Issue
Block a user