flowrep 0.4.1__tar.gz → 0.5.1__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 (54) hide show
  1. {flowrep-0.4.1 → flowrep-0.5.1}/.gitignore +2 -0
  2. {flowrep-0.4.1 → flowrep-0.5.1}/PKG-INFO +28 -27
  3. {flowrep-0.4.1 → flowrep-0.5.1}/docs/README.md +22 -21
  4. {flowrep-0.4.1 → flowrep-0.5.1}/pyproject.toml +5 -5
  5. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/__init__.py +6 -4
  6. flowrep-0.5.1/src/flowrep/_version.py +24 -0
  7. flowrep-0.5.1/src/flowrep/api/__init__.py +15 -0
  8. flowrep-0.5.1/src/flowrep/api/schemas.py +51 -0
  9. flowrep-0.5.1/src/flowrep/api/tools.py +17 -0
  10. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/base_models.py +2 -2
  11. flowrep-0.5.1/src/flowrep/compiler/__init__.py +4 -0
  12. flowrep-0.5.1/src/flowrep/compiler/annotate.py +141 -0
  13. flowrep-0.5.1/src/flowrep/compiler/flow_control.py +384 -0
  14. flowrep-0.5.1/src/flowrep/compiler/function.py +362 -0
  15. flowrep-0.5.1/src/flowrep/compiler/source.py +270 -0
  16. flowrep-0.5.1/src/flowrep/compiler/statements.py +345 -0
  17. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/converters/python_workflow_definition.py +18 -18
  18. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/atomic_parser.py +19 -19
  19. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/case_helpers.py +9 -7
  20. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/for_parser.py +4 -4
  21. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/if_parser.py +6 -4
  22. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/parser_helpers.py +2 -2
  23. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/parser_protocol.py +3 -3
  24. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/symbol_scope.py +2 -2
  25. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/try_parser.py +3 -3
  26. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/while_parser.py +4 -4
  27. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/workflow_parser.py +24 -20
  28. {flowrep-0.4.1/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/__init__.py +15 -15
  29. flowrep-0.4.1/src/flowrep/nodes/atomic_model.py → flowrep-0.5.1/src/flowrep/prospective/atomic_recipe.py +1 -1
  30. flowrep-0.4.1/src/flowrep/nodes/for_model.py → flowrep-0.5.1/src/flowrep/prospective/for_recipe.py +5 -5
  31. {flowrep-0.4.1/src/flowrep/nodes → flowrep-0.5.1/src/flowrep/prospective}/helper_models.py +6 -6
  32. flowrep-0.4.1/src/flowrep/nodes/if_model.py → flowrep-0.5.1/src/flowrep/prospective/if_recipe.py +5 -5
  33. flowrep-0.4.1/src/flowrep/nodes/try_model.py → flowrep-0.5.1/src/flowrep/prospective/try_recipe.py +6 -6
  34. flowrep-0.5.1/src/flowrep/prospective/union_types.py +26 -0
  35. flowrep-0.4.1/src/flowrep/nodes/while_model.py → flowrep-0.5.1/src/flowrep/prospective/while_recipe.py +4 -4
  36. flowrep-0.4.1/src/flowrep/nodes/workflow_model.py → flowrep-0.5.1/src/flowrep/prospective/workflow_recipe.py +15 -3
  37. flowrep-0.4.1/src/flowrep/live.py → flowrep-0.5.1/src/flowrep/retrospective/datastructures.py +140 -77
  38. {flowrep-0.4.1/src/flowrep → flowrep-0.5.1/src/flowrep/retrospective}/storage.py +24 -22
  39. {flowrep-0.4.1/src/flowrep → flowrep-0.5.1/src/flowrep/retrospective}/storage_widget.py +1 -1
  40. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/subgraph_validation.py +77 -35
  41. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/wfms.py +80 -77
  42. flowrep-0.4.1/src/flowrep/_version.py +0 -34
  43. flowrep-0.4.1/src/flowrep/api/schemas.py +0 -41
  44. flowrep-0.4.1/src/flowrep/api/tools.py +0 -31
  45. flowrep-0.4.1/src/flowrep/nodes/union.py +0 -26
  46. flowrep-0.4.1/src/flowrep/parsers/dependency_parser.py +0 -166
  47. flowrep-0.4.1/src/flowrep/parsers/import_parser.py +0 -51
  48. {flowrep-0.4.1 → flowrep-0.5.1}/LICENSE +0 -0
  49. {flowrep-0.4.1/src/flowrep/api → flowrep-0.5.1/src/flowrep/converters}/__init__.py +0 -0
  50. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/edge_models.py +0 -0
  51. {flowrep-0.4.1/src/flowrep/converters → flowrep-0.5.1/src/flowrep/parsers}/__init__.py +0 -0
  52. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/label_helpers.py +0 -0
  53. {flowrep-0.4.1 → flowrep-0.5.1}/src/flowrep/parsers/object_scope.py +0 -0
  54. {flowrep-0.4.1/src/flowrep/parsers → flowrep-0.5.1/src/flowrep/retrospective}/__init__.py +0 -0
@@ -1,5 +1,6 @@
1
1
  *.pyc
2
2
  .DS_Store
3
+ .claude/
3
4
  .coverage
4
5
  .pre-commit-config.yaml
5
6
  nohup.out
@@ -10,5 +11,6 @@ _build/
10
11
  apidoc/
11
12
  .ipynb_checkpoints/
12
13
  test_times.dat
14
+ CLAUDE.md
13
15
  core.*
14
16
  src/flowrep/_version.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flowrep
3
- Version: 0.4.1
3
+ Version: 0.5.1
4
4
  Summary: flowrep - Your premier tool for workflow representations
5
5
  Project-URL: Homepage, https://pyiron.org/
6
6
  Project-URL: Documentation, https://flowrep.readthedocs.io
@@ -48,15 +48,15 @@ Classifier: Programming Language :: Python :: 3.14
48
48
  Classifier: Topic :: Scientific/Engineering
49
49
  Requires-Python: <3.15,>=3.11
50
50
  Requires-Dist: pydantic<2.13.0,>=2.12.0
51
- Requires-Dist: pyiron-snippets<2.0.0,>=1.2.1
51
+ Requires-Dist: pyiron-snippets<2.0.0,>=1.3.0
52
52
  Provides-Extra: notebooks
53
- Requires-Dist: numpy==2.4.3; extra == 'notebooks'
53
+ Requires-Dist: numpy==2.4.6; extra == 'notebooks'
54
54
  Provides-Extra: pwd
55
- Requires-Dist: python-workflow-definition==0.1.4; extra == 'pwd'
55
+ Requires-Dist: python-workflow-definition==0.1.5; extra == 'pwd'
56
56
  Provides-Extra: storage
57
- Requires-Dist: bagofholding==0.1.10; extra == 'storage'
57
+ Requires-Dist: bagofholding==0.1.12; extra == 'storage'
58
58
  Provides-Extra: storage-widget
59
- Requires-Dist: bagofholding==0.1.10; extra == 'storage-widget'
59
+ Requires-Dist: bagofholding==0.1.12; extra == 'storage-widget'
60
60
  Requires-Dist: ipytree==0.2.2; extra == 'storage-widget'
61
61
  Description-Content-Type: text/markdown
62
62
 
@@ -76,10 +76,11 @@ Description-Content-Type: text/markdown
76
76
  ## flowrep — Workflow Recipes from Python
77
77
 
78
78
  **flowrep** turns plain Python functions into shareable, versionable *workflow
79
- recipes* — JSON-serialisable graphs that describe *what* to compute (which
79
+ recipes* — JSON-serialisable "class view" graphs that describe *what* to compute (which
80
80
  functions, how they connect) without doing the computation or holding any data.
81
81
  Recipes are prospective blueprints that a Workflow Management System (WfMS) can
82
- digest, visualise, and execute.
82
+ digest, visualise, and execute. To support this, **flowrep** also provides a set of
83
+ retrospective, "instance view" data classes for WfMS to populate when executing recipes.
83
84
 
84
85
  Flowchart-style representations are already the lingua franca for describing
85
86
  processes in science and engineering. flowrep gives you a way to author them in
@@ -209,6 +210,11 @@ control structures you use in real code. Flow control nodes are inherently
209
210
  dynamic: their exact execution path depends on data and cannot be known until
210
211
  run-time, but their IO signature is always fully known a priori.
211
212
 
213
+ **Clear separation.** Of prospective, "class view" recipes, and retrospective,
214
+ "instance view" data objects.
215
+
216
+ **Convertable and compatible.** Recipes can be compiled back to python functions, and converted from [`python-workflow-definition`](https://github.com/pythonworkflow/python-workflow-definition/) recipes (also _to_ PWD, where recipe operations are the subset supported by the PWD framework).
217
+
212
218
  ### Example: flow control and nesting
213
219
 
214
220
  So far we've seen `"workflow"` nodes, and alluded to `"atomic"` nodes.
@@ -307,34 +313,30 @@ the decorated functions are still just python functions; second, to show in the
307
313
  section that the recipe we parse from this are intended to give the same result as
308
314
  these underlying functions when we run the recipe with a WfMS.
309
315
 
310
- ## Beyond Recipes: Live Data and Execution
316
+ ## Beyond Recipes: Retrospective "Instance View" Data and Execution
311
317
 
312
318
  Recipes are *prospective* — they describe a computation template without holding
313
319
  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
+ run), flowrep provides two additional layers accessible through the `tools` and
321
+ `schemas` submodules of the API.
320
322
 
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:
323
+ **`flowrep.tools.recipe2data`** converts a recipe from the prospective "class view"
324
+ to the retrospective "instance view" — a mutable data structure whose input and output
325
+ ports can hold actual Python values. Retrospective data objects mirror the recipe graph
326
+ but trade JSON-serializability for the ability to carry arbitrary data:
325
327
 
326
328
  ```python
327
- >>> live_wf = frt.recipe2live(double_and_add.flowrep_recipe)
329
+ >>> wf_data = fr.tools.recipe2data(double_and_add.flowrep_recipe)
328
330
 
329
331
  ```
330
332
 
331
333
  **`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
334
+ the provided inputs and returns a fully populated data object. This is powered
333
335
  by a minimal, built-in WfMS intended as a reference implementation and for use
334
336
  in tests and documentation (like this!):
335
337
 
336
338
  ```python
337
- >>> retrospective = frt.run_recipe(
339
+ >>> retrospective = fr.tools.run_recipe(
338
340
  ... double_and_add.flowrep_recipe, a=3, b=100, target=40
339
341
  ... )
340
342
  >>> retrospective.output_ports["result"].value
@@ -342,8 +344,8 @@ in tests and documentation (like this!):
342
344
 
343
345
  ```
344
346
 
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
+ Because every child node's ports are populated too, the retrospective data graph gives
348
+ you full data provenance — you can walk the tree and inspect exactly what each node
347
349
  received and produced.
348
350
  For flow control nodes, which are _prospectively_ "black boxes", we find that
349
351
  _retrospectively_ they are simple DAGs.
@@ -360,14 +362,13 @@ each loop iteration:
360
362
  ```
361
363
 
362
364
  For a deeper look at all available node types, edge semantics, version
363
- provenance, and the live/WfMS layer, see the
365
+ provenance, and the retropective data/WfMS layer, see the
364
366
  [user guide](https://mybinder.org/v2/gh/pyiron/flowrep/HEAD?urlpath=%2Fdoc%2Ftree%2Fuser-guide.ipynb).
365
367
 
366
368
 
367
369
  ## Documentation
368
370
 
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
+ - The user guide notebook comprehensively covers all node types, edge models, flow control, versioning, retrospective data formats, a demo WfMS implementation, and recipe format converters.
371
372
  Launch it interactively on
372
373
  [mybinder](https://mybinder.org/v2/gh/pyiron/flowrep/HEAD?urlpath=%2Fdoc%2Ftree%2Fuser-guide.ipynb).
373
374
  - [readthedocs](https://flowrep.readthedocs.io/en/latest/)
@@ -14,10 +14,11 @@
14
14
  ## flowrep — Workflow Recipes from Python
15
15
 
16
16
  **flowrep** turns plain Python functions into shareable, versionable *workflow
17
- recipes* — JSON-serialisable graphs that describe *what* to compute (which
17
+ recipes* — JSON-serialisable "class view" graphs that describe *what* to compute (which
18
18
  functions, how they connect) without doing the computation or holding any data.
19
19
  Recipes are prospective blueprints that a Workflow Management System (WfMS) can
20
- digest, visualise, and execute.
20
+ digest, visualise, and execute. To support this, **flowrep** also provides a set of
21
+ retrospective, "instance view" data classes for WfMS to populate when executing recipes.
21
22
 
22
23
  Flowchart-style representations are already the lingua franca for describing
23
24
  processes in science and engineering. flowrep gives you a way to author them in
@@ -147,6 +148,11 @@ control structures you use in real code. Flow control nodes are inherently
147
148
  dynamic: their exact execution path depends on data and cannot be known until
148
149
  run-time, but their IO signature is always fully known a priori.
149
150
 
151
+ **Clear separation.** Of prospective, "class view" recipes, and retrospective,
152
+ "instance view" data objects.
153
+
154
+ **Convertable and compatible.** Recipes can be compiled back to python functions, and converted from [`python-workflow-definition`](https://github.com/pythonworkflow/python-workflow-definition/) recipes (also _to_ PWD, where recipe operations are the subset supported by the PWD framework).
155
+
150
156
  ### Example: flow control and nesting
151
157
 
152
158
  So far we've seen `"workflow"` nodes, and alluded to `"atomic"` nodes.
@@ -245,34 +251,30 @@ the decorated functions are still just python functions; second, to show in the
245
251
  section that the recipe we parse from this are intended to give the same result as
246
252
  these underlying functions when we run the recipe with a WfMS.
247
253
 
248
- ## Beyond Recipes: Live Data and Execution
254
+ ## Beyond Recipes: Retrospective "Instance View" Data and Execution
249
255
 
250
256
  Recipes are *prospective* — they describe a computation template without holding
251
257
  data. For retrospective analysis (inspecting what actually happened during a
252
- run), flowrep provides two additional layers accessible through the API:
253
-
254
- ```python
255
- >>> from flowrep.api import tools as frt
256
-
257
- ```
258
+ run), flowrep provides two additional layers accessible through the `tools` and
259
+ `schemas` submodules of the API.
258
260
 
259
- **`flowrep.api.tools.recipe2live`** converts a recipe into a *live* object — a mutable
260
- data structure whose input and output ports can hold actual Python values. Live
261
- objects mirror the recipe graph but trade JSON-serializability for the ability to
262
- carry arbitrary data:
261
+ **`flowrep.tools.recipe2data`** converts a recipe from the prospective "class view"
262
+ to the retrospective "instance view" — a mutable data structure whose input and output
263
+ ports can hold actual Python values. Retrospective data objects mirror the recipe graph
264
+ but trade JSON-serializability for the ability to carry arbitrary data:
263
265
 
264
266
  ```python
265
- >>> live_wf = frt.recipe2live(double_and_add.flowrep_recipe)
267
+ >>> wf_data = fr.tools.recipe2data(double_and_add.flowrep_recipe)
266
268
 
267
269
  ```
268
270
 
269
271
  **`flowrep.api.tools.run_recipe`** goes one step further: it executes the recipe with
270
- the provided inputs and returns a fully populated live object. This is powered
272
+ the provided inputs and returns a fully populated data object. This is powered
271
273
  by a minimal, built-in WfMS intended as a reference implementation and for use
272
274
  in tests and documentation (like this!):
273
275
 
274
276
  ```python
275
- >>> retrospective = frt.run_recipe(
277
+ >>> retrospective = fr.tools.run_recipe(
276
278
  ... double_and_add.flowrep_recipe, a=3, b=100, target=40
277
279
  ... )
278
280
  >>> retrospective.output_ports["result"].value
@@ -280,8 +282,8 @@ in tests and documentation (like this!):
280
282
 
281
283
  ```
282
284
 
283
- Because every child node's ports are populated too, the live graph gives you
284
- full data provenance — you can walk the tree and inspect exactly what each node
285
+ Because every child node's ports are populated too, the retrospective data graph gives
286
+ you full data provenance — you can walk the tree and inspect exactly what each node
285
287
  received and produced.
286
288
  For flow control nodes, which are _prospectively_ "black boxes", we find that
287
289
  _retrospectively_ they are simple DAGs.
@@ -298,14 +300,13 @@ each loop iteration:
298
300
  ```
299
301
 
300
302
  For a deeper look at all available node types, edge semantics, version
301
- provenance, and the live/WfMS layer, see the
303
+ provenance, and the retropective data/WfMS layer, see the
302
304
  [user guide](https://mybinder.org/v2/gh/pyiron/flowrep/HEAD?urlpath=%2Fdoc%2Ftree%2Fuser-guide.ipynb).
303
305
 
304
306
 
305
307
  ## Documentation
306
308
 
307
- - The user guide notebook comprehensively covers all node types, edge models, flow control, versioning,
308
- live/retrospective data formats, a demo WfMS implementation, and recipe format converters.
309
+ - The user guide notebook comprehensively covers all node types, edge models, flow control, versioning, retrospective data formats, a demo WfMS implementation, and recipe format converters.
309
310
  Launch it interactively on
310
311
  [mybinder](https://mybinder.org/v2/gh/pyiron/flowrep/HEAD?urlpath=%2Fdoc%2Ftree%2Fuser-guide.ipynb).
311
312
  - [readthedocs](https://flowrep.readthedocs.io/en/latest/)
@@ -1,5 +1,5 @@
1
1
  [build-system]
2
- requires = [ "hatchling==1.29.0", "hatch-vcs==0.5.0",]
2
+ requires = [ "hatchling==1.30.1", "hatch-vcs==0.5.0",]
3
3
  build-backend = "hatchling.build"
4
4
 
5
5
  [project]
@@ -9,7 +9,7 @@ readme = "docs/README.md"
9
9
  keywords = [ "pyiron",]
10
10
  requires-python = ">=3.11, <3.15"
11
11
  classifiers = [ "Development Status :: 4 - Beta", "Topic :: Scientific/Engineering", "License :: OSI Approved :: BSD License", "Intended Audience :: Science/Research", "Operating System :: OS Independent", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14",]
12
- dependencies = [ "pydantic>=2.12.0,<2.13.0", "pyiron_snippets>=1.2.1,<2.0.0",]
12
+ dependencies = [ "pydantic>=2.12.0,<2.13.0", "pyiron_snippets>=1.3.0,<2.0.0",]
13
13
  dynamic = [ "version",]
14
14
  [[project.authors]]
15
15
  name = "Sam Waseda"
@@ -28,9 +28,9 @@ Documentation = "https://flowrep.readthedocs.io"
28
28
  Repository = "https://github.com/pyiron/flowrep"
29
29
 
30
30
  [project.optional-dependencies]
31
- pwd = [ "python-workflow-definition==0.1.4",]
32
- notebooks = [ "numpy==2.4.3",]
33
- storage = [ "bagofholding==0.1.10",]
31
+ pwd = [ "python-workflow-definition==0.1.5",]
32
+ notebooks = [ "numpy==2.4.6",]
33
+ storage = [ "bagofholding==0.1.12",]
34
34
  storage-widget = [ "flowrep[storage]", "ipytree==0.2.2",]
35
35
 
36
36
  [tool.ruff]
@@ -6,10 +6,12 @@ attribute onto a function at definition time, and parsers (`parse_atomic`,
6
6
 
7
7
  import importlib.metadata
8
8
 
9
- from flowrep.api.tools import atomic as atomic
10
- from flowrep.api.tools import parse_atomic as parse_atomic
11
- from flowrep.api.tools import parse_workflow as parse_workflow
12
- from flowrep.api.tools import workflow as workflow
9
+ from flowrep.api import atomic as atomic
10
+ from flowrep.api import parse_atomic as parse_atomic
11
+ from flowrep.api import parse_workflow as parse_workflow
12
+ from flowrep.api import schemas as schemas
13
+ from flowrep.api import tools as tools
14
+ from flowrep.api import workflow as workflow
13
15
 
14
16
  try:
15
17
  # Installed package will find its version
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '0.5.1'
22
+ __version_tuple__ = version_tuple = (0, 5, 1)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,15 @@
1
+ """
2
+ The public interface for flowrep, protected by semver
3
+
4
+ The most fundamental user tools (parsing and decorating) are provided here at the top
5
+ level, while other resources are nested by category as tools or schemas. The shorter
6
+ tools list is likely to be useful for all power users, while schemas are intended to
7
+ primarily benefit downstream developers (e.g. WfMS creators).
8
+ """
9
+
10
+ from flowrep.api import schemas as schemas
11
+ from flowrep.api import tools as tools
12
+ from flowrep.api.tools import atomic as atomic
13
+ from flowrep.api.tools import parse_atomic as parse_atomic
14
+ from flowrep.api.tools import parse_workflow as parse_workflow
15
+ from flowrep.api.tools import workflow as workflow
@@ -0,0 +1,51 @@
1
+ """
2
+ Pydantic models, types and classes, enums, and constants used in constructing or
3
+ inspecting recipe models.
4
+
5
+ Primarily intended for power-users like workflow management system (WfMS) designers to
6
+ work deeply with recipe objects in a structured, well-typed way.
7
+ """
8
+
9
+ from flowrep.base_models import RESERVED_NAMES as RESERVED_NAMES
10
+ from flowrep.base_models import Label as Label
11
+ from flowrep.base_models import Labels as Labels
12
+ from flowrep.base_models import NodeRecipe as NodeRecipe
13
+ from flowrep.base_models import PythonReference as PythonReference
14
+ from flowrep.base_models import RecipeElementType as RecipeElementType
15
+ from flowrep.base_models import RestrictedParamKind as RestrictedParamKind
16
+ from flowrep.compiler.source import RenderedSource as RenderedSource
17
+ from flowrep.edge_models import Edges as Edges
18
+ from flowrep.edge_models import InputEdges as InputEdges
19
+ from flowrep.edge_models import InputSource as InputSource
20
+ from flowrep.edge_models import OutputEdges as OutputEdges
21
+ from flowrep.edge_models import OutputTarget as OutputTarget
22
+ from flowrep.edge_models import SourceHandle as SourceHandle
23
+ from flowrep.edge_models import TargetHandle as TargetHandle
24
+ from flowrep.prospective.atomic_recipe import AtomicRecipe as AtomicRecipe
25
+ from flowrep.prospective.atomic_recipe import UnpackMode as UnpackMode
26
+ from flowrep.prospective.for_recipe import ForEachRecipe as ForEachRecipe
27
+ from flowrep.prospective.helper_models import ConditionalCase as ConditionalCase
28
+ from flowrep.prospective.helper_models import ExceptionCase as ExceptionCase
29
+ from flowrep.prospective.helper_models import LabeledRecipe as LabeledRecipe
30
+ from flowrep.prospective.if_recipe import IfRecipe as IfRecipe
31
+ from flowrep.prospective.try_recipe import TryRecipe as TryRecipe
32
+ from flowrep.prospective.union_types import RecipeDiscrimination as RecipeDiscrimination
33
+ from flowrep.prospective.union_types import Recipes as Recipes
34
+ from flowrep.prospective.while_recipe import WhileRecipe as WhileRecipe
35
+ from flowrep.prospective.workflow_recipe import WorkflowRecipe as WorkflowRecipe
36
+ from flowrep.retrospective.datastructures import NOT_DATA as NOT_DATA
37
+ from flowrep.retrospective.datastructures import AtomicData as AtomicData
38
+ from flowrep.retrospective.datastructures import CompositeData as CompositeData
39
+ from flowrep.retrospective.datastructures import DagData as DagData
40
+ from flowrep.retrospective.datastructures import FlowControlData as FlowControlData
41
+ from flowrep.retrospective.datastructures import ForEachData as ForEachData
42
+ from flowrep.retrospective.datastructures import IfData as IfData
43
+ from flowrep.retrospective.datastructures import InputDataPort as InputDataPort
44
+ from flowrep.retrospective.datastructures import InputDataPorts as InputDataPorts
45
+ from flowrep.retrospective.datastructures import NodeData as NodeData
46
+ from flowrep.retrospective.datastructures import NotData as NotData
47
+ from flowrep.retrospective.datastructures import OutputDataPort as OutputDataPort
48
+ from flowrep.retrospective.datastructures import OutputDataPorts as OutputDataPorts
49
+ from flowrep.retrospective.datastructures import TryData as TryData
50
+ from flowrep.retrospective.datastructures import WhileData as WhileData
51
+ from flowrep.subgraph_validation import ProspectiveOutputEdges as ProspectiveOutputEdges
@@ -0,0 +1,17 @@
1
+ """
2
+ Functions for creating and transforming flowrep data.
3
+
4
+ Intended for users who want to go beyond the simple parsers and decorators, as well as
5
+ for downstream library developers.
6
+ """
7
+
8
+ from flowrep.compiler.source import flowrep2python as flowrep2python
9
+ from flowrep.converters.python_workflow_definition import flowrep2pwd as flowrep2pwd
10
+ from flowrep.converters.python_workflow_definition import pwd2flowrep as pwd2flowrep
11
+ from flowrep.parsers.atomic_parser import atomic as atomic
12
+ from flowrep.parsers.atomic_parser import parse_atomic as parse_atomic
13
+ from flowrep.parsers.workflow_parser import parse_workflow as parse_workflow
14
+ from flowrep.parsers.workflow_parser import workflow as workflow
15
+ from flowrep.retrospective.datastructures import recipe2data as recipe2data
16
+ from flowrep.retrospective.storage import LexicalBagBrowser as LexicalBagBrowser
17
+ from flowrep.wfms import run_recipe as run_recipe
@@ -69,7 +69,7 @@ UniqueList = Annotated[list[T], pydantic.AfterValidator(validate_unique)]
69
69
  Labels = UniqueList[Label]
70
70
 
71
71
 
72
- class NodeModel(pydantic.BaseModel):
72
+ class NodeRecipe(pydantic.BaseModel):
73
73
  type: RecipeElementType
74
74
  inputs: Labels
75
75
  outputs: Labels
@@ -78,7 +78,7 @@ class NodeModel(pydantic.BaseModel):
78
78
  @classmethod
79
79
  def __pydantic_init_subclass__(cls, **kwargs):
80
80
  super().__pydantic_init_subclass__(**kwargs)
81
- if cls.__name__ != NodeModel.__name__: # I.e. for subclasses
81
+ if cls.__name__ != NodeRecipe.__name__: # I.e. for subclasses
82
82
  type_field = cls.model_fields["type"]
83
83
  if type_field.default is pydantic_core.PydanticUndefined:
84
84
  raise TypeError(
@@ -0,0 +1,4 @@
1
+ """
2
+ This module handles the reverse transformation of flowrep objects back to python whence
3
+ they can be parsed.
4
+ """
@@ -0,0 +1,141 @@
1
+ """Render Python defaults and annotations into self-contained source text.
2
+
3
+ Each helper produces candidate source text and then verifies that the text
4
+ reconstructs the original object (render-then-verify). On any failure it returns
5
+ ``None`` so the caller can fall back to binding the live object into a namespace.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import ast
11
+ import builtins
12
+ import importlib
13
+ import types
14
+ import typing
15
+ from typing import Any
16
+
17
+ from pyiron_snippets import versions
18
+
19
+ _ANNOTATED_ORIGIN_SENTINEL = typing.get_origin(typing.Annotated[int, "x"])
20
+
21
+
22
+ def render_annotation(ann: Any, imports: set[str]) -> str | None:
23
+ """Return verified source text for an annotation, or ``None``.
24
+
25
+ Mutates ``imports`` with the bare module name for every non-builtin module
26
+ the text references. Returns ``None`` (without leaving partial imports
27
+ behind) when any part of the annotation cannot be rendered or the rendered
28
+ text fails to evaluate back to an equal object.
29
+ """
30
+ local_imports: set[str] = set()
31
+ text = _render(ann, local_imports)
32
+ if text is None:
33
+ return None
34
+ if text == "None": # None / NoneType both legal and normalized by get_type_hints
35
+ return "None"
36
+ if not _verifies(text, ann, local_imports): # pragma: no cover - defensive
37
+ return None
38
+ imports.update(local_imports)
39
+ return text
40
+
41
+
42
+ def _render(ann: Any, imports: set[str]) -> str | None:
43
+ if ann is None or ann is type(None):
44
+ return "None"
45
+
46
+ origin = typing.get_origin(ann)
47
+ if origin is not None:
48
+ return _render_subscripted(ann, origin, imports)
49
+
50
+ if isinstance(ann, type):
51
+ return _render_plain_type(ann, imports)
52
+ return None
53
+
54
+
55
+ def _render_subscripted(ann: Any, origin: Any, imports: set[str]) -> str | None:
56
+ args = typing.get_args(ann)
57
+
58
+ # Union / Optional: render members joined by ` | `. typing.Union[...] has
59
+ # origin typing.Union; the PEP 604 `int | str` form has origin
60
+ # types.UnionType. Both compare equal after eval, so render both as `A | B`.
61
+ if origin is typing.Union or origin is types.UnionType:
62
+ rendered = [_render(a, imports) for a in args]
63
+ if any(part is None for part in rendered):
64
+ return None
65
+ return " | ".join(part for part in rendered if part is not None)
66
+
67
+ # Annotated[T, *meta]: base via _render, metadata via render_default first
68
+ # (literals) then _render (types).
69
+ if origin is _ANNOTATED_ORIGIN_SENTINEL:
70
+ base = _render(args[0], imports)
71
+ if base is None:
72
+ return None
73
+ meta_texts = []
74
+ for meta in args[1:]:
75
+ meta_text = render_default(meta)
76
+ if meta_text is None:
77
+ meta_text = _render(meta, imports)
78
+ if meta_text is None:
79
+ return None
80
+ meta_texts.append(meta_text)
81
+ return f"typing.Annotated[{', '.join([base, *meta_texts])}]"
82
+
83
+ # Literal[...]: every arg must be a literal value.
84
+ if origin is typing.Literal:
85
+ arg_texts = [render_default(a) for a in args]
86
+ if any(part is None for part in arg_texts):
87
+ return None
88
+ return f"typing.Literal[{', '.join(part for part in arg_texts if part)}]"
89
+
90
+ # Generic alias over a real origin type (list[int], dict[str, int], ...).
91
+ if isinstance(origin, type):
92
+ origin_text = _render_plain_type(origin, imports)
93
+ if origin_text is None:
94
+ return None
95
+ arg_texts = [_render(a, imports) for a in args]
96
+ if any(part is None for part in arg_texts):
97
+ return None
98
+ return f"{origin_text}[{', '.join(part for part in arg_texts if part)}]"
99
+
100
+ return None
101
+
102
+
103
+ def _render_plain_type(ann: type, imports: set[str]) -> str | None:
104
+ info = versions.VersionInfo.of(ann)
105
+ if info.is_local or info.is_lambda or info.in_main:
106
+ return None
107
+ if info.module != "builtins":
108
+ imports.add(info.module)
109
+ return info.findable_at
110
+
111
+
112
+ def _verifies(text: str, ann: Any, imports: set[str]) -> bool:
113
+ namespace: dict[str, Any] = {"__builtins__": builtins}
114
+ namespace["typing"] = importlib.import_module("typing")
115
+ for line in imports:
116
+ top = line.split(".")[0]
117
+ namespace[top] = importlib.import_module(top)
118
+ try:
119
+ evaluated = eval(text, namespace) # noqa: S307 - controlled codegen text
120
+ except Exception: # noqa: BLE001 # pragma: no cover - defensive verify net
121
+ return False
122
+ return evaluated == ann or evaluated is ann
123
+
124
+
125
+ def render_default(value: Any) -> str | None:
126
+ """Return source text for a *literal* default, or ``None``.
127
+
128
+ The text is accepted only when ``ast.literal_eval`` reproduces a value equal
129
+ to (and of the same type as) the input. This covers ints, floats, strs,
130
+ bytes, bools, ``None``, and nested tuple/list/dict/set of these, and rejects
131
+ sentinels, enums, ``nan``, ``frozenset`` (no literal syntax), and arbitrary
132
+ instances. No imports are required for any accepted value.
133
+ """
134
+ try:
135
+ text = repr(value)
136
+ parsed = ast.literal_eval(text)
137
+ except (ValueError, SyntaxError, TypeError, MemoryError, RecursionError):
138
+ return None
139
+ if type(parsed) is type(value) and parsed == value:
140
+ return text
141
+ return None