Metadata-Version: 2.4
Name: pyflowx
Version: 0.4.10
Summary: Lightweight, type-safe DAG task scheduler with multi-strategy execution.
Author: pyflowx
License: MIT
License-File: LICENSE
Keywords: async,dag,scheduler,task,workflow
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
Requires-Python: >=3.10
Requires-Dist: pyyaml>=6.0.1
Requires-Dist: rich>=13.7.0
Requires-Dist: typer>=0.24.0
Requires-Dist: typing-extensions>=4.13.2; python_version < '3.13'
Provides-Extra: dev
Requires-Dist: hatch>=1.14.2; extra == 'dev'
Requires-Dist: httpx>=0.28.0; extra == 'dev'
Requires-Dist: prek>=0.4.5; extra == 'dev'
Requires-Dist: pyrefly>=1.1.1; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.24.0; extra == 'dev'
Requires-Dist: pytest-cov>=5.0.0; extra == 'dev'
Requires-Dist: pytest-html>=4.1.1; extra == 'dev'
Requires-Dist: pytest-mock>=3.14.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.6.1; extra == 'dev'
Requires-Dist: pytest>=8.0.0; extra == 'dev'
Requires-Dist: ruff>=0.8.0; extra == 'dev'
Requires-Dist: tox-uv>=1.13.1; extra == 'dev'
Requires-Dist: tox>=4.25.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: myst-parser>=3.0; extra == 'docs'
Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
Requires-Dist: sphinx>=7.0; extra == 'docs'
Provides-Extra: fast
Requires-Dist: orjson>=3.10.0; extra == 'fast'
Provides-Extra: office
Requires-Dist: pillow>=10.4.0; extra == 'office'
Requires-Dist: pymupdf>=1.24.11; extra == 'office'
Requires-Dist: pypdf>=5.9.0; extra == 'office'
Requires-Dist: pytesseract>=0.3.13; extra == 'office'
Description-Content-Type: text/markdown

# 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 <yaml>` 终端 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 <yaml>` 在终端可视化任务图 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>` 生成 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 <tool> [command]`，按需导入 `ops/<tool>.py` 触发 `@px.tool` 注册 |
| `cli/legacy/profiler.py` | 性能分析 CLI（legacy） |
| `cli/legacy/emlmanager.py` | 邮件管理 CLI（legacy） |

## 许可证

MIT
