33 Commits

Author SHA1 Message Date
zhou 0afdb54e5c ~
Release / Pre-release Check (push) Failing after 1m31s
Release / Build Artifacts (push) Has been skipped
Release / Publish to PyPI (push) Has been skipped
Release / Publish Release (push) Has been skipped
2026-06-25 12:49:26 +08:00
zhou 9e99a1f1ba ~
Release / Pre-release Check (push) Failing after 31s
Release / Build Artifacts (push) Has been skipped
Release / Publish to PyPI (push) Has been skipped
Release / Publish Release (push) Has been skipped
2026-06-25 12:35:27 +08:00
zhou 50575c6e91 style: 格式化代码并补充开发工具依赖
Release / Pre-release Check (push) Failing after 42s
Release / Build Artifacts (push) Has been skipped
Release / Publish to PyPI (push) Has been skipped
Release / Publish Release (push) Has been skipped
1. 统一格式化多个文件的字典/列表缩进样式
2. 为pymake的bump命令新增typecheck、ruff_lint、ruff_format检查步骤
3. 扩充test_packtool.py的嵌入式Python安装测试用例
2026-06-25 12:26:25 +08:00
zhou f8436f6b8c refactor(emlmanager): 重构EML解析逻辑,提取公共方法并优化字符编码处理
1.  拆分邮件解析为多部分/单部分处理函数,抽离正文提取、日期解析逻辑
2.  完善字符编码检测与 fallback 处理,使用replace模式避免解码失败崩溃
3.  统一使用配置的最大正文长度限制,添加详细日志记录
4.  修复原代码中解码异常未妥善处理的问题
5.  优化测试用例,使用tmp_path替代固定临时目录提升测试稳定性
2026-06-25 12:21:23 +08:00
zhou 5c0f51e272 ~ 2026-06-25 12:14:09 +08:00
zhou 4e3622ef02 +emlman 2026-06-25 07:57:44 +08:00
zhou f69ddc5133 +hfdownload 2026-06-24 21:36:47 +08:00
zhou 477d901281 ~
Release / Pre-release Check (push) Failing after 42s
Release / Build Artifacts (push) Has been skipped
Release / Publish to PyPI (push) Has been skipped
Release / Publish Release (push) Has been skipped
2026-06-22 12:46:50 +08:00
zhou 0df795237d ~tests 2026-06-22 12:31:26 +08:00
zhou 413ab40044 refactor(tests): 重构测试代码并优化ruff检查规则
1.  在pyproject.toml中为测试文件添加ARG001和ARG002规则忽略
2.  重构多个CLI测试文件,移除冗余的mock断言、导入顺序调整
3.  统一测试用例的帮助信息输出逻辑,移除SystemExit捕获,简化测试流程
4.  拆分合并冗余的测试类,按功能细化测试用例
5.  移除测试代码中多余的注释和pytest导入
2026-06-22 12:18:10 +08:00
zhou d4a1a5c2de test: 重构CLI测试用例,统一使用px.CliRunner和px.run测试主函数
1.  替换所有旧的main函数测试逻辑,统一使用pyflowx的CliRunner和run方法进行测试
2.  重构测试类命名,将零散测试合并为TaskSpec验证测试
3.  优化测试用例结构,移除冗余的pytest依赖导入和旧版测试代码
4.  更新文件夹备份、压缩等模块的测试逻辑,适配新的工具函数实现
2026-06-22 12:03:30 +08:00
zhou 843e9369fe refactor: 统一格式化代码中的多行列表与函数调用
对多处代码进行了统一的多行列表和函数调用进行格式化调整,包括将单行代码拆分为多行以提升可读性。
2026-06-22 11:45:10 +08:00
zhou 48f6d8a7f0 +cli tests 2026-06-22 11:43:00 +08:00
zhou 0b97846d77 refactor: 重构所有CLI工具,替换内置Runner为原生argparse实现 2026-06-22 07:51:39 +08:00
Young 50e74180a2 更新 ci.yml 2026-06-21 23:01:53 +08:00
zhou 71e6ba316a chore: bump version to 0.1.7
Release / Pre-release Check (push) Failing after 45s
Release / Build Artifacts (push) Has been skipped
Release / Publish to PyPI (push) Has been skipped
Release / Publish Release (push) Has been skipped
2026-06-21 22:55:25 +08:00
zhou 707e2ac07c feat(cli): 新增批量CLI工具模块及配套命令
新增17个CLI工具实现,覆盖清屏、进程管理、环境配置、文件处理、SSH部署、代码格式化、打包等场景,同时更新pyproject.toml添加对应命令入口和office依赖包
2026-06-21 22:46:05 +08:00
zhou 983d47bd2e refactor(executors): 重构任务跳过逻辑,提取公共函数并格式化代码
1.  提取上游任务跳过检查和条件检查为公共工具函数
2.  重构同步和异步执行器的跳过判断逻辑,减少代码重复
3.  格式化gittool.py和测试文件的列表语法,提升可读性
2026-06-21 21:55:18 +08:00
zhou 9cc91d1153 feat: 新增任务跳过原因记录,完善上游任务跳过传播逻辑
1. 为TaskResult和TaskEvent新增reason字段记录跳过原因
2. 为同步/异步任务执行器添加上游任务跳过检测,自动跳过下游任务
3. 完善任务跳过的原因判断,支持条件不满足、缓存命中、上游跳过场景
4. 优化gittool工具,新增排除目录配置和更灵活的git操作流程
5. 重构测试用例格式,新增上游任务跳过的测试覆盖
6. 默认启用verbose输出,优化跳过任务的日志提示
2026-06-21 21:45:33 +08:00
zhou 2f3041c169 ~isub 2026-06-21 21:18:27 +08:00
zhou 6a004a54b9 ~ 2026-06-21 21:11:07 +08:00
zhou 2d0873af45 ~ 2026-06-21 20:56:05 +08:00
zhou 4cc21be562 chore: bump version to 0.1.6
Release / Pre-release Check (push) Failing after 41s
Release / Build Artifacts (push) Has been skipped
Release / Publish to PyPI (push) Has been skipped
Release / Publish Release (push) Has been skipped
2026-06-21 20:54:38 +08:00
zhou 98cf3b54a1 chore: 发布v0.1.5版本并完成代码清理优化
1. 移除pyproject.toml中冗余的ruff格式化配置
2. 删除CliRunner内置的类型校验逻辑并移除对应测试用例
3. 修复条件判断模块的匿名函数命名兼容非函数对象场景
4. 优化task.py中的类型转换和命令执行逻辑
5. 更新pymake.py的格式化任务配置并调整测试任务依赖
6. 从依赖和锁文件中移除ruff包,统一pre-commit配置格式
2026-06-21 20:12:24 +08:00
zhou af8a074484 chore: add LICENSE file and format README.md
- add MIT LICENSE file for the project
- reformat README.md code block indentation for better readability
2026-06-21 19:15:39 +08:00
zhou ff1122cb68 chore(cli): add git push related task specs and alias 2026-06-21 19:09:48 +08:00
zhou cbc02c5aee chore: bump version to 0.1.5
Release / Pre-release Check (push) Failing after 37s
Release / Build Artifacts (push) Has been skipped
Release / Publish to PyPI (push) Has been skipped
Release / Publish Release (push) Has been skipped
2026-06-21 19:07:51 +08:00
zhou c8e9354e87 fix(runner): 修复命令行策略默认值与构造参数不一致的问题 2026-06-21 19:07:47 +08:00
zhou 1ecff5fdf7 refactor(runner): simplify command help text generation 2026-06-21 19:04:40 +08:00
zhou c856c9b6a6 refactor(cli): 调整pymake运行策略和命令映射
将默认运行策略从sequential改为thread,重构开发工具命令的映射关系,统一类型检查相关命令为tc
2026-06-21 19:02:23 +08:00
zhou ea591d1088 feat: 新增skip_if_missing特性,支持命令不存在时自动跳过任务
本次提交实现了命令任务的自动跳过功能:
1. 为TaskSpec新增skip_if_missing参数,默认开启,仅对list[str]类型cmd生效
2. 通过shutil.which检查命令是否存在,不存在则标记任务为SKIPPED而非失败
3. 重构should_execute方法,整合条件检查与命令可用性检查
4. 更新文档与示例代码,添加该参数的使用说明
5. 移除cli/pymake.py中的冗余check辅助函数,改用内置特性
6. 为所有内置任务添加skip_if_missing=True配置
7. 修复线程并行测试的超时阈值,放宽到1.0秒
8. 优化代码格式与压缩单行表达式
9. 新增完整的单元测试覆盖该特性的各种场景
2026-06-21 18:55:24 +08:00
zhou cae51856d2 ~CI config 2026-06-21 18:20:48 +08:00
zhou be03662e4c 更新CI 2026-06-21 18:17:28 +08:00
63 changed files with 9092 additions and 694 deletions
+13 -19
View File
@@ -2,9 +2,9 @@ name: CI
on:
push:
branches: [main, develop]
branches: [ main, develop ]
pull_request:
branches: [main, develop]
branches: [ main, develop ]
workflow_dispatch:
concurrency:
@@ -38,16 +38,16 @@ jobs:
run: uv sync --extra dev --frozen
- name: Ruff 检查
run: uv run ruff check src tests examples
run: uv run ruff check src tests
- name: Ruff 格式检查
run: uv run ruff format --check src tests examples
run: uv run ruff format --check src tests
# ─────────────────────────────────────────────────────────────
# typecheckmypy 严格类型检查
# typecheckpyrefly 严格类型检查
# ─────────────────────────────────────────────────────────────
typecheck:
name: Typecheck (mypy)
name: Typecheck (pyrefly)
runs-on: ubuntu-latest
steps:
- name: Checkout
@@ -68,8 +68,8 @@ jobs:
- name: 安装依赖
run: uv sync --extra dev --frozen
- name: Mypy 严格类型检查
run: uv run mypy
- name: pyrefly 严格类型检查
run: uv run pyrefly check .
# ─────────────────────────────────────────────────────────────
# test:多平台 × 多 Python 版本矩阵测试 + 覆盖率
@@ -80,8 +80,8 @@ jobs:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12', '3.13']
os: [ ubuntu-latest, windows-latest, macos-latest ]
python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13' ]
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -101,14 +101,8 @@ jobs:
- name: 安装依赖
run: uv sync --extra dev --frozen
- name: 运行测试(含覆盖率,强制 100%
run: uv run pytest -v --cov=pyflowx --cov-report=xml --cov-report=term-missing --cov-fail-under=95
- name: 运行示例冒烟测试
run: |
uv run python examples/etl_pipeline.py
uv run python examples/parallel_run.py
uv run python examples/async_aggregation.py
- name: 运行测试
run: uv run pytest -v --cov=pyflowx --cov-report=xml --cov-report=term-missing
- name: 上传覆盖率
if: matrix.os == 'ubuntu-latest' && matrix.python-version == '3.13'
@@ -124,7 +118,7 @@ jobs:
ci-pass:
name: CI Pass
runs-on: ubuntu-latest
needs: [lint, typecheck, test]
needs: [ lint, typecheck, test ]
if: always()
steps:
- name: 检查依赖任务结果
+3 -3
View File
@@ -7,10 +7,10 @@ repos:
hooks:
# Run the linter
- id: ruff
args: [ --fix, --exit-non-zero-on-fix ]
args: [--fix, --exit-non-zero-on-fix]
# Run the formatter
- id: ruff-format
args: [ --config=pyproject.toml]
args: [--config=pyproject.toml]
- repo: https://gitcode.com/gh_mirrors/pr/pre-commit-hooks.git
rev: v5.0.0
hooks:
@@ -18,5 +18,5 @@ repos:
- id: debug-statements
- id: fix-byte-order-marker
- id: trailing-whitespace
args: [ --markdown-linebreak-ext=md ]
args: [--markdown-linebreak-ext=md]
- id: end-of-file-fixer
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 endo Team
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.
+45 -32
View File
@@ -2,11 +2,11 @@
> 轻量、类型安全的 DAG 任务调度器。
[![CI](https://github.com/pyflowx/pyflowx/actions/workflows/ci.yml/badge.svg)](https://github.com/pyflowx/pyflowx/actions/workflows/ci.yml)
[![CI](https://github.com/gookeryoung/pyflowx/actions/workflows/ci.yml/badge.svg)](https://github.com/gookeryoung/pyflowx/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/pyflowx.svg)](https://pypi.org/project/pyflowx/)
[![Python](https://img.shields.io/pypi/pyversions/pyflowx.svg)](https://pypi.org/project/pyflowx/)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/pyflowx/pyflowx)
[![License](https://img.shields.io/pypi/l/pyflowx.svg)](https://github.com/pyflowx/pyflowx/blob/main/LICENSE)
[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen.svg)](https://github.com/gookeryoung/pyflowx)
[![License](https://img.shields.io/pypi/l/pyflowx.svg)](https://github.com/gookeryoung/pyflowx/blob/main/LICENSE)
PyFlowX 把"任务依赖"这件事做到极致简单:**参数名就是依赖声明**。无需装饰器、
无需样板包装器,写一个普通函数,框架按参数名自动注入上游结果。
@@ -25,7 +25,7 @@ PyFlowX 把"任务依赖"这件事做到极致简单:**参数名就是依赖
- **CLI 运行器** —— `CliRunner` 把多个图映射为命令行子命令,替代 Makefile
- **可观测** —— `on_event` 回调、`dry_run` 预览、`verbose` 生命周期日志、Mermaid 可视化
- **零运行时依赖** —— 仅依赖标准库(3.8 需 `graphlib_backport`
- **100% 测试覆盖** —— 分支覆盖率达 100%
- **95% 测试覆盖** —— 分支覆盖率>= 95%
## 安装
@@ -44,13 +44,16 @@ uv add pyflowx
```python
import pyflowx as px
def extract() -> list[int]:
return [1, 2, 3]
# 参数名 extract 自动匹配上游任务名 → 自动注入
def double(extract: list[int]) -> list[int]:
return [x * 2 for x in extract]
graph = px.Graph.from_specs([
px.TaskSpec("extract", extract),
px.TaskSpec("double", double, ("extract",)),
@@ -68,18 +71,19 @@ print(report["double"]) # [2, 4, 6]
```python
px.TaskSpec(
name="fetch_user", # 唯一标识
fn=fetch_user, # 同步或异步函数
cmd=["curl", "..."], # 或: 执行命令(覆盖 fn
depends_on=("auth",), # 依赖的任务名
args=(uid,), # 静态位置参数(追加在注入参数后)
kwargs={"timeout": 30}, # 静态关键字参数
retries=3, # 失败重试次数(0 = 仅一次)
timeout=30.0, # 超时秒数(None = 不限制)
tags=("api", "user"), # 自由标签,用于子图过滤
conditions=(is_prod,), # 条件函数列表(全部为 True 才执行)
cwd=Path("/tmp"), # 命令工作目录(仅 cmd 模式)
verbose=True, # 打印命令输出(仅 cmd 模式)
name="fetch_user", # 唯一标识
fn=fetch_user, # 同步或异步函数
cmd=["curl", "..."], # 或: 执行命令(覆盖 fn
depends_on=("auth",), # 依赖的任务名
args=(uid,), # 静态位置参数(追加在注入参数后)
kwargs={"timeout": 30}, # 静态关键字参数
retries=3, # 失败重试次数(0 = 仅一次)
timeout=30.0, # 超时秒数(None = 不限制)
tags=("api", "user"), # 自由标签,用于子图过滤
conditions=(is_prod,), # 条件函数列表(全部为 True 才执行)
cwd=Path("/tmp"), # 命令工作目录(仅 cmd 模式)
verbose=True, # 打印命令输出(仅 cmd 模式)
skip_if_missing=True, # 命令不存在时自动跳过(仅 list[str] cmd
)
```
@@ -88,20 +92,22 @@ px.TaskSpec(
- **函数任务**`fn`):普通 Python 函数,参数名驱动自动注入
- **命令任务**`cmd`):执行外部命令,支持 `list[str]``str`shell)、`Callable` 三种形态
`skip_if_missing=True` 时,`list[str]` 类型的 `cmd` 会通过 `shutil.which` 检查命令是否存在,不存在则跳过任务(标记为 `SKIPPED`)而非失败。适用于构建工具场景,避免因未安装某些工具而导致整个图执行失败。
### Graph —— DAG 构建
```python
graph = px.Graph.from_specs([...]) # 整批校验(推荐)
graph = px.Graph.from_specs([...]) # 整批校验(推荐)
# 或增量构建
graph = px.Graph()
graph.add(px.TaskSpec("a", fn_a))
graph.add(px.TaskSpec("b", fn_b, ("a",)))
graph.validate() # 显式校验(环检测)
graph.layers() # 拓扑分层
graph.to_mermaid() # Mermaid 可视化
graph.describe() # 人类可读摘要
graph.subgraph(("api",)) # 按标签切片
graph.validate() # 显式校验(环检测)
graph.layers() # 拓扑分层
graph.to_mermaid() # Mermaid 可视化
graph.describe() # 人类可读摘要
graph.subgraph(("api",)) # 按标签切片
graph.subgraph_by_names(("a", "b")) # 按名称切片
```
@@ -110,11 +116,11 @@ graph.subgraph_by_names(("a", "b")) # 按名称切片
```python
report = px.run(
graph,
strategy="async", # sequential | thread | async
max_workers=8, # thread 策略的线程池大小
dry_run=False, # True = 仅打印计划
verbose=False, # True = 打印任务生命周期日志
on_event=callback, # 状态转换回调
strategy="async", # sequential | thread | async
max_workers=8, # thread 策略的线程池大小
dry_run=False, # True = 仅打印计划
verbose=False, # True = 打印任务生命周期日志
on_event=callback, # 状态转换回调
state=px.JSONBackend("state.json"), # 断点续跑后端
)
```
@@ -122,12 +128,12 @@ report = px.run(
### RunReport —— 结果
```python
report["task_name"] # 任务返回值
report["task_name"] # 任务返回值
report.result_of("task_name") # 完整 TaskResult
report.success # 整体是否成功
report.summary() # 统计字典
report.failed_tasks() # 失败任务名列表
report.describe() # 人类可读报告
report.success # 整体是否成功
report.summary() # 统计字典
report.failed_tasks() # 失败任务名列表
report.describe() # 人类可读报告
```
## 上下文注入规则
@@ -142,14 +148,17 @@ report.describe() # 人类可读报告
```python
from typing import Any, Dict
def aggregate(ctx: px.Context) -> Dict[str, Any]:
"""ctx 包含所有 depends_on 任务的返回值。"""
return dict(ctx)
def merge(fetch_a: str, fetch_b: str) -> str:
"""fetch_a / fetch_b 自动注入。"""
return fetch_a + fetch_b
def fetch_user(uid: int) -> dict: # uid 来自 TaskSpec.args
...
```
@@ -176,11 +185,15 @@ graph = px.Graph.from_specs([
px.TaskSpec("check_git", cmd="git status | head"),
# 带工作目录与超时
px.TaskSpec("build", cmd=["make", "all"], cwd=Path("/project"), timeout=300),
# 命令不存在时自动跳过(而非失败)
px.TaskSpec("optional_tool", cmd=["maturin", "build"], skip_if_missing=True),
])
```
`verbose=True` 时打印执行的命令、工作目录、返回码与输出;`verbose=False` 时静默执行(失败信息仍包含 stderr)。
`skip_if_missing=True` 时,`list[str]` 类型的 `cmd` 会通过 `shutil.which` 检查命令是否存在,不存在则跳过任务(标记为 `SKIPPED`)而非失败。适用于构建工具场景,避免因未安装某些工具而导致整个图执行失败。对于 `str`shell)和 `Callable` 类型的 `cmd`,此参数无效。
## 条件执行
`conditions` 参数让任务按条件跳过(标记为 `SKIPPED`):
+67 -41
View File
@@ -17,18 +17,38 @@ license = { text = "MIT" }
name = "pyflowx"
readme = "README.md"
requires-python = ">=3.8"
version = "0.1.4"
version = "0.1.12"
[project.scripts]
pymake = "pyflowx.cli.pymake:main"
autofmt = "pyflowx.cli.autofmt:main"
bumpver = "pyflowx.cli.bumpversion:main"
clr = "pyflowx.cli.clearscreen:main"
emlman = "pyflowx.cli.emlmanager:main"
envpy = "pyflowx.cli.envpy:main"
envqt = "pyflowx.cli.envqt:main"
envrs = "pyflowx.cli.envrs:main"
filedate = "pyflowx.cli.filedate:main"
filelvl = "pyflowx.cli.filelevel:main"
foldback = "pyflowx.cli.folderback:main"
foldzip = "pyflowx.cli.folderzip:main"
gitt = "pyflowx.cli.gittool:main"
hfdown = "pyflowx.cli.hfdownload:main"
lscalc = "pyflowx.cli.lscalc:main"
packtool = "pyflowx.cli.packtool:main"
pdftool = "pyflowx.cli.pdftool:main"
piptool = "pyflowx.cli.piptool:main"
pymake = "pyflowx.cli.pymake:main"
scrcap = "pyflowx.cli.screenshot:main"
sshcopy = "pyflowx.cli.sshcopyid:main"
taskk = "pyflowx.cli.taskkill:main"
wch = "pyflowx.cli.which:main"
[project.optional-dependencies]
dev = [
"basedpyright>=1.39.8",
"hatch>=1.14.2",
"httpx>=0.28.0",
"mypy>=1.14.1",
"prek>=0.4.5",
"pyrefly>=1.1.1",
"pytest-asyncio>=0.24.0",
"pytest-cov>=5.0.0",
"pytest-html>=4.1.1",
@@ -39,6 +59,12 @@ dev = [
"tox-uv>=1.13.1",
"tox>=4.25.0",
]
office = [
"pillow>=10.4.0",
"pymupdf>=1.24.11",
"pypdf>=5.9.0",
"pytesseract>=0.3.13",
]
[build-system]
build-backend = "hatchling.build"
@@ -58,7 +84,7 @@ packages = ["src/pyflowx"]
pyflowx = { workspace = true }
[dependency-groups]
dev = ["pyflowx[dev]"]
dev = ["pyflowx[dev,office]"]
[tool.coverage.run]
branch = true
@@ -73,61 +99,61 @@ exclude_lines = [
"pragma: no cover",
"raise NotImplementedError",
]
fail_under = 95
fail_under = 80
show_missing = true
[tool.pytest.ini_options]
asyncio_default_fixture_loop_scope = "function"
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
[tool.basedpyright]
exclude = ["**/.git", "**/.venv", "**/__pycache__", "**/build", "**/dist"]
include = ["src"]
pythonVersion = "3.8"
reportImplicitStringConcatenation = "error"
reportMissingTypeStubs = "none"
reportUnusedCallResult = "warning"
typeCheckingMode = "basic" # 类型检查严格度:off / basic / standard / recommended(默认) / strict / all
markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"]
# Ruff 配置 - 与 .pre-commit-config.yaml 保持一致
[tool.ruff]
line-length = 120
target-version = "py38"
line-length = 120
[tool.ruff.format]
# 使用双引号
quote-style = "double"
# 缩进使用空格
indent-style = "space"
# 保留尾随逗号
skip-magic-trailing-comma = false
# 行长度由 [tool.ruff] 中的 line-length 控制
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # Pyflakes
"I", # isort
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"UP", # pyupgrade
"ARG", # flake8-unused-arguments
"SIM", # flake8-simplify
"PTH", # flake8-use-pathlib
"PL", # Pylint
"RUF", # Ruff-specific rules
]
ignore = [
"E501", # line too long (handled by formatter)
"PLC0415", # import should be at top-level (intentional for lazy imports)
"PLR0913", # too many arguments
"PLR0915", # too many statements (intentional for complex methods)
"PLR2004", # magic value comparison
"PTH119", # os.path.basename (intentional for sys.argv)
"PTH123", # pathlib open() replacement
"SIM108", # use ternary operator
"RUF001", # ambiguous unicode characters in string
"RUF002", # ambiguous unicode characters in docstring
"RUF003", # ambiguous unicode characters in comment
"RUF012", # mutable class attributes (intentional for config)
"PLC0415", # import should be at top-level (intentional for lazy imports)
"PLR0915", # too many statements (intentional for complex methods)
"PTH119", # os.path.basename (intentional for sys.argv)
"SIM108", # use ternary operator
]
select = [
"ARG", # flake8-unused-arguments
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"E", # pycodestyle errors
"F", # Pyflakes
"I", # isort
"PL", # Pylint
"PTH", # flake8-use-pathlib
"RUF", # Ruff-specific rules
"SIM", # flake8-simplify
"UP", # pyupgrade
"W", # pycodestyle warnings
]
[tool.ruff.lint.isort]
known-first-party = ["pyflowx"]
[tool.ruff.lint.per-file-ignores]
"**/tests/**" = ["ARG001", "ARG002"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
docstring-code-format = true
[tool.pyrefly]
preset = "basic"
project-includes = ["**/*.ipynb", "**/*.py*"]
python-version = "3.8"
+7 -1
View File
@@ -45,6 +45,12 @@
cmd=["git", "--version"],
conditions=(BuiltinConditions.HAS_INSTALLED("git"),)
),
# 命令不存在时自动跳过(而非失败)
px.TaskSpec(
"optional_build",
cmd=["maturin", "build"],
skip_if_missing=True
),
])
report = px.run(graph)
"""
@@ -78,7 +84,7 @@ from .runner import CliExitCode, CliRunner
from .storage import JSONBackend, MemoryBackend, StateBackend
from .task import TaskCmd, TaskEvent, TaskResult, TaskSpec, TaskStatus
__version__ = "0.1.4"
__version__ = "0.1.12"
__all__ = [
"IS_LINUX",
+78
View File
@@ -0,0 +1,78 @@
"""CLI 工具模块.
提供各种命令行工具的入口点.
"""
from __future__ import annotations
# 自动格式化工具
from pyflowx.cli.autofmt import main as autofmt_main
from pyflowx.cli.bumpversion import main as bumpversion_main
from pyflowx.cli.clearscreen import main as clearscreen_main
# EML 邮件管理工具
from pyflowx.cli.emlmanager import main as emlmanager_main
# EML 邮件管理工具
from pyflowx.cli.emlmanager import main as emlmanager_web_main
from pyflowx.cli.envpy import main as envpy_main
from pyflowx.cli.envqt import main as envqt_main
from pyflowx.cli.envrs import main as envrs_main
# 文件工具
from pyflowx.cli.filedate import main as filedate_main
from pyflowx.cli.filelevel import main as filelevel_main
from pyflowx.cli.folderback import main as folderback_main
from pyflowx.cli.folderzip import main as folderzip_main
# Git 工具
from pyflowx.cli.gittool import main as gittool_main
# 仿真工具
from pyflowx.cli.lscalc import main as lscalc_main
# 打包工具
from pyflowx.cli.packtool import main as packtool_main
# PDF 工具
from pyflowx.cli.pdftool import main as pdftool_main
# 开发工具
from pyflowx.cli.piptool import main as piptool_main
from pyflowx.cli.pymake import main as pymake_main
from pyflowx.cli.screenshot import main as screenshot_main
from pyflowx.cli.sshcopyid import main as sshcopyid_main
__all__ = [
# 自动格式化工具
"autofmt_main",
"bumpversion_main",
"clearscreen_main",
# EML 邮件管理工具
"emlmanager_main",
"emlmanager_web_main",
"envpy_main",
"envqt_main",
"envrs_main",
# 文件工具
"filedate_main",
"filelevel_main",
"folderback_main",
"folderzip_main",
# Git 工具
"gittool_main",
# 仿真工具
"lscalc_main",
# 打包工具
"packtool_main",
# PDF 工具
"pdftool_main",
# 开发工具
"piptool_main",
"pymake_main",
"screenshot_main",
"sshcopyid_main",
# 系统工具
"taskkill_main",
"which_main",
]
+282
View File
@@ -0,0 +1,282 @@
"""自动格式化工具模块.
提供 Python 代码自动格式化的常用功能封装,
支持 docstring 自动生成、pyproject.toml 配置同步等功能.
"""
from __future__ import annotations
import argparse
import ast
import subprocess
from pathlib import Path
import pyflowx as px
try:
import tomllib # noqa: F401
HAS_TOMLLIB = True
except ImportError:
HAS_TOMLLIB = False
# ============================================================================
# 配置
# ============================================================================
IGNORE_PATTERNS = [
"__pycache__",
"*.pyc",
"*.pyo",
".git",
".venv",
".idea",
".vscode",
"*.egg-info",
"dist",
"build",
".pytest_cache",
".tox",
".mypy_cache",
]
# ============================================================================
# 辅助函数
# ============================================================================
def format_with_ruff(target: Path, fix: bool = True) -> None:
"""使用 ruff 格式化代码.
Parameters
----------
target : Path
目标路径
fix : bool
是否自动修复
"""
cmd = ["ruff", "format", str(target)]
if fix:
cmd.append("--fix")
subprocess.run(cmd, check=True)
print(f"ruff format 完成: {target}")
def lint_with_ruff(target: Path, fix: bool = True) -> None:
"""使用 ruff 检查代码.
Parameters
----------
target : Path
目标路径
fix : bool
是否自动修复
"""
cmd = ["ruff", "check", str(target)]
if fix:
cmd.extend(["--fix", "--unsafe-fixes"])
subprocess.run(cmd, check=True)
print(f"ruff check 完成: {target}")
def add_docstring(file_path: Path, docstring: str) -> bool:
"""为文件添加 docstring.
Parameters
----------
file_path : Path
文件路径
docstring : str
docstring 内容
Returns
-------
bool
是否成功添加
"""
try:
content = file_path.read_text(encoding="utf-8")
tree = ast.parse(content)
# 检查是否已有 docstring
first_node = tree.body[0] if tree.body else None
if first_node and isinstance(first_node, ast.Expr) and isinstance(first_node.value, ast.Constant):
return False
# 添加 docstring
lines = content.splitlines()
doc_lines = docstring.splitlines()
doc_lines.append("")
new_content = "\n".join(doc_lines + lines)
file_path.write_text(new_content, encoding="utf-8")
print(f"添加 docstring: {file_path}")
return True
except (OSError, UnicodeDecodeError, SyntaxError) as e:
print(f"处理失败: {file_path} - {e}")
return False
def generate_module_docstring(file_path: Path) -> str:
"""生成模块 docstring.
Parameters
----------
file_path : Path
文件路径
Returns
-------
str
生成的 docstring
"""
stem = file_path.stem
parent = file_path.parent.name
# 关键词匹配
keywords = {
"cli": f"Command-line interface for {parent}",
"gui": f"Graphical user interface for {parent}",
"core": f"Core functionality for {parent}",
"util": f"Utility functions for {parent}",
"model": f"Data models for {parent}",
"test": f"Tests for {parent}",
}
for key, desc in keywords.items():
if key in stem.lower():
return f'"""{desc}."""'
return f'"""{stem.replace("_", " ").title()} module."""'
def auto_add_docstrings(root_dir: Path) -> int:
"""自动为所有 Python 文件添加 docstring.
Parameters
----------
root_dir : Path
根目录
Returns
-------
int
添加的 docstring 数量
"""
count = 0
for py_file in root_dir.rglob("*.py"):
# 跳过忽略的文件
if any(pattern in str(py_file) for pattern in IGNORE_PATTERNS):
continue
docstring = generate_module_docstring(py_file)
if add_docstring(py_file, docstring):
count += 1
print(f"共添加 {count} 个 docstring")
return count
def sync_pyproject_config(root_dir: Path) -> None:
"""同步 pyproject.toml 配置到子项目.
Parameters
----------
root_dir : Path
根目录
"""
main_toml = root_dir / "pyproject.toml"
if not main_toml.exists():
print(f"主项目配置文件不存在: {main_toml}")
return
# 查找所有子项目的 pyproject.toml
sub_tomls = [p for p in root_dir.rglob("pyproject.toml") if p != main_toml and ".venv" not in str(p)]
if not sub_tomls:
print("没有找到子项目的 pyproject.toml")
return
print(f"找到 {len(sub_tomls)} 个子项目配置文件")
# 对每个子项目调用 ruff format
for sub_toml in sub_tomls:
subprocess.run(["ruff", "format", str(sub_toml)], check=False)
print("配置同步完成")
def format_all(root_dir: Path) -> None:
"""格式化所有 Python 文件.
Parameters
----------
root_dir : Path
根目录
"""
# 使用 ruff format
subprocess.run(["ruff", "format", str(root_dir)], check=True)
# 使用 ruff check
subprocess.run(["ruff", "check", "--fix", "--unsafe-fixes", str(root_dir)], check=True)
print(f"格式化完成: {root_dir}")
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""自动格式化工具主函数."""
parser = argparse.ArgumentParser(
description="AutoFmt - 自动格式化工具",
usage="autofmt <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# ruff format 命令
format_parser = subparsers.add_parser("fmt", help="使用 ruff 格式化代码")
format_parser.add_argument("--target", type=str, default=".", help="目标路径")
# ruff check 命令
lint_parser = subparsers.add_parser("lint", help="使用 ruff 检查代码")
lint_parser.add_argument("--target", type=str, default=".", help="目标路径")
lint_parser.add_argument("--fix", action="store_true", help="自动修复")
# 自动添加 docstring 命令
doc_parser = subparsers.add_parser("doc", help="自动添加 docstring")
doc_parser.add_argument("--root-dir", type=str, default=".", help="根目录")
# 同步配置命令
sync_parser = subparsers.add_parser("sync", help="同步 pyproject.toml 配置")
sync_parser.add_argument("--root-dir", type=str, default=".", help="根目录")
args = parser.parse_args()
if args.command == "fmt":
graph = px.Graph.from_specs([px.TaskSpec("ruff_format", cmd=["ruff", "format", args.target], verbose=True)])
elif args.command == "lint":
cmd = ["ruff", "check", args.target]
if args.fix:
cmd.extend(["--fix", "--unsafe-fixes"])
graph = px.Graph.from_specs([px.TaskSpec("ruff_check", cmd=cmd, verbose=True)])
elif args.command == "doc":
graph = px.Graph.from_specs(
[px.TaskSpec("auto_docstring", fn=auto_add_docstrings, args=(Path(args.root_dir),), verbose=True)]
)
elif args.command == "sync":
graph = px.Graph.from_specs(
[px.TaskSpec("sync_config", fn=sync_pyproject_config, args=(Path(args.root_dir),), verbose=True)]
)
else:
parser.print_help()
return
px.run(graph, strategy="thread")
+101
View File
@@ -0,0 +1,101 @@
"""版本号自动管理工具.
使用 TaskSpec 模式实现, 支持语义化版本管理和多文件格式的版本号更新.
"""
from __future__ import annotations
import subprocess
import pyflowx as px
# ============================================================================
# 辅助函数
# ============================================================================
def bump_version(part: str = "patch", tag: bool = False, commit: bool = False) -> None:
"""递增版本号.
Parameters
----------
part : str
版本部分: patch, minor, major
tag : bool
是否创建 Git 标签
commit : bool
是否提交更改
"""
try:
subprocess.run(["bumpversion", part], check=True)
if commit:
subprocess.run(["git", "add", "."], check=True)
subprocess.run(["git", "commit", "-m", f"bump version {part}"], check=True)
if tag:
# 获取当前版本号
result = subprocess.run(
["git", "describe", "--tags", "--abbrev=0"],
check=True,
capture_output=True,
text=True,
)
version = result.stdout.strip() if result.returncode == 0 else f"v{part}"
subprocess.run(
["git", "tag", "-a", version, "-m", f"version {part}"],
check=True,
)
except FileNotFoundError:
print("未找到 bumpversion 工具,请先安装: pip install bumpversion")
raise
def bump_version_alpha(part: str = "patch") -> None:
"""递增版本号并添加 alpha 预发布标识."""
try:
subprocess.run(["bumpversion", part, "--new-version", f"{part}-alpha"], check=True)
except FileNotFoundError:
print("未找到 bumpversion 工具,请先安装: pip install bumpversion")
raise
# ============================================================================
# TaskSpec 定义
# ============================================================================
bump_patch: px.TaskSpec = px.TaskSpec("bump_patch", fn=lambda: bump_version("patch"))
bump_minor: px.TaskSpec = px.TaskSpec("bump_minor", fn=lambda: bump_version("minor"))
bump_major: px.TaskSpec = px.TaskSpec("bump_major", fn=lambda: bump_version("major"))
bump_patch_tag: px.TaskSpec = px.TaskSpec("bump_patch_tag", fn=lambda: bump_version("patch", tag=True))
bump_minor_tag: px.TaskSpec = px.TaskSpec("bump_minor_tag", fn=lambda: bump_version("minor", tag=True))
bump_major_tag: px.TaskSpec = px.TaskSpec("bump_major_tag", fn=lambda: bump_version("major", tag=True))
bump_patch_alpha: px.TaskSpec = px.TaskSpec("bump_patch_alpha", fn=lambda: bump_version_alpha("patch"))
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""版本号管理工具主函数."""
runner = px.CliRunner(
strategy="thread",
description="BumpVersion - 版本号自动管理工具",
graphs={
# 递增补丁号 (1.0.0 -> 1.0.1)
"p": px.Graph.from_specs([bump_patch]),
# 递增次版本号 (1.0.0 -> 1.1.0)
"m": px.Graph.from_specs([bump_minor]),
# 递增主版本号 (1.0.0 -> 2.0.0)
"M": px.Graph.from_specs([bump_major]),
# 递增补丁号并创建标签
"pt": px.Graph.from_specs([bump_patch_tag]),
# 递增次版本号并创建标签
"mt": px.Graph.from_specs([bump_minor_tag]),
# 递增主版本号并创建标签
"Mt": px.Graph.from_specs([bump_major_tag]),
# 递增补丁号并添加 alpha 预发布标识
"pa": px.Graph.from_specs([bump_patch_alpha]),
},
)
runner.run_cli()
+27
View File
@@ -0,0 +1,27 @@
"""清屏工具.
跨平台清屏工具, 支持终端和控制台清屏.
"""
from __future__ import annotations
import subprocess
import pyflowx as px
from pyflowx.conditions import Constants
def clear_screen() -> None:
"""使用系统命令清屏."""
if Constants.IS_WINDOWS:
subprocess.run(["cmd", "/c", "cls"], check=False)
else:
subprocess.run(["clear"], check=False)
print("\033[2J\033[H", end="")
def main() -> None:
"""清屏工具主函数."""
graph = px.Graph.from_specs([px.TaskSpec("clearscreen", fn=clear_screen)])
px.run(graph, strategy="thread")
File diff suppressed because it is too large Load Diff
+122
View File
@@ -0,0 +1,122 @@
"""Python 环境配置工具.
用于设置 pip 镜像源, 支持清华和阿里云等国内镜像源,
同时配置 UV 和 Conda 的镜像源.
"""
from __future__ import annotations
import argparse
import os
from pathlib import Path
import pyflowx as px
from pyflowx.conditions import Constants
# ============================================================================
# 配置
# ============================================================================
PIP_INDEX_URLS: dict[str, str] = {
"tsinghua": "https://pypi.tuna.tsinghua.edu.cn/simple",
"aliyun": "https://mirrors.aliyun.com/pypi/simple/",
}
PIP_TRUSTED_HOSTS: dict[str, str] = {
"tsinghua": "pypi.tuna.tsinghua.edu.cn",
"aliyun": "mirrors.aliyun.com",
}
UV_INDEX_URL: str = "https://mirrors.aliyun.com/pypi/simple/"
UV_PYTHON_INSTALL_MIRROR: str = "https://registry.npmmirror.com/-/binary/python-build-standalone"
CONDA_MIRROR_URLS: dict[str, list[str]] = {
"tsinghua": [
"https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main/",
"https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free/",
"https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/",
],
"aliyun": [
"https://mirrors.aliyun.com/anaconda/pkgs/main/",
"https://mirrors.aliyun.com/anaconda/pkgs/free/",
"https://mirrors.aliyun.com/anaconda/cloud/conda-forge/",
],
}
# ============================================================================
# 辅助函数
# ============================================================================
def set_pip_mirror(mirror: str = "tsinghua", token: str | None = None) -> None:
"""设置 pip 镜像源.
Parameters
----------
mirror : str
镜像源名称: tsinghua, aliyun
token : str | None
PyPI token for publishing
"""
index_url = PIP_INDEX_URLS.get(mirror, PIP_INDEX_URLS["tsinghua"])
trusted_host = PIP_TRUSTED_HOSTS.get(mirror, "")
# 设置环境变量
os.environ["PIP_INDEX_URL"] = index_url
os.environ["UV_INDEX_URL"] = UV_INDEX_URL
os.environ["UV_DEFAULT_INDEX"] = UV_INDEX_URL
os.environ["UV_PYTHON_INSTALL_MIRROR"] = UV_PYTHON_INSTALL_MIRROR
# 写入 pip 配置文件
pip_dir = Path.home() / "pip"
pip_dir.mkdir(exist_ok=True)
pip_conf = pip_dir / ("pip.ini" if Constants.IS_WINDOWS else "pip.conf")
pip_conf.write_text(f"[global]\nindex-url = {index_url}\n[install]\ntrusted-host = {trusted_host}\n")
# 写入 conda 配置文件
condarc = Path.home() / ".condarc"
conda_urls = CONDA_MIRROR_URLS.get(mirror, CONDA_MIRROR_URLS["tsinghua"])
condarc.write_text(
"show_channel_urls: true\nchannels:\n" + "\n".join(f" - {url}" for url in conda_urls) + "\n - defaults\n"
)
# 写入 pypirc 配置文件 (如果有 token)
if token:
pypirc = Path.home() / ".pypirc"
pypirc.write_text(
f"[pypi]\nrepository: https://upload.pypi.org/legacy/\nusername: __token__\npassword: {token}\n"
)
print(f"已设置 pip 镜像源: {mirror} ({index_url})")
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""Python 环境配置工具主函数."""
parser = argparse.ArgumentParser(
description="EnvPy - Python 环境配置工具",
usage="envpy <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 设置镜像源命令
mirror_parser = subparsers.add_parser("mirror", help="设置 pip 镜像源")
mirror_parser.add_argument("name", choices=["tsinghua", "aliyun"], help="镜像源名称")
mirror_parser.add_argument("--token", type=str, help="PyPI token for publishing")
args = parser.parse_args()
if args.command == "mirror":
graph = px.Graph.from_specs(
[px.TaskSpec("set_pip_mirror", fn=set_pip_mirror, args=(args.name,), kwargs={"token": args.token})]
)
else:
parser.print_help()
return
px.run(graph, strategy="thread")
+57
View File
@@ -0,0 +1,57 @@
"""PyQt 环境配置工具.
用于设置 PyQt 相关环境变量, 安装依赖环境.
"""
from __future__ import annotations
import pyflowx as px
from pyflowx.conditions import Constants
QT_LIBS: list[str] = [
"build-essential",
"libgl1",
"libegl1",
"libglib2.0-0",
"libfontconfig1",
"libfreetype6",
"libxkbcommon0",
"libdbus-1-3",
"libxcb-xinerama0",
"libxcb-icccm4",
"libxcb-image0",
"libxcb-keysyms1",
"libxcb-randr0",
"libxcb-render-util0",
"libxcb-shape0",
"libxcb-xfixes0",
"libxcb-cursor0",
]
CHINESE_FONTS: list[str] = [
"fonts-noto-cjk",
"fonts-wqy-microhei",
"fonts-wqy-zenhei",
"fonts-noto-color-emoji",
]
def main() -> None:
"""PyQt 环境配置工具主函数."""
graph = px.Graph.from_specs(
[
px.TaskSpec(
"envqt_install",
cmd=["sudo", "apt", "install", "-y", *QT_LIBS],
conditions=(lambda: Constants.IS_LINUX,),
verbose=True,
),
px.TaskSpec(
"envqt_fonts",
cmd=["sudo", "apt", "install", "-y", *CHINESE_FONTS],
conditions=(lambda: Constants.IS_LINUX,),
verbose=True,
),
],
)
px.run(graph, strategy="thread", verbose=True)
+150
View File
@@ -0,0 +1,150 @@
"""Rust 环境配置工具.
配置 Rustup 和 Cargo 的国内镜像源,
加速 Rust 工具链和依赖包的下载.
"""
from __future__ import annotations
import argparse
import os
import subprocess
from pathlib import Path
from typing import Literal, get_args
import pyflowx as px
# ============================================================================
# 配置
# ============================================================================
RUSTUP_MIRRORS: dict[str, dict[str, str]] = {
"aliyun": {
"RUSTUP_DIST_SERVER": "https://mirrors.aliyun.com/rustup",
"RUSTUP_UPDATE_ROOT": "https://mirrors.aliyun.com/rustup/rustup",
"TOML_REGISTRY": "https://mirrors.aliyun.com/crates.io-index/",
},
"ustc": {
"RUSTUP_DIST_SERVER": "https://mirrors.ustc.edu.cn/rust-static",
"RUSTUP_UPDATE_ROOT": "https://mirrors.ustc.edu.cn/rust-static/rustup",
"TOML_REGISTRY": "https://mirrors.ustc.edu.cn/crates.io-index/",
},
"tsinghua": {
"RUSTUP_DIST_SERVER": "https://mirrors.tuna.tsinghua.edu.cn/rustup",
"RUSTUP_UPDATE_ROOT": "https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup",
"TOML_REGISTRY": "https://mirrors.tuna.tsinghua.edu.cn/crates.io-index/",
},
}
UsableRustVersion = Literal["stable", "nightly", "beta"]
UsableMirror = Literal["aliyun", "ustc", "tsinghua"]
DEFAULT_RUST_VERSION: str = "stable"
DEFAULT_MIRROR: UsableMirror = "tsinghua"
# ============================================================================
# 辅助函数
# ============================================================================
def set_rust_mirror(mirror: UsableMirror = DEFAULT_MIRROR) -> None:
"""设置 Rust 镜像源.
Parameters
----------
mirror : str
镜像源名称: aliyun, ustc, tsinghua
"""
mirror_dict = RUSTUP_MIRRORS.get(mirror, RUSTUP_MIRRORS[DEFAULT_MIRROR])
server = mirror_dict["RUSTUP_DIST_SERVER"]
update_root = mirror_dict["RUSTUP_UPDATE_ROOT"]
toml_registry = mirror_dict["TOML_REGISTRY"]
# 设置环境变量
os.environ["RUSTUP_DIST_SERVER"] = server
os.environ["RUSTUP_UPDATE_ROOT"] = update_root
# 写入 cargo 配置
cargo_dir = Path.home() / ".cargo"
cargo_dir.mkdir(exist_ok=True)
cargo_config = cargo_dir / "config.toml"
cargo_config.write_text(
f"""[source.crates-io]
replace-with = '{mirror}'
[source.{mirror}]
registry = "sparse+{toml_registry}"
[registries.{mirror}]
index = "sparse+{toml_registry}"
"""
)
print(f"已设置 Rust 镜像源: {mirror}")
def install_rust(version: UsableRustVersion = DEFAULT_RUST_VERSION) -> None:
"""安装 Rust 工具链.
Parameters
----------
version : str
Rust 版本: stable, nightly, beta
"""
try:
subprocess.run(["rustup", "toolchain", "install", version], check=True)
print(f"已安装 Rust {version}")
except FileNotFoundError:
print("未找到 rustup,请先安装 Rust: https://rustup.rs")
raise
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""Rust 环境配置工具主函数."""
parser = argparse.ArgumentParser(
description="EnvRs - Rust 环境配置工具",
usage="envrs <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 设置镜像源命令
mirror_parser = subparsers.add_parser("mirror", help="设置 Rust 镜像源")
mirror_parser.add_argument(
"name",
nargs="?",
default=DEFAULT_MIRROR,
choices=get_args(UsableMirror),
help=f"镜像源名称 ({get_args(UsableMirror)})",
)
# 安装 Rust 命令
install_parser = subparsers.add_parser("install", help="安装 Rust 工具链")
install_parser.add_argument(
"version",
nargs="?",
default=DEFAULT_RUST_VERSION,
choices=get_args(UsableRustVersion),
help=f"Rust 版本 ({get_args(UsableRustVersion)})",
)
args = parser.parse_args()
if args.command == "mirror":
graph = px.Graph.from_specs(
[px.TaskSpec("set_rust_mirror", fn=set_rust_mirror, args=(args.name,), verbose=True)]
)
elif args.command == "install":
graph = px.Graph.from_specs(
[px.TaskSpec("install_rust", cmd=["rustup", "toolchain", "install", args.version], verbose=True)]
)
else:
parser.print_help()
return
px.run(graph, strategy="thread", verbose=True)
+141
View File
@@ -0,0 +1,141 @@
"""文件日期处理工具.
自动检测文件名的日期前缀,
并根据文件的实际创建或修改时间重命名文件.
"""
from __future__ import annotations
import argparse
import re
import time
from pathlib import Path
import pyflowx as px
# ============================================================================
# 配置
# ============================================================================
DATE_PATTERN = re.compile(r"(20|19)\d{2}[-_#.~]?((0[1-9])|(1[012]))[-_#.~]?((0[1-9])|([12]\d)|(3[01]))[-_#.~]?")
SEP = "_"
# ============================================================================
# 辅助函数
# ============================================================================
def get_file_timestamp(filepath: Path) -> str:
"""获取文件时间戳."""
modified_time = filepath.stat().st_mtime
created_time = filepath.stat().st_ctime
return time.strftime("%Y%m%d", time.localtime(max((modified_time, created_time))))
def remove_date_prefix(filepath: Path) -> Path:
"""移除文件日期前缀."""
stem = filepath.stem
new_stem = DATE_PATTERN.sub("", stem)
if new_stem != stem:
new_path = filepath.with_name(new_stem + filepath.suffix)
filepath.rename(new_path)
return new_path
return filepath
def add_date_prefix(filepath: Path) -> Path:
"""添加文件日期前缀."""
timestamp = get_file_timestamp(filepath)
stem = filepath.stem
new_stem = f"{timestamp}{SEP}{stem}"
new_path = filepath.with_name(new_stem + filepath.suffix)
if new_path != filepath:
filepath.rename(new_path)
return new_path
return filepath
def process_file_date(filepath: Path, clear: bool = False) -> None:
"""处理单个文件的日期前缀.
Parameters
----------
filepath : Path
文件路径
clear : bool
是否清除日期前缀
"""
if clear:
remove_date_prefix(filepath)
else:
# 先移除旧日期前缀,再添加新日期前缀
new_path = remove_date_prefix(filepath)
add_date_prefix(new_path)
def process_files_date(targets: list[Path], clear: bool = False) -> None:
"""批量处理文件日期前缀.
Parameters
----------
targets : list[Path]
文件路径列表
clear : bool
是否清除日期前缀
"""
for target in targets:
if target.exists() and not target.name.startswith("."):
process_file_date(target, clear)
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""文件日期处理工具主函数."""
parser = argparse.ArgumentParser(
description="FileDate - 文件日期处理工具",
usage="filedate <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 添加日期前缀命令
add_parser = subparsers.add_parser("add", help="添加日期前缀")
add_parser.add_argument("files", nargs="+", help="文件路径")
# 清除日期前缀命令
clear_parser = subparsers.add_parser("clear", help="清除日期前缀")
clear_parser.add_argument("files", nargs="+", help="文件路径")
args = parser.parse_args()
if args.command == "add":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"process_files_date",
fn=process_files_date,
args=([Path(f) for f in args.files],),
kwargs={"clear": False},
)
]
)
elif args.command == "clear":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"process_files_date",
fn=process_files_date,
args=([Path(f) for f in args.files],),
kwargs={"clear": True},
)
]
)
else:
parser.print_help()
return
px.run(graph, strategy="thread")
+140
View File
@@ -0,0 +1,140 @@
"""文件等级重命名工具.
根据文件等级配置自动重命名文件,
支持多种等级标识和括号格式.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
# ============================================================================
# 配置
# ============================================================================
LEVELS: dict[str, str] = {
"0": "",
"1": "PUB,NOR",
"2": "INT",
"3": "CON",
"4": "CLA",
}
BRACKETS: tuple[str, str] = (" ([_(【-", " )]_)】")
# ============================================================================
# 辅助函数
# ============================================================================
def remove_marks(stem: str, marks: list[str]) -> str:
"""从文件名主干中移除所有标记."""
left_brackets, right_brackets = BRACKETS
for mark in marks:
pos = 0
while True:
pos = stem.find(mark, pos)
if pos == -1:
break
b, e = pos - 1, pos + len(mark)
if b >= 0 and e < len(stem) and stem[b] in left_brackets and stem[e] in right_brackets:
stem = stem[:b] + stem[e + 1 :]
else:
pos = e
return stem
def process_file_level(filepath: Path, level: int = 0) -> None:
"""处理单个文件的等级标记.
Parameters
----------
filepath : Path
文件路径
level : int
文件等级 (0-4), 0 用于清除等级
"""
if not (0 <= level < len(LEVELS)):
print(f"无效的等级 {level}, 必须在 0 和 {len(LEVELS) - 1} 之间")
return
if not filepath.exists():
print(f"文件不存在: {filepath}")
return
filestem = filepath.stem
original_stem = filestem
# 移除所有等级标记
for level_names in LEVELS.values():
if level_names:
filestem = remove_marks(filestem, level_names.split(","))
# 移除数字标记
for digit in map(str, range(1, 10)):
filestem = remove_marks(filestem, [digit])
# 添加等级标记
if level > 0:
levelstr = LEVELS.get(str(level), "").split(",")[0]
if levelstr:
filestem = f"{filestem}({levelstr})"
# 重命名文件
if filestem != original_stem:
new_path = filepath.with_name(filestem + filepath.suffix)
filepath.rename(new_path)
print(f"重命名: {filepath} -> {new_path}")
def process_files_level(targets: list[Path], level: int = 0) -> None:
"""批量处理文件等级标记.
Parameters
----------
targets : list[Path]
文件路径列表
level : int
文件等级 (0-4)
"""
for target in targets:
process_file_level(target, level)
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""文件等级重命名工具主函数."""
parser = argparse.ArgumentParser(
description="FileLevel - 文件等级重命名工具",
usage="filelevel <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 设置等级命令
level_parser = subparsers.add_parser("set", help="设置文件等级")
level_parser.add_argument("files", nargs="+", help="文件路径")
level_parser.add_argument("--level", type=int, choices=[0, 1, 2, 3, 4], required=True, help="文件等级 (0-4)")
args = parser.parse_args()
if args.command == "set":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"process_files_level", fn=process_files_level, args=([Path(f) for f in args.files], args.level)
)
]
)
else:
parser.print_help()
return
px.run(graph, strategy="thread")
+94
View File
@@ -0,0 +1,94 @@
"""文件夹备份工具.
备份文件和文件夹为 zip 文件,
自动删除超过最大数量的旧备份文件.
"""
from __future__ import annotations
import time
import zipfile
from pathlib import Path
import pyflowx as px
# ============================================================================
# 辅助函数
# ============================================================================
def remove_dump(src: Path, dst: Path, max_zip: int) -> None:
"""递归删除旧的备份 zip 文件."""
zip_paths = [filepath for filepath in dst.rglob("*.zip") if src.stem in str(filepath)]
zip_files = sorted(zip_paths, key=lambda fn: str(fn)[-19:-4])
if len(zip_files) > max_zip:
zip_files[0].unlink()
remove_dump(src, dst, max_zip)
def zip_target(src: Path, dst: Path, max_zip: int) -> None:
"""将单个文件或文件夹压缩为 zip 文件."""
files = [str(_) for _ in src.rglob("*")]
timestamp = time.strftime("_%Y%m%d_%H%M%S")
target_path = dst / (src.stem + timestamp + ".zip")
with zipfile.ZipFile(target_path, "w") as zip_file:
for file in files:
zip_file.write(file, arcname=file.replace(str(src.parent), ""))
remove_dump(src, dst, max_zip)
print(f"备份完成: {target_path}")
def backup_folder(src: str, dst: str, max_zip: int = 5) -> None:
"""备份文件夹.
Parameters
----------
src : str
源文件夹路径
dst : str
目标文件夹路径
max_zip : int
最大备份数量
"""
src_path = Path(src)
dst_path = Path(dst)
if not src_path.exists():
print(f"源文件夹不存在: {src_path}")
return
if not dst_path.exists():
dst_path.mkdir(parents=True, exist_ok=True)
print(f"创建目标文件夹: {dst_path}")
zip_target(src_path, dst_path, max_zip)
# ============================================================================
# TaskSpec 定义
# ============================================================================
folderback_default: px.TaskSpec = px.TaskSpec(
"folderback_default",
fn=lambda: backup_folder(".", "./backup", 5),
)
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""文件夹备份工具主函数."""
runner = px.CliRunner(
strategy="thread",
description="FolderBack - 文件夹备份工具",
graphs={
# 备份当前目录到 ./backup
"b": px.Graph.from_specs([folderback_default]),
},
)
runner.run_cli()
+82
View File
@@ -0,0 +1,82 @@
"""文件夹压缩工具.
压缩目录下的所有文件/文件夹为 zip 文件,
默认压缩当前目录下的所有子文件夹.
"""
from __future__ import annotations
import shutil
from pathlib import Path
import pyflowx as px
# ============================================================================
# 配置
# ============================================================================
IGNORE_DIRS: list[str] = [".git", ".idea", ".vscode", "__pycache__"]
IGNORE_FILES: list[str] = [".gitignore"]
IGNORE: list[str] = [*IGNORE_DIRS, *IGNORE_FILES]
IGNORE_EXT: list[str] = [".zip", ".rar", ".7z", ".tar", ".gz"]
# ============================================================================
# 辅助函数
# ============================================================================
def archive_folder(folder: Path) -> None:
"""压缩单个文件夹."""
shutil.make_archive(
str(folder.with_name(folder.name)),
format="zip",
base_dir=folder,
)
print(f"压缩完成: {folder.name}.zip")
def zip_folders(cwd: str = ".") -> None:
"""压缩目录下的所有文件夹.
Parameters
----------
cwd : str
工作目录
"""
cwd_path = Path(cwd)
if not cwd_path.exists():
print(f"目录不存在: {cwd_path}")
return
dirs: list[Path] = [
e for e in cwd_path.iterdir() if e.is_dir() and e.name not in IGNORE_DIRS and e.suffix not in IGNORE_EXT
]
for dir_path in dirs:
archive_folder(dir_path)
# ============================================================================
# TaskSpec 定义
# ============================================================================
folderzip_default: px.TaskSpec = px.TaskSpec("folderzip_default", fn=lambda: zip_folders("."))
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""文件夹压缩工具主函数."""
runner = px.CliRunner(
strategy="thread",
description="FolderZip - 文件夹压缩工具",
graphs={
# 压缩当前目录下的所有文件夹
"z": px.Graph.from_specs([folderzip_default]),
},
)
runner.run_cli()
+109
View File
@@ -0,0 +1,109 @@
"""Git 工具模块.
提供 Git 仓库管理的常用操作封装,
支持初始化、提交、清理、推送等功能.
"""
from __future__ import annotations
from pathlib import Path
import pyflowx as px
EXCLUDE_DIRS = [
# 编辑器相关目录
".vscode",
".idea",
".editorconfig",
".trae",
".qoder",
# 项目相关目录
".venv",
".git",
".tox",
".pytest_cache",
"node_modules",
".ruff_cache",
]
EXCLUDE_CMDS = [arg for d in EXCLUDE_DIRS for arg in ["-e", d]]
def init_sub_dirs() -> None:
"""初始化子目录的Git仓库."""
sub_dirs = [subdir for subdir in Path.cwd().iterdir() if subdir.is_dir()]
for subdir in sub_dirs:
px.run(
px.Graph.from_specs(
[
px.TaskSpec(
"init",
cmd=["git", "init"],
conditions=[not_has_git_repo],
cwd=str(subdir),
),
px.TaskSpec("add", cmd=["git", "add", "."], depends_on=["init"], cwd=str(subdir)),
px.TaskSpec(
"commit", cmd=["git", "commit", "-m", "init commit"], depends_on=["add"], cwd=str(subdir)
),
]
),
)
isub: px.TaskSpec = px.TaskSpec("isub", fn=init_sub_dirs)
push: px.TaskSpec = px.TaskSpec("push", cmd=["git", "push"])
pull: px.TaskSpec = px.TaskSpec("pull", cmd=["git", "pull"])
kill_tgit: px.TaskSpec = px.TaskSpec("task_kill", cmd=["taskkill", "/f", "/t", "/im", "tgitcache.exe"])
def not_has_git_repo() -> bool:
"""检查当前目录没有Git仓库."""
return not Path.cwd().exists() or not (Path.cwd() / ".git").is_dir()
def has_files() -> bool:
"""检查当前目录是否有文件."""
return bool(list(Path.cwd().glob("*")))
def main() -> None:
"""Git工具主函数."""
runner = px.CliRunner(
strategy="thread",
description="Gittool - Git 执行工具.",
graphs={
# 添加并提交
"a": px.Graph.from_specs(
[
px.TaskSpec("add", cmd=["git", "add", "."], conditions=[has_files]),
px.TaskSpec("commit", cmd=["git", "commit", "-m", "chore: update"], depends_on=["add"]),
]
),
# 清理
"c": px.Graph.from_specs(
[
px.TaskSpec("clean", cmd=["git", "clean", "-xfd", *EXCLUDE_CMDS]),
px.TaskSpec("status", cmd=["git", "status", "--porcelain"], depends_on=["clean"]),
]
),
# 初始化、添加并提交
"i": px.Graph.from_specs(
[
px.TaskSpec("init", cmd=["git", "init"], conditions=[not_has_git_repo]),
px.TaskSpec("add", cmd=["git", "add", "."], depends_on=["init"], conditions=[has_files]),
px.TaskSpec(
"commit", cmd=["git", "commit", "-m", "init commit"], depends_on=["add"], conditions=[has_files]
),
]
),
# 初始化子目录
"isub": px.Graph.from_specs([isub]),
# 推送
"p": px.Graph.from_specs([push]),
# 拉取
"pl": px.Graph.from_specs([pull]),
# 重启TGit缓存
"r": px.Graph.from_specs([kill_tgit]),
},
)
runner.run_cli()
+86
View File
@@ -0,0 +1,86 @@
import argparse
import os
from pathlib import Path
from typing import Literal, get_args
import pyflowx as px
HFDownloadType = Literal["model", "dataset", "space"]
def setenvs():
"""设置 HuggingFace mirror 环境变量."""
os.environ["HF_ENDPOINT"] = "https://hf-mirror.com"
def main():
parser = argparse.ArgumentParser(description="Download a model from HuggingFace.")
parser.add_argument("dataset_name", type=str, help="HuggingFace dataset name.")
parser.add_argument(
"--type",
type=str,
nargs="?",
default="dataset",
choices=get_args(HFDownloadType),
help="HuggingFace dataset type.",
)
parser.add_argument("--use-hfd", action="store_true", help="Use HFD tool to download dataset.")
args = parser.parse_args()
if not args.dataset_name:
parser.error("dataset_name is required")
dataset_name = args.dataset_name
# 创建下载目录
download_dir = Path.cwd() / dataset_name
download_dir.mkdir(parents=True, exist_ok=True)
if args.use_hfd:
graph = px.Graph.from_specs(
[
px.TaskSpec(name="setenvs", fn=setenvs, verbose=True),
px.TaskSpec(
name="download_hfd",
cmd=["wget", "https://hf-mirror.com/hfd/hfd.sh"],
depends_on=["setenvs"],
verbose=True,
),
px.TaskSpec(
name="chmod_hfd",
cmd=["chmod", "a+x", "hfd.sh"],
depends_on=["download_hfd"],
verbose=True,
),
px.TaskSpec(
name="run_hfd",
cmd=["./hfd.sh", dataset_name, args.type],
depends_on=["chmod_hfd"],
verbose=True,
),
]
)
else:
graph = px.Graph.from_specs(
[
px.TaskSpec(name="setenvs", fn=setenvs, verbose=True),
px.TaskSpec(
name="download",
cmd=[
"uvx",
"hf",
"download",
"--repo-type",
args.type,
"--force-download",
dataset_name,
"--local-dir",
str(Path.cwd() / dataset_name),
],
depends_on=["setenvs"],
verbose=True,
),
]
)
px.run(graph, strategy="thread", verbose=True)
+174
View File
@@ -0,0 +1,174 @@
"""LS-DYNA 计算工具.
用于管理 LS-DYNA 仿真计算任务,
支持启动、监控和管理计算进程.
"""
from __future__ import annotations
import argparse
import subprocess
from pathlib import Path
import pyflowx as px
from pyflowx.conditions import Constants
# ============================================================================
# 配置
# ============================================================================
LS_DYNA_COMMANDS: dict[str, list[str]] = {
"windows": ["ls-dyna_mpp", "i=input.k", "ncpu=4"],
"linux": ["ls-dyna_mpp", "i=input.k", "ncpu=8"],
"macos": ["ls-dyna_mpp", "i=input.k", "ncpu=4"],
}
DEFAULT_INPUT_FILE: str = "input.k"
DEFAULT_NCPU: int = 4
# ============================================================================
# 辅助函数
# ============================================================================
def get_ls_dyna_command(input_file: str, ncpu: int) -> list[str]:
"""获取 LS-DYNA 命令.
Parameters
----------
input_file : str
输入文件路径
ncpu : int
CPU 核心数
Returns
-------
list[str]
LS-DYNA 命令列表
"""
if Constants.IS_WINDOWS or Constants.IS_MACOS:
return ["ls-dyna_mpp", f"i={input_file}", f"ncpu={ncpu}"]
else:
return ["ls-dyna_mpp", f"i={input_file}", f"ncpu={ncpu}"]
def run_ls_dyna(input_file: str, ncpu: int = DEFAULT_NCPU) -> None:
"""运行 LS-DYNA 计算.
Parameters
----------
input_file : str
输入文件路径
ncpu : int
CPU 核心数
"""
input_path = Path(input_file)
if not input_path.exists():
print(f"输入文件不存在: {input_path}")
return
cmd = get_ls_dyna_command(input_file, ncpu)
try:
subprocess.run(cmd, check=True)
print(f"LS-DYNA 计算完成: {input_file}")
except FileNotFoundError:
print("未找到 ls-dyna_mpp 命令")
except subprocess.CalledProcessError as e:
print(f"LS-DYNA 计算失败: {e}")
def run_ls_dyna_mpi(input_file: str, ncpu: int = DEFAULT_NCPU) -> None:
"""运行 LS-DYNA MPI 计算.
Parameters
----------
input_file : str
输入文件路径
ncpu : int
CPU 核心数
"""
input_path = Path(input_file)
if not input_path.exists():
print(f"输入文件不存在: {input_path}")
return
cmd = ["mpirun", "-np", str(ncpu), "ls-dyna_mpp", f"i={input_file}"]
try:
subprocess.run(cmd, check=True)
print(f"LS-DYNA MPI 计算完成: {input_file}")
except FileNotFoundError:
print("未找到 mpirun 或 ls-dyna_mpp 命令")
except subprocess.CalledProcessError as e:
print(f"LS-DYNA MPI 计算失败: {e}")
def check_ls_dyna_status() -> None:
"""检查 LS-DYNA 进程状态."""
try:
if Constants.IS_WINDOWS:
result = subprocess.run(
["tasklist", "/fi", "imagename eq ls-dyna_mpp.exe"],
capture_output=True,
text=True,
check=True,
)
print(result.stdout)
else:
result = subprocess.run(
["pgrep", "-f", "ls-dyna"],
capture_output=True,
text=True,
check=False,
)
if result.stdout.strip():
print(f"运行中的 LS-DYNA 进程 PID: {result.stdout.strip()}")
else:
print("没有运行中的 LS-DYNA 进程")
except subprocess.CalledProcessError as e:
print(f"检查进程状态失败: {e}")
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""LS-DYNA 计算工具主函数."""
parser = argparse.ArgumentParser(
description="LSCalc - LS-DYNA 计算工具",
usage="lscalc <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 运行计算命令
run_parser = subparsers.add_parser("run", help="运行 LS-DYNA 计算")
run_parser.add_argument("input_file", help="输入文件路径")
run_parser.add_argument("--ncpu", type=int, default=DEFAULT_NCPU, help="CPU 核心数")
# 运行 MPI 计算命令
mpi_parser = subparsers.add_parser("mpi", help="运行 LS-DYNA MPI 计算")
mpi_parser.add_argument("input_file", help="输入文件路径")
mpi_parser.add_argument("--ncpu", type=int, default=DEFAULT_NCPU, help="CPU 核心数")
# 检查进程状态命令
subparsers.add_parser("status", help="检查 LS-DYNA 进程状态")
args = parser.parse_args()
if args.command == "run":
graph = px.Graph.from_specs(
[px.TaskSpec("run_ls_dyna", fn=run_ls_dyna, args=(args.input_file,), kwargs={"ncpu": args.ncpu})]
)
elif args.command == "mpi":
graph = px.Graph.from_specs(
[px.TaskSpec("run_ls_dyna_mpi", fn=run_ls_dyna_mpi, args=(args.input_file,), kwargs={"ncpu": args.ncpu})]
)
elif args.command == "status":
graph = px.Graph.from_specs([px.TaskSpec("check_ls_dyna_status", fn=check_ls_dyna_status)])
else:
parser.print_help()
return
px.run(graph, strategy="thread")
+349
View File
@@ -0,0 +1,349 @@
"""Python 打包工具模块.
提供 Python 项目打包的常用功能封装,
支持源码打包、依赖打包、嵌入式 Python 安装等功能.
"""
from __future__ import annotations
import argparse
import shutil
import subprocess
import zipfile
from pathlib import Path
import pyflowx as px
# ============================================================================
# 配置
# ============================================================================
DEFAULT_BUILD_DIR = ".pypack"
DEFAULT_DIST_DIR = "dist"
DEFAULT_LIB_DIR = "libs"
DEFAULT_CACHE_DIR = ".cache/pypack"
IGNORE_PATTERNS = [
"__pycache__",
"*.pyc",
"*.pyo",
".git",
".venv",
".idea",
".vscode",
"*.egg-info",
"dist",
"build",
".pytest_cache",
".tox",
".mypy_cache",
]
# ============================================================================
# 辅助函数
# ============================================================================
def pack_source(project_dir: Path, output_dir: Path) -> None:
"""打包项目源码.
Parameters
----------
project_dir : Path
项目目录
output_dir : Path
输出目录
"""
output_dir.mkdir(parents=True, exist_ok=True)
# 检测项目名称
pyproject_file = project_dir / "pyproject.toml"
project_name = project_dir.name
if pyproject_file.exists():
try:
import tomllib
content = pyproject_file.read_text(encoding="utf-8")
data = tomllib.loads(content)
project_name = data.get("project", {}).get("name", project_name)
except ImportError:
pass
# 打包源码
source_dir = output_dir / "src" / project_name
source_dir.mkdir(parents=True, exist_ok=True)
# 复制文件
src_subdir = project_dir / "src"
if src_subdir.exists():
shutil.copytree(
src_subdir,
source_dir / "src",
ignore=shutil.ignore_patterns(*IGNORE_PATTERNS),
dirs_exist_ok=True,
)
else:
for item in project_dir.iterdir():
if item.name in IGNORE_PATTERNS or item.name.startswith("."):
continue
dst_item = source_dir / item.name
if item.is_dir():
shutil.copytree(
item,
dst_item,
ignore=shutil.ignore_patterns(*IGNORE_PATTERNS),
dirs_exist_ok=True,
)
else:
shutil.copy2(item, dst_item)
print(f"源码打包完成: {source_dir}")
def pack_dependencies(lib_dir: Path, dependencies: list[str]) -> None:
"""打包项目依赖.
Parameters
----------
lib_dir : Path
依赖库目录
dependencies : list[str]
依赖列表
"""
lib_dir.mkdir(parents=True, exist_ok=True)
if not dependencies:
print("没有依赖需要打包")
return
# 使用 pip 安装依赖到目标目录
cmd = [
"pip",
"install",
"--target",
str(lib_dir),
"--no-compile",
"--no-warn-script-location",
]
cmd.extend(dependencies)
subprocess.run(cmd, check=True)
print(f"依赖打包完成: {lib_dir}")
def pack_wheel(project_dir: Path, output_dir: Path) -> None:
"""打包项目为 wheel 文件.
Parameters
----------
project_dir : Path
项目目录
output_dir : Path
输出目录
"""
output_dir.mkdir(parents=True, exist_ok=True)
# 使用 pip wheel 打包
cmd = [
"pip",
"wheel",
"--no-deps",
"--wheel-dir",
str(output_dir),
str(project_dir),
]
subprocess.run(cmd, check=True)
print(f"Wheel 打包完成: {output_dir}")
def install_embed_python(version: str, output_dir: Path) -> None:
"""安装嵌入式 Python.
Parameters
----------
version : str
Python 版本 (如: 3.10, 3.11)
output_dir : Path
输出目录
"""
import platform
output_dir.mkdir(parents=True, exist_ok=True)
# 构建下载 URL
arch = platform.machine().lower()
if arch in ["x86_64", "amd64"]:
arch = "amd64"
elif arch in ["arm64", "aarch64"]:
arch = "arm64"
# 解析完整版本号
version_map = {
"3.8": "3.8.10",
"3.9": "3.9.13",
"3.10": "3.10.11",
"3.11": "3.11.9",
"3.12": "3.12.4",
}
full_version = version_map.get(version, f"{version}.0")
# Windows 嵌入式 Python 下载 URL
url = f"https://www.python.org/ftp/python/{full_version}/python-{full_version}-embed-{arch}.zip"
# 下载并解压
cache_file = Path(DEFAULT_CACHE_DIR) / f"python-{full_version}-embed-{arch}.zip"
cache_file.parent.mkdir(parents=True, exist_ok=True)
if not cache_file.exists():
print(f"正在下载嵌入式 Python {full_version}...")
import urllib.request
urllib.request.urlretrieve(url, cache_file)
print(f"下载完成: {cache_file}")
# 解压
with zipfile.ZipFile(cache_file, "r") as zf:
zf.extractall(output_dir)
print(f"嵌入式 Python 安装完成: {output_dir}")
def create_zip_package(source_dir: Path, output_file: Path) -> None:
"""创建 ZIP 打包文件.
Parameters
----------
source_dir : Path
源目录
output_file : Path
输出文件
"""
output_file.parent.mkdir(parents=True, exist_ok=True)
with zipfile.ZipFile(output_file, "w", zipfile.ZIP_DEFLATED) as zf:
for file in source_dir.rglob("*"):
if file.is_file():
arcname = file.relative_to(source_dir)
zf.write(file, arcname)
print(f"ZIP 打包完成: {output_file}")
def clean_build_dir(build_dir: Path) -> None:
"""清理构建目录.
Parameters
----------
build_dir : Path
构建目录
"""
if build_dir.exists():
shutil.rmtree(build_dir)
print(f"清理完成: {build_dir}")
else:
print(f"目录不存在: {build_dir}")
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""Python 打包工具主函数."""
parser = argparse.ArgumentParser(
description="PackTool - Python 打包工具",
usage="packtool <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 源码打包命令
src_parser = subparsers.add_parser("src", help="打包项目源码")
src_parser.add_argument("--project-dir", type=str, default=".", help="项目目录")
src_parser.add_argument("--output-dir", type=str, default=DEFAULT_BUILD_DIR, help="输出目录")
# 依赖打包命令
deps_parser = subparsers.add_parser("deps", help="打包项目依赖")
deps_parser.add_argument("--lib-dir", type=str, default=DEFAULT_LIB_DIR, help="依赖库目录")
deps_parser.add_argument("dependencies", nargs="*", help="依赖列表")
# Wheel 打包命令
wheel_parser = subparsers.add_parser("wheel", help="打包项目为 wheel 文件")
wheel_parser.add_argument("--project-dir", type=str, default=".", help="项目目录")
wheel_parser.add_argument("--output-dir", type=str, default=DEFAULT_DIST_DIR, help="输出目录")
# 嵌入式 Python 安装命令
embed_parser = subparsers.add_parser("embed", help="安装嵌入式 Python")
embed_parser.add_argument("--version", type=str, default="3.10", help="Python 版本")
embed_parser.add_argument("--output-dir", type=str, default="python", help="输出目录")
# ZIP 打包命令
zip_parser = subparsers.add_parser("zip", help="创建 ZIP 打包文件")
zip_parser.add_argument("--source-dir", type=str, default=".", help="源目录")
zip_parser.add_argument("--output-file", type=str, default="package.zip", help="输出文件")
# 清理命令
subparsers.add_parser("clean", help="清理构建目录")
args = parser.parse_args()
if args.command == "src":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"pack_source",
fn=pack_source,
args=(Path(args.project_dir), Path(args.output_dir)),
)
]
)
elif args.command == "deps":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"pack_deps",
fn=pack_dependencies,
args=(Path(args.lib_dir), args.dependencies),
)
]
)
elif args.command == "wheel":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"pack_wheel",
fn=pack_wheel,
args=(Path(args.project_dir), Path(args.output_dir)),
)
]
)
elif args.command == "embed":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"install_embed",
fn=install_embed_python,
args=(args.version, Path(args.output_dir)),
)
]
)
elif args.command == "zip":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"create_zip",
fn=create_zip_package,
args=(Path(args.source_dir), Path(args.output_file)),
)
]
)
elif args.command == "clean":
graph = px.Graph.from_specs([px.TaskSpec("clean_build", fn=clean_build_dir, args=(Path(DEFAULT_BUILD_DIR),))])
else:
parser.print_help()
return
px.run(graph, strategy="thread")
+524
View File
@@ -0,0 +1,524 @@
"""PDF 工具模块.
提供 PDF 文件操作的常用功能封装,
支持合并、拆分、压缩、加密、水印、OCR等功能.
"""
from __future__ import annotations
import argparse
from pathlib import Path
import pyflowx as px
try:
import fitz # PyMuPDF
HAS_PYMUPDF = True
except ImportError:
HAS_PYMUPDF = False
try:
import pypdf
HAS_PYPDF = True
except ImportError:
HAS_PYPDF = False
# ============================================================================
# 配置
# ============================================================================
PDF_SUFFIX = ".pdf"
DEFAULT_QUALITY = 75
DEFAULT_PASSWORD = ""
# ============================================================================
# 辅助函数
# ============================================================================
def pdf_merge(input_paths: list[Path], output_path: Path) -> None:
"""合并多个 PDF 文件."""
if not HAS_PYPDF:
print("未安装 pypdf 库,请安装: pip install pypdf")
return
writer = pypdf.PdfWriter()
for input_path in input_paths:
if input_path.exists():
reader = pypdf.PdfReader(str(input_path))
for page in reader.pages:
writer.add_page(page)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "wb") as f:
writer.write(f)
print(f"合并完成: {output_path}")
def pdf_split(input_path: Path, output_dir: Path) -> None:
"""拆分 PDF 文件为单页."""
if not HAS_PYPDF:
print("未安装 pypdf 库,请安装: pip install pypdf")
return
reader = pypdf.PdfReader(str(input_path))
output_dir.mkdir(parents=True, exist_ok=True)
for i, page in enumerate(reader.pages):
writer = pypdf.PdfWriter()
writer.add_page(page)
output_file = output_dir / f"{input_path.stem}_page_{i + 1}.pdf"
with open(output_file, "wb") as f:
writer.write(f)
print(f"拆分完成: {output_dir}")
def pdf_compress(input_path: Path, output_path: Path) -> None:
"""压缩 PDF 文件."""
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
output_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(output_path), garbage=4, deflate=True, clean=True)
doc.close()
original_size = input_path.stat().st_size
new_size = output_path.stat().st_size
ratio = (1 - new_size / original_size) * 100
print(f"压缩完成: {output_path} (缩小 {ratio:.1f}%)")
def pdf_encrypt(input_path: Path, output_path: Path, password: str) -> None:
"""加密 PDF 文件."""
if not HAS_PYPDF:
print("未安装 pypdf 库,请安装: pip install pypdf")
return
reader = pypdf.PdfReader(str(input_path))
writer = pypdf.PdfWriter()
for page in reader.pages:
writer.add_page(page)
writer.encrypt(user_password=password, owner_password=password, use_128bit=True)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "wb") as f:
writer.write(f)
print(f"加密完成: {output_path}")
def pdf_decrypt(input_path: Path, output_path: Path, password: str) -> None:
"""解密 PDF 文件."""
if not HAS_PYPDF:
print("未安装 pypdf 库,请安装: pip install pypdf")
return
reader = pypdf.PdfReader(str(input_path))
if reader.is_encrypted:
reader.decrypt(password)
writer = pypdf.PdfWriter()
for page in reader.pages:
writer.add_page(page)
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "wb") as f:
writer.write(f)
print(f"解密完成: {output_path}")
def pdf_extract_text(input_path: Path, output_path: Path) -> None:
"""提取 PDF 文本."""
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
text = ""
for page in doc:
text += page.get_text() + "\n\n"
doc.close()
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(text, encoding="utf-8")
print(f"文本提取完成: {output_path}")
def pdf_extract_images(input_path: Path, output_dir: Path) -> None:
"""提取 PDF 图片."""
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
output_dir.mkdir(parents=True, exist_ok=True)
image_count = 0
for page_num, page in enumerate(doc):
images = page.get_images(full=True)
for img_idx, img in enumerate(images):
xref = img[0]
base_image = doc.extract_image(xref)
image_data = base_image["image"]
image_ext = base_image["ext"]
image_path = output_dir / f"page_{page_num + 1}_img_{img_idx + 1}.{image_ext}"
image_path.write_bytes(image_data)
image_count += 1
doc.close()
print(f"图片提取完成: {output_dir} (共 {image_count} 张)")
def pdf_add_watermark(input_path: Path, output_path: Path, text: str = "CONFIDENTIAL") -> None:
"""添加 PDF 水印."""
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
for page in doc:
rect = page.rect
text_width = fitz.get_text_length(text, fontsize=48)
x = (rect.width - text_width) / 2
y = rect.height / 2
page.insert_text((x, y), text, fontsize=48, rotate=45, color=(0, 0, 0))
output_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(output_path))
doc.close()
print(f"水印添加完成: {output_path}")
def pdf_rotate(input_path: Path, output_path: Path, rotation: int = 90) -> None:
"""旋转 PDF 页面."""
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
for page in doc:
page.set_rotation(rotation)
output_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(output_path))
doc.close()
print(f"旋转完成: {output_path}")
def pdf_crop(input_path: Path, output_path: Path, margins: tuple[int, int, int, int]) -> None:
"""裁剪 PDF 页面."""
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
left, top, right, bottom = margins
for page in doc:
rect = page.rect
new_rect = fitz.Rect(
rect.x0 + left,
rect.y0 + top,
rect.x1 - right,
rect.y1 - bottom,
)
page.set_cropbox(new_rect)
output_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(output_path))
doc.close()
print(f"裁剪完成: {output_path}")
def pdf_info(input_path: Path) -> None:
"""显示 PDF 信息."""
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
print(f"文件: {input_path}")
print(f"页数: {doc.page_count}")
print(f"标题: {doc.metadata.get('title', 'N/A')}")
print(f"作者: {doc.metadata.get('author', 'N/A')}")
print(f"创建日期: {doc.metadata.get('creationDate', 'N/A')}")
print(f"修改日期: {doc.metadata.get('modDate', 'N/A')}")
print(f"文件大小: {input_path.stat().st_size / 1024:.1f} KB")
doc.close()
def pdf_ocr(input_path: Path, output_path: Path, lang: str = "chi_sim+eng") -> None:
"""PDF OCR 识别."""
try:
import pytesseract
from PIL import Image
except ImportError:
print("未安装 OCR 相关库,请安装: pip install pytesseract pillow")
return
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
new_doc = fitz.open()
for page in doc:
pix = page.get_pixmap()
img = Image.frombytes("RGB", (pix.width, pix.height), pix.samples)
ocr_text = pytesseract.image_to_string(img, lang=lang)
new_page = new_doc.new_page(width=page.rect.width, height=page.rect.height)
new_page.insert_image(new_page.rect, pixmap=pix)
text_rect = fitz.Rect(0, 0, page.rect.width, page.rect.height)
new_page.insert_textbox(text_rect, ocr_text)
output_path.parent.mkdir(parents=True, exist_ok=True)
new_doc.save(str(output_path))
new_doc.close()
doc.close()
print(f"OCR 识别完成: {output_path}")
def pdf_reorder(input_path: Path, output_path: Path, order: list[int]) -> None:
"""重排 PDF 页面顺序."""
if not HAS_PYPDF:
print("未安装 pypdf 库,请安装: pip install pypdf")
return
reader = pypdf.PdfReader(str(input_path))
writer = pypdf.PdfWriter()
for page_num in order:
if 0 <= page_num < len(reader.pages):
writer.add_page(reader.pages[page_num])
output_path.parent.mkdir(parents=True, exist_ok=True)
with open(output_path, "wb") as f:
writer.write(f)
print(f"重排完成: {output_path}")
def pdf_to_images(input_path: Path, output_dir: Path, dpi: int = 300) -> None:
"""PDF 转图片."""
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
output_dir.mkdir(parents=True, exist_ok=True)
for page_num, page in enumerate(doc):
pix = page.get_pixmap(dpi=dpi)
image_path = output_dir / f"{input_path.stem}_page_{page_num + 1}.png"
pix.save(str(image_path))
doc.close()
print(f"转换完成: {output_dir}")
def pdf_repair(input_path: Path, output_path: Path) -> None:
"""修复 PDF 文件."""
if not HAS_PYMUPDF:
print("未安装 PyMuPDF 库,请安装: pip install PyMuPDF")
return
doc = fitz.open(str(input_path))
output_path.parent.mkdir(parents=True, exist_ok=True)
doc.save(str(output_path), garbage=4, deflate=True, clean=True)
doc.close()
print(f"修复完成: {output_path}")
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None: # noqa: PLR0912
"""PDF 工具主函数."""
parser = argparse.ArgumentParser(
description="PDFTool - PDF 文件工具集",
usage="pdftool <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 合并 PDF 命令
merge_parser = subparsers.add_parser("m", help="合并 PDF 文件")
merge_parser.add_argument("inputs", nargs="+", help="输入 PDF 文件路径")
merge_parser.add_argument("--output", type=str, default="merged.pdf", help="输出文件路径")
# 拆分 PDF 命令
split_parser = subparsers.add_parser("s", help="拆分 PDF 文件为单页")
split_parser.add_argument("input", help="输入 PDF 文件路径")
split_parser.add_argument("--output-dir", type=str, default="split", help="输出目录")
# 压缩 PDF 命令
compress_parser = subparsers.add_parser("c", help="压缩 PDF 文件")
compress_parser.add_argument("input", help="输入 PDF 文件路径")
compress_parser.add_argument("--output", type=str, default="compressed.pdf", help="输出文件路径")
# 加密 PDF 命令
encrypt_parser = subparsers.add_parser("e", help="加密 PDF 文件")
encrypt_parser.add_argument("input", help="输入 PDF 文件路径")
encrypt_parser.add_argument("--output", type=str, default="encrypted.pdf", help="输出文件路径")
encrypt_parser.add_argument("--password", type=str, required=True, help="密码")
# 解密 PDF 命令
decrypt_parser = subparsers.add_parser("d", help="解密 PDF 文件")
decrypt_parser.add_argument("input", help="输入 PDF 文件路径")
decrypt_parser.add_argument("--output", type=str, default="decrypted.pdf", help="输出文件路径")
decrypt_parser.add_argument("--password", type=str, required=True, help="密码")
# 提取文本命令
extract_text_parser = subparsers.add_parser("xt", help="提取 PDF 文本")
extract_text_parser.add_argument("input", help="输入 PDF 文件路径")
extract_text_parser.add_argument("--output", type=str, default="output.txt", help="输出文件路径")
# 提取图片命令
extract_images_parser = subparsers.add_parser("xi", help="提取 PDF 图片")
extract_images_parser.add_argument("input", help="输入 PDF 文件路径")
extract_images_parser.add_argument("--output-dir", type=str, default="images", help="输出目录")
# 添加水印命令
watermark_parser = subparsers.add_parser("w", help="添加 PDF 水印")
watermark_parser.add_argument("input", help="输入 PDF 文件路径")
watermark_parser.add_argument("--output", type=str, default="watermarked.pdf", help="输出文件路径")
watermark_parser.add_argument("--text", type=str, default="CONFIDENTIAL", help="水印文本")
# 旋转 PDF 命令
rotate_parser = subparsers.add_parser("r", help="旋转 PDF 页面")
rotate_parser.add_argument("input", help="输入 PDF 文件路径")
rotate_parser.add_argument("--output", type=str, default="rotated.pdf", help="输出文件路径")
rotate_parser.add_argument("--rotation", type=int, default=90, help="旋转角度 (90, 180, 270)")
# 裁剪 PDF 命令
crop_parser = subparsers.add_parser("crop", help="裁剪 PDF 页面")
crop_parser.add_argument("input", help="输入 PDF 文件路径")
crop_parser.add_argument("--output", type=str, default="cropped.pdf", help="输出文件路径")
crop_parser.add_argument("--left", type=int, default=10, help="左边裁剪")
crop_parser.add_argument("--top", type=int, default=10, help="顶部裁剪")
crop_parser.add_argument("--right", type=int, default=10, help="右边裁剪")
crop_parser.add_argument("--bottom", type=int, default=10, help="底部裁剪")
# 显示信息命令
info_parser = subparsers.add_parser("i", help="显示 PDF 信息")
info_parser.add_argument("input", help="输入 PDF 文件路径")
# OCR 识别命令
ocr_parser = subparsers.add_parser("ocr", help="PDF OCR 识别")
ocr_parser.add_argument("input", help="输入 PDF 文件路径")
ocr_parser.add_argument("--output", type=str, default="ocr.pdf", help="输出文件路径")
ocr_parser.add_argument("--lang", type=str, default="chi_sim+eng", help="OCR 语言")
# 转换图片命令
to_images_parser = subparsers.add_parser("img", help="PDF 转图片")
to_images_parser.add_argument("input", help="输入 PDF 文件路径")
to_images_parser.add_argument("--output-dir", type=str, default="images", help="输出目录")
to_images_parser.add_argument("--dpi", type=int, default=300, help="图片 DPI")
# 修复 PDF 命令
repair_parser = subparsers.add_parser("repair", help="修复 PDF 文件")
repair_parser.add_argument("input", help="输入 PDF 文件路径")
repair_parser.add_argument("--output", type=str, default="repaired.pdf", help="输出文件路径")
args = parser.parse_args()
if args.command == "m":
graph = px.Graph.from_specs(
[px.TaskSpec("pdf_merge", fn=pdf_merge, args=([Path(p) for p in args.inputs], Path(args.output)))]
)
elif args.command == "s":
graph = px.Graph.from_specs(
[px.TaskSpec("pdf_split", fn=pdf_split, args=(Path(args.input), Path(args.output_dir)))]
)
elif args.command == "c":
graph = px.Graph.from_specs(
[px.TaskSpec("pdf_compress", fn=pdf_compress, args=(Path(args.input), Path(args.output)))]
)
elif args.command == "e":
graph = px.Graph.from_specs(
[px.TaskSpec("pdf_encrypt", fn=pdf_encrypt, args=(Path(args.input), Path(args.output), args.password))]
)
elif args.command == "d":
graph = px.Graph.from_specs(
[px.TaskSpec("pdf_decrypt", fn=pdf_decrypt, args=(Path(args.input), Path(args.output), args.password))]
)
elif args.command == "xt":
graph = px.Graph.from_specs(
[px.TaskSpec("pdf_extract_text", fn=pdf_extract_text, args=(Path(args.input), Path(args.output)))]
)
elif args.command == "xi":
graph = px.Graph.from_specs(
[px.TaskSpec("pdf_extract_images", fn=pdf_extract_images, args=(Path(args.input), Path(args.output_dir)))]
)
elif args.command == "w":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"pdf_watermark",
fn=pdf_add_watermark,
args=(Path(args.input), Path(args.output)),
kwargs={"text": args.text},
)
]
)
elif args.command == "r":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"pdf_rotate",
fn=pdf_rotate,
args=(Path(args.input), Path(args.output)),
kwargs={"rotation": args.rotation},
)
]
)
elif args.command == "crop":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"pdf_crop",
fn=pdf_crop,
args=(Path(args.input), Path(args.output)),
kwargs={"margins": (args.left, args.top, args.right, args.bottom)},
)
]
)
elif args.command == "i":
graph = px.Graph.from_specs([px.TaskSpec("pdf_info", fn=pdf_info, args=(Path(args.input),))])
elif args.command == "ocr":
graph = px.Graph.from_specs(
[px.TaskSpec("pdf_ocr", fn=pdf_ocr, args=(Path(args.input), Path(args.output)), kwargs={"lang": args.lang})]
)
elif args.command == "img":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"pdf_to_images",
fn=pdf_to_images,
args=(Path(args.input), Path(args.output_dir)),
kwargs={"dpi": args.dpi},
)
]
)
elif args.command == "repair":
graph = px.Graph.from_specs(
[px.TaskSpec("pdf_repair", fn=pdf_repair, args=(Path(args.input), Path(args.output)))]
)
else:
parser.print_help()
return
px.run(graph, strategy="thread")
+201
View File
@@ -0,0 +1,201 @@
"""pip 包管理工具模块.
提供 pip 包管理操作的封装,
支持安装、卸载、下载等功能.
"""
from __future__ import annotations
import argparse
import fnmatch
import subprocess
from pathlib import Path
import pyflowx as px
# ============================================================================
# 配置
# ============================================================================
PACKAGE_DIR = "packages"
REQUIREMENTS_FILE = "requirements.txt"
# 受保护的包名集合
_PROTECTED_PACKAGES: frozenset[str] = frozenset(
{
"pyflowx",
"bitool",
}
)
# ============================================================================
# 辅助函数
# ============================================================================
def _get_installed_packages() -> list[str]:
"""获取当前环境中所有已安装的包名."""
try:
result = subprocess.run(
["pip", "list", "--format=freeze"],
capture_output=True,
text=True,
check=True,
)
packages: list[str] = []
for line in result.stdout.strip().split("\n"):
if line and "==" in line:
pkg_name = line.split("==")[0].strip()
packages.append(pkg_name)
except (subprocess.SubprocessError, OSError):
return []
return packages
def _expand_wildcard_packages(pattern: str) -> list[str]:
"""展开通配符模式为实际的包名列表."""
if not any(char in pattern for char in ["*", "?", "[", "]"]):
return [pattern]
installed_packages = _get_installed_packages()
matched = [pkg for pkg in installed_packages if fnmatch.fnmatchcase(pkg.lower(), pattern.lower())]
return matched
def _filter_protected_packages(packages: list[str]) -> list[str]:
"""过滤掉受保护的包名."""
safe = [p for p in packages if p.lower() not in {p.lower() for p in _PROTECTED_PACKAGES}]
filtered = [p for p in packages if p.lower() in {p.lower() for p in _PROTECTED_PACKAGES}]
if filtered:
print(f"跳过受保护的包: {', '.join(filtered)}")
return safe
def pip_uninstall(pkg_names: list[str]) -> None:
"""卸载包."""
packages_to_uninstall: list[str] = []
for pattern in pkg_names:
packages_to_uninstall.extend(_expand_wildcard_packages(pattern))
packages_to_uninstall = _filter_protected_packages(packages_to_uninstall)
if not packages_to_uninstall:
return
subprocess.run(["pip", "uninstall", "-y", *packages_to_uninstall], check=True)
def pip_reinstall(pkg_names: list[str], offline: bool = False) -> None:
"""重新安装包."""
safe_pkgs = _filter_protected_packages(pkg_names)
if not safe_pkgs:
print("所有指定的包均为受保护包, 跳过重装")
return
subprocess.run(["pip", "uninstall", "-y", *safe_pkgs], check=True)
options = ["--no-index", "--find-links", "."] if offline else []
subprocess.run(["pip", "install", *options, *safe_pkgs], check=True)
def pip_download(pkg_names: list[str], offline: bool = False) -> None:
"""下载包."""
options = ["--no-index", "--find-links", "."] if offline else []
subprocess.run(
["pip", "download", *pkg_names, *options, "-d", PACKAGE_DIR],
check=True,
)
def pip_freeze() -> None:
"""冻结依赖."""
result = subprocess.run(
["pip", "freeze", "--exclude-editable"],
capture_output=True,
text=True,
check=True,
)
Path(REQUIREMENTS_FILE).write_text(result.stdout)
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""pip 工具主函数."""
parser = argparse.ArgumentParser(
description="PipTool - pip 包管理工具",
usage="piptool <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 安装命令
install_parser = subparsers.add_parser("i", help="安装包")
install_parser.add_argument("packages", nargs="+", help="要安装的包名")
# 卸载命令
uninstall_parser = subparsers.add_parser("u", help="卸载包")
uninstall_parser.add_argument("packages", nargs="+", help="要卸载的包名 (支持通配符)")
# 重装命令
reinstall_parser = subparsers.add_parser("r", help="重新安装包")
reinstall_parser.add_argument("packages", nargs="+", help="要重装的包名")
reinstall_parser.add_argument("--offline", action="store_true", help="使用离线模式")
# 下载命令
download_parser = subparsers.add_parser("d", help="下载包")
download_parser.add_argument("packages", nargs="+", help="要下载的包名")
download_parser.add_argument("--offline", action="store_true", help="使用离线模式")
# 升级 pip 命令
subparsers.add_parser("up", help="升级 pip")
# 冻结依赖命令
subparsers.add_parser("f", help="冻结依赖到 requirements.txt")
args = parser.parse_args()
if args.command == "i":
graph = px.Graph.from_specs([px.TaskSpec("pip_install", cmd=["pip", "install", *args.packages], verbose=True)])
elif args.command == "u":
graph = px.Graph.from_specs(
[px.TaskSpec("pip_uninstall", fn=pip_uninstall, args=(args.packages,), verbose=True)]
)
elif args.command == "r":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"pip_reinstall",
fn=pip_reinstall,
args=(args.packages,),
kwargs={"offline": args.offline},
verbose=True,
)
]
)
elif args.command == "d":
graph = px.Graph.from_specs(
[
px.TaskSpec(
"pip_download",
fn=pip_download,
args=(args.packages,),
kwargs={"offline": args.offline},
verbose=True,
)
]
)
elif args.command == "up":
graph = px.Graph.from_specs(
[px.TaskSpec("pip_upgrade", cmd=["python", "-m", "pip", "install", "--upgrade", "pip"], verbose=True)]
)
elif args.command == "f":
graph = px.Graph.from_specs([px.TaskSpec("pip_freeze", fn=pip_freeze, verbose=True)])
else:
parser.print_help()
return
px.run(graph, strategy="thread")
+47 -109
View File
@@ -7,7 +7,7 @@
from __future__ import annotations
import pyflowx as px
from pyflowx.conditions import BuiltinConditions, Constants
from pyflowx.conditions import Constants
def maturin_build_cmd() -> list[str]:
@@ -18,9 +18,9 @@ def maturin_build_cmd() -> list[str]:
list[str]
完整的 maturin 构建命令列表.
"""
base_cmd = ["maturin", "build", "-r"].copy()
command = ["maturin", "build", "-r"].copy()
if Constants.IS_WINDOWS:
base_cmd.extend(
command.extend(
[
"--target",
"x86_64-win7-windows-msvc",
@@ -29,112 +29,49 @@ def maturin_build_cmd() -> list[str]:
"python3.8",
]
)
return base_cmd
return command
def check(name: str) -> px.Condition:
"""检查指定工具是否已安装.
Returns
-------
bool
如果已安装则返回 True,否则返回 False.
"""
return BuiltinConditions.HAS_INSTALLED(name)
uv_build: px.TaskSpec = px.TaskSpec("uv_build", cmd=["uv", "build"], conditions=(check("uv"),))
maturin_build: px.TaskSpec = px.TaskSpec("maturin_build", cmd=maturin_build_cmd(), conditions=(check("maturin"),))
uv_sync: px.TaskSpec = px.TaskSpec("uv_sync", cmd=["uv", "sync"], conditions=(check("uv"),))
git_clean: px.TaskSpec = px.TaskSpec("git_clean", cmd=["gitt", "c"], conditions=(check("gitt"),))
uv_build: px.TaskSpec = px.TaskSpec("uv_build", cmd=["uv", "build"])
maturin_build: px.TaskSpec = px.TaskSpec("maturin_build", cmd=maturin_build_cmd())
uv_sync: px.TaskSpec = px.TaskSpec("uv_sync", cmd=["uv", "sync"])
git_clean: px.TaskSpec = px.TaskSpec("git_clean", cmd=["gitt", "c"])
test: px.TaskSpec = px.TaskSpec(
"test",
cmd=[
"pytest",
"-m",
"not slow",
"-n",
"8",
"--dist",
"loadfile",
"--color=yes",
"--durations=10",
],
conditions=(check("pytest"),),
"test", cmd=["pytest", "-m", "not slow", "-n", "8", "--dist", "loadfile", "--color=yes", "--durations=10"]
)
test_fast: px.TaskSpec = px.TaskSpec(
"test_fast",
cmd=[
"pytest",
"-m",
"not slow",
"--dist",
"loadfile",
"--color=yes",
"--durations=10",
],
conditions=(check("pytest"),),
"test_fast", cmd=["pytest", "-m", "not slow", "--dist", "loadfile", "--color=yes", "--durations=10"]
)
test_coverage: px.TaskSpec = px.TaskSpec(
"test_coverage",
cmd=[
"pytest",
"--cov",
"-n",
"8",
"--dist",
"loadfile",
"--tb=short",
"-v",
"--color=yes",
"--durations=10",
],
conditions=(check("pytest"),),
cmd=["pytest", "--cov", "-n", "8", "--dist", "loadfile", "--tb=short", "-v", "--color=yes", "--durations=10"],
)
ruff_lint: px.TaskSpec = px.TaskSpec(
"lint",
cmd=[
"ruff",
"check",
"--fix",
"--unsafe-fixes",
],
conditions=(check("ruff"),),
)
mypy_check: px.TaskSpec = px.TaskSpec("typecheck", cmd=["mypy", "."], conditions=(check("mypy"),))
ty_check: px.TaskSpec = px.TaskSpec("ty_check", cmd=["ty", "check", "."], conditions=(check("ty"),))
doc: px.TaskSpec = px.TaskSpec(
"doc", cmd=["sphinx-build", "-b", "html", "docs", "docs/_build"], conditions=(check("sphinx-build"),)
)
hatch_publish: px.TaskSpec = px.TaskSpec("publish_python", cmd=["hatch", "publish"], conditions=(check("hatch"),))
twine_publish: px.TaskSpec = px.TaskSpec(
"twine_publish",
cmd=[
"twine",
"upload",
"--disable-progress-bar",
],
conditions=(check("twine"),),
)
tox: px.TaskSpec = px.TaskSpec("tox", cmd=["tox", "-p", "auto"], conditions=(check("tox"),))
ruff_lint: px.TaskSpec = px.TaskSpec("lint", cmd=["ruff", "check", "--fix", "--unsafe-fixes"])
ruff_format: px.TaskSpec = px.TaskSpec("format", cmd=["ruff", "format", "."], depends_on=("lint",))
typecheck: px.TaskSpec = px.TaskSpec("pyrefly_check", cmd=["pyrefly", "check", "."])
git_add_all: px.TaskSpec = px.TaskSpec("git_add_all", cmd=["git", "add", "-A"])
bump: px.TaskSpec = px.TaskSpec("bumpversion", cmd=["bumpversion", "-t"])
doc: px.TaskSpec = px.TaskSpec("doc", cmd=["sphinx-build", "-b", "html", "docs", "docs/_build"])
git_push: px.TaskSpec = px.TaskSpec("git_push", cmd=["git", "push"])
git_push_tags: px.TaskSpec = px.TaskSpec("git_push_tags", cmd=["git", "push", "--tags"])
hatch_publish: px.TaskSpec = px.TaskSpec("publish_python", cmd=["hatch", "publish"])
twine_publish: px.TaskSpec = px.TaskSpec("twine_publish", cmd=["twine", "upload", "--disable-progress-bar"])
tox: px.TaskSpec = px.TaskSpec("tox", cmd=["tox", "-p", "auto"])
def main():
"""
╔══════════════════════════════════════════════════════════╗
║ PyMake 构建工具 ║
╚══════════════════════════════════════════════════════════╝
"""pymake 构建工具.
🔨 构建命令:
pymake b - 构建 Python 主包 (uv build)
pymake bc - 构建 Rust 核心模块 (maturin build)
pymake ba - 构建所有包 (先 Rust 后 Python)
pymake ba - 构建所有包 (先 Python 后 Rust)
📦 安装命令 (开发模式):
pymake sync - 安装依赖包 (uv sync)
🧹 清理命令:
pymake c - 清理所有构建产物
pymake c - 清理所有构建产物 (gitt c)
🛠️ 开发工具:
pymake t - 运行测试 (pytest)
@@ -145,51 +82,52 @@ def main():
pymake doc - 构建文档 (sphinx)
🔬 多版本测试:
pymake tox - 多版本 Python 测试 (3.8-3.14)
pymake tox_install - 安装所有 Python 版本 (仅安装不测试)
pymake tox - 多版本 Python 测试 (tox -p auto)
📦 发布命令:
pymake pb - 发布到 PyPI (hatch publish)
pymake pba - 发布所有包 (先 Rust 后 Python)
pymake pbc - 发布 Rust 核心模块 (maturin publish)
pymake pb - 发布到 PyPI (twine + hatch)
💡 常用工作流:
1. 初始化开发环境: pymake ia
2. 日常开发: pymake lint && pymake t
3. 构建发布包: pymake ba
4. 多版本兼容性测试: pymake tox
5. 发布到 PyPI: pymake pb
6. 清理重新开始: pymake ca && pymake ia
版本管理:
pymake bump - 自动升级版本号并提交修改 (清理 + 检查 + 格式化 + git add + bumpversion)
💡 常用工作流:
1. 日常开发: pymake lint && pymake t
2. 构建发布包: pymake ba
3. 多版本兼容性测试: pymake tox
4. 发布到 PyPI: pymake pb
📝 示例:
pymake ba # 构建所有包
pymake ia # 安装开发环境
pymake sync # 安装依赖
pymake t # 运行测试
pymake tox # 多版本兼容性测试
pymake lint # 格式化代码
pymake ca # 清理所有构建产物
pymake type # 类型检查
"""
runner = px.CliRunner(
strategy="sequential",
description="PyMake - Python 构建工具 (替代 Makefile)",
description="PyMake - Python 构建工具",
graphs={
# 构建命令
"b": px.Graph.from_specs([uv_build]),
"bc": px.Graph.from_specs([maturin_build]),
"ba": px.Graph.from_specs([uv_build, maturin_build]),
"ba": px.Graph.from_specs(["b", "bc"]),
# 安装命令
"sync": px.Graph.from_specs([uv_sync]),
# 清理命令
"c": px.Graph.from_specs([git_clean]),
# 开发工具
"t": px.Graph.from_specs([test]),
"tc": px.Graph.from_specs([test, test_coverage]),
"tf": px.Graph.from_specs([test_fast]),
"lint": px.Graph.from_specs([ruff_lint]),
"type": px.Graph.from_specs([mypy_check, ty_check]),
"bump": px.Graph.from_specs(["c", "tc", git_add_all, bump]),
"cov": px.Graph.from_specs([git_clean, test_coverage]),
"doc": px.Graph.from_specs([doc]),
"lint": px.Graph.from_specs([ruff_lint, ruff_format]),
"pb": px.Graph.from_specs([twine_publish, hatch_publish]),
"t": px.Graph.from_specs([test]),
"tf": px.Graph.from_specs([test_fast]),
"tc": px.Graph.from_specs([typecheck, "lint"]),
"tox": px.Graph.from_specs([tox]),
# 发布命令
"p": px.Graph.from_specs([git_clean, git_push, git_push_tags]),
},
)
runner.run_cli()
+163
View File
@@ -0,0 +1,163 @@
"""截图工具.
跨平台截图工具, 支持全屏截图和区域截图.
"""
from __future__ import annotations
import argparse
import subprocess
from datetime import datetime
from pathlib import Path
import pyflowx as px
from pyflowx.conditions import Constants
# ============================================================================
# 辅助函数
# ============================================================================
def get_screenshot_path(filename: str | None = None) -> Path:
"""获取截图保存路径.
Parameters
----------
filename : str | None
文件名, 如果为 None 则自动生成
Returns
-------
Path
截图保存路径
"""
if filename is None:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"screenshot_{timestamp}.png"
screenshots_dir = Path.home() / "Pictures" / "screenshots"
screenshots_dir.mkdir(parents=True, exist_ok=True)
return screenshots_dir / filename
def take_screenshot_full(filename: str | None = None) -> None:
"""全屏截图.
Parameters
----------
filename : str | None
文件名
"""
output_path = get_screenshot_path(filename)
if Constants.IS_WINDOWS:
# Windows: 使用 PowerShell 截图
ps_script = f"""
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
$bounds = $screen.Bounds
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
$bitmap.Save('{output_path.as_posix()}')
$graphics.Dispose()
$bitmap.Dispose()
"""
subprocess.run(["powershell", "-Command", ps_script], check=True)
elif Constants.IS_MACOS:
# macOS: 使用 screencapture
subprocess.run(["screencapture", "-x", str(output_path)], check=True)
else:
# Linux: 使用 gnome-screenshot 或 scrot
try:
subprocess.run(["gnome-screenshot", "-f", str(output_path)], check=True)
except FileNotFoundError:
subprocess.run(["scrot", str(output_path)], check=True)
print(f"截图已保存: {output_path}")
def take_screenshot_area(filename: str | None = None) -> None:
"""区域截图.
Parameters
----------
filename : str | None
文件名
"""
output_path = get_screenshot_path(filename)
if Constants.IS_WINDOWS:
# Windows: 使用 PowerShell 截图 (需要用户选择区域)
ps_script = f"""
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.WindowState = 'Maximized'
$form.FormBorderStyle = 'None'
$form.BackColor = [System.Drawing.Color]::FromArgb(1, 0, 0)
$form.Opacity = 0.5
$form.TopMost = $true
$form.Show()
Start-Sleep -Milliseconds 100
$screen = [System.Windows.Forms.Screen]::PrimaryScreen
$bounds = $screen.Bounds
$bitmap = New-Object System.Drawing.Bitmap $bounds.Width, $bounds.Height
$graphics = [System.Drawing.Graphics]::FromImage($bitmap)
$graphics.CopyFromScreen($bounds.Location, [System.Drawing.Point]::Empty, $bounds.Size)
$form.Close()
$bitmap.Save('{output_path.as_posix()}')
$graphics.Dispose()
$bitmap.Dispose()
"""
subprocess.run(["powershell", "-Command", ps_script], check=True)
elif Constants.IS_MACOS:
# macOS: 使用 screencapture 交互模式
subprocess.run(["screencapture", "-i", str(output_path)], check=True)
else:
# Linux: 使用 gnome-screenshot 交互模式
try:
subprocess.run(["gnome-screenshot", "-a", "-f", str(output_path)], check=True)
except FileNotFoundError:
subprocess.run(["scrot", "-s", str(output_path)], check=True)
print(f"截图已保存: {output_path}")
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""截图工具主函数."""
parser = argparse.ArgumentParser(
description="Screenshot - 截图工具",
usage="screenshot <command> [options]",
)
subparsers = parser.add_subparsers(dest="command", help="可用命令")
# 全屏截图命令
full_parser = subparsers.add_parser("full", help="全屏截图")
full_parser.add_argument("--filename", type=str, help="文件名")
# 区域截图命令
area_parser = subparsers.add_parser("area", help="区域截图")
area_parser.add_argument("--filename", type=str, help="文件名")
args = parser.parse_args()
if args.command == "full":
graph = px.Graph.from_specs(
[px.TaskSpec("screenshot_full", fn=take_screenshot_full, kwargs={"filename": args.filename})]
)
elif args.command == "area":
graph = px.Graph.from_specs(
[px.TaskSpec("screenshot_area", fn=take_screenshot_area, kwargs={"filename": args.filename})]
)
else:
parser.print_help()
return
px.run(graph, strategy="thread")
+122
View File
@@ -0,0 +1,122 @@
"""SSH 密钥部署工具.
类似 ssh-copy-id, 自动将 SSH 公钥部署到远程服务器,
支持密码认证和密钥认证两种方式.
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
import pyflowx as px
# ============================================================================
# 辅助函数
# ============================================================================
def ssh_copy_id(
hostname: str,
username: str,
password: str,
port: int = 22,
keypath: str = "~/.ssh/id_rsa.pub",
timeout: int = 30,
) -> None:
"""将 SSH 公钥部署到远程服务器.
Parameters
----------
hostname : str
远程服务器主机名或 IP 地址
username : str
远程服务器用户名
password : str
远程服务器密码
port : int
SSH 端口, 默认 22
keypath : str
公钥文件路径, 默认 ~/.ssh/id_rsa.pub
timeout : int
SSH 操作超时秒数, 默认 30
"""
# 读取公钥
pub_key_path = Path(keypath).expanduser()
if not pub_key_path.exists():
print(f"公钥文件不存在: {pub_key_path}")
sys.exit(1)
pub_key = pub_key_path.read_text().strip()
# 构建部署脚本
script = f"""mkdir -p ~/.ssh && chmod 700 ~/.ssh
cd ~/.ssh && touch authorized_keys && chmod 600 authorized_keys
grep -qF '{pub_key.split()[1]}' authorized_keys 2>/dev/null || echo '{pub_key}' >> authorized_keys"""
# 使用 sshpass 执行
try:
subprocess.run(
[
"sshpass",
"-p",
password,
"ssh",
"-p",
str(port),
"-o",
"StrictHostKeyChecking=no",
"-o",
"UserKnownHostsFile=/dev/null",
"-o",
f"ConnectTimeout={timeout}",
f"{username}@{hostname}",
script,
],
check=True,
timeout=timeout,
)
print(f"SSH 密钥已部署到 {username}@{hostname}:{port}")
except FileNotFoundError:
print(f"未找到 sshpass 工具,请手动执行: ssh-copy-id -p {port} {username}@{hostname}")
sys.exit(1)
except subprocess.TimeoutExpired:
print("SSH 连接超时")
sys.exit(1)
except subprocess.CalledProcessError as e:
print(f"SSH 执行失败: {e}")
sys.exit(1)
# ============================================================================
# CLI Runner
# ============================================================================
def main() -> None:
"""SSH 密钥部署工具主函数."""
parser = argparse.ArgumentParser(
description="SSHCopyID - SSH 密钥部署工具",
usage="sshcopyid <hostname> <username> <password> [--port PORT] [--keypath KEYPATH]",
)
parser.add_argument("hostname", type=str, help="远程服务器主机名或 IP 地址")
parser.add_argument("username", type=str, help="远程服务器用户名")
parser.add_argument("password", type=str, help="远程服务器密码")
parser.add_argument("--port", type=int, default=22, help="SSH 端口 (默认: 22)")
parser.add_argument("--keypath", type=str, default="~/.ssh/id_rsa.pub", help="公钥文件路径")
parser.add_argument("--timeout", type=int, default=30, help="SSH 操作超时秒数 (默认: 30)")
args = parser.parse_args()
graph = px.Graph.from_specs(
[
px.TaskSpec(
"ssh_deploy",
fn=ssh_copy_id,
args=(args.hostname, args.username, args.password),
kwargs={"port": args.port, "keypath": args.keypath, "timeout": args.timeout},
)
]
)
px.run(graph, strategy="thread")
+40
View File
@@ -0,0 +1,40 @@
"""进程终止工具.
跨平台进程终止工具, 支持按名称终止进程.
用法: taskkill proc_name [proc_name ...]
"""
from __future__ import annotations
import argparse
import pyflowx as px
from pyflowx.conditions import Constants
def main() -> None:
"""进程终止工具主函数."""
parser = argparse.ArgumentParser(
description="TaskKill - 进程终止工具",
usage="taskkill <process_name> [process_name ...]",
)
parser.add_argument(
"process_names",
type=str,
nargs="+",
help="进程名称 (如: chrome.exe python node)",
)
args = parser.parse_args()
if Constants.IS_WINDOWS:
cmd = ["taskkill", "/f", "/im"]
else:
cmd = ["pkill", "-f"]
graph = px.Graph.from_specs(
[
px.TaskSpec(f"kill_{proc_name}", cmd=[*cmd, f"{proc_name}*"], verbose=True)
for proc_name in args.process_names
]
)
px.run(graph, strategy="thread")
+51
View File
@@ -0,0 +1,51 @@
"""命令查找工具.
跨平台查找可执行命令路径, 类似 Unix 的 which 命令.
"""
from __future__ import annotations
import argparse
import shutil
from pathlib import Path
import pyflowx as px
def which_command(command: str) -> Path | None:
"""查找命令路径.
Parameters
----------
command : str
命令名称
Returns
-------
Path | None
命令路径, 如果未找到则返回 None
"""
cmd_path = shutil.which(command)
if cmd_path:
print(f"匹配路径: - {cmd_path}")
return Path(cmd_path)
else:
print(f"{command}: 未找到")
return None
def main() -> None:
"""命令查找工具主函数."""
parser = argparse.ArgumentParser(
description="Which - 命令查找工具",
usage="which <command> [command ...]",
)
parser.add_argument(
"commands",
type=str,
nargs="+",
help="要查找的命令名称 (如: python pip node npm git uv rustc cargo)",
)
args = parser.parse_args()
graph = px.Graph.from_specs([px.TaskSpec(f"which_{cmd}", fn=which_command, args=(cmd,)) for cmd in args.commands])
px.run(graph, strategy="thread")
+8 -10
View File
@@ -6,6 +6,7 @@
from __future__ import annotations
import os
import shutil
import sys
from typing import Callable
@@ -167,7 +168,7 @@ class BuiltinConditions:
def _check() -> bool:
return not condition()
_check.__name__ = f"NOT({condition.__name__})"
_check.__name__ = f"NOT({getattr(condition, '__name__', repr(condition))})"
return _check
@staticmethod
@@ -188,7 +189,7 @@ class BuiltinConditions:
def _check() -> bool:
return all(c() for c in conditions)
names = [c.__name__ for c in conditions]
names = [getattr(c, "__name__", repr(c)) for c in conditions]
_check.__name__ = f"AND({', '.join(names)})"
return _check
@@ -210,16 +211,13 @@ class BuiltinConditions:
def _check() -> bool:
return any(c() for c in conditions)
names = [c.__name__ for c in conditions]
names = [getattr(c, "__name__", repr(c)) for c in conditions]
_check.__name__ = f"OR({', '.join(names)})"
return _check
# 导出常用条件
IS_WINDOWS = BuiltinConditions.IS_WINDOWS
IS_LINUX = BuiltinConditions.IS_LINUX
IS_MACOS = BuiltinConditions.IS_MACOS
IS_POSIX = BuiltinConditions.IS_POSIX
# 导入 os 用于环境变量检查
import os # noqa: E402
IS_WINDOWS: Callable[[], bool] = BuiltinConditions.IS_WINDOWS
IS_LINUX: Callable[[], bool] = BuiltinConditions.IS_LINUX
IS_MACOS: Callable[[], bool] = BuiltinConditions.IS_MACOS
IS_POSIX: Callable[[], bool] = BuiltinConditions.IS_POSIX
+1 -3
View File
@@ -82,9 +82,7 @@ def build_call_args(
)
# 与本任务相关的上下文子集。
dep_context: dict[str, Any] = {
name: context[name] for name in spec.depends_on if name in context
}
dep_context: dict[str, Any] = {name: context[name] for name in spec.depends_on if name in context}
# 检测静态 kwargs 与依赖名的冲突。
collisions = set(spec.kwargs) & set(dep_context)
+118 -26
View File
@@ -54,6 +54,7 @@ def _emit(
attempts=result.attempts,
error=repr(result.error) if result.error else None,
duration=result.duration,
reason=result.reason,
)
)
@@ -86,19 +87,81 @@ def _finalize_failure(
)
def _check_upstream_skipped(
spec: TaskSpec[Any],
report: RunReport | None,
) -> tuple[bool, str | None]:
"""检查上游任务是否被 SKIPPED。
Returns
-------
tuple[bool, str | None]
(是否应该跳过, 跳过原因)
"""
if report is None:
return False, None
for dep in spec.depends_on:
if dep in report.results and report.results[dep].status == TaskStatus.SKIPPED:
return True, f"上游任务 '{dep}' 被跳过"
return False, None
def _check_conditions_for_skip(
spec: TaskSpec[Any],
) -> str | None:
"""检查任务条件是否满足,返回跳过原因(如果不满足)。
Returns
-------
str | None
跳过原因,如果条件满足则返回 None
"""
if spec.should_execute():
return None
# 检查是哪个条件不满足
failed_conditions = []
for condition in spec.conditions:
try:
if not condition():
failed_conditions.append(condition.__name__ or "匿名条件")
except Exception:
failed_conditions.append(condition.__name__ or "匿名条件(执行错误)")
if failed_conditions:
return f"条件不满足: {', '.join(failed_conditions)}"
elif spec.skip_if_missing and not spec._is_cmd_available():
return f"命令不存在: {spec.cmd[0] if spec.cmd else 'unknown'}"
else:
return "条件不满足"
def _run_sync_with_retry(
spec: TaskSpec[Any],
context: Mapping[str, Any],
layer_idx: int | None,
on_event: EventCallback | None = None,
report: RunReport | None = None,
) -> TaskResult[Any]:
"""执行同步任务并带重试;返回填充好的 TaskResult。"""
result: TaskResult[Any] = TaskResult(spec=spec)
# 检查条件是否满足
if spec.conditions and not spec.should_execute():
# 检查上游任务是否被 SKIPPED
should_skip, skip_reason = _check_upstream_skipped(spec, report)
if should_skip:
result.status = TaskStatus.SKIPPED
result.finished_at = datetime.now()
result.reason = skip_reason
logger.info("task %r skipped (上游任务被跳过)", spec.name)
return result
# 检查条件是否满足
skip_reason = _check_conditions_for_skip(spec)
if skip_reason is not None:
result.status = TaskStatus.SKIPPED
result.finished_at = datetime.now()
result.reason = skip_reason
logger.info("task %r skipped (条件不满足)", spec.name)
return result
@@ -121,19 +184,61 @@ def _run_sync_with_retry(
raise AssertionError("unreachable") # pragma: no cover
async def _execute_async_task(
spec: TaskSpec[Any],
args: tuple[Any, ...],
kwargs: dict[str, Any],
loop: asyncio.AbstractEventLoop,
) -> Any:
"""执行异步或同步任务(带超时处理)。
Returns
-------
Any
任务返回值
"""
if _is_async_fn(spec):
coro = cast(Awaitable[Any], spec.effective_fn(*args, **kwargs))
if spec.timeout is not None:
return await asyncio.wait_for(coro, timeout=spec.timeout)
else:
return await coro
else:
# 将同步工作卸载到线程,保持事件循环存活。
def fn_call() -> Any:
return spec.effective_fn(*args, **kwargs)
if spec.timeout is not None:
return await asyncio.wait_for(loop.run_in_executor(None, fn_call), timeout=spec.timeout)
else:
return await loop.run_in_executor(None, fn_call)
async def _run_async_with_retry(
spec: TaskSpec[Any],
context: Mapping[str, Any],
layer_idx: int | None,
on_event: EventCallback | None = None,
report: RunReport | None = None,
) -> TaskResult[Any]:
"""在事件循环上执行任务(同步或异步)并带重试。"""
result: TaskResult[Any] = TaskResult[Any](spec=spec)
# 检查条件是否满足
if spec.conditions and not spec.should_execute():
# 检查上游任务是否被 SKIPPED
should_skip, skip_reason = _check_upstream_skipped(spec, report)
if should_skip:
result.status = TaskStatus.SKIPPED
result.finished_at = datetime.now()
result.reason = skip_reason
logger.info("task %r skipped (上游任务被跳过)", spec.name)
return result
# 检查条件是否满足
skip_reason = _check_conditions_for_skip(spec)
if skip_reason is not None:
result.status = TaskStatus.SKIPPED
result.finished_at = datetime.now()
result.reason = skip_reason
logger.info("task %r skipped (条件不满足)", spec.name)
return result
@@ -145,21 +250,7 @@ async def _run_async_with_retry(
while True:
result.attempts += 1
try:
if _is_async_fn(spec):
coro = cast(Awaitable[Any], spec.effective_fn(*args, **kwargs))
if spec.timeout is not None:
result.value = await asyncio.wait_for(coro, timeout=spec.timeout)
else:
result.value = await coro
else:
# 将同步工作卸载到线程,保持事件循环存活。
def fn_call() -> Any:
return spec.effective_fn(*args, **kwargs)
if spec.timeout is not None:
result.value = await asyncio.wait_for(loop.run_in_executor(None, fn_call), timeout=spec.timeout)
else:
result.value = await loop.run_in_executor(None, fn_call)
result.value = await _execute_async_task(spec, args, kwargs, loop)
result.status = TaskStatus.SUCCESS
result.finished_at = datetime.now()
return result
@@ -207,12 +298,12 @@ def _execute_layer_sequential(
if backend.has(name):
cached = backend.get(name)
context[name] = cached
result = TaskResult(spec=spec, status=TaskStatus.SKIPPED, value=cached)
result = TaskResult(spec=spec, status=TaskStatus.SKIPPED, value=cached, reason="缓存命中")
report.results[name] = result
_emit(on_event, result)
logger.info("task %r skipped (cached)", name)
continue
result = _run_sync_with_retry(spec, _build_context(spec, context), layer_idx, on_event)
result = _run_sync_with_retry(spec, _build_context(spec, context), layer_idx, on_event, report)
context[name] = result.value
backend.save(name, result.value)
report.results[name] = result
@@ -236,7 +327,7 @@ def _execute_layer_threaded(
if backend.has(name):
cached = backend.get(name)
context[name] = cached
result = TaskResult(spec=graph.spec(name), status=TaskStatus.SKIPPED, value=cached)
result = TaskResult(spec=graph.spec(name), status=TaskStatus.SKIPPED, value=cached, reason="缓存命中")
report.results[name] = result
_emit(on_event, result)
else:
@@ -251,7 +342,7 @@ def _execute_layer_threaded(
spec = graph.spec(name)
# 为本任务快照上下文以避免竞态。
task_ctx = _build_context(spec, context)
fut = pool.submit(_run_sync_with_retry, spec, task_ctx, layer_idx, on_event)
fut = pool.submit(_run_sync_with_retry, spec, task_ctx, layer_idx, on_event, report)
future_to_name[fut] = name
for fut in concurrent.futures.as_completed(future_to_name):
@@ -278,7 +369,7 @@ async def _execute_layer_async(
if backend.has(name):
cached = backend.get(name)
context[name] = cached
result = TaskResult(spec=graph.spec(name), status=TaskStatus.SKIPPED, value=cached)
result = TaskResult(spec=graph.spec(name), status=TaskStatus.SKIPPED, value=cached, reason="缓存命中")
report.results[name] = result
_emit(on_event, result)
else:
@@ -291,7 +382,7 @@ async def _execute_layer_async(
for name in to_run:
spec = graph.spec(name)
task_ctx = _build_context(spec, context)
coros.append(_run_async_with_retry(spec, task_ctx, layer_idx, on_event))
coros.append(_run_async_with_retry(spec, task_ctx, layer_idx, on_event, report))
results = await asyncio.gather(*coros)
for name, result in zip(to_run, results):
@@ -334,7 +425,8 @@ def _make_verbose_callback(
flush=True,
)
elif event.status == TaskStatus.SKIPPED: # pragma: no branch
print(f"[verbose] 任务 {event.task!r} 跳过", flush=True)
reason = f" ({event.reason})" if event.reason else ""
print(f"[verbose] 任务 {event.task!r} 跳过{reason}", flush=True)
else: # pragma: no cover
# 不可达: 执行器只发出 RUNNING/SUCCESS/FAILED/SKIPPED 事件
pass
+47 -6
View File
@@ -57,18 +57,59 @@ class Graph:
return self
@classmethod
def from_specs(cls, specs: Iterable[TaskSpec[Any]]) -> Graph:
"""从可迭代的 task spec 构建图
def from_specs(cls, specs: Iterable[TaskSpec[Any] | str]) -> Graph:
"""从可迭代的 task spec 构建图.
先收集所有 spec,再统一校验。这意味着任务可以引用*后出现*的
依赖——顺序无关,就像声明式配置文件的读取方式。
支持字符串引用,允许引用其他命令图中的任务。
字符串引用将在CliRunner中解析展开。
Parameters
----------
specs : Iterable[TaskSpec[Any] | str]
TaskSpec对象或字符串引用的列表
Returns
-------
Graph
构建完成的图
Note
-----
字符串引用格式:
- "command_name" - 引用整个命令图
- "command_name.task_name" - 引用特定任务
Examples
--------
>>> graph = Graph.from_specs([
... TaskSpec("build", cmd=["uv", "build"]),
... "test", # 引用test命令图
... ])
"""
graph = cls()
pending_refs: list[str] = []
for spec in specs:
if spec.name in graph.specs:
raise DuplicateTaskError(spec.name)
graph.specs[spec.name] = spec
graph.deps[spec.name] = spec.depends_on
if isinstance(spec, str):
# 字符串引用,稍后解析
pending_refs.append(spec)
elif isinstance(spec, TaskSpec):
if spec.name in graph.specs:
raise DuplicateTaskError(spec.name)
graph.specs[spec.name] = spec
graph.deps[spec.name] = spec.depends_on
else:
raise TypeError(f"from_specs只接受TaskSpec或str,收到: {type(spec)}")
# 存储待解析的引用
if pending_refs:
# 使用特殊属性存储引用,稍后在CliRunner中解析
# 由于Graph是frozen dataclass,我们需要特殊处理
object.__setattr__(graph, "_pending_refs", pending_refs)
graph._validate_references()
graph.validate()
return graph
+151 -8
View File
@@ -114,9 +114,155 @@ class CliRunner:
if not self.graphs:
raise ValueError("CliRunner 至少需要一个命令 (通过关键字参数提供)")
for name, graph in self.graphs.items():
if not isinstance(graph, Graph):
raise TypeError(f"CliRunner 命令 {name!r} 的值必须是 Graph 实例, 实际是 {type(graph).__name__}")
# 解析并展开字符串引用
self._resolve_graph_refs()
def _resolve_graph_refs(self) -> None:
"""解析并展开图中的字符串引用.
支持两种引用格式:
1. "command_name" - 引用整个命令图
2. "command_name.task_name" - 引用特定任务
递归解析所有引用,直到所有图都只包含TaskSpec对象。
"""
resolved_graphs: dict[str, Graph] = {}
for cmd_name, graph in self.graphs.items():
resolved_graph = self._expand_refs(graph, cmd_name)
resolved_graphs[cmd_name] = resolved_graph
# 更新graphs字典
object.__setattr__(self, "graphs", resolved_graphs)
def _expand_refs(self, graph: Graph, current_cmd: str) -> Graph:
"""展开图中的字符串引用.
Parameters
----------
graph : Graph
包含可能的字符串引用的图
current_cmd : str
当前命令名(用于避免循环引用)
Returns
-------
Graph
展开后的图,只包含TaskSpec对象
Note
-----
引用按顺序展开,后续引用的任务依赖于前面引用的任务完成。
例如:["c", "tc", bump] 会展开为:
- c的所有任务(无依赖)
- tc的所有任务(依赖于c的最后一个任务)
- bump任务(依赖于tc的最后一个任务)
"""
# 检查是否有待解析的引用
pending_refs = getattr(graph, "_pending_refs", None)
if not pending_refs:
return graph
# 收集所有TaskSpec(按正确顺序:先引用,后原始TaskSpec)
all_specs: list[TaskSpec[Any]] = []
# 记录每个引用展开后的所有任务名,用于建立依赖链
previous_ref_last_task: str | None = None
# 先解析每个引用,并建立依赖关系
for ref in pending_refs:
expanded_specs = self._parse_ref(ref, current_cmd)
# 如果有前面的引用,让当前引用的所有任务依赖于前面引用的最后一个任务
if previous_ref_last_task and expanded_specs:
# 为当前引用的每个任务添加依赖
for i, task in enumerate(expanded_specs):
# 只为没有依赖的任务添加依赖,或者为第一个任务添加依赖
if i == 0 or not task.depends_on:
updated_task = replace(task, depends_on=tuple({*task.depends_on, previous_ref_last_task}))
expanded_specs[i] = updated_task
# 记录当前引用的最后一个任务名
if expanded_specs:
previous_ref_last_task = expanded_specs[-1].name
all_specs.extend(expanded_specs)
# 然后添加原始图中的TaskSpec,并让它们按顺序执行
original_specs = list(graph.all_specs().values())
if original_specs:
# 第一个原始TaskSpec依赖于最后一个引用的任务
if previous_ref_last_task:
first_original = original_specs[0]
updated_first = replace(
first_original, depends_on=tuple({*first_original.depends_on, previous_ref_last_task})
)
all_specs.append(updated_first)
else:
# 如果没有引用,直接添加第一个原始TaskSpec
all_specs.append(original_specs[0])
# 后续的原始TaskSpec依赖于前一个原始TaskSpec
for i in range(1, len(original_specs)):
current_task = original_specs[i]
previous_task_name = original_specs[i - 1].name
# 更新依赖,确保顺序执行
updated_task = replace(current_task, depends_on=tuple({*current_task.depends_on, previous_task_name}))
all_specs.append(updated_task)
# 创建新的图(不包含引用)
return Graph.from_specs(all_specs)
def _parse_ref(self, ref: str, current_cmd: str) -> list[TaskSpec[Any]]:
"""解析单个字符串引用.
Parameters
----------
ref : str
引用字符串(如"tc""tc.lint"
current_cmd : str
当前命令名(用于避免循环引用)
Returns
-------
list[TaskSpec[Any]]
解析后的TaskSpec列表
Raises
------
ValueError
如果引用无效或存在循环引用
"""
# 避免循环引用
if ref == current_cmd:
raise ValueError(f"循环引用: 命令 '{current_cmd}' 引用了自己")
# 解析引用格式
if "." in ref:
# 特定任务引用: "command_name.task_name"
cmd_name, task_name = ref.split(".", 1)
if cmd_name not in self.graphs:
raise ValueError(f"引用的命令 '{cmd_name}' 不存在")
# 获取特定任务
ref_graph = self.graphs[cmd_name]
if task_name not in ref_graph.all_specs():
raise ValueError(f"任务 '{task_name}' 不存在于命令 '{cmd_name}'")
return [ref_graph.all_specs()[task_name]]
else:
# 整个命令图引用: "command_name"
cmd_name = ref
if cmd_name not in self.graphs:
raise ValueError(f"引用的命令 '{cmd_name}' 不存在")
# 获取整个图的所有任务
ref_graph = self.graphs[cmd_name]
# 递归展开引用(如果引用的图也有引用)
ref_graph = self._expand_refs(ref_graph, cmd_name)
return list(ref_graph.all_specs().values())
# ------------------------------------------------------------------ #
# 内省
@@ -161,7 +307,7 @@ class CliRunner:
_ = parser.add_argument(
"--strategy",
choices=list(get_args(Strategy)),
default="sequential",
default=self.strategy,
help="执行策略 (默认: %(default)s)",
)
_ = parser.add_argument(
@@ -183,10 +329,7 @@ class CliRunner:
def _format_commands_help(self) -> str:
"""格式化命令帮助文本."""
lines = ["可用命令:"]
for cmd in self.graphs:
lines.append(f" {cmd}")
return "\n".join(lines)
return "可用命令:\n" + " | ".join(self.graphs.keys())
# ------------------------------------------------------------------ #
# 执行
+1 -3
View File
@@ -112,9 +112,7 @@ class JSONBackend(StateBackend):
try:
_ = json.dumps(value)
except (TypeError, ValueError) as exc:
raise StorageError(
f"result of task {name!r} is not JSON-serialisable", exc
) from exc
raise StorageError(f"result of task {name!r} is not JSON-serialisable", exc) from exc
self._store[name] = value
self._flush()
+37 -5
View File
@@ -112,6 +112,12 @@ class TaskSpec(Generic[T]):
是否在命令执行时显示详细输出``True`` 时会打印执行的命令
及其标准输出/标准错误仅在使用 ``cmd`` 参数时有效
``False`` 时静默捕获输出失败时仍会包含在错误信息中
skip_if_missing:
仅对 ``cmd`` ``list[str]`` 的任务有效``True`` 时自动检查
命令是否存在通过 :func:`shutil.which`不存在则跳过任务
标记为 SKIPPED而非失败适用于构建工具场景避免因未安装
某些工具 maturintox而导致整个图执行失败
对于 ``str`` (shell) ``Callable`` 类型的 ``cmd``此参数无效
"""
name: str
@@ -126,6 +132,7 @@ class TaskSpec(Generic[T]):
conditions: Tuple[Condition, ...] = ()
cwd: Optional[Path] = None
verbose: bool = False
skip_if_missing: bool = True
def __post_init__(self) -> None:
if not self.name:
@@ -167,18 +174,19 @@ class TaskSpec(Generic[T]):
verbose = self.verbose
if isinstance(cmd, list):
cmd_list = cast(List[str], cmd)
def _run_list() -> T:
import subprocess
cmd_str = " ".join(str(arg) for arg in cmd)
cmd_str = " ".join(str(arg) for arg in cmd_list)
if verbose:
print(f"[verbose] 执行命令: {cmd_str}", flush=True)
if cwd is not None:
print(f"[verbose] 工作目录: {cwd}", flush=True)
try:
result = subprocess.run(
cmd,
cmd_list,
cwd=cwd,
timeout=timeout,
capture_output=not verbose,
@@ -257,10 +265,32 @@ class TaskSpec(Generic[T]):
Returns
-------
bool
若所有条件都返回 ``True``则返回 ``True``
否则返回 ``False``
若所有条件都返回 ``True`` ``skip_if_missing`` 检查通过
则返回 ``True``否则返回 ``False``
"""
return all(condition() for condition in self.conditions)
if not all(condition() for condition in self.conditions):
return False
return not (self.skip_if_missing and not self._is_cmd_available())
def _is_cmd_available(self) -> bool:
"""检查 ``cmd`` 是否可用.
仅对 ``list[str]`` 类型的 ``cmd`` 进行检查通过 :func:`shutil.which`
对于 ``str`` (shell) ``Callable`` 类型始终返回 ``True``
Returns
-------
bool
命令可用返回 ``True``否则返回 ``False``
"""
import shutil
cmd = self.cmd
if isinstance(cmd, list) and cmd:
first_arg = cast(str, cmd[0])
return shutil.which(first_arg) is not None
return True
@dataclass
@@ -278,6 +308,7 @@ class TaskResult(Generic[T]):
attempts: int = 0
started_at: Optional[datetime] = None
finished_at: Optional[datetime] = None
reason: Optional[str] = None # 跳过原因
@property
def duration(self) -> Optional[float]:
@@ -300,3 +331,4 @@ class TaskEvent:
attempts: int = 0
error: Optional[str] = None
duration: Optional[float] = None
reason: Optional[str] = None # 跳过原因,如 "条件不满足"、"上游任务被跳过"、"缓存"
+301
View File
@@ -0,0 +1,301 @@
"""Tests for cli.autofmt module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pyflowx as px
from pyflowx.cli import autofmt
# ---------------------------------------------------------------------- #
# format_with_ruff
# ---------------------------------------------------------------------- #
class TestFormatWithRuff:
"""Test format_with_ruff function."""
def test_format_with_ruff(self, tmp_path: Path) -> None:
"""Should format with ruff."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
autofmt.format_with_ruff(tmp_path, fix=True)
assert mock_run.called
def test_format_with_ruff_no_fix(self, tmp_path: Path) -> None:
"""Should format with ruff without fix."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
autofmt.format_with_ruff(tmp_path, fix=False)
# Should not include --fix flag
call_args = mock_run.call_args[0][0]
assert "--fix" not in call_args
# ---------------------------------------------------------------------- #
# lint_with_ruff
# ---------------------------------------------------------------------- #
class TestLintWithRuff:
"""Test lint_with_ruff function."""
def test_lint_with_ruff(self, tmp_path: Path) -> None:
"""Should lint with ruff."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
autofmt.lint_with_ruff(tmp_path, fix=True)
assert mock_run.called
def test_lint_with_ruff_no_fix(self, tmp_path: Path) -> None:
"""Should lint with ruff without fix."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
autofmt.lint_with_ruff(tmp_path, fix=False)
# Should not include --fix flag
call_args = mock_run.call_args[0][0]
assert "--fix" not in call_args
# ---------------------------------------------------------------------- #
# add_docstring
# ---------------------------------------------------------------------- #
class TestAddDocstring:
"""Test add_docstring function."""
def test_add_docstring_to_file(self, tmp_path: Path) -> None:
"""Should add docstring to file."""
py_file = tmp_path / "test.py"
py_file.write_text("def test():\n pass\n")
result = autofmt.add_docstring(py_file, '"""Test module."""')
assert result is True
def test_add_docstring_skips_files_with_docstring(self, tmp_path: Path) -> None:
"""Should skip files that already have docstring."""
py_file = tmp_path / "test.py"
py_file.write_text('"""Existing docstring."""\ndef test():\n pass\n')
result = autofmt.add_docstring(py_file, '"""New docstring."""')
assert result is False
def test_add_docstring_empty_file(self, tmp_path: Path) -> None:
"""Should handle empty file."""
py_file = tmp_path / "test.py"
py_file.write_text("")
result = autofmt.add_docstring(py_file, '"""Test module."""')
# Should handle empty file
assert result is True
# ---------------------------------------------------------------------- #
# generate_module_docstring
# ---------------------------------------------------------------------- #
class TestGenerateModuleDocstring:
"""Test generate_module_docstring function."""
def test_generate_module_docstring_basic(self, tmp_path: Path) -> None:
"""Should generate basic docstring."""
py_file = tmp_path / "test.py"
py_file.write_text("def test():\n pass\n")
result = autofmt.generate_module_docstring(py_file)
# Should contain "Tests for" since stem contains "test"
assert "Tests for" in result
def test_generate_module_docstring_with_package(self, tmp_path: Path) -> None:
"""Should generate docstring for package."""
py_file = tmp_path / "mypackage" / "test.py"
py_file.parent.mkdir(parents=True)
py_file.write_text("def test():\n pass\n")
result = autofmt.generate_module_docstring(py_file)
assert "mypackage" in result
def test_generate_module_docstring_cli(self, tmp_path: Path) -> None:
"""Should generate docstring for CLI module."""
py_file = tmp_path / "cli.py"
py_file.write_text("def test():\n pass\n")
result = autofmt.generate_module_docstring(py_file)
assert "Command-line interface" in result
def test_generate_module_docstring_util(self, tmp_path: Path) -> None:
"""Should generate docstring for utility module."""
py_file = tmp_path / "utils.py"
py_file.write_text("def test():\n pass\n")
result = autofmt.generate_module_docstring(py_file)
assert "Utility functions" in result
# ---------------------------------------------------------------------- #
# auto_add_docstrings
# ---------------------------------------------------------------------- #
class TestAutoAddDocstrings:
"""Test auto_add_docstrings function."""
def test_auto_add_docstrings(self, tmp_path: Path) -> None:
"""Should auto add docstrings."""
py_file = tmp_path / "test.py"
py_file.write_text("def test():\n pass\n")
with patch.object(autofmt, "add_docstring", return_value=True):
count = autofmt.auto_add_docstrings(tmp_path)
assert count >= 0
def test_auto_add_docstrings_skips_ignored(self, tmp_path: Path) -> None:
"""Should skip ignored directories."""
py_file = tmp_path / "__pycache__" / "test.py"
py_file.parent.mkdir()
py_file.write_text("def test():\n pass\n")
count = autofmt.auto_add_docstrings(tmp_path)
# Should skip __pycache__
assert count == 0
def test_auto_add_docstrings_no_files(self, tmp_path: Path) -> None:
"""Should handle no Python files."""
txt_file = tmp_path / "test.txt"
txt_file.write_text("test content")
count = autofmt.auto_add_docstrings(tmp_path)
assert count == 0
# ---------------------------------------------------------------------- #
# sync_pyproject_config
# ---------------------------------------------------------------------- #
class TestSyncPyprojectConfig:
"""Test sync_pyproject_config function."""
def test_sync_pyproject_config_creates_file(self, tmp_path: Path) -> None:
"""Should sync pyproject.toml config."""
main_toml = tmp_path / "pyproject.toml"
main_toml.write_text("[tool.ruff]\n")
sub_dir = tmp_path / "subproject"
sub_dir.mkdir()
sub_toml = sub_dir / "pyproject.toml"
sub_toml.write_text("[tool.ruff]\n")
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
autofmt.sync_pyproject_config(tmp_path)
assert mock_run.called
def test_sync_pyproject_config_updates_file(self, tmp_path: Path) -> None:
"""Should update existing pyproject.toml."""
main_toml = tmp_path / "pyproject.toml"
main_toml.write_text("[tool.ruff]\n")
sub_dir = tmp_path / "subproject"
sub_dir.mkdir()
sub_toml = sub_dir / "pyproject.toml"
sub_toml.write_text("[tool.ruff]\n")
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
autofmt.sync_pyproject_config(tmp_path)
assert mock_run.called
# ---------------------------------------------------------------------- #
# format_all
# ---------------------------------------------------------------------- #
class TestFormatAll:
"""Test format_all function."""
def test_format_all_runs_ruff_format(self, tmp_path: Path) -> None:
"""Should run ruff format."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
autofmt.format_all(tmp_path)
assert mock_run.called
def test_format_all_runs_ruff_check(self, tmp_path: Path) -> None:
"""Should run ruff check."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
autofmt.format_all(tmp_path)
# Should call ruff format and ruff check
assert mock_run.call_count == 2
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_fmt_default_target(self) -> None:
"""main() should handle fmt command with default target."""
with patch("sys.argv", ["autofmt", "fmt"]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_fmt_custom_target(self) -> None:
"""main() should handle fmt command with custom target."""
with patch("sys.argv", ["autofmt", "fmt", "--target", "src"]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_lint_default_target(self) -> None:
"""main() should handle lint command with default target."""
with patch("sys.argv", ["autofmt", "lint"]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_lint_with_fix(self) -> None:
"""main() should handle lint command with fix."""
with patch("sys.argv", ["autofmt", "lint", "--fix"]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_lint_custom_target(self) -> None:
"""main() should handle lint command with custom target."""
with patch("sys.argv", ["autofmt", "lint", "--target", "src"]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_doc_default_root(self) -> None:
"""main() should handle doc command with default root."""
with patch("sys.argv", ["autofmt", "doc"]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_doc_custom_root(self) -> None:
"""main() should handle doc command with custom root."""
with patch("sys.argv", ["autofmt", "doc", "--root-dir", "src"]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_sync_default_root(self) -> None:
"""main() should handle sync command with default root."""
with patch("sys.argv", ["autofmt", "sync"]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_sync_custom_root(self) -> None:
"""main() should handle sync command with custom root."""
with patch("sys.argv", ["autofmt", "sync", "--root-dir", "."]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help."""
with patch("sys.argv", ["autofmt"]), patch.object(autofmt, "main"):
# Just call main, it should show help and return
autofmt.main()
# main() should return without calling px.run
assert True
def test_main_creates_task_specs_with_verbose(self) -> None:
"""main() should create TaskSpecs with verbose=True."""
with patch("sys.argv", ["autofmt", "fmt"]), patch.object(px, "run") as mock_run:
autofmt.main()
assert mock_run.called
def test_main_uses_thread_strategy(self) -> None:
"""main() should use thread strategy."""
with patch("sys.argv", ["autofmt", "fmt"]), patch.object(px, "run") as mock_run:
autofmt.main()
# Check that strategy="thread" was used
assert mock_run.called
+106
View File
@@ -0,0 +1,106 @@
"""Tests for cli.bumpversion module."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pytest
import pyflowx as px
from pyflowx.cli import bumpversion
# ---------------------------------------------------------------------- #
# bump_version
# ---------------------------------------------------------------------- #
class TestBumpVersion:
"""Test bump_version function."""
def test_bump_version_patch(self) -> None:
"""Should bump patch version."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
bumpversion.bump_version("patch")
assert mock_run.called
def test_bump_version_minor(self) -> None:
"""Should bump minor version."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
bumpversion.bump_version("minor")
assert mock_run.called
def test_bump_version_major(self) -> None:
"""Should bump major version."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
bumpversion.bump_version("major")
assert mock_run.called
def test_bump_version_with_tag(self) -> None:
"""Should bump version with tag."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0, stdout="v1.0.0")
bumpversion.bump_version("patch", tag=True)
assert mock_run.called
def test_bump_version_with_commit(self) -> None:
"""Should bump version with commit."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
bumpversion.bump_version("patch", commit=True)
assert mock_run.called
def test_bump_version_file_not_found(self) -> None:
"""Should handle FileNotFoundError."""
with patch("subprocess.run", side_effect=FileNotFoundError), pytest.raises(FileNotFoundError):
bumpversion.bump_version("patch")
# ---------------------------------------------------------------------- #
# bump_version_alpha
# ---------------------------------------------------------------------- #
class TestBumpVersionAlpha:
"""Test bump_version_alpha function."""
def test_bump_version_alpha_patch(self) -> None:
"""Should bump alpha patch version."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
bumpversion.bump_version_alpha("patch")
assert mock_run.called
# ---------------------------------------------------------------------- #
# TaskSpec definitions
# ---------------------------------------------------------------------- #
class TestTaskSpecDefinitions:
"""Test that all TaskSpec definitions are valid."""
def test_bump_patch_spec(self) -> None:
"""bump_patch spec should be properly defined."""
assert bumpversion.bump_patch.name == "bump_patch"
assert bumpversion.bump_patch.fn is not None
def test_bump_minor_spec(self) -> None:
"""bump_minor spec should be properly defined."""
assert bumpversion.bump_minor.name == "bump_minor"
assert bumpversion.bump_minor.fn is not None
def test_bump_major_spec(self) -> None:
"""bump_major spec should be properly defined."""
assert bumpversion.bump_major.name == "bump_major"
assert bumpversion.bump_major.fn is not None
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_calls_run_cli(self) -> None:
"""main() should create a CliRunner and call run_cli()."""
with patch.object(px.CliRunner, "run_cli") as mock_run_cli:
bumpversion.main()
assert mock_run_cli.called
+44
View File
@@ -0,0 +1,44 @@
"""Tests for cli.clearscreen module."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
import pyflowx as px
from pyflowx.cli import clearscreen
from pyflowx.conditions import Constants
# ---------------------------------------------------------------------- #
# clear_screen
# ---------------------------------------------------------------------- #
class TestClearScreen:
"""Test clear_screen function."""
def test_clear_screen_windows(self) -> None:
"""Should clear screen on Windows."""
if Constants.IS_WINDOWS:
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
clearscreen.clear_screen()
assert mock_run.called
def test_clear_screen_linux(self) -> None:
"""Should clear screen on Linux."""
with patch.object(Constants, "IS_WINDOWS", False), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
clearscreen.clear_screen()
assert mock_run.called
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_creates_graph_and_runs(self) -> None:
"""main() should create a Graph and run it."""
with patch.object(px, "run") as mock_run:
clearscreen.main()
assert mock_run.called
+110
View File
@@ -0,0 +1,110 @@
"""Tests for cli.envpy module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
import pyflowx as px
from pyflowx.cli import envpy
# ---------------------------------------------------------------------- #
# set_pip_mirror
# ---------------------------------------------------------------------- #
class TestSetPipMirror:
"""Test set_pip_mirror function."""
def test_set_pip_mirror_tsinghua(self, tmp_path: Path) -> None:
"""Should set tsinghua mirror."""
with patch.object(Path, "home", return_value=tmp_path):
envpy.set_pip_mirror("tsinghua")
# Check pip config
pip_config = tmp_path / "pip" / "pip.ini"
if envpy.Constants.IS_WINDOWS:
assert pip_config.exists() or (tmp_path / "pip" / "pip.conf").exists()
def test_set_pip_mirror_aliyun(self, tmp_path: Path) -> None:
"""Should set aliyun mirror."""
with patch.object(Path, "home", return_value=tmp_path):
envpy.set_pip_mirror("aliyun")
# Check pip config
pip_dir = tmp_path / "pip"
assert pip_dir.exists()
def test_set_pip_mirror_with_token(self, tmp_path: Path) -> None:
"""Should set mirror with token."""
with patch.object(Path, "home", return_value=tmp_path):
envpy.set_pip_mirror("tsinghua", token="test_token")
# Check that token is set
def test_set_pip_mirror_creates_pip_dir(self, tmp_path: Path) -> None:
"""Should create pip directory if it doesn't exist."""
pip_dir = tmp_path / "pip"
with patch.object(Path, "home", return_value=tmp_path):
envpy.set_pip_mirror("tsinghua")
assert pip_dir.exists()
assert pip_dir.is_dir()
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_mirror_tsinghua(self) -> None:
"""main() should handle mirror tsinghua command."""
with patch("sys.argv", ["envpy", "mirror", "tsinghua"]), patch.object(px, "run") as mock_run, patch.object(
envpy, "set_pip_mirror"
):
envpy.main()
assert mock_run.called
def test_main_mirror_aliyun(self) -> None:
"""main() should handle mirror aliyun command."""
with patch("sys.argv", ["envpy", "mirror", "aliyun"]), patch.object(px, "run") as mock_run, patch.object(
envpy, "set_pip_mirror"
):
envpy.main()
assert mock_run.called
def test_main_mirror_with_token(self) -> None:
"""main() should handle mirror with token."""
with patch("sys.argv", ["envpy", "mirror", "tsinghua", "--token", "test_token"]), patch.object(
px, "run"
) as mock_run, patch.object(envpy, "set_pip_mirror"):
envpy.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help and return."""
with patch("sys.argv", ["envpy"]):
envpy.main()
# Should print help and return
def test_main_invalid_mirror_shows_error(self) -> None:
"""main() with invalid mirror should show error."""
with patch("sys.argv", ["envpy", "mirror", "invalid"]), pytest.raises(SystemExit) as exc_info:
envpy.main()
assert exc_info.value.code == 2
def test_main_creates_task_spec_with_correct_name(self) -> None:
"""main() should create TaskSpec with correct name."""
with patch("sys.argv", ["envpy", "mirror", "tsinghua"]), patch.object(px, "run") as mock_run, patch.object(
envpy, "set_pip_mirror"
):
envpy.main()
graph = mock_run.call_args[0][0]
task_names = list(graph.all_specs().keys())
assert "set_pip_mirror" in task_names
def test_main_uses_thread_strategy(self) -> None:
"""main() should use thread strategy."""
with patch("sys.argv", ["envpy", "mirror", "tsinghua"]), patch.object(px, "run") as mock_run, patch.object(
envpy, "set_pip_mirror"
):
envpy.main()
assert mock_run.call_args[1]["strategy"] == "thread"
+209
View File
@@ -0,0 +1,209 @@
"""Tests for cli.envrs module."""
from __future__ import annotations
import os
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
import pyflowx as px
from pyflowx.cli import envrs
# ---------------------------------------------------------------------- #
# set_rust_mirror
# ---------------------------------------------------------------------- #
class TestSetRustMirror:
"""Test set_rust_mirror function."""
def test_set_rust_mirror_aliyun(self, tmp_path: Path) -> None:
"""Should set aliyun mirror."""
with patch.object(Path, "home", return_value=tmp_path):
envrs.set_rust_mirror("aliyun")
# Check environment variables
assert os.environ.get("RUSTUP_DIST_SERVER") == "https://mirrors.aliyun.com/rustup"
assert os.environ.get("RUSTUP_UPDATE_ROOT") == "https://mirrors.aliyun.com/rustup/rustup"
# Check cargo config
cargo_config = tmp_path / ".cargo" / "config.toml"
assert cargo_config.exists()
content = cargo_config.read_text()
assert "aliyun" in content
def test_set_rust_mirror_ustc(self, tmp_path: Path) -> None:
"""Should set ustc mirror."""
with patch.object(Path, "home", return_value=tmp_path):
envrs.set_rust_mirror("ustc")
assert os.environ.get("RUSTUP_DIST_SERVER") == "https://mirrors.ustc.edu.cn/rust-static"
assert os.environ.get("RUSTUP_UPDATE_ROOT") == "https://mirrors.ustc.edu.cn/rust-static/rustup"
def test_set_rust_mirror_tsinghua(self, tmp_path: Path) -> None:
"""Should set tsinghua mirror."""
with patch.object(Path, "home", return_value=tmp_path):
envrs.set_rust_mirror("tsinghua")
assert os.environ.get("RUSTUP_DIST_SERVER") == "https://mirrors.tuna.tsinghua.edu.cn/rustup"
assert os.environ.get("RUSTUP_UPDATE_ROOT") == "https://mirrors.tuna.tsinghua.edu.cn/rustup/rustup"
def test_set_rust_mirror_unknown_uses_default(self, tmp_path: Path) -> None:
"""Should use default mirror for unknown mirror name."""
with patch.object(Path, "home", return_value=tmp_path):
envrs.set_rust_mirror("unknown")
# Should use default mirror (tsinghua)
assert os.environ.get("RUSTUP_DIST_SERVER") == "https://mirrors.tuna.tsinghua.edu.cn/rustup"
def test_set_rust_mirror_creates_cargo_dir(self, tmp_path: Path) -> None:
"""Should create .cargo directory if it doesn't exist."""
cargo_dir = tmp_path / ".cargo"
with patch.object(Path, "home", return_value=tmp_path):
envrs.set_rust_mirror("aliyun")
assert cargo_dir.exists()
assert cargo_dir.is_dir()
def test_set_rust_mirror_prints_message(self, tmp_path: Path, capsys: pytest.CaptureFixture[str]) -> None:
"""Should print mirror name."""
with patch.object(Path, "home", return_value=tmp_path):
envrs.set_rust_mirror("aliyun")
captured = capsys.readouterr()
assert "已设置 Rust 镜像源: aliyun" in captured.out
# ---------------------------------------------------------------------- #
# install_rust
# ---------------------------------------------------------------------- #
class TestInstallRust:
"""Test install_rust function."""
def test_install_rust_stable(self) -> None:
"""Should install stable Rust."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
envrs.install_rust("stable")
mock_run.assert_called_once_with(["rustup", "toolchain", "install", "stable"], check=True)
def test_install_rust_nightly(self) -> None:
"""Should install nightly Rust."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
envrs.install_rust("nightly")
mock_run.assert_called_once_with(["rustup", "toolchain", "install", "nightly"], check=True)
def test_install_rust_beta(self) -> None:
"""Should install beta Rust."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
envrs.install_rust("beta")
mock_run.assert_called_once_with(["rustup", "toolchain", "install", "beta"], check=True)
def test_install_rust_file_not_found(self) -> None:
"""Should raise FileNotFoundError when rustup not found."""
with patch("subprocess.run", side_effect=FileNotFoundError), pytest.raises(FileNotFoundError):
envrs.install_rust("stable")
def test_install_rust_prints_message(self, capsys: pytest.CaptureFixture[str]) -> None:
"""Should print installation message."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
envrs.install_rust("stable")
captured = capsys.readouterr()
assert "已安装 Rust stable" in captured.out
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_mirror_aliyun(self) -> None:
"""main() should handle mirror aliyun command."""
with patch("sys.argv", ["envrs", "mirror", "aliyun"]), patch.object(px, "run") as mock_run, patch.object(
envrs, "set_rust_mirror"
):
envrs.main()
assert mock_run.called
def test_main_mirror_ustc(self) -> None:
"""main() should handle mirror ustc command."""
with patch("sys.argv", ["envrs", "mirror", "ustc"]), patch.object(px, "run") as mock_run, patch.object(
envrs, "set_rust_mirror"
):
envrs.main()
assert mock_run.called
def test_main_mirror_tsinghua(self) -> None:
"""main() should handle mirror tsinghua command."""
with patch("sys.argv", ["envrs", "mirror", "tsinghua"]), patch.object(px, "run") as mock_run, patch.object(
envrs, "set_rust_mirror"
):
envrs.main()
assert mock_run.called
def test_main_mirror_default(self) -> None:
"""main() should use default mirror when not specified."""
with patch("sys.argv", ["envrs", "mirror"]), patch.object(px, "run") as mock_run, patch.object(
envrs, "set_rust_mirror"
):
envrs.main()
assert mock_run.called
def test_main_install_stable(self) -> None:
"""main() should handle install stable command."""
with patch("sys.argv", ["envrs", "install", "stable"]), patch.object(px, "run") as mock_run:
envrs.main()
assert mock_run.called
def test_main_install_nightly(self) -> None:
"""main() should handle install nightly command."""
with patch("sys.argv", ["envrs", "install", "nightly"]), patch.object(px, "run") as mock_run:
envrs.main()
assert mock_run.called
def test_main_install_beta(self) -> None:
"""main() should handle install beta command."""
with patch("sys.argv", ["envrs", "install", "beta"]), patch.object(px, "run") as mock_run:
envrs.main()
assert mock_run.called
def test_main_install_default(self) -> None:
"""main() should use default version when not specified."""
with patch("sys.argv", ["envrs", "install"]), patch.object(px, "run") as mock_run:
envrs.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help and return."""
with patch("sys.argv", ["envrs"]):
envrs.main()
# Should print help and return
def test_main_invalid_version_shows_error(self) -> None:
"""main() with invalid version should show error."""
with patch("sys.argv", ["envrs", "install", "invalid"]), pytest.raises(SystemExit) as exc_info:
envrs.main()
assert exc_info.value.code == 2
def test_main_invalid_mirror_shows_error(self) -> None:
"""main() with invalid mirror should show error."""
with patch("sys.argv", ["envrs", "mirror", "invalid"]), pytest.raises(SystemExit) as exc_info:
envrs.main()
assert exc_info.value.code == 2
def test_main_creates_task_spec_with_verbose(self) -> None:
"""main() should create TaskSpec with verbose=True."""
with patch("sys.argv", ["envrs", "mirror", "aliyun"]), patch.object(px, "run") as mock_run, patch.object(
envrs, "set_rust_mirror"
):
envrs.main()
graph = mock_run.call_args[0][0]
specs = graph.all_specs()
for spec in specs.values():
assert spec.verbose is True
def test_main_uses_thread_strategy(self) -> None:
"""main() should use thread strategy."""
with patch("sys.argv", ["envrs", "mirror", "aliyun"]), patch.object(px, "run") as mock_run, patch.object(
envrs, "set_rust_mirror"
):
envrs.main()
assert mock_run.call_args[1]["strategy"] == "thread"
+136
View File
@@ -0,0 +1,136 @@
"""Tests for cli.filedate module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pyflowx as px
from pyflowx.cli import filedate
# ---------------------------------------------------------------------- #
# get_file_timestamp
# ---------------------------------------------------------------------- #
class TestGetFileTimestamp:
"""Test get_file_timestamp function."""
def test_get_file_timestamp(self, tmp_path: Path) -> None:
"""Should get file timestamp."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
timestamp = filedate.get_file_timestamp(test_file)
assert len(timestamp) == 8 # YYYYMMDD format
assert timestamp.isdigit()
# ---------------------------------------------------------------------- #
# remove_date_prefix
# ---------------------------------------------------------------------- #
class TestRemoveDatePrefix:
"""Test remove_date_prefix function."""
def test_remove_date_prefix_with_date(self, tmp_path: Path) -> None:
"""Should remove date prefix from filename."""
test_file = tmp_path / "20240101_test.txt"
test_file.write_text("test content")
new_path = filedate.remove_date_prefix(test_file)
assert new_path.name == "test.txt"
def test_remove_date_prefix_without_date(self, tmp_path: Path) -> None:
"""Should not change filename without date prefix."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
new_path = filedate.remove_date_prefix(test_file)
assert new_path == test_file
# ---------------------------------------------------------------------- #
# add_date_prefix
# ---------------------------------------------------------------------- #
class TestAddDatePrefix:
"""Test add_date_prefix function."""
def test_add_date_prefix(self, tmp_path: Path) -> None:
"""Should add date prefix to filename."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
new_path = filedate.add_date_prefix(test_file)
assert new_path.name.startswith("20") # Starts with year
assert "_test.txt" in new_path.name
# ---------------------------------------------------------------------- #
# process_file_date
# ---------------------------------------------------------------------- #
class TestProcessFileDate:
"""Test process_file_date function."""
def test_process_file_date_add(self, tmp_path: Path) -> None:
"""Should add date prefix."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
filedate.process_file_date(test_file, clear=False)
# File should be renamed with date prefix
def test_process_file_date_clear(self, tmp_path: Path) -> None:
"""Should clear date prefix."""
test_file = tmp_path / "20240101_test.txt"
test_file.write_text("test content")
filedate.process_file_date(test_file, clear=True)
# File should be renamed without date prefix
# ---------------------------------------------------------------------- #
# process_files_date
# ---------------------------------------------------------------------- #
class TestProcessFilesDate:
"""Test process_files_date function."""
def test_process_files_date_batch(self, tmp_path: Path) -> None:
"""Should process multiple files."""
files = []
for i in range(3):
test_file = tmp_path / f"test{i}.txt"
test_file.write_text(f"content{i}")
files.append(test_file)
filedate.process_files_date(files, clear=False)
# All files should be processed
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_add_command(self, tmp_path: Path) -> None:
"""main() should handle add command."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
with patch("sys.argv", ["filedate", "add", str(test_file)]), patch.object(px, "run") as mock_run:
filedate.main()
assert mock_run.called
def test_main_clear_command(self, tmp_path: Path) -> None:
"""main() should handle clear command."""
test_file = tmp_path / "20240101_test.txt"
test_file.write_text("test content")
with patch("sys.argv", ["filedate", "clear", str(test_file)]), patch.object(px, "run") as mock_run:
filedate.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help."""
with patch("sys.argv", ["filedate"]):
filedate.main()
# Should print help and return
+133
View File
@@ -0,0 +1,133 @@
"""Tests for cli.filelevel module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pyflowx as px
from pyflowx.cli import filelevel
# ---------------------------------------------------------------------- #
# remove_marks
# ---------------------------------------------------------------------- #
class TestRemoveMarks:
"""Test remove_marks function."""
def test_remove_marks_single_mark(self) -> None:
"""Should remove single mark."""
stem = "filename(PUB)"
result = filelevel.remove_marks(stem, ["PUB"])
assert result == "filename"
def test_remove_marks_multiple_marks(self) -> None:
"""Should remove multiple marks."""
stem = "filename(PUB)(NOR)"
result = filelevel.remove_marks(stem, ["PUB", "NOR"])
assert result == "filename"
def test_remove_marks_no_marks(self) -> None:
"""Should not change stem without marks."""
stem = "filename"
result = filelevel.remove_marks(stem, ["PUB"])
assert result == "filename"
# ---------------------------------------------------------------------- #
# process_file_level
# ---------------------------------------------------------------------- #
class TestProcessFileLevel:
"""Test process_file_level function."""
def test_process_file_level_set_pub(self, tmp_path: Path) -> None:
"""Should set PUB level."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
filelevel.process_file_level(test_file, level=1)
# File should be renamed with PUB level
def test_process_file_level_set_int(self, tmp_path: Path) -> None:
"""Should set INT level."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
filelevel.process_file_level(test_file, level=2)
# File should be renamed with INT level
def test_process_file_level_clear(self, tmp_path: Path) -> None:
"""Should clear level."""
test_file = tmp_path / "test(PUB).txt"
test_file.write_text("test content")
filelevel.process_file_level(test_file, level=0)
# File should be renamed without level
def test_process_file_level_invalid_level(self, tmp_path: Path) -> None:
"""Should handle invalid level."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
filelevel.process_file_level(test_file, level=5)
# Should print error message
def test_process_file_level_nonexistent_file(self, tmp_path: Path) -> None:
"""Should handle nonexistent file."""
test_file = tmp_path / "nonexistent.txt"
filelevel.process_file_level(test_file, level=1)
# Should print error message
# ---------------------------------------------------------------------- #
# process_files_level
# ---------------------------------------------------------------------- #
class TestProcessFilesLevel:
"""Test process_files_level function."""
def test_process_files_level_batch(self, tmp_path: Path) -> None:
"""Should process multiple files."""
files = []
for i in range(3):
test_file = tmp_path / f"test{i}.txt"
test_file.write_text(f"content{i}")
files.append(test_file)
filelevel.process_files_level(files, level=1)
# All files should be processed
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_set_command(self, tmp_path: Path) -> None:
"""main() should handle set command."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
with patch("sys.argv", ["filelevel", "set", str(test_file), "--level", "1"]), patch.object(
px, "run"
) as mock_run:
filelevel.main()
assert mock_run.called
def test_main_set_command_level_2(self, tmp_path: Path) -> None:
"""main() should handle set command with level 2."""
test_file = tmp_path / "test.txt"
test_file.write_text("test content")
with patch("sys.argv", ["filelevel", "set", str(test_file), "--level", "2"]), patch.object(
px, "run"
) as mock_run:
filelevel.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help."""
with patch("sys.argv", ["filelevel"]):
filelevel.main()
# Should print help and return
+173
View File
@@ -0,0 +1,173 @@
"""Tests for cli.folderback module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pyflowx as px
from pyflowx.cli import folderback
# ---------------------------------------------------------------------- #
# remove_dump
# ---------------------------------------------------------------------- #
class TestRemoveDump:
"""Test remove_dump function."""
def test_remove_dump_no_files(self, tmp_path: Path) -> None:
"""Should handle no zip files."""
src = tmp_path / "source"
src.mkdir()
dst = tmp_path / "backup"
dst.mkdir()
folderback.remove_dump(src, dst, 5)
# Should not raise error
def test_remove_dump_within_limit(self, tmp_path: Path) -> None:
"""Should not remove files within limit."""
src = tmp_path / "source"
src.mkdir()
dst = tmp_path / "backup"
dst.mkdir()
# Create some zip files
for i in range(3):
zip_file = dst / f"source_20240101_12000{i}.zip"
zip_file.write_bytes(b"ZIP content")
folderback.remove_dump(src, dst, 5)
# All files should remain
assert len(list(dst.glob("*.zip"))) == 3
def test_remove_dump_exceeds_limit(self, tmp_path: Path) -> None:
"""Should remove oldest files when exceeds limit."""
src = tmp_path / "source"
src.mkdir()
dst = tmp_path / "backup"
dst.mkdir()
# Create more zip files than limit
for i in range(7):
zip_file = dst / f"source_20240101_12000{i}.zip"
zip_file.write_bytes(b"ZIP content")
folderback.remove_dump(src, dst, 5)
# Should have only 5 files
assert len(list(dst.glob("*.zip"))) == 5
# ---------------------------------------------------------------------- #
# zip_target
# ---------------------------------------------------------------------- #
class TestZipTarget:
"""Test zip_target function."""
def test_zip_target_creates_zip(self, tmp_path: Path) -> None:
"""Should create zip file."""
src = tmp_path / "source"
src.mkdir()
(src / "test.txt").write_text("test content")
dst = tmp_path / "backup"
dst.mkdir()
with patch("time.strftime", return_value="_20240101_120000"):
folderback.zip_target(src, dst, 5)
# Should create zip file
zip_files = list(dst.glob("*.zip"))
assert len(zip_files) == 1
def test_zip_target_with_subdirectories(self, tmp_path: Path) -> None:
"""Should zip files in subdirectories."""
src = tmp_path / "source"
src.mkdir()
subdir = src / "subdir"
subdir.mkdir()
(src / "test.txt").write_text("test content")
(subdir / "nested.txt").write_text("nested content")
dst = tmp_path / "backup"
dst.mkdir()
with patch("time.strftime", return_value="_20240101_120000"):
folderback.zip_target(src, dst, 5)
# Should create zip file
zip_files = list(dst.glob("*.zip"))
assert len(zip_files) == 1
# ---------------------------------------------------------------------- #
# backup_folder
# ---------------------------------------------------------------------- #
class TestBackupFolder:
"""Test backup_folder function."""
def test_backup_folder_with_source_and_backup(self, tmp_path: Path) -> None:
"""Should backup folder with source and backup paths."""
source_dir = tmp_path / "source"
source_dir.mkdir()
(source_dir / "test.txt").write_text("test content")
backup_dir = tmp_path / "backup"
with patch.object(folderback, "zip_target") as mock_zip:
folderback.backup_folder(str(source_dir), str(backup_dir), 5)
assert mock_zip.called
def test_backup_folder_with_max_backups(self, tmp_path: Path) -> None:
"""Should backup folder with max backups."""
source_dir = tmp_path / "source"
source_dir.mkdir()
(source_dir / "test.txt").write_text("test content")
backup_dir = tmp_path / "backup"
with patch.object(folderback, "zip_target") as mock_zip:
folderback.backup_folder(str(source_dir), str(backup_dir), 10)
assert mock_zip.called
def test_backup_folder_source_not_exists(self, tmp_path: Path) -> None:
"""Should handle non-existent source folder."""
source_dir = tmp_path / "nonexistent"
backup_dir = tmp_path / "backup"
backup_dir.mkdir()
folderback.backup_folder(str(source_dir), str(backup_dir), 5)
# Should print error message and return
def test_backup_folder_creates_dst(self, tmp_path: Path) -> None:
"""Should create destination directory."""
source_dir = tmp_path / "source"
source_dir.mkdir()
(source_dir / "test.txt").write_text("test content")
backup_dir = tmp_path / "backup"
with patch.object(folderback, "zip_target") as mock_zip:
folderback.backup_folder(str(source_dir), str(backup_dir), 5)
assert backup_dir.exists()
assert mock_zip.called
# ---------------------------------------------------------------------- #
# TaskSpec definitions
# ---------------------------------------------------------------------- #
class TestTaskSpecDefinitions:
"""Test that all TaskSpec definitions are valid."""
def test_folderback_default_spec(self) -> None:
"""folderback_default spec should be properly defined."""
assert folderback.folderback_default.name == "folderback_default"
assert folderback.folderback_default.fn is not None
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_calls_run_cli(self) -> None:
"""main() should create a CliRunner and call run_cli()."""
with patch.object(px.CliRunner, "run_cli") as mock_run_cli:
folderback.main()
assert mock_run_cli.called
+75
View File
@@ -0,0 +1,75 @@
"""Tests for cli.folderzip module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pyflowx as px
from pyflowx.cli import folderzip
# ---------------------------------------------------------------------- #
# archive_folder
# ---------------------------------------------------------------------- #
class TestArchiveFolder:
"""Test archive_folder function."""
def test_archive_folder(self, tmp_path: Path) -> None:
"""Should archive a folder."""
folder = tmp_path / "test_folder"
folder.mkdir()
(folder / "test.txt").write_text("test content")
with patch("shutil.make_archive") as mock_archive:
folderzip.archive_folder(folder)
assert mock_archive.called
# ---------------------------------------------------------------------- #
# zip_folders
# ---------------------------------------------------------------------- #
class TestZipFolders:
"""Test zip_folders function."""
def test_zip_folders_with_cwd(self, tmp_path: Path) -> None:
"""Should zip folders in cwd."""
# Create some folders
(tmp_path / "folder1").mkdir()
(tmp_path / "folder2").mkdir()
(tmp_path / ".git").mkdir() # Should be ignored
with patch.object(folderzip, "archive_folder") as mock_archive:
folderzip.zip_folders(str(tmp_path))
# Should archive folder1 and folder2, but not .git
assert mock_archive.call_count == 2
def test_zip_folders_nonexistent_cwd(self, tmp_path: Path) -> None:
"""Should handle nonexistent cwd."""
folderzip.zip_folders(str(tmp_path / "nonexistent"))
# Should print error message and return
# ---------------------------------------------------------------------- #
# TaskSpec definitions
# ---------------------------------------------------------------------- #
class TestTaskSpecDefinitions:
"""Test that all TaskSpec definitions are valid."""
def test_folderzip_default_spec(self) -> None:
"""folderzip_default spec should be properly defined."""
assert folderzip.folderzip_default.name == "folderzip_default"
assert folderzip.folderzip_default.fn is not None
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_calls_run_cli(self) -> None:
"""main() should create a CliRunner and call run_cli()."""
with patch.object(px.CliRunner, "run_cli") as mock_run_cli:
folderzip.main()
assert mock_run_cli.called
+136
View File
@@ -0,0 +1,136 @@
"""Tests for cli.gittool module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import patch
import pytest
import pyflowx as px
from pyflowx.cli import gittool
# ---------------------------------------------------------------------- #
# not_has_git_repo
# ---------------------------------------------------------------------- #
class TestNotHasGitRepo:
"""Test not_has_git_repo function."""
def test_not_has_git_repo_true(self, tmp_path: Path) -> None:
"""Should return True when no .git directory."""
with patch.object(Path, "cwd", return_value=tmp_path):
result = gittool.not_has_git_repo()
assert result is True
def test_not_has_git_repo_false(self, tmp_path: Path) -> None:
"""Should return False when .git directory exists."""
git_dir = tmp_path / ".git"
git_dir.mkdir()
with patch.object(Path, "cwd", return_value=tmp_path):
result = gittool.not_has_git_repo()
assert result is False
def test_not_has_git_repo_cwd_not_exists(self, tmp_path: Path) -> None:
"""Should return True when cwd doesn't exist."""
nonexistent = tmp_path / "nonexistent"
with patch.object(Path, "cwd", return_value=nonexistent):
result = gittool.not_has_git_repo()
assert result is True
# ---------------------------------------------------------------------- #
# has_files
# ---------------------------------------------------------------------- #
class TestHasFiles:
"""Test has_files function."""
def test_has_files_true(self, tmp_path: Path) -> None:
"""Should return True when files exist."""
(tmp_path / "test.txt").write_text("test")
with patch.object(Path, "cwd", return_value=tmp_path):
result = gittool.has_files()
assert result is True
def test_has_files_false(self, tmp_path: Path) -> None:
"""Should return False when no files."""
with patch.object(Path, "cwd", return_value=tmp_path):
result = gittool.has_files()
assert result is False
# ---------------------------------------------------------------------- #
# init_sub_dirs
# ---------------------------------------------------------------------- #
class TestInitSubDirs:
"""Test init_sub_dirs function."""
def test_init_sub_dirs_with_subdirectories(self, tmp_path: Path) -> None:
"""Should initialize git in subdirectories."""
subdir1 = tmp_path / "subdir1"
subdir1.mkdir()
subdir2 = tmp_path / "subdir2"
subdir2.mkdir()
with patch.object(Path, "cwd", return_value=tmp_path), patch.object(px, "run") as mock_run:
gittool.init_sub_dirs()
# Should call px.run for each subdirectory
assert mock_run.call_count == 2
def test_init_sub_dirs_no_subdirectories(self, tmp_path: Path) -> None:
"""Should handle no subdirectories."""
with patch.object(Path, "cwd", return_value=tmp_path), patch.object(px, "run") as mock_run:
gittool.init_sub_dirs()
# Should not call px.run
assert mock_run.call_count == 0
# ---------------------------------------------------------------------- #
# TaskSpec definitions
# ---------------------------------------------------------------------- #
class TestTaskSpecDefinitions:
"""Test that all TaskSpec definitions are valid."""
def test_push_spec(self) -> None:
"""push spec should be properly defined."""
assert gittool.push.name == "push"
assert gittool.push.cmd == ["git", "push"]
def test_pull_spec(self) -> None:
"""pull spec should be properly defined."""
assert gittool.pull.name == "pull"
assert gittool.pull.cmd == ["git", "pull"]
def test_kill_tgit_spec(self) -> None:
"""kill_tgit spec should be properly defined."""
assert gittool.kill_tgit.name == "task_kill"
assert "taskkill" in gittool.kill_tgit.cmd
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_calls_run_cli(self) -> None:
"""main() should create a CliRunner and call run_cli()."""
with pytest.raises(SystemExit) as exc_info:
gittool.main()
# run_cli() calls sys.exit(), so we should get SystemExit
assert exc_info.value.code in (0, 1, 2)
def test_main_with_list_argument(self) -> None:
"""main() should handle --list argument."""
with patch("sys.argv", ["gittool", "--list"]), pytest.raises(SystemExit) as exc_info:
gittool.main()
assert exc_info.value.code == 0
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help and exit."""
with patch("sys.argv", ["gittool"]), pytest.raises(SystemExit) as exc_info:
gittool.main()
assert exc_info.value.code == 1
+157
View File
@@ -0,0 +1,157 @@
"""Tests for cli.lscalc module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pyflowx as px
from pyflowx.cli import lscalc
from pyflowx.conditions import Constants
# ---------------------------------------------------------------------- #
# get_ls_dyna_command
# ---------------------------------------------------------------------- #
class TestGetLsDynaCommand:
"""Test get_ls_dyna_command function."""
def test_get_ls_dyna_command_windows(self) -> None:
"""Should get LS-DYNA command for Windows."""
with patch.object(Constants, "IS_WINDOWS", True), patch.object(Constants, "IS_MACOS", False):
cmd = lscalc.get_ls_dyna_command("input.k", 4)
assert "ls-dyna_mpp" in cmd
assert "i=input.k" in cmd
assert "ncpu=4" in cmd
def test_get_ls_dyna_command_linux(self) -> None:
"""Should get LS-DYNA command for Linux."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False):
cmd = lscalc.get_ls_dyna_command("input.k", 8)
assert "ls-dyna_mpp" in cmd
assert "i=input.k" in cmd
assert "ncpu=8" in cmd
# ---------------------------------------------------------------------- #
# run_ls_dyna
# ---------------------------------------------------------------------- #
class TestRunLsDyna:
"""Test run_ls_dyna function."""
def test_run_ls_dyna_success(self, tmp_path: Path) -> None:
"""Should run LS-DYNA successfully."""
input_file = tmp_path / "input.k"
input_file.write_text("LS-DYNA input")
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
lscalc.run_ls_dyna(str(input_file), ncpu=4)
assert mock_run.called
def test_run_ls_dyna_file_not_found(self, tmp_path: Path) -> None:
"""Should handle nonexistent input file."""
input_file = tmp_path / "nonexistent.k"
lscalc.run_ls_dyna(str(input_file), ncpu=4)
# Should print error message
def test_run_ls_dyna_command_not_found(self, tmp_path: Path) -> None:
"""Should handle command not found."""
input_file = tmp_path / "input.k"
input_file.write_text("LS-DYNA input")
with patch("subprocess.run", side_effect=FileNotFoundError):
lscalc.run_ls_dyna(str(input_file), ncpu=4)
# Should print error message
# ---------------------------------------------------------------------- #
# run_ls_dyna_mpi
# ---------------------------------------------------------------------- #
class TestRunLsDynaMpi:
"""Test run_ls_dyna_mpi function."""
def test_run_ls_dyna_mpi_success(self, tmp_path: Path) -> None:
"""Should run LS-DYNA MPI successfully."""
input_file = tmp_path / "input.k"
input_file.write_text("LS-DYNA input")
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
lscalc.run_ls_dyna_mpi(str(input_file), ncpu=8)
assert mock_run.called
def test_run_ls_dyna_mpi_file_not_found(self, tmp_path: Path) -> None:
"""Should handle nonexistent input file."""
input_file = tmp_path / "nonexistent.k"
lscalc.run_ls_dyna_mpi(str(input_file), ncpu=8)
# Should print error message
# ---------------------------------------------------------------------- #
# check_ls_dyna_status
# ---------------------------------------------------------------------- #
class TestCheckLsDynaStatus:
"""Test check_ls_dyna_status function."""
def test_check_ls_dyna_status_windows(self) -> None:
"""Should check LS-DYNA status on Windows."""
with patch.object(Constants, "IS_WINDOWS", True), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="ls-dyna_mpp.exe", returncode=0)
lscalc.check_ls_dyna_status()
assert mock_run.called
def test_check_ls_dyna_status_linux(self) -> None:
"""Should check LS-DYNA status on Linux."""
with patch.object(Constants, "IS_WINDOWS", False), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="1234", returncode=0)
lscalc.check_ls_dyna_status()
assert mock_run.called
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_run_command(self, tmp_path: Path) -> None:
"""main() should handle run command."""
input_file = tmp_path / "input.k"
input_file.write_text("LS-DYNA input")
with patch("sys.argv", ["lscalc", "run", str(input_file)]), patch.object(px, "run") as mock_run:
lscalc.main()
assert mock_run.called
def test_main_run_command_with_ncpu(self, tmp_path: Path) -> None:
"""main() should handle run command with ncpu."""
input_file = tmp_path / "input.k"
input_file.write_text("LS-DYNA input")
with patch("sys.argv", ["lscalc", "run", str(input_file), "--ncpu", "8"]), patch.object(px, "run") as mock_run:
lscalc.main()
assert mock_run.called
def test_main_mpi_command(self, tmp_path: Path) -> None:
"""main() should handle mpi command."""
input_file = tmp_path / "input.k"
input_file.write_text("LS-DYNA input")
with patch("sys.argv", ["lscalc", "mpi", str(input_file)]), patch.object(px, "run") as mock_run:
lscalc.main()
assert mock_run.called
def test_main_status_command(self) -> None:
"""main() should handle status command."""
with patch("sys.argv", ["lscalc", "status"]), patch.object(px, "run") as mock_run:
lscalc.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help."""
with patch("sys.argv", ["lscalc"]):
lscalc.main()
# Should print help and return
+306
View File
@@ -0,0 +1,306 @@
"""Tests for cli.packtool module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pyflowx as px
from pyflowx.cli import packtool
# ---------------------------------------------------------------------- #
# pack_source
# ---------------------------------------------------------------------- #
class TestPackSource:
"""Test pack_source function."""
def test_pack_source_basic(self, tmp_path: Path) -> None:
"""Should pack source code."""
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "main.py").write_text("print('hello')")
output_dir = tmp_path / "output"
packtool.pack_source(project_dir, output_dir)
assert output_dir.exists()
def test_pack_source_with_pyproject(self, tmp_path: Path) -> None:
"""Should pack source with pyproject.toml."""
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "pyproject.toml").write_text("[project]\nname = 'test'")
(project_dir / "main.py").write_text("print('hello')")
output_dir = tmp_path / "output"
packtool.pack_source(project_dir, output_dir)
assert output_dir.exists()
# ---------------------------------------------------------------------- #
# pack_dependencies
# ---------------------------------------------------------------------- #
class TestPackDependencies:
"""Test pack_dependencies function."""
def test_pack_dependencies_empty(self, tmp_path: Path) -> None:
"""Should handle empty dependencies."""
lib_dir = tmp_path / "libs"
packtool.pack_dependencies(lib_dir, [])
# Should print message and return
def test_pack_dependencies_with_deps(self, tmp_path: Path) -> None:
"""Should pack dependencies."""
lib_dir = tmp_path / "libs"
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
packtool.pack_dependencies(lib_dir, ["numpy", "pandas"])
assert mock_run.called
# ---------------------------------------------------------------------- #
# pack_wheel
# ---------------------------------------------------------------------- #
class TestPackWheel:
"""Test pack_wheel function."""
def test_pack_wheel(self, tmp_path: Path) -> None:
"""Should pack wheel."""
project_dir = tmp_path / "project"
project_dir.mkdir()
(project_dir / "pyproject.toml").write_text("[project]\nname = 'test'")
output_dir = tmp_path / "dist"
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
packtool.pack_wheel(project_dir, output_dir)
assert mock_run.called
# ---------------------------------------------------------------------- #
# install_embed_python
# ---------------------------------------------------------------------- #
class TestInstallEmbedPython:
"""Test install_embed_python function."""
def test_install_embed_python_basic(self, tmp_path: Path) -> None:
"""Should install embedded Python (mocked for speed)."""
output_dir = tmp_path / "python"
# Create a mock cache file that doesn't exist (force download)
with patch("urllib.request.urlretrieve") as mock_urlretrieve, patch("zipfile.ZipFile") as mock_zipfile:
# Mock successful download
mock_urlretrieve.return_value = None
mock_zip_instance = MagicMock()
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
# Ensure cache doesn't exist by using tmp_path as cache dir
with patch.object(packtool, "DEFAULT_CACHE_DIR", str(tmp_path / ".cache")):
packtool.install_embed_python("3.10", output_dir)
# Verify download was called
assert mock_urlretrieve.called
# Verify extraction was called
assert mock_zip_instance.extractall.called
# Verify output directory was created
assert output_dir.exists()
def test_install_embed_python_with_cache(self, tmp_path: Path) -> None:
"""Should use cached Python if available."""
output_dir = tmp_path / "python"
cache_dir = tmp_path / ".cache" / "pypack"
cache_dir.mkdir(parents=True)
# Create a fake cached zip file
cache_file = cache_dir / "python-3.10.11-embed-amd64.zip"
cache_file.write_bytes(b"PK\x03\x04" + b"\x00" * 100) # Minimal ZIP header
with patch("zipfile.ZipFile") as mock_zipfile:
mock_zip_instance = MagicMock()
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
packtool.install_embed_python("3.10", output_dir)
# Verify extraction was called (using cache)
assert mock_zip_instance.extractall.called
# Verify output directory was created
assert output_dir.exists()
def test_install_embed_python_real_download(self, tmp_path: Path) -> None:
"""Should actually download and extract embedded Python (requires network).
This test performs a real download to verify the entire workflow.
It's marked to run only when network is available.
"""
import platform
import zipfile
output_dir = tmp_path / "python_real"
# Only run on Windows (embed Python is Windows-specific)
if platform.system() != "Windows":
return
# Perform real installation
packtool.install_embed_python("3.10", output_dir)
# Verify installation succeeded
assert output_dir.exists()
# Verify key files are present
expected_files = [
"python.exe",
"python310.dll",
"python310.zip",
]
for expected_file in expected_files:
file_path = output_dir / expected_file
assert file_path.exists(), f"Expected file {expected_file} not found"
assert file_path.stat().st_size > 0, f"File {expected_file} is empty"
# Verify python.exe is executable
python_exe = output_dir / "python.exe"
assert python_exe.is_file()
# Verify the installation is functional
# Check that we can at least read the zip file
python_zip = output_dir / "python310.zip"
assert zipfile.is_zipfile(python_zip)
print(f"✅ Successfully downloaded and installed embed Python to {output_dir}")
print(f" Files: {list(output_dir.iterdir())}")
def test_install_embed_python_different_versions(self, tmp_path: Path) -> None:
"""Should handle different Python versions."""
output_dir = tmp_path / "python"
with patch("urllib.request.urlretrieve") as mock_urlretrieve, patch("zipfile.ZipFile") as mock_zipfile:
mock_zip_instance = MagicMock()
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
# Test different versions
for version in ["3.8", "3.9", "3.10", "3.11", "3.12"]:
packtool.install_embed_python(version, output_dir)
assert mock_urlretrieve.called
def test_install_embed_python_creates_cache(self, tmp_path: Path) -> None:
"""Should create cache directory and file."""
output_dir = tmp_path / "python"
with patch("urllib.request.urlretrieve") as mock_urlretrieve, patch("zipfile.ZipFile") as mock_zipfile:
mock_urlretrieve.return_value = None
mock_zip_instance = MagicMock()
mock_zipfile.return_value.__enter__.return_value = mock_zip_instance
packtool.install_embed_python("3.10", output_dir)
# Verify cache directory was created
Path(packtool.DEFAULT_CACHE_DIR)
# Note: In test environment, cache might not persist due to mocking
# ---------------------------------------------------------------------- #
# create_zip_package
# ---------------------------------------------------------------------- #
class TestCreateZipPackage:
"""Test create_zip_package function."""
def test_create_zip_package(self, tmp_path: Path) -> None:
"""Should create ZIP package."""
source_dir = tmp_path / "source"
source_dir.mkdir()
(source_dir / "test.txt").write_text("test content")
output_file = tmp_path / "package.zip"
packtool.create_zip_package(source_dir, output_file)
assert output_file.exists()
# ---------------------------------------------------------------------- #
# clean_build_dir
# ---------------------------------------------------------------------- #
class TestCleanBuildDir:
"""Test clean_build_dir function."""
def test_clean_build_dir_exists(self, tmp_path: Path) -> None:
"""Should clean existing build directory."""
build_dir = tmp_path / "build"
build_dir.mkdir()
(build_dir / "test.txt").write_text("test")
packtool.clean_build_dir(build_dir)
assert not build_dir.exists()
def test_clean_build_dir_not_exists(self, tmp_path: Path) -> None:
"""Should handle nonexistent build directory."""
build_dir = tmp_path / "nonexistent"
packtool.clean_build_dir(build_dir)
# Should print message
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_src_command(self, tmp_path: Path) -> None:
"""main() should handle src command."""
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch("sys.argv", ["packtool", "src", "--project-dir", str(project_dir)]), patch.object(
px, "run"
) as mock_run:
packtool.main()
assert mock_run.called
def test_main_deps_command(self, tmp_path: Path) -> None:
"""main() should handle deps command."""
with patch("sys.argv", ["packtool", "deps", "numpy", "pandas"]), patch.object(px, "run") as mock_run:
packtool.main()
assert mock_run.called
def test_main_wheel_command(self, tmp_path: Path) -> None:
"""main() should handle wheel command."""
project_dir = tmp_path / "project"
project_dir.mkdir()
with patch("sys.argv", ["packtool", "wheel", "--project-dir", str(project_dir)]), patch.object(
px, "run"
) as mock_run:
packtool.main()
assert mock_run.called
def test_main_embed_command(self, tmp_path: Path) -> None:
"""main() should handle embed command."""
with patch("sys.argv", ["packtool", "embed", "--version", "3.10"]), patch.object(px, "run") as mock_run:
packtool.main()
assert mock_run.called
def test_main_zip_command(self, tmp_path: Path) -> None:
"""main() should handle zip command."""
source_dir = tmp_path / "source"
source_dir.mkdir()
with patch("sys.argv", ["packtool", "zip", "--source-dir", str(source_dir)]), patch.object(
px, "run"
) as mock_run:
packtool.main()
assert mock_run.called
def test_main_clean_command(self) -> None:
"""main() should handle clean command."""
with patch("sys.argv", ["packtool", "clean"]), patch.object(px, "run") as mock_run:
packtool.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help."""
with patch("sys.argv", ["packtool"]):
packtool.main()
# Should print help and return
+322
View File
@@ -0,0 +1,322 @@
"""Tests for cli.pdftool module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
import pyflowx as px
from pyflowx.cli import pdftool
# ---------------------------------------------------------------------- #
# pdf_merge
# ---------------------------------------------------------------------- #
class TestPdfMerge:
"""Test pdf_merge function."""
def test_pdf_merge_files(self, tmp_path: Path) -> None:
"""Should merge PDF files."""
pytest.importorskip("pypdf")
input_files = [tmp_path / "input1.pdf", tmp_path / "input2.pdf"]
for f in input_files:
f.write_bytes(b"PDF content")
output_file = tmp_path / "merged.pdf"
with patch("pypdf.PdfReader"), patch("pypdf.PdfWriter") as mock_writer:
mock_writer_instance = MagicMock()
mock_writer.return_value = mock_writer_instance
pdftool.pdf_merge(input_files, output_file)
assert mock_writer_instance.write.called
# ---------------------------------------------------------------------- #
# pdf_split
# ---------------------------------------------------------------------- #
class TestPdfSplit:
"""Test pdf_split function."""
def test_pdf_split_file(self, tmp_path: Path) -> None:
"""Should split PDF file."""
pytest.importorskip("pypdf")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_dir = tmp_path / "split"
with patch("pypdf.PdfReader") as mock_reader, patch("pypdf.PdfWriter"):
mock_reader_instance = MagicMock()
mock_reader.return_value = mock_reader_instance
mock_reader_instance.pages = [MagicMock()]
pdftool.pdf_split(input_file, output_dir)
assert output_dir.exists()
# ---------------------------------------------------------------------- #
# pdf_compress
# ---------------------------------------------------------------------- #
class TestPdfCompress:
"""Test pdf_compress function."""
def test_pdf_compress_file(self, tmp_path: Path) -> None:
"""Should compress PDF file."""
pytest.importorskip("fitz")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_file = tmp_path / "compressed.pdf"
with patch("fitz.open") as mock_fitz_open:
mock_doc = MagicMock()
mock_fitz_open.return_value = mock_doc
# Mock save to actually create the file
def mock_save(*args, **kwargs):
output_file.write_bytes(b"Compressed PDF")
mock_doc.save = mock_save
pdftool.pdf_compress(input_file, output_file)
assert output_file.exists()
# ---------------------------------------------------------------------- #
# pdf_extract_text
# ---------------------------------------------------------------------- #
class TestPdfExtractText:
"""Test pdf_extract_text function."""
def test_pdf_extract_text_file(self, tmp_path: Path) -> None:
"""Should extract text from PDF."""
pytest.importorskip("fitz")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_file = tmp_path / "output.txt"
with patch("fitz.open") as mock_fitz_open:
mock_doc = MagicMock()
mock_page = MagicMock()
mock_page.get_text.return_value = "Test text"
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
mock_fitz_open.return_value = mock_doc
pdftool.pdf_extract_text(input_file, output_file)
assert output_file.exists()
# ---------------------------------------------------------------------- #
# pdf_extract_images
# ---------------------------------------------------------------------- #
class TestPdfExtractImages:
"""Test pdf_extract_images function."""
def test_pdf_extract_images_file(self, tmp_path: Path) -> None:
"""Should extract images from PDF."""
pytest.importorskip("fitz")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_dir = tmp_path / "images"
with patch("fitz.open") as mock_fitz_open:
mock_doc = MagicMock()
mock_page = MagicMock()
mock_page.get_images.return_value = [[0]]
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
mock_doc.extract_image.return_value = {"image": b"image data", "ext": "png"}
mock_fitz_open.return_value = mock_doc
pdftool.pdf_extract_images(input_file, output_dir)
assert output_dir.exists()
# ---------------------------------------------------------------------- #
# pdf_add_watermark
# ---------------------------------------------------------------------- #
class TestPdfAddWatermark:
"""Test pdf_add_watermark function."""
def test_pdf_add_watermark_file(self, tmp_path: Path) -> None:
"""Should add watermark to PDF."""
pytest.importorskip("fitz")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_file = tmp_path / "watermarked.pdf"
with patch("fitz.open") as mock_fitz_open, patch("fitz.get_text_length") as mock_text_length:
mock_doc = MagicMock()
mock_page = MagicMock()
mock_page.rect = MagicMock(width=800, height=600)
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
mock_fitz_open.return_value = mock_doc
mock_text_length.return_value = 100
pdftool.pdf_add_watermark(input_file, output_file)
assert mock_doc.save.called
# ---------------------------------------------------------------------- #
# pdf_rotate
# ---------------------------------------------------------------------- #
class TestPdfRotate:
"""Test pdf_rotate function."""
def test_pdf_rotate_file_90(self, tmp_path: Path) -> None:
"""Should rotate PDF by 90 degrees."""
pytest.importorskip("fitz")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_file = tmp_path / "rotated.pdf"
with patch("fitz.open") as mock_fitz_open:
mock_doc = MagicMock()
mock_page = MagicMock()
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
mock_fitz_open.return_value = mock_doc
pdftool.pdf_rotate(input_file, output_file, rotation=90)
assert mock_doc.save.called
def test_pdf_rotate_file_180(self, tmp_path: Path) -> None:
"""Should rotate PDF by 180 degrees."""
pytest.importorskip("fitz")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_file = tmp_path / "rotated.pdf"
with patch("fitz.open") as mock_fitz_open:
mock_doc = MagicMock()
mock_page = MagicMock()
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
mock_fitz_open.return_value = mock_doc
pdftool.pdf_rotate(input_file, output_file, rotation=180)
assert mock_doc.save.called
# ---------------------------------------------------------------------- #
# pdf_crop
# ---------------------------------------------------------------------- #
class TestPdfCrop:
"""Test pdf_crop function."""
def test_pdf_crop_file(self, tmp_path: Path) -> None:
"""Should crop PDF."""
pytest.importorskip("fitz")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_file = tmp_path / "cropped.pdf"
with patch("fitz.open") as mock_fitz_open, patch("fitz.Rect"):
mock_doc = MagicMock()
mock_page = MagicMock()
mock_page.rect = MagicMock(x0=0, y0=0, x1=800, y1=600)
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
mock_fitz_open.return_value = mock_doc
pdftool.pdf_crop(input_file, output_file, margins=(10, 10, 10, 10))
assert mock_doc.save.called
# ---------------------------------------------------------------------- #
# pdf_info
# ---------------------------------------------------------------------- #
class TestPdfInfo:
"""Test pdf_info function."""
def test_pdf_info_file(self, tmp_path: Path) -> None:
"""Should show PDF info."""
pytest.importorskip("fitz")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
with patch("fitz.open") as mock_fitz_open:
mock_doc = MagicMock()
mock_doc.page_count = 10
mock_doc.metadata = {"title": "Test", "author": "Author"}
mock_fitz_open.return_value = mock_doc
pdftool.pdf_info(input_file)
assert mock_fitz_open.called
# ---------------------------------------------------------------------- #
# pdf_ocr
# ---------------------------------------------------------------------- #
class TestPdfOcr:
"""Test pdf_ocr function."""
def test_pdf_ocr_file(self, tmp_path: Path) -> None:
"""Should OCR PDF."""
pytest.importorskip("fitz")
pytest.importorskip("pytesseract")
pytest.importorskip("PIL")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_file = tmp_path / "ocr.pdf"
with patch("fitz.open") as mock_fitz_open, patch("PIL.Image.frombytes"), patch(
"pytesseract.image_to_string"
) as mock_ocr:
mock_doc = MagicMock()
mock_page = MagicMock()
mock_page.rect = MagicMock(width=800, height=600)
mock_doc.__iter__ = MagicMock(return_value=iter([mock_page]))
mock_fitz_open.return_value = mock_doc
mock_ocr.return_value = "OCR text"
pdftool.pdf_ocr(input_file, output_file)
# Should complete OCR
# ---------------------------------------------------------------------- #
# pdf_repair
# ---------------------------------------------------------------------- #
class TestPdfRepair:
"""Test pdf_repair function."""
def test_pdf_repair_file(self, tmp_path: Path) -> None:
"""Should repair PDF."""
pytest.importorskip("fitz")
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
output_file = tmp_path / "repaired.pdf"
with patch("fitz.open") as mock_fitz_open:
mock_doc = MagicMock()
mock_fitz_open.return_value = mock_doc
pdftool.pdf_repair(input_file, output_file)
assert mock_doc.save.called
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_merge_command(self, tmp_path: Path) -> None:
"""main() should handle merge command."""
input_files = [tmp_path / "input1.pdf", tmp_path / "input2.pdf"]
for f in input_files:
f.write_bytes(b"PDF content")
with patch("sys.argv", ["pdftool", "m", str(input_files[0]), str(input_files[1])]), patch.object(
px, "run"
) as mock_run:
pdftool.main()
assert mock_run.called
def test_main_split_command(self, tmp_path: Path) -> None:
"""main() should handle split command."""
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
with patch("sys.argv", ["pdftool", "s", str(input_file)]), patch.object(px, "run") as mock_run:
pdftool.main()
assert mock_run.called
def test_main_compress_command(self, tmp_path: Path) -> None:
"""main() should handle compress command."""
input_file = tmp_path / "input.pdf"
input_file.write_bytes(b"PDF content")
with patch("sys.argv", ["pdftool", "c", str(input_file)]), patch.object(px, "run") as mock_run:
pdftool.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help."""
with patch("sys.argv", ["pdftool"]):
pdftool.main()
# Should print help and return
+254
View File
@@ -0,0 +1,254 @@
"""Tests for cli.piptool module."""
from __future__ import annotations
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pyflowx as px
from pyflowx.cli import piptool
# ---------------------------------------------------------------------- #
# _get_installed_packages
# ---------------------------------------------------------------------- #
class TestGetInstalledPackages:
"""Test _get_installed_packages function."""
def test_get_installed_packages_success(self) -> None:
"""Should get installed packages."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="numpy==1.0.0\npandas==2.0.0\n", returncode=0)
result = piptool._get_installed_packages()
assert "numpy" in result
assert "pandas" in result
def test_get_installed_packages_empty(self) -> None:
"""Should handle empty output."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="", returncode=0)
result = piptool._get_installed_packages()
assert result == []
def test_get_installed_packages_error(self) -> None:
"""Should handle subprocess error."""
with patch("subprocess.run", side_effect=subprocess.SubprocessError):
result = piptool._get_installed_packages()
assert result == []
def test_get_installed_packages_oserror(self) -> None:
"""Should handle OSError."""
with patch("subprocess.run", side_effect=OSError):
result = piptool._get_installed_packages()
assert result == []
# ---------------------------------------------------------------------- #
# _expand_wildcard_packages
# ---------------------------------------------------------------------- #
class TestExpandWildcardPackages:
"""Test _expand_wildcard_packages function."""
def test_expand_wildcard_no_pattern(self) -> None:
"""Should return package name when no wildcard."""
result = piptool._expand_wildcard_packages("numpy")
assert result == ["numpy"]
def test_expand_wildcard_with_star(self) -> None:
"""Should expand wildcard with star."""
with patch.object(piptool, "_get_installed_packages", return_value=["numpy", "numpy-core", "pandas"]):
result = piptool._expand_wildcard_packages("numpy*")
assert "numpy" in result
assert "numpy-core" in result
def test_expand_wildcard_with_question(self) -> None:
"""Should expand wildcard with question mark."""
with patch.object(piptool, "_get_installed_packages", return_value=["numpy", "numba"]):
result = piptool._expand_wildcard_packages("num??")
assert len(result) > 0
def test_expand_wildcard_no_match(self) -> None:
"""Should return empty list when no match."""
with patch.object(piptool, "_get_installed_packages", return_value=["pandas", "scipy"]):
result = piptool._expand_wildcard_packages("numpy*")
assert result == []
# ---------------------------------------------------------------------- #
# _filter_protected_packages
# ---------------------------------------------------------------------- #
class TestFilterProtectedPackages:
"""Test _filter_protected_packages function."""
def test_filter_protected_packages_normal(self) -> None:
"""Should filter protected packages."""
result = piptool._filter_protected_packages(["numpy", "pandas", "pyflowx"])
assert "numpy" in result
assert "pandas" in result
assert "pyflowx" not in result
def test_filter_protected_packages_all_protected(self) -> None:
"""Should filter all protected packages."""
result = piptool._filter_protected_packages(["pyflowx", "bitool"])
assert result == []
def test_filter_protected_packages_case_insensitive(self) -> None:
"""Should filter case insensitive."""
result = piptool._filter_protected_packages(["PyFlowX", "BITOOL"])
assert result == []
# ---------------------------------------------------------------------- #
# pip_uninstall
# ---------------------------------------------------------------------- #
class TestPipUninstall:
"""Test pip_uninstall function."""
def test_pip_uninstall_single_package(self) -> None:
"""Should uninstall single package."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
piptool.pip_uninstall(["numpy"])
assert mock_run.called
def test_pip_uninstall_multiple_packages(self) -> None:
"""Should uninstall multiple packages."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
piptool.pip_uninstall(["numpy", "pandas", "scipy"])
# Should call pip uninstall
assert mock_run.called
def test_pip_uninstall_with_wildcard(self) -> None:
"""Should handle wildcard in package name."""
with patch.object(piptool, "_expand_wildcard_packages", return_value=["numpy", "numpy-core"]), patch(
"subprocess.run"
) as mock_run:
mock_run.return_value = MagicMock(returncode=0)
piptool.pip_uninstall(["numpy*"])
assert mock_run.called
def test_pip_uninstall_empty_packages(self) -> None:
"""Should handle empty packages list."""
with patch.object(piptool, "_expand_wildcard_packages", return_value=[]):
piptool.pip_uninstall(["nonexistent*"])
# Should not call subprocess.run
def test_pip_uninstall_all_protected(self) -> None:
"""Should handle all protected packages."""
piptool.pip_uninstall(["pyflowx"])
# Should not call subprocess.run
# ---------------------------------------------------------------------- #
# pip_reinstall
# ---------------------------------------------------------------------- #
class TestPipReinstall:
"""Test pip_reinstall function."""
def test_pip_reinstall_single_package(self) -> None:
"""Should reinstall single package."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
piptool.pip_reinstall(["numpy"])
# Should call pip uninstall and pip install
assert mock_run.call_count == 2
def test_pip_reinstall_offline(self) -> None:
"""Should reinstall packages offline."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
piptool.pip_reinstall(["numpy"], offline=True)
# Should call pip install with offline flags
assert mock_run.called
def test_pip_reinstall_all_protected(self) -> None:
"""Should handle all protected packages."""
piptool.pip_reinstall(["pyflowx"])
# Should not call subprocess.run
# ---------------------------------------------------------------------- #
# pip_download
# ---------------------------------------------------------------------- #
class TestPipDownload:
"""Test pip_download function."""
def test_pip_download_single_package(self) -> None:
"""Should download single package."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
piptool.pip_download(["numpy"])
assert mock_run.called
def test_pip_download_offline(self) -> None:
"""Should download packages offline."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
piptool.pip_download(["numpy"], offline=True)
# Should call pip download with offline flags
assert mock_run.called
# ---------------------------------------------------------------------- #
# pip_freeze
# ---------------------------------------------------------------------- #
class TestPipFreeze:
"""Test pip_freeze function."""
def test_pip_freeze(self, tmp_path: Path) -> None:
"""Should freeze dependencies."""
with patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(stdout="numpy==1.0.0\npandas==2.0.0", returncode=0)
piptool.pip_freeze()
assert mock_run.called
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_install_command(self) -> None:
"""main() should handle install command."""
with patch("sys.argv", ["piptool", "i", "numpy", "pandas"]), patch.object(px, "run") as mock_run:
piptool.main()
assert mock_run.called
def test_main_uninstall_command(self) -> None:
"""main() should handle uninstall command."""
with patch("sys.argv", ["piptool", "u", "numpy"]), patch.object(px, "run") as mock_run:
piptool.main()
assert mock_run.called
def test_main_reinstall_command(self) -> None:
"""main() should handle reinstall command."""
with patch("sys.argv", ["piptool", "r", "numpy"]), patch.object(px, "run") as mock_run:
piptool.main()
assert mock_run.called
def test_main_download_command(self) -> None:
"""main() should handle download command."""
with patch("sys.argv", ["piptool", "d", "numpy"]), patch.object(px, "run") as mock_run:
piptool.main()
assert mock_run.called
def test_main_upgrade_command(self) -> None:
"""main() should handle upgrade command."""
with patch("sys.argv", ["piptool", "up"]), patch.object(px, "run") as mock_run:
piptool.main()
assert mock_run.called
def test_main_freeze_command(self) -> None:
"""main() should handle freeze command."""
with patch("sys.argv", ["piptool", "f"]), patch.object(px, "run") as mock_run:
piptool.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help."""
with patch("sys.argv", ["piptool"]):
piptool.main()
# Should print help and return
+12 -43
View File
@@ -67,37 +67,6 @@ class TestMaturinBuildCmd:
assert "-Zbuild-std" not in cmd
# ---------------------------------------------------------------------- #
# check helper
# ---------------------------------------------------------------------- #
class TestCheckHelper:
"""Test check helper function."""
def test_check_returns_condition(self) -> None:
"""check() should return a Condition callable."""
cond = pymake.check("python")
assert callable(cond)
def test_check_uses_has_installed(self) -> None:
"""check() should use BuiltinConditions.HAS_INSTALLED."""
cond = pymake.check("python")
# The condition should be a callable that returns a bool
result = cond()
assert isinstance(result, bool)
def test_check_for_nonexistent_app(self) -> None:
"""check() for a nonexistent app should return False."""
cond = pymake.check("definitely_not_installed_app_xyz")
assert cond() is False
def test_check_for_python(self) -> None:
"""check() for python should return True (python is always available)."""
cond = pymake.check("python")
# On some systems, 'python' might not be in PATH, but 'python3' might be
# Just verify it returns a bool
assert isinstance(cond(), bool)
# ---------------------------------------------------------------------- #
# TaskSpec definitions
# ---------------------------------------------------------------------- #
@@ -108,23 +77,25 @@ class TestTaskSpecDefinitions:
"""uv_build spec should be properly defined."""
assert pymake.uv_build.name == "uv_build"
assert pymake.uv_build.cmd == ["uv", "build"]
assert len(pymake.uv_build.conditions) == 1
assert pymake.uv_build.skip_if_missing is True
def test_maturin_build_spec(self) -> None:
"""maturin_build spec should be properly defined."""
assert pymake.maturin_build.name == "maturin_build"
assert isinstance(pymake.maturin_build.cmd, list)
assert len(pymake.maturin_build.conditions) == 1
assert pymake.maturin_build.skip_if_missing is True
def test_uv_sync_spec(self) -> None:
"""uv_sync spec should be properly defined."""
assert pymake.uv_sync.name == "uv_sync"
assert pymake.uv_sync.cmd == ["uv", "sync"]
assert pymake.uv_sync.skip_if_missing is True
def test_git_clean_spec(self) -> None:
"""git_clean spec should be properly defined."""
assert pymake.git_clean.name == "git_clean"
assert pymake.git_clean.cmd == ["gitt", "c"]
assert pymake.git_clean.skip_if_missing is True
def test_test_spec(self) -> None:
"""test spec should be properly defined."""
@@ -133,6 +104,7 @@ class TestTaskSpecDefinitions:
assert "pytest" in pymake.test.cmd
assert "-m" in pymake.test.cmd
assert "not slow" in pymake.test.cmd
assert pymake.test.skip_if_missing is True
def test_test_fast_spec(self) -> None:
"""test_fast spec should be properly defined."""
@@ -140,6 +112,7 @@ class TestTaskSpecDefinitions:
assert isinstance(pymake.test_fast.cmd, list)
assert "pytest" in pymake.test_fast.cmd
assert "-n" not in pymake.test_fast.cmd # test_fast doesn't use parallel
assert pymake.test_fast.skip_if_missing is True
def test_test_coverage_spec(self) -> None:
"""test_coverage spec should be properly defined."""
@@ -147,6 +120,7 @@ class TestTaskSpecDefinitions:
assert isinstance(pymake.test_coverage.cmd, list)
assert "pytest" in pymake.test_coverage.cmd
assert "--cov" in pymake.test_coverage.cmd
assert pymake.test_coverage.skip_if_missing is True
def test_ruff_lint_spec(self) -> None:
"""ruff_lint spec should be properly defined."""
@@ -154,27 +128,20 @@ class TestTaskSpecDefinitions:
assert isinstance(pymake.ruff_lint.cmd, list)
assert "ruff" in pymake.ruff_lint.cmd
assert "check" in pymake.ruff_lint.cmd
def test_mypy_check_spec(self) -> None:
"""mypy_check spec should be properly defined."""
assert pymake.mypy_check.name == "typecheck"
assert pymake.mypy_check.cmd == ["mypy", "."]
def test_ty_check_spec(self) -> None:
"""ty_check spec should be properly defined."""
assert pymake.ty_check.name == "ty_check"
assert pymake.ty_check.cmd == ["ty", "check", "."]
assert pymake.ruff_lint.skip_if_missing is True
def test_doc_spec(self) -> None:
"""doc spec should be properly defined."""
assert pymake.doc.name == "doc"
assert isinstance(pymake.doc.cmd, list)
assert "sphinx-build" in pymake.doc.cmd
assert pymake.doc.skip_if_missing is True
def test_hatch_publish_spec(self) -> None:
"""hatch_publish spec should be properly defined."""
assert pymake.hatch_publish.name == "publish_python"
assert pymake.hatch_publish.cmd == ["hatch", "publish"]
assert pymake.hatch_publish.skip_if_missing is True
def test_twine_publish_spec(self) -> None:
"""twine_publish spec should be properly defined."""
@@ -182,11 +149,13 @@ class TestTaskSpecDefinitions:
assert isinstance(pymake.twine_publish.cmd, list)
assert "twine" in pymake.twine_publish.cmd
assert "upload" in pymake.twine_publish.cmd
assert pymake.twine_publish.skip_if_missing is True
def test_tox_spec(self) -> None:
"""tox spec should be properly defined."""
assert pymake.tox.name == "tox"
assert pymake.tox.cmd == ["tox", "-p", "auto"]
assert pymake.tox.skip_if_missing is True
# ---------------------------------------------------------------------- #
+123
View File
@@ -0,0 +1,123 @@
"""Tests for cli.screenshot module."""
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock, patch
import pyflowx as px
from pyflowx.cli import screenshot
from pyflowx.conditions import Constants
# ---------------------------------------------------------------------- #
# get_screenshot_path
# ---------------------------------------------------------------------- #
class TestGetScreenshotPath:
"""Test get_screenshot_path function."""
def test_get_screenshot_path_with_filename(self, tmp_path: Path) -> None:
"""Should get screenshot path with filename."""
with patch.object(Path, "home", return_value=tmp_path):
result = screenshot.get_screenshot_path("test.png")
assert result.name == "test.png"
def test_get_screenshot_path_without_filename(self, tmp_path: Path) -> None:
"""Should get screenshot path without filename."""
with patch.object(Path, "home", return_value=tmp_path):
result = screenshot.get_screenshot_path()
assert "screenshot_" in result.name
assert result.suffix == ".png"
# ---------------------------------------------------------------------- #
# take_screenshot_full
# ---------------------------------------------------------------------- #
class TestTakeScreenshotFull:
"""Test take_screenshot_full function."""
def test_take_screenshot_full_windows(self, tmp_path: Path) -> None:
"""Should take full screenshot on Windows."""
with patch.object(Constants, "IS_WINDOWS", True), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
screenshot.take_screenshot_full()
assert mock_run.called
def test_take_screenshot_full_macos(self, tmp_path: Path) -> None:
"""Should take full screenshot on macOS."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", True), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
screenshot.take_screenshot_full()
assert mock_run.called
def test_take_screenshot_full_linux(self, tmp_path: Path) -> None:
"""Should take full screenshot on Linux."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
screenshot.take_screenshot_full()
assert mock_run.called
# ---------------------------------------------------------------------- #
# take_screenshot_area
# ---------------------------------------------------------------------- #
class TestTakeScreenshotArea:
"""Test take_screenshot_area function."""
def test_take_screenshot_area_windows(self, tmp_path: Path) -> None:
"""Should take area screenshot on Windows."""
with patch.object(Constants, "IS_WINDOWS", True), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
screenshot.take_screenshot_area()
assert mock_run.called
def test_take_screenshot_area_macos(self, tmp_path: Path) -> None:
"""Should take area screenshot on macOS."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", True), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
screenshot.take_screenshot_area()
assert mock_run.called
def test_take_screenshot_area_linux(self, tmp_path: Path) -> None:
"""Should take area screenshot on Linux."""
with patch.object(Constants, "IS_WINDOWS", False), patch.object(Constants, "IS_MACOS", False), patch.object(
Path, "home", return_value=tmp_path
), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
screenshot.take_screenshot_area()
assert mock_run.called
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_full_command(self, tmp_path: Path) -> None:
"""main() should handle full command."""
with patch("sys.argv", ["screenshot", "full"]), patch.object(px, "run") as mock_run:
screenshot.main()
assert mock_run.called
def test_main_area_command(self, tmp_path: Path) -> None:
"""main() should handle area command."""
with patch("sys.argv", ["screenshot", "area"]), patch.object(px, "run") as mock_run:
screenshot.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help."""
with patch("sys.argv", ["screenshot"]):
screenshot.main()
# Should print help and return
+163
View File
@@ -0,0 +1,163 @@
"""Tests for cli.sshcopyid module."""
from __future__ import annotations
import subprocess
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
import pyflowx as px
from pyflowx.cli import sshcopyid
# ---------------------------------------------------------------------- #
# ssh_copy_id
# ---------------------------------------------------------------------- #
class TestSshCopyId:
"""Test ssh_copy_id function."""
def test_ssh_copy_id_pub_key_not_exists(self, tmp_path: Path) -> None:
"""Should handle nonexistent public key."""
with patch.object(Path, "expanduser", return_value=tmp_path / "nonexistent.pub"), pytest.raises(SystemExit):
sshcopyid.ssh_copy_id("localhost", "user", "password")
def test_ssh_copy_id_sshpass_not_found(self, tmp_path: Path) -> None:
"""Should handle sshpass not found."""
pub_key = tmp_path / "id_rsa.pub"
pub_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=pub_key), patch(
"subprocess.run", side_effect=FileNotFoundError
), pytest.raises(SystemExit):
sshcopyid.ssh_copy_id("localhost", "user", "password")
def test_ssh_copy_id_timeout(self, tmp_path: Path) -> None:
"""Should handle SSH timeout."""
pub_key = tmp_path / "id_rsa.pub"
pub_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=pub_key), patch(
"subprocess.run", side_effect=subprocess.TimeoutExpired("cmd", 30)
), pytest.raises(SystemExit):
sshcopyid.ssh_copy_id("localhost", "user", "password")
def test_ssh_copy_id_process_error(self, tmp_path: Path) -> None:
"""Should handle SSH process error."""
pub_key = tmp_path / "id_rsa.pub"
pub_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=pub_key), patch(
"subprocess.run", side_effect=subprocess.CalledProcessError(1, "cmd")
), pytest.raises(SystemExit):
sshcopyid.ssh_copy_id("localhost", "user", "password")
def test_ssh_copy_id_success(self, tmp_path: Path) -> None:
"""Should deploy SSH key successfully."""
pub_key = tmp_path / "id_rsa.pub"
pub_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=pub_key), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
sshcopyid.ssh_copy_id("localhost", "user", "password")
assert mock_run.called
def test_ssh_copy_id_with_custom_port(self, tmp_path: Path) -> None:
"""Should handle custom port."""
pub_key = tmp_path / "id_rsa.pub"
pub_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=pub_key), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
sshcopyid.ssh_copy_id("localhost", "user", "password", port=2222)
# Verify port is used
call_args = mock_run.call_args[0][0]
assert "2222" in call_args
def test_ssh_copy_id_with_custom_keypath(self, tmp_path: Path) -> None:
"""Should handle custom keypath."""
custom_key = tmp_path / "custom.pub"
custom_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=custom_key), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
sshcopyid.ssh_copy_id("localhost", "user", "password", keypath=str(custom_key))
assert mock_run.called
def test_ssh_copy_id_with_custom_timeout(self, tmp_path: Path) -> None:
"""Should handle custom timeout."""
pub_key = tmp_path / "id_rsa.pub"
pub_key.write_text("ssh-rsa AAAAB3...")
with patch.object(Path, "expanduser", return_value=pub_key), patch("subprocess.run") as mock_run:
mock_run.return_value = MagicMock(returncode=0)
sshcopyid.ssh_copy_id("localhost", "user", "password", timeout=60)
# Verify timeout is used in ConnectTimeout option
call_args = mock_run.call_args[0][0]
assert "ConnectTimeout=60" in call_args
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_with_required_args(self) -> None:
"""main() should handle required arguments."""
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password"]), patch.object(
px, "run"
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
sshcopyid.main()
assert mock_run.called
graph = mock_run.call_args[0][0]
assert isinstance(graph, px.Graph)
def test_main_with_custom_port(self) -> None:
"""main() should handle custom port argument."""
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password", "--port", "2222"]), patch.object(
px, "run"
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
sshcopyid.main()
assert mock_run.called
def test_main_with_custom_keypath(self) -> None:
"""main() should handle custom keypath argument."""
with patch(
"sys.argv", ["sshcopyid", "localhost", "user", "password", "--keypath", "/custom/key.pub"]
), patch.object(px, "run") as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
sshcopyid.main()
assert mock_run.called
def test_main_with_custom_timeout(self) -> None:
"""main() should handle custom timeout argument."""
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password", "--timeout", "60"]), patch.object(
px, "run"
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
sshcopyid.main()
assert mock_run.called
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help and exit."""
with patch("sys.argv", ["sshcopyid"]), pytest.raises(SystemExit) as exc_info:
sshcopyid.main()
assert exc_info.value.code == 2
def test_main_creates_task_spec_with_correct_name(self) -> None:
"""main() should create TaskSpec with correct name."""
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password"]), patch.object(
px, "run"
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
sshcopyid.main()
graph = mock_run.call_args[0][0]
task_names = list(graph.all_specs().keys())
assert "ssh_deploy" in task_names
def test_main_uses_thread_strategy(self) -> None:
"""main() should use thread strategy."""
with patch("sys.argv", ["sshcopyid", "localhost", "user", "password"]), patch.object(
px, "run"
) as mock_run, patch.object(sshcopyid, "ssh_copy_id"):
sshcopyid.main()
assert mock_run.call_args[1]["strategy"] == "thread"
+102
View File
@@ -0,0 +1,102 @@
"""Tests for cli.taskkill module."""
from __future__ import annotations
from unittest.mock import patch
import pytest
import pyflowx as px
from pyflowx.cli import taskkill
from pyflowx.conditions import Constants
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_with_single_process(self) -> None:
"""main() should handle single process argument."""
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
taskkill.main()
assert mock_run.called
graph = mock_run.call_args[0][0]
assert isinstance(graph, px.Graph)
def test_main_with_multiple_processes(self) -> None:
"""main() should handle multiple process arguments."""
with patch("sys.argv", ["taskkill", "chrome.exe", "python.exe", "node.exe"]), patch.object(
px, "run"
) as mock_run:
taskkill.main()
assert mock_run.called
graph = mock_run.call_args[0][0]
assert isinstance(graph, px.Graph)
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help and exit."""
with patch("sys.argv", ["taskkill"]), pytest.raises(SystemExit) as exc_info:
taskkill.main()
assert exc_info.value.code == 2
def test_main_creates_task_specs_with_correct_names(self) -> None:
"""main() should create TaskSpecs with correct names."""
with patch("sys.argv", ["taskkill", "chrome.exe", "python.exe"]), patch.object(px, "run") as mock_run:
taskkill.main()
graph = mock_run.call_args[0][0]
task_names = list(graph.all_specs().keys())
assert "kill_chrome.exe" in task_names
assert "kill_python.exe" in task_names
def test_main_uses_thread_strategy(self) -> None:
"""main() should use thread strategy."""
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
taskkill.main()
assert mock_run.call_args[1]["strategy"] == "thread"
def test_main_windows_command_format(self) -> None:
"""main() should use Windows command format on Windows."""
if Constants.IS_WINDOWS:
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
taskkill.main()
graph = mock_run.call_args[0][0]
specs = graph.all_specs()
# Check that command includes Windows taskkill format
for spec in specs.values():
assert spec.cmd[0] == "taskkill"
assert spec.cmd[1] == "/f"
assert spec.cmd[2] == "/im"
def test_main_linux_command_format(self) -> None:
"""main() should use Linux command format on Linux."""
with patch.object(Constants, "IS_WINDOWS", False), patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(
px, "run"
) as mock_run:
taskkill.main()
graph = mock_run.call_args[0][0]
specs = graph.all_specs()
# Check that command includes Linux pkill format
for spec in specs.values():
assert spec.cmd[0] == "pkill"
assert spec.cmd[1] == "-f"
def test_main_tasks_have_verbose_true(self) -> None:
"""main() should create tasks with verbose=True."""
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
taskkill.main()
graph = mock_run.call_args[0][0]
specs = graph.all_specs()
for spec in specs.values():
assert spec.verbose is True
def test_main_adds_wildcard_to_process_name(self) -> None:
"""main() should add wildcard to process name."""
with patch("sys.argv", ["taskkill", "chrome.exe"]), patch.object(px, "run") as mock_run:
taskkill.main()
graph = mock_run.call_args[0][0]
specs = graph.all_specs()
# Check that wildcard is added
for spec in specs.values():
assert spec.cmd[-1].endswith("*")
+106
View File
@@ -0,0 +1,106 @@
"""Tests for cli.which module."""
from __future__ import annotations
import shutil
from pathlib import Path
from unittest.mock import patch
import pytest
import pyflowx as px
from pyflowx.cli import which
# ---------------------------------------------------------------------- #
# which_command
# ---------------------------------------------------------------------- #
class TestWhichCommand:
"""Test which_command function."""
def test_returns_path_when_command_found(self, capsys: pytest.CaptureFixture[str]) -> None:
"""Should return Path when command is found."""
with patch.object(shutil, "which", return_value="/usr/bin/python"):
result = which.which_command("python")
assert result == Path("/usr/bin/python")
captured = capsys.readouterr()
assert "匹配路径" in captured.out
assert "/usr/bin/python" in captured.out
def test_returns_none_when_command_not_found(self, capsys: pytest.CaptureFixture[str]) -> None:
"""Should return None when command is not found."""
with patch.object(shutil, "which", return_value=None):
result = which.which_command("nonexistent_cmd")
assert result is None
captured = capsys.readouterr()
assert "未找到" in captured.out
assert "nonexistent_cmd" in captured.out
def test_prints_match_path_on_success(self, capsys: pytest.CaptureFixture[str]) -> None:
"""Should print '匹配路径: - <path>' on success."""
with patch.object(shutil, "which", return_value="C:\\Python\\python.exe"):
_ = which.which_command("python")
captured = capsys.readouterr()
assert "匹配路径: - C:\\Python\\python.exe" in captured.out
def test_prints_not_found_on_failure(self, capsys: pytest.CaptureFixture[str]) -> None:
"""Should print '<command>: 未找到' on failure."""
with patch.object(shutil, "which", return_value=None):
_ = which.which_command("missing")
captured = capsys.readouterr()
assert "missing: 未找到" in captured.out
# ---------------------------------------------------------------------- #
# main function
# ---------------------------------------------------------------------- #
class TestMain:
"""Test main function."""
def test_main_with_single_command(self) -> None:
"""main() should handle single command argument."""
with patch("sys.argv", ["which", "python"]), patch.object(
shutil, "which", return_value="/usr/bin/python"
), patch.object(px, "run") as mock_run:
which.main()
# Should create a graph with one task
assert mock_run.called
graph = mock_run.call_args[0][0]
assert isinstance(graph, px.Graph)
def test_main_with_multiple_commands(self) -> None:
"""main() should handle multiple command arguments."""
with patch("sys.argv", ["which", "python", "pip", "node"]), patch.object(
shutil, "which", return_value="/usr/bin/cmd"
), patch.object(px, "run") as mock_run:
which.main()
# Should create a graph with three tasks
assert mock_run.called
graph = mock_run.call_args[0][0]
assert isinstance(graph, px.Graph)
def test_main_with_no_args_shows_help(self) -> None:
"""main() with no args should show help and exit."""
with patch("sys.argv", ["which"]), pytest.raises(SystemExit) as exc_info:
which.main()
assert exc_info.value.code == 2
def test_main_creates_task_specs_with_correct_names(self) -> None:
"""main() should create TaskSpecs with correct names."""
with patch("sys.argv", ["which", "git", "npm"]), patch.object(
shutil, "which", return_value="/usr/bin/cmd"
), patch.object(px, "run") as mock_run:
which.main()
graph = mock_run.call_args[0][0]
# Check that task names are correct
task_names = list(graph.all_specs().keys())
assert "which_git" in task_names
assert "which_npm" in task_names
def test_main_uses_thread_strategy(self) -> None:
"""main() should use thread strategy."""
with patch("sys.argv", ["which", "python"]), patch.object(
shutil, "which", return_value="/usr/bin/python"
), patch.object(px, "run") as mock_run:
which.main()
assert mock_run.call_args[1]["strategy"] == "thread"
+499
View File
@@ -0,0 +1,499 @@
"""Tests for command reference feature in CliRunner."""
from __future__ import annotations
import pytest
import pyflowx as px
class TestCommandReferences:
"""Test string references in Graph.from_specs."""
def test_simple_command_reference(self) -> None:
"""Should expand simple command reference."""
build_task = px.TaskSpec("build", cmd=["echo", "building"])
test_task = px.TaskSpec("test", cmd=["echo", "testing"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"build": px.Graph.from_specs([build_task]),
"test": px.Graph.from_specs([test_task]),
"all": px.Graph.from_specs([build_task, "test"]),
},
)
# Check that 'all' command has both tasks
all_tasks = list(runner.graphs["all"].all_specs().keys())
assert "build" in all_tasks
assert "test" in all_tasks
assert len(all_tasks) == 2
def test_multiple_command_references(self) -> None:
"""Should expand multiple command references."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"])
task3 = px.TaskSpec("task3", cmd=["echo", "3"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1]),
"cmd2": px.Graph.from_specs([task2]),
"cmd3": px.Graph.from_specs([task3]),
"all": px.Graph.from_specs(["cmd1", "cmd2", "cmd3"]),
},
)
# Check that 'all' command has all tasks
all_tasks = list(runner.graphs["all"].all_specs().keys())
assert set(all_tasks) == {"task1", "task2", "task3"}
def test_specific_task_reference(self) -> None:
"""Should expand specific task reference."""
lint_task = px.TaskSpec("lint", cmd=["echo", "linting"])
format_task = px.TaskSpec("format", cmd=["echo", "formatting"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"lint": px.Graph.from_specs([lint_task, format_task]),
"quick": px.Graph.from_specs(["lint.lint"]),
},
)
# Check that 'quick' command only has lint task
quick_tasks = list(runner.graphs["quick"].all_specs().keys())
assert quick_tasks == ["lint"]
def test_nested_command_reference(self) -> None:
"""Should expand nested command references."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"])
task3 = px.TaskSpec("task3", cmd=["echo", "3"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1]),
"cmd2": px.Graph.from_specs(["cmd1", task2]),
"cmd3": px.Graph.from_specs(["cmd2", task3]),
},
)
# Check that 'cmd3' has all tasks
cmd3_tasks = list(runner.graphs["cmd3"].all_specs().keys())
assert set(cmd3_tasks) == {"task1", "task2", "task3"}
def test_circular_reference_error(self) -> None:
"""Should raise error for circular references."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
with pytest.raises(ValueError, match="循环引用"):
px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs(["cmd1", task1]),
},
)
def test_invalid_command_reference_error(self) -> None:
"""Should raise error for invalid command reference."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
with pytest.raises(ValueError, match="引用的命令 'invalid' 不存在"):
px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs(["invalid", task1]),
},
)
def test_invalid_task_reference_error(self) -> None:
"""Should raise error for invalid task reference."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
with pytest.raises(ValueError, match="任务 'invalid' 不存在于命令 'cmd1'"):
px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1]),
"cmd2": px.Graph.from_specs(["cmd1.invalid"]),
},
)
def test_reference_preserves_dependencies(self) -> None:
"""Should preserve dependencies when expanding references."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"], depends_on=("task1",))
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1, task2]),
"cmd2": px.Graph.from_specs(["cmd1"]),
},
)
# Check that dependencies are preserved
cmd2_deps = runner.graphs["cmd2"].deps
assert cmd2_deps["task2"] == ("task1",)
def test_mixed_references_and_tasks(self) -> None:
"""Should handle mixed references and direct tasks."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"])
task3 = px.TaskSpec("task3", cmd=["echo", "3"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1, task2]),
"cmd2": px.Graph.from_specs(["cmd1", task3]),
},
)
# Check that 'cmd2' has all tasks
cmd2_tasks = list(runner.graphs["cmd2"].all_specs().keys())
assert set(cmd2_tasks) == {"task1", "task2", "task3"}
def test_execution_order_with_references(self) -> None:
"""Should execute references in correct order."""
task1 = px.TaskSpec("task1", cmd=["echo", "step1"])
task2 = px.TaskSpec("task2", cmd=["echo", "step2"])
task3 = px.TaskSpec("task3", cmd=["echo", "step3"])
task4 = px.TaskSpec("task4", cmd=["echo", "step4"])
task5 = px.TaskSpec("task5", cmd=["echo", "step5"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1]),
"cmd2": px.Graph.from_specs([task2, task3]),
"cmd3": px.Graph.from_specs([task4]),
"ordered": px.Graph.from_specs(["cmd1", "cmd2", "cmd3", task5]),
},
)
# Check execution order through layers
layers = runner.graphs["ordered"].layers()
# Layer 1 should have task1 (cmd1)
assert "task1" in layers[0]
# Layer 2 should have task2 and task3 (cmd2)
assert "task2" in layers[1]
assert "task3" in layers[1]
# Layer 3 should have task4 (cmd3)
assert "task4" in layers[2]
# Layer 4 should have task5 (original task)
assert "task5" in layers[3]
# Verify total layers
assert len(layers) == 4
def test_execution_order_multiple_original_tasks(self) -> None:
"""Should execute multiple original TaskSpecs in correct order."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"])
task3 = px.TaskSpec("task3", cmd=["echo", "3"])
task4 = px.TaskSpec("task4", cmd=["echo", "4"])
task5 = px.TaskSpec("task5", cmd=["echo", "5"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1]),
"cmd2": px.Graph.from_specs([task2]),
"all": px.Graph.from_specs(["cmd1", "cmd2", task3, task4, task5]),
},
)
# Check execution order through layers
layers = runner.graphs["all"].layers()
# Layer 1: task1 (cmd1)
assert "task1" in layers[0]
# Layer 2: task2 (cmd2)
assert "task2" in layers[1]
# Layer 3: task3 (first original TaskSpec)
assert "task3" in layers[2]
# Layer 4: task4 (second original TaskSpec)
assert "task4" in layers[3]
# Layer 5: task5 (third original TaskSpec)
assert "task5" in layers[4]
# Verify total layers
assert len(layers) == 5
def test_execution_order_with_internal_dependencies(self) -> None:
"""Should preserve internal dependencies within referenced commands."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"], depends_on=("task1",))
task3 = px.TaskSpec("task3", cmd=["echo", "3"])
task4 = px.TaskSpec("task4", cmd=["echo", "4"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1, task2]),
"cmd2": px.Graph.from_specs([task3]),
"all": px.Graph.from_specs(["cmd1", "cmd2", task4]),
},
)
# Check execution order through layers
layers = runner.graphs["all"].layers()
# Layer 1: task1
assert "task1" in layers[0]
# Layer 2: task2 (depends on task1)
assert "task2" in layers[1]
# Layer 3: task3 (cmd2, depends on task2)
assert "task3" in layers[2]
# Layer 4: task4 (original TaskSpec, depends on task3)
assert "task4" in layers[3]
# Verify total layers
assert len(layers) == 4
def test_execution_order_pymake_bump_scenario(self) -> None:
"""Should execute pymake bump command in correct order."""
# Simulate pymake bump scenario
git_clean = px.TaskSpec("git_clean", cmd=["echo", "clean"])
typecheck = px.TaskSpec("typecheck", cmd=["echo", "typecheck"])
lint = px.TaskSpec("lint", cmd=["echo", "lint"])
format_task = px.TaskSpec("format", cmd=["echo", "format"], depends_on=("lint",))
git_add_all = px.TaskSpec("git_add_all", cmd=["echo", "git add -A"])
bump = px.TaskSpec("bumpversion", cmd=["echo", "bumpversion -t"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"c": px.Graph.from_specs([git_clean]),
"tc": px.Graph.from_specs([typecheck, "lint"]),
"lint": px.Graph.from_specs([lint, format_task]),
"bump": px.Graph.from_specs(["c", "tc", git_add_all, bump]),
},
)
# Check execution order through layers
layers = runner.graphs["bump"].layers()
# Layer 1: git_clean (c)
assert "git_clean" in layers[0]
# Layer 2: lint (tc.lint, depends on git_clean)
assert "lint" in layers[1]
# Layer 3: format (tc.lint.format, depends on lint)
assert "format" in layers[2]
# Layer 4: typecheck (tc.typecheck, depends on format)
assert "typecheck" in layers[3]
# Layer 5: git_add_all (original TaskSpec, depends on typecheck)
assert "git_add_all" in layers[4]
# Layer 6: bumpversion (original TaskSpec, depends on git_add_all)
assert "bumpversion" in layers[5]
# Verify total layers
assert len(layers) == 6
def test_execution_order_only_references(self) -> None:
"""Should execute only references without original TaskSpecs."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"])
task3 = px.TaskSpec("task3", cmd=["echo", "3"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1]),
"cmd2": px.Graph.from_specs([task2]),
"cmd3": px.Graph.from_specs([task3]),
"all": px.Graph.from_specs(["cmd1", "cmd2", "cmd3"]),
},
)
# Check execution order through layers
layers = runner.graphs["all"].layers()
# Layer 1: task1 (cmd1)
assert "task1" in layers[0]
# Layer 2: task2 (cmd2, depends on task1)
assert "task2" in layers[1]
# Layer 3: task3 (cmd3, depends on task2)
assert "task3" in layers[2]
# Verify total layers
assert len(layers) == 3
def test_execution_order_only_original_tasks(self) -> None:
"""Should execute only original TaskSpecs without references."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"])
task3 = px.TaskSpec("task3", cmd=["echo", "3"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"all": px.Graph.from_specs([task1, task2, task3]),
},
)
# Check execution order through layers
layers = runner.graphs["all"].layers()
# All tasks should be in layer 1 (no dependencies)
assert "task1" in layers[0]
assert "task2" in layers[0]
assert "task3" in layers[0]
# Verify total layers
assert len(layers) == 1
def test_execution_order_single_reference(self) -> None:
"""Should execute single reference correctly."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1, task2]),
"all": px.Graph.from_specs(["cmd1"]),
},
)
# Check execution order through layers
layers = runner.graphs["all"].layers()
# Should have the same structure as cmd1
assert "task1" in layers[0]
assert "task2" in layers[0]
# Verify total layers
assert len(layers) == 1
def test_execution_order_deep_nesting(self) -> None:
"""Should execute deeply nested references correctly."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"])
task3 = px.TaskSpec("task3", cmd=["echo", "3"])
task4 = px.TaskSpec("task4", cmd=["echo", "4"])
task5 = px.TaskSpec("task5", cmd=["echo", "5"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1]),
"cmd2": px.Graph.from_specs(["cmd1", task2]),
"cmd3": px.Graph.from_specs(["cmd2", task3]),
"cmd4": px.Graph.from_specs(["cmd3", task4]),
"cmd5": px.Graph.from_specs(["cmd4", task5]),
},
)
# Check execution order through layers
layers = runner.graphs["cmd5"].layers()
# Should execute in order: task1 -> task2 -> task3 -> task4 -> task5
assert "task1" in layers[0]
assert "task2" in layers[1]
assert "task3" in layers[2]
assert "task4" in layers[3]
assert "task5" in layers[4]
# Verify total layers
assert len(layers) == 5
def test_execution_order_with_parallel_tasks_in_reference(self) -> None:
"""Should handle parallel tasks within referenced commands."""
task1 = px.TaskSpec("task1", cmd=["echo", "1"])
task2 = px.TaskSpec("task2", cmd=["echo", "2"])
task3 = px.TaskSpec("task3", cmd=["echo", "3"])
task4 = px.TaskSpec("task4", cmd=["echo", "4"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"cmd1": px.Graph.from_specs([task1, task2]), # Parallel tasks
"cmd2": px.Graph.from_specs([task3, task4]), # Parallel tasks
"all": px.Graph.from_specs(["cmd1", "cmd2"]),
},
)
# Check execution order through layers
layers = runner.graphs["all"].layers()
# Layer 1: task1 and task2 (cmd1, parallel)
assert "task1" in layers[0]
assert "task2" in layers[0]
# Layer 2: task3 and task4 (cmd2, depends on cmd1's last task)
# Note: Both task3 and task4 should depend on the last task of cmd1
assert "task3" in layers[1]
assert "task4" in layers[1]
# Verify total layers
assert len(layers) == 2
def test_execution_order_complex_mixed_scenario(self) -> None:
"""Should handle complex mixed scenario with references and TaskSpecs."""
# Create a complex scenario
clean = px.TaskSpec("clean", cmd=["echo", "clean"])
build1 = px.TaskSpec("build1", cmd=["echo", "build1"])
build2 = px.TaskSpec("build2", cmd=["echo", "build2"], depends_on=("build1",))
test1 = px.TaskSpec("test1", cmd=["echo", "test1"])
test2 = px.TaskSpec("test2", cmd=["echo", "test2"])
package = px.TaskSpec("package", cmd=["echo", "package"])
deploy = px.TaskSpec("deploy", cmd=["echo", "deploy"])
runner = px.CliRunner(
strategy="sequential",
graphs={
"clean": px.Graph.from_specs([clean]),
"build": px.Graph.from_specs([build1, build2]),
"test": px.Graph.from_specs([test1, test2]),
"release": px.Graph.from_specs(["clean", "build", "test", package, deploy]),
},
)
# Check execution order through layers
layers = runner.graphs["release"].layers()
# Layer 1: clean
assert "clean" in layers[0]
# Layer 2: build1 (depends on clean)
assert "build1" in layers[1]
# Layer 3: build2 (depends on build1)
assert "build2" in layers[2]
# Layer 4: test1 and test2 (depends on build2)
assert "test1" in layers[3]
assert "test2" in layers[3]
# Layer 5: package (depends on test1/test2)
assert "package" in layers[4]
# Layer 6: deploy (depends on package)
assert "deploy" in layers[5]
# Verify total layers
assert len(layers) == 6
+6 -6
View File
@@ -136,7 +136,7 @@ class TestDescribeInjection:
def test_describe_injection(self) -> None:
"""应正确描述依赖注入、Context 标注和默认值."""
def fn(a: int, ctx: px.Context, flag: bool = False) -> None: # noqa: ARG001
def fn(a: int, ctx: px.Context, flag: bool = False) -> None:
return None
spec = px.TaskSpec("t", fn, depends_on=("a",))
@@ -148,7 +148,7 @@ class TestDescribeInjection:
def test_var_positional(self) -> None:
"""*args 参数应显示为 *args."""
def fn(*args: Any) -> None: # noqa: ARG001
def fn(*args: Any) -> None:
return None
spec = px.TaskSpec("t", fn)
@@ -158,7 +158,7 @@ class TestDescribeInjection:
def test_var_keyword(self) -> None:
"""**kwargs 参数应显示为 **kwargs=<all-deps>."""
def fn(**kwargs: Any) -> None: # pyright: ignore[reportExplicitAny, reportAny] # noqa: ARG001
def fn(**kwargs: Any) -> None: # pyright: ignore[reportExplicitAny, reportAny]
return None
spec = px.TaskSpec("t", fn, depends_on=("a",))
@@ -168,7 +168,7 @@ class TestDescribeInjection:
def test_unresolved(self) -> None:
"""无依赖、无静态值、无默认的参数应显示为 <UNRESOLVED>."""
def fn(missing: int) -> None: # noqa: ARG001
def fn(missing: int) -> None:
return None
spec = px.TaskSpec("t", fn)
@@ -178,7 +178,7 @@ class TestDescribeInjection:
def test_static_kwargs(self) -> None:
"""静态 kwargs 应显示具体值."""
def fn(flag: bool = False) -> None: # noqa: ARG001
def fn(flag: bool = False) -> None:
return None
spec = px.TaskSpec("t", fn, kwargs={"flag": True})
@@ -188,7 +188,7 @@ class TestDescribeInjection:
def test_positional_args_filled(self) -> None:
"""spec.args 填充的位置参数应显示具体值(覆盖 args_filled 分支)."""
def fn(a: int, b: str) -> None: # noqa: ARG001
def fn(a: int, b: str) -> None:
return None
spec = px.TaskSpec("t", fn, args=(1, "x"))
+89 -2
View File
@@ -127,8 +127,8 @@ def test_threaded_parallelism() -> None:
report = px.run(graph, strategy="thread", max_workers=3)
elapsed = time.time() - start
assert report.success
# Three 0.3s tasks in parallel should be well under 0.8s.
assert elapsed < 0.8
# Three 0.3s tasks in parallel should be well under 1.0s.
assert elapsed < 1.0
@pytest.mark.slow
@@ -507,3 +507,90 @@ def test_run_empty_graph() -> None:
report = px.run(graph, strategy="sequential")
assert report.success
assert len(report) == 0
# ---------------------------------------------------------------------- #
# 上游任务被 SKIPPED 后,下游任务也应被 SKIPPED
# ---------------------------------------------------------------------- #
def test_downstream_skipped_when_upstream_skipped_sequential() -> None:
"""上游任务被 SKIPPED 后,下游任务也应被 SKIPPEDsequential 策略)."""
never_true = lambda: False # noqa: E731
def downstream(upstream: str) -> str:
return upstream + "_processed"
graph = px.Graph.from_specs(
[
px.TaskSpec("upstream", cmd=["echo", "hello"], conditions=(never_true,)),
px.TaskSpec("downstream", downstream, depends_on=("upstream",)),
]
)
report = px.run(graph, strategy="sequential")
assert report.success
assert report.result_of("upstream").status == px.TaskStatus.SKIPPED
assert report.result_of("downstream").status == px.TaskStatus.SKIPPED
def test_downstream_skipped_when_upstream_skipped_thread() -> None:
"""上游任务被 SKIPPED 后,下游任务也应被 SKIPPEDthread 策略)."""
never_true = lambda: False # noqa: E731
def downstream(upstream: str) -> str:
return upstream + "_processed"
graph = px.Graph.from_specs(
[
px.TaskSpec("upstream", cmd=["echo", "hello"], conditions=(never_true,)),
px.TaskSpec("downstream", downstream, depends_on=("upstream",)),
]
)
report = px.run(graph, strategy="thread", max_workers=2)
assert report.success
assert report.result_of("upstream").status == px.TaskStatus.SKIPPED
assert report.result_of("downstream").status == px.TaskStatus.SKIPPED
def test_downstream_skipped_when_upstream_skipped_async() -> None:
"""上游任务被 SKIPPED 后,下游任务也应被 SKIPPEDasync 策略)."""
async def upstream() -> str:
return "hello"
async def downstream(upstream: str) -> str:
return upstream + "_processed"
never_true = lambda: False # noqa: E731
graph = px.Graph.from_specs(
[
px.TaskSpec("upstream", upstream, conditions=(never_true,)),
px.TaskSpec("downstream", downstream, depends_on=("upstream",)),
]
)
report = px.run(graph, strategy="async")
assert report.success
assert report.result_of("upstream").status == px.TaskStatus.SKIPPED
assert report.result_of("downstream").status == px.TaskStatus.SKIPPED
def test_downstream_executes_when_upstream_succeeds() -> None:
"""上游任务成功时,下游任务应正常执行."""
always_true = lambda: True # noqa: E731
def upstream() -> str:
return "hello"
def downstream(upstream: str) -> str:
return upstream + "_processed"
graph = px.Graph.from_specs(
[
px.TaskSpec("upstream", upstream, conditions=(always_true,)),
px.TaskSpec("downstream", downstream, depends_on=("upstream",)),
]
)
report = px.run(graph, strategy="sequential")
assert report.success
assert report.result_of("upstream").status == px.TaskStatus.SUCCESS
assert report.result_of("downstream").status == px.TaskStatus.SUCCESS
assert report["downstream"] == "hello_processed"
+2 -19
View File
@@ -167,10 +167,10 @@ class TestCliRunnerParser:
def test_parser_strategy_default(self) -> None:
"""--strategy 默认值应与构造时一致."""
runner = px.CliRunner({"clean": _echo_graph()}, "async")
runner = px.CliRunner({"clean": _echo_graph()}, strategy="async")
parser = runner.create_parser()
parsed = parser.parse_args(["clean"])
assert parsed.strategy == "sequential"
assert parsed.strategy == "async"
def test_parser_has_dry_run_flag(self) -> None:
"""解析器应有 --dry-run 标志."""
@@ -603,23 +603,6 @@ class TestCliRunnerIntegration:
assert exit_code == CliExitCode.SUCCESS.value
# ---------------------------------------------------------------------- #
# 构造校验 (补充覆盖)
# ---------------------------------------------------------------------- #
class TestCliRunnerConstructionValidation:
"""测试 CliRunner 的构造校验 (补充覆盖)."""
def test_non_graph_value_raises_type_error(self) -> None:
"""非 Graph 值应抛出 TypeError (覆盖 runner.py line 119)."""
with pytest.raises(TypeError, match="必须是 Graph 实例"):
_ = px.CliRunner(graphs={"bad": "not a graph"}) # type: ignore[dict-item]
def test_non_graph_value_dict_raises_type_error(self) -> None:
"""dict 中包含非 Graph 值应抛出 TypeError."""
with pytest.raises(TypeError, match="必须是 Graph 实例"):
_ = px.CliRunner(graphs={"good": _echo_graph(), "bad": 123}) # type: ignore[dict-item]
# ---------------------------------------------------------------------- #
# _apply_verbose_to_graph (补充覆盖)
# ---------------------------------------------------------------------- #
+96
View File
@@ -211,3 +211,99 @@ def test_taskspec_shell_cmd_os_error_mocked():
RuntimeError, match="Shell 命令执行异常"
):
_ = wrapped_fn()
# ---------------------------------------------------------------------- #
# skip_if_missing
# ---------------------------------------------------------------------- #
def test_skip_if_missing_with_available_command():
"""skip_if_missing=True 时,命令存在应返回 True."""
# python 命令在测试环境中一定存在
spec = TaskSpec("test", cmd=["python", "--version"], skip_if_missing=True)
assert spec.should_execute() is True
def test_skip_if_missing_with_missing_command():
"""skip_if_missing=True 时,命令不存在应返回 False."""
spec = TaskSpec("test", cmd=["definitely_not_installed_app_xyz"], skip_if_missing=True)
assert spec.should_execute() is False
def test_skip_if_missing_false_with_missing_command():
"""skip_if_missing=False 时,命令不存在也应返回 True(不检查)."""
spec = TaskSpec("test", cmd=["definitely_not_installed_app_xyz"], skip_if_missing=False)
assert spec.should_execute() is True
def test_skip_if_missing_with_shell_cmd_not_checked():
"""skip_if_missing=True 时,shell 命令(str)不检查,应返回 True."""
spec = TaskSpec("test", cmd="definitely_not_installed_app_xyz", skip_if_missing=True)
assert spec.should_execute() is True
def test_skip_if_missing_with_callable_cmd_not_checked():
"""skip_if_missing=True 时,Callable 命令不检查,应返回 True."""
def custom_cmd() -> int:
return 0
spec = TaskSpec("test", cmd=custom_cmd, skip_if_missing=True)
assert spec.should_execute() is True
def test_skip_if_missing_with_fn_not_checked():
"""skip_if_missing=True 时,fn 任务不检查命令,应返回 True."""
def my_fn() -> int:
return 0
spec = TaskSpec("test", fn=my_fn, skip_if_missing=True)
assert spec.should_execute() is True
def test_skip_if_missing_with_empty_cmd_list():
"""skip_if_missing=True 时,空命令列表应返回 True(不检查)."""
spec = TaskSpec("test", cmd=[""], skip_if_missing=True)
# 空字符串命令,shutil.which 返回 None
# 但 cmd[0] 是空字符串,shutil.which("") 返回 None
assert spec.should_execute() is False
def test_skip_if_missing_combined_with_conditions():
"""skip_if_missing=True 与 conditions 组合使用."""
# conditions 返回 False,应跳过
spec = TaskSpec(
"test",
cmd=["python", "--version"],
skip_if_missing=True,
conditions=(lambda: False,),
)
assert spec.should_execute() is False
# conditions 返回 True,命令存在,应执行
spec = TaskSpec(
"test",
cmd=["python", "--version"],
skip_if_missing=True,
conditions=(lambda: True,),
)
assert spec.should_execute() is True
# conditions 返回 True,命令不存在,应跳过
spec = TaskSpec(
"test",
cmd=["definitely_not_installed_app_xyz"],
skip_if_missing=True,
conditions=(lambda: True,),
)
assert spec.should_execute() is False
def test_skip_if_missing_skips_task_in_run():
"""skip_if_missing=True 时,命令不存在的任务在 run 中应被跳过."""
spec = TaskSpec("missing_cmd", cmd=["definitely_not_installed_app_xyz"], skip_if_missing=True)
graph = px.Graph.from_specs([spec])
report = px.run(graph, strategy="sequential")
assert report.success is True
result = report.result_of("missing_cmd")
assert result.status == px.TaskStatus.SKIPPED
+3 -1
View File
@@ -428,7 +428,9 @@ class TestTaskSpecCmdErrors:
"""命令不存在时应抛出 RuntimeError."""
from pyflowx.errors import TaskFailedError
graph = px.Graph.from_specs([px.TaskSpec("missing", cmd=["this-command-does-not-exist-xyz"])])
graph = px.Graph.from_specs(
[px.TaskSpec("missing", cmd=["this-command-does-not-exist-xyz"], skip_if_missing=False)],
)
with pytest.raises(TaskFailedError) as exc_info:
_ = px.run(graph, strategy="sequential")
# 错误信息应包含命令未找到
+1
View File
@@ -2,6 +2,7 @@
This type stub file was generated by pyright.
"""
# pyrefly: ignore [missing-import]
from .graphlib import CycleError, TopologicalSorter
__all__ = ["CycleError", "TopologicalSorter"]
Generated
+454 -357
View File
@@ -63,46 +63,6 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9" },
]
[[package]]
name = "ast-serialize"
version = "0.5.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/81/9d/09e27731bd5864a9ce04e3244074e674bb8936bf62b45e0357248717adac/ast_serialize-0.5.0.tar.gz", hash = "sha256:5880091bfe6f4f986f22866375c2e884843e7a0b6343ae41aeea659613d879b6" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/c0/9a/13dde51ba9e15f8b97957ab7cb0120d0e381524d651c6bd630b9c359227f/ast_serialize-0.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8f5c14f169eb0972c0c21bada5358b23d6047c76583b005234f865b11f1fa00a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/37/de/5a7f0a9fe68944f536632a5af84676739c7d2582be42deb082634bf3a754/ast_serialize-0.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7d1a2de9de5be04652f0ed60738356ef94f66db37924a9499fffe98dc491aa0b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9c/81/0bb853e76e4f6e9a1855d569003c59e19ffac45f7079d91505d1bb212f92/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be5173fb66f9b49026d9d5a2ff0fc7c7009077107c0eb285b2d60fdf1fe10bd1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e5/d3/4cf705beeccc08754d0bbda99aefff26110e209b9a07ac8a6b60eec48531/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f8015cd071ac1339924ee2b8098c93e00e155f30a16f40ec9816fcf84f4753f6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/26/c8/ee097e437ea27dd2b8b227865c875492b585650a5802a22d82b304c8201b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5499e8797edff2a9186aa313ed382c6b422e798e9332d9953badcee6e69a88f2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ff/bd/68063442838f1ba68ec72b5436430bc75b3bb17a1a3c3063f09b0c05ae2b/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6848f2a093fb5548751a9a09bff8fcd229e2bbeb0e3331f391b6ae6d26cd9903" },
{ url = "https://mirrors.aliyun.com/pypi/packages/50/e2/1e520793bc6a4e4524a6ab022391e827825eaa0c3811828bfdc6852eca26/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:832d4c998e0b091fd60a6d6bceee535483c4d490de9ba85003af835225719261" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4e/e1/49b60f467979979cfe6913b43948ff25bca971ad0591d181812f163a988e/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:16db7c62ec0b8efe1d7afd283a388d8f74f2605d56032e5a37747d2de8dba027" },
{ url = "https://mirrors.aliyun.com/pypi/packages/74/ba/66ab9555de6275677566f6574e5ef6c29cb185ea866f643bc06f8280a8ee/ast_serialize-0.5.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf5eb061eb5bccade4128ad42da33787d72f6013809cd1b590376ece8b3c937" },
{ url = "https://mirrors.aliyun.com/pypi/packages/66/42/6aca9b9abc710014b2be9059689e5dd1679339e78f567ffb4d255a9e2050/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:104e4a35bd7c124173c41760ef9aaea17ddb3f86c65cb643671d59afbe3ee94c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/47/68/2f76594432a22581ecf878b5e75a9b8601c24b2241cf0bbeb1e21fcf370c/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:36be371028fc1675acb38a331bde160dbab7ff907fdf00b67eb6911aa106951b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/ac/a93c9b58292653f6c595752f677a08e608f903b710594909e9231a389b3b/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:061ee58bdb52341c8201a6df41182a977736bae3b7ded87ca7176ca25a8a47ab" },
{ url = "https://mirrors.aliyun.com/pypi/packages/14/2e/b278f68c497ee2f1d1576cbbef8db5281cd4a5f2db040537592ac9c8862e/ast_serialize-0.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15219e9cdc9f53f6f4cb51c009203507228226148c05c5e8fe451c28b435eb3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0b/43/419be1c566a4c504cd8fd60ce2f84e790f295495c0f327cfaeadf3d51012/ast_serialize-0.5.0-cp314-cp314t-win32.whl", hash = "sha256:842d1c004bb466c7df036f95fabef789570541922b10976b12f5592a69cf0b38" },
{ url = "https://mirrors.aliyun.com/pypi/packages/03/6f/c9d4d549295ed05111aeb8853232d1afd9d0a179fddb01eeffbb3a4a6842/ast_serialize-0.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b0c06d760909b095cc466356dfccd05a1c7233a6ca191c020dca2c6a6f16c24c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d0/8e/d00c5ab30c58222e07d62956fca86c59d91b9ad32997e633c38b526623a3/ast_serialize-0.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:787baedb0262cc49e8ce37cc15c00ae818e46a165a3b36f5e21ed174998104cb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e0/9e/dc2530acb3a60dc6e46d65abf27d1d9f86721694757906a148d90a6860de/ast_serialize-0.5.0-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:0668aa9459cfa8c9c49ddd2163ebcf43088ba045ef7492af6fe22e0098303101" },
{ url = "https://mirrors.aliyun.com/pypi/packages/26/0a/bd3d18a582f273d6c843d16bb9e22e9e16365ff7991e92f18f798e9f1224/ast_serialize-0.5.0-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf683d6363edf2b39eed6b6d4fe22d34b6203867a67e27134d9e2a2680c4bc4a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/ae/1f919100f8620887af58fcc381c61a1f218cdf89c6e155f87b213e61010a/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cc22cf0c9be65e71cf88fda130af60d61eb4a79370ad4cfe7900d48a4aa2211" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c6/ca/6376559dcce707cdbc1d0d9a13c8d3baaaa501e949ce0ebdc4230cd881aa/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f66173891548c9f2726bf27957b41cabce12fa679dc6da505ddbde4d4b3b31cf" },
{ url = "https://mirrors.aliyun.com/pypi/packages/35/b2/a620e206b5aeb7efbf2710336df57d457cffbb3991076bbcc1147ef9abd4/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e42d729ef2be96a14efbad355093284739e3670ece3e534f82cc8832790911d9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fa/e0/4ad5c04c24a40481b2935ce9a0ccdb6023dc8b667167d06ae530cc3512f2/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b725026bafa801dbd7310eb13a75f0a2e370e7e51b2cb225f9d21fcfadf919ee" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b2/71/4d1d479aa56d0101c40e17720c3d6ac2af7269ea0487a80b18e7bfd1a5b7/ast_serialize-0.5.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b54f60c1d78767a53b67eaa663f0dfac3afe606aa07f1301572f588b73d64809" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6d/4f/0de1bbe06f6edef9fde4ed12ca8e7b3ec7e6e2bd4e672c5af487f7957665/ast_serialize-0.5.0-cp39-abi3-manylinux_2_31_riscv64.whl", hash = "sha256:27d51654fc240a1e87e742d353d98eb45b75f62f129086b3596ab53df2ac2a43" },
{ url = "https://mirrors.aliyun.com/pypi/packages/75/61/e00872439cfdddcc3c1b6cdaa6e5d904ba8e26a18807c67c4e14409d0ca8/ast_serialize-0.5.0-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c36237c46dd1674542f2109740ea5ea485a169bf1431939ada0434e17934" },
{ url = "https://mirrors.aliyun.com/pypi/packages/76/8e/699a5b955f7926956c95e9e1d74132acad73c2fe7a426f94da89123c20aa/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1943db345233cc7194a470f13afa9c59772c0b123dea0c9414c4d4ca54369759" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a9/ae/d5b7626874478997adc7a29ab28accf21e596fb590c944290401dfd0b29e/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:df1c00022cbbcb064bfaa505aa9c9295362443ce5dacb459d1331d3da353f887" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0c/ce/b59e02a82d9c4244d64cde502e0b00e83e38816abe19155ceb5437402c7f/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:cae65289fc456fde04af979a2be09302ef5d8ab92ef23e596d6746dc267ada27" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8b/38/d8d90042747d05aa08d4efcf1c99035a5f670a6bf4c214d31644392afbca/ast_serialize-0.5.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:239a4c354e8d676e9d94631d1d4a64edc6b266f86ff3a5a80aedd344f342c01d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/dd/51/5b840c4df7334104cecffa28f23904fe81ca89ca223d2450e288de39fd3c/ast_serialize-0.5.0-cp39-abi3-win32.whl", hash = "sha256:143a4ef63285a075871908fda3672dc21864b83a8ec3ee12304aa3e4c5387b9a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/41/11/ca5672c7d491825bc4cd6702dea106a6b60d928707712ec257c7833ae476/ast_serialize-0.5.0-cp39-abi3-win_amd64.whl", hash = "sha256:cf25572c526add400f26a4750dc6ce0c3bb93fc1f75e7ae0cad4ce4f2cd5c590" },
{ url = "https://mirrors.aliyun.com/pypi/packages/45/19/cc8bd127d28a43da249aa955cfd164cf8fd534e79e42cea96c4854d72fd0/ast_serialize-0.5.0-cp39-abi3-win_arm64.whl", hash = "sha256:92a31c9c20d25a076edaeec76b128a3535d74a24f340b9a8a7e96c9b86dc9642" },
]
[[package]]
name = "backports-asyncio-runner"
version = "1.2.0"
@@ -214,18 +174,6 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/3c/56/70860ece85cd49b564305cbc22bf6c4183975427ff6dfe2097e855f5dd5e/backports_zstd-1.6.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:994167ff6551b9c1ce226e0aab16295b98c94507b5701aa60d2c32b7d50796b1" },
]
[[package]]
name = "basedpyright"
version = "1.39.8"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
dependencies = [
{ name = "nodejs-wheel-binaries" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d2/62/8550c75850b2185df984d1de437b4805b039ba856cacbee2966236203133/basedpyright-1.39.8.tar.gz", hash = "sha256:bb1a86d4d71425d52d1501b317fe23d45527baed06bd5d5e1a07cd4b60d07b55" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/f5/94/878454aefe94328ba7ad808ecd63da8311aae1198da46cfb29f5cfe130a8/basedpyright-1.39.8-py3-none-any.whl", hash = "sha256:a79d89928064bd9023d429b50c625d87d023bacc2fe3932ef6c7bd13b5426048" },
]
[[package]]
name = "cachetools"
version = "5.5.2"
@@ -1509,103 +1457,6 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/81/db/e655086b7f3a705df045bf0933bdd9c2f79bb3c97bfef1384598bb79a217/keyring-25.7.0-py3-none-any.whl", hash = "sha256:be4a0b195f149690c166e850609a477c532ddbfbaed96a404d4e43f8d5e2689f" },
]
[[package]]
name = "librt"
version = "0.11.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/40/08/9e7f6b5d2b5bed6ad055cdd5925f192bb403a51280f86b56554d9d0699a2/librt-0.11.0.tar.gz", hash = "sha256:075dc3ef4458a278e0195cbf6ac9d38808d9b906c5a6c7f7f79c3888276a3fb1" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/83/10/37fd9e9ba96cb0bd742dfb20fc3d082e54bdbec759d7300df927f360ef07/librt-0.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e94ebfcfa2d5e9926d6c3b9aa4617ffc42a845b4321fb84021b872358c82a0f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cf/72/1b1466f358e4a0b728051f69bc27e67b432c6eaa2e05b88db49d3785ae0d/librt-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ae627397a2f351560440d872d6f7c8dbb4072e57868e7b2fc5b8b430fe489d45" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ca/85/ed26dd2f6bc9a0baf48306433e579e8d354d70b2bcb78134ed950a5d0e1e/librt-0.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc329359321b67d24efdf4bc69012b0597001649544db662c001db5a0184794c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/66/fe/11891191c0e0a3fd617724e891f6e67a71a7658974a892b9a9a97fdb2977/librt-0.11.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:7e82e642ab0f7608ce2fe53d76ca2280a9ee33a1b06556142c7c6fe80a86fc33" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6f/50/5ec949d7f9ce1a07af903aa3e13abb98b717923bdead6e719b2f824ccc07/librt-0.11.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88145c15c67731d54283d135b03244028c750cc9edc334a96a4f5950ebdb2884" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ea/c4/177336c7524e34875a38bf668e88b193a6723a4eb4045d07f74df6e1506c/librt-0.11.0-cp310-cp310-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d36a51b3d93320b686588e27123f4995804dbf1bce81df78c02fc3c6eea9280" },
{ url = "https://mirrors.aliyun.com/pypi/packages/13/1f/da3112f7569eda3b49f9a2629bae1fe059812b6085df16c885f6454dff49/librt-0.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d00f3ac06a2a8b246327f11e186a53a100a4d5c7ed52346367e5ec751d51586c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fa/94/03fec301522e172d105581431223be56b27594ff46440ebfbb658a3735d5/librt-0.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:461bbceede621f1ffb8839755f8663e886087ee7af16294cab7fb4d782c62eeb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b7/6e/339f6e5a7b413ce014f1917a756dae630fe59cc99f34153205b1cb540901/librt-0.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0cad8a4d6a8ff03c9b76f9414caccd78e7cfbc8a2e12fa334d8e1d9932753783" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cd/43/acdd5ce317cb46e8253ca9bfbdb8b12e68a24d745949336a7f3d5fb79ba0/librt-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f37aa505b3cf60701562eddb32df74b12a9e380c207fd8b06dd157a943ac7ea0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/29/b5/7a25bb12e3172839f647f196b3e988318b7bb1ca7501732a225c4dce2ec0/librt-0.11.0-cp310-cp310-win32.whl", hash = "sha256:94663a21534637f0e787ec2a2a756022df6e5b7b2335a5cdd7d8e33d68a2af89" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c6/0d/ebbcf4d77999c02c937b05d2b90ff4cd4dcc7e9a365ba132329ac1fe7a0f/librt-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:dec7db73758c2b54953fd8b7fe348c45188fe26b39ee18446196edd08453a5d4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fe/87/2bf31fe17587b29e3f93ec31421e2b1e1c3e349b8bf6c7c313dbad1d5340/librt-0.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:93d95bd45b7d58343d8b90d904450a545144eec19a002511163426f8ab1fae29" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cf/08/5c5bf772920b7ebac6e32bc91a643e0ab3870199c0b542356d3baa83970a/librt-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ee278c769a713638cdacd4c0436d72156e75df3ebc0166ab2b9dc43acc386c9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/06/20/662a03d254e5b000d838e8b345d83303ddb768c080fd488e40634c0fa66b/librt-0.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f230cb1cbc9faaa616f9a678f530ebcf186e414b6bcbd88b960e4ba1b92428d5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/de/f3/aa81523e45184c6ec23dc7f63263362ec55f80a09d424c012359ecbe7e35/librt-0.11.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:5d63c855d86938d9de93e265c9bd8c705b51ec494de5738340ee93767a686e4b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6b/6f/59c74b560ca8853834d5501d589c8a2519f4184f273a085ffd0f37a1cc47/librt-0.11.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f028be9e96a08d31df3479ac80d99be374d17f3b78e4796b3fd3c913d4e89" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fe/7b/5aa4d2c9600a719401160bf7055417df0b2a47439b9d88286ce45e56b65f/librt-0.11.0-cp311-cp311-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:258d73a0aa66a055e65b2e4d1b8cdb23b9d132c5bb915d9547d804fcaed116cc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d6/31/9143803d7da6856a69153785768c4936864430eec0fd9461c3ea527d9922/librt-0.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0827efe7854718f04aaddf6496e96960a956e676fe1d0f04eb41511fd8ad06d5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2f/5a/bce08184488426bda4ccc2c4964ac048c8f68ae89bd7120082eef4233cfd/librt-0.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7753e57d6e12d019c0d8786f1c09c709f4c3fcc57c3887b24e36e6c06ec938b7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/89/8c/bb5e213d254b7505a0e658da199d8ab719086632ce09eef311ab27976523/librt-0.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:11bd19822431cc21af9f27374e7ae2e58103c7d98bda823536a6c47f6bb2bb3d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9d/fb/541cdad5b1ab1300398c74c4c9a497b88e5074c21b1244c8f49731d3a284/librt-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:22bdf239b219d3993761a148ffa134b19e52e9989c84f845d5d7b71d70a17412" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8f/f2/464bb69295c320cb06bddb4f14a4ec67934ee14b2bffb12b19fb7ab287ba/librt-0.11.0-cp311-cp311-win32.whl", hash = "sha256:46c60b61e308eb535fbd6fa622b1ee1bb2815691c1ad9c98bf7b84952ec3bc8d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6d/e7/a17ee1788f9e4fbf548c19f4afa07c92089b9e24fef6cb2410863781ef4c/librt-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:902e546ff044f579ff1c953ff5fce97b636fe9e3943996b2177710c6ef076f73" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cc/c7/6c766214f9f9903bcfcfbef97d807af8d8f5aa3502d247858ab17582d212/librt-0.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:65ac3bc20f78aa0ee5ae84baa68917f89fef4af63e941084dd019a0d0e749f0c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8b/d0/07c77e067f0838949b43bd89232c29d72efebb9d2801a9750184eb706b71/librt-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b87504f1690a23b9a2cca841191a04f83895d4fc2dd04df91d82b1a04ca2ad46" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7a/24/8493538fa4f62f982686398a5b8f68008138a75086abdea19ade64bf4255/librt-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40071fc5fe0ce8daa6de616702314a01e1250711682b0523d6ab8d4525910cb3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ff/1e/f8bad050810d9171f34a1648ed910e56814c2ba61639f2bd53c6377ae24b/librt-0.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:137e79445c896a0ea7b265f52d23954e05b64222ee1af69e2cb34219067cbb67" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c0/fe/3594ebfbaf03084ba4b120c9ba5c3183fd938a48725e9bbe6ff0a5159ad8/librt-0.11.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:cca6644054e78746d8d4ef238681f9c34ff8b584fe6b988ecebb8db3b15e622a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b0/da/5d1876984b3746c85dbd219dbfcb73c85f54ee263fd32e5b2a632ec14571/librt-0.11.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5b0eea49f5562861ee8d757a32ef7d559c1d35be2aaaa1ec28941d74c9ffc8a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/19/6e/55bdf5d5ca00c3e18430690bf2c953d8d3ffd3c337418173d33dec985dc9/librt-0.11.0-cp312-cp312-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0d1029d7e1ae1a7e647ed6fb5df8c4ce2dffefb7a9f5fd1376a4554d96dac09f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/07/10/f1f23a7c595ee90ece4d35c851e5d104b1311a887ed1b4ac4c35bbd13da8/librt-0.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bc3ce6b33c5828d9e80592011a5c584cb2ce86edbc4088405f70da47dc1d1b3b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b6/02/5720f5697a7f54b78b3aefbe20df3a48cedcff1276618c4aa481177942ed/librt-0.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:936c5995f3514a42111f20099397d8177c79b4d7e70961e396c6f5a0a3566766" },
{ url = "https://mirrors.aliyun.com/pypi/packages/50/db/b4a47c6f91db4ff76348a0b3dd0cc65e090a078b765a810a62ff9434c3d3/librt-0.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9bc0ca6ad9381cbe8e4aa6e5726e4c80c78115a6e9723c599ed1d73e092bc49d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9e/58/9384b2f4eb1ed1d273d40948a7c5c4b2360213b402ef3be4641c06299f9c/librt-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:070aa8c26c0a74774317a72df8851facc7f0f012a5b406557ac56992d92e1ec8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/21/7b/5aa8848a7c6a9278c79375146da1812e695754ceec5f005e6043461a7315/librt-0.11.0-cp312-cp312-win32.whl", hash = "sha256:6bf14feb84b05ae945277395451998c89c54d0def4070eb5c08de544930b245a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/37/33/8a745436944947575b584231750a41417de1a38cf6a2e9251d1065651c09/librt-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:75672f0bc524ede266287d532d7923dbce94c7514ad07627bac3d0c6d92cc4d9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/59/67/a6739ac96e28b7855808bdb0370e250606104a859750d209e5a0716fe7ab/librt-0.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:2f10cf143e4a9bb0f4f5af568a00df94a2d69ef41c2579584454bb0fe5cc642c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/82/61/e59168d4d0bf2bf90f4f0caf7a001bfc60254c3af4586013b04dc3ef517b/librt-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:78dc31f7fdfe9c9d0eb0e8f42d139db230e826415bbcabd9f0e9faaaee909894" },
{ url = "https://mirrors.aliyun.com/pypi/packages/61/fd/caa1d60b12f7dd79ccea23054e06eeaebe266a5f52c40a6b651069200ce5/librt-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fa475675db22290c3158e1d42326d0f5a65f04f44a0e68c3630a25b53560fb9c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b8/a9/dc744f5c2b4978d48db970be29f22716d3413d28b14ad99740817315cf2c/librt-0.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:621db29691044bdeda22e789e482e1b0f3a985d90e3426c9c6d17606416205ea" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8f/21/7f8e97a1e4dae952a5a95948f6f8507a173bc1e669f54340bba6ca1ca31b/librt-0.11.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:a9010e2ed5b3a9e158c5fd966b3ab7e834bb3d3aacc8f66c91dd4b57a3799230" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a6/6d/d8ee9c114bebf2c50e29ec2aa940826fccb62a645c3e4c18760987d0e16d/librt-0.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c39513d8b7477a2e1ed8c43fc21c524e8d5a0f8d4e8b7b074dbdbe7820a08e2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f0/43/0b5708af2bd30a46400e72ba6bdaa8f066f15fb9a688527e34220e8d6c06/librt-0.11.0-cp313-cp313-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7aef3cf1d5af86e770ab04bfd993dfc4ae8b8c17f66fb77dd4a7d50de7bbb1a3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4a/50/356187247d09013490481033183b3532b58acf8028bcb34b2b56a375c9b2/librt-0.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:557183ddc36babe46b27dd60facbd5adb4492181a5be887587d57cda6e092f21" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/e7/c6ac4240899c7f3248079d5a9900debe0dadb3fdeaf856684c987105ba47/librt-0.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:83d3e1f72bd42f6c5c0b7daec530c3f829bd02db42c70b8ddf0c2d90a2459930" },
{ url = "https://mirrors.aliyun.com/pypi/packages/eb/b5/a81322dbeedeeaf9c1ee6f001734d28a09d8383ac9e6779bc24bbd0743c6/librt-0.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:4ce1f21fbe589bc1afd7872dece84fb0e1144f794a288e58a10d2c54a55c43be" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ae/66/6e6323787d592b55204a42595ff1102da5115601b53a7e9ddebc889a6da5/librt-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b09f7044ea2b64c9da42fd3d335666518cfd1c6e8a182c95da73d0214b41e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9c/21/623f8ca230857102066d9ca8c6c1734995908c4d0d1bee7bb2ef0021cb33/librt-0.11.0-cp313-cp313-win32.whl", hash = "sha256:78fddc31cd4d3caa897ad5d31f856b1faadc9474021ad6cb182b9018793e254e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b3/1d/b4ebd44dd723f768469007515cb92251e0ae286c94c140f374801140fa74/librt-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ca8aa88751a775870b764e93bad5135385f563cb8dcee399abf034ea4d3cb47" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3b/e4/b2f4ca7965ca373b491cdb4bc25cdb30c1649ca81a8782056a83850292a9/librt-0.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:96f044bb325fd9cf1a723015638c219e9143f0dfbc0ca54c565df2b7fc748b44" },
{ url = "https://mirrors.aliyun.com/pypi/packages/29/eb/dbce197da4e227779e56b5735f2decc3eb36e55a1cdbf1bd65d6639d76c1/librt-0.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:4a017a95e5837dc15a8c5661d60e05daa96b90908b1aa6b7acdf443cd25c8ebd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/76/a3/254bebd0c11c8ba684018efb8006ff22e466abce445215cca6c778e7d9de/librt-0.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b1ecbd9819deccc39b7542bf4d2a740d8a620694d39989e58661d3763458f8d4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f1/3f/f77d6122d21ac7bf6ae8a7dfced1bd2a7ac545d3273ebdcaf8042f6d619f/librt-0.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7da327dacd7be8f8ec36547373550744a3cc0e536d54665cd83f8bcd961200e8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ac/0a/2c996dadebaa7d9bbbd43ef2d4f3e66b6da545f838a41694ef6172cebec8/librt-0.11.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:0dc56b1f8d06e60db362cc3fdae206681817f86ce4725d34511473487f12a34b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0a/7e/f5d92af8486b8272c23b3e686b46ff72d89c8169585eb61eef01a2ac7147/librt-0.11.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05fb8fb2ab90e21c8d12ea240d744ad514da9baf381ebfa70d91d20d21713175" },
{ url = "https://mirrors.aliyun.com/pypi/packages/af/1a/cb0734fe86398eb33193ab753b7326255c74cac5eb09e76b9b16536e7adb/librt-0.11.0-cp314-cp314-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cae74872be221df4374d10fec61f93ed1513b9546ea84f2c0bf73ab3e9bd0b03" },
{ url = "https://mirrors.aliyun.com/pypi/packages/18/06/094820f91558b66e29943c0ec41c9914f460f48dd51fc503c3101e10842d/librt-0.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:32bcc918c0148eb7e3d57385125bac7e5f9e4359d05f07448b09f6f778c2f31c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0b/c2/00de9018871a282f530cacb457d5ec0428f6ac7e6fedde9aff7468d9fb04/librt-0.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f9743fc99135d5f78d2454435615f6dec0473ca507c26ce9d92b10b562a280d3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/51/9d/64631832348fd1834fb3a61b996434edddaaf25a31d03b0a76273159d2cf/librt-0.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5ba067f4aadae8fda802d91d2124c90c42195ff32d9161d3549e6d05cfe26f96" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a5/ec/ae5525eb16edc827a044e7bb8777a455ff95d4bca9379e7e6bddd7383647/librt-0.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:de3bf945454d032f9e390b85c4072e0a0570bf825421c8be0e71209fa65e1abe" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5a/09/adce371f27ca039411da9659f7430fcc2ba6cd0c7b3e4467a0f091be7fa9/librt-0.11.0-cp314-cp314-win32.whl", hash = "sha256:d2277a05f6dcb9fd13db9566aac4fabd68c3ea1ea46ee5567d4eef8efa495a2f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d6/ee/8ac720d98548f173c7ce2e632a7ca94673f74cacd5c8162a84af5b35958a/librt-0.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ab73e8db5e3f564d812c1f5c3a175930a5f9bc96ccb5e3b22a34d7858b401cf7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/94/20/c900cf14efeb09b6bef2b2dff20779f73464b97fd58d1c6bccc379588ae3/librt-0.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aea3caa317752e3a466fa8af45d91ee0ea8c7fdd96e42b0a8dd9b76a7931eba1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0c/71/944bfe4b64e12abffcd3c15e1cce07f72f3d55655083786285f4dedeb532/librt-0.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:d1b36540d7aaf9b9101b3a6f376c8d8e9f7a9aec93ed05918f2c69d493ffef72" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b6/10/99e64a5c86989357fda078c8143c533389585f6473b7439172dd8f3b3b2d/librt-0.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:efbb343ab2ce3540f4ecbe6315d677ed70f37cd9a72b1e58066c918ca83acbaa" },
{ url = "https://mirrors.aliyun.com/pypi/packages/21/31/5072ad880946d83e5ea4147d6d018c78eefce85b77819b19bdd0ee229435/librt-0.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aa0dd688aab3f7914d3e6e5e3554978e0383312fb8e771d84be008a35b9ee548" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5e/8d/70b5fb7cfbab60edbe7381614ab985da58e144fbf465c86d44c95f43cdca/librt-0.11.0-cp314-cp314t-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:f5fb36b8c6c63fdcbb1d526d94c0d1331610d43f4118cc1beb4efef4f3faacb2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fa/a3/ba3495a0b3edbd24a4cae0d1d3c64f39a9fc45d06e812101289b50c1a619/librt-0.11.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4a9a237d13addb93715b6fee74023d5ee3469b53fce527626c0e088aa585805f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f7/db/36e25fb81f99937ff1b96612a1dc9fd66f039cb9cc3aee12c01fac31aab9/librt-0.11.0-cp314-cp314t-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5ddd17bd87b2c56ddd60e546a7984a2e64c4e8eab92fb4cf3830a48ad5469d51" },
{ url = "https://mirrors.aliyun.com/pypi/packages/33/0d/3f622b47f0b013eeb9cf4cc07ae9bfe378d832a4eec998b2b209fe84244d/librt-0.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bd43992b4473d42f12ff9e68326079f0696d9d4e6000e8f39a0238d482ba6ee2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a9/02/71b90bc93039c46a2000651f6ad60122b114c8f54c4ad306e0e96f5b75ad/librt-0.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:f8e3e8056dd674e279741485e2e512d6e9a751c7455809d0114e6ebf8d781085" },
{ url = "https://mirrors.aliyun.com/pypi/packages/04/04/418cb3f75621e2b761fb1ab0f017f4d70a1a72a6e7c74ee4f7e8d198c2f3/librt-0.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c1f708d8ae9c56cf38a903c44297243d2ec83fd82b396b977e0144a3e76217e3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cc/2c/5a2183ac58dd911f26b5d7e7d7d8f1d87fcecdddd99d6c12169a258ff62c/librt-0.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0add982e0e7b9fc14cf4b33789d5f13f66581889b88c2f58099f6ce8f92617bd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/15/1f/dc6771a52592a4451be6effa200cbfc9cec61e4393d3033d81a9d307961d/librt-0.11.0-cp314-cp314t-win32.whl", hash = "sha256:2b481d846ac894c4e8403c5fd0e87c5d11d6499e404b474602508a224ff531c8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/62/4a/7d1415567027286a75ba1093ec4aca11f073e0f559c530cf3e0a757ad55c/librt-0.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:28edb433edde181112a908c78907af28f964eabc15f4dd16c9d66c834302677c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ce/62/b40b382fa0c66fee1478073eb8db352a4a6beda4a1adccf1df911d8c289c/librt-0.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:dee008f20b542e3cd162ba338a7f9ec0f6d23d395f66fe8aeeec3c9d067ea253" },
{ url = "https://mirrors.aliyun.com/pypi/packages/66/54/5d5f27cc840d2d8a64d60e0650dba14044a95d85a875e42af2eb104ac8b9/librt-0.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6bd72d903911d995ab666dbd1871f8b1e80925a699af8063fbf50053329fb05f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f9/72/535efe79cf47f70975e0b14ceb3b7984bb7e8b97fb2867d3979771be0b6a/librt-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ef69ac715f3cd8e5cd252cb2aebfa72c015492aacc339d5d7bf8fef3c62c677" },
{ url = "https://mirrors.aliyun.com/pypi/packages/83/cc/4130d462aeaf190357517d2a48a0a25030fbfd604230f6c45908452fff9c/librt-0.11.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:624a40c4a4ad7773315c287276cd024509b2c66ff5904f504bfc08d2c70293ab" },
{ url = "https://mirrors.aliyun.com/pypi/packages/62/e8/3c8000edefeb443fd2139692fb966f6c5556cb1032c44f734550896df3b9/librt-0.11.0-cp39-cp39-manylinux2014_i686.manylinux_2_17_i686.manylinux_2_28_i686.whl", hash = "sha256:41dc19fe150b69716c8ece4f76773a9e8813fe3e35e032a58b4d46423fb8d7c0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b2/a1/6de754256493924874e5fa6c0f4f990d8b101c38d974589020d9dc3d02af/librt-0.11.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e8bd98ea9c47ae90b319a087ab28dac493f1ffbc1ecd1f28fcdbf3b7e1108d1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/92/fb/c34cb5358d6f993f85014045decd6dccd089a6f11d188660e062ee6262ff/librt-0.11.0-cp39-cp39-manylinux_2_34_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84308fc49423ce6475d1c5d1985cd69a8ca9f0325fc7d5f81bb690a3f3625d4e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/48/65/7761d70841bac875be9627496546b2eccbdeb07da3e42431bc4a40cf0819/librt-0.11.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:ff0fbaf5f44a21beeb0110f2ab64f45135a9536a834b79c0d1ef018f2786bbfa" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cb/8d/af9d4ac1057cd4e472b89553924b528b3d34afa6b7167645b7e6db39596b/librt-0.11.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9c028a9442a18e266955d364ce42259136e79a7ba14d773e0d778d5f70cd56f1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/86/f4/08faaf48ce0833d3717ebe0a0054c09a05df1bc83ee2715113c9901cc147/librt-0.11.0-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:9f1692105a02bcf853f355032a5fdc5494358ef83d8fd22d16de375c85cec3f5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0d/11/ec3e390627f70477093909875a38843c826ee2ff554d1649645c7cc59248/librt-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7a80a71e1fda83cc752a9141e87aae7fef279538597564d670e9ce513f286192" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cd/a7/649401dae7ea8645dd218aa2d9c351afa7b9e0645f07dc8776a1972c0cad/librt-0.11.0-cp39-cp39-win32.whl", hash = "sha256:140695816ddf3c86eb972981a26f35efd871c44b0c3aed44c8cd01749386617f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2a/7e/6a9711d78f338445e36992a90071962294f5bab388b554ef8a313e6412dd/librt-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:92f7ff819c197fc30473190a12c2856f325ac90aabfccbeb2072d28cc2e234e3" },
]
[[package]]
name = "markdown-it-py"
version = "3.0.0"
@@ -1849,203 +1700,6 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/e8/3d/1087453384dbde46a8c7f9356eead2c58be8a7bf156bca40243377c85715/more_itertools-11.1.0-py3-none-any.whl", hash = "sha256:4b65538ae22f6fed0ce4874efd317463a7489796a0939fa66824dd542125a192" },
]
[[package]]
name = "mypy"
version = "1.14.1"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version < '3.9'",
]
dependencies = [
{ name = "mypy-extensions", marker = "python_full_version < '3.9'" },
{ name = "tomli", marker = "python_full_version < '3.9'" },
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b9/eb/2c92d8ea1e684440f54fa49ac5d9a5f19967b7b472a281f419e69a8d228e/mypy-1.14.1.tar.gz", hash = "sha256:7ec88144fe9b510e8475ec2f5f251992690fcf89ccb4500b214b4226abcd32d6" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/9b/7a/87ae2adb31d68402da6da1e5f30c07ea6063e9f09b5e7cfc9dfa44075e74/mypy-1.14.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:52686e37cf13d559f668aa398dd7ddf1f92c5d613e4f8cb262be2fb4fedb0fcb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e1/23/eada4c38608b444618a132be0d199b280049ded278b24cbb9d3fc59658e4/mypy-1.14.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1fb545ca340537d4b45d3eecdb3def05e913299ca72c290326be19b3804b39c0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/43/c9/d6785c6f66241c62fd2992b05057f404237deaad1566545e9f144ced07f5/mypy-1.14.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:90716d8b2d1f4cd503309788e51366f07c56635a3309b0f6a32547eaaa36a64d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c3/62/daa7e787770c83c52ce2aaf1a111eae5893de9e004743f51bfcad9e487ec/mypy-1.14.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae753f5c9fef278bcf12e1a564351764f2a6da579d4a81347e1d5a15819997b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1b/a2/5fb18318a3637f29f16f4e41340b795da14f4751ef4f51c99ff39ab62e52/mypy-1.14.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e0fe0f5feaafcb04505bcf439e991c6d8f1bf8b15f12b05feeed96e9e7bf1427" },
{ url = "https://mirrors.aliyun.com/pypi/packages/28/99/e153ce39105d164b5f02c06c35c7ba958aaff50a2babba7d080988b03fe7/mypy-1.14.1-cp310-cp310-win_amd64.whl", hash = "sha256:7d54bd85b925e501c555a3227f3ec0cfc54ee8b6930bd6141ec872d1c572f81f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/da/11/a9422850fd506edbcdc7f6090682ecceaf1f87b9dd847f9df79942da8506/mypy-1.14.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f995e511de847791c3b11ed90084a7a0aafdc074ab88c5a9711622fe4751138c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b6/9e/47e450fd39078d9c02d620545b2cb37993a8a8bdf7db3652ace2f80521ca/mypy-1.14.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d64169ec3b8461311f8ce2fd2eb5d33e2d0f2c7b49116259c51d0d96edee48d1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/01/b5/6c8d33bd0f851a7692a8bfe4ee75eb82b6983a3cf39e5e32a5d2a723f0c1/mypy-1.14.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba24549de7b89b6381b91fbc068d798192b1b5201987070319889e93038967a8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f0/4c/e10e2c46ea37cab5c471d0ddaaa9a434dc1d28650078ac1b56c2d7b9b2e4/mypy-1.14.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:183cf0a45457d28ff9d758730cd0210419ac27d4d3f285beda038c9083363b1f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/88/55/beacb0c69beab2153a0f57671ec07861d27d735a0faff135a494cd4f5020/mypy-1.14.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f2a0ecc86378f45347f586e4163d1769dd81c5a223d577fe351f26b179e148b1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a2/75/8c93ff7f315c4d086a2dfcde02f713004357d70a163eddb6c56a6a5eff40/mypy-1.14.1-cp311-cp311-win_amd64.whl", hash = "sha256:ad3301ebebec9e8ee7135d8e3109ca76c23752bac1e717bc84cd3836b4bf3eae" },
{ url = "https://mirrors.aliyun.com/pypi/packages/43/1b/b38c079609bb4627905b74fc6a49849835acf68547ac33d8ceb707de5f52/mypy-1.14.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:30ff5ef8519bbc2e18b3b54521ec319513a26f1bba19a7582e7b1f58a6e69f14" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6b/75/2ed0d2964c1ffc9971c729f7a544e9cd34b2cdabbe2d11afd148d7838aa2/mypy-1.14.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cb9f255c18052343c70234907e2e532bc7e55a62565d64536dbc7706a20b78b9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a1/5f/7b8051552d4da3c51bbe8fcafffd76a6823779101a2b198d80886cd8f08e/mypy-1.14.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b4e3413e0bddea671012b063e27591b953d653209e7a4fa5e48759cda77ca11" },
{ url = "https://mirrors.aliyun.com/pypi/packages/04/90/f53971d3ac39d8b68bbaab9a4c6c58c8caa4d5fd3d587d16f5927eeeabe1/mypy-1.14.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:553c293b1fbdebb6c3c4030589dab9fafb6dfa768995a453d8a5d3b23784af2e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/03/d2/8bc0aeaaf2e88c977db41583559319f1821c069e943ada2701e86d0430b7/mypy-1.14.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fad79bfe3b65fe6a1efaed97b445c3d37f7be9fdc348bdb2d7cac75579607c89" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6f/17/07815114b903b49b0f2cf7499f1c130e5aa459411596668267535fe9243c/mypy-1.14.1-cp312-cp312-win_amd64.whl", hash = "sha256:8fa2220e54d2946e94ab6dbb3ba0a992795bd68b16dc852db33028df2b00191b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9e/15/bb6a686901f59222275ab228453de741185f9d54fecbaacec041679496c6/mypy-1.14.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:92c3ed5afb06c3a8e188cb5da4984cab9ec9a77ba956ee419c68a388b4595255" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f8/b3/8b0f74dfd072c802b7fa368829defdf3ee1566ba74c32a2cb2403f68024c/mypy-1.14.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dbec574648b3e25f43d23577309b16534431db4ddc09fda50841f1e34e64ed34" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c5/9b/4fd95ab20c52bb5b8c03cc49169be5905d931de17edfe4d9d2986800b52e/mypy-1.14.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8c6d94b16d62eb3e947281aa7347d78236688e21081f11de976376cf010eb31a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/56/9d/4a236b9c57f5d8f08ed346914b3f091a62dd7e19336b2b2a0d85485f82ff/mypy-1.14.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d4b19b03fdf54f3c5b2fa474c56b4c13c9dbfb9a2db4370ede7ec11a2c5927d9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/88/a61a5497e2f68d9027de2bb139c7bb9abaeb1be1584649fa9d807f80a338/mypy-1.14.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0c911fde686394753fff899c409fd4e16e9b294c24bfd5e1ea4675deae1ac6fd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/54/da/3d6fc5d92d324701b0c23fb413c853892bfe0e1dbe06c9138037d459756b/mypy-1.14.1-cp313-cp313-win_amd64.whl", hash = "sha256:8b21525cb51671219f5307be85f7e646a153e5acc656e5cebf64bfa076c50107" },
{ url = "https://mirrors.aliyun.com/pypi/packages/39/02/1817328c1372be57c16148ce7d2bfcfa4a796bedaed897381b1aad9b267c/mypy-1.14.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7084fb8f1128c76cd9cf68fe5971b37072598e7c31b2f9f95586b65c741a9d31" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b9/07/99db9a95ece5e58eee1dd87ca456a7e7b5ced6798fd78182c59c35a7587b/mypy-1.14.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8f845a00b4f420f693f870eaee5f3e2692fa84cc8514496114649cfa8fd5e2c6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9a/eb/85ea6086227b84bce79b3baf7f465b4732e0785830726ce4a51528173b71/mypy-1.14.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44bf464499f0e3a2d14d58b54674dee25c031703b2ffc35064bd0df2e0fac319" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4b/bb/f01bebf76811475d66359c259eabe40766d2f8ac8b8250d4e224bb6df379/mypy-1.14.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99f27732c0b7dc847adb21c9d47ce57eb48fa33a17bc6d7d5c5e9f9e7ae5bac" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2f/c9/84837ff891edcb6dcc3c27d85ea52aab0c4a34740ff5f0ccc0eb87c56139/mypy-1.14.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:bce23c7377b43602baa0bd22ea3265c49b9ff0b76eb315d6c34721af4cdf1d9b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/84/5f/901e18464e6a13f8949b4909535be3fa7f823291b8ab4e4b36cfe57d6769/mypy-1.14.1-cp38-cp38-win_amd64.whl", hash = "sha256:8edc07eeade7ebc771ff9cf6b211b9a7d93687ff892150cb5692e4f4272b0837" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ca/1f/186d133ae2514633f8558e78cd658070ba686c0e9275c5a5c24a1e1f0d67/mypy-1.14.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3888a1816d69f7ab92092f785a462944b3ca16d7c470d564165fe703b0970c35" },
{ url = "https://mirrors.aliyun.com/pypi/packages/af/fc/4842485d034e38a4646cccd1369f6b1ccd7bc86989c52770d75d719a9941/mypy-1.14.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:46c756a444117c43ee984bd055db99e498bc613a70bbbc120272bd13ca579fbc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b4/e6/457b83f2d701e23869cfec013a48a12638f75b9d37612a9ddf99072c1051/mypy-1.14.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27fc248022907e72abfd8e22ab1f10e903915ff69961174784a3900a8cba9ad9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f1/bf/76a569158db678fee59f4fd30b8e7a0d75bcbaeef49edd882a0d63af6d66/mypy-1.14.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:499d6a72fb7e5de92218db961f1a66d5f11783f9ae549d214617edab5d4dbdbb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/43/bc/0bc6b694b3103de9fed61867f1c8bd33336b913d16831431e7cb48ef1c92/mypy-1.14.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:57961db9795eb566dc1d1b4e9139ebc4c6b0cb6e7254ecde69d1552bf7613f60" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b0/79/5f5ec47849b6df1e6943d5fd8e6632fbfc04b4fd4acfa5a5a9535d11b4e2/mypy-1.14.1-cp39-cp39-win_amd64.whl", hash = "sha256:07ba89fdcc9451f2ebb02853deb6aaaa3d2239a236669a63ab3801bbf923ef5c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a0/b5/32dd67b69a16d088e533962e5044e51004176a9952419de0370cdaead0f8/mypy-1.14.1-py3-none-any.whl", hash = "sha256:b66a60cc4073aeb8ae00057f9c1f64d49e90f918fbcef9a977eb121da8b8f1d1" },
]
[[package]]
name = "mypy"
version = "1.19.1"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version > '3.9' and python_full_version < '3.10'",
"python_full_version == '3.9'",
]
dependencies = [
{ name = "librt", marker = "python_full_version == '3.9.*' and platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions", marker = "python_full_version == '3.9.*'" },
{ name = "pathspec", version = "1.1.1", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "tomli", marker = "python_full_version == '3.9.*'" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version == '3.9.*'" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec" },
{ url = "https://mirrors.aliyun.com/pypi/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab" },
{ url = "https://mirrors.aliyun.com/pypi/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042" },
{ url = "https://mirrors.aliyun.com/pypi/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13" },
{ url = "https://mirrors.aliyun.com/pypi/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef" },
{ url = "https://mirrors.aliyun.com/pypi/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75" },
{ url = "https://mirrors.aliyun.com/pypi/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b5/f7/88436084550ca9af5e610fa45286be04c3b63374df3e021c762fe8c4369f/mypy-1.19.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7bcfc336a03a1aaa26dfce9fff3e287a3ba99872a157561cbfcebe67c13308e3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ca/a5/43dfad311a734b48a752790571fd9e12d61893849a01bff346a54011957f/mypy-1.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b7951a701c07ea584c4fe327834b92a30825514c868b1f69c30445093fdd9d5a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/88/f0/efbfa391395cce2f2771f937e0620cfd185ec88f2b9cd88711028a768e96/mypy-1.19.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b13cfdd6c87fc3efb69ea4ec18ef79c74c3f98b4e5498ca9b85ab3b2c2329a67" },
{ url = "https://mirrors.aliyun.com/pypi/packages/25/05/58b3ba28f5aed10479e899a12d2120d582ba9fa6288851b20bf1c32cbb4f/mypy-1.19.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f28f99c824ecebcdaa2e55d82953e38ff60ee5ec938476796636b86afa3956e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c5/a0/c006ccaff50b31e542ae69b92fe7e2f55d99fba3a55e01067dd564325f85/mypy-1.19.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:c608937067d2fc5a4dd1a5ce92fd9e1398691b8c5d012d66e1ddd430e9244376" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b2/ff/8bdb051cd710f01b880472241bd36b3f817a8e1c5d5540d0b761675b6de2/mypy-1.19.1-cp39-cp39-win_amd64.whl", hash = "sha256:409088884802d511ee52ca067707b90c883426bd95514e8cfda8281dc2effe24" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247" },
]
[[package]]
name = "mypy"
version = "2.1.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.10' and python_full_version < '3.15'",
]
dependencies = [
{ name = "ast-serialize", marker = "python_full_version >= '3.10'" },
{ name = "librt", marker = "python_full_version >= '3.10' and platform_python_implementation != 'PyPy'" },
{ name = "mypy-extensions", marker = "python_full_version >= '3.10'" },
{ name = "pathspec", version = "1.1.1", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.10'" },
{ name = "tomli", marker = "python_full_version == '3.10.*'" },
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.10'" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/82/15/cca9d88503549ed6fedeaa1d448cdddd542ee8a490232d732e278036fbf2/mypy-2.1.0.tar.gz", hash = "sha256:81e76ad12c2d804512e9b13240d1588316531bfba07558286078bfbce9613633" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/a4/71/d351dca3e9b30da2328ee9d445c88b8388072808ebfbc49eb69d30b67749/mypy-2.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:11a6beb180257a805961aea9ec591bbd0bd17f1e18d35b8456d57aee5bedfedc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2f/45/7d51594b644c17c0bcf74ed8cd5fc33b324276d708e8506f220b70dab9d9/mypy-2.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ef78c1d306bbf9a8a12f526c44902c9c28dffd6c52c52bf6a72641ce18d3849" },
{ url = "https://mirrors.aliyun.com/pypi/packages/65/01/455c31b170e9468265074840bf18863a8482a24103fdaabe4e199392aa5f/mypy-2.1.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c209a90853081ff01d01ee895cafe10f7db1474e0d95beaeef0f6c1db9119bbd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/41/5a/93093f0b29a9e982deafde698f740a2eb2e05886e79ccf0594c7fd5413a3/mypy-2.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47cebf61abde7c088a4e27718a8b13a81655686b2e9c251f5c0915a802248166" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7f/2f/a196f5331d96170ad3d28f144d2aba690d4b2911381f68d51e489c7ab82a/mypy-2.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:d57a90ae5e872138a425ec328edbc9b235d1934c4377881a33ec05b341acc9a8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/54/de/94d321cc12da9f71341ac0c270efbed5c725750c7b4c334d957de9a087d9/mypy-2.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:aea7f7a8a55b459c34275fc468ada6ca7c173a5e43a68f5dbe588a563d8a06b8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e1/62/0c27ca55219a7c764a7fb88c7bb2b7b2f9780ade8bbf16bc8ed8400eef6b/mypy-2.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c989640253f0d76843e9c6c1bbf4bd48c5e85ada61bde4beb37cb3eca035685e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0a/a1/639f3024794a2a15899cb90707fe02e044c4412794c39c5769fd3df2e2ef/mypy-2.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a683016b16fe2f572dc04c72be7ee0504ac1605a265d0200f5cea695fb788f41" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3b/08/9a585dea4325f20d8b80dc78623fa50d1fd2173b710f6237afd6ba6ab39b/mypy-2.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1a293c534adb55271fef24a26da04b855540a8c13cc07bc5917b9fd2c394f2ca" },
{ url = "https://mirrors.aliyun.com/pypi/packages/81/dc/7c42cc9c6cb01e8eb09961f1f738741d3e9c7e9d5c5b30ec69222625cd5f/mypy-2.1.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7406f4d048e71e576f5356d317e5b0a9e666dfd966bd99f9d14ca06e1a341538" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d4/fa/285946c33bce716e082c11dfeee9ee196eaf1f5042efb3581a31f9f205e4/mypy-2.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e0210d626fc8b31ccc90233754c7bc90e1f43205e85d96387f7db1285b55c398" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2b/83/82397f48af6c27e295d57979ded8490c9829040152cf7571b2f026aeb9a0/mypy-2.1.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3712c20deed54e814eaaa825603bada8ea1c390670a397c95b98405347acc563" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/68/b02dec39057b88eb03dc0aa854732e26e8361f34f9d0e20c7614967d1eba/mypy-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:fcaa0e479066e31f7cceb6a3bea39cb22b2ff51a6b2f24f193d19179ba17c389" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cf/a8/ea3dcbef31f99b634f2ee23bb0321cbc8c1b388b76a861eb849f13c347dc/mypy-2.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:0b1a5260c95aa443083f9ed3592662941951bca3d4ca224a5dc517c38b7cf666" },
{ url = "https://mirrors.aliyun.com/pypi/packages/95/b1/55861beb5c339b44f9a2ba92df9e2cb1eeb4ae1eee674cdf7772c797778b/mypy-2.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:244358bf1c0da7722230bce60683d52e8e9fd030554926f15b747a84efb5b3af" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0b/b3/b7f770114b7d0ac92d0f76e8d93c2780844a70488a90e91821927850da86/mypy-2.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4ec7c57657493c7a75534df2751c8ae2cda383c16ecc55d2106c54476b1b16f6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b6/f3/8ae2037967e2126689a0c11d99e2b707134a565191e92c60ca2572aec60a/mypy-2.1.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8161b6ff4392410023224f0969d17db93e1e154bc3e4ba62598e720723ae211" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a0/32/615eb5911859e43d054941b0d0a7d06cfa2870eba86529cf385b052b111c/mypy-2.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf03e12003084a67395184d3eb8cbd6a489dc3655b5664b28c210a9e2403ab0b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d4/03/4eafbfff8bfab1b87082741eae6e6a624028c984e6708b73bce2a8570c9d/mypy-2.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:20509760fd791c51579d573153407d226385ec1f8bcce55d730b354f3336bc22" },
{ url = "https://mirrors.aliyun.com/pypi/packages/99/ee/919661478e5891a3c96e549c036e467e64563ab85995b10c53c8358e16a3/mypy-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:6753d0c1fdd6b1a23b9e4f283ce80b2153b724adcb2653b20b85a8a28ac6436b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/24/0a/6a12b9782ca0831a553192f351679f4548abc9d19a7cc93bb7feb02084c7/mypy-2.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:98ebb6589bb3b6d0c6f0c459d53ca55b8091fbc13d277c4041c885392e8195e8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6e/dd/c7191469c777f07689c032a8f7326e393ea34c92d6d76eb7ce5ba57ea66d/mypy-2.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:35aac3bb114e03888f535d5eb51b8bafbb3266586b599da1940f9b1be3ec5bd5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/55/8c/aed55408879043d72bb9135f4d0d19a02b886dd569631e113e3d2706cb8d/mypy-2.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8de55a8c861f2a49331f807be98d90caeceeef520bde13d43a160207f8af613e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3a/8e/f371a824b1f1fa8ea6e3dbb8703d232977d572be2329554a3bc4d960302f/mypy-2.1.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5fdf2941a07434af755837d9880f7d7d25f1dacb1af9dcd4b9b66f2220a3024e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/94/21/f54be870d6dd53a82c674407e0f8eed7174b05ec78d42e5abd7b42e84fd5/mypy-2.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e195b817c13f02352a9c124301f9f30f078405444679b6753c1b96b6eed37285" },
{ url = "https://mirrors.aliyun.com/pypi/packages/17/99/bf21748626a40ce59fd29a39386ab46afec88b7bd2f0fa6c3a97c995523f/mypy-2.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5431d42af987ebd92ba2f71d45c85ed41d8e6ca9f5fd209a69f68f707d2469e5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d6/d7/9e90d2cf47100bea550ed2bc7b0d4de3a62181d84d5e37da0003e8462637/mypy-2.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:767fe8c66dc3e01e19e1737d4c38ebefead16125e1b8e58ad421903b376f5c65" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ec/46/e5c449e858798e35ffc90946282a27c62a77be743fe17480e4977374eb91/mypy-2.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:ecfe70d43775ab99562ab128ce49854a362044c9f894961f68f898c23cb7429d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b0/ca/b279a672e874aedd5498ae25f722dacc8aa86bbffb939b3f97cbb1cf6686/mypy-2.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:7354c5a7f69d9345c3d6e69921d57088eea3ddeeb6b20d34c1b3855b02c36ec2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/27/e6/3efe56c631d959b9b4454e208b0ac4b7f4f58b404c89f8bec7b49efdfc21/mypy-2.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:49890d4f76ac9e06ec117f9e09f3174da70a620a0c300953d8595c926e80947f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/84/7f/8107ea87a44fd1f1b59882442f033c9c3488c127201b1d1d15f1cbd6022e/mypy-2.1.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:761be68e023ef5d94678772396a8af1220030f80837a3afd8d0aef3b419666f4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/51/4d/b6d34db183133b83761b9199a82d31557cdbb70a380d8c3b3438e11882a3/mypy-2.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c90345fc182dc363b891350457ec69c35140858538f38b4540845afcc32b1aef" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ff/d7/f08360c691d758acb02f45022c34d98b92892f4ea756644e1000d4b9f3d8/mypy-2.1.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b84802e7b5a6daf1f5e15bc9fcd7ddae77be13981ffab037f1c67bb84d67d135" },
{ url = "https://mirrors.aliyun.com/pypi/packages/67/1b/09460a13719530a19bce27bd3bc8449e83569dd2ba7faf51c9c3c30c0b61/mypy-2.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:022c771234936ceac541ebaf836fe9e2abeb3f5e09aff21588fe543ff006fe21" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/62/75dbf0f82f7b6680340efc614af29dd0b3c17b8a4f1cd09b8bd2fd6bc814/mypy-2.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:498207db725cec88829a6a5c2fc771205fd043719ef98bc49aba8fb9fc4e6d57" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b2/66/caca04ed7d972fb6eb6dd1ccd6df1de5c38fae8c5b3dc1c4e8e0d85ee6b9/mypy-2.1.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d5e5cad0efeba72b93cd17490cc0d69c5ac9ca132994fe3fb0314808aeeb83e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ed/52/2d90cbe49d014b13ed7ff337930c30bad35893fe38a1e4641e756bb62191/mypy-2.1.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff715050c127d724fd260a2e666e7747fdd83511c0c47d449d98238970aef780" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ac/37/d98f4a14e081b238992d0ed96b6d39c7cc0148c9699eb71eaa68629665ea/mypy-2.1.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82208da9e09414d520e912d3e462d454854bed0810b71540bb016dcbca7308fd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a3/c2/15c46613b24a84fad2aea1248bf9619b99c2767ae9071fe224c179a0b7d4/mypy-2.1.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e79ebc1b904b84f0310dff7469655a9c36c7a68bddb37bdd42b67a332df61d08" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5c/90/9c16a57f482c76d25f6379762b56bbf65c711d8158cf271fb2802cfb0640/mypy-2.1.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e583edc957cfb0deb142079162ae826f58449b116c1d442f2d91c69d9fced081" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0f/4c/215a4eeb63cacc5f17f516691ea7285d11e249802b942476bff15922a314/mypy-2.1.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b33b6cd332695bba180d55e717a79d3038e479a2c49cc5eb3d53603409b9a5d7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4b/50/1043e1db5f455ffe4c9ab22747cd8ca2bc492b1e4f4e21b130a44ee2b217/mypy-2.1.0-cp314-cp314t-win_arm64.whl", hash = "sha256:4f910fe825376a7b66ef7ca8c98e5a149e8cd64c19ae71d84047a74ee060d4e6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0d/2a/13ca1f292f6db1b98ff495ef3467736b331621c5917cad984b7043e7348d/mypy-2.1.0-py3-none-any.whl", hash = "sha256:a663814603a5c563fb87a4f96fb473eeb30d1f5a4885afcf44f9db000a366289" },
]
[[package]]
name = "mypy-extensions"
version = "1.1.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505" },
]
[[package]]
name = "nodejs-wheel-binaries"
version = "24.16.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/a3/22/2a5beb4e21417c73233d9f65cf6f3e96e891b80d2f550a8f630ebc6b88c6/nodejs_wheel_binaries-24.16.0.tar.gz", hash = "sha256:c973cb69dc5fd16e6f6dc6e579e2c3d5534e2a1f57619dddf5ba070efa7dde37" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/83/d1/68b43b53cd0fa83ae6fd406705023ca988d9e0ca41c724d82e66fbeb2ef6/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_arm64.whl", hash = "sha256:d9f8f677dcf30e37ac244f07869726abe043f01eb0f45722b1df31cc2af7093c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e9/b2/40a989159599080da485de966c4c2d207e852ac7aa7864702626d96c8bf5/nodejs_wheel_binaries-24.16.0-py2.py3-none-macosx_13_0_x86_64.whl", hash = "sha256:3d0370fe7120ce9697a4f60d40480d2bd8808d9f30131458d5afc0040d4e5a51" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d7/a7/cd42174fb5ff6faff7fa8d326a18914d8f232098ab5de055b57c16fa13ca/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:85dc92bbb79c851569c5925dcc2a4c915a034efab375f99e4e7e6bbe9cca8342" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2b/95/c8a1f9ae140aa28df8744d984d01d4b3af7cdd6555af12127f40ceb45a7d/nodejs_wheel_binaries-24.16.0-py2.py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:2f3036292811514ba847b3708492644764f88a833ac425c5f55007014308ddfd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/64/c9/7c35b3737f59e36d0249c265397b7bff570519b95301d6e16ea361e904ad/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:db8a8a76ebd2b28ecbfc9ad464baa3707241b9e050a30e2efdf6f60c0f886502" },
{ url = "https://mirrors.aliyun.com/pypi/packages/04/96/d931255cf9d11a84d6b54d882dba7434646467d568ccf070ea3418638df3/nodejs_wheel_binaries-24.16.0-py2.py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f1a3d8f7b4491cbbd023ba3fc4e901fcca2d9fb80d57f24ba3890de8b1dbac03" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a2/7b/8b7a3f41bc255411be30b6d7d288aab8ffd9ea2055db8555ced3548007b9/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_amd64.whl", hash = "sha256:bb136be9944f0662dcf1120f45193a6b75b13fac378971a95cc42c9f879a81aa" },
{ url = "https://mirrors.aliyun.com/pypi/packages/17/66/1ed71f1f529b8ca727d42c7ceb9db0bef145ce4a13dfc86fb50aa44f3be6/nodejs_wheel_binaries-24.16.0-py2.py3-none-win_arm64.whl", hash = "sha256:8308940b5edd0a50dc5267ea36ba21c9f668e83fe0d9f293937174d3a7e31c36" },
]
[[package]]
name = "packaging"
version = "26.2"
@@ -2094,6 +1748,315 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523" },
]
[[package]]
name = "pillow"
version = "10.4.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version < '3.9'",
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/cd/74/ad3d526f3bf7b6d3f408b73fde271ec69dfac8b81341a318ce825f2b3812/pillow-10.4.0.tar.gz", hash = "sha256:166c1cd4d24309b30d61f79f4a9114b7b2313d7450912277855ff5dfd7cd4a06" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/0e/69/a31cccd538ca0b5272be2a38347f8839b97a14be104ea08b0db92f749c74/pillow-10.4.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:4d9667937cfa347525b319ae34375c37b9ee6b525440f3ef48542fcf66f2731e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9a/9e/4143b907be8ea0bce215f2ae4f7480027473f8b61fcedfda9d851082a5d2/pillow-10.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:543f3dc61c18dafb755773efc89aae60d06b6596a63914107f75459cf984164d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8a/25/1fc45761955f9359b1169aa75e241551e74ac01a09f487adaaf4c3472d11/pillow-10.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7928ecbf1ece13956b95d9cbcfc77137652b02763ba384d9ab508099a2eca856" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5e/dd/425b95d0151e1d6c951f45051112394f130df3da67363b6bc75dc4c27aba/pillow-10.4.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4d49b85c4348ea0b31ea63bc75a9f3857869174e2bf17e7aba02945cd218e6f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b1/84/9a15cc5726cbbfe7f9f90bfb11f5d028586595907cd093815ca6644932e3/pillow-10.4.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:6c762a5b0997f5659a5ef2266abc1d8851ad7749ad9a6a5506eb23d314e4f46b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b5/5b/6651c288b08df3b8c1e2f8c1152201e0b25d240e22ddade0f1e242fc9fa0/pillow-10.4.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:a985e028fc183bf12a77a8bbf36318db4238a3ded7fa9df1b9a133f1cb79f8fc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/07/8b/34854bf11a83c248505c8cb0fcf8d3d0b459a2246c8809b967963b6b12ae/pillow-10.4.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:812f7342b0eee081eaec84d91423d1b4650bb9828eb53d8511bcef8ce5aecf1e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/78/63/0632aee4e82476d9cbe5200c0cdf9ba41ee04ed77887432845264d81116d/pillow-10.4.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ac1452d2fbe4978c2eec89fb5a23b8387aba707ac72810d9490118817d9c0b46" },
{ url = "https://mirrors.aliyun.com/pypi/packages/df/56/b8663d7520671b4398b9d97e1ed9f583d4afcbefbda3c6188325e8c297bd/pillow-10.4.0-cp310-cp310-win32.whl", hash = "sha256:bcd5e41a859bf2e84fdc42f4edb7d9aba0a13d29a2abadccafad99de3feff984" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f4/72/0203e94a91ddb4a9d5238434ae6c1ca10e610e8487036132ea9bf806ca2a/pillow-10.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:ecd85a8d3e79cd7158dec1c9e5808e821feea088e2f69a974db5edf84dc53141" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bd/52/7e7e93d7a6e4290543f17dc6f7d3af4bd0b3dd9926e2e8a35ac2282bc5f4/pillow-10.4.0-cp310-cp310-win_arm64.whl", hash = "sha256:ff337c552345e95702c5fde3158acb0625111017d0e5f24bf3acdb9cc16b90d1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a7/62/c9449f9c3043c37f73e7487ec4ef0c03eb9c9afc91a92b977a67b3c0bbc5/pillow-10.4.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:0a9ec697746f268507404647e531e92889890a087e03681a3606d9b920fbee3c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f4/5f/491dafc7bbf5a3cc1845dc0430872e8096eb9e2b6f8161509d124594ec2d/pillow-10.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dfe91cb65544a1321e631e696759491ae04a2ea11d36715eca01ce07284738be" },
{ url = "https://mirrors.aliyun.com/pypi/packages/73/d5/c4011a76f4207a3c151134cd22a1415741e42fa5ddecec7c0182887deb3d/pillow-10.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dc6761a6efc781e6a1544206f22c80c3af4c8cf461206d46a1e6006e4429ff3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ac/10/c67e20445a707f7a610699bba4fe050583b688d8cd2d202572b257f46600/pillow-10.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e84b6cc6a4a3d76c153a6b19270b3526a5a8ed6b09501d3af891daa2a9de7d6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a9/83/6523837906d1da2b269dee787e31df3b0acb12e3d08f024965a3e7f64665/pillow-10.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:bbc527b519bd3aa9d7f429d152fea69f9ad37c95f0b02aebddff592688998abe" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ba/e5/8c68ff608a4203085158cff5cc2a3c534ec384536d9438c405ed6370d080/pillow-10.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:76a911dfe51a36041f2e756b00f96ed84677cdeb75d25c767f296c1c1eda1319" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f4/7c/01b8dbdca5bc6785573f4cee96e2358b0918b7b2c7b60d8b6f3abf87a070/pillow-10.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:59291fb29317122398786c2d44427bbd1a6d7ff54017075b22be9d21aa59bd8d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c8/57/2899b82394a35a0fbfd352e290945440e3b3785655a03365c0ca8279f351/pillow-10.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:416d3a5d0e8cfe4f27f574362435bc9bae57f679a7158e0096ad2beb427b8696" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4d/d7/a44f193d4c26e58ee5d2d9db3d4854b2cfb5b5e08d360a5e03fe987c0086/pillow-10.4.0-cp311-cp311-win32.whl", hash = "sha256:7086cc1d5eebb91ad24ded9f58bec6c688e9f0ed7eb3dbbf1e4800280a896496" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c1/d0/5866318eec2b801cdb8c82abf190c8343d8a1cd8bf5a0c17444a6f268291/pillow-10.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:cbed61494057c0f83b83eb3a310f0bf774b09513307c434d4366ed64f4128a91" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d4/c8/310ac16ac2b97e902d9eb438688de0d961660a87703ad1561fd3dfbd2aa0/pillow-10.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:f5f0c3e969c8f12dd2bb7e0b15d5c468b51e5017e01e2e867335c81903046a22" },
{ url = "https://mirrors.aliyun.com/pypi/packages/05/cb/0353013dc30c02a8be34eb91d25e4e4cf594b59e5a55ea1128fde1e5f8ea/pillow-10.4.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:673655af3eadf4df6b5457033f086e90299fdd7a47983a13827acf7459c15d94" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e7/cf/5c558a0f247e0bf9cec92bff9b46ae6474dd736f6d906315e60e4075f737/pillow-10.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:866b6942a92f56300012f5fbac71f2d610312ee65e22f1aa2609e491284e5597" },
{ url = "https://mirrors.aliyun.com/pypi/packages/84/48/6e394b86369a4eb68b8a1382c78dc092245af517385c086c5094e3b34428/pillow-10.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29dbdc4207642ea6aad70fbde1a9338753d33fb23ed6956e706936706f52dd80" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3b/f3/a8c6c11fa84b59b9df0cd5694492da8c039a24cd159f0f6918690105c3be/pillow-10.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf2342ac639c4cf38799a44950bbc2dfcb685f052b9e262f446482afaf4bffca" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7d/1b/c14b4197b80150fb64453585247e6fb2e1d93761fa0fa9cf63b102fde822/pillow-10.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f5b92f4d70791b4a67157321c4e8225d60b119c5cc9aee8ecf153aace4aad4ef" },
{ url = "https://mirrors.aliyun.com/pypi/packages/55/77/40daddf677897a923d5d33329acd52a2144d54a9644f2a5422c028c6bf2d/pillow-10.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:86dcb5a1eb778d8b25659d5e4341269e8590ad6b4e8b44d9f4b07f8d136c414a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/54/90de3e4256b1207300fb2b1d7168dd912a2fb4b2401e439ba23c2b2cabde/pillow-10.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:780c072c2e11c9b2c7ca37f9a2ee8ba66f44367ac3e5c7832afcfe5104fd6d1b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/13/24/1bfba52f44193860918ff7c93d03d95e3f8748ca1de3ceaf11157a14cf16/pillow-10.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37fb69d905be665f68f28a8bba3c6d3223c8efe1edf14cc4cfa06c241f8c81d9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/55/04/5e6de6e6120451ec0c24516c41dbaf80cce1b6451f96561235ef2429da2e/pillow-10.4.0-cp312-cp312-win32.whl", hash = "sha256:7dfecdbad5c301d7b5bde160150b4db4c659cee2b69589705b6f8a0c509d9f42" },
{ url = "https://mirrors.aliyun.com/pypi/packages/74/0a/d4ce3c44bca8635bd29a2eab5aa181b654a734a29b263ca8efe013beea98/pillow-10.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1d846aea995ad352d4bdcc847535bd56e0fd88d36829d2c90be880ef1ee4668a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b5/ca/184349ee40f2e92439be9b3502ae6cfc43ac4b50bc4fc6b3de7957563894/pillow-10.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:e553cad5179a66ba15bb18b353a19020e73a7921296a7979c4a2b7f6a5cd57f9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c3/00/706cebe7c2c12a6318aabe5d354836f54adff7156fd9e1bd6c89f4ba0e98/pillow-10.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8bc1a764ed8c957a2e9cacf97c8b2b053b70307cf2996aafd70e91a082e70df3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cf/76/f658cbfa49405e5ecbfb9ba42d07074ad9792031267e782d409fd8fe7c69/pillow-10.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6209bb41dc692ddfee4942517c19ee81b86c864b626dbfca272ec0f7cff5d9fb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/46/2b/99c28c4379a85e65378211971c0b430d9c7234b1ec4d59b2668f6299e011/pillow-10.4.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bee197b30783295d2eb680b311af15a20a8b24024a19c3a26431ff83eb8d1f70" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f1/74/b1ec314f624c0c43711fdf0d8076f82d9d802afd58f1d62c2a86878e8615/pillow-10.4.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef61f5dd14c300786318482456481463b9d6b91ebe5ef12f405afbba77ed0be" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4a/2a/4b04157cb7b9c74372fa867096a1607e6fedad93a44deeff553ccd307868/pillow-10.4.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:297e388da6e248c98bc4a02e018966af0c5f92dfacf5a5ca22fa01cb3179bca0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ac/7b/8f1d815c1a6a268fe90481232c98dd0e5fa8c75e341a75f060037bd5ceae/pillow-10.4.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:e4db64794ccdf6cb83a59d73405f63adbe2a1887012e308828596100a0b2f6cc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e5/77/05fa64d1f45d12c22c314e7b97398ffb28ef2813a485465017b7978b3ce7/pillow-10.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd2880a07482090a3bcb01f4265f1936a903d70bc740bfcb1fd4e8a2ffe5cf5a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/12/63/b0397cfc2caae05c3fb2f4ed1b4fc4fc878f0243510a7a6034ca59726494/pillow-10.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4b35b21b819ac1dbd1233317adeecd63495f6babf21b7b2512d244ff6c6ce309" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7b/f9/cfaa5082ca9bc4a6de66ffe1c12c2d90bf09c309a5f52b27759a596900e7/pillow-10.4.0-cp313-cp313-win32.whl", hash = "sha256:551d3fd6e9dc15e4c1eb6fc4ba2b39c0c7933fa113b220057a34f4bb3268a060" },
{ url = "https://mirrors.aliyun.com/pypi/packages/01/6a/30ff0eef6e0c0e71e55ded56a38d4859bf9d3634a94a88743897b5f96936/pillow-10.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:030abdbe43ee02e0de642aee345efa443740aa4d828bfe8e2eb11922ea6a21ea" },
{ url = "https://mirrors.aliyun.com/pypi/packages/48/2c/2e0a52890f269435eee38b21c8218e102c621fe8d8df8b9dd06fabf879ba/pillow-10.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b001114dd152cfd6b23befeb28d7aee43553e2402c9f159807bf55f33af8a8d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/56/70/f40009702a477ce87d8d9faaa4de51d6562b3445d7a314accd06e4ffb01d/pillow-10.4.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8d4d5063501b6dd4024b8ac2f04962d661222d120381272deea52e3fc52d3736" },
{ url = "https://mirrors.aliyun.com/pypi/packages/10/43/105823d233c5e5d31cea13428f4474ded9d961652307800979a59d6a4276/pillow-10.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c1ee6f42250df403c5f103cbd2768a28fe1a0ea1f0f03fe151c8741e1469c8b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3c/ad/7850c10bac468a20c918f6a5dbba9ecd106ea1cdc5db3c35e33a60570408/pillow-10.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b15e02e9bb4c21e39876698abf233c8c579127986f8207200bc8a8f6bb27acf2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/84/4c/69bbed9e436ac22f9ed193a2b64f64d68fcfbc9f4106249dc7ed4889907b/pillow-10.4.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a8d4bade9952ea9a77d0c3e49cbd8b2890a399422258a77f357b9cc9be8d680" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8f/4f/c183c63828a3f37bf09644ce94cbf72d4929b033b109160a5379c2885932/pillow-10.4.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:43efea75eb06b95d1631cb784aa40156177bf9dd5b4b03ff38979e048258bc6b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fb/ad/435fe29865f98a8fbdc64add8875a6e4f8c97749a93577a8919ec6f32c64/pillow-10.4.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:950be4d8ba92aca4b2bb0741285a46bfae3ca699ef913ec8416c1b78eadd64cd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/80/74/be8bf8acdfd70e91f905a12ae13cfb2e17c0f1da745c40141e26d0971ff5/pillow-10.4.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d7480af14364494365e89d6fddc510a13e5a2c3584cb19ef65415ca57252fb84" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e4/90/763616e66dc9ad59c9b7fb58f863755e7934ef122e52349f62c7742b82d3/pillow-10.4.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:73664fe514b34c8f02452ffb73b7a92c6774e39a647087f83d67f010eb9a0cf0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/69/66/03002cb5b2c27bb519cba63b9f9aa3709c6f7a5d3b285406c01f03fb77e5/pillow-10.4.0-cp38-cp38-win32.whl", hash = "sha256:e88d5e6ad0d026fba7bdab8c3f225a69f063f116462c49892b0149e21b6c0a0e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f2/75/3cb820b2812405fc7feb3d0deb701ef0c3de93dc02597115e00704591bc9/pillow-10.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:5161eef006d335e46895297f642341111945e2c1c899eb406882a6c61a4357ab" },
{ url = "https://mirrors.aliyun.com/pypi/packages/31/85/955fa5400fa8039921f630372cfe5056eed6e1b8e0430ee4507d7de48832/pillow-10.4.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:0ae24a547e8b711ccaaf99c9ae3cd975470e1a30caa80a6aaee9a2f19c05701d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/23/9c/343827267eb28d41cd82b4180d33b10d868af9077abcec0af9793aa77d2d/pillow-10.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:298478fe4f77a4408895605f3482b6cc6222c018b2ce565c2b6b9c354ac3229b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/60/a3/7ebbeabcd341eab722896d1a5b59a3df98c4b4d26cf4b0385f8aa94296f7/pillow-10.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:134ace6dc392116566980ee7436477d844520a26a4b1bd4053f6f47d096997fd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/32/3f/c02268d0c6fb6b3958bdda673c17b315c821d97df29ae6969f20fb49388a/pillow-10.4.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:930044bb7679ab003b14023138b50181899da3f25de50e9dbee23b61b4de2126" },
{ url = "https://mirrors.aliyun.com/pypi/packages/67/5d/1c93c8cc35f2fdd3d6cc7e4ad72d203902859a2867de6ad957d9b708eb8d/pillow-10.4.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:c76e5786951e72ed3686e122d14c5d7012f16c8303a674d18cdcd6d89557fc5b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bc/a8/8655557c9c7202b8abbd001f61ff36711cefaf750debcaa1c24d154ef602/pillow-10.4.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b2724fdb354a868ddf9a880cb84d102da914e99119211ef7ecbdc613b8c96b3c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/58/78/6f95797af64d137124f68af1bdaa13b5332da282b86031f6fa70cf368261/pillow-10.4.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dbc6ae66518ab3c5847659e9988c3b60dc94ffb48ef9168656e0019a93dbf8a1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8a/6d/2b3ce34f1c4266d79a78c9a51d1289a33c3c02833fe294ef0dcbb9cba4ed/pillow-10.4.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:06b2f7898047ae93fad74467ec3d28fe84f7831370e3c258afa533f81ef7f3df" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e3/e0/456258c74da1ff5bf8ef1eab06a95ca994d8b9ed44c01d45c3f8cbd1db7e/pillow-10.4.0-cp39-cp39-win32.whl", hash = "sha256:7970285ab628a3779aecc35823296a7869f889b8329c16ad5a71e4901a3dc4ef" },
{ url = "https://mirrors.aliyun.com/pypi/packages/37/f8/bef952bdb32aa53741f58bf21798642209e994edc3f6598f337f23d5400a/pillow-10.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:961a7293b2457b405967af9c77dcaa43cc1a8cd50d23c532e62d48ab6cdd56f5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bb/8e/805201619cad6651eef5fc1fdef913804baf00053461522fabbc5588ea12/pillow-10.4.0-cp39-cp39-win_arm64.whl", hash = "sha256:32cda9e3d601a52baccb2856b8ea1fc213c90b340c542dcef77140dfa3278a9e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/38/30/095d4f55f3a053392f75e2eae45eba3228452783bab3d9a920b951ac495c/pillow-10.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5b4815f2e65b30f5fbae9dfffa8636d992d49705723fe86a3661806e069352d4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f3/e8/4ff79788803a5fcd5dc35efdc9386af153569853767bff74540725b45863/pillow-10.4.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8f0aef4ef59694b12cadee839e2ba6afeab89c0f39a3adc02ed51d109117b8da" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d7/ac/4184edd511b14f760c73f5bb8a5d6fd85c591c8aff7c2229677a355c4179/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9f4727572e2918acaa9077c919cbbeb73bd2b3ebcfe033b72f858fc9fbef0026" },
{ url = "https://mirrors.aliyun.com/pypi/packages/da/21/1749cd09160149c0a246a81d646e05f35041619ce76f6493d6a96e8d1103/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff25afb18123cea58a591ea0244b92eb1e61a1fd497bf6d6384f09bc3262ec3e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b6/f5/f71fe1888b96083b3f6dfa0709101f61fc9e972c0c8d04e9d93ccef2a045/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:dc3e2db6ba09ffd7d02ae9141cfa0ae23393ee7687248d46a7507b75d610f4f5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/96/b9/c0362c54290a31866c3526848583a2f45a535aa9d725fd31e25d318c805f/pillow-10.4.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:02a2be69f9c9b8c1e97cf2713e789d4e398c751ecfd9967c18d0ce304efbf885" },
{ url = "https://mirrors.aliyun.com/pypi/packages/52/3b/ce7a01026a7cf46e5452afa86f97a5e88ca97f562cafa76570178ab56d8d/pillow-10.4.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:0755ffd4a0c6f267cccbae2e9903d95477ca2f77c4fcf3a3a09570001856c8a5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e1/1f/5a9fcd6ced51633c22481417e11b1b47d723f64fb536dfd67c015eb7f0ab/pillow-10.4.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:a02364621fe369e06200d4a16558e056fe2805d3468350df3aef21e00d26214b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cb/e6/3ea4755ed5320cb62aa6be2f6de47b058c6550f752dd050e86f694c59798/pillow-10.4.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1b5dea9831a90e9d0721ec417a80d4cbd7022093ac38a568db2dd78363b00908" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0a/22/492f9f61e4648422b6ca39268ec8139277a5b34648d28f400faac14e0f48/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b885f89040bb8c4a1573566bbb2f44f5c505ef6e74cec7ab9068c900047f04b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f9/19/559a48ad4045704bb0547965b9a9345f5cd461347d977a56d178db28819e/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87dd88ded2e6d74d31e1e0a99a726a6765cda32d00ba72dc37f0651f306daaa8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d9/de/cebaca6fb79905b3a1aa0281d238769df3fb2ede34fd7c0caa286575915a/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:2db98790afc70118bd0255c2eeb465e9767ecf1f3c25f9a1abb8ffc8cfd1fe0a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/71/f0/86d5b2f04693b0116a01d75302b0a307800a90d6c351a8aa4f8ae76cd499/pillow-10.4.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:f7baece4ce06bade126fb84b8af1c33439a76d8a6fd818970215e0560ca28c27" },
{ url = "https://mirrors.aliyun.com/pypi/packages/37/ae/2dbfc38cc4fd14aceea14bc440d5151b21f64c4c3ba3f6f4191610b7ee5d/pillow-10.4.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:cfdd747216947628af7b259d274771d84db2268ca062dd5faf373639d00113a3" },
]
[[package]]
name = "pillow"
version = "11.3.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version > '3.9' and python_full_version < '3.10'",
"python_full_version == '3.9'",
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/4c/5d/45a3553a253ac8763f3561371432a90bdbe6000fbdcf1397ffe502aa206c/pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7c/c8/67c12ab069ef586a25a4a79ced553586748fad100c77c0ce59bb4983ac98/pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2f/bd/6741ebd56263390b382ae4c5de02979af7f8bd9807346d068700dd6d5cf9/pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ca/0b/c412a9e27e1e6a829e6ab6c2dca52dd563efbedf4c9c6aa453d9a9b77359/pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/59/9d/9b7076aaf30f5dd17e5e5589b2d2f5a5d7e30ff67a171eb686e4eecc2adf/pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f0/16/1a6bf01fb622fb9cf5c91683823f073f053005c849b1f52ed613afcf8dae/pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7b/e6/6ff7077077eb47fde78739e7d570bdcd7c10495666b6afcd23ab56b19a43/pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c3/3a/b13f36832ea6d279a697231658199e0a03cd87ef12048016bdcc84131601/pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6c/e4/61b2e1a7528740efbc70b3d581f33937e38e98ef3d50b05007267a55bcb2/pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a9/d3/60c781c83a785d6afbd6a326ed4d759d141de43aa7365725cbcd65ce5e54/pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9f/28/4f4a0203165eefb3763939c6789ba31013a2e90adffb456610f30f613850/pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94" },
{ url = "https://mirrors.aliyun.com/pypi/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024" },
{ url = "https://mirrors.aliyun.com/pypi/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1e/93/0952f2ed8db3a5a4c7a11f91965d6184ebc8cd7cbb7941a260d5f018cd2d/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4b/e8/100c3d114b1a0bf4042f27e0f87d2f25e857e838034e98ca98fe7b8c0a9c/pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/aa/86/3f758a28a6e381758545f7cdb4942e1cb79abd271bea932998fc0db93cb6/pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/01/f4/91d5b3ffa718df2f53b0dc109877993e511f4fd055d7e9508682e8aba092/pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f9/0e/37d7d3eca6c879fbd9dba21268427dffda1ab00d4eb05b32923d4fbe3b12/pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ff/b0/3426e5c7f6565e752d81221af9d3676fdbb4f352317ceafd42899aaf5d8a/pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fc/c1/c6c423134229f2a221ee53f838d4be9d82bab86f7e2f8e75e47b6bf6cd77/pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ba/c9/09e6746630fe6372c67c648ff9deae52a2bc20897d51fa293571977ceb5d/pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d5/1c/a2a29649c0b1983d3ef57ee87a66487fdeb45132df66ab30dd37f7dbe162/pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/36/de/d5cc31cc4b055b6c6fd990e3e7f0f8aaf36229a2698501bcb0cdf67c7146/pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d5/ea/502d938cbaeec836ac28a9b730193716f0114c41325db428e6b280513f09/pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/45/9c/9c5e2a73f125f6cbc59cc7087c8f2d649a7ae453f83bd0362ff7c9e2aee2/pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/23/85/397c73524e0cd212067e0c969aa245b01d50183439550d24d9f55781b776/pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51" },
{ url = "https://mirrors.aliyun.com/pypi/packages/17/d2/622f4547f69cd173955194b78e4d19ca4935a1b0f03a302d655c9f6aae65/pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580" },
{ url = "https://mirrors.aliyun.com/pypi/packages/dd/80/a8a2ac21dda2e82480852978416cfacd439a4b490a501a288ecf4fe2532d/pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/44/d6/b79754ca790f315918732e18f82a8146d33bcd7f4494380457ea89eb883d/pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/49/20/716b8717d331150cb00f7fdd78169c01e8e0c219732a78b0e59b6bdb2fd6/pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced" },
{ url = "https://mirrors.aliyun.com/pypi/packages/74/cf/a9f3a2514a65bb071075063a96f0a5cf949c2f2fce683c15ccc83b1c1cab/pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/98/3c/da78805cbdbee9cb43efe8261dd7cc0b4b93f2ac79b676c03159e9db2187/pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6c/fa/ce044b91faecf30e635321351bba32bab5a7e034c60187fe9698191aef4f/pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7b/51/90f9291406d09bf93686434f9183aba27b831c10c87746ff49f127ee80cb/pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cd/5a/6fec59b1dfb619234f7636d4157d11fb4e196caeee220232a8d2ec48488d/pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/49/6b/00187a044f98255225f172de653941e61da37104a9ea60e4f6887717e2b5/pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e8/5c/6caaba7e261c0d75bab23be79f1d06b5ad2a2ae49f028ccec801b0e853d6/pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f3/7e/b623008460c09a0cb38263c93b828c666493caee2eb34ff67f778b87e58c/pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/73/f4/04905af42837292ed86cb1b1dabe03dce1edc008ef14c473c5c7e1443c5d/pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12" },
{ url = "https://mirrors.aliyun.com/pypi/packages/41/b0/33d79e377a336247df6348a54e6d2a2b85d644ca202555e3faa0cf811ecc/pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/49/2d/ed8bc0ab219ae8768f529597d9509d184fe8a6c4741a6864fea334d25f3f/pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b5/3d/b932bb4225c80b58dfadaca9d42d08d0b7064d2d1791b6a237f87f661834/pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673" },
{ url = "https://mirrors.aliyun.com/pypi/packages/09/b5/0487044b7c096f1b48f0d7ad416472c02e0e4bf6919541b111efd3cae690/pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a8/2d/524f9318f6cbfcc79fbc004801ea6b607ec3f843977652fdee4857a7568b/pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6f/d2/a9a4f280c6aefedce1e8f615baaa5474e0701d86dd6f1dede66726462bbd/pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874" },
{ url = "https://mirrors.aliyun.com/pypi/packages/fe/54/86b0cd9dbb683a9d5e960b66c7379e821a19be4ac5810e2e5a715c09a0c0/pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e7/95/88efcaf384c3588e24259c4203b909cbe3e3c2d887af9e938c2022c9dd48/pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2e/cc/934e5820850ec5eb107e7b1a72dd278140731c669f396110ebc326f2a503/pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d6/e9/9c0a616a71da2a5d163aa37405e8aced9a906d574b4a214bede134e731bc/pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1a/33/c88376898aff369658b225262cd4f2659b13e8178e7534df9e6e1fa289f6/pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1f/70/d376247fb36f1844b42910911c83a02d5544ebd2a8bad9efcc0f707ea774/pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653" },
{ url = "https://mirrors.aliyun.com/pypi/packages/eb/1c/537e930496149fbac69efd2fc4329035bbe2e5475b4165439e3be9cb183b/pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bd/57/80f53264954dcefeebcf9dae6e3eb1daea1b488f0be8b8fef12f79a3eb10/pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36" },
{ url = "https://mirrors.aliyun.com/pypi/packages/70/ff/4727d3b71a8578b4587d9c276e90efad2d6fe0335fd76742a6da08132e8c/pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/05/ae/716592277934f85d3be51d7256f3636672d7b1abfafdc42cf3f8cbd4b4c8/pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e7/bb/7fe6cddcc8827b01b1a9766f5fdeb7418680744f9082035bdbabecf1d57f/pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8b/f5/06bfaa444c8e80f1a8e4bff98da9c83b37b5be3b1deaa43d27a0db37ef84/pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f0/77/bc6f92a3e8e6e46c0ca78abfffec0037845800ea38c73483760362804c41/pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4a/82/3a721f7d69dca802befb8af08b7c79ebcab461007ce1c18bd91a5d5896f9/pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db" },
{ url = "https://mirrors.aliyun.com/pypi/packages/89/c7/5572fa4a3f45740eaab6ae86fcdf7195b55beac1371ac8c619d880cfe948/pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9e/8e/9c089f01677d1264ab8648352dcb7773f37da6ad002542760c80107da816/pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b5/a9/5749930caf674695867eb56a581e78eb5f524b7583ff10b01b6e5048acb3/pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081" },
{ url = "https://mirrors.aliyun.com/pypi/packages/43/46/0b85b763eb292b691030795f9f6bb6fcaf8948c39413c81696a01c3577f7/pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5e/c6/1a230ec0067243cbd60bc2dad5dc3ab46a8a41e21c15f5c9b52b26873069/pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc" },
{ url = "https://mirrors.aliyun.com/pypi/packages/63/dd/f296c27ffba447bfad76c6a0c44c1ea97a90cb9472b9304c94a732e8dbfb/pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a5/a0/98a3630f0b57f77bae67716562513d3032ae70414fcaf02750279c389a9e/pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/de/e6/83dfba5646a290edd9a21964da07674409e410579c341fc5b8f7abd81620/pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bc/41/15ab268fe6ee9a2bc7391e2bbb20a98d3974304ab1a406a992dcb297a370/pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/64/79/6d4f638b288300bed727ff29f2a3cb63db054b33518a95f27724915e3fbc/pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71" },
{ url = "https://mirrors.aliyun.com/pypi/packages/46/05/4106422f45a05716fd34ed21763f8ec182e8ea00af6e9cb05b93a247361a/pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada" },
{ url = "https://mirrors.aliyun.com/pypi/packages/63/c6/287fd55c2c12761d0591549d48885187579b7c257bef0c6660755b0b59ae/pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6f/8b/209bd6b62ce8367f47e68a218bffac88888fdf2c9fcf1ecadc6c3ec1ebc7/pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2e/e6/231a0b76070c2cfd9e260a7a5b504fb72da0a95279410fa7afd99d9751d6/pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe" },
{ url = "https://mirrors.aliyun.com/pypi/packages/13/f4/10cf94fda33cb12765f2397fc285fa6d8eb9c29de7f3185165b702fc7386/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/72/c9/583821097dc691880c92892e8e2d41fe0a5a3d6021f4963371d2f6d57250/pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3b/8e/5c9d410f9217b12320efc7c413e72693f48468979a013ad17fd690397b9a/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27" },
{ url = "https://mirrors.aliyun.com/pypi/packages/62/bb/78347dbe13219991877ffb3a91bf09da8317fbfcd4b5f9140aeae020ad71/pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d9/28/1000353d5e61498aaeaaf7f1e4b49ddb05f2c6575f9d4f9f914a3538b6e1/pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8" },
]
[[package]]
name = "pillow"
version = "12.2.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.10' and python_full_version < '3.15'",
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/3a/aa/d0b28e1c811cd4d5f5c2bfe2e022292bd255ae5744a3b9ac7d6c8f72dd75/pillow-12.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:a4e8f36e677d3336f35089648c8955c51c6d386a13cf6ee9c189c5f5bd713a9f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/27/8e/1d5b39b8ae2bd7650d0c7b6abb9602d16043ead9ebbfef4bc4047454da2a/pillow-12.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e589959f10d9824d39b350472b92f0ce3b443c0a3442ebf41c40cb8361c5b97" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f0/c5/dcb7a6ca6b7d3be41a76958e90018d56c8462166b3ef223150360850c8da/pillow-12.2.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a52edc8bfff4429aaabdf4d9ee0daadbbf8562364f940937b941f87a4290f5ff" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ea/f1/aa1bb13b2f4eba914e9637893c73f2af8e48d7d4023b9d3750d4c5eb2d0c/pillow-12.2.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:975385f4776fafde056abb318f612ef6285b10a1f12b8570f3647ad0d74b48ec" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a1/2a/8c79d6a53169937784604a8ae8d77e45888c41537f7f6f65ed1f407fe66d/pillow-12.2.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd9c0c7a0c681a347b3194c500cb1e6ca9cab053ea4d82a5cf45b6b754560136" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b5/42/bbcb6051030e1e421d103ce7a8ecadf837aa2f39b8f82ef1a8d37c3d4ebc/pillow-12.2.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88d387ff40b3ff7c274947ed3125dedf5262ec6919d83946753b5f3d7c67ea4c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3f/e1/c2a7d6dd8cfa6b231227da096fd2d58754bab3603b9d73bf609d3c18b64f/pillow-12.2.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:51c4167c34b0d8ba05b547a3bb23578d0ba17b80a5593f93bd8ecb123dd336a3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5f/41/7c8617da5d32e1d2f026e509484fdb6f3ad7efaef1749a0c1928adbb099e/pillow-12.2.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34c0d99ecccea270c04882cb3b86e7b57296079c9a4aff88cb3b33563d95afaa" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2d/de/a777627e19fd6d62f84070ee1521adde5eeda4855b5cf60fe0b149118bca/pillow-12.2.0-cp310-cp310-win32.whl", hash = "sha256:b85f66ae9eb53e860a873b858b789217ba505e5e405a24b85c0464822fe88032" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e7/34/fc4cb5204896465842767b96d250c08410f01f2f28afc43b257de842eed5/pillow-12.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:673aa32138f3e7531ccdbca7b3901dba9b70940a19ccecc6a37c77d5fdeb05b5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2d/a0/32852d36bc7709f14dc3f64f929a275e958ad8c19a6deba9610d458e28b3/pillow-12.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:3e080565d8d7c671db5802eedfb438e5565ffa40115216eabb8cd52d0ecce024" },
{ url = "https://mirrors.aliyun.com/pypi/packages/68/e1/748f5663efe6edcfc4e74b2b93edfb9b8b99b67f21a854c3ae416500a2d9/pillow-12.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:8be29e59487a79f173507c30ddf57e733a357f67881430449bb32614075a40ab" },
{ url = "https://mirrors.aliyun.com/pypi/packages/47/a1/d5ff69e747374c33a3b53b9f98cca7889fce1fd03d79cdc4e1bccc6c5a87/pillow-12.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:71cde9a1e1551df7d34a25462fc60325e8a11a82cc2e2f54578e5e9a1e153d65" },
{ url = "https://mirrors.aliyun.com/pypi/packages/df/21/e3fbdf54408a973c7f7f89a23b2cb97a7ef30c61ab4142af31eee6aebc88/pillow-12.2.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f490f9368b6fc026f021db16d7ec2fbf7d89e2edb42e8ec09d2c60505f5729c7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d3/f1/00b7278c7dd52b17ad4329153748f87b6756ec195ff786c2bdf12518337d/pillow-12.2.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8bd7903a5f2a4545f6fd5935c90058b89d30045568985a71c79f5fd6edf9b91e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ad/cf/220a5994ef1b10e70e85748b75649d77d506499352be135a4989c957b701/pillow-12.2.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3997232e10d2920a68d25191392e3a4487d8183039e1c74c2297f00ed1c50705" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e9/bd/e51a61b1054f09437acfbc2ff9106c30d1eb76bc1453d428399946781253/pillow-12.2.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e74473c875d78b8e9d5da2a70f7099549f9eb37ded4e2f6a463e60125bccd176" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6b/3d/45132c57d5fb4b5744567c3817026480ac7fc3ce5d4c47902bc0e7f6f853/pillow-12.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:56a3f9c60a13133a98ecff6197af34d7824de9b7b38c3654861a725c970c197b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/7d/2e/9df2fc1e82097b1df3dce58dc43286aa01068e918c07574711fcc53e6fb4/pillow-12.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:90e6f81de50ad6b534cab6e5aef77ff6e37722b2f5d908686f4a5c9eba17a909" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bd/2e/2941e42858ebb67e50ae741473de81c2984e6eff7b397017623c676e2e8d/pillow-12.2.0-cp311-cp311-win32.whl", hash = "sha256:8c984051042858021a54926eb597d6ee3012393ce9c181814115df4c60b9a808" },
{ url = "https://mirrors.aliyun.com/pypi/packages/69/42/836b6f3cd7f3e5fa10a1f1a5420447c17966044c8fbf589cc0452d5502db/pillow-12.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:6e6b2a0c538fc200b38ff9eb6628228b77908c319a005815f2dde585a0664b60" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c2/88/549194b5d6f1f494b485e493edc6693c0a16f4ada488e5bd974ed1f42fad/pillow-12.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:9a8a34cc89c67a65ea7437ce257cea81a9dad65b29805f3ecee8c8fe8ff25ffe" },
{ url = "https://mirrors.aliyun.com/pypi/packages/58/be/7482c8a5ebebbc6470b3eb791812fff7d5e0216c2be3827b30b8bb6603ed/pillow-12.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2d192a155bbcec180f8564f693e6fd9bccff5a7af9b32e2e4bf8c9c69dbad6b5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d8/95/0a351b9289c2b5cbde0bacd4a83ebc44023e835490a727b2a3bd60ddc0f4/pillow-12.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3f40b3c5a968281fd507d519e444c35f0ff171237f4fdde090dd60699458421" },
{ url = "https://mirrors.aliyun.com/pypi/packages/de/af/4e8e6869cbed569d43c416fad3dc4ecb944cb5d9492defaed89ddd6fe871/pillow-12.2.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:03e7e372d5240cc23e9f07deca4d775c0817bffc641b01e9c3af208dbd300987" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e9/9e/c05e19657fd57841e476be1ab46c4d501bffbadbafdc31a6d665f8b737b6/pillow-12.2.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b86024e52a1b269467a802258c25521e6d742349d760728092e1bc2d135b4d76" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2b/54/1789c455ed10176066b6e7e6da1b01e50e36f94ba584dc68d9eebfe9156d/pillow-12.2.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7371b48c4fa448d20d2714c9a1f775a81155050d383333e0a6c15b1123dda005" },
{ url = "https://mirrors.aliyun.com/pypi/packages/43/e3/fdc657359e919462369869f1c9f0e973f353f9a9ee295a39b1fea8ee1a77/pillow-12.2.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62f5409336adb0663b7caa0da5c7d9e7bdbaae9ce761d34669420c2a801b2780" },
{ url = "https://mirrors.aliyun.com/pypi/packages/8b/f8/2f6825e441d5b1959d2ca5adec984210f1ec086435b0ed5f52c19b3b8a6e/pillow-12.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:01afa7cf67f74f09523699b4e88c73fb55c13346d212a59a2db1f86b0a63e8c5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/67/f9/029a27095ad20f854f9dba026b3ea6428548316e057e6fc3545409e86651/pillow-12.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc3d34d4a8fbec3e88a79b92e5465e0f9b842b628675850d860b8bd300b159f5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/be/42/025cfe05d1be22dbfdb4f264fe9de1ccda83f66e4fc3aac94748e784af04/pillow-12.2.0-cp312-cp312-win32.whl", hash = "sha256:58f62cc0f00fd29e64b29f4fd923ffdb3859c9f9e6105bfc37ba1d08994e8940" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5d/7b/25a221d2c761c6a8ae21bfa3874988ff2583e19cf8a27bf2fee358df7942/pillow-12.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7f84204dee22a783350679a0333981df803dac21a0190d706a50475e361c93f5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/10/e1/542a474affab20fd4a0f1836cb234e8493519da6b76899e30bcc5d990b8b/pillow-12.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:af73337013e0b3b46f175e79492d96845b16126ddf79c438d7ea7ff27783a414" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795" },
{ url = "https://mirrors.aliyun.com/pypi/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed" },
{ url = "https://mirrors.aliyun.com/pypi/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795" },
{ url = "https://mirrors.aliyun.com/pypi/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea" },
{ url = "https://mirrors.aliyun.com/pypi/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98" },
{ url = "https://mirrors.aliyun.com/pypi/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295" },
{ url = "https://mirrors.aliyun.com/pypi/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae" },
{ url = "https://mirrors.aliyun.com/pypi/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7" },
{ url = "https://mirrors.aliyun.com/pypi/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463" },
{ url = "https://mirrors.aliyun.com/pypi/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166" },
{ url = "https://mirrors.aliyun.com/pypi/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06" },
{ url = "https://mirrors.aliyun.com/pypi/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/4e/b7/2437044fb910f499610356d1352e3423753c98e34f915252aafecc64889f/pillow-12.2.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538bd5e05efec03ae613fd89c4ce0368ecd2ba239cc25b9f9be7ed426b0af1f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f6/f4/8316e31de11b780f4ac08ef3654a75555e624a98db1056ecb2122d008d5a/pillow-12.2.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:394167b21da716608eac917c60aa9b969421b5dcbbe02ae7f013e7b85811c69d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/d4/37/664fca7201f8bb2aa1d20e2c3d5564a62e6ae5111741966c8319ca802361/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5d04bfa02cc2d23b497d1e90a0f927070043f6cbf303e738300532379a4b4e0f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/49/62/5b0ed78fce87346be7a5cfcfaaad91f6a1f98c26f86bdbafa2066c647ef6/pillow-12.2.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0c838a5125cee37e68edec915651521191cef1e6aa336b855f495766e77a366e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c3/28/ec0fc38107fc32536908034e990c47914c57cd7c5a3ece4d8d8f7ffd7e27/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a6c9fa44005fa37a91ebfc95d081e8079757d2e904b27103f4f5fa6f0bf78c0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5e/8b/51b0eddcfa2180d60e41f06bd6d0a62202b20b59c68f5a132e615b75aecf/pillow-12.2.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25373b66e0dd5905ed63fa3cae13c82fbddf3079f2c8bf15c6fb6a35586324c1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bc/60/5382c03e1970de634027cee8e1b7d39776b778b81812aaf45b694dfe9e28/pillow-12.2.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:bfa9c230d2fe991bed5318a5f119bd6780cda2915cca595393649fc118ab895e" },
]
[[package]]
name = "platformdirs"
version = "4.3.6"
@@ -2221,7 +2184,7 @@ wheels = [
[[package]]
name = "pyflowx"
version = "0.1.3"
version = "0.1.8"
source = { editable = "." }
dependencies = [
{ name = "graphlib-backport", marker = "python_full_version < '3.9'" },
@@ -2229,15 +2192,12 @@ dependencies = [
[package.optional-dependencies]
dev = [
{ name = "basedpyright" },
{ name = "hatch", version = "1.14.2", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "hatch", version = "1.15.1", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "hatch", version = "1.17.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.10'" },
{ name = "httpx" },
{ name = "mypy", version = "1.14.1", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "mypy", version = "1.19.1", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "mypy", version = "2.1.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.10'" },
{ name = "prek" },
{ name = "pyrefly" },
{ name = "pytest", version = "8.3.5", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "pytest", version = "8.4.2", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "pytest", version = "9.1.1", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.10'" },
@@ -2260,20 +2220,34 @@ dev = [
{ name = "tox-uv", version = "1.28.1", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "tox-uv", version = "1.35.2", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.10'" },
]
office = [
{ name = "pillow", version = "10.4.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "pillow", version = "11.3.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "pillow", version = "12.2.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.10'" },
{ name = "pymupdf", version = "1.24.11", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "pymupdf", version = "1.26.5", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "pymupdf", version = "1.27.2.3", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.10'" },
{ name = "pypdf", version = "5.9.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "pypdf", version = "6.13.3", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.9'" },
{ name = "pytesseract" },
]
[package.dev-dependencies]
dev = [
{ name = "pyflowx", extra = ["dev"] },
{ name = "pyflowx", extra = ["dev", "office"] },
]
[package.metadata]
requires-dist = [
{ name = "basedpyright", marker = "extra == 'dev'", specifier = ">=1.39.8" },
{ name = "graphlib-backport", marker = "python_full_version < '3.9'", specifier = ">=1.0.0" },
{ name = "hatch", marker = "extra == 'dev'", specifier = ">=1.14.2" },
{ name = "httpx", marker = "extra == 'dev'", specifier = ">=0.28.0" },
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.14.1" },
{ name = "pillow", marker = "extra == 'office'", specifier = ">=10.4.0" },
{ name = "prek", marker = "extra == 'dev'", specifier = ">=0.4.5" },
{ name = "pymupdf", marker = "extra == 'office'", specifier = ">=1.24.11" },
{ name = "pypdf", marker = "extra == 'office'", specifier = ">=5.9.0" },
{ name = "pyrefly", marker = "extra == 'dev'", specifier = ">=1.1.1" },
{ name = "pytesseract", marker = "extra == 'office'", specifier = ">=0.3.13" },
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" },
@@ -2284,10 +2258,10 @@ requires-dist = [
{ name = "tox", marker = "extra == 'dev'", specifier = ">=4.25.0" },
{ name = "tox-uv", marker = "extra == 'dev'", specifier = ">=1.13.1" },
]
provides-extras = ["dev"]
provides-extras = ["dev", "office"]
[package.metadata.requires-dev]
dev = [{ name = "pyflowx", extras = ["dev"], editable = "." }]
dev = [{ name = "pyflowx", extras = ["dev", "office"], editable = "." }]
[[package]]
name = "pygments"
@@ -2316,6 +2290,96 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176" },
]
[[package]]
name = "pymupdf"
version = "1.24.11"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version < '3.9'",
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/d4/a3/3edbb6be649e311107b320141cae0353d4cc9c6593eba7691f16c53c9c71/PyMuPDF-1.24.11.tar.gz", hash = "sha256:6e45e57f14ac902029d4aacf07684958d0e58c769f47d9045b2048d0a3d20155" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/f5/75/b059d603530d99926de2b6a64314f3534e2149ee5496142de550c66907ac/PyMuPDF-1.24.11-cp38-abi3-macosx_10_9_x86_64.whl", hash = "sha256:24c35ba9e731027ff24566b90d4986e9aac75e1ce47589b25de51e3c687ddb73" },
{ url = "https://mirrors.aliyun.com/pypi/packages/16/f8/8396ca7218622cb3600c919b320a24f05b7c14bd81eea03f3f2182844a06/PyMuPDF-1.24.11-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:20c8eb65b855a33411246d6697a3f3166727fe2d8585753cf0db648730104be6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/55/3d/84bd559129d2ff07267baae0bde0c6f4f49232408b547971f7a2e1534cb9/PyMuPDF-1.24.11-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32fd013e3c844f105c0a6a43ee82acc7cd0c900f6ff14f5eed9492840bbcbdd9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/ca/21/ad66778ad2485f87ef1d5a36f17ec8d4aee8ce247c8e46c673eff776a877/PyMuPDF-1.24.11-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2efb793644df99db0fe2468149048175cf25c5803997828efc9152aca838f5f2" },
{ url = "https://mirrors.aliyun.com/pypi/packages/6a/92/9ff020892560f80433876ec904c0f2669d1d69403adf412565e54a946615/PyMuPDF-1.24.11-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9b7ac5b8ec3daec17f2e830962ed091610e576a5e531d2fe28c437fbd69b1969" },
{ url = "https://mirrors.aliyun.com/pypi/packages/28/6b/a0247598f06585d84ae9927d6ed191d89d38686ad6bf0dadc0ed699a77e7/PyMuPDF-1.24.11-cp38-abi3-win32.whl", hash = "sha256:6fda6c7ed7e6ad74d9cfac5c3837ef42efd58c506440e2513a0a200bc3c4dbc0" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f6/03/99895f003d7ff59c83d524aeccecff4e1ee1f39a7724f88acfda4f67b8bc/PyMuPDF-1.24.11-cp38-abi3-win_amd64.whl", hash = "sha256:745ce77532702d6ddeeecb47306d3669629aa5ff82708318cd652881f493b0ba" },
]
[[package]]
name = "pymupdf"
version = "1.26.5"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version > '3.9' and python_full_version < '3.10'",
"python_full_version == '3.9'",
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8d/9a/e0a4e92a85fc17be7c54afdbb113f0ade2a8bca49856d510e28bd249e462/pymupdf-1.26.5.tar.gz", hash = "sha256:8ef335e07f648492df240f2247854d0e7c0467afb9c4dc2376ec30978ec158c3" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/dd/3f/7fc927fd66922ce838d4c974ff9a685c5f5aba108a5d94914dc05c9371f5/pymupdf-1.26.5-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2bfb58f07ad631e5f71ad0bd6f1ff52700f7ba7ebb4973130e81e75b721beae1" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c1/e2/e87e62284ba98d59f1fd4fc7542ef2ed0002525754a485fa4077b3bbddae/pymupdf-1.26.5-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:d58599479bc471d3ae56c3d68d9160d0b7de8a3bd40221ddc3a4eaae2d281b86" },
{ url = "https://mirrors.aliyun.com/pypi/packages/df/c2/af93c6367f79e9b5435f803bde51c1dc8225f054f8238162dda80b44986d/pymupdf-1.26.5-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:7dfea81fdd73437a6a6ce83e1fcf556faee9327a6540571e58bf04fa362bb0cd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/5b/5a/1292a0df4ff71fbc00dfa8c08759d17c97e1e8ea9277eb5bc5f079ca188d/pymupdf-1.26.5-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:caad0ffeb63dcc4a29ca40f3c68d7b78d32a932e834b0056b529cc0bdbaaffc9" },
{ url = "https://mirrors.aliyun.com/pypi/packages/28/90/87b7fdfc9cd6991a3eb69a5752f6343374c34f258c511c242f4d60791eea/pymupdf-1.26.5-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e24e7a7d696bd398543cc5c147869edb2026d5d5a21b7f8e35db2f20170b389e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/2c/99/9d4b36485538e29df0a013fb02bbf6b5b0743a428fa07515e36631c43363/pymupdf-1.26.5-cp39-abi3-win32.whl", hash = "sha256:a2a42f5911d153a47bf5c3e162a0bfe8745eb9bec3e59fbaf87617b4003d8270" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c6/96/fd59c1532891762ea4815e73956c532053d5e26d56969e1e5d1e4ca4b207/pymupdf-1.26.5-cp39-abi3-win_amd64.whl", hash = "sha256:39a6fb58182b27b51ea8150a0cd2e4ee7e0cf71e9d6723978f28699b42ee61ae" },
]
[[package]]
name = "pymupdf"
version = "1.27.2.3"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.10' and python_full_version < '3.15'",
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/22/32/708bedc9dde7b328d45abbc076091769d44f2f24ad151ad92d56a6ec142b/pymupdf-1.27.2.3.tar.gz", hash = "sha256:7a92faa25129e8bbec5e50eeb9214f187665428c31b05c4ef6e36c58c0b1c6d2" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/dc/09/ddbdfa7ee91fbabd6f63d7d744884cbdfe3e7ff9b8604749fb38bddf5c5d/pymupdf-1.27.2.3-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:fc1bc3cae6e9e150b0dbb0a9221bdfd411d65f0db2fe359eaa22467d7cc2a05f" },
{ url = "https://mirrors.aliyun.com/pypi/packages/01/89/3f8edd6c4f50ca370e2a2f2a3011face36f3760728ffe76dffec91c0fca0/pymupdf-1.27.2.3-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:660d93cb6da5bbddf11d3982ae27745dd3a9902d9f24cdb69adab83962294b5a" },
{ url = "https://mirrors.aliyun.com/pypi/packages/c3/26/b7e5a70eb83bd189f8b5df87ec442746b992f2f632662839b288170d357d/pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1dd460a3ae4597a755f00a3bd9771f5ebf1531dc111f6a36bf05dd00a6b84425" },
{ url = "https://mirrors.aliyun.com/pypi/packages/e4/a0/aa1ee2240f29481a04a827c313333b4ecd8a14d6ac3e15d3f41a30574781/pymupdf-1.27.2.3-cp310-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:857842b4888827bd6155a1131341b2822a7ebe9a8c15a975fd7d490d7a64a30c" },
{ url = "https://mirrors.aliyun.com/pypi/packages/69/49/4f742451f980840829fc00ba158bebb25d389c846d8f4f8c65936ee55de8/pymupdf-1.27.2.3-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:580983849c64a08d08344ca3d1580e87c01f046a8392421797bc850efd72a5b6" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f6/3f/3853d6608f394faf6eec2bd4e8ea9f6a00beea329b071abdb29f4164cc3d/pymupdf-1.27.2.3-cp310-abi3-win32.whl", hash = "sha256:a5c1088a87189891a4946ab314a14b7934ac4c5b6077f7e74ebee956f8906d0e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/44/47/5fb10fe73f96b31253a41647c362ea9e0380920bddf16028414a051247fc/pymupdf-1.27.2.3-cp310-abi3-win_amd64.whl", hash = "sha256:d20f68ef15195e073071dbc4ae7455257c7889af7584e39df490c0a92728526e" },
{ url = "https://mirrors.aliyun.com/pypi/packages/53/a4/b9e91aac82293f9c954654c85581ee8212b5b05efadc534b581141241e6f/pymupdf-1.27.2.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:77691604c5d1d0233827139bbcdea61fd57879c84712b8e49b1f45520f7ab9c2" },
]
[[package]]
name = "pypdf"
version = "5.9.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version < '3.9'",
]
dependencies = [
{ name = "typing-extensions", version = "4.13.2", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/89/3a/584b97a228950ed85aec97c811c68473d9b8d149e6a8c155668287cf1a28/pypdf-5.9.0.tar.gz", hash = "sha256:30f67a614d558e495e1fbb157ba58c1de91ffc1718f5e0dfeb82a029233890a1" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/48/d9/6cff57c80a6963e7dd183bf09e9f21604a77716644b1e580e97b259f7612/pypdf-5.9.0-py3-none-any.whl", hash = "sha256:be10a4c54202f46d9daceaa8788be07aa8cd5ea8c25c529c50dd509206382c35" },
]
[[package]]
name = "pypdf"
version = "6.13.3"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.10' and python_full_version < '3.15'",
"python_full_version > '3.9' and python_full_version < '3.10'",
"python_full_version == '3.9'",
]
dependencies = [
{ name = "typing-extensions", version = "4.15.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.9' and python_full_version < '3.11'" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/17/18/9947cc201af9ccf76720fd3347bf4f70eb882ce3fcf4cb05f7443e4cf871/pypdf-6.13.3.tar.gz", hash = "sha256:f3cb822769725f1bac658c406cfc9460399043f3750c2d3e4650e0a85eacabd7" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/94/56/2967e621598987905fb8cdfadd8f8de6b5c68c9351f0523c4df8409f28f1/pypdf-6.13.3-py3-none-any.whl", hash = "sha256:c6e3f86afb625791510b02ad5480e94b63970bb957df75d44657c282ecc52224" },
]
[[package]]
name = "pyproject-api"
version = "1.8.0"
@@ -2375,6 +2439,40 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913" },
]
[[package]]
name = "pyrefly"
version = "1.1.1"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/8e/20/976165fa4b1517a1a92f393b3f4d4badabfff1165eff09d4cd4908428183/pyrefly-1.1.1.tar.gz", hash = "sha256:6deda959f8603a7dbdf112c48983e2275b2903cf33c8c739ed65d7e71a4fd520" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/b5/d6/02ba666018c6a1cb4ddfa2db98ada721adddd374db5c29ba47a0bf2637fa/pyrefly-1.1.1-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f4b8595f91885bc8b5e3c282ab68d1df21201668a84e6508b1e15f2feec0bb8d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/71/47/7a3457dbbddb513a83cf4fe527d5d5ebda5201a1010ad2a6034030e3e358/pyrefly-1.1.1-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6b238e1362622d47a6eb5af704fd8b613c94e8c303386efd6350e3da59fecc8" },
{ url = "https://mirrors.aliyun.com/pypi/packages/84/df/70f4b3f42d58ed686a80df31e04eca54d88036cea4f9b96195c64ad0b2b5/pyrefly-1.1.1-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b50d4510e4f8aaea79e2c4b343a4d7a060c9451c0b2aa9bfe10d7ca1ef33d68d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/3c/53/12a19bd6c7af985bcbc13c6910d0f9f6684069ead2282a5c08c2bfbb5d03/pyrefly-1.1.1-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f330cf039ef3da3b910c84f3a7e431f0cf8d0c1d2dad26491d6cadf3c7cd4759" },
{ url = "https://mirrors.aliyun.com/pypi/packages/93/f0/e55c48a50076fc0f9ecf4bdedec50456db383e01162f5e2121f8468be071/pyrefly-1.1.1-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a6342d87c52b04f72156da04f554c4d57f3616f2b32d1763969efb22d05a1407" },
{ url = "https://mirrors.aliyun.com/pypi/packages/b6/e7/30e085b31fed978ecb675bdbb54df566673ab550469e5af2d350f6af0be6/pyrefly-1.1.1-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c08b814ad03175e9cf47111390537161828b472044c39ab3320252b3ac6b2edd" },
{ url = "https://mirrors.aliyun.com/pypi/packages/47/58/49c3e67641133d3fe5d8d9a660dc0826c6c37ca197d86cad05fa7dd8bfd6/pyrefly-1.1.1-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d50cad97f19fc893b04deff7239626cffff5dd27ffb29b7d303a1b770247b208" },
{ url = "https://mirrors.aliyun.com/pypi/packages/71/1e/65a7ba8355e2c39d8331832905fb74dcc85fc122a3f1dfd6dbf2a88907ad/pyrefly-1.1.1-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:2150b450ee6a6bcbe69b2d45d9a4ebc934a609e1abcf65e490433f38eb873d84" },
{ url = "https://mirrors.aliyun.com/pypi/packages/37/de/b7ee1ab2392c36945738246fba7524439810befa3cfcc03cb6157567fc10/pyrefly-1.1.1-py3-none-win32.whl", hash = "sha256:5ffd8a8ed62fe4e6bf0afe1837d1bad149bb3b9f80e928ef248c96b836db3742" },
{ url = "https://mirrors.aliyun.com/pypi/packages/a6/9c/a0f5b52934bf80e9c7eff08222e7caf318287b9aef76acb8d9ac5740581b/pyrefly-1.1.1-py3-none-win_amd64.whl", hash = "sha256:4e0430f3ef69c8ac73505fd6584db70ed504665a9f0816fef7f723de510f26cb" },
{ url = "https://mirrors.aliyun.com/pypi/packages/42/3d/4c6bcb3d456835f51445d3662a428f56c3ea5643ec798c577030ae34298c/pyrefly-1.1.1-py3-none-win_arm64.whl", hash = "sha256:83baf0db71e172665db1fca0ced50b8f7773f5192ca57e8ac6773a772b6d2fc5" },
]
[[package]]
name = "pytesseract"
version = "0.3.13"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
dependencies = [
{ name = "packaging" },
{ name = "pillow", version = "10.4.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version < '3.9'" },
{ name = "pillow", version = "11.3.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version == '3.9.*'" },
{ name = "pillow", version = "12.2.0", source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }, marker = "python_full_version >= '3.10'" },
]
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/9f/a6/7d679b83c285974a7cb94d739b461fa7e7a9b17a3abfd7bf6cbc5c2394b0/pytesseract-0.3.13.tar.gz", hash = "sha256:4bf5f880c99406f52a3cfc2633e42d9dc67615e69d8a509d74867d3baddb5db9" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/7a/33/8312d7ce74670c9d39a532b2c246a853861120486be9443eebf048043637/pytesseract-0.3.13-py3-none-any.whl", hash = "sha256:7a99c6c2ac598360693d83a416e36e0b33a67638bb9d77fdcac094a3589d4b34" },
]
[[package]]
name = "pytest"
version = "8.3.5"
@@ -3081,7 +3179,6 @@ name = "typing-extensions"
version = "4.15.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple/" }
resolution-markers = [
"python_full_version >= '3.15'",
"python_full_version >= '3.10' and python_full_version < '3.15'",
"python_full_version > '3.9' and python_full_version < '3.10'",
"python_full_version == '3.9'",