29 KiB
PyFlowX
轻量、类型安全的 DAG 任务调度器。
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 <yaml>终端 DAG 渲染(ASCII 分层树按标签着色 / Mermaid / 依赖表),pf yamlrun --progressrich 进度条,pf info [tool]工具详情,pf completion bash|zsh|fishshell 补全 - 失败诊断 ——
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%
安装
pip install pyflowx
或使用 uv:
uv add pyflowx
快速上手
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),是唯一需要配置的东西:
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 构建
# 图级默认值: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:
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 声明:
# 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
import pyflowx as px
graph = px.Graph.from_yaml("pipeline.yaml")
# 或从字符串解析:graph = px.parse_yaml_string("...")
report = px.run(graph, strategy="thread")
CLI 一键执行:
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:
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 —— 执行
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 —— 结果
report["task_name"] # 任务返回值
report.result_of("task_name") # 完整 TaskResult
report.success # 整体是否成功
report.summary() # 统计字典
report.failed_tasks() # 失败任务名列表
report.describe() # 人类可读报告
上下文注入规则
按顺序求值:
- 标注为
Context的参数 → 接收完整上游结果映射 - 名称匹配依赖 的参数 → 接收该依赖的结果(含软依赖,缺失时注入默认值)
**kwargs参数 → 接收所有依赖结果(dict)TaskSpec.args/kwargs→ 为非依赖参数提供静态值
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 函数:
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):
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):
runner = px.CliRunner(
strategy="sequential",
description="My Build Tool",
graphs={
"clean": clean_graph,
"build": build_graph,
"test": test_graph,
},
)
runner.run_cli() # 解析 sys.argv 并执行
命令行用法:
pf pymake clean # 执行 clean 图
pf pymake build --strategy thread # 覆盖执行策略
pf pymake test --dry-run # 仅打印执行计划
pf pymake --list # 列出所有命令
pf pymake --quiet # 静默模式
verbose=True(默认)时打印任务生命周期(开始/成功/失败/跳过)与命令输出;--quiet 关闭。
断点续跑
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",
使不同输入产生独立缓存条目:
px.TaskSpec(
"fetch_user",
fn=fetch_user,
cache_key=lambda ctx: str(ctx.get("uid")), # 不同 uid 独立缓存
)
LRU 驱逐:MemoryBackend(maxsize=N) 限制最大条目数,超限时按 LRU(最近最少使用)
策略驱逐最旧条目。get/has 命中会将条目移到最近使用位置:
from pyflowx import MemoryBackend
backend = MemoryBackend(maxsize=100) # 最多 100 条,超出驱逐最旧
手动失效:invalidate(key) 删除单个键的缓存,clear() 清除全部:
backend.invalidate("fetch_user") # 删除单个键,返回是否已删除
backend.clear() # 清除全部
错误处理
所有错误都是 PyFlowXError 的子类:
| 错误 | 触发时机 |
|---|---|
DuplicateTaskError |
任务名重复注册 |
MissingDependencyError |
依赖了不存在的任务 |
CycleError |
依赖图存在环 |
TaskFailedError |
任务耗尽重试后仍失败 |
TaskTimeoutError |
任务超时 |
InjectionError |
上下文注入无法满足签名 |
StorageError |
状态后端持久化失败 |
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 分组限流,避免压垮下游资源:
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 在任务生命周期触发(异常仅记录,不影响任务状态):
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 降序执行(稳定排序):
px.TaskSpec("low", fn=work, priority=0)
px.TaskSpec("high", fn=work, priority=10) # 同层内先执行
任务取消
CancelToken 提供线程安全的取消信号,传入 run(cancel_event=...) 即可外部取消正在执行的图。
KeyboardInterrupt(Ctrl+C)也会触发相同的优雅取消流程:
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 作为取消信号:
event = threading.Event()
px.run(graph, cancel_event=event)
子图执行
run(only=...) / run(tags=...) 只运行指定任务及其传递依赖,适用于调试单个任务或增量执行:
# 只运行 "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:
pf yamlrun pipeline.yaml --only report,deploy
pf yamlrun pipeline.yaml --tags ingest,report
结果序列化
RunReport 支持将运行结果导出为 JSON / dict / CSV,便于持久化、跨进程传输或对接外部工具:
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 自定义:
# 自定义值序列化(如把 Path 转为字符串)
text = report.to_json(value_serializer=str)
from_json() 可从 JSON 字符串重建 RunReport,仅供查询/分析,不能重新
执行(TaskSpec 的 fn/cmd 等不可序列化字段无法恢复):
restored = px.RunReport.from_json(text)
print(restored["double"]) # 任务返回值
print(restored.result_of("a").status) # TaskStatus
CLI 可视化
pf graph <yaml> 在终端可视化任务图 DAG,无需运行即可检查依赖关系:
# 默认 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 进度条显示运行中任务、完成
数与耗时:
pf yamlrun pipeline.yaml --progress
任务列表过滤
pf yamlrun --list 支持 --list-tag / --list-name 过滤,便于大型 YAML
中快速定位任务:
# 列出所有任务
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] 显示工具详情:
# 列出所有工具的简表(含子命令数、描述)
pf info
# 显示指定工具的子命令详情
pf info clr
pf info gittool
pf completion <shell> 生成 shell 补全脚本,提升日常使用体验:
# 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 等常见异常并给出提示
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:
pf yamlrun pipeline.yaml # 失败时自动输出诊断报告
report.describe() 在失败时也会附诊断摘要。
可观测性
每次 run() 自动生成 8 字符十六进制 run_id,作为本次运行的唯一追踪 ID,
贯穿日志、序列化结果与诊断报告:
report = px.run(graph)
print(report.run_id) # 如 "a3f4b2c1"
print(report.summary()["run_id"]) # summary 也含 run_id
run_id 通过结构化日志的 extra 字段注入所有执行器日志,便于跨系统关联。
日志格式遵循 logging 标准延迟格式化,extra 字段包括:
run_id—— 本次运行 IDtask_name—— 任务名(任务级日志)status—— 任务状态(running/success/failed/skipped)attempts—— 尝试次数(失败日志)error_type—— 异常类型名(失败日志)
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 瓶颈),零运行时开销:
profile = report.profile(graph)
print(profile.describe()) # 人类可读报告
print(profile.critical_path) # 关键路径任务序列
bottlenecks = profile.top_bottlenecks(5) # Top-5 瓶颈任务
开发
# 安装开发依赖
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 <tool> [command],按需导入 ops/<tool>.py 触发 @px.tool 注册 |
cli/legacy/profiler.py |
性能分析 CLI(legacy) |
cli/legacy/emlmanager.py |
邮件管理 CLI(legacy) |
许可证
MIT