flowrep 0.2.0__tar.gz → 0.4.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 (92) hide show
  1. {flowrep-0.2.0 → flowrep-0.4.0}/.gitignore +1 -0
  2. flowrep-0.4.0/PKG-INFO +379 -0
  3. flowrep-0.4.0/docs/README.md +317 -0
  4. {flowrep-0.2.0 → flowrep-0.4.0}/pyproject.toml +16 -12
  5. flowrep-0.4.0/src/flowrep/__init__.py +19 -0
  6. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages → flowrep-0.4.0/src}/flowrep/_version.py +2 -2
  7. flowrep-0.4.0/src/flowrep/api/schemas.py +41 -0
  8. flowrep-0.4.0/src/flowrep/api/tools.py +31 -0
  9. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/base_models.py +9 -4
  10. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/converters/python_workflow_definition.py +3 -3
  11. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/edge_models.py +1 -1
  12. flowrep-0.4.0/src/flowrep/live.py +303 -0
  13. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/nodes/__init__.py +3 -3
  14. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/nodes/atomic_model.py +6 -1
  15. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/nodes/for_model.py +15 -8
  16. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/nodes/helper_models.py +2 -2
  17. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/nodes/if_model.py +3 -3
  18. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/nodes/try_model.py +3 -3
  19. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/nodes/union.py +3 -3
  20. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/nodes/while_model.py +3 -3
  21. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/nodes/workflow_model.py +12 -2
  22. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/atomic_parser.py +50 -27
  23. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/case_helpers.py +3 -3
  24. flowrep-0.4.0/src/flowrep/parsers/dependency_parser.py +166 -0
  25. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/for_parser.py +5 -5
  26. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/if_parser.py +3 -3
  27. flowrep-0.4.0/src/flowrep/parsers/import_parser.py +51 -0
  28. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/object_scope.py +63 -13
  29. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/parser_helpers.py +3 -3
  30. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/parser_protocol.py +3 -3
  31. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/symbol_scope.py +2 -2
  32. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/try_parser.py +2 -2
  33. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/while_parser.py +3 -3
  34. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/workflow_parser.py +9 -5
  35. flowrep-0.4.0/src/flowrep/storage.py +241 -0
  36. flowrep-0.4.0/src/flowrep/storage_widget.py +184 -0
  37. {flowrep-0.2.0/flowrep/models → flowrep-0.4.0/src/flowrep}/subgraph_validation.py +3 -3
  38. flowrep-0.4.0/src/flowrep/wfms.py +507 -0
  39. flowrep-0.2.0/PKG-INFO +0 -60
  40. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/__init__.py +0 -8
  41. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/__init__.py +0 -10
  42. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/converters.py +0 -12
  43. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/nodes.py +0 -13
  44. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/parsers.py +0 -10
  45. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/schemas.py +0 -25
  46. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/edge_models.py +0 -55
  47. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/__init__.py +0 -32
  48. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/atomic_model.py +0 -74
  49. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/helper_models.py +0 -80
  50. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/atomic_parser.py +0 -287
  51. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/dependency_parser.py +0 -111
  52. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/if_parser.py +0 -136
  53. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/object_scope.py +0 -103
  54. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/parser_protocol.py +0 -48
  55. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/try_parser.py +0 -107
  56. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/subgraph_validation.py +0 -196
  57. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/tools.py +0 -131
  58. flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/workflow.py +0 -1221
  59. flowrep-0.2.0/docs/README.md +0 -5
  60. flowrep-0.2.0/flowrep/__init__.py +0 -8
  61. flowrep-0.2.0/flowrep/_version.py +0 -34
  62. flowrep-0.2.0/flowrep/models/__init__.py +0 -10
  63. flowrep-0.2.0/flowrep/models/api/__init__.py +0 -0
  64. flowrep-0.2.0/flowrep/models/api/converters.py +0 -12
  65. flowrep-0.2.0/flowrep/models/api/nodes.py +0 -13
  66. flowrep-0.2.0/flowrep/models/api/parsers.py +0 -10
  67. flowrep-0.2.0/flowrep/models/api/schemas.py +0 -25
  68. flowrep-0.2.0/flowrep/models/base_models.py +0 -165
  69. flowrep-0.2.0/flowrep/models/converters/__init__.py +0 -0
  70. flowrep-0.2.0/flowrep/models/converters/python_workflow_definition.py +0 -489
  71. flowrep-0.2.0/flowrep/models/nodes/for_model.py +0 -174
  72. flowrep-0.2.0/flowrep/models/nodes/if_model.py +0 -118
  73. flowrep-0.2.0/flowrep/models/nodes/try_model.py +0 -105
  74. flowrep-0.2.0/flowrep/models/nodes/union.py +0 -26
  75. flowrep-0.2.0/flowrep/models/nodes/while_model.py +0 -145
  76. flowrep-0.2.0/flowrep/models/nodes/workflow_model.py +0 -85
  77. flowrep-0.2.0/flowrep/models/parsers/__init__.py +0 -0
  78. flowrep-0.2.0/flowrep/models/parsers/case_helpers.py +0 -146
  79. flowrep-0.2.0/flowrep/models/parsers/dependency_parser.py +0 -111
  80. flowrep-0.2.0/flowrep/models/parsers/for_parser.py +0 -262
  81. flowrep-0.2.0/flowrep/models/parsers/label_helpers.py +0 -151
  82. flowrep-0.2.0/flowrep/models/parsers/parser_helpers.py +0 -166
  83. flowrep-0.2.0/flowrep/models/parsers/symbol_scope.py +0 -254
  84. flowrep-0.2.0/flowrep/models/parsers/while_parser.py +0 -116
  85. flowrep-0.2.0/flowrep/models/parsers/workflow_parser.py +0 -451
  86. flowrep-0.2.0/flowrep/tools.py +0 -131
  87. flowrep-0.2.0/flowrep/workflow.py +0 -1221
  88. {flowrep-0.2.0 → flowrep-0.4.0}/LICENSE +0 -0
  89. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/api/__init__.py +0 -0
  90. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/converters/__init__.py +0 -0
  91. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/__init__.py +0 -0
  92. {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models → flowrep-0.4.0/src/flowrep}/parsers/label_helpers.py +0 -0
@@ -11,3 +11,4 @@ apidoc/
11
11
  .ipynb_checkpoints/
12
12
  test_times.dat
13
13
  core.*
14
+ src/flowrep/_version.py
flowrep-0.4.0/PKG-INFO ADDED
@@ -0,0 +1,379 @@
1
+ Metadata-Version: 2.4
2
+ Name: flowrep
3
+ Version: 0.4.0
4
+ Summary: flowrep - Your premier tool for workflow representations
5
+ Project-URL: Homepage, https://pyiron.org/
6
+ Project-URL: Documentation, https://flowrep.readthedocs.io
7
+ Project-URL: Repository, https://github.com/pyiron/flowrep
8
+ Author-email: Sam Waseda <waseda@mpie.de>, Liam Huber <liamhuber@greyhavensolutions.com>
9
+ License: BSD 3-Clause License
10
+
11
+ Copyright (c) 2024, Max-Planck-Institut für Nachhaltige Materialien GmbH - Computational Materials Design (CM) Department
12
+ All rights reserved.
13
+
14
+ Redistribution and use in source and binary forms, with or without
15
+ modification, are permitted provided that the following conditions are met:
16
+
17
+ * Redistributions of source code must retain the above copyright notice, this
18
+ list of conditions and the following disclaimer.
19
+
20
+ * Redistributions in binary form must reproduce the above copyright notice,
21
+ this list of conditions and the following disclaimer in the documentation
22
+ and/or other materials provided with the distribution.
23
+
24
+ * Neither the name of the copyright holder nor the names of its
25
+ contributors may be used to endorse or promote products derived from
26
+ this software without specific prior written permission.
27
+
28
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
29
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
30
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
31
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
32
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
33
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
34
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
35
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
36
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
37
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
38
+ License-File: LICENSE
39
+ Keywords: pyiron
40
+ Classifier: Development Status :: 4 - Beta
41
+ Classifier: Intended Audience :: Science/Research
42
+ Classifier: License :: OSI Approved :: BSD License
43
+ Classifier: Operating System :: OS Independent
44
+ Classifier: Programming Language :: Python :: 3.11
45
+ Classifier: Programming Language :: Python :: 3.12
46
+ Classifier: Programming Language :: Python :: 3.13
47
+ Classifier: Programming Language :: Python :: 3.14
48
+ Classifier: Topic :: Scientific/Engineering
49
+ Requires-Python: <3.15,>=3.11
50
+ Requires-Dist: pydantic<2.13.0,>=2.12.0
51
+ Requires-Dist: pyiron-snippets<2.0.0,>=1.2.1
52
+ Provides-Extra: notebooks
53
+ Requires-Dist: numpy==2.4.3; extra == 'notebooks'
54
+ Provides-Extra: pwd
55
+ Requires-Dist: python-workflow-definition==0.1.4; extra == 'pwd'
56
+ Provides-Extra: storage
57
+ Requires-Dist: bagofholding==0.1.10; extra == 'storage'
58
+ Provides-Extra: storage-widget
59
+ Requires-Dist: bagofholding==0.1.10; extra == 'storage-widget'
60
+ Requires-Dist: ipytree==0.2.2; extra == 'storage-widget'
61
+ Description-Content-Type: text/markdown
62
+
63
+ # flowrep
64
+
65
+ [![Binder](https://mybinder.org/badge_logo.svg)](https://mybinder.org/v2/gh/pyiron/flowrep/HEAD?urlpath=%2Fdoc%2Ftree%2Fuser-guide.ipynb)
66
+ [![License](https://img.shields.io/badge/License-BSD_3--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
67
+ [![Coverage](https://codecov.io/gh/pyiron/flowrep/graph/badge.svg)](https://codecov.io/gh/pyiron/flowrep)
68
+ [![Documentation](https://readthedocs.org/projects/flowrep/badge/?version=latest)](https://flowrep.readthedocs.io/en/latest/?badge=latest)
69
+
70
+ [![Anaconda](https://anaconda.org/conda-forge/flowrep/badges/version.svg)](https://anaconda.org/conda-forge/flowrep)
71
+ [![Last Updated](https://anaconda.org/conda-forge/flowrep/badges/latest_release_date.svg)](https://anaconda.org/conda-forge/flowrep)
72
+ [![Platform](https://anaconda.org/conda-forge/flowrep/badges/platforms.svg)](https://anaconda.org/conda-forge/flowrep)
73
+ [![Downloads](https://anaconda.org/conda-forge/flowrep/badges/downloads.svg)](https://anaconda.org/conda-forge/flowrep)
74
+
75
+
76
+ ## flowrep — Workflow Recipes from Python
77
+
78
+ **flowrep** turns plain Python functions into shareable, versionable *workflow
79
+ recipes* — JSON-serialisable graphs that describe *what* to compute (which
80
+ functions, how they connect) without doing the computation or holding any data.
81
+ Recipes are prospective blueprints that a Workflow Management System (WfMS) can
82
+ digest, visualise, and execute.
83
+
84
+ Flowchart-style representations are already the lingua franca for describing
85
+ processes in science and engineering. flowrep gives you a way to author them in
86
+ Python and pass them around as simple JSON, validated by
87
+ [Pydantic](https://docs.pydantic.dev/) and enriched with version provenance so
88
+ they stay robust through time.
89
+
90
+
91
+ ## Installation
92
+
93
+ ```bash
94
+ conda install -c conda-forge flowrep # recommended
95
+ pip install flowrep # also available from PyPI
96
+ ```
97
+
98
+
99
+ ## Quick Start
100
+
101
+ Define a couple of simple functions — it will be helpful to make our return statements
102
+ nicely named variables, but it's not a necessity:
103
+
104
+ ```python
105
+ >>> import flowrep as fr
106
+
107
+ >>> def add(a, b):
108
+ ... return a + b
109
+
110
+ >>> def multiply(x, y):
111
+ ... product = x * y
112
+ ... return product
113
+
114
+ ```
115
+
116
+ Compose them into a workflow with the `@workflow` decorator. The body reads like
117
+ normal Python — assign function calls to variables, wire outputs into inputs,
118
+ return results:
119
+
120
+ ```python
121
+ >>> @fr.workflow
122
+ ... def linear(x, slope, intercept):
123
+ ... """y = slope * x + intercept"""
124
+ ... scaled = multiply(x, slope)
125
+ ... result = add(scaled, intercept)
126
+ ... return result
127
+
128
+ ```
129
+
130
+ The decorated function is still just its usual callable self:
131
+
132
+ ```python
133
+ >>> linear(3, 2, 1)
134
+ 7
135
+
136
+ ```
137
+
138
+ But it now carries a recipe on its `flowrep_recipe` attribute — a pure-data
139
+ Pydantic model whose structure you can inspect directly
140
+ (`model_dump(mode="json")` gives a Python dict, `model_dump_json()` gives a
141
+ JSON string):
142
+
143
+ ```python
144
+ >>> recipe = linear.flowrep_recipe.model_dump(mode="json")
145
+ >>> recipe["type"]
146
+ 'workflow'
147
+ >>> recipe["inputs"]
148
+ ['x', 'slope', 'intercept']
149
+ >>> recipe["outputs"]
150
+ ['result']
151
+ >>> sorted(recipe["nodes"])
152
+ ['add_0', 'multiply_0']
153
+ >>> recipe["nodes"]["multiply_0"]["outputs"]
154
+ ['product']
155
+ >>> recipe["edges"]
156
+ {'add_0.a': 'multiply_0.product'}
157
+ >>> recipe["output_edges"]
158
+ {'result': 'add_0.output_0'}
159
+ >>> recipe["nodes"]["add_0"]["reference"]["info"]["qualname"]
160
+ 'add'
161
+
162
+ ```
163
+
164
+ A key difference between a workflow graph and typical python is that for a graph we
165
+ need named handles for all of our output values.
166
+ We can see in the `edges` that the recipe nicely used our return variable to give
167
+ the "multiply" a nice output label.
168
+ For our "add" node, what we returned couldn't be parsed nicely as a label, so in
169
+ `output_edges` we see it got assigned a default name `"output_0"`.
170
+
171
+ Every `"atomic"` node for running a Python function and every `"workflow"` not created
172
+ by parsing a function defition carries a `reference` recording for the Python function
173
+ to which it maps (module, qualname, and package version when available), so a WfMS can resolve
174
+ and execute them later. The recipe also captures exactly how data flows:
175
+ `input_edges` wire workflow inputs to child node ports, `edges` connect sibling
176
+ outputs to inputs, and `output_edges` name which child port produces each
177
+ workflow output.
178
+
179
+ The recipe *is* the shareable artifact. It could equally have been authored in a
180
+ GUI editor or generated by another tool — flowrep doesn't care where it came
181
+ from, only that it validates. (The availability of referenced functions on your
182
+ machine, and their actual behaviour, lie outside flowrep's responsibility. We
183
+ simply check that the recipe is internally consistent.)
184
+
185
+
186
+ ## Features
187
+
188
+ **Pure Python foundation.** Knowing basic Python is enough to get started. The
189
+ decorators `@atomic` and `@workflow` (plus their functional counterparts
190
+ `parse_atomic` and `parse_workflow`) handle the heavy lifting; the recipe format
191
+ is plain JSON.
192
+
193
+ **Pydantic validation.** Recipes are full Pydantic models — they validate on
194
+ construction, serialise with `model_dump_json()`, and deserialise with
195
+ `model_validate_json()`.
196
+
197
+ **Version provenance.** Every node reference carries module, qualname, and
198
+ (optionally) package version metadata. Constraints like `forbid_main`,
199
+ `forbid_locals`, and `require_version` can be enforced at parse time to ensure
200
+ recipes only reference published, versioned code.
201
+
202
+ **Composability.** Workflows nest: a `@workflow`-decorated function can call
203
+ another `@workflow`-decorated function, and the child's recipe is embedded as a
204
+ sub-graph. Build complex pipelines from small, testable pieces.
205
+
206
+ **Flow control.** Recipes go beyond simple DAGs. The `@workflow` parser
207
+ recognises `for`, `while`, `if`/`elif`/`else`, and `try`/`except` — the same
208
+ control structures you use in real code. Flow control nodes are inherently
209
+ dynamic: their exact execution path depends on data and cannot be known until
210
+ run-time, but their IO signature is always fully known a priori.
211
+
212
+ ### Example: flow control and nesting
213
+
214
+ So far we've seen `"workflow"` nodes, and alluded to `"atomic"` nodes.
215
+ You can also explicitly attach an atomic recipe to a function definition, but most of
216
+ the time you won't need to since the workflow parser auto-parses undecorated function
217
+ calls as atomic nodes.
218
+
219
+ On the other hand, we can parse `@workflow`-decorated functions _as workflows_ to
220
+ compose complex workflows with nested subgraphs.
221
+
222
+ Beyond simple `"atomic"` and DAG `"workflow"` nodes, there are also recipe formats
223
+ for flow control structures like `while`.
224
+ Just like the other nodes, these can be written directly as JSON, or parsed inside a
225
+ workflow from a python function definition -- with some syntax restrictions.
226
+ This is covered in more detail in the [user guide](https://mybinder.org/v2/gh/pyiron/flowrep/HEAD?urlpath=%2Fdoc%2Ftree%2Fuser-guide.ipynb), but here let's see both of these features
227
+ in use at once by nesting a while loop inside a sub-workflow.
228
+ Let's look at a `while` loop, where our main syntax restriction is that the condition
229
+ must be a function call:
230
+
231
+ ```python
232
+ >>> def is_less_than_target(value, target):
233
+ ... result = value < target
234
+ ... return result
235
+
236
+ >>> @fr.atomic # This is optional, but doesn't hurt us
237
+ ... def double(x):
238
+ ... doubled = x * 2
239
+ ... return doubled
240
+
241
+ >>> @fr.workflow
242
+ ... def double_until(x, target):
243
+ ... """Repeatedly double `x` until it reaches `target`."""
244
+ ... while is_less_than_target(x, target):
245
+ ... x = double(x)
246
+ ... return x
247
+
248
+ >>> double_until(3, 40)
249
+ 48
250
+
251
+ ```
252
+
253
+ And then we can compose workflows by nesting:
254
+
255
+ ```python
256
+ >>> @fr.workflow
257
+ ... def double_and_add(a, b, target):
258
+ ... big_a = double_until(a, target)
259
+ ... result = add(big_a, b)
260
+ ... return result
261
+
262
+ >>> double_and_add(3, 100, 40)
263
+ 148
264
+
265
+ ```
266
+
267
+ The resulting recipe captures the full structure — the while-loop, the nested
268
+ workflow, and all the edges between them — as a single pydantic model that can be dumped
269
+ to a JSON document.
270
+
271
+ We can see the layers of nested subgraphs:
272
+
273
+ ```python
274
+ >>> recipe_json = double_and_add.flowrep_recipe.model_dump(mode="json")
275
+ >>> [child for child in recipe_json["nodes"]]
276
+ ['double_until_0', 'add_0']
277
+
278
+ >>> [nested_child for nested_child in recipe_json["nodes"]["double_until_0"]["nodes"]]
279
+ ['while_0']
280
+
281
+ ```
282
+
283
+ Although the while-node is dynamic -- and therefore we _can't_ know its exact nodes
284
+ until run-time, it must and does still have well-defined IO signature.
285
+ We can look at the template it will follow, e.g., by peeking at the part of the recipe
286
+ used for the "while" condition:
287
+
288
+ ```python
289
+ >>> recipe_json["nodes"]["double_until_0"]["nodes"]["while_0"]["case"]["condition"]["node"]["type"]
290
+ 'atomic'
291
+
292
+ >>> recipe_json["nodes"]["double_until_0"]["nodes"]["while_0"]["case"]["condition"]["node"]["reference"]["info"]["qualname"]
293
+ 'is_less_than_target'
294
+
295
+ ```
296
+
297
+ And to see how it will forward it's inputs down into its prospective subgraph:
298
+
299
+ ```python
300
+ >>> recipe_json["nodes"]["double_until_0"]["nodes"]["while_0"]["input_edges"]
301
+ {'condition.value': 'x', 'condition.target': 'target', 'body.x': 'x'}
302
+
303
+ ```
304
+
305
+ We run these in the examples above to show two things: first, that even when nested,
306
+ the decorated functions are still just python functions; second, to show in the following
307
+ section that the recipe we parse from this are intended to give the same result as
308
+ these underlying functions when we run the recipe with a WfMS.
309
+
310
+ ## Beyond Recipes: Live Data and Execution
311
+
312
+ Recipes are *prospective* — they describe a computation template without holding
313
+ data. For retrospective analysis (inspecting what actually happened during a
314
+ run), flowrep provides two additional layers accessible through the API:
315
+
316
+ ```python
317
+ >>> from flowrep.api import tools as frt
318
+
319
+ ```
320
+
321
+ **`flowrep.api.tools.recipe2live`** converts a recipe into a *live* object — a mutable
322
+ data structure whose input and output ports can hold actual Python values. Live
323
+ objects mirror the recipe graph but trade JSON-serializability for the ability to
324
+ carry arbitrary data:
325
+
326
+ ```python
327
+ >>> live_wf = frt.recipe2live(double_and_add.flowrep_recipe)
328
+
329
+ ```
330
+
331
+ **`flowrep.api.tools.run_recipe`** goes one step further: it executes the recipe with
332
+ the provided inputs and returns a fully populated live object. This is powered
333
+ by a minimal, built-in WfMS intended as a reference implementation and for use
334
+ in tests and documentation (like this!):
335
+
336
+ ```python
337
+ >>> retrospective = frt.run_recipe(
338
+ ... double_and_add.flowrep_recipe, a=3, b=100, target=40
339
+ ... )
340
+ >>> retrospective.output_ports["result"].value
341
+ 148
342
+
343
+ ```
344
+
345
+ Because every child node's ports are populated too, the live graph gives you
346
+ full data provenance — you can walk the tree and inspect exactly what each node
347
+ received and produced.
348
+ For flow control nodes, which are _prospectively_ "black boxes", we find that
349
+ _retrospectively_ they are simple DAGs.
350
+ For our while-loop, that means that we can retrospectively inspect the provenance of
351
+ each loop iteration:
352
+
353
+ ```python
354
+ >>> while_loop = retrospective.nodes["double_until_0"].nodes["while_0"]
355
+ >>> while_loop.nodes["body_0"].output_ports["x"].value
356
+ 6
357
+ >>> while_loop.nodes["body_1"].output_ports["x"].value
358
+ 12
359
+
360
+ ```
361
+
362
+ For a deeper look at all available node types, edge semantics, version
363
+ provenance, and the live/WfMS layer, see the
364
+ [user guide](https://mybinder.org/v2/gh/pyiron/flowrep/HEAD?urlpath=%2Fdoc%2Ftree%2Fuser-guide.ipynb).
365
+
366
+
367
+ ## Documentation
368
+
369
+ - The user guide notebook comprehensively covers all node types, edge models, flow control, versioning,
370
+ live/retrospective data formats, a demo WfMS implementation, and recipe format converters.
371
+ Launch it interactively on
372
+ [mybinder](https://mybinder.org/v2/gh/pyiron/flowrep/HEAD?urlpath=%2Fdoc%2Ftree%2Fuser-guide.ipynb).
373
+ - [readthedocs](https://flowrep.readthedocs.io/en/latest/)
374
+
375
+
376
+ ## Contributing
377
+
378
+ Contributions are welcome! Please see
379
+ [`CODE_OF_CONDUCT.md`](../CODE_OF_CONDUCT.md) for community guidelines.