From 707e2ac07c01f2985a167b4233333a3fe7858fb9 Mon Sep 17 00:00:00 2001 From: gooker_young Date: Sun, 21 Jun 2026 22:46:05 +0800 Subject: [PATCH] =?UTF-8?q?feat(cli):=20=E6=96=B0=E5=A2=9E=E6=89=B9?= =?UTF-8?q?=E9=87=8FCLI=E5=B7=A5=E5=85=B7=E6=A8=A1=E5=9D=97=E5=8F=8A?= =?UTF-8?q?=E9=85=8D=E5=A5=97=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增17个CLI工具实现,覆盖清屏、进程管理、环境配置、文件处理、SSH部署、代码格式化、打包等场景,同时更新pyproject.toml添加对应命令入口和office依赖包 --- pyproject.toml | 101 +++++--- src/pyflowx/cli/__init__.py | 73 ++++++ src/pyflowx/cli/autofmt.py | 273 ++++++++++++++++++++ src/pyflowx/cli/bumpversion.py | 101 ++++++++ src/pyflowx/cli/clearscreen.py | 68 +++++ src/pyflowx/cli/envpy.py | 118 +++++++++ src/pyflowx/cli/envqt.py | 86 +++++++ src/pyflowx/cli/envrs.py | 135 ++++++++++ src/pyflowx/cli/filedate.py | 116 +++++++++ src/pyflowx/cli/filelevel.py | 141 ++++++++++ src/pyflowx/cli/folderback.py | 94 +++++++ src/pyflowx/cli/folderzip.py | 82 ++++++ src/pyflowx/cli/lscalc.py | 167 ++++++++++++ src/pyflowx/cli/packtool.py | 307 ++++++++++++++++++++++ src/pyflowx/cli/pdftool.py | 458 +++++++++++++++++++++++++++++++++ src/pyflowx/cli/piptool.py | 164 ++++++++++++ src/pyflowx/cli/screenshot.py | 152 +++++++++++ src/pyflowx/cli/sshcopyid.py | 118 +++++++++ src/pyflowx/cli/taskkill.py | 78 ++++++ src/pyflowx/cli/which.py | 149 +++++++++++ uv.lock | 435 ++++++++++++++++++++++++++++++- 21 files changed, 3373 insertions(+), 43 deletions(-) create mode 100644 src/pyflowx/cli/autofmt.py create mode 100644 src/pyflowx/cli/bumpversion.py create mode 100644 src/pyflowx/cli/clearscreen.py create mode 100644 src/pyflowx/cli/envpy.py create mode 100644 src/pyflowx/cli/envqt.py create mode 100644 src/pyflowx/cli/envrs.py create mode 100644 src/pyflowx/cli/filedate.py create mode 100644 src/pyflowx/cli/filelevel.py create mode 100644 src/pyflowx/cli/folderback.py create mode 100644 src/pyflowx/cli/folderzip.py create mode 100644 src/pyflowx/cli/lscalc.py create mode 100644 src/pyflowx/cli/packtool.py create mode 100644 src/pyflowx/cli/pdftool.py create mode 100644 src/pyflowx/cli/piptool.py create mode 100644 src/pyflowx/cli/screenshot.py create mode 100644 src/pyflowx/cli/sshcopyid.py create mode 100644 src/pyflowx/cli/taskkill.py create mode 100644 src/pyflowx/cli/which.py diff --git a/pyproject.toml b/pyproject.toml index 9718c8f..cc2ea73 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,26 @@ requires-python = ">=3.8" version = "0.1.6" [project.scripts] -pymake = "pyflowx.cli.pymake:main" -gitt = "pyflowx.cli.gittool:main" +autofmt = "pyflowx.cli.autofmt:main" +bumpver = "pyflowx.cli.bumpversion:main" +clrscr = "pyflowx.cli.clearscreen: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" +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" +whichcmd = "pyflowx.cli.which:main" [project.optional-dependencies] dev = [ @@ -39,14 +57,20 @@ 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" -requires = ["hatchling"] +requires = ["hatchling"] [[tool.uv.index]] default = true -url = "https://mirrors.aliyun.com/pypi/simple/" +url = "https://mirrors.aliyun.com/pypi/simple/" [tool.hatch.build.targets.wheel] packages = ["src/pyflowx"] @@ -58,13 +82,13 @@ packages = ["src/pyflowx"] pyflowx = { workspace = true } [dependency-groups] -dev = ["pyflowx[dev]"] +dev = ["pyflowx[dev,office]"] [tool.coverage.run] -branch = true +branch = true concurrency = ["thread"] -omit = ["src/pyflowx/examples/*", "tests/*"] -source = ["pyflowx"] +omit = ["src/pyflowx/examples/*", "tests/*"] +source = ["pyflowx"] [tool.coverage.report] exclude_lines = [ @@ -78,12 +102,12 @@ show_missing = true [tool.pytest.ini_options] asyncio_default_fixture_loop_scope = "function" -markers = ["slow: marks tests as slow (deselect with '-m \"not slow\"')"] +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] # 使用双引号 @@ -95,39 +119,36 @@ 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) - "PLR0913", # too many arguments - "PLR2004", # magic value comparison - "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) + "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) - "PTH119", # os.path.basename (intentional for sys.argv) + "PLR2004", # magic value comparison + "PTH119", # os.path.basename (intentional for sys.argv) + "PTH123", # pathlib open() replacement + "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) + "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.pyrefly] -project-includes = [ - "**/*.py*", - "**/*.ipynb", -] -preset = "basic" -python-version = "3.8" +preset = "basic" +project-includes = ["**/*.ipynb", "**/*.py*"] +python-version = "3.8" diff --git a/src/pyflowx/cli/__init__.py b/src/pyflowx/cli/__init__.py index e69de29..e74def8 100644 --- a/src/pyflowx/cli/__init__.py +++ b/src/pyflowx/cli/__init__.py @@ -0,0 +1,73 @@ +"""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 +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 + +# 系统工具 +from pyflowx.cli.taskkill import main as taskkill_main +from pyflowx.cli.which import main as which_main + +__all__ = [ + # 自动格式化工具 + "autofmt_main", + "bumpversion_main", + "clearscreen_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", +] diff --git a/src/pyflowx/cli/autofmt.py b/src/pyflowx/cli/autofmt.py new file mode 100644 index 0000000..0d22941 --- /dev/null +++ b/src/pyflowx/cli/autofmt.py @@ -0,0 +1,273 @@ +"""自动格式化工具模块. + +提供 Python 代码自动格式化的常用功能封装, +支持 docstring 自动生成、pyproject.toml 配置同步等功能. +""" + +from __future__ import annotations + +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}") + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +# ruff format +ruff_format: px.TaskSpec = px.TaskSpec("ruff_format", cmd=["ruff", "format", "."]) + +# ruff check +ruff_check: px.TaskSpec = px.TaskSpec("ruff_check", cmd=["ruff", "check", "--fix", "--unsafe-fixes", "."]) + +# 自动添加 docstring +auto_docstring: px.TaskSpec = px.TaskSpec("auto_docstring", fn=lambda: auto_add_docstrings(Path())) + +# 同步 pyproject.toml 配置 +sync_config: px.TaskSpec = px.TaskSpec("sync_config", fn=lambda: sync_pyproject_config(Path())) + +# 格式化所有文件 +format_all_files: px.TaskSpec = px.TaskSpec("format_all", fn=lambda: format_all(Path())) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """自动格式化工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="AutoFmt - 自动格式化工具", + graphs={ + # ruff format + "fmt": px.Graph.from_specs([ruff_format]), + # ruff check + "lint": px.Graph.from_specs([ruff_check]), + # 自动添加 docstring + "doc": px.Graph.from_specs([auto_docstring]), + # 同步 pyproject.toml 配置 + "sync": px.Graph.from_specs([sync_config]), + # 格式化所有文件 + "all": px.Graph.from_specs([ruff_format, ruff_check]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/bumpversion.py b/src/pyflowx/cli/bumpversion.py new file mode 100644 index 0000000..2b37c21 --- /dev/null +++ b/src/pyflowx/cli/bumpversion.py @@ -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() diff --git a/src/pyflowx/cli/clearscreen.py b/src/pyflowx/cli/clearscreen.py new file mode 100644 index 0000000..9f13846 --- /dev/null +++ b/src/pyflowx/cli/clearscreen.py @@ -0,0 +1,68 @@ +"""清屏工具. + +跨平台清屏工具, 支持终端和控制台清屏. +""" + +from __future__ import annotations + +import os +import subprocess + +import pyflowx as px +from pyflowx.conditions import Constants + +# ============================================================================ +# 辅助函数 +# ============================================================================ + + +def clear_screen() -> None: + """清屏.""" + if Constants.IS_WINDOWS: + os.system("cls") + else: + os.system("clear") + + +def clear_screen_python() -> None: + """Python 方式清屏 (跨平台).""" + print("\033[2J\033[H", end="") + + +def clear_screen_cmd() -> None: + """使用系统命令清屏.""" + if Constants.IS_WINDOWS: + subprocess.run(["cmd", "/c", "cls"], check=False) + else: + subprocess.run(["clear"], check=False) + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +clearscreen: px.TaskSpec = px.TaskSpec("clearscreen", fn=clear_screen) +clearscreen_py: px.TaskSpec = px.TaskSpec("clearscreen_py", fn=clear_screen_python) +clearscreen_cmd: px.TaskSpec = px.TaskSpec("clearscreen_cmd", fn=clear_screen_cmd) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """清屏工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="ClearScreen - 清屏工具", + graphs={ + # 清屏 (os.system) + "c": px.Graph.from_specs([clearscreen]), + # 清屏 (Python) + "p": px.Graph.from_specs([clearscreen_py]), + # 清屏 (cmd) + "cmd": px.Graph.from_specs([clearscreen_cmd]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/envpy.py b/src/pyflowx/cli/envpy.py new file mode 100644 index 0000000..48e2620 --- /dev/null +++ b/src/pyflowx/cli/envpy.py @@ -0,0 +1,118 @@ +"""Python 环境配置工具. + +用于设置 pip 镜像源, 支持清华和阿里云等国内镜像源, +同时配置 UV 和 Conda 的镜像源. +""" + +from __future__ import annotations + +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})") + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +envpy_tsinghua: px.TaskSpec = px.TaskSpec("envpy_tsinghua", fn=lambda: set_pip_mirror("tsinghua")) +envpy_aliyun: px.TaskSpec = px.TaskSpec("envpy_aliyun", fn=lambda: set_pip_mirror("aliyun")) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """Python 环境配置工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="EnvPy - Python 环境配置工具", + graphs={ + # 设置清华镜像源 + "t": px.Graph.from_specs([envpy_tsinghua]), + # 设置阿里云镜像源 + "a": px.Graph.from_specs([envpy_aliyun]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/envqt.py b/src/pyflowx/cli/envqt.py new file mode 100644 index 0000000..2b82e99 --- /dev/null +++ b/src/pyflowx/cli/envqt.py @@ -0,0 +1,86 @@ +"""PyQt 环境配置工具. + +用于设置 PyQt 相关环境变量, 安装依赖环境. +""" + +from __future__ import annotations + +import pyflowx as px +from pyflowx.conditions import Constants + +# ============================================================================ +# Qt 依赖列表 +# ============================================================================ + +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", +] + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + + +# 条件: 仅在 Unix 系统上执行 +def is_linux() -> bool: + """判断是否为 Linux 系统.""" + return Constants.IS_LINUX and not Constants.IS_MACOS + + +envqt_install: px.TaskSpec = px.TaskSpec( + "envqt_install", + cmd=["sudo", "apt", "install", "-y", *QT_LIBS], + conditions=(is_linux,), +) + +envqt_fonts: px.TaskSpec = px.TaskSpec( + "envqt_fonts", + cmd=["sudo", "apt", "install", "-y", *CHINESE_FONTS], + conditions=(is_linux,), +) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """PyQt 环境配置工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="EnvQt - PyQt 环境配置工具", + graphs={ + # 安装 Qt 依赖 + "i": px.Graph.from_specs([envqt_install]), + # 安装中文字体 + "f": px.Graph.from_specs([envqt_fonts]), + # 安装全部 + "a": px.Graph.from_specs([envqt_install, envqt_fonts]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/envrs.py b/src/pyflowx/cli/envrs.py new file mode 100644 index 0000000..4cb6348 --- /dev/null +++ b/src/pyflowx/cli/envrs.py @@ -0,0 +1,135 @@ +"""Rust 环境配置工具. + +配置 Rustup 和 Cargo 的国内镜像源, +加速 Rust 工具链和依赖包的下载. +""" + +from __future__ import annotations + +import os +import subprocess +from pathlib import Path + +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/", + }, +} + +DEFAULT_PYTHON_VERSION: str = "nightly" +DEFAULT_MIRROR: str = "aliyun" + + +# ============================================================================ +# 辅助函数 +# ============================================================================ + + +def set_rust_mirror(mirror: str = "aliyun") -> None: + """设置 Rust 镜像源. + + Parameters + ---------- + mirror : str + 镜像源名称: aliyun, ustc, tsinghua + """ + mirror_dict = RUSTUP_MIRRORS.get(mirror, RUSTUP_MIRRORS["aliyun"]) + 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: str = "nightly") -> 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 + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +envrs_aliyun: px.TaskSpec = px.TaskSpec("envrs_aliyun", fn=lambda: set_rust_mirror("aliyun")) +envrs_ustc: px.TaskSpec = px.TaskSpec("envrs_ustc", fn=lambda: set_rust_mirror("ustc")) +envrs_tsinghua: px.TaskSpec = px.TaskSpec("envrs_tsinghua", fn=lambda: set_rust_mirror("tsinghua")) + +rust_install_stable: px.TaskSpec = px.TaskSpec("rust_install_stable", cmd=["rustup", "toolchain", "install", "stable"]) +rust_install_nightly: px.TaskSpec = px.TaskSpec( + "rust_install_nightly", cmd=["rustup", "toolchain", "install", "nightly"] +) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """Rust 环境配置工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="EnvRs - Rust 环境配置工具", + graphs={ + # 设置阿里云镜像源 + "a": px.Graph.from_specs([envrs_aliyun]), + # 设置中科大镜像源 + "u": px.Graph.from_specs([envrs_ustc]), + # 设置清华镜像源 + "t": px.Graph.from_specs([envrs_tsinghua]), + # 安装 stable 版本 + "s": px.Graph.from_specs([rust_install_stable]), + # 安装 nightly 版本 + "n": px.Graph.from_specs([rust_install_nightly]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/filedate.py b/src/pyflowx/cli/filedate.py new file mode 100644 index 0000000..800eaf5 --- /dev/null +++ b/src/pyflowx/cli/filedate.py @@ -0,0 +1,116 @@ +"""文件日期处理工具. + +自动检测文件名的日期前缀, +并根据文件的实际创建或修改时间重命名文件. +""" + +from __future__ import annotations + +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) + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +filedate_clear: px.TaskSpec = px.TaskSpec("filedate_clear", fn=lambda: process_files_date([], clear=True)) +filedate_add: px.TaskSpec = px.TaskSpec("filedate_add", fn=lambda: process_files_date([], clear=False)) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """文件日期处理工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="FileDate - 文件日期处理工具", + graphs={ + # 清除日期前缀 + "c": px.Graph.from_specs([filedate_clear]), + # 添加日期前缀 + "a": px.Graph.from_specs([filedate_add]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/filelevel.py b/src/pyflowx/cli/filelevel.py new file mode 100644 index 0000000..33edd4f --- /dev/null +++ b/src/pyflowx/cli/filelevel.py @@ -0,0 +1,141 @@ +"""文件等级重命名工具. + +根据文件等级配置自动重命名文件, +支持多种等级标识和括号格式. +""" + +from __future__ import annotations + +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) + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +filelevel_clear: px.TaskSpec = px.TaskSpec("filelevel_clear", fn=lambda: process_files_level([], level=0)) +filelevel_pub: px.TaskSpec = px.TaskSpec("filelevel_pub", fn=lambda: process_files_level([], level=1)) +filelevel_int: px.TaskSpec = px.TaskSpec("filelevel_int", fn=lambda: process_files_level([], level=2)) +filelevel_con: px.TaskSpec = px.TaskSpec("filelevel_con", fn=lambda: process_files_level([], level=3)) +filelevel_cla: px.TaskSpec = px.TaskSpec("filelevel_cla", fn=lambda: process_files_level([], level=4)) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """文件等级重命名工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="FileLevel - 文件等级重命名工具", + graphs={ + # 清除等级标记 + "c": px.Graph.from_specs([filelevel_clear]), + # 设置公开等级 (PUB) + "pub": px.Graph.from_specs([filelevel_pub]), + # 设置内部等级 (INT) + "int": px.Graph.from_specs([filelevel_int]), + # 设置机密等级 (CON) + "con": px.Graph.from_specs([filelevel_con]), + # 设置绝密等级 (CLA) + "cla": px.Graph.from_specs([filelevel_cla]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/folderback.py b/src/pyflowx/cli/folderback.py new file mode 100644 index 0000000..d2ff74c --- /dev/null +++ b/src/pyflowx/cli/folderback.py @@ -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() diff --git a/src/pyflowx/cli/folderzip.py b/src/pyflowx/cli/folderzip.py new file mode 100644 index 0000000..7d3e200 --- /dev/null +++ b/src/pyflowx/cli/folderzip.py @@ -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() diff --git a/src/pyflowx/cli/lscalc.py b/src/pyflowx/cli/lscalc.py new file mode 100644 index 0000000..00b33b9 --- /dev/null +++ b/src/pyflowx/cli/lscalc.py @@ -0,0 +1,167 @@ +"""LS-DYNA 计算工具. + +用于管理 LS-DYNA 仿真计算任务, +支持启动、监控和管理计算进程. +""" + +from __future__ import annotations + +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}") + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +lscalc_default: px.TaskSpec = px.TaskSpec( + "lscalc_default", + fn=lambda: run_ls_dyna(DEFAULT_INPUT_FILE, DEFAULT_NCPU), +) + +lscalc_mpi: px.TaskSpec = px.TaskSpec( + "lscalc_mpi", + fn=lambda: run_ls_dyna_mpi(DEFAULT_INPUT_FILE, DEFAULT_NCPU), +) + +lscalc_status: px.TaskSpec = px.TaskSpec("lscalc_status", fn=check_ls_dyna_status) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """LS-DYNA 计算工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="LSCalc - LS-DYNA 计算工具", + graphs={ + # 运行 LS-DYNA 计算 + "r": px.Graph.from_specs([lscalc_default]), + # 运行 LS-DYNA MPI 计算 + "mpi": px.Graph.from_specs([lscalc_mpi]), + # 检查进程状态 + "s": px.Graph.from_specs([lscalc_status]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/packtool.py b/src/pyflowx/cli/packtool.py new file mode 100644 index 0000000..490f4b1 --- /dev/null +++ b/src/pyflowx/cli/packtool.py @@ -0,0 +1,307 @@ +"""Python 打包工具模块. + +提供 Python 项目打包的常用功能封装, +支持源码打包、依赖打包、嵌入式 Python 安装等功能. +""" + +from __future__ import annotations + +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}") + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +# 源码打包 +pack_source_default: px.TaskSpec = px.TaskSpec("pack_source", fn=lambda: pack_source(Path(), Path(DEFAULT_BUILD_DIR))) + +# 依赖打包 +pack_deps_default: px.TaskSpec = px.TaskSpec("pack_deps", fn=lambda: pack_dependencies(Path(DEFAULT_LIB_DIR), [])) + +# Wheel 打包 +pack_wheel_default: px.TaskSpec = px.TaskSpec("pack_wheel", fn=lambda: pack_wheel(Path(), Path(DEFAULT_DIST_DIR))) + +# 嵌入式 Python 安装 +install_embed_default: px.TaskSpec = px.TaskSpec( + "install_embed", fn=lambda: install_embed_python("3.10", Path("python")) +) + +# ZIP 打包 +create_zip_default: px.TaskSpec = px.TaskSpec("create_zip", fn=lambda: create_zip_package(Path(), Path("package.zip"))) + +# 清理构建目录 +clean_build: px.TaskSpec = px.TaskSpec("clean_build", fn=lambda: clean_build_dir(Path(DEFAULT_BUILD_DIR))) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """Python 打包工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="PackTool - Python 打包工具", + graphs={ + # 源码打包 + "src": px.Graph.from_specs([pack_source_default]), + # 依赖打包 + "deps": px.Graph.from_specs([pack_deps_default]), + # Wheel 打包 + "wheel": px.Graph.from_specs([pack_wheel_default]), + # 嵌入式 Python 安装 + "embed": px.Graph.from_specs([install_embed_default]), + # ZIP 打包 + "zip": px.Graph.from_specs([create_zip_default]), + # 清理构建目录 + "clean": px.Graph.from_specs([clean_build]), + # 完整打包流程 + "all": px.Graph.from_specs( + [ + pack_source_default, + pack_deps_default, + pack_wheel_default, + ] + ), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/pdftool.py b/src/pyflowx/cli/pdftool.py new file mode 100644 index 0000000..4d83ded --- /dev/null +++ b/src/pyflowx/cli/pdftool.py @@ -0,0 +1,458 @@ +"""PDF 工具模块. + +提供 PDF 文件操作的常用功能封装, +支持合并、拆分、压缩、加密、水印、OCR等功能. +""" + +from __future__ import annotations + +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}") + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +# PDF 合并 +pdf_merge_default: px.TaskSpec = px.TaskSpec("pdf_merge", fn=lambda: pdf_merge([], Path("merged.pdf"))) + +# PDF 拆分 +pdf_split_default: px.TaskSpec = px.TaskSpec("pdf_split", fn=lambda: pdf_split(Path("input.pdf"), Path("split"))) + +# PDF 压缩 +pdf_compress_default: px.TaskSpec = px.TaskSpec( + "pdf_compress", fn=lambda: pdf_compress(Path("input.pdf"), Path("compressed.pdf")) +) + +# PDF 加密 +pdf_encrypt_default: px.TaskSpec = px.TaskSpec( + "pdf_encrypt", fn=lambda: pdf_encrypt(Path("input.pdf"), Path("encrypted.pdf"), "password") +) + +# PDF 解密 +pdf_decrypt_default: px.TaskSpec = px.TaskSpec( + "pdf_decrypt", fn=lambda: pdf_decrypt(Path("input.pdf"), Path("decrypted.pdf"), "password") +) + +# PDF 提取文本 +pdf_extract_text_default: px.TaskSpec = px.TaskSpec( + "pdf_extract_text", fn=lambda: pdf_extract_text(Path("input.pdf"), Path("output.txt")) +) + +# PDF 提取图片 +pdf_extract_images_default: px.TaskSpec = px.TaskSpec( + "pdf_extract_images", fn=lambda: pdf_extract_images(Path("input.pdf"), Path("images")) +) + +# PDF 添加水印 +pdf_watermark_default: px.TaskSpec = px.TaskSpec( + "pdf_watermark", fn=lambda: pdf_add_watermark(Path("input.pdf"), Path("watermarked.pdf")) +) + +# PDF 旋转 +pdf_rotate_default: px.TaskSpec = px.TaskSpec( + "pdf_rotate", fn=lambda: pdf_rotate(Path("input.pdf"), Path("rotated.pdf"), 90) +) + +# PDF 裁剪 +pdf_crop_default: px.TaskSpec = px.TaskSpec( + "pdf_crop", fn=lambda: pdf_crop(Path("input.pdf"), Path("cropped.pdf"), (10, 10, 10, 10)) +) + +# PDF 信息 +pdf_info_default: px.TaskSpec = px.TaskSpec("pdf_info", fn=lambda: pdf_info(Path("input.pdf"))) + +# PDF OCR +pdf_ocr_default: px.TaskSpec = px.TaskSpec("pdf_ocr", fn=lambda: pdf_ocr(Path("input.pdf"), Path("ocr.pdf"))) + +# PDF 重排 +pdf_reorder_default: px.TaskSpec = px.TaskSpec( + "pdf_reorder", fn=lambda: pdf_reorder(Path("input.pdf"), Path("reordered.pdf"), []) +) + +# PDF 转图片 +pdf_to_images_default: px.TaskSpec = px.TaskSpec( + "pdf_to_images", fn=lambda: pdf_to_images(Path("input.pdf"), Path("images")) +) + +# PDF 修复 +pdf_repair_default: px.TaskSpec = px.TaskSpec( + "pdf_repair", fn=lambda: pdf_repair(Path("input.pdf"), Path("repaired.pdf")) +) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """PDF 工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="PDFTool - PDF 文件工具集", + graphs={ + # 合并 PDF + "m": px.Graph.from_specs([pdf_merge_default]), + # 拆分 PDF + "s": px.Graph.from_specs([pdf_split_default]), + # 压缩 PDF + "c": px.Graph.from_specs([pdf_compress_default]), + # 加密 PDF + "e": px.Graph.from_specs([pdf_encrypt_default]), + # 解密 PDF + "d": px.Graph.from_specs([pdf_decrypt_default]), + # 提取文本 + "xt": px.Graph.from_specs([pdf_extract_text_default]), + # 提取图片 + "xi": px.Graph.from_specs([pdf_extract_images_default]), + # 添加水印 + "w": px.Graph.from_specs([pdf_watermark_default]), + # 旋转 PDF + "r": px.Graph.from_specs([pdf_rotate_default]), + # 裁剪 PDF + "crop": px.Graph.from_specs([pdf_crop_default]), + # 显示信息 + "i": px.Graph.from_specs([pdf_info_default]), + # OCR 识别 + "ocr": px.Graph.from_specs([pdf_ocr_default]), + # 重排页面 + "order": px.Graph.from_specs([pdf_reorder_default]), + # 转换图片 + "img": px.Graph.from_specs([pdf_to_images_default]), + # 修复 PDF + "repair": px.Graph.from_specs([pdf_repair_default]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/piptool.py b/src/pyflowx/cli/piptool.py new file mode 100644 index 0000000..f183aaf --- /dev/null +++ b/src/pyflowx/cli/piptool.py @@ -0,0 +1,164 @@ +"""pip 包管理工具模块. + +提供 pip 包管理操作的封装, +支持安装、卸载、下载等功能. +""" + +from __future__ import annotations + +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) + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +pip_install: px.TaskSpec = px.TaskSpec("pip_install", cmd=["pip", "install", "."]) +pip_upgrade: px.TaskSpec = px.TaskSpec("pip_upgrade", cmd=["python", "-m", "pip", "install", "--upgrade", "pip"]) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """pip 工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="PipTool - pip 包管理工具", + graphs={ + # 安装包 + "i": px.Graph.from_specs([pip_install]), + # 升级 pip + "up": px.Graph.from_specs([pip_upgrade]), + # 卸载包 (需要参数) + "u": px.Graph.from_specs( + [ + px.TaskSpec("pip_uninstall", fn=lambda: pip_uninstall([])), + ] + ), + # 下载包 + "d": px.Graph.from_specs( + [ + px.TaskSpec("pip_download", fn=lambda: pip_download([])), + ] + ), + # 冻结依赖 + "f": px.Graph.from_specs( + [ + px.TaskSpec("pip_freeze", fn=pip_freeze), + ] + ), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/screenshot.py b/src/pyflowx/cli/screenshot.py new file mode 100644 index 0000000..0f03fb4 --- /dev/null +++ b/src/pyflowx/cli/screenshot.py @@ -0,0 +1,152 @@ +"""截图工具. + +跨平台截图工具, 支持全屏截图和区域截图. +""" + +from __future__ import annotations + +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}") + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +screenshot_full: px.TaskSpec = px.TaskSpec("screenshot_full", fn=take_screenshot_full) +screenshot_area: px.TaskSpec = px.TaskSpec("screenshot_area", fn=take_screenshot_area) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """截图工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="Screenshot - 截图工具", + graphs={ + # 全屏截图 + "f": px.Graph.from_specs([screenshot_full]), + # 区域截图 + "a": px.Graph.from_specs([screenshot_area]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/sshcopyid.py b/src/pyflowx/cli/sshcopyid.py new file mode 100644 index 0000000..3114c42 --- /dev/null +++ b/src/pyflowx/cli/sshcopyid.py @@ -0,0 +1,118 @@ +"""SSH 密钥部署工具. + +类似 ssh-copy-id, 自动将 SSH 公钥部署到远程服务器, +支持密码认证和密钥认证两种方式. +""" + +from __future__ import annotations + +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) + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +# SSH 密钥部署需要参数,这里提供默认示例 +ssh_deploy_default: px.TaskSpec = px.TaskSpec( + "ssh_deploy_default", + fn=lambda: ssh_copy_id("localhost", "user", "password"), +) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """SSH 密钥部署工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="SSHCopyID - SSH 密钥部署工具", + graphs={ + # 部署 SSH 密钥 (需要参数) + "d": px.Graph.from_specs([ssh_deploy_default]), + }, + ) + runner.run_cli() diff --git a/src/pyflowx/cli/taskkill.py b/src/pyflowx/cli/taskkill.py new file mode 100644 index 0000000..9451ad9 --- /dev/null +++ b/src/pyflowx/cli/taskkill.py @@ -0,0 +1,78 @@ +"""进程终止工具. + +跨平台进程终止工具, 支持按名称终止进程. +用法: taskkill proc_name [proc_name ...] +""" + +from __future__ import annotations + +import argparse +import sys + +import pyflowx as px +from pyflowx.conditions import Constants + + +def main() -> None: + """进程终止工具主函数.""" + parser = argparse.ArgumentParser( + description="TaskKill - 进程终止工具", + usage="taskkill [process_name ...]", + ) + parser.add_argument( + "process_names", + type=str, + nargs="+", + help="进程名称 (如: chrome.exe python node)", + ) + parser.add_argument( + "--strategy", + choices=["sequential", "thread"], + default="sequential", + help="执行策略 (默认: sequential)", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="只打印执行计划, 不实际运行", + ) + parser.add_argument( + "--quiet", + action="store_true", + help="静默模式, 不显示执行过程", + ) + + args = parser.parse_args() + + # 动态创建 TaskSpec + specs: list[px.TaskSpec] = [] + for proc_name in args.process_names: + if Constants.IS_WINDOWS: + cmd = ["taskkill", "/f", "/im", f"{proc_name}*"] + else: + cmd = ["pkill", "-f", f"{proc_name}*"] + + spec = px.TaskSpec( + name=f"kill_{proc_name}", + cmd=cmd, + verbose=not args.quiet, + ) + specs.append(spec) + + # 创建 Graph 并执行 + graph = px.Graph.from_specs(specs) + + try: + report = px.run( + graph, + strategy=args.strategy, + dry_run=args.dry_run, + verbose=not args.quiet, + ) + sys.exit(0 if report.success else 1) + except KeyboardInterrupt: + print("\n操作已取消", file=sys.stderr) + sys.exit(130) + except px.PyFlowXError as e: + print(f"错误: {e}", file=sys.stderr) + sys.exit(1) diff --git a/src/pyflowx/cli/which.py b/src/pyflowx/cli/which.py new file mode 100644 index 0000000..4d2e3b7 --- /dev/null +++ b/src/pyflowx/cli/which.py @@ -0,0 +1,149 @@ +"""命令查找工具. + +跨平台查找可执行命令路径, 类似 Unix 的 which 命令. +""" + +from __future__ import annotations + +import shutil +import subprocess +from pathlib import Path + +import pyflowx as px +from pyflowx.conditions import Constants + +# ============================================================================ +# 辅助函数 +# ============================================================================ + + +def which_command(command: str) -> Path | None: + """查找命令路径. + + Parameters + ---------- + command : str + 命令名称 + + Returns + ------- + Path | None + 命令路径, 如果未找到则返回 None + """ + cmd_path = shutil.which(command) + return Path(cmd_path) if cmd_path else None + + +def which_all_commands(commands: list[str]) -> dict[str, Path | None]: + """查找多个命令路径. + + Parameters + ---------- + commands : list[str] + 命令名称列表 + + Returns + ------- + dict[str, Path | None] + 命令路径字典 + """ + results: dict[str, Path | None] = {} + for cmd in commands: + results[cmd] = which_command(cmd) + return results + + +def where_command_windows(command: str) -> list[Path]: + """Windows 下使用 where 命令查找所有匹配路径. + + Parameters + ---------- + command : str + 命令名称 + + Returns + ------- + list[Path] + 匹配的路径列表 + """ + if not Constants.IS_WINDOWS: + return [] + + try: + result = subprocess.run( + ["where", command], + capture_output=True, + text=True, + check=True, + ) + paths = [Path(line.strip()) for line in result.stdout.strip().split("\n") if line.strip()] + return paths + except subprocess.CalledProcessError: + return [] + + +def print_command_info(command: str) -> None: + """打印命令信息. + + Parameters + ---------- + command : str + 命令名称 + """ + cmd_path = which_command(command) + if cmd_path: + print(f"{command}: {cmd_path}") + if Constants.IS_WINDOWS: + all_paths = where_command_windows(command) + if len(all_paths) > 1: + print("所有匹配路径:") + for path in all_paths: + print(f" {path}") + else: + print(f"{command}: 未找到") + + +# ============================================================================ +# TaskSpec 定义 +# ============================================================================ + +which_python: px.TaskSpec = px.TaskSpec("which_python", fn=lambda: print_command_info("python")) +which_pip: px.TaskSpec = px.TaskSpec("which_pip", fn=lambda: print_command_info("pip")) +which_node: px.TaskSpec = px.TaskSpec("which_node", fn=lambda: print_command_info("node")) +which_npm: px.TaskSpec = px.TaskSpec("which_npm", fn=lambda: print_command_info("npm")) +which_git: px.TaskSpec = px.TaskSpec("which_git", fn=lambda: print_command_info("git")) +which_uv: px.TaskSpec = px.TaskSpec("which_uv", fn=lambda: print_command_info("uv")) +which_rustc: px.TaskSpec = px.TaskSpec("which_rustc", fn=lambda: print_command_info("rustc")) +which_cargo: px.TaskSpec = px.TaskSpec("which_cargo", fn=lambda: print_command_info("cargo")) + + +# ============================================================================ +# CLI Runner +# ============================================================================ + + +def main() -> None: + """命令查找工具主函数.""" + runner = px.CliRunner( + strategy="thread", + description="Which - 命令查找工具", + graphs={ + # 查找 python + "py": px.Graph.from_specs([which_python]), + # 查找 pip + "pip": px.Graph.from_specs([which_pip]), + # 查找 node + "node": px.Graph.from_specs([which_node]), + # 查找 npm + "npm": px.Graph.from_specs([which_npm]), + # 查找 git + "git": px.Graph.from_specs([which_git]), + # 查找 uv + "uv": px.Graph.from_specs([which_uv]), + # 查找 rustc + "rustc": px.Graph.from_specs([which_rustc]), + # 查找 cargo + "cargo": px.Graph.from_specs([which_cargo]), + }, + ) + runner.run_cli() diff --git a/uv.lock b/uv.lock index 0d6a53b..697b45c 100644 --- a/uv.lock +++ b/uv.lock @@ -1748,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" @@ -1911,10 +2220,21 @@ 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] @@ -1922,8 +2242,12 @@ requires-dist = [ { 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 = "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" }, @@ -1934,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" @@ -1966,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" @@ -2044,6 +2458,21 @@ wheels = [ { 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"