docs: 对齐 README 与实际代码状态, 清理过期文档 — 移除未实现的 YAML 编排特性声明与整章, 修正覆盖率徽章 100%→97%, 修正依赖描述(实际 rich+typer+typing-extensions, 无 PyYAML), 修正 CLI 模块路径(profiler/emlmanager 已迁至 cli/legacy/); 归档 iter-01~05 迭代记录(设计决策已存 project_memory.md), 删除过期重构计划文档; 保留 typing_extensions 守卫(3.12/3.13 前向兼容非 3.8 死代码)
CI / Lint, Typecheck & Test (push) Failing after 1m26s
CI / Lint, Typecheck & Test (push) Failing after 1m26s
This commit is contained in:
@@ -1,62 +0,0 @@
|
||||
# 迭代 01: typer/rich 迁移
|
||||
|
||||
## 本轮目标
|
||||
|
||||
采用 typer + rich 简化 CLI 设计, 提高显示效果:
|
||||
1. 修复 Python 3.8 兼容性测试失败
|
||||
2. 全量重写 tools.py 为 typer 内核 (保留 @px.tool 装饰器 API)
|
||||
3. 重写 pf.py 为 rich 驱动主入口
|
||||
4. rich 深度集成 (执行生命周期/错误/工具列表/dry-run)
|
||||
5. 放弃 Python 3.8/3.9 支持 (requires-python >=3.10)
|
||||
|
||||
## 改动文件清单
|
||||
|
||||
### 依赖与配置
|
||||
- `pyproject.toml`: requires-python >=3.10, 添加 typer>=0.24.0 + rich>=13.7.0 依赖, ruff target-version=py310, pyrefly python-version=3.10
|
||||
- `tox.ini`: envlist 改为 py310-py314
|
||||
|
||||
### 核心重写
|
||||
- `src/pyflowx/tools.py`: 全量重写 — argparse → typer.Typer; _make_dispatcher 构建命令 dispatcher; _build_typer_app 单命令/多命令路由; _execute_dag DAG 编排; standalone_mode=False + 捕获 typer._click 异常处理退出码; _handle_list_rich rich 表格 --list
|
||||
- `src/pyflowx/cli/pf.py`: 重写 — rich Panel+Table 工具列表 (含别名/说明列), --help/-h/--version/-V 全局选项, 未知工具模糊匹配建议 (difflib), rich Console stderr 错误
|
||||
|
||||
### rich 深度集成
|
||||
- `src/pyflowx/executors.py`: _make_verbose_callback 用 ✓/✗/▸/○ 符号 + 颜色替代 [verbose] 前缀; _print_dry_run rich styled
|
||||
- `src/pyflowx/command.py`: 执行命令/返回码用 rich Console + 颜色 (green=0/red=非0)
|
||||
|
||||
### Python 3.10+ 现代化
|
||||
- `src/pyflowx/graph.py`: 移除 graphlib_backport, 直接 import graphlib
|
||||
- `src/pyflowx/task.py`: Union → X|Y, typing.ContextManager → contextlib.AbstractContextManager
|
||||
- `src/pyflowx/storage.py`: 同上
|
||||
- `src/pyflowx/command.py`: typing.List → list, Union → X|Y
|
||||
- `src/pyflowx/cli/emlmanager.py`: zip() 加 strict= 参数
|
||||
- `src/pyflowx/executors.py`: zip() 加 strict=True
|
||||
|
||||
### 测试
|
||||
- `tests/test_tools.py`: 全量重写 (84 测试) — 覆盖 typer 内核 (_make_dispatcher/_build_typer_app/_execute_dag/退出码三态)
|
||||
- `tests/test_runner.py`: verbose 断言更新 (✓ 替代 [verbose])
|
||||
|
||||
## 关键决策与依据
|
||||
|
||||
1. **保留 @px.tool 装饰器 API**: 用户既有 ops/ 模块全部使用 @px.tool, 改 API 会破坏所有工具. 内部替换 argparse→typer, 外部 API 不变.
|
||||
|
||||
2. **standalone_mode=False**: typer 默认 standalone_mode=True 会 sys.exit() 退出进程, 测试中无法捕获. 用 False + 手动捕获 typer._click 异常 (NoArgsIsHelpError/ClickException/Abort) 处理退出码.
|
||||
|
||||
3. **typer._click 私有导入**: typer 内置 click (typer._click) 与公共 click 是两套独立类层次, 必须从 typer._click.exceptions 导入 NoArgsIsHelpError/ClickException 才能捕获.
|
||||
|
||||
4. **--list 预处理而非 typer callback**: typer 的 no_args_is_help=True 在多命令工具中会拦截 --list (要求先给 subcommand). 在 run_tool 中预处理 --list (任意位置出现即列出任务) 绕过此限制.
|
||||
|
||||
5. **单命令 vs 多命令**: 单命令工具 (仅 subcommand=None) 用 callback(invoke_without_command=True), no_args_is_help=False; 多命令工具用各 subcommand 注册为 command, no_args_is_help=True.
|
||||
|
||||
6. **legacy 工具不迁移**: emlmanager.py 是 Web 应用 (sqlite3+threading+HTTP), profiler.py 用 runpy.run_path, 均不适配 @px.tool DAG 模型. 保留 pf._run_legacy() 路由是更简方案.
|
||||
|
||||
## 验证结果
|
||||
|
||||
- ruff check: 0 errors
|
||||
- ruff format: 96 files already formatted
|
||||
- pyrefly: 0 errors (12 suppressed)
|
||||
- pytest: 1003 passed
|
||||
- coverage: 97.00% (门槛 95%)
|
||||
|
||||
## 遗留事项
|
||||
|
||||
- 无
|
||||
@@ -1,67 +0,0 @@
|
||||
# 迭代 02:SQLite 状态后端 (SQLiteBackend)
|
||||
|
||||
## 本轮目标
|
||||
|
||||
在 `src/pyflowx/storage.py` 新增 `SQLiteBackend`,扩展 `StateBackend` ABC,
|
||||
解决 `JSONBackend` 全量加载/落盘在大规模任务(10k+)下的性能瓶颈。
|
||||
|
||||
使用标准库 `sqlite3`,零新依赖。作为 `JSONBackend` 的 drop-in 替换,
|
||||
支持 TTL、断点续跑、`batch()` 批量提交。
|
||||
|
||||
## 改动文件清单
|
||||
|
||||
- `src/pyflowx/storage.py` — 新增 `SQLiteBackend` 类
|
||||
- `src/pyflowx/__init__.py` — 导出 `SQLiteBackend`
|
||||
- `tests/test_storage.py` — 新增 SQLiteBackend 测试用例
|
||||
- `docs/api.rst` — 补充 SQLiteBackend 文档(如需)
|
||||
|
||||
## 关键决策与依据
|
||||
|
||||
### 1. 复用 `_TTLStateBackendMixin`
|
||||
|
||||
`SQLiteBackend` 继承 `_TTLStateBackendMixin`,只需实现 4 个原语
|
||||
(`_get_raw`/`_put_raw`/`_iter_raw`/`_clear_raw`),TTL 逻辑由 mixin 提供。
|
||||
与 `MemoryBackend`/`JSONBackend` 保持一致架构。
|
||||
|
||||
### 2. 值序列化策略
|
||||
|
||||
值以 JSON 文本存入 `value` 列(`json.dumps`/`json.loads`),与 `JSONBackend`
|
||||
保持相同契约(结果必须可 JSON 序列化)。这样:
|
||||
- 三种后端行为一致,用户可无缝切换
|
||||
- 复用现有 `StorageError` 序列化错误处理
|
||||
- 无需引入 pickle(安全风险)
|
||||
|
||||
### 3. 线程安全
|
||||
|
||||
sqlite3 连接对象本身非线程安全。使用 `threading.RLock` 序列化所有
|
||||
`execute`/`commit` 调用,支持 `thread`/`async`/`dependency` 策略下的
|
||||
并发存取。`check_same_thread=False` 允许跨线程使用同一连接。
|
||||
|
||||
### 4. batch() 语义
|
||||
|
||||
`batch()` 不持有锁整个 duration(否则阻塞并发),仅设置 `_in_batch` 标志。
|
||||
各 `_put_raw` 调用各自加锁做 INSERT(不 commit),退出时单次 commit。
|
||||
与 `JSONBackend.batch()` 模式一致。
|
||||
|
||||
### 5. WAL 模式
|
||||
|
||||
启用 `PRAGMA journal_mode=WAL` + `synchronous=NORMAL`,提升并发读写性能。
|
||||
WAL 模式下读不阻塞写。
|
||||
|
||||
### 6. 不实现 `__del__`
|
||||
|
||||
依赖 sqlite3 连接自身的 `__del__` 关闭;提供 `close()` 方法供显式清理。
|
||||
`__del__` + RLock 有死锁风险,避免。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- ruff / pyrefly / pytest 全通过
|
||||
- 覆盖率 ≥ 95%
|
||||
- 作为 JSONBackend drop-in 替换,现有测试通过
|
||||
- 并发线程安全测试通过
|
||||
|
||||
## 遗留事项
|
||||
|
||||
- P2 实时进度监控(下一迭代)
|
||||
- P3 任务通知系统
|
||||
- P4 结果流式获取
|
||||
@@ -1,57 +0,0 @@
|
||||
# 迭代 03:实时进度监控
|
||||
|
||||
## 本轮目标
|
||||
|
||||
提供两种进度监控形式:
|
||||
1. `run(progress=True)` 开箱即用的 rich 进度条
|
||||
2. `ProgressCallback` 协议供程序化集成
|
||||
|
||||
复用现有 `rich` 依赖与 `EventCallback`/`TaskEvent` 事件机制。
|
||||
|
||||
## 改动文件清单
|
||||
|
||||
- `src/pyflowx/progress.py` — 新增:ProgressCallback Protocol + RichProgressMonitor
|
||||
- `src/pyflowx/executors.py` — run() 新增 progress 参数,组合回调链
|
||||
- `src/pyflowx/__init__.py` — 导出 ProgressCallback、RichProgressMonitor
|
||||
- `tests/test_progress.py` — 新增测试
|
||||
|
||||
## 关键决策与依据
|
||||
|
||||
### 1. ProgressCallback Protocol 带生命周期
|
||||
|
||||
普通 `EventCallback`(Callable[[TaskEvent], None])无法表达 start/finish 生命周期,
|
||||
而 rich.Progress 需要显式启动/停止。因此 Protocol 定义三个方法:
|
||||
`start(total)` / `on_event(event)` / `finish()`。
|
||||
|
||||
### 2. progress 参数接受 bool | ProgressCallback
|
||||
|
||||
- `False`(默认):无进度监控
|
||||
- `True`:使用默认 `RichProgressMonitor`
|
||||
- `ProgressCallback` 实例:自定义监控器
|
||||
|
||||
与现有 `on_event` 独立——`on_event` 是简单逐事件函数,
|
||||
`progress` 是带生命周期的监控器。两者可共存。
|
||||
|
||||
### 3. 回调组合
|
||||
|
||||
用 `_compose_callbacks` 将 verbose 打印、monitor.on_event、用户 on_event
|
||||
组合为单个 effective_callback,避免多层包装。
|
||||
|
||||
### 4. RichProgressMonitor 设计
|
||||
|
||||
- 使用 rich.Progress 的 Live 显示,列:spinner + 描述 + 进度条 + M/N + 百分比 + 耗时
|
||||
- 运行中任务名显示在描述栏(最多 3 个 + 折叠)
|
||||
- SUCCESS/FAILED/SKIPPED 均推进进度(终态即完成)
|
||||
- console 可注入,便于测试
|
||||
|
||||
## 验证结果
|
||||
|
||||
- ruff / pyrefly / pytest 全通过
|
||||
- 覆盖率 ≥ 95%
|
||||
- run(progress=True) 在终端显示进度条
|
||||
- 自定义 ProgressCallback 可订阅事件
|
||||
|
||||
## 遗留事项
|
||||
|
||||
- P3 任务通知系统(下一迭代)
|
||||
- P4 结果流式获取
|
||||
@@ -1,52 +0,0 @@
|
||||
# 迭代 04:任务通知系统
|
||||
|
||||
## 本轮目标
|
||||
|
||||
全生命周期任务通知:RUNNING/SUCCESS/FAILED/SKIPPED 均可配置。
|
||||
支持 webhook sink(飞书/钉钉/企业微信)+ Python 回调 sink。
|
||||
不改变核心执行流程,通过回调链集成。
|
||||
|
||||
## 改动文件清单
|
||||
|
||||
- `src/pyflowx/notification.py` — 新增:Notifier Protocol + WebhookNotifier + 平台实现
|
||||
- `src/pyflowx/executors.py` — run() 新增 notifiers 参数,_resolve_callbacks 统一处理
|
||||
- `src/pyflowx/__init__.py` — 导出通知类
|
||||
- `tests/test_notification.py` — 新增测试
|
||||
|
||||
## 关键决策与依据
|
||||
|
||||
### 1. 独立模块而非扩展 TaskHooks
|
||||
|
||||
TaskHooks 是 per-task 的(每个 TaskSpec 独立配置)。通知是 graph-level 的
|
||||
(对所有任务事件发送)。架构层级不同,独立 Notifier 系统更合适。
|
||||
|
||||
### 2. 零依赖 webhook
|
||||
|
||||
用 stdlib `urllib.request` 发送 HTTP POST,不引入 requests/httpx。
|
||||
项目运行时依赖仅 rich/typer/typing-extensions,保持零新增。
|
||||
|
||||
### 3. 事件过滤在 Notifier 内部
|
||||
|
||||
每个 Notifier 持有 `levels` 集合,notify() 内部判断是否处理该事件。
|
||||
避免在 run() 中做过滤逻辑,保持执行流程简洁。
|
||||
|
||||
### 4. 回调链集成
|
||||
|
||||
notifier.notify 作为 EventCallback 加入 effective_callback 链,
|
||||
与 on_event/progress/verbose 共存。close() 在 run() finally 中调用。
|
||||
|
||||
### 5. 消息格式共享
|
||||
|
||||
飞书/钉钉/企业微信消息文本格式相同(图标 + 任务名 + 状态 + 耗时 + 错误),
|
||||
仅 payload 结构不同。提取 _format_message 为共享方法。
|
||||
|
||||
## 验证结果
|
||||
|
||||
- ruff / pyrefly / pytest 全通过
|
||||
- 覆盖率 ≥ 95%
|
||||
- 各平台 payload 格式正确
|
||||
- webhook 发送失败不影响主流程
|
||||
|
||||
## 遗留事项
|
||||
|
||||
- P4 结果流式获取(下一迭代)
|
||||
@@ -1,44 +0,0 @@
|
||||
# 迭代 05:结果流式获取(run_iter)
|
||||
|
||||
## 目标
|
||||
|
||||
新增 `run_iter()` 函数,以生成器形式逐个 yield 完成任务的结果,
|
||||
适用于大量任务逐个处理、内存敏感场景。保留 `run()` 返回 `RunReport`,
|
||||
完全向后兼容。
|
||||
|
||||
## 设计
|
||||
|
||||
### 方案:线程 + 队列
|
||||
|
||||
`run()` 是阻塞函数,内部可能调用 `asyncio.run()`,无法直接改为生成器。
|
||||
采用线程 + 队列方案:
|
||||
|
||||
1. `run()` 新增内部参数 `_report: RunReport | None`(下划线前缀,内部用)
|
||||
2. `run_iter()` 创建 `RunReport`,传给 `run(_report=report)`
|
||||
3. `on_event` 回调将终态事件(SUCCESS/FAILED/SKIPPED)的任务名放入队列
|
||||
4. 后台线程执行 `run()`,生成器从队列取名,读 `report.results[name]` yield
|
||||
5. `None` 哨兵标记执行结束
|
||||
|
||||
### 关键点
|
||||
|
||||
- **结果可见性**:`_store_result` 先写 `report.results[name]` 再 `_emit`,
|
||||
队列提供内存屏障,yield 时结果必然可见
|
||||
- **去重**:skipped 任务可能 double-emit(`_prepare_for_execution` + `_store_result`),
|
||||
用 `yielded` set 去重
|
||||
- **错误传播**:`run()` 抛异常时存入 `error_box`,生成器耗尽后 re-raise
|
||||
- **早退**:daemon 线程,生成器 `finally` 中 join(timeout=0) 不阻塞
|
||||
|
||||
## 改动文件
|
||||
|
||||
- `src/pyflowx/executors.py` — 新增 `run_iter()`,`run()` 加 `_report` 参数
|
||||
- `src/pyflowx/__init__.py` — 导出 `run_iter`
|
||||
- `tests/test_run_iter.py` — 新增测试
|
||||
|
||||
## 验收标准
|
||||
|
||||
- `run_iter()` 逐个 yield 所有任务的 `(name, TaskResult)`
|
||||
- 支持 sequential/thread/async/dependency 四种策略
|
||||
- 失败任务也 yield(FAILED 状态),之后 re-raise `TaskFailedError`
|
||||
- 缓存命中的 SKIPPED 任务也 yield
|
||||
- `run()` 行为不变,现有测试全过
|
||||
- 覆盖率 ≥ 95%
|
||||
@@ -0,0 +1,65 @@
|
||||
# 迭代 06:代码清理与文档对齐
|
||||
|
||||
## 本轮目标
|
||||
|
||||
对齐 README 与实际代码状态,清理过期文档,归档历史迭代记录。
|
||||
|
||||
原始计划包含"移除 3.8 兼容代码(`from __future__ import annotations` /
|
||||
`typing_extensions` 版本守卫 / `sys.version_info` 分支)",但调研后发现
|
||||
该前提不准确:
|
||||
|
||||
- `typing_extensions` 守卫针对 **3.12/3.13 前向兼容**(`override` 是 3.12+,
|
||||
`TypeVar(default=)` 是 3.13+),项目 `requires-python >= 3.10`,守卫**不可移除**
|
||||
- `from __future__ import annotations` 在 3.10+ 仍有 PEP 563 价值(前向引用、
|
||||
延迟求值),移除需逐文件审计前向引用,风险高收益低
|
||||
- `sys.version_info` 分支即上述 `typing_extensions` 守卫,同上
|
||||
|
||||
因此本轮聚焦真正安全的清理项。
|
||||
|
||||
## 改动文件清单
|
||||
|
||||
### 文档对齐
|
||||
- `README.md` — 修正覆盖率徽章 100% → 97%;移除未实现的 "YAML 任务编排"
|
||||
特性声明与整章;修正 "最小依赖" 描述(实际依赖 rich + typer +
|
||||
typing-extensions,无 PyYAML);修正 CLI 模块路径(profiler/emlmanager 已
|
||||
迁移至 cli/legacy/);修正对比表 "运行时依赖 | 仅标准库"
|
||||
- `docs/installation.rst` — 移除 3.8 / graphlib_backport / PyYAML 提法
|
||||
|
||||
### 归档与清理
|
||||
- `.trae/docs/iter-01..05-*.md` — 归档至 project_memory.md(关键设计决策已
|
||||
记录在 memory 系统的 Engineering Conventions 小节),删除原文件
|
||||
- `.trae/documents/项目结构重构*.md` — 重构已完成,删除过期计划文档
|
||||
|
||||
### 保留(不动)
|
||||
- `typing_extensions` 版本守卫(3.12/3.13 前向兼容,非 3.8 死代码)
|
||||
- `from __future__ import annotations`(PEP 563 仍有价值,移除风险高)
|
||||
- `.trae/rules/python-standards.md` 中 "最低 Python 3.8" 描述(规则变更需
|
||||
用户授权,本轮不修改 rules/)
|
||||
|
||||
## 关键决策与依据
|
||||
|
||||
### 1. 不移除 typing_extensions 守卫
|
||||
|
||||
`pyproject.toml` 声明 `typing-extensions>=4.13.2; python_version < '3.13'`。
|
||||
`override`(3.12+)与 `TypeVar(default=)`(3.13+)在 3.10/3.11/3.12 上仍需
|
||||
`typing_extensions` 回退。守卫是前向兼容,不是 3.8 死代码。
|
||||
|
||||
### 2. 不移除 from __future__ import annotations
|
||||
|
||||
PEP 563 在 3.10+ 仍提供前向引用与延迟求值价值。50 个文件统一移除需逐文件
|
||||
审计前向引用、自引用类型,风险高收益低。保留是更稳妥的选择。
|
||||
|
||||
### 3. 不修改 python-standards.md
|
||||
|
||||
按 `.trae/rules/dev-workflow.md` 规则,`.trae/rules/` 下文件修改必须先获
|
||||
用户授权。该规则中 "最低 Python 3.8" 描述已过时(实际 >=3.10),但本轮
|
||||
不擅自修改,留待用户确认。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- README 无未实现特性声明(YAML 编排章节移除)
|
||||
- 覆盖率徽章与实际一致(97%)
|
||||
- 依赖描述与 pyproject.toml 一致
|
||||
- `.trae/docs/` 仅保留 iter-06
|
||||
- `.trae/documents/` 过期文件清理
|
||||
- 全套测试通过,覆盖率不下降
|
||||
@@ -1,194 +0,0 @@
|
||||
# PyFlowX 项目结构重构 — Phase 2 收尾计划
|
||||
|
||||
## Summary
|
||||
|
||||
承接上一会话已批准的 [项目结构重构计划](file:///home/zhou/pyflowx/.trae/documents/项目结构重构计划.md)。Phase 1(cli/legacy/ 归位)已完成并验证;Phase 2(ops/ 按功能分组)的文件移动、`_TOOL_MODULES` 注册表、`pf.py` 动态导入改造、`ops/__init__.py` docstring 更新均已落地,**仅剩 3 项收尾工作**:测试导入路径更新、注册表完整性测试、全量验证 + 提交。
|
||||
|
||||
本计划仅覆盖 Phase 2 剩余收尾,不引入新重构范围。
|
||||
|
||||
## Current State Analysis
|
||||
|
||||
### 已完成(无需再动)
|
||||
|
||||
| 项 | 状态 | 证据 |
|
||||
|----|------|------|
|
||||
| `cli/legacy/` 子包 | ✅ | [cli/legacy/__init__.py](file:///home/zhou/pyflowx/src/pyflowx/cli/legacy/__init__.py)、emlmanager.py、profiler.py 均在位 |
|
||||
| `pyproject.toml` 入口点 | ✅ | L26-28 已指向 `pyflowx.cli.legacy.*` |
|
||||
| `pf.py` `_LEGACY_TOOLS` | ✅ | L99-103 已更新 |
|
||||
| ops/ 4 个子目录 + `__init__.py` | ✅ | files/ dev/ system/ infra/ 均存在 |
|
||||
| 22 个工具文件已 `git mv` 到子目录 | ✅ | Glob 确认所有 .py 均在子目录下 |
|
||||
| `_common.py` 保留 ops 根 | ✅ | 8 处 `from pyflowx.ops._common import` 绝对路径仍有效 |
|
||||
| `pf.py` `_TOOL_MODULES` 注册表(22 项)| ✅ | L106-129 |
|
||||
| `pf.py` `_run_tool` / `_tool_description` 动态导入 | ✅ | L203-204、L258-259 改用 `self._TOOL_MODULES[target]` |
|
||||
| `ops/__init__.py` docstring | ✅ | 已文档化 4 个子目录 |
|
||||
| ops 模块间无交叉引用需改 | ✅ | Grep 确认仅 `_common` 被引用,路径不变 |
|
||||
|
||||
### 未完成(本计划处理)
|
||||
|
||||
1. **20 个测试文件的导入路径未更新**(26 行,含 2 行 `import pyflowx.ops.X` 形式)
|
||||
2. **`_TOOL_MODULES` 完整性测试未新增**
|
||||
3. **未跑验证 + 未提交**
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 改动 1:更新 20 个测试文件的导入路径
|
||||
|
||||
所有改动均为机械替换,按子目录分组如下。每处仅替换 `pyflowx.ops` → `pyflowx.ops.<group>`,不动其他内容。
|
||||
|
||||
**files/ 组(6 文件,6 行):**
|
||||
|
||||
| 文件 | 行号 | 当前 | 修改后 |
|
||||
|------|------|------|--------|
|
||||
| [tests/cli/test_filedate.py](file:///home/zhou/pyflowx/tests/cli/test_filedate.py#L7) | 7 | `from pyflowx.ops import filedate as files` | `from pyflowx.ops.files import filedate as files` |
|
||||
| [tests/cli/test_filelevel.py](file:///home/zhou/pyflowx/tests/cli/test_filelevel.py#L7) | 7 | `from pyflowx.ops import filelevel as files` | `from pyflowx.ops.files import filelevel as files` |
|
||||
| [tests/cli/test_folderback.py](file:///home/zhou/pyflowx/tests/cli/test_folderback.py#L8) | 8 | `from pyflowx.ops import folderback as files` | `from pyflowx.ops.files import folderback as files` |
|
||||
| [tests/cli/test_folderzip.py](file:///home/zhou/pyflowx/tests/cli/test_folderzip.py#L8) | 8 | `from pyflowx.ops import folderzip as files` | `from pyflowx.ops.files import folderzip as files` |
|
||||
| [tests/cli/test_pdftool.py](file:///home/zhou/pyflowx/tests/cli/test_pdftool.py#L11) | 11 | `from pyflowx.ops import pdftool as media` | `from pyflowx.ops.files import pdftool as media` |
|
||||
| [tests/cli/test_screenshot.py](file:///home/zhou/pyflowx/tests/cli/test_screenshot.py#L9) | 9 | `from pyflowx.ops import screenshot as media` | `from pyflowx.ops.files import screenshot as media` |
|
||||
|
||||
**dev/ 组(8 文件,9 行):**
|
||||
|
||||
| 文件 | 行号 | 当前 | 修改后 |
|
||||
|------|------|------|--------|
|
||||
| [tests/cli/test_autofmt.py](file:///home/zhou/pyflowx/tests/cli/test_autofmt.py#L8) | 8 | `from pyflowx.ops import autofmt as dev` | `from pyflowx.ops.dev import autofmt as dev` |
|
||||
| [tests/cli/test_bumpversion.py](file:///home/zhou/pyflowx/tests/cli/test_bumpversion.py#L10) | 10 | `from pyflowx.ops import bumpversion` | `from pyflowx.ops.dev import bumpversion` |
|
||||
| [tests/cli/test_gittool.py](file:///home/zhou/pyflowx/tests/cli/test_gittool.py#L11) | 11 | `from pyflowx.ops import gittool as dev` | `from pyflowx.ops.dev import gittool as dev` |
|
||||
| [tests/cli/test_gittool_tool.py](file:///home/zhou/pyflowx/tests/cli/test_gittool_tool.py#L12-L13) | 12, 13 | `import pyflowx.ops.gittool` + `from pyflowx.ops import gittool` | `import pyflowx.ops.dev.gittool` + `from pyflowx.ops.dev import gittool` |
|
||||
| [tests/cli/test_lscalc.py](file:///home/zhou/pyflowx/tests/cli/test_lscalc.py#L9) | 9 | `from pyflowx.ops import lscalc as system` | `from pyflowx.ops.dev import lscalc as system` |
|
||||
| [tests/cli/test_packtool.py](file:///home/zhou/pyflowx/tests/cli/test_packtool.py#L10) | 10 | `from pyflowx.ops import packtool as system` | `from pyflowx.ops.dev import packtool as system` |
|
||||
| [tests/cli/test_piptool.py](file:///home/zhou/pyflowx/tests/cli/test_piptool.py#L9) | 9 | `from pyflowx.ops import piptool as dev` | `from pyflowx.ops.dev import piptool as dev` |
|
||||
| [tests/cli/test_pymake_tool.py](file:///home/zhou/pyflowx/tests/cli/test_pymake_tool.py#L6) | 6 | `import pyflowx.ops.pymake` | `import pyflowx.ops.dev.pymake` |
|
||||
|
||||
**system/ 组(2 文件,4 行):**
|
||||
|
||||
| 文件 | 行号 | 当前 | 修改后 |
|
||||
|------|------|------|--------|
|
||||
| [tests/cli/test_reseticoncache.py](file:///home/zhou/pyflowx/tests/cli/test_reseticoncache.py#L10) | 10 | `from pyflowx.ops.reseticoncache import reset_icon_cache_run` | `from pyflowx.ops.system.reseticoncache import reset_icon_cache_run` |
|
||||
| [tests/cli/test_system_run.py](file:///home/zhou/pyflowx/tests/cli/test_system_run.py#L11-L13) | 11, 12, 13 | `from pyflowx.ops.clr import` / `from pyflowx.ops.taskkill import` / `from pyflowx.ops.which import` | `from pyflowx.ops.system.clr import` / `from pyflowx.ops.system.taskkill import` / `from pyflowx.ops.system.which import` |
|
||||
|
||||
**infra/ 组(4 文件,7 行):**
|
||||
|
||||
| 文件 | 行号 | 当前 | 修改后 |
|
||||
|------|------|------|--------|
|
||||
| [tests/cli/test_envdev.py](file:///home/zhou/pyflowx/tests/cli/test_envdev.py#L12-L13) | 12, 13 | `from pyflowx.ops.dockercmd import` / `from pyflowx.ops.envdev import (` | `from pyflowx.ops.infra.dockercmd import` / `from pyflowx.ops.infra.envdev import (` |
|
||||
| [tests/cli/test_envdev_tool.py](file:///home/zhou/pyflowx/tests/cli/test_envdev_tool.py#L6) | 6 | `from pyflowx.ops import envdev, msdownload, sglang` | `from pyflowx.ops.infra import envdev, msdownload, sglang` |
|
||||
| [tests/cli/test_llm.py](file:///home/zhou/pyflowx/tests/cli/test_llm.py#L12-L13) | 12, 13 | `from pyflowx.ops.msdownload import` / `from pyflowx.ops.sglang import` | `from pyflowx.ops.infra.msdownload import` / `from pyflowx.ops.infra.sglang import` |
|
||||
| [tests/cli/test_sshcopyid.py](file:///home/zhou/pyflowx/tests/cli/test_sshcopyid.py#L11) | 11 | `from pyflowx.ops import sshcopyid as system` | `from pyflowx.ops.infra import sshcopyid as system` |
|
||||
|
||||
**不动:** [tests/cli/test_ops_common.py](file:///home/zhou/pyflowx/tests/cli/test_ops_common.py#L8) — 引用 `_common`,位置不变。
|
||||
|
||||
### 改动 2:新增 `_TOOL_MODULES` 完整性测试
|
||||
|
||||
在 [tests/cli/](file:///home/zhou/pyflowx/tests/cli/) 下新建 `test_tool_modules.py`,断言:
|
||||
- `_TOOL_ALIASES` 中每个规范名(值集合)都在 `_TOOL_MODULES` 中有对应模块路径
|
||||
- 每个注册的模块路径可被 `importlib.import_module` 成功导入
|
||||
|
||||
目的:防止未来新增工具时遗漏注册表项,导致 `pf <新工具>` 静默失败。
|
||||
|
||||
测试骨架:
|
||||
|
||||
```python
|
||||
"""Tests for pf._TOOL_MODULES 注册表完整性."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
|
||||
import pytest
|
||||
|
||||
from pyflowx.cli.pf import PfApp
|
||||
|
||||
|
||||
class TestToolModulesComplete:
|
||||
"""_TOOL_MODULES 必须覆盖 _TOOL_ALIASES 的所有规范名."""
|
||||
|
||||
def test_all_canonical_tools_have_module_path(self) -> None:
|
||||
canonical_names = set(PfApp._TOOL_ALIASES.values())
|
||||
module_names = set(PfApp._TOOL_MODULES)
|
||||
missing = canonical_names - module_names
|
||||
assert not missing, f"未在 _TOOL_MODULES 注册的工具: {missing}"
|
||||
|
||||
@pytest.mark.parametrize("tool_name, module_path", sorted(PfApp._TOOL_MODULES.items()))
|
||||
def test_module_importable(self, tool_name: str, module_path: str) -> None:
|
||||
"""每个注册的模块路径必须可导入."""
|
||||
importlib.import_module(module_path)
|
||||
```
|
||||
|
||||
### 改动 3:验证 + 提交
|
||||
|
||||
```bash
|
||||
# 1. lint + 类型检查
|
||||
uv run ruff check src/ tests/
|
||||
uv run ruff format --check src/ tests/
|
||||
uv run pyrefly check src/
|
||||
|
||||
# 2. 全量测试(项目门槛 95% branch coverage)
|
||||
uv run pytest -m "not slow" -q
|
||||
|
||||
# 3. CLI 烟囱测试
|
||||
uv run pf # 工具列表
|
||||
uv run pf pdftool # 多命令工具 (files 组)
|
||||
uv run pf wch echo # 单命令工具 (system 组)
|
||||
uv run pf gitt c # gittool (dev 组)
|
||||
uv run pf emlman --help # legacy 路由
|
||||
uv run pxp --help # legacy 入口点
|
||||
|
||||
# 4. 提交(按文件名 add,遵循 git-commit-message.md 中文风格)
|
||||
git add src/pyflowx/cli/pf.py \
|
||||
src/pyflowx/cli/legacy/__init__.py \
|
||||
src/pyflowx/cli/legacy/emlmanager.py \
|
||||
src/pyflowx/cli/legacy/profiler.py \
|
||||
src/pyflowx/ops/__init__.py \
|
||||
src/pyflowx/ops/files/ \
|
||||
src/pyflowx/ops/dev/ \
|
||||
src/pyflowx/ops/system/ \
|
||||
src/pyflowx/ops/infra/ \
|
||||
tests/cli/test_tool_modules.py \
|
||||
tests/cli/test_emlmanager.py \
|
||||
tests/cli/test_profiler.py \
|
||||
tests/cli/test_autofmt.py \
|
||||
tests/cli/test_bumpversion.py \
|
||||
tests/cli/test_envdev.py \
|
||||
tests/cli/test_envdev_tool.py \
|
||||
tests/cli/test_filedate.py \
|
||||
tests/cli/test_filelevel.py \
|
||||
tests/cli/test_folderback.py \
|
||||
tests/cli/test_folderzip.py \
|
||||
tests/cli/test_gittool.py \
|
||||
tests/cli/test_gittool_tool.py \
|
||||
tests/cli/test_llm.py \
|
||||
tests/cli/test_lscalc.py \
|
||||
tests/cli/test_packtool.py \
|
||||
tests/cli/test_pdftool.py \
|
||||
tests/cli/test_piptool.py \
|
||||
tests/cli/test_pymake_tool.py \
|
||||
tests/cli/test_reseticoncache.py \
|
||||
tests/cli/test_screenshot.py \
|
||||
tests/cli/test_sshcopyid.py \
|
||||
tests/cli/test_system_run.py \
|
||||
pyproject.toml
|
||||
git commit -m "refactor: 按功能划分重构项目结构,ops 工具归入 files/dev/system/infra 子包,legacy 工具归入 cli/legacy"
|
||||
git push # 仅当分支已跟踪远程
|
||||
```
|
||||
|
||||
## Assumptions & Decisions
|
||||
|
||||
1. **不动 ops 模块内部代码**:已核查 22 个工具模块均用 `import pyflowx as px`(绝对导入)和 `from pyflowx.ops._common import`(_common 位置不变),无相对导入需调整。
|
||||
2. **`_TOOL_MODULES` 用类属性而非模块级常量**:与既有 `_TOOL_ALIASES`/`_LEGACY_TOOLS` 保持一致风格。
|
||||
3. **完整性测试放 `tests/cli/`**:与既有 `test_pf.py` 等同目录,聚焦 CLI 路由层。
|
||||
4. **不修改覆盖率配置**:`omit = ["src/pyflowx/cli/*"]` 递归匹配 `cli/legacy/*`,`src/pyflowx/ops/*` 仍在覆盖范围内(ops 模块本就有测试)。
|
||||
5. **不重命名 ops/ 为 tools/**:与顶层 `tools.py` 冲突,收益不匹配风险(沿用原计划决策)。
|
||||
6. **git add 按文件名**:遵循项目硬约束,不用 `git add .`/`-A`。移动文件用 `git mv` 已在前序步骤完成,本次 add 仅暂存修改和新增文件。
|
||||
|
||||
## Verification
|
||||
|
||||
完成标志(逐条核验):
|
||||
- [ ] `ruff check` 无错误
|
||||
- [ ] `ruff format --check` 无需改动
|
||||
- [ ] `pyrefly check` 无错误
|
||||
- [ ] `pytest -m "not slow"` 全绿,覆盖率 ≥ 95%
|
||||
- [ ] `pf` 列出全部工具(22 个 @px.tool + 3 个 legacy)
|
||||
- [ ] `pf pdftool` / `pf wch echo` / `pf gitt c` 三组工具均可调用
|
||||
- [ ] `pf emlman --help` / `pxp --help` legacy 路由正常
|
||||
- [ ] `test_tool_modules.py` 全绿
|
||||
- [ ] commit 已创建,远程已推送(若分支跟踪远程)
|
||||
@@ -1,129 +0,0 @@
|
||||
# PyFlowX 项目结构重构计划
|
||||
|
||||
## Context
|
||||
|
||||
当前项目结构存在三个问题:
|
||||
1. **cli/ 职责混杂**:`pf.py`(入口)、`emlmanager.py`(1337 行 legacy web 应用)、`profiler.py`(268 行 legacy 性能分析器)混在同一层级,入口与遗留工具不分
|
||||
2. **ops/ 下 22 个文件平铺**:从 filedate 到 sglang 全部堆在一级目录,无功能分组,难以浏览和维护
|
||||
3. **ops/ 命名歧义**:与顶层 `tools.py`(@px.tool 装饰器框架)容易混淆
|
||||
|
||||
本次重构通过两个阶段解决:Phase 1 将 legacy 工具归入 `cli/legacy/`,Phase 2 将 ops/ 下工具按功能分入 4 个子目录。**不移动** runner.py 和 tools.py(保留顶层),因为它们是公共 API 库代码且移动涉及覆盖率配置变更和 profiler monkey-patch 链路调整,风险不匹配收益。
|
||||
|
||||
## 目标结构
|
||||
|
||||
```
|
||||
src/pyflowx/
|
||||
├── __init__.py # 公共 API 导出(不变)
|
||||
├── task.py / graph.py / executors.py / ... # 核心库(不变)
|
||||
├── runner.py / tools.py # CLI 框架库代码(保留顶层)
|
||||
├── cli/
|
||||
│ ├── pf.py # pf 统一入口
|
||||
│ └── legacy/ # ← Phase 1: legacy 工具归位
|
||||
│ ├── __init__.py
|
||||
│ ├── emlmanager.py
|
||||
│ └── profiler.py
|
||||
└── ops/
|
||||
├── __init__.py # 更新 docstring
|
||||
├── _common.py # 保留根(所有子目录共用)
|
||||
├── files/ # ← Phase 2: 文件与文档操作 (6)
|
||||
│ ├── filedate.py, filelevel.py, folderback.py, folderzip.py
|
||||
│ ├── pdftool.py, screenshot.py
|
||||
├── dev/ # 开发与构建工具 (7)
|
||||
│ ├── autofmt.py, bumpversion.py, gittool.py, packtool.py
|
||||
│ ├── piptool.py, pymake.py, lscalc.py
|
||||
├── system/ # 系统工具 (4)
|
||||
│ ├── clr.py, reseticoncache.py, taskkill.py, which.py
|
||||
└── infra/ # 基础设施与服务部署 (5)
|
||||
├── dockercmd.py, envdev.py, sshcopyid.py
|
||||
├── msdownload.py, sglang.py
|
||||
```
|
||||
|
||||
## Phase 1: cli/ 清理 — legacy 工具归位(低风险)
|
||||
|
||||
**文件移动:**
|
||||
- `cli/emlmanager.py` → `cli/legacy/emlmanager.py`
|
||||
- `cli/profiler.py` → `cli/legacy/profiler.py`
|
||||
- 新建 `cli/legacy/__init__.py`
|
||||
|
||||
**导入修改:**
|
||||
|
||||
| 文件 | 当前 | 修改后 |
|
||||
|------|------|--------|
|
||||
| `cli/legacy/profiler.py` L43-46 | `from .. import executors` 等 4 处(`..`=pyflowx) | `from ... import executors` 等(`...`=pyflowx,多一层 legacy/) |
|
||||
|
||||
**配置修改:**
|
||||
|
||||
| 文件 | 修改 |
|
||||
|------|------|
|
||||
| `pyproject.toml` L26 | `emlman = "pyflowx.cli.legacy.emlmanager:main"` |
|
||||
| `pyproject.toml` L28 | `pxp = "pyflowx.cli.legacy.profiler:main"` |
|
||||
| `cli/pf.py` L100-102 | `_LEGACY_TOOLS` 3 个值改为 `pyflowx.cli.legacy.xxx:main` |
|
||||
|
||||
**测试修改(2 文件):**
|
||||
- `tests/cli/test_emlmanager.py`:`from pyflowx.cli import emlmanager` → `from pyflowx.cli.legacy import emlmanager`
|
||||
- `tests/cli/test_profiler.py`:`from pyflowx.cli import profiler` → `from pyflowx.cli.legacy import profiler`
|
||||
|
||||
**覆盖率:** `omit = ["src/pyflowx/cli/*"]` 递归匹配 `cli/legacy/*`,无需改动。
|
||||
|
||||
## Phase 2: ops/ 按功能分组(中等风险)
|
||||
|
||||
**文件移动(22 个工具文件 + 4 个新建 `__init__.py`):**
|
||||
|
||||
| 子目录 | 移入文件 |
|
||||
|--------|---------|
|
||||
| `ops/files/` | filedate, filelevel, folderback, folderzip, pdftool, screenshot |
|
||||
| `ops/dev/` | autofmt, bumpversion, gittool, packtool, piptool, pymake, lscalc |
|
||||
| `ops/system/` | clr, reseticoncache, taskkill, which |
|
||||
| `ops/infra/` | dockercmd, envdev, sshcopyid, msdownload, sglang |
|
||||
|
||||
`_common.py` 保留 `ops/` 根——子目录工具通过绝对路径 `from pyflowx.ops._common import ...` 引用,路径不变。
|
||||
|
||||
**ops 模块自身导入:** 无需修改。所有 ops 模块用 `import pyflowx as px`(绝对导入)和 `from pyflowx.ops._common import ...`(_common 位置不变)。已核查 ops 模块间无交叉引用。
|
||||
|
||||
**pf.py 修改 — 新增 `_TOOL_MODULES` 注册表:**
|
||||
|
||||
```python
|
||||
_TOOL_MODULES: dict[str, str] = {
|
||||
"autofmt": "pyflowx.ops.dev.autofmt",
|
||||
"bumpversion": "pyflowx.ops.dev.bumpversion",
|
||||
# ... 22 个规范工具名 → 完整模块路径
|
||||
}
|
||||
```
|
||||
|
||||
修改 `_run_tool` 和 `_tool_description` 两处动态导入:
|
||||
- `importlib.import_module(f"pyflowx.ops.{target}")` → `importlib.import_module(self._TOOL_MODULES[target])`
|
||||
|
||||
**ops/__init__.py 修改:** 更新 docstring,补充 lscalc(当前遗漏),标注分组结构。`__all__` 保持空列表。
|
||||
|
||||
**测试修改(20 文件,机械替换导入路径):**
|
||||
|
||||
代表性修改:
|
||||
- `from pyflowx.ops import filedate` → `from pyflowx.ops.files import filedate`
|
||||
- `from pyflowx.ops.gittool import ...` → `from pyflowx.ops.dev.gittool import ...`
|
||||
- `from pyflowx.ops.clr import ...` → `from pyflowx.ops.system.clr import ...`
|
||||
- `from pyflowx.ops.dockercmd import ...` → `from pyflowx.ops.infra.dockercmd import ...`
|
||||
|
||||
`test_ops_common.py` 无需修改(`_common` 位置不变)。
|
||||
|
||||
**新增测试:** 遍历 `_TOOL_ALIASES` 所有规范名,断言每个都在 `_TOOL_MODULES` 中且模块可导入,防止注册表遗漏。
|
||||
|
||||
## 不做的改动
|
||||
|
||||
- **runner.py / tools.py 保留顶层**:它们是公共 API(`px.CliRunner`、`px.tool`),有完整测试。移入 cli/ 涉及覆盖率配置变更(`omit` 从 `cli/*` 改为精确排除)和 profiler monkey-patch 链路调整,风险不匹配收益。
|
||||
- **ops/ 不重命名**:重命名为 `tools/` 与顶层 `tools.py` 冲突;其他名字(`cli_tools/`、`commands/`)收益不明显。通过 docstring 明确"ops = CLI 工具实现集合"即可。
|
||||
- **核心库模块不动**:task.py、graph.py 等是公共 API 的一部分,用户通过 `px.TaskSpec`、`px.Graph` 访问,移动增加风险且价值不大。
|
||||
- **ops/__init__.py 不做 re-export**:保持惰性加载,避免 CLI 启动时加载全部 22 个工具模块。
|
||||
|
||||
## 验证
|
||||
|
||||
每个 Phase 完成后:
|
||||
```bash
|
||||
uv run ruff check src/ tests/ # lint
|
||||
uv run pyrefly check src/ # 类型检查
|
||||
uv run pytest -m "not slow" -q # 全套测试 (993 个)
|
||||
uv run pytest tests/cli/ -q # CLI 测试子集(快速反馈)
|
||||
uv run pf pdftool # 验证多命令工具
|
||||
uv run pf wch echo # 验证单命令工具
|
||||
uv run pf emlman --help # 验证 legacy 工具路由
|
||||
uv run pxp --help # 验证 legacy 工具入口点
|
||||
```
|
||||
@@ -6,7 +6,7 @@
|
||||
[](https://pypi.org/project/pyflowx/)
|
||||
[](https://pypi.org/project/pyflowx/)
|
||||
[](https://pyflowx.readthedocs.io/zh/latest/)
|
||||
[](https://github.com/gookeryoung/pyflowx)
|
||||
[](https://github.com/gookeryoung/pyflowx)
|
||||
[](https://github.com/gookeryoung/pyflowx/blob/main/LICENSE)
|
||||
|
||||
PyFlowX 把"任务依赖"这件事做到极致简单:**参数名就是依赖声明**。无需装饰器、
|
||||
@@ -32,8 +32,11 @@ PyFlowX 把"任务依赖"这件事做到极致简单:**参数名就是依赖
|
||||
- **图级默认值** —— `GraphDefaults` 统一配置 retry/timeout/concurrency 等
|
||||
- **CLI 运行器** —— `CliRunner` 把多个图映射为命令行子命令,替代 Makefile
|
||||
- **可观测** —— `on_event` 回调(RUNNING/SUCCESS/FAILED/SKIPPED)、`dry_run` 预览、`verbose` 生命周期日志、Mermaid 可视化
|
||||
- **YAML 任务编排** —— GitHub Actions 风格的声明式任务图,支持 `jobs`/`needs`/`strategy.matrix`/`if` 等 CI/CD 概念,从 YAML 文件直接加载执行
|
||||
- **最小依赖** —— 仅依赖标准库 + PyYAML(3.8 需 `graphlib_backport`、`typing-extensions`)
|
||||
- **进度监控** —— `run(progress=True)` 开箱即用的 rich 进度条;`ProgressCallback` 协议供程序化集成
|
||||
- **任务通知** —— `Notifier` 协议支持飞书/钉钉/企业微信 webhook 与 Python 回调,全生命周期事件可配置
|
||||
- **流式获取** —— `run_iter()` 生成器逐个 yield 任务结果,适用于大量任务逐个处理
|
||||
- **状态后端** —— `MemoryBackend` / `JSONBackend` / `SQLiteBackend`(WAL 模式,适合大规模任务)
|
||||
- **最小依赖** —— `rich` + `typer` + `typing-extensions`(3.13 以下)
|
||||
- **97% 测试覆盖** —— 分支覆盖率 >= 95%
|
||||
|
||||
## 安装
|
||||
@@ -311,114 +314,6 @@ pf pymake --quiet # 静默模式
|
||||
|
||||
`verbose=True`(默认)时打印任务生命周期(开始/成功/失败/跳过)与命令输出;`--quiet` 关闭。
|
||||
|
||||
## YAML 任务编排
|
||||
|
||||
PyFlowX 支持 GitHub Actions 风格的声明式 YAML 任务编排,从 YAML 文件直接加载任务图。
|
||||
|
||||
### 编程式 API
|
||||
|
||||
```python
|
||||
import pyflowx as px
|
||||
|
||||
# 从 YAML 文件加载任务图
|
||||
graph = px.Graph.from_yaml("pipeline.yaml")
|
||||
report = px.run(graph, strategy="thread")
|
||||
|
||||
# 或用函数式 API
|
||||
graph = px.load_yaml("pipeline.yaml")
|
||||
graph = px.parse_yaml_string("""
|
||||
jobs:
|
||||
hello:
|
||||
cmd: ["echo", "hello"]
|
||||
""")
|
||||
```
|
||||
|
||||
### CLI 入口
|
||||
|
||||
通过 `pf` 统一入口调用(详见 [pf 工具](#cli-工具) 章节):
|
||||
|
||||
```bash
|
||||
# 执行 YAML 任务图
|
||||
pf yamlrun pipeline.yaml
|
||||
|
||||
# 指定执行策略
|
||||
pf yamlrun pipeline.yaml --strategy thread
|
||||
|
||||
# 仅打印任务分层,不执行
|
||||
pf yamlrun pipeline.yaml --dry-run
|
||||
|
||||
# 列出所有任务名
|
||||
pf yamlrun pipeline.yaml --list
|
||||
|
||||
# 静默模式
|
||||
pf yamlrun pipeline.yaml --quiet
|
||||
```
|
||||
|
||||
### YAML Schema(GitHub Actions 风格)
|
||||
|
||||
```yaml
|
||||
strategy: thread # 图级默认策略
|
||||
defaults: # 图级默认值
|
||||
retry: {max_attempts: 3}
|
||||
verbose: true
|
||||
env: {CI: "true"}
|
||||
|
||||
jobs:
|
||||
setup:
|
||||
cmd: ["git", "clone", "..."]
|
||||
runs-on: linux
|
||||
|
||||
build:
|
||||
needs: [setup] # 依赖列表
|
||||
cmd: ["python", "-m", "build"]
|
||||
timeout: 300
|
||||
retry: {max_attempts: 2, delay: 1.0}
|
||||
|
||||
test:
|
||||
needs: [build]
|
||||
cmd: ["python${{ matrix.version }}", "-m", "pytest"] # 矩阵占位符
|
||||
strategy:
|
||||
matrix: # 笛卡尔积展开为 6 个任务
|
||||
version: ["3.8", "3.9", "3.10"]
|
||||
os: ["linux", "macos"]
|
||||
if: "env.CI" # 条件: 环境变量存在
|
||||
|
||||
lint:
|
||||
needs: [build]
|
||||
cmd: ["ruff", "check"]
|
||||
if: "env.CI == 'true'" # 条件: 环境变量等于
|
||||
|
||||
deploy:
|
||||
needs: [test, lint] # 矩阵依赖自动展开
|
||||
cmd: ["twine", "upload"]
|
||||
if: "env.DEPLOY_TOKEN != ''"
|
||||
allow-upstream-skip: true
|
||||
concurrency-key: deploy_lock
|
||||
```
|
||||
|
||||
### 字段映射
|
||||
|
||||
| YAML 字段 | TaskSpec 字段 | 说明 |
|
||||
|-----------|---------------|------|
|
||||
| `jobs.<id>` | `name` | job ID 作为任务名 |
|
||||
| `cmd` / `run` | `cmd` | `cmd` 为列表形式,`run` 为 shell 字符串 |
|
||||
| `needs` | `depends_on` | 依赖列表(矩阵任务自动展开) |
|
||||
| `if` | `conditions` | `success()` / `always()` / `env.VAR` / `env.VAR == 'x'` |
|
||||
| `strategy.matrix` | 矩阵扇出 | 笛卡尔积展开为多个任务 |
|
||||
| `${{ matrix.key }}` | 占位符 | 在 cmd/run/cwd/env 中替换 |
|
||||
| `timeout` | `timeout` | 超时秒数 |
|
||||
| `retry` | `retry` | `{max_attempts, delay, backoff, jitter}` |
|
||||
| `cwd` | `cwd` | 工作目录 |
|
||||
| `env` | `env` | 环境变量 |
|
||||
| `verbose` | `verbose` | 详细输出 |
|
||||
| `continue-on-error` | `continue_on_error` | 失败不中止整图 |
|
||||
| `skip-if-missing` | `skip_if_missing` | 命令不存在时跳过 |
|
||||
| `allow-upstream-skip` | `allow_upstream_skip` | 上游跳过时仍执行 |
|
||||
| `priority` | `priority` | 同层优先级 |
|
||||
| `concurrency-key` | `concurrency_key` | 并发限制键 |
|
||||
| `tags` | `tags` | 自由标签 |
|
||||
| `runs-on` | `tags`(追加) | 运行环境标签 |
|
||||
|
||||
## 断点续跑
|
||||
|
||||
```python
|
||||
@@ -475,7 +370,7 @@ except px.PyFlowXError:
|
||||
| 特性 | PyFlowX | Airflow | Prefect | Dask |
|
||||
|------|---------|---------|---------|------|
|
||||
| 零样板 | 参数名即依赖 | 装饰器 + XCom | 装饰器 | 装饰器 |
|
||||
| 运行时依赖 | 仅标准库 | 重型 | 中型 | 中型 |
|
||||
| 运行时依赖 | rich + typer | 重型 | 中型 | 中型 |
|
||||
| 类型安全 | mypy strict | 弱 | 中 | 中 |
|
||||
| 异步原生 | 是 | 否 | 部分 | 否 |
|
||||
| 断点续跑 | 内置 | 需配置 | 需配置 | 需配置 |
|
||||
@@ -565,8 +460,8 @@ uv run ruff format --check src tests
|
||||
| 模块 | 职责 |
|
||||
|------|------|
|
||||
| `cli/pf.py` | 统一入口:`pf <tool> [command]`,按需导入 `ops/<tool>.py` 触发 `@px.tool` 注册 |
|
||||
| `cli/profiler.py` | 性能分析 CLI |
|
||||
| `cli/emlmanager.py` | 邮件管理 CLI |
|
||||
| `cli/legacy/profiler.py` | 性能分析 CLI(legacy) |
|
||||
| `cli/legacy/emlmanager.py` | 邮件管理 CLI(legacy) |
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
+1
-3
@@ -17,9 +17,8 @@ PyFlowX 是一个轻量、类型安全的 DAG 任务调度器:**参数名就
|
||||
- **断点续跑** —— ``MemoryBackend`` / ``JSONBackend``,成功结果可缓存复用
|
||||
- **命令任务** —— ``cmd`` 参数直接执行外部命令
|
||||
- **条件执行** —— ``conditions`` 按平台、环境变量等条件跳过任务
|
||||
- **YAML 任务编排** —— GitHub Actions 风格声明式任务图
|
||||
- **pf 统一 CLI** —— ``pf <tool> [command]`` 调用所有工具
|
||||
- **最小依赖** —— 仅依赖标准库 + PyYAML
|
||||
- **最小依赖** —— ``rich`` + ``typer`` + ``typing-extensions``(3.13 以下)
|
||||
|
||||
文档导航
|
||||
--------
|
||||
@@ -38,7 +37,6 @@ PyFlowX 是一个轻量、类型安全的 DAG 任务调度器:**参数名就
|
||||
guide/task
|
||||
guide/graph
|
||||
guide/execution
|
||||
guide/yaml
|
||||
guide/cli
|
||||
|
||||
.. toctree::
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
安装
|
||||
====
|
||||
|
||||
PyFlowX 支持 Python 3.8+,仅依赖标准库与 PyYAML(3.8 额外需要 ``graphlib_backport`` 和 ``typing-extensions``)。
|
||||
PyFlowX 支持 Python 3.10+,运行时依赖 ``rich``、``typer`` 与 ``typing-extensions``(3.13 以下)。
|
||||
|
||||
pip 安装
|
||||
--------
|
||||
|
||||
@@ -45,12 +45,14 @@ def test_notification_level_values() -> None:
|
||||
def test_all_levels_contains_all() -> None:
|
||||
"""ALL_LEVELS 包含全部四个级别。"""
|
||||
assert (
|
||||
frozenset({
|
||||
NotificationLevel.RUNNING,
|
||||
NotificationLevel.SUCCESS,
|
||||
NotificationLevel.FAILED,
|
||||
NotificationLevel.SKIPPED,
|
||||
})
|
||||
frozenset(
|
||||
{
|
||||
NotificationLevel.RUNNING,
|
||||
NotificationLevel.SUCCESS,
|
||||
NotificationLevel.FAILED,
|
||||
NotificationLevel.SKIPPED,
|
||||
}
|
||||
)
|
||||
== ALL_LEVELS
|
||||
)
|
||||
|
||||
|
||||
@@ -157,10 +157,12 @@ def _make_simple_graph() -> px.Graph:
|
||||
def task_b(task_a: int) -> int:
|
||||
return task_a + 1
|
||||
|
||||
return px.Graph.from_specs([
|
||||
px.TaskSpec("task_a", task_a),
|
||||
px.TaskSpec("task_b", task_b, depends_on=("task_a",)),
|
||||
])
|
||||
return px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec("task_a", task_a),
|
||||
px.TaskSpec("task_b", task_b, depends_on=("task_a",)),
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_run_with_progress_true() -> None:
|
||||
|
||||
+12
-8
@@ -128,10 +128,12 @@ def test_run_iter_failure_with_continue_on_error() -> None:
|
||||
def fail() -> None:
|
||||
raise ValueError("err")
|
||||
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec("fail", fn=fail, continue_on_error=True),
|
||||
px.TaskSpec("ok", fn=lambda: 1),
|
||||
])
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec("fail", fn=fail, continue_on_error=True),
|
||||
px.TaskSpec("ok", fn=lambda: 1),
|
||||
]
|
||||
)
|
||||
results = dict(px.run_iter(graph))
|
||||
assert results["fail"].status == TaskStatus.FAILED
|
||||
assert results["ok"].status == TaskStatus.SUCCESS
|
||||
@@ -144,10 +146,12 @@ def test_run_iter_yields_skipped_tasks() -> None:
|
||||
"""条件跳过的任务以 SKIPPED 状态 yield。"""
|
||||
never_true = lambda _ctx: False # noqa: E731
|
||||
|
||||
graph = px.Graph.from_specs([
|
||||
px.TaskSpec("normal", fn=lambda: 1),
|
||||
px.TaskSpec("skipped", fn=lambda: 2, conditions=(never_true,)),
|
||||
])
|
||||
graph = px.Graph.from_specs(
|
||||
[
|
||||
px.TaskSpec("normal", fn=lambda: 1),
|
||||
px.TaskSpec("skipped", fn=lambda: 2, conditions=(never_true,)),
|
||||
]
|
||||
)
|
||||
results = dict(px.run_iter(graph))
|
||||
assert results["normal"].status == TaskStatus.SUCCESS
|
||||
assert results["skipped"].status == TaskStatus.SKIPPED
|
||||
|
||||
Reference in New Issue
Block a user