celestialflow 3.0.5__tar.gz → 3.0.6__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. {celestialflow-3.0.5 → celestialflow-3.0.6}/PKG-INFO +32 -21
  2. {celestialflow-3.0.5 → celestialflow-3.0.6}/README.md +31 -20
  3. {celestialflow-3.0.5 → celestialflow-3.0.6}/pyproject.toml +1 -1
  4. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/__init__.py +3 -0
  5. celestialflow-3.0.6/src/celestialflow/adapters/__init__.py +0 -0
  6. celestialflow-3.0.6/src/celestialflow/adapters/celestialtree/__init__.py +2 -0
  7. celestialflow-3.0.6/src/celestialflow/adapters/celestialtree/client.py +184 -0
  8. celestialflow-3.0.6/src/celestialflow/adapters/celestialtree/tools.py +36 -0
  9. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_graph.py +46 -7
  10. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_logging.py +81 -15
  11. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_manage.py +256 -116
  12. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_queue.py +3 -3
  13. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_report.py +25 -36
  14. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_tools.py +14 -14
  15. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_types.py +26 -0
  16. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_web.py +42 -10
  17. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow.egg-info/PKG-INFO +32 -21
  18. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow.egg-info/SOURCES.txt +4 -0
  19. {celestialflow-3.0.5 → celestialflow-3.0.6}/tests/test_graph.py +16 -6
  20. celestialflow-3.0.6/tests/test_manage.py +119 -0
  21. {celestialflow-3.0.5 → celestialflow-3.0.6}/tests/test_nodes.py +9 -9
  22. celestialflow-3.0.5/tests/test_manage.py +0 -64
  23. {celestialflow-3.0.5 → celestialflow-3.0.6}/setup.cfg +0 -0
  24. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/css/base.css +0 -0
  25. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/css/dashboard.css +0 -0
  26. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/css/errors.css +0 -0
  27. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/css/inject.css +0 -0
  28. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/favicon.ico +0 -0
  29. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/js/main.js +0 -0
  30. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/js/task_errors.js +0 -0
  31. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/js/task_injection.js +0 -0
  32. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/js/task_statuses.js +0 -0
  33. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/js/task_structure.js +0 -0
  34. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/js/task_topology.js +0 -0
  35. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/static/js/utils.js +0 -0
  36. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_nodes.py +0 -0
  37. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_progress.py +0 -0
  38. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/task_structure.py +0 -0
  39. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow/templates/index.html +0 -0
  40. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow.egg-info/dependency_links.txt +0 -0
  41. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow.egg-info/entry_points.txt +0 -0
  42. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow.egg-info/requires.txt +0 -0
  43. {celestialflow-3.0.5 → celestialflow-3.0.6}/src/celestialflow.egg-info/top_level.txt +0 -0
  44. {celestialflow-3.0.5 → celestialflow-3.0.6}/tests/test_structure.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: celestialflow
3
- Version: 3.0.5
3
+ Version: 3.0.6
4
4
  Summary: A flexible GRAPH-based task orchestration framework.
5
5
  Author-email: Mr-xiaotian <mingxiaomingtian@gmail.com>
6
6
  License: MIT
@@ -36,10 +36,17 @@ Requires-Dist: jinja2
36
36
  <a href="https://pypi.org/project/celestialflow/"><img src="https://img.shields.io/pypi/pyversions/celestialflow.svg"></a>
37
37
  </p>
38
38
 
39
- **CelestialFlow** 是一个轻量级但功能完全体的任务流框架,适合需要“灵活拓扑 + 多执行模式 + 可视化监控”的中/大型 Python 任务系统。
39
+ <p align="center">
40
+ <img src="https://img.shields.io/badge/Task%20Graph-DAG-blueviolet">
41
+ <img src="https://img.shields.io/badge/Workflow-Orchestrator-7c3aed">
42
+ <img src="https://img.shields.io/badge/IPC-Redis%20Ready-red">
43
+ <img src="https://img.shields.io/badge/Distributed-Worker%20Friendly-orange">
44
+ </p>
45
+
46
+ **CelestialFlow** 是一个轻量级但功能完全的任务流框架,适合需要 **复杂依赖关系**、**灵活执行模型**、**跨设备运行**与**实时可视化监控** 的中/大型 Python 任务系统。
40
47
 
41
48
  - 相比 Airflow/Dagster 更轻、更快开始
42
- - 相比 multiprocessing/threading 更结构化,可直接表达 loop / complete graph 等复杂依赖
49
+ - 相比 multiprocessing/threading 更结构化,可直接表达 loop / complete graph 等复杂依赖模式
43
50
 
44
51
  框架的基本单元为 **TaskStage**(由 `TaskManager` 派生),每个 stage 内部绑定一个独立的执行函数,并支持四种运行模式:
45
52
 
@@ -48,16 +55,16 @@ Requires-Dist: jinja2
48
55
  * **多进程(process)**
49
56
  * **协程(async)**
50
57
 
51
- 每个 stage 均可独立运行,也可作为节点互相连接,形成具有上游与下游依赖关系的任务图(**TaskGraph**)。下游 stage 会自动接收上游执行完成的结果作为输入,从而实现数据的流动与传递。
58
+ 每个 stage 均可独立运行,也可作为节点互相连接,形成具有上游与下游依赖关系的任务图(**TaskGraph**)。下游 stage 会自动接收上游执行完成的结果作为输入,从而形成明确的数据流。
52
59
 
53
- 在图级别上,TaskGraph 支持两种布局模式:
60
+ 在图级别上,每个 Stage 支持两种上下文模式:
54
61
 
55
- * **线性执行(serial layout)**:前一节点执行完毕后再启动下一节点(下游节点可提前接收任务但不会立即执行)。
56
- * **并行执行(process layout)**:所有节点同时启动运行,由队列自动协调任务传递与依赖顺序。
62
+ * **线性执行(serial layout)**:当前节点执行完毕再启动下一节点(下游节点可提前接收任务但不会立即执行)。
63
+ * **并行执行(process layout)**:当前节点启动后立刻前去启动下一节点。
57
64
 
58
- TaskGraph 能构建完整的 **有向图结构(Directed Graph)**,不仅支持传统的有向无环图(DAG),也能灵活表达 **环形(loop)** **复杂交叉** 的任务依赖。
65
+ TaskGraph 能构建完整的 **有向图结构(Directed Graph)**,不仅支持传统的有向无环图(DAG),也能灵活表达 **树形(Tree)**、**环形(loop)** 乃至于 **完全图(Complete Graph)** 形式的任务依赖。
59
66
 
60
- 在此基础上项目支持 Web 可视化与通过 Redis 外接go代码,弥补 Python 在cpu密集任务上速度过慢的问题。
67
+ 在此基础上,CelestialFlow 支持 Web 可视化监控,并可通过 Redis 实现跨进程、跨设备协作;同时引入基于 Go 的外部 worker(通过 Redis 通信),用于承载 CPU 密集型任务,弥补 Python 在该场景下的性能瓶颈。
61
68
 
62
69
  ## 项目结构(Project Structure)
63
70
 
@@ -313,21 +320,25 @@ flowchart TD
313
320
 
314
321
  ## 更新日志(Change Log)
315
322
 
316
- - [2021] 建立一个支持多线程与单线程处理函数的类
317
- - [2023] 在GPT4帮助下添加多进程与携程运行模式
318
- - [5/9/2024] 将原有的处理类抽象为节点, 添加TaskChain类, 可以线性连接多个节点, 并设定节点在Chain中的运行模式, 支持serial和process两种, 后者Chain所有节点同时运行
319
- - [12/12/2024-12/16/2024] 在原有链式结构基础上允许节点有复数下级节点, 实现Tree结构; 将原有TaskChain改名为TaskTree
320
- - [3/16/2025] 支持Web端任务完成情况可视化
321
- - [6/9/2025] 支持节点拥有复数上级节点, 脱离纯Tree结构, 为之后循环图做准备
322
- - [6/11/2025] 自[CelestialVault](https://github.com/Mr-xiaotian/CelestialVault)项目instances.inst_task迁入
323
- - [6/12/2025] 支持循环图, 下级节点可指向上级节点
324
- - [6/13/2025] 支持loop结构, 即节点可指向自己
325
- - [6/14/2025] 支持forest结构, 即可有多个根节点
326
- - [6/16/2025] 多轮评测后, 当前框架已支持完整有向图结构, 故将TaskTree改名为TaskGraph
323
+ - 2021: 建立一个支持多线程与单线程处理函数的类
324
+ - 2023: 在GPT4帮助下添加多进程与携程运行模式
325
+ - 5/9/2024: 将原有的处理类抽象为节点, 添加TaskChain类, 可以线性连接多个节点, 并设定节点在Chain中的运行模式, 支持serial和process两种, 后者Chain所有节点同时运行
326
+ - 12/12/2024-12/16/2024: 在原有链式结构基础上允许节点有复数下级节点, 实现Tree结构; 将原有TaskChain改名为TaskTree
327
+ - 3/16/2025: 支持Web端任务完成情况可视化
328
+ - 6/9/2025: 支持节点拥有复数上级节点, 脱离纯Tree结构, 为之后循环图做准备
329
+ - 6/11/2025: 自[CelestialVault](https://github.com/Mr-xiaotian/CelestialVault)项目instances.inst_task迁入
330
+ - 6/12/2025: 支持循环图, 下级节点可指向上级节点
331
+ - 6/13/2025: 支持loop结构, 即节点可指向自己
332
+ - 6/14/2025: 支持forest结构, 即可有多个根节点
333
+ - 6/16/2025: 多轮评测后, 当前框架已支持完整有向图结构, TaskTree改名为TaskGraph
334
+ - 3.0.1: 上线Pypi, 可喜可贺
335
+ - 3.0.4: 新增一个抽象结构TaskQueue, 用于表示节点的所有"入边"与"出边"; 恢复未消费任务的保存功能
336
+ - 3.0.5: 删除原有的TaskRedisTransfer节点, 并增添三种新的redis交互节点TaskRedisSink TaskRedisSource TaskRedisAck, 用于跨语言 跨进程 跨设备处理任务; 并在Web页面添加展示拓扑信息的卡片
337
+ - 3.0.6: 添加对[CelestialTree](https://github.com/Mr-xiaotian/CelestialTree)系统的支持, 现在可以追踪单个任务的流向
327
338
 
328
339
  ## Star 历史趋势(Star History)
329
340
 
330
- 如果对项目感兴趣的话,还请star
341
+ 如果对项目感兴趣的话,欢迎star。如果有问题或者建议的话, 欢迎提交[Issues](https://github.com/Mr-xiaotian/CelestialFlow/issues)或者在[Discussion](https://github.com/Mr-xiaotian/CelestialFlow/discussions)中告诉我。
331
342
 
332
343
  [![Star History Chart](https://api.star-history.com/svg?repos=Mr-xiaotian/CelestialFlow&type=Date)](https://star-history.com/#Mr-xiaotian/CelestialFlow&Date)
333
344
 
@@ -11,10 +11,17 @@
11
11
  <a href="https://pypi.org/project/celestialflow/"><img src="https://img.shields.io/pypi/pyversions/celestialflow.svg"></a>
12
12
  </p>
13
13
 
14
- **CelestialFlow** 是一个轻量级但功能完全体的任务流框架,适合需要“灵活拓扑 + 多执行模式 + 可视化监控”的中/大型 Python 任务系统。
14
+ <p align="center">
15
+ <img src="https://img.shields.io/badge/Task%20Graph-DAG-blueviolet">
16
+ <img src="https://img.shields.io/badge/Workflow-Orchestrator-7c3aed">
17
+ <img src="https://img.shields.io/badge/IPC-Redis%20Ready-red">
18
+ <img src="https://img.shields.io/badge/Distributed-Worker%20Friendly-orange">
19
+ </p>
20
+
21
+ **CelestialFlow** 是一个轻量级但功能完全的任务流框架,适合需要 **复杂依赖关系**、**灵活执行模型**、**跨设备运行**与**实时可视化监控** 的中/大型 Python 任务系统。
15
22
 
16
23
  - 相比 Airflow/Dagster 更轻、更快开始
17
- - 相比 multiprocessing/threading 更结构化,可直接表达 loop / complete graph 等复杂依赖
24
+ - 相比 multiprocessing/threading 更结构化,可直接表达 loop / complete graph 等复杂依赖模式
18
25
 
19
26
  框架的基本单元为 **TaskStage**(由 `TaskManager` 派生),每个 stage 内部绑定一个独立的执行函数,并支持四种运行模式:
20
27
 
@@ -23,16 +30,16 @@
23
30
  * **多进程(process)**
24
31
  * **协程(async)**
25
32
 
26
- 每个 stage 均可独立运行,也可作为节点互相连接,形成具有上游与下游依赖关系的任务图(**TaskGraph**)。下游 stage 会自动接收上游执行完成的结果作为输入,从而实现数据的流动与传递。
33
+ 每个 stage 均可独立运行,也可作为节点互相连接,形成具有上游与下游依赖关系的任务图(**TaskGraph**)。下游 stage 会自动接收上游执行完成的结果作为输入,从而形成明确的数据流。
27
34
 
28
- 在图级别上,TaskGraph 支持两种布局模式:
35
+ 在图级别上,每个 Stage 支持两种上下文模式:
29
36
 
30
- * **线性执行(serial layout)**:前一节点执行完毕后再启动下一节点(下游节点可提前接收任务但不会立即执行)。
31
- * **并行执行(process layout)**:所有节点同时启动运行,由队列自动协调任务传递与依赖顺序。
37
+ * **线性执行(serial layout)**:当前节点执行完毕再启动下一节点(下游节点可提前接收任务但不会立即执行)。
38
+ * **并行执行(process layout)**:当前节点启动后立刻前去启动下一节点。
32
39
 
33
- TaskGraph 能构建完整的 **有向图结构(Directed Graph)**,不仅支持传统的有向无环图(DAG),也能灵活表达 **环形(loop)** **复杂交叉** 的任务依赖。
40
+ TaskGraph 能构建完整的 **有向图结构(Directed Graph)**,不仅支持传统的有向无环图(DAG),也能灵活表达 **树形(Tree)**、**环形(loop)** 乃至于 **完全图(Complete Graph)** 形式的任务依赖。
34
41
 
35
- 在此基础上项目支持 Web 可视化与通过 Redis 外接go代码,弥补 Python 在cpu密集任务上速度过慢的问题。
42
+ 在此基础上,CelestialFlow 支持 Web 可视化监控,并可通过 Redis 实现跨进程、跨设备协作;同时引入基于 Go 的外部 worker(通过 Redis 通信),用于承载 CPU 密集型任务,弥补 Python 在该场景下的性能瓶颈。
36
43
 
37
44
  ## 项目结构(Project Structure)
38
45
 
@@ -288,21 +295,25 @@ flowchart TD
288
295
 
289
296
  ## 更新日志(Change Log)
290
297
 
291
- - [2021] 建立一个支持多线程与单线程处理函数的类
292
- - [2023] 在GPT4帮助下添加多进程与携程运行模式
293
- - [5/9/2024] 将原有的处理类抽象为节点, 添加TaskChain类, 可以线性连接多个节点, 并设定节点在Chain中的运行模式, 支持serial和process两种, 后者Chain所有节点同时运行
294
- - [12/12/2024-12/16/2024] 在原有链式结构基础上允许节点有复数下级节点, 实现Tree结构; 将原有TaskChain改名为TaskTree
295
- - [3/16/2025] 支持Web端任务完成情况可视化
296
- - [6/9/2025] 支持节点拥有复数上级节点, 脱离纯Tree结构, 为之后循环图做准备
297
- - [6/11/2025] 自[CelestialVault](https://github.com/Mr-xiaotian/CelestialVault)项目instances.inst_task迁入
298
- - [6/12/2025] 支持循环图, 下级节点可指向上级节点
299
- - [6/13/2025] 支持loop结构, 即节点可指向自己
300
- - [6/14/2025] 支持forest结构, 即可有多个根节点
301
- - [6/16/2025] 多轮评测后, 当前框架已支持完整有向图结构, 故将TaskTree改名为TaskGraph
298
+ - 2021: 建立一个支持多线程与单线程处理函数的类
299
+ - 2023: 在GPT4帮助下添加多进程与携程运行模式
300
+ - 5/9/2024: 将原有的处理类抽象为节点, 添加TaskChain类, 可以线性连接多个节点, 并设定节点在Chain中的运行模式, 支持serial和process两种, 后者Chain所有节点同时运行
301
+ - 12/12/2024-12/16/2024: 在原有链式结构基础上允许节点有复数下级节点, 实现Tree结构; 将原有TaskChain改名为TaskTree
302
+ - 3/16/2025: 支持Web端任务完成情况可视化
303
+ - 6/9/2025: 支持节点拥有复数上级节点, 脱离纯Tree结构, 为之后循环图做准备
304
+ - 6/11/2025: 自[CelestialVault](https://github.com/Mr-xiaotian/CelestialVault)项目instances.inst_task迁入
305
+ - 6/12/2025: 支持循环图, 下级节点可指向上级节点
306
+ - 6/13/2025: 支持loop结构, 即节点可指向自己
307
+ - 6/14/2025: 支持forest结构, 即可有多个根节点
308
+ - 6/16/2025: 多轮评测后, 当前框架已支持完整有向图结构, TaskTree改名为TaskGraph
309
+ - 3.0.1: 上线Pypi, 可喜可贺
310
+ - 3.0.4: 新增一个抽象结构TaskQueue, 用于表示节点的所有"入边"与"出边"; 恢复未消费任务的保存功能
311
+ - 3.0.5: 删除原有的TaskRedisTransfer节点, 并增添三种新的redis交互节点TaskRedisSink TaskRedisSource TaskRedisAck, 用于跨语言 跨进程 跨设备处理任务; 并在Web页面添加展示拓扑信息的卡片
312
+ - 3.0.6: 添加对[CelestialTree](https://github.com/Mr-xiaotian/CelestialTree)系统的支持, 现在可以追踪单个任务的流向
302
313
 
303
314
  ## Star 历史趋势(Star History)
304
315
 
305
- 如果对项目感兴趣的话,还请star
316
+ 如果对项目感兴趣的话,欢迎star。如果有问题或者建议的话, 欢迎提交[Issues](https://github.com/Mr-xiaotian/CelestialFlow/issues)或者在[Discussion](https://github.com/Mr-xiaotian/CelestialFlow/discussions)中告诉我。
306
317
 
307
318
  [![Star History Chart](https://api.star-history.com/svg?repos=Mr-xiaotian/CelestialFlow&type=Date)](https://star-history.com/#Mr-xiaotian/CelestialFlow&Date)
308
319
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "celestialflow"
7
- version = "3.0.5"
7
+ version = "3.0.6"
8
8
  description = "A flexible GRAPH-based task orchestration framework."
9
9
  readme = "README.md"
10
10
  license = { text = "MIT" }
@@ -22,6 +22,7 @@ from .task_tools import (
22
22
  format_table,
23
23
  )
24
24
  from .task_web import TaskWebServer
25
+ from .adapters.celestialtree import Client as CelestialTreeClient, format_tree_root
25
26
 
26
27
  __all__ = [
27
28
  "TaskGraph",
@@ -38,6 +39,8 @@ __all__ = [
38
39
  "TaskRedisAck",
39
40
  "TerminationSignal",
40
41
  "TaskWebServer",
42
+ "CelestialTreeClient",
43
+ "format_tree_root",
41
44
  "load_task_by_stage",
42
45
  "load_task_by_error",
43
46
  "make_hashable",
@@ -0,0 +1,2 @@
1
+ from .client import Client, NullClient
2
+ from .tools import format_tree_root
@@ -0,0 +1,184 @@
1
+ import json
2
+ import threading
3
+ import requests
4
+ from typing import List, Optional, Dict, Any, Callable
5
+
6
+
7
+ class Client:
8
+ """
9
+ Python client for CelestialTree HTTP API.
10
+ """
11
+
12
+ def __init__(self, base_url: str, timeout: float = 5.0):
13
+ self.base_url = base_url.rstrip("/")
14
+ self.timeout = timeout
15
+
16
+ def init_session(self):
17
+ if hasattr(self, "session"):
18
+ return
19
+
20
+ self.session = requests.Session()
21
+ self.session.headers.update(
22
+ {
23
+ "Content-Type": "application/json",
24
+ "Accept": "application/json",
25
+ }
26
+ )
27
+
28
+ # ---------- Core APIs ----------
29
+
30
+ def emit(
31
+ self,
32
+ type_: str,
33
+ parents: Optional[List[int]] = None,
34
+ message: Optional[str] = None,
35
+ payload: Optional[bytes | dict] = None,
36
+ ) -> int:
37
+ """
38
+ Emit a new event into CelestialTree.
39
+ """
40
+ self.init_session()
41
+
42
+ body = {
43
+ "type": type_,
44
+ "parents": parents or [],
45
+ }
46
+
47
+ if message is not None:
48
+ body["message"] = message
49
+
50
+ if payload is not None:
51
+ if isinstance(payload, (dict, list)):
52
+ body["payload"] = json.dumps(payload).encode("utf-8")
53
+ elif isinstance(payload, (bytes, bytearray)):
54
+ body["payload"] = payload
55
+ else:
56
+ raise TypeError("payload must be bytes or dict")
57
+
58
+ r = self.session.post(
59
+ f"{self.base_url}/emit",
60
+ data=json.dumps(body),
61
+ timeout=self.timeout,
62
+ )
63
+ r.raise_for_status()
64
+ return r.json()["id"]
65
+
66
+ def get_event(self, event_id: int) -> Dict[str, Any]:
67
+ self.init_session()
68
+
69
+ r = self.session.get(
70
+ f"{self.base_url}/event/{event_id}",
71
+ timeout=self.timeout,
72
+ )
73
+ r.raise_for_status()
74
+ return r.json()
75
+
76
+ def children(self, event_id: int) -> List[int]:
77
+ self.init_session()
78
+
79
+ r = self.session.get(
80
+ f"{self.base_url}/children/{event_id}",
81
+ timeout=self.timeout,
82
+ )
83
+ r.raise_for_status()
84
+ return r.json()["children"]
85
+
86
+ def descendants(self, event_id: int) -> Dict[str, Any]:
87
+ self.init_session()
88
+
89
+ r = self.session.get(
90
+ f"{self.base_url}/descendants/{event_id}",
91
+ timeout=self.timeout,
92
+ )
93
+ r.raise_for_status()
94
+ return r.json()
95
+
96
+ def heads(self) -> List[int]:
97
+ self.init_session()
98
+
99
+ r = self.session.get(
100
+ f"{self.base_url}/heads",
101
+ timeout=self.timeout,
102
+ )
103
+ r.raise_for_status()
104
+ return r.json()["heads"]
105
+
106
+ def health(self) -> bool:
107
+ self.init_session()
108
+
109
+ r = self.session.get(
110
+ f"{self.base_url}/healthz",
111
+ timeout=self.timeout,
112
+ )
113
+ return r.status_code == 200
114
+
115
+ def version(self) -> Dict[str, Any]:
116
+ self.init_session()
117
+
118
+ r = self.session.get(
119
+ f"{self.base_url}/version",
120
+ timeout=self.timeout,
121
+ )
122
+ r.raise_for_status()
123
+ return r.json()
124
+
125
+ # ---------- SSE Subscribe ----------
126
+
127
+ def subscribe(
128
+ self,
129
+ on_event: Callable[[Dict[str, Any]], None],
130
+ daemon: bool = True,
131
+ ) -> threading.Thread:
132
+ """
133
+ Subscribe to SSE stream.
134
+ on_event will be called for each emitted Event.
135
+ """
136
+
137
+ def _run():
138
+ with self.session.get(
139
+ f"{self.base_url}/subscribe",
140
+ stream=True,
141
+ timeout=None,
142
+ ) as r:
143
+ r.raise_for_status()
144
+ buf = ""
145
+ for line in r.iter_lines(decode_unicode=True):
146
+ if not line:
147
+ continue
148
+
149
+ if line.startswith("data:"):
150
+ data = line[len("data:") :].strip()
151
+ try:
152
+ ev = json.loads(data)
153
+ on_event(ev)
154
+ except Exception:
155
+ pass
156
+
157
+ self.init_session()
158
+
159
+ t = threading.Thread(target=_run, daemon=daemon)
160
+ t.start()
161
+ return t
162
+
163
+
164
+ class NullClient:
165
+ event_id = 0
166
+
167
+ def emit(self, *args, **kwargs):
168
+ self.event_id += 1
169
+ return self.event_id
170
+
171
+ def get_event(self, *args, **kwargs):
172
+ return None
173
+
174
+ def children(self, *args, **kwargs):
175
+ return []
176
+
177
+ def descendants(self, *args, **kwargs):
178
+ return None
179
+
180
+ def heads(self):
181
+ return []
182
+
183
+ def subscribe(self, *args, **kwargs):
184
+ return None
@@ -0,0 +1,36 @@
1
+ def format_tree(node: dict, prefix: str = "", is_last: bool = True) -> str:
2
+ """
3
+ 将 {"id": x, "children": [...]} 结构格式化为树状文本。
4
+
5
+ :param node: 当前节点
6
+ :param prefix: 前缀(递归用)
7
+ :param is_last: 是否是同级最后一个节点
8
+ :return: 树状字符串
9
+ """
10
+ lines = []
11
+
12
+ connector = "└── " if is_last else "├── "
13
+ lines.append(f"{prefix}{connector}{node['id']}")
14
+
15
+ children = node.get("children", [])
16
+ if children:
17
+ next_prefix = prefix + (" " if is_last else "│ ")
18
+ for i, child in enumerate(children):
19
+ last = i == len(children) - 1
20
+ lines.append(format_tree(child, next_prefix, last))
21
+
22
+ return "\n".join(lines)
23
+
24
+
25
+ def format_tree_root(tree: dict) -> str:
26
+ """
27
+ 格式化整棵树(根节点无连接符)
28
+ """
29
+ lines = [str(tree["id"])]
30
+
31
+ children = tree.get("children", [])
32
+ for i, child in enumerate(children):
33
+ last = i == len(children) - 1
34
+ lines.append(format_tree(child, "", last))
35
+
36
+ return "\n".join(lines)
@@ -6,15 +6,14 @@ from multiprocessing import Queue as MPQueue
6
6
  from typing import Any, Dict, List
7
7
 
8
8
  from .task_manage import TaskManager
9
- from .task_report import TaskReporter
9
+ from .task_report import TaskReporter, NullTaskReporter
10
10
  from .task_logging import LogListener, TaskLogger
11
11
  from .task_queue import TaskQueue
12
- from .task_types import StageStatus, TerminationSignal, TERMINATION_SIGNAL
12
+ from .task_types import TaskEnvelope, StageStatus, TerminationSignal, TERMINATION_SIGNAL
13
13
  from .task_tools import (
14
14
  format_duration,
15
15
  format_timestamp,
16
16
  cleanup_mpqueue,
17
- make_hashable,
18
17
  build_structure_graph,
19
18
  format_structure_list_from_graph,
20
19
  append_jsonl_log,
@@ -26,6 +25,10 @@ from .task_tools import (
26
25
  load_task_by_error,
27
26
  format_repr,
28
27
  )
28
+ from .adapters.celestialtree import (
29
+ Client as CelestialTreeClient,
30
+ NullClient as NullCelestialTreeClient,
31
+ )
29
32
 
30
33
 
31
34
  class TaskGraph:
@@ -61,6 +64,7 @@ class TaskGraph:
61
64
  self.analyze_graph()
62
65
  self.set_layout_mode(layout_mode)
63
66
  self.set_reporter()
67
+ self.set_ctree()
64
68
 
65
69
  def init_env(self):
66
70
  """
@@ -188,8 +192,30 @@ class TaskGraph:
188
192
  :param host: 报告器主机地址
189
193
  :param port: 报告器端口
190
194
  """
191
- self.is_report = is_report
192
- self.reporter = TaskReporter(self, self.log_listener.get_queue(), host, port)
195
+ if is_report:
196
+ self.reporter = TaskReporter(
197
+ self, self.log_listener.get_queue(), host, port
198
+ )
199
+ else:
200
+ self.reporter = NullTaskReporter()
201
+
202
+ def set_ctree(self, use_ctree=False, host="127.0.0.1", port=7777):
203
+ """
204
+ 设定事件树客户端
205
+
206
+ :param use_ctree: 是否使用事件树
207
+ :param host: 事件树主机地址
208
+ :param port: 事件树端口
209
+ """
210
+ self._use_ctree = use_ctree
211
+ self._ctree_host = host
212
+ self._ctree_port = port
213
+
214
+ if use_ctree:
215
+ base_url = f"http://{host}:{port}"
216
+ self.ctree_client = CelestialTreeClient(base_url)
217
+ else:
218
+ self.ctree_client = NullCelestialTreeClient()
193
219
 
194
220
  def set_graph_mode(self, stage_mode: str, execution_mode: str):
195
221
  """
@@ -230,8 +256,18 @@ class TaskGraph:
230
256
  in_queue.put(TERMINATION_SIGNAL)
231
257
  continue
232
258
 
233
- in_queue.put_first(make_hashable(task))
259
+ task_id = self.ctree_client.emit(
260
+ "task.input", message=f"In '{stage.get_stage_tag()}'"
261
+ )
262
+ envelope = TaskEnvelope.wrap(task, task_id)
263
+ in_queue.put_first(envelope)
234
264
  stage.task_counter.add_init_value(1)
265
+ self.task_logger.task_inject(
266
+ stage.get_func_name(),
267
+ stage.get_task_info(task),
268
+ stage.get_stage_tag(),
269
+ f"[{task_id}]",
270
+ )
235
271
 
236
272
  if put_termination_signal:
237
273
  for root_stage in self.root_stages:
@@ -253,7 +289,7 @@ class TaskGraph:
253
289
  self.start_time = time.time()
254
290
  self.task_logger.start_graph(self.get_structure_list())
255
291
  self._persist_structure_metadata()
256
- self.reporter.start() if self.is_report else None
292
+ self.reporter.start()
257
293
 
258
294
  self.put_stage_queue(init_tasks_dict, put_termination_signal)
259
295
  self._excute_stages()
@@ -318,6 +354,9 @@ class TaskGraph:
318
354
  self.stages_status_dict[stage_tag]["status"] = StageStatus.RUNNING
319
355
  self.stages_status_dict[stage_tag]["start_time"] = time.time()
320
356
 
357
+ if self._use_ctree:
358
+ stage.set_ctree(self._ctree_host, self._ctree_port)
359
+
321
360
  if stage.stage_mode == "process":
322
361
  p = multiprocessing.Process(
323
362
  target=stage.start_stage,