# PyFlowX > 轻量、类型安全的 DAG 任务调度器。 [![CI](https://github.com/gookeryoung/pyflowx/actions/workflows/ci.yml/badge.svg)](https://github.com/gookeryoung/pyflowx/actions/workflows/ci.yml) [![PyPI](https://img.shields.io/pypi/v/pyflowx.svg)](https://pypi.org/project/pyflowx/) [![Python](https://img.shields.io/pypi/pyversions/pyflowx.svg)](https://pypi.org/project/pyflowx/) [![Documentation Status](https://readthedocs.org/projects/pyflowx/badge/?version=latest)](https://pyflowx.readthedocs.io/zh/latest/) [![Coverage](https://img.shields.io/badge/coverage-97%25-brightgreen.svg)](https://github.com/gookeryoung/pyflowx) [![License](https://img.shields.io/pypi/l/pyflowx.svg)](https://github.com/gookeryoung/pyflowx/blob/main/LICENSE) PyFlowX 把"任务依赖"这件事做到极致简单:**参数名就是依赖声明**。无需装饰器、 无需样板包装器,写一个普通函数,框架按参数名自动注入上游结果。 ## 特性 - **零样板** —— 参数名即依赖,框架自动注入上游结果 - **四种执行策略** —— `sequential`(调试)/ `thread`(I/O 密集同步)/ `async`(I/O 密集异步)/ `dependency`(依赖驱动,最大化并行) - **类型安全** —— `TaskSpec[T]` 把返回类型一路传到 `RunReport`,mypy strict 通过 - **DAG 校验** —— 构建时即时校验重名、缺失依赖、环 - **自动分层** —— Kahn 算法分组,同层任务可并行 - **重试与超时** —— 每个任务独立配置 `RetryPolicy`(max_attempts/delay/backoff/jitter/retry_on)与 `timeout` - **软依赖** —— `soft_depends_on` 仅用于上下文注入,不参与拓扑分层 - **并发限制** —— `concurrency_key` + `concurrency_limits` 按组限流 - **任务钩子** —— `TaskHooks`(pre_run/post_run/on_failure)生命周期回调 - **断点续跑** —— `MemoryBackend` / `JSONBackend`,成功结果可缓存复用;`batch()` 批量落盘;`invalidate(key)` 单键失效 - **缓存键** —— `cache_key` 函数基于输入计算稳定键,使不同输入产生独立缓存 - **缓存驱逐** —— `MemoryBackend(maxsize=N)` LRU 驱逐最旧条目;TTL 过期自动忽略 - **命令任务** —— `cmd` 参数直接执行外部命令,支持列表/shell/可调用对象 - **条件执行** —— `conditions` 参数按平台、环境变量、应用安装等条件跳过任务 - **图组合** —— `compose` / `GraphComposer` 编程式展开多图字符串引用 - **任务模板** —— `task_template` 工厂批量生成相似 TaskSpec - **图级默认值** —— `GraphDefaults` 统一配置 retry/timeout/concurrency 等 - **CLI 运行器** —— `CliRunner` 把多个图映射为命令行子命令,替代 Makefile - **可观测** —— `on_event` 回调(RUNNING/SUCCESS/FAILED/SKIPPED)、`dry_run` 预览、`verbose` 生命周期日志、Mermaid 可视化 - **进度监控** —— `run(progress=True)` 开箱即用的 rich 进度条;`ProgressCallback` 协议供程序化集成 - **任务通知** —— `Notifier` 协议支持飞书/钉钉/企业微信/Slack/Discord/Telegram webhook 与 Python 回调,全生命周期事件可配置 - **流式获取** —— `run_iter()` 生成器逐个 yield 任务结果,适用于大量任务逐个处理 - **任务取消** —— `CancelToken` 线程安全取消令牌,支持外部取消与 `KeyboardInterrupt` 优雅停止 - **子图执行** —— `run(only=[...], tags=[...])` 按名称或标签运行图的子集,自动包含传递依赖 - **结果序列化** —— `RunReport.to_json()` / `to_dict()` / `to_csv()` / `to_html()` 多格式导出运行结果,`from_json()` 支持反序列化重建;HTML 报告自包含内联 CSS,适合浏览器直接打开或邮件分享 - **CLI 可视化** —— `pf graph ` 终端 DAG 渲染(ASCII 分层树按标签着色 / Mermaid / 依赖表),`pf yamlrun --progress` rich 进度条,`pf info [tool]` 工具详情,`pf completion bash|zsh|fish` shell 补全 - **失败诊断** —— `RunReport.diagnose()` 自动分析根因、依赖链回溯、相似失败聚类与可操作建议;CLI 失败时自动打印诊断摘要 - **可观测性** —— `run_id` 贯穿日志/序列化/诊断;结构化日志 `extra` 含 `task_name`/`status`/`attempts`/`error_type`;`profile(graph)` 离线性能分析(关键路径/并行度/瓶颈) - **状态后端** —— `MemoryBackend` / `JSONBackend` / `SQLiteBackend`(WAL 模式,适合大规模任务) - **YAML 任务编排** —— GitHub Actions 风格 `jobs`/`needs`/`strategy.matrix`/`if` 条件,`pf yamlrun pipeline.yaml` 一键执行 - **最小依赖** —— `rich` + `typer` + `typing-extensions`(3.13 以下)+ `pyyaml`;安装 `orjson` 可加速 JSON 序列化(`pip install pyflowx[fast]`) - **97% 测试覆盖** —— 分支覆盖率 >= 95% ## 安装 ```bash pip install pyflowx ``` 或使用 [uv](https://docs.astral.sh/uv/): ```bash uv add pyflowx ``` ## 快速上手 ```python import pyflowx as px def extract() -> list[int]: return [1, 2, 3] # 参数名 extract 自动匹配上游任务名 → 自动注入 def double(extract: list[int]) -> list[int]: return [x * 2 for x in extract] graph = px.Graph.from_specs([ px.TaskSpec("extract", extract), px.TaskSpec("double", double, ("extract",)), ]) report = px.run(graph, strategy="sequential") print(report["double"]) # [2, 4, 6] ``` ## 核心概念 ### TaskSpec —— 任务描述 `TaskSpec` 是不可变的任务描述符(`Generic[T]`,返回类型一路传到 `RunReport`),是唯一需要配置的东西: ```python px.TaskSpec( name="fetch_user", # 唯一标识 fn=fetch_user, # 同步或异步函数 cmd=["curl", "..."], # 或: 执行命令(覆盖 fn) depends_on=("auth",), # 硬依赖(参与拓扑分层) soft_depends_on=("cache",), # 软依赖(仅注入,不参与分层) args=(uid,), # 静态位置参数(追加在注入参数后) kwargs={"timeout": 30}, # 静态关键字参数 retry=px.RetryPolicy(max_attempts=3, delay=1.0, backoff=2.0), # 重试策略 timeout=30.0, # 超时秒数(None = 不限制) tags=("api", "user"), # 自由标签,用于子图过滤 conditions=(is_prod,), # 条件函数列表(全部为 True 才执行) priority=10, # 同层内优先级(高优先执行,默认 0) concurrency_key="db", # 并发分组键(配合 concurrency_limits 限流) cache_key=lambda ctx: str(ctx.get("uid")), # 缓存键函数(不同输入独立缓存) hooks=px.TaskHooks(pre_run=..., post_run=..., on_failure=...), # 生命周期钩子 cwd=Path("/tmp"), # 命令工作目录(仅 cmd 模式) env={"DEBUG": "1"}, # 环境变量覆盖(fn 与 cmd 模式均生效) verbose=True, # 打印命令输出(仅 cmd 模式) skip_if_missing=True, # 命令不存在时自动跳过(仅 list[str] cmd) allow_upstream_skip=False, # 上游 SKIPPED/FAILED 时是否仍执行 continue_on_error=False, # 本任务失败是否不中断整体 ) ``` 支持两种任务形态: - **函数任务**(`fn`):普通 Python 函数,参数名驱动自动注入 - **命令任务**(`cmd`):执行外部命令,支持 `list[str]`、`str`(shell)、`Callable` 三种形态 `skip_if_missing=True` 时,`list[str]` 类型的 `cmd` 会通过 `shutil.which` 检查命令是否存在,不存在则跳过任务(标记为 `SKIPPED`)而非失败。适用于构建工具场景,避免因未安装某些工具而导致整个图执行失败。 ### Graph —— DAG 构建 ```python # 图级默认值:TaskSpec 字段为 None 时回退 defaults = px.GraphDefaults(retry=px.RetryPolicy(max_attempts=2), timeout=60.0) graph = px.Graph.from_specs([...], defaults=defaults) # 整批校验(推荐) # 或增量构建 graph = px.Graph(defaults=defaults) graph.add(px.TaskSpec("a", fn_a)) graph.add(px.TaskSpec("b", fn_b, ("a",))) graph.validate() # 显式校验(环检测) graph.layers() # 拓扑分层(run() 入口已统一校验,直接调用需自行先 validate) graph.to_mermaid() # Mermaid 可视化 graph.describe() # 人类可读摘要 graph.subgraph(("api",)) # 按标签切片 graph.subgraph_by_names(("a", "b")) # 按名称切片 graph.map("fetch", [1, 2, 3], lambda i: TaskSpec(f"fetch_{i}", ...)) # 批量 fan-out ``` ### 图组合 —— compose `compose` / `GraphComposer` 把带字符串引用的多个图展开为纯 `Graph`: ```python graphs = { "build": px.Graph.from_specs([px.TaskSpec("b", cmd=["echo", "b"])]), "all": px.Graph.from_specs(["build", px.TaskSpec("t", cmd=["echo", "t"])]), } resolved = px.compose(graphs) # "all" 图中的 "build" 引用被展开 ``` 引用格式:`"command_name"`(整个图)或 `"command_name.task_name"`(特定任务)。 `CliRunner` 内部自动调用 `compose`。 ### YAML 任务编排 GitHub Actions 风格的 YAML 文件直接加载为 `Graph`,支持 `jobs`/`needs`/`strategy.matrix`/`if`/`continue-on-error`/`env`/`defaults`,以及 `matrix.include`/`exclude` 过滤、条件依赖(`needs: [{job: ..., if: ...}]`)、`outputs` 声明: ```yaml # pipeline.yaml strategy: thread defaults: retry: {max_attempts: 3} env: {CI: "true"} jobs: setup: cmd: ["echo", "setup"] runs-on: linux build: needs: [setup] cmd: ["echo", "build-${{ matrix.version }}"] strategy: matrix: version: ["3.10", "3.11", "3.12"] os: ["linux", "macos"] exclude: - version: "3.10" os: "macos" include: - version: "3.13" os: "linux" if: "env.CI" outputs: version: "${{ matrix.version }}" deploy: needs: - job: build if: "env.BRANCH == 'main'" cmd: ["echo", "deploy"] test: needs: [build] cmd: ["echo", "test"] continue-on-error: true ``` ```python import pyflowx as px graph = px.Graph.from_yaml("pipeline.yaml") # 或从字符串解析:graph = px.parse_yaml_string("...") report = px.run(graph, strategy="thread") ``` CLI 一键执行: ```bash pf yamlrun pipeline.yaml # 执行 pf yamlrun pipeline.yaml --dry-run # 仅预览,不执行 pf yamlrun pipeline.yaml --list # 列出所有任务 pf yamlrun pipeline.yaml --quiet # 静默模式 pf yamlrun pipeline.yaml --strategy sequential # 覆盖策略 ``` **字段映射**: | YAML 字段 | TaskSpec 字段 | 说明 | |-----------|---------------|------| | `cmd` / `run` | `cmd` | `cmd` 为列表,`run` 为 shell 字符串 | | `needs` | `depends_on` | 矩阵 job 依赖自动展开为所有变体 | | `if` | `conditions` | `success()`/`always()`/`env.VAR`/`env.VAR == 'x'`/`env.VAR != 'x'` | | `strategy.matrix` | 矩阵扇出 | 笛卡尔积,命名 `{job}_{key}-{value}` | | `strategy.matrix.include` | 矩阵追加 | 额外组合(可引入新键) | | `strategy.matrix.exclude` | 矩阵剔除 | 匹配所有 key-value 的组合被移除 | | `${{ matrix.key }}` | 占位符 | `cmd`/`run`/`cwd`/`env`/`outputs` 中替换 | | `outputs` | `outputs` | 任务声明的输出键值映射(元数据,不参与执行) | | `needs: [{job: ..., if: ...}]` | 条件依赖 | `if` 不满足时跳过该依赖 | | `continue-on-error` | `continue_on_error` | 字段名支持 hyphen/underscore | | `runs-on` | `tags` | 追加到 tags 元组 | | `defaults` | `GraphDefaults` | 图级默认值(retry/timeout/env/cwd 等) | ### 任务模板 —— task_template `task_template` 工厂批量生成相似 TaskSpec: ```python fetch = px.task_template( fn=fetch_url, retry=px.RetryPolicy(max_attempts=5), timeout=30.0, tags=("api",), ) graph = px.Graph.from_specs([ fetch("users", url="https://api.example.com/users"), fetch("posts", url="https://api.example.com/posts"), ]) ``` ### run —— 执行 ```python report = px.run( graph, strategy="async", # sequential | thread | async | dependency max_workers=8, # thread 策略的线程池大小 concurrency_limits={"db": 2}, # 按 concurrency_key 限流 dry_run=False, # True = 仅打印计划 verbose=False, # True = 打印任务生命周期日志 on_event=callback, # 状态转换回调(RUNNING/SUCCESS/FAILED/SKIPPED) state=px.JSONBackend("state.json"), # 断点续跑后端 continue_on_error=False, # True = 单任务失败不中断整体 ) ``` ### RunReport —— 结果 ```python report["task_name"] # 任务返回值 report.result_of("task_name") # 完整 TaskResult report.success # 整体是否成功 report.summary() # 统计字典 report.failed_tasks() # 失败任务名列表 report.describe() # 人类可读报告 ``` ## 上下文注入规则 按顺序求值: 1. **标注为 `Context`** 的参数 → 接收完整上游结果映射 2. **名称匹配依赖** 的参数 → 接收该依赖的结果(含软依赖,缺失时注入默认值) 3. **`**kwargs`** 参数 → 接收所有依赖结果(dict) 4. **`TaskSpec.args` / `kwargs`** → 为非依赖参数提供静态值 ```python from typing import Any, Dict def aggregate(ctx: px.Context) -> Dict[str, Any]: """ctx 包含所有 depends_on 任务的返回值。""" return dict(ctx) def merge(fetch_a: str, fetch_b: str) -> str: """fetch_a / fetch_b 自动注入。""" return fetch_a + fetch_b def fetch_user(uid: int) -> dict: # uid 来自 TaskSpec.args ... ``` ## 执行策略对比 | 策略 | 并发模型 | 适用场景 | 同步任务 | 异步任务 | |------|---------|---------|---------|---------| | `sequential` | 串行 | 调试、CPU 密集 | 直接调用 | 事件循环 | | `thread` | 线程池 | I/O 密集同步 | 线程池 | 不支持 | | `async` | 事件循环 | I/O 密集异步 | 卸载到线程池 | 事件循环 | | `dependency` | 依赖驱动 | 最大化并行度 | 卸载到线程池 | 事件循环 | 所有策略都遵循 `RetryPolicy`、`timeout`、上下文注入、状态后端、`concurrency_limits`, 并发出 `TaskEvent`(RUNNING/SUCCESS/FAILED/SKIPPED)。`dependency` 策略无层屏障: 任务在其所有硬依赖完成后立即启动。 ## 命令任务 `TaskSpec` 的 `cmd` 参数支持执行外部命令,无需包装 Python 函数: ```python graph = px.Graph.from_specs([ # 命令列表(推荐,参数无需转义) px.TaskSpec("list_files", cmd=["ls", "-la"]), # shell 字符串(支持管道、重定向) px.TaskSpec("check_git", cmd="git status | head"), # 带工作目录与超时 px.TaskSpec("build", cmd=["make", "all"], cwd=Path("/project"), timeout=300), # 命令不存在时自动跳过(而非失败) px.TaskSpec("optional_tool", cmd=["maturin", "build"], skip_if_missing=True), ]) ``` `verbose=True` 时打印执行的命令、工作目录、返回码与输出;`verbose=False` 时静默执行(失败信息仍包含 stderr)。 `skip_if_missing=True` 时,`list[str]` 类型的 `cmd` 会通过 `shutil.which` 检查命令是否存在,不存在则跳过任务(标记为 `SKIPPED`)而非失败。适用于构建工具场景,避免因未安装某些工具而导致整个图执行失败。对于 `str`(shell)和 `Callable` 类型的 `cmd`,此参数无效。 ## 条件执行 `conditions` 参数让任务按条件跳过(标记为 `SKIPPED`): ```python from pyflowx.conditions import IS_WINDOWS, BuiltinConditions graph = px.Graph.from_specs([ # 仅在 Windows 上运行 px.TaskSpec("win_only", cmd=["dir"], conditions=(IS_WINDOWS,)), # 仅在 git 已安装时运行 px.TaskSpec( "git_check", cmd=["git", "--version"], conditions=(BuiltinConditions.HAS_INSTALLED("git"),), ), # 组合条件 px.TaskSpec( "prod_deploy", fn=deploy, conditions=( BuiltinConditions.ENV_VAR_EQUALS("ENV", "prod"), BuiltinConditions.HAS_INSTALLED("docker"), ), ), ]) ``` 内置条件:`IS_WINDOWS` / `IS_LINUX` / `IS_MACOS` / `IS_POSIX` / `PYTHON_VERSION` / `HAS_INSTALLED` / `ENV_VAR_EXISTS` / `ENV_VAR_EQUALS` / `NOT` / `AND` / `OR`。 ## CLI 运行器 `CliRunner` 把多个 Graph 映射为命令行子命令,适合构建项目专属构建工具(替代 Makefile): ```python runner = px.CliRunner( strategy="sequential", description="My Build Tool", graphs={ "clean": clean_graph, "build": build_graph, "test": test_graph, }, ) runner.run_cli() # 解析 sys.argv 并执行 ``` 命令行用法: ```bash pf pymake clean # 执行 clean 图 pf pymake build --strategy thread # 覆盖执行策略 pf pymake test --dry-run # 仅打印执行计划 pf pymake --list # 列出所有命令 pf pymake --quiet # 静默模式 ``` `verbose=True`(默认)时打印任务生命周期(开始/成功/失败/跳过)与命令输出;`--quiet` 关闭。 ## 断点续跑 ```python from pyflowx import JSONBackend # 第一次运行:成功结果写入 state.json backend = JSONBackend("state.json", ttl=3600) # ttl 秒数,过期条目自动忽略 report = px.run(graph, strategy="sequential", state=backend) # 第二次运行:已缓存任务自动跳过(状态为 SKIPPED) report = px.run(graph, strategy="sequential", state=backend) ``` `run()` 内部以 `backend.batch()` 包裹整个执行:所有 `save` 延迟到运行结束时统一落盘一次 (`JSONBackend` 从 O(N²) 降为 O(N) 磁盘写入;`MemoryBackend` 为 no-op)。 **缓存键**:默认存储键为任务名。配置 `cache_key` 函数后,键为 `"name:cache_key_value"`, 使不同输入产生独立缓存条目: ```python px.TaskSpec( "fetch_user", fn=fetch_user, cache_key=lambda ctx: str(ctx.get("uid")), # 不同 uid 独立缓存 ) ``` **LRU 驱逐**:`MemoryBackend(maxsize=N)` 限制最大条目数,超限时按 LRU(最近最少使用) 策略驱逐最旧条目。`get`/`has` 命中会将条目移到最近使用位置: ```python from pyflowx import MemoryBackend backend = MemoryBackend(maxsize=100) # 最多 100 条,超出驱逐最旧 ``` **手动失效**:`invalidate(key)` 删除单个键的缓存,`clear()` 清除全部: ```python backend.invalidate("fetch_user") # 删除单个键,返回是否已删除 backend.clear() # 清除全部 ``` ## 错误处理 所有错误都是 `PyFlowXError` 的子类: | 错误 | 触发时机 | |------|---------| | `DuplicateTaskError` | 任务名重复注册 | | `MissingDependencyError` | 依赖了不存在的任务 | | `CycleError` | 依赖图存在环 | | `TaskFailedError` | 任务耗尽重试后仍失败 | | `TaskTimeoutError` | 任务超时 | | `InjectionError` | 上下文注入无法满足签名 | | `StorageError` | 状态后端持久化失败 | ```python try: report = px.run(graph, strategy="async") except px.TaskFailedError as exc: print(f"{exc.task} 失败: {exc.cause}(尝试 {exc.attempts} 次)") except px.PyFlowXError: # 捕获整个错误家族 raise ``` ## 与其他库对比 | 特性 | PyFlowX | Airflow | Prefect | Dask | |------|---------|---------|---------|------| | 零样板 | 参数名即依赖 | 装饰器 + XCom | 装饰器 | 装饰器 | | 运行时依赖 | rich + typer | 重型 | 中型 | 中型 | | 类型安全 | mypy strict | 弱 | 中 | 中 | | 异步原生 | 是 | 否 | 部分 | 否 | | 断点续跑 | 内置 | 需配置 | 需配置 | 需配置 | | 学习曲线 | 极低 | 高 | 中 | 中 | | 适用规模 | 单机 | 集群 | 单机/集群 | 集群 | PyFlowX 专注于**单机 DAG 调度**的极致简洁,适合 ETL、数据处理、CI 流水线等场景。 ## 高级特性 ### 并发限制 按 `concurrency_key` 分组限流,避免压垮下游资源: ```python graph = px.Graph.from_specs([ px.TaskSpec("q1", fn=query_db, concurrency_key="db"), px.TaskSpec("q2", fn=query_db, concurrency_key="db"), px.TaskSpec("q3", fn=query_db, concurrency_key="db"), ]) # 同一时刻最多 2 个 "db" 组任务运行 px.run(graph, strategy="async", concurrency_limits={"db": 2}) ``` ### 任务钩子 `TaskHooks` 在任务生命周期触发(异常仅记录,不影响任务状态): ```python hooks = px.TaskHooks( pre_run=lambda spec: print(f"start {spec.name}"), post_run=lambda spec, value: print(f"done {spec.name}"), on_failure=lambda spec, exc: alert(spec.name, exc), ) px.TaskSpec("task", fn=work, hooks=hooks) ``` ### 优先级 同层内按 `priority` 降序执行(稳定排序): ```python px.TaskSpec("low", fn=work, priority=0) px.TaskSpec("high", fn=work, priority=10) # 同层内先执行 ``` ### 任务取消 `CancelToken` 提供线程安全的取消信号,传入 `run(cancel_event=...)` 即可外部取消正在执行的图。 `KeyboardInterrupt`(Ctrl+C)也会触发相同的优雅取消流程: ```python token = px.CancelToken() # 在另一个线程中触发取消 import threading def cancel_later(): import time time.sleep(2) token.cancel("超时取消") threading.Thread(target=cancel_later, daemon=True).start() # 取消后:待运行任务标记 SKIPPED,运行中任务等待完成,report.success = False report = px.run(graph, strategy="dependency", cancel_event=token) for name, result in report.results.items(): print(f"{name}: {result.status.value}") ``` 也支持标准 `threading.Event` 作为取消信号: ```python event = threading.Event() px.run(graph, cancel_event=event) ``` ### 子图执行 `run(only=...)` / `run(tags=...)` 只运行指定任务及其传递依赖,适用于调试单个任务或增量执行: ```python # 只运行 "report" 任务及其所有上游依赖 report = px.run(graph, only=["report"]) # 按标签过滤:只运行带 "ingest" 标签的任务及其依赖 report = px.run(graph, tags=["ingest"]) # 两者可组合(取并集) report = px.run(graph, only=["report"], tags=["ingest"]) ``` CLI 也支持 `--only` / `--tags`: ```bash pf yamlrun pipeline.yaml --only report,deploy pf yamlrun pipeline.yaml --tags ingest,report ``` ### 结果序列化 `RunReport` 支持将运行结果导出为 JSON / dict / CSV,便于持久化、跨进程传输或对接外部工具: ```python report = px.run(graph, strategy="sequential") # JSON 字符串(含 success/summary/results) text = report.to_json(indent=2) # JSON 兼容字典 data = report.to_dict() # CSV 表格导出(不含 value 列以避免特殊字符干扰) csv_text = report.to_csv(include_value=False) ``` 任务返回值可能是任意 Python 对象(不一定 JSON 可序列化)。默认策略: 可序列化原样保留,不可序列化回退到 `repr(value)`;调用方可通过 `value_serializer` 自定义: ```python # 自定义值序列化(如把 Path 转为字符串) text = report.to_json(value_serializer=str) ``` `from_json()` 可从 JSON 字符串重建 `RunReport`,**仅供查询/分析**,不能重新 执行(`TaskSpec` 的 `fn`/`cmd` 等不可序列化字段无法恢复): ```python restored = px.RunReport.from_json(text) print(restored["double"]) # 任务返回值 print(restored.result_of("a").status) # TaskStatus ``` ### CLI 可视化 `pf graph ` 在终端可视化任务图 DAG,无需运行即可检查依赖关系: ```bash # 默认 ASCII 渲染:分层树(按标签着色) + 依赖关系表 pf graph pipeline.yaml # 输出 Mermaid 语法(可粘贴到 mermaid.live) pf graph pipeline.yaml --format mermaid # 自定义方向 pf graph pipeline.yaml --format mermaid --orientation LR # 仅列出任务名 pf graph pipeline.yaml --format list # 纯依赖关系表 pf graph pipeline.yaml --format deps # 关闭标签着色(统一单色) pf graph pipeline.yaml --color-by none ``` ASCII 模式默认 `--color-by tag`,按任务首标签选择 rich 颜色并在图下方 显示标签颜色图例,便于视觉分组。 `pf yamlrun` 新增 `--progress` 选项,启用 rich 进度条显示运行中任务、完成 数与耗时: ```bash pf yamlrun pipeline.yaml --progress ``` ### 任务列表过滤 `pf yamlrun --list` 支持 `--list-tag` / `--list-name` 过滤,便于大型 YAML 中快速定位任务: ```bash # 列出所有任务 pf yamlrun pipeline.yaml --list # 只列出含 ingest 标签的任务 pf yamlrun pipeline.yaml --list --list-tag ingest # 按任务名子串过滤(大小写不敏感) pf yamlrun pipeline.yaml --list --list-name extract # 组合过滤(交集) pf yamlrun pipeline.yaml --list --list-tag ingest --list-name extract ``` ### 工具详情与 Shell 补全 `pf info [tool]` 显示工具详情: ```bash # 列出所有工具的简表(含子命令数、描述) pf info # 显示指定工具的子命令详情 pf info clr pf info gittool ``` `pf completion ` 生成 shell 补全脚本,提升日常使用体验: ```bash # bash —— 添加到 ~/.bashrc pf completion bash >> ~/.bashrc # zsh —— 添加到 ~/.zshrc pf completion zsh >> ~/.zshrc # fish —— 保存到 ~/.config/fish/completions/pf.fish pf completion fish > ~/.config/fish/completions/pf.fish ``` 补全脚本覆盖所有内建命令(`yamlrun`/`graph`/`info`/`completion`)与 `@px.tool` 注册的工具名;二级补全交给具体子命令的 `--help` 自描述。 ### 失败诊断 运行失败时,`RunReport.diagnose()` 自动生成结构化诊断报告,包含: - **根因任务**:FAILED 且所有硬依赖非 FAILED 的任务(依赖链中最先出问题的环节) - **依赖链回溯**:从根因到每个失败任务的路径 - **相似失败聚类**:按 `(异常类型, 消息前缀)` 聚类,发现批量失败模式 - **可操作建议**:识别 FileNotFoundError/ImportError/TimeoutError 等常见异常并给出提示 ```python report = px.run(graph, strategy="sequential") if not report.success: diag = report.diagnose() if diag is not None: print(diag.describe()) # 根因任务 print(diag.root_causes) # 依赖链 for chain in diag.dependency_chains: print(f"{chain.failed_task}: {' -> '.join(chain.chain)}") ``` `pf yamlrun` 失败时自动打印诊断摘要到 stderr: ```bash pf yamlrun pipeline.yaml # 失败时自动输出诊断报告 ``` `report.describe()` 在失败时也会附诊断摘要。 ### 可观测性 每次 `run()` 自动生成 8 字符十六进制 `run_id`,作为本次运行的唯一追踪 ID, 贯穿日志、序列化结果与诊断报告: ```python report = px.run(graph) print(report.run_id) # 如 "a3f4b2c1" print(report.summary()["run_id"]) # summary 也含 run_id ``` `run_id` 通过结构化日志的 `extra` 字段注入所有执行器日志,便于跨系统关联。 日志格式遵循 `logging` 标准延迟格式化,`extra` 字段包括: - `run_id` —— 本次运行 ID - `task_name` —— 任务名(任务级日志) - `status` —— 任务状态(`running`/`success`/`failed`/`skipped`) - `attempts` —— 尝试次数(失败日志) - `error_type` —— 异常类型名(失败日志) ```python import logging logging.basicConfig(level=logging.INFO) # 日志输出示例: # INFO 运行开始: run_id=a3f4b2c1 strategy=dependency tasks=3 # INFO task 'a' skipped (cached) # WARNING task 'b' failed (attempt 1/3): RuntimeError('boom'); retrying # INFO 运行结束: run_id=a3f4b2c1 success=False tasks=3 ``` `to_json()` / `to_dict()` / `from_json()` 同步保留 `run_id`,反序列化时可还原: 旧版 JSON(无 `run_id` 字段)会自动生成新 ID,向前兼容。 `report.profile(graph)` 返回 `ProfileReport`,基于 `started_at`/`finished_at` 做离线性能分析(关键路径、并行度、Top 瓶颈),零运行时开销: ```python profile = report.profile(graph) print(profile.describe()) # 人类可读报告 print(profile.critical_path) # 关键路径任务序列 bottlenecks = profile.top_bottlenecks(5) # Top-5 瓶颈任务 ``` ## 开发 ```bash # 安装开发依赖 uv sync --extra dev # 运行测试(含覆盖率,阈值 95%) uv run pytest --cov=pyflowx --cov-fail-under=95 # 类型检查 uv run pyrefly check . # 代码风格 uv run ruff check . uv run ruff format --check . ``` ## 模块结构 ### 核心 | 模块 | 职责 | |------|------| | `task.py` | 纯数据结构:`TaskSpec`、`RetryPolicy`、`TaskHooks`、`TaskStatus` | | `graph.py` | DAG 构建、校验、分层、可视化 | | `compose.py` | 多图组合:`GraphComposer` / `compose` | | `context.py` | 上下文注入:参数名→依赖解析 | | `command.py` | 命令执行:`run_command`(list/shell/Callable) | | `cancellation.py` | 任务取消:`CancelToken` 线程安全取消令牌 | | `conditions.py` | 条件执行:内置条件与组合器 | | `executors.py` | 执行器与 `run` 入口:四种策略共享模块级辅助;verbose 统一应用到 spec | | `storage.py` | 状态后端:`MemoryBackend` / `JSONBackend`(batch flush) | | `yaml_loader.py` | YAML 任务编排:`load_yaml` / `parse_yaml_string`(GitHub Actions 风格) | | `runner.py` | CLI 运行器:`CliRunner` | | `report.py` | 运行结果:`RunReport` / `TaskResult` | | `tools.py` | `@px.tool` 装饰器:`ToolSpec` / `run_tool` / `list_tools` / `list_subcommands` | | `profiling.py` | 性能分析:`Profiler` 任务耗时统计 | | `errors.py` | 错误家族:`PyFlowXError` 子类 | | `ops/` | CLI 工具模块集合(每个子模块用 `@px.tool` 注册一个工具) | ### CLI 工具 | 模块 | 职责 | |------|------| | `cli/pf.py` | 统一入口:`pf [command]`,按需导入 `ops/.py` 触发 `@px.tool` 注册 | | `cli/legacy/profiler.py` | 性能分析 CLI(legacy) | | `cli/legacy/emlmanager.py` | 邮件管理 CLI(legacy) | ## 许可证 MIT