stageflow-framework 0.9.0__tar.gz → 0.11.0__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 (72) hide show
  1. stageflow_framework-0.11.0/PKG-INFO +186 -0
  2. stageflow_framework-0.11.0/README.md +153 -0
  3. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/pyproject.toml +5 -1
  4. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/__init__.py +5 -0
  5. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/builtins/_args.py +4 -4
  6. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/builtins/lists.py +5 -5
  7. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/builtins/strings.py +4 -4
  8. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/builtins/vars.py +1 -1
  9. stageflow_framework-0.11.0/stageflow/capabilities.py +56 -0
  10. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/__init__.py +2 -0
  11. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/cel.py +4 -4
  12. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/context.py +2 -2
  13. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/debug.py +1 -1
  14. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/__init__.py +2 -0
  15. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/base.py +4 -4
  16. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/bindings.py +3 -3
  17. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/branching.py +5 -5
  18. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/entry.py +6 -6
  19. stageflow_framework-0.11.0/stageflow/core/nodes/map_block.py +269 -0
  20. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/parallel.py +7 -7
  21. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/stage.py +14 -14
  22. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/subpipeline.py +6 -6
  23. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/try_block.py +44 -11
  24. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/pipeline.py +7 -7
  25. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/session.py +4 -1
  26. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/typesys.py +16 -16
  27. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/docs/schemas/pipeline.json +111 -12
  28. stageflow_framework-0.11.0/stageflow_framework.egg-info/PKG-INFO +186 -0
  29. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow_framework.egg-info/SOURCES.txt +4 -0
  30. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow_framework.egg-info/requires.txt +4 -0
  31. stageflow_framework-0.11.0/tests/test_capabilities.py +42 -0
  32. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_core_flow.py +6 -6
  33. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_debug.py +10 -10
  34. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_entry_node.py +28 -28
  35. stageflow_framework-0.11.0/tests/test_map.py +405 -0
  36. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_removed_local_scope.py +2 -2
  37. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_std_stages.py +9 -9
  38. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_try_block.py +60 -2
  39. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_typesystem.py +3 -3
  40. stageflow_framework-0.9.0/PKG-INFO +0 -478
  41. stageflow_framework-0.9.0/README.md +0 -448
  42. stageflow_framework-0.9.0/stageflow_framework.egg-info/PKG-INFO +0 -478
  43. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/LICENSE +0 -0
  44. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/setup.cfg +0 -0
  45. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/builtins/__init__.py +0 -0
  46. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/builtins/dicts.py +0 -0
  47. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/builtins/logic.py +0 -0
  48. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/event.py +0 -0
  49. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/inputs.py +0 -0
  50. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/recovery.py +0 -0
  51. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/nodes/terminal.py +0 -0
  52. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/payload_schema.py +0 -0
  53. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/registry.py +0 -0
  54. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/spec.py +0 -0
  55. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/core/stage.py +0 -0
  56. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/docs/__init__.py +0 -0
  57. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/docs/schema.py +0 -0
  58. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/exceptions.py +0 -0
  59. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/py.typed +0 -0
  60. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow/testing.py +0 -0
  61. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow_framework.egg-info/dependency_links.txt +0 -0
  62. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/stageflow_framework.egg-info/top_level.txt +0 -0
  63. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_docs_schema.py +0 -0
  64. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_full_pipeline.py +0 -0
  65. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_packaging.py +0 -0
  66. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_parallel.py +0 -0
  67. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_payload_validation.py +0 -0
  68. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_pipeline_tester.py +0 -0
  69. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_session_control.py +0 -0
  70. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_session_wait_input.py +0 -0
  71. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_snapshot.py +0 -0
  72. {stageflow_framework-0.9.0 → stageflow_framework-0.11.0}/tests/test_subpipeline.py +0 -0
@@ -0,0 +1,186 @@
1
+ Metadata-Version: 2.4
2
+ Name: stageflow-framework
3
+ Version: 0.11.0
4
+ Summary: StageFlow: pipeline framework for stages
5
+ Author-email: лень <pzrnqt1vrss@protonmail.com>
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/leo-need-more-coffee/stageflow
8
+ Project-URL: Issues, https://github.com/leo-need-more-coffee/stageflow/issues
9
+ Keywords: pipeline,workflow,orchestration,state-machine,cel
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Programming Language :: Python :: 3.14
16
+ Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: >=3.11
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: pyyaml
22
+ Requires-Dist: jsonschema>=4.0.0
23
+ Requires-Dist: immutables>=0.20
24
+ Requires-Dist: common-expression-language>=0.9
25
+ Provides-Extra: dev
26
+ Requires-Dist: flake8; extra == "dev"
27
+ Requires-Dist: build; extra == "dev"
28
+ Requires-Dist: twine; extra == "dev"
29
+ Provides-Extra: docs
30
+ Requires-Dist: mkdocs-material; extra == "docs"
31
+ Requires-Dist: mkdocs-static-i18n; extra == "docs"
32
+ Dynamic: license-file
33
+
34
+ <div align="center">
35
+
36
+ # StageFlow
37
+
38
+ **Pipelines described in JSON, executed in Python — and debuggable node by node.**
39
+
40
+ [![tests](https://github.com/leo-need-more-coffee/stageflow/actions/workflows/tests.yml/badge.svg)](https://github.com/leo-need-more-coffee/stageflow/actions/workflows/tests.yml)
41
+ [![PyPI](https://img.shields.io/pypi/v/stageflow-framework)](https://pypi.org/project/stageflow-framework/)
42
+ [![Python](https://img.shields.io/pypi/pyversions/stageflow-framework)](https://pypi.org/project/stageflow-framework/)
43
+ [![docs](https://img.shields.io/badge/docs-stageflow-blue)](https://leo-need-more-coffee.github.io/stageflow/)
44
+ [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
45
+
46
+ [Documentation](https://leo-need-more-coffee.github.io/stageflow/) ·
47
+ [Tutorial](https://leo-need-more-coffee.github.io/stageflow/tutorial/) ·
48
+ [Документация на русском](https://leo-need-more-coffee.github.io/stageflow/ru/)
49
+
50
+ </div>
51
+
52
+ A pipeline is a graph of nodes in JSON. The work happens in stages — Python
53
+ classes you write. Between them travels one immutable frame of variables, and
54
+ everything the graph does with that frame is visible in the JSON: branching,
55
+ retries, error handling, concurrency, nested graphs.
56
+
57
+ Because the pipeline is data, it can be stored, diffed, generated, validated
58
+ before it runs — and drawn:
59
+
60
+ ![A pipeline open in the StageFlow editor](docs/img/tut-final.png)
61
+
62
+ That is the [editor](https://github.com/leo-need-more-coffee/stageflow-ui): a
63
+ separate static page that draws and debugs a graph while this core executes it.
64
+
65
+ ## Install
66
+
67
+ ```bash
68
+ pip install stageflow-framework
69
+ ```
70
+
71
+ Python 3.11+.
72
+
73
+ ## A pipeline in 30 seconds
74
+
75
+ ```python
76
+ import asyncio
77
+
78
+ from stageflow import BaseStage, Pipeline, Session, register_stage
79
+
80
+
81
+ @register_stage("HelloStage")
82
+ class HelloStage(BaseStage):
83
+ """
84
+ description: "Greets whoever the pipeline points at"
85
+ arguments:
86
+ name: string
87
+ outputs:
88
+ greeting: string
89
+ """
90
+
91
+ async def run(self):
92
+ name = self.get_arguments().get("name", "world")
93
+ self.set_outputs({"greeting": f"Hello, {name}!"})
94
+
95
+
96
+ pipeline = Pipeline.from_dict({
97
+ "nodes": [
98
+ {"id": "start", "type": "entry",
99
+ "variables": {"user_name": "Alice"}, "next": "hello"},
100
+ {"id": "hello", "type": "stage", "stage": "HelloStage",
101
+ "arguments": {"vars": {"name": "user_name"}},
102
+ "outputs": {"greeting": "greeting"}, "next": "finish"},
103
+ {"id": "finish", "type": "terminal",
104
+ "result": {"status": "ok"}, "artifacts": ["greeting"]},
105
+ ],
106
+ })
107
+ pipeline.validate()
108
+
109
+ result = asyncio.run(Session(id="demo", pipeline=pipeline).run())
110
+ print(result.result) # {'status': 'ok'}
111
+ print(result.artifacts) # {'greeting': 'Hello, Alice!'}
112
+ ```
113
+
114
+ The docstring is the stage's specification. `validate()` checks the graph
115
+ against it, so a wrong argument name is an error before anything runs.
116
+
117
+ ## What the graph can do
118
+
119
+ | | |
120
+ |---|---|
121
+ | **Nine node types** | `entry`, `stage`, `condition`, `switch`, `parallel`, `try`, `map`, `subpipeline`, `terminal` |
122
+ | **One immutable frame** | variables travel along the path; a write produces a new frame, so branches never collide |
123
+ | **CEL expressions** | in conditions, in switch cases, and in any argument or output through the `.$` suffix |
124
+ | **Errors as roads** | `retry` on a node, `try`/`except` over a region of the graph derived from its shape |
125
+ | **Real concurrency** | `parallel` branches with their own frames and explicit merge rules |
126
+ | **Loops over data** | `map` runs a region of the graph once per element, sequentially or at once |
127
+ | **Nested graphs** | a `subpipeline` starts with a fresh frame and returns artifacts |
128
+ | **Gradual typing** | declare the variables that matter; checked at validation and on every write |
129
+ | **Step debugging** | stop between nodes, read and edit the frame, replay the event stream |
130
+
131
+ ## Documentation
132
+
133
+ The [tutorial](https://leo-need-more-coffee.github.io/stageflow/tutorial/)
134
+ builds one working pipeline step by step, with screenshots from the editor.
135
+ The reference covers the rest:
136
+
137
+ [Quick start](https://leo-need-more-coffee.github.io/stageflow/quick-start/) ·
138
+ [Node types](https://leo-need-more-coffee.github.io/stageflow/node-types/) ·
139
+ [Data model](https://leo-need-more-coffee.github.io/stageflow/data-model/) ·
140
+ [Expressions](https://leo-need-more-coffee.github.io/stageflow/expressions/) ·
141
+ [Errors](https://leo-need-more-coffee.github.io/stageflow/errors/) ·
142
+ [Variable typing](https://leo-need-more-coffee.github.io/stageflow/variable-typing/) ·
143
+ [Session control](https://leo-need-more-coffee.github.io/stageflow/session-control/) ·
144
+ [Step debugging](https://leo-need-more-coffee.github.io/stageflow/step-debugging/)
145
+
146
+ One page per node type:
147
+ [entry](https://leo-need-more-coffee.github.io/stageflow/entry-node/) ·
148
+ [stage](https://leo-need-more-coffee.github.io/stageflow/stage-node/) ·
149
+ [condition](https://leo-need-more-coffee.github.io/stageflow/condition-node/) ·
150
+ [switch](https://leo-need-more-coffee.github.io/stageflow/switch-node/) ·
151
+ [parallel](https://leo-need-more-coffee.github.io/stageflow/parallel-branches/) ·
152
+ [try](https://leo-need-more-coffee.github.io/stageflow/try-node/) ·
153
+ [map](https://leo-need-more-coffee.github.io/stageflow/map-node/) ·
154
+ [subpipeline](https://leo-need-more-coffee.github.io/stageflow/subpipeline-node/) ·
155
+ [terminal](https://leo-need-more-coffee.github.io/stageflow/terminal-node/)
156
+
157
+ Sources are in [`docs/`](docs) (`*.md` English, `*.ru.md` Russian) and publish
158
+ themselves on every push to `main`.
159
+
160
+ ## The rest of the project
161
+
162
+ | Repository | What it is |
163
+ |---|---|
164
+ | **stageflow** | this one: the core that runs the pipelines |
165
+ | [stageflow-ui](https://github.com/leo-need-more-coffee/stageflow-ui) | the editor: a static page that draws and debugs a graph |
166
+ | [stageflow-example](https://github.com/leo-need-more-coffee/stageflow-example) | a working backend for the editor: a support bot in four pipelines |
167
+
168
+ ## Development
169
+
170
+ ```bash
171
+ pip install -e ".[dev]"
172
+ python -m unittest discover -s tests
173
+ ```
174
+
175
+ Pipelines can be tested declaratively:
176
+
177
+ ```python
178
+ from stageflow.testing import PipelineTestSpec, run_pipeline_test
179
+ ```
180
+
181
+ Releases are tag-driven: the tag has to match `project.version` in
182
+ `pyproject.toml`, and the workflow publishes to PyPI.
183
+
184
+ ## License
185
+
186
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,153 @@
1
+ <div align="center">
2
+
3
+ # StageFlow
4
+
5
+ **Pipelines described in JSON, executed in Python — and debuggable node by node.**
6
+
7
+ [![tests](https://github.com/leo-need-more-coffee/stageflow/actions/workflows/tests.yml/badge.svg)](https://github.com/leo-need-more-coffee/stageflow/actions/workflows/tests.yml)
8
+ [![PyPI](https://img.shields.io/pypi/v/stageflow-framework)](https://pypi.org/project/stageflow-framework/)
9
+ [![Python](https://img.shields.io/pypi/pyversions/stageflow-framework)](https://pypi.org/project/stageflow-framework/)
10
+ [![docs](https://img.shields.io/badge/docs-stageflow-blue)](https://leo-need-more-coffee.github.io/stageflow/)
11
+ [![license](https://img.shields.io/badge/license-MIT-green)](LICENSE)
12
+
13
+ [Documentation](https://leo-need-more-coffee.github.io/stageflow/) ·
14
+ [Tutorial](https://leo-need-more-coffee.github.io/stageflow/tutorial/) ·
15
+ [Документация на русском](https://leo-need-more-coffee.github.io/stageflow/ru/)
16
+
17
+ </div>
18
+
19
+ A pipeline is a graph of nodes in JSON. The work happens in stages — Python
20
+ classes you write. Between them travels one immutable frame of variables, and
21
+ everything the graph does with that frame is visible in the JSON: branching,
22
+ retries, error handling, concurrency, nested graphs.
23
+
24
+ Because the pipeline is data, it can be stored, diffed, generated, validated
25
+ before it runs — and drawn:
26
+
27
+ ![A pipeline open in the StageFlow editor](docs/img/tut-final.png)
28
+
29
+ That is the [editor](https://github.com/leo-need-more-coffee/stageflow-ui): a
30
+ separate static page that draws and debugs a graph while this core executes it.
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install stageflow-framework
36
+ ```
37
+
38
+ Python 3.11+.
39
+
40
+ ## A pipeline in 30 seconds
41
+
42
+ ```python
43
+ import asyncio
44
+
45
+ from stageflow import BaseStage, Pipeline, Session, register_stage
46
+
47
+
48
+ @register_stage("HelloStage")
49
+ class HelloStage(BaseStage):
50
+ """
51
+ description: "Greets whoever the pipeline points at"
52
+ arguments:
53
+ name: string
54
+ outputs:
55
+ greeting: string
56
+ """
57
+
58
+ async def run(self):
59
+ name = self.get_arguments().get("name", "world")
60
+ self.set_outputs({"greeting": f"Hello, {name}!"})
61
+
62
+
63
+ pipeline = Pipeline.from_dict({
64
+ "nodes": [
65
+ {"id": "start", "type": "entry",
66
+ "variables": {"user_name": "Alice"}, "next": "hello"},
67
+ {"id": "hello", "type": "stage", "stage": "HelloStage",
68
+ "arguments": {"vars": {"name": "user_name"}},
69
+ "outputs": {"greeting": "greeting"}, "next": "finish"},
70
+ {"id": "finish", "type": "terminal",
71
+ "result": {"status": "ok"}, "artifacts": ["greeting"]},
72
+ ],
73
+ })
74
+ pipeline.validate()
75
+
76
+ result = asyncio.run(Session(id="demo", pipeline=pipeline).run())
77
+ print(result.result) # {'status': 'ok'}
78
+ print(result.artifacts) # {'greeting': 'Hello, Alice!'}
79
+ ```
80
+
81
+ The docstring is the stage's specification. `validate()` checks the graph
82
+ against it, so a wrong argument name is an error before anything runs.
83
+
84
+ ## What the graph can do
85
+
86
+ | | |
87
+ |---|---|
88
+ | **Nine node types** | `entry`, `stage`, `condition`, `switch`, `parallel`, `try`, `map`, `subpipeline`, `terminal` |
89
+ | **One immutable frame** | variables travel along the path; a write produces a new frame, so branches never collide |
90
+ | **CEL expressions** | in conditions, in switch cases, and in any argument or output through the `.$` suffix |
91
+ | **Errors as roads** | `retry` on a node, `try`/`except` over a region of the graph derived from its shape |
92
+ | **Real concurrency** | `parallel` branches with their own frames and explicit merge rules |
93
+ | **Loops over data** | `map` runs a region of the graph once per element, sequentially or at once |
94
+ | **Nested graphs** | a `subpipeline` starts with a fresh frame and returns artifacts |
95
+ | **Gradual typing** | declare the variables that matter; checked at validation and on every write |
96
+ | **Step debugging** | stop between nodes, read and edit the frame, replay the event stream |
97
+
98
+ ## Documentation
99
+
100
+ The [tutorial](https://leo-need-more-coffee.github.io/stageflow/tutorial/)
101
+ builds one working pipeline step by step, with screenshots from the editor.
102
+ The reference covers the rest:
103
+
104
+ [Quick start](https://leo-need-more-coffee.github.io/stageflow/quick-start/) ·
105
+ [Node types](https://leo-need-more-coffee.github.io/stageflow/node-types/) ·
106
+ [Data model](https://leo-need-more-coffee.github.io/stageflow/data-model/) ·
107
+ [Expressions](https://leo-need-more-coffee.github.io/stageflow/expressions/) ·
108
+ [Errors](https://leo-need-more-coffee.github.io/stageflow/errors/) ·
109
+ [Variable typing](https://leo-need-more-coffee.github.io/stageflow/variable-typing/) ·
110
+ [Session control](https://leo-need-more-coffee.github.io/stageflow/session-control/) ·
111
+ [Step debugging](https://leo-need-more-coffee.github.io/stageflow/step-debugging/)
112
+
113
+ One page per node type:
114
+ [entry](https://leo-need-more-coffee.github.io/stageflow/entry-node/) ·
115
+ [stage](https://leo-need-more-coffee.github.io/stageflow/stage-node/) ·
116
+ [condition](https://leo-need-more-coffee.github.io/stageflow/condition-node/) ·
117
+ [switch](https://leo-need-more-coffee.github.io/stageflow/switch-node/) ·
118
+ [parallel](https://leo-need-more-coffee.github.io/stageflow/parallel-branches/) ·
119
+ [try](https://leo-need-more-coffee.github.io/stageflow/try-node/) ·
120
+ [map](https://leo-need-more-coffee.github.io/stageflow/map-node/) ·
121
+ [subpipeline](https://leo-need-more-coffee.github.io/stageflow/subpipeline-node/) ·
122
+ [terminal](https://leo-need-more-coffee.github.io/stageflow/terminal-node/)
123
+
124
+ Sources are in [`docs/`](docs) (`*.md` English, `*.ru.md` Russian) and publish
125
+ themselves on every push to `main`.
126
+
127
+ ## The rest of the project
128
+
129
+ | Repository | What it is |
130
+ |---|---|
131
+ | **stageflow** | this one: the core that runs the pipelines |
132
+ | [stageflow-ui](https://github.com/leo-need-more-coffee/stageflow-ui) | the editor: a static page that draws and debugs a graph |
133
+ | [stageflow-example](https://github.com/leo-need-more-coffee/stageflow-example) | a working backend for the editor: a support bot in four pipelines |
134
+
135
+ ## Development
136
+
137
+ ```bash
138
+ pip install -e ".[dev]"
139
+ python -m unittest discover -s tests
140
+ ```
141
+
142
+ Pipelines can be tested declaratively:
143
+
144
+ ```python
145
+ from stageflow.testing import PipelineTestSpec, run_pipeline_test
146
+ ```
147
+
148
+ Releases are tag-driven: the tag has to match `project.version` in
149
+ `pyproject.toml`, and the workflow publishes to PyPI.
150
+
151
+ ## License
152
+
153
+ MIT — see [LICENSE](LICENSE).
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "stageflow-framework"
7
- version = "0.9.0"
7
+ version = "0.11.0"
8
8
  description = "StageFlow: pipeline framework for stages"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -41,6 +41,10 @@ dev = [
41
41
  "build",
42
42
  "twine",
43
43
  ]
44
+ docs = [
45
+ "mkdocs-material",
46
+ "mkdocs-static-i18n",
47
+ ]
44
48
 
45
49
  [tool.setuptools.packages.find]
46
50
  where = ["."]
@@ -1,4 +1,5 @@
1
1
  from . import builtins # noqa: F401
2
+ from .capabilities import __version__, capabilities
2
3
  from .core import (
3
4
  BaseStage,
4
5
  CelEngine,
@@ -9,6 +10,7 @@ from .core import (
9
10
  Event,
10
11
  EventSpec,
11
12
  InputSpec,
13
+ MapNode,
12
14
  Node,
13
15
  ParallelNode,
14
16
  Pipeline,
@@ -49,6 +51,8 @@ from .exceptions import (
49
51
  )
50
52
 
51
53
  __all__ = [
54
+ "__version__",
55
+ "capabilities",
52
56
  "Pipeline",
53
57
  "Session",
54
58
  "SessionResult",
@@ -67,6 +71,7 @@ __all__ = [
67
71
  "ConditionNode",
68
72
  "SwitchNode",
69
73
  "ParallelNode",
74
+ "MapNode",
70
75
  "SubPipelineNode",
71
76
  "TerminalNode",
72
77
  "Retrier",
@@ -8,7 +8,7 @@ from ..exceptions import StageContractError
8
8
  def require_list(stage_name: str, field: str, value: Any) -> list:
9
9
  if not isinstance(value, list):
10
10
  raise StageContractError(
11
- f"{stage_name}: '{field}' должен быть списком, получен {type(value).__name__}"
11
+ f"{stage_name}: '{field}' must be a list, got {type(value).__name__}"
12
12
  )
13
13
  return value
14
14
 
@@ -16,7 +16,7 @@ def require_list(stage_name: str, field: str, value: Any) -> list:
16
16
  def require_dict(stage_name: str, field: str, value: Any) -> dict:
17
17
  if not isinstance(value, dict):
18
18
  raise StageContractError(
19
- f"{stage_name}: '{field}' должен быть объектом, получен {type(value).__name__}"
19
+ f"{stage_name}: '{field}' must be an object, got {type(value).__name__}"
20
20
  )
21
21
  return value
22
22
 
@@ -24,12 +24,12 @@ def require_dict(stage_name: str, field: str, value: Any) -> dict:
24
24
  def require_number(stage_name: str, field: str, value: Any) -> int | float:
25
25
  if isinstance(value, bool) or not isinstance(value, (int, float)):
26
26
  raise StageContractError(
27
- f"{stage_name}: '{field}' должен быть числом, получен {type(value).__name__}"
27
+ f"{stage_name}: '{field}' must be a number, got {type(value).__name__}"
28
28
  )
29
29
  return value
30
30
 
31
31
 
32
32
  def require_present(stage_name: str, field: str, value: Any) -> Any:
33
33
  if value is None:
34
- raise StageContractError(f"{stage_name}: требуется аргумент '{field}'")
34
+ raise StageContractError(f"{stage_name}: argument '{field}' is required")
35
35
  return value
@@ -29,7 +29,7 @@ class AppendListStage(BaseStage):
29
29
  async def run(self):
30
30
  args = self.get_arguments()
31
31
  if "value" not in args:
32
- raise StageContractError("AppendListStage: требуется аргумент 'value'")
32
+ raise StageContractError("AppendListStage: argument 'value' is required")
33
33
  value = args["value"]
34
34
  base = require_list("AppendListStage", "list", args.get("list") or [])
35
35
  self.set_outputs({"list": [*base, value]})
@@ -63,7 +63,7 @@ class ExtendListStage(BaseStage):
63
63
  items = args.get("items", [])
64
64
  if not isinstance(items, (list, tuple)):
65
65
  raise StageContractError(
66
- f"ExtendListStage: 'items' должен быть списком, получен {type(items).__name__}"
66
+ f"ExtendListStage: 'items' must be a list, got {type(items).__name__}"
67
67
  )
68
68
  self.set_outputs({"list": [*base, *items]})
69
69
 
@@ -95,7 +95,7 @@ class FilterListStage(BaseStage):
95
95
  args = self.get_arguments()
96
96
  condition = args.pop("condition", None)
97
97
  if condition is None:
98
- raise StageContractError("FilterListStage: требуется аргумент 'condition'")
98
+ raise StageContractError("FilterListStage: argument 'condition' is required")
99
99
  items = require_list("FilterListStage", "items", args.pop("items", []))
100
100
  scope = Context(vars=args)
101
101
  result = [item for item in items if self.session.cel.eval(condition, scope, item=item)]
@@ -159,13 +159,13 @@ class PopListStage(BaseStage):
159
159
  args = self.get_arguments()
160
160
  items = require_list("PopListStage", "items", args.get("items", []))
161
161
  if not items:
162
- raise StageContractError("PopListStage: нельзя извлечь элемент из пустого списка")
162
+ raise StageContractError("PopListStage: cannot pop from an empty list")
163
163
  index = args.get("index", -1)
164
164
  remaining = list(items)
165
165
  try:
166
166
  popped = remaining.pop(index)
167
167
  except IndexError:
168
168
  raise StageContractError(
169
- f"PopListStage: индекс {index} вне списка длины {len(items)}"
169
+ f"PopListStage: index {index} is out of range for a list of length {len(items)}"
170
170
  ) from None
171
171
  self.set_outputs({"list": remaining, "popped": popped})
@@ -9,13 +9,13 @@ class _NameOnlyFormatter(string.Formatter):
9
9
  def get_field(self, field_name, args, kwargs):
10
10
  if not field_name.isidentifier():
11
11
  raise StageContractError(
12
- f"TemplateStage: плейсхолдер '{{{field_name}}}' — не имя аргумента; "
13
- "атрибуты, индексы и позиционные номера в шаблоне запрещены"
12
+ f"TemplateStage: placeholder '{{{field_name}}}' is not an argument name; "
13
+ "attributes, indexes and positional numbers are not allowed"
14
14
  )
15
15
  if field_name not in kwargs:
16
16
  raise StageContractError(
17
- f"TemplateStage: шаблон ссылается на '{field_name}', "
18
- f"но такого аргумента нет (есть: {sorted(kwargs)})"
17
+ f"TemplateStage: template references '{field_name}', "
18
+ f"but there is no such argument (available: {sorted(kwargs)})"
19
19
  )
20
20
  return kwargs[field_name], field_name
21
21
 
@@ -23,7 +23,7 @@ class SetValueStage(BaseStage):
23
23
  async def run(self):
24
24
  args = self.get_arguments()
25
25
  if "value" not in args:
26
- raise StageContractError("SetValueStage: требуется аргумент 'value'")
26
+ raise StageContractError("SetValueStage: argument 'value' is required")
27
27
  self.set_outputs({"value": args["value"]})
28
28
 
29
29
 
@@ -0,0 +1,56 @@
1
+ """What this build of StageFlow can do, in a form a client can read.
2
+
3
+ A visual editor is written against one version of the core and then pointed at
4
+ whatever backend the user has running. A version number alone does not answer
5
+ the question it actually has — "can I offer a `map` node here?" — because the
6
+ answer depends on the build, not on a range of releases someone has to keep a
7
+ table of. So the core answers it directly: here are the node types I execute.
8
+
9
+ A backend is expected to hand this to its clients (the example backend serves
10
+ it at `GET /api/meta`); the shape is small and additive on purpose.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from importlib.metadata import PackageNotFoundError, version as _package_version
16
+ from typing import Any
17
+
18
+ from .core.nodes import get_node_types
19
+ from .core.stage import get_stages
20
+
21
+ DISTRIBUTION = "stageflow-framework"
22
+
23
+
24
+ def _installed_version() -> str:
25
+ """The version of the installed distribution.
26
+
27
+ Running from a source checkout that was never installed there is nothing
28
+ to read it from, and guessing would be worse than saying so: a client that
29
+ sees "0.0.0+unknown" knows not to compare it with anything, while a made-up
30
+ number would be compared and believed.
31
+ """
32
+ try:
33
+ return _package_version(DISTRIBUTION)
34
+ except PackageNotFoundError: # pragma: no cover - only in a bare checkout
35
+ return "0.0.0+unknown"
36
+
37
+
38
+ __version__ = _installed_version()
39
+
40
+
41
+ def capabilities() -> dict[str, Any]:
42
+ """The language this build understands.
43
+
44
+ - `stageflow` — the version of the core, for a human reading a log;
45
+ - `node_types` — the types a pipeline may use here, sorted. This is the
46
+ field a client should branch on: a name that is not in the list will be
47
+ rejected by `Pipeline.from_dict` with `Unknown node type`.
48
+ - `stages` — how many stages are registered. The specs themselves are a
49
+ separate, much larger answer (`get_stages`), so this is only a hint that
50
+ a registry is there at all.
51
+ """
52
+ return {
53
+ "stageflow": __version__,
54
+ "node_types": sorted(get_node_types()),
55
+ "stages": len(get_stages()),
56
+ }
@@ -6,6 +6,7 @@ from .inputs import InputHub
6
6
  from .nodes import (
7
7
  ConditionNode,
8
8
  EntryNode,
9
+ MapNode,
9
10
  Node,
10
11
  ParallelNode,
11
12
  Retrier,
@@ -48,6 +49,7 @@ __all__ = [
48
49
  "ExceptHandler",
49
50
  "EntryNode",
50
51
  "StageNode",
52
+ "MapNode",
51
53
  "ConditionNode",
52
54
  "SwitchNode",
53
55
  "ParallelNode",
@@ -28,8 +28,8 @@ class CelEngine:
28
28
  self._compiled: dict[str, Any] = {}
29
29
  if _BACKEND is None: # pragma: no cover
30
30
  raise ExpressionError(
31
- "Не найден ни один CEL-байндинг. Поставь "
32
- "'common-expression-language' (рекомендуется) или 'cel-python'."
31
+ "No CEL backend found. Install "
32
+ "'common-expression-language' (recommended) or 'cel-python'."
33
33
  )
34
34
 
35
35
  @property
@@ -46,7 +46,7 @@ class CelEngine:
46
46
  env = _celpy.Environment()
47
47
  program = env.program(env.compile(expr))
48
48
  except Exception as exc: # noqa: BLE001
49
- raise ExpressionError(f"Не удалось скомпилировать CEL {expr!r}: {exc}") from exc
49
+ raise ExpressionError(f"Cannot compile CEL {expr!r}: {exc}") from exc
50
50
  self._compiled[expr] = program
51
51
  return program
52
52
 
@@ -72,4 +72,4 @@ class CelEngine:
72
72
  return program.execute(activation)
73
73
  return program.evaluate(activation) # pragma: no cover
74
74
  except Exception as exc: # noqa: BLE001
75
- raise ExpressionError(f"Ошибка вычисления CEL {expr!r}: {exc}") from exc
75
+ raise ExpressionError(f"Cannot evaluate CEL {expr!r}: {exc}") from exc
@@ -39,8 +39,8 @@ class Context:
39
39
  def from_dict(cls, data: dict[str, Any]) -> "Context":
40
40
  if "local" in data and "vars" not in data:
41
41
  raise PipelineDefinitionError(
42
- "Снапшот содержит скоуп 'local' (до 0.7.0); "
43
- "фрейм теперь называется 'vars'"
42
+ "Snapshot contains the 'local' scope (pre-0.7.0); "
43
+ "the frame is now called 'vars'"
44
44
  )
45
45
  return cls(vars=data.get("vars", {}))
46
46
 
@@ -124,7 +124,7 @@ class StepDebugger:
124
124
  for name, value in values.items():
125
125
  try:
126
126
  if types is not None:
127
- types.check_write(name, value, f"отладчик перед узлом '{self.node}'")
127
+ types.check_write(name, value, f"debugger before node '{self.node}'")
128
128
  except Exception as exc: # noqa: BLE001
129
129
  self._emit("var_rejected", name=name, error=str(exc))
130
130
  continue
@@ -1,6 +1,7 @@
1
1
  from .base import Node, get_node_types, register_node
2
2
  from .branching import ConditionNode, SwitchNode
3
3
  from .entry import EntryNode
4
+ from .map_block import MapNode
4
5
  from .parallel import ParallelNode
5
6
  from .recovery import Retrier, run_with_retry
6
7
  from .stage import StageNode
@@ -18,6 +19,7 @@ __all__ = [
18
19
  "ExceptHandler",
19
20
  "EntryNode",
20
21
  "StageNode",
22
+ "MapNode",
21
23
  "ConditionNode",
22
24
  "SwitchNode",
23
25
  "ParallelNode",
@@ -77,10 +77,10 @@ class Node:
77
77
  errors: list[str] = []
78
78
  for src, dst in self.expose.items():
79
79
  malformed = False
80
- for path, side in ((src, "источник"), (dst, "назначение")):
80
+ for path, side in ((src, "source"), (dst, "destination")):
81
81
  if not isinstance(path, str) or not path.isidentifier():
82
82
  errors.append(
83
- f"{self.id}: expose {side} '{path}' должен быть именем переменной"
83
+ f"{self.id}: expose {side} '{path}' must be a variable name"
84
84
  )
85
85
  malformed = True
86
86
  if malformed:
@@ -90,8 +90,8 @@ class Node:
90
90
  dst_type = ts.declared(dst)
91
91
  if src_type and dst_type and not ts.expose_compatible(src_type, dst_type):
92
92
  errors.append(
93
- f"{self.id}: expose {src} -> {dst}: несовместимые типы "
94
- f"'{src_type}' и '{dst_type}'"
93
+ f"{self.id}: expose {src} -> {dst}: incompatible types "
94
+ f"'{src_type}' and '{dst_type}'"
95
95
  )
96
96
  return errors
97
97