flowrep 0.2.0__tar.gz → 0.3.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.
- {flowrep-0.2.0 → flowrep-0.3.0}/.gitignore +1 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/PKG-INFO +3 -1
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/_version.py +2 -2
- flowrep-0.3.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/live.py +9 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/schemas.py +19 -3
- flowrep-0.3.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/wfms.py +8 -0
- flowrep-0.3.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/live.py +303 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/for_model.py +6 -1
- {flowrep-0.2.0 → flowrep-0.3.0/cached-miniforge/my-env/lib/python3.1/site-packages}/flowrep/models/parsers/atomic_parser.py +4 -3
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/dependency_parser.py +15 -2
- flowrep-0.3.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/import_parser.py +51 -0
- {flowrep-0.2.0 → flowrep-0.3.0/cached-miniforge/my-env/lib/python3.1/site-packages}/flowrep/models/parsers/object_scope.py +63 -13
- flowrep-0.3.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/wfms.py +502 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/_version.py +2 -2
- flowrep-0.3.0/flowrep/models/api/live.py +9 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/api/schemas.py +19 -3
- flowrep-0.3.0/flowrep/models/api/wfms.py +8 -0
- flowrep-0.3.0/flowrep/models/live.py +303 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/nodes/for_model.py +6 -1
- {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages → flowrep-0.3.0}/flowrep/models/parsers/atomic_parser.py +4 -3
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/dependency_parser.py +15 -2
- flowrep-0.3.0/flowrep/models/parsers/import_parser.py +51 -0
- {flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages → flowrep-0.3.0}/flowrep/models/parsers/object_scope.py +63 -13
- flowrep-0.3.0/flowrep/models/wfms.py +502 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/pyproject.toml +1 -0
- flowrep-0.2.0/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/nodes.py +0 -13
- flowrep-0.2.0/flowrep/models/api/nodes.py +0 -13
- {flowrep-0.2.0 → flowrep-0.3.0}/LICENSE +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/converters.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/api/parsers.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/base_models.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/converters/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/converters/python_workflow_definition.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/edge_models.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/atomic_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/helper_models.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/if_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/try_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/union.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/while_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/nodes/workflow_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/case_helpers.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/for_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/if_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/label_helpers.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/parser_helpers.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/parser_protocol.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/symbol_scope.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/try_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/while_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/parsers/workflow_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/models/subgraph_validation.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/tools.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/cached-miniforge/my-env/lib/python3.1/site-packages/flowrep/workflow.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/docs/README.md +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/api/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/api/converters.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/api/parsers.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/base_models.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/converters/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/converters/python_workflow_definition.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/edge_models.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/nodes/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/nodes/atomic_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/nodes/helper_models.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/nodes/if_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/nodes/try_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/nodes/union.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/nodes/while_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/nodes/workflow_model.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/__init__.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/case_helpers.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/for_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/if_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/label_helpers.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/parser_helpers.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/parser_protocol.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/symbol_scope.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/try_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/while_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/parsers/workflow_parser.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/models/subgraph_validation.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/tools.py +0 -0
- {flowrep-0.2.0 → flowrep-0.3.0}/flowrep/workflow.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flowrep
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
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
|
|
@@ -49,6 +49,8 @@ Requires-Python: <3.14,>=3.11
|
|
|
49
49
|
Requires-Dist: networkx<3.7.0,>=3.4.2
|
|
50
50
|
Requires-Dist: pydantic<2.13.0,>=2.12.0
|
|
51
51
|
Requires-Dist: pyiron-snippets<2.0.0,>=1.2.1
|
|
52
|
+
Provides-Extra: notebooks
|
|
53
|
+
Requires-Dist: numpy==2.4.2; extra == 'notebooks'
|
|
52
54
|
Provides-Extra: pwd
|
|
53
55
|
Requires-Dist: python-workflow-definition==0.1.4; extra == 'pwd'
|
|
54
56
|
Description-Content-Type: text/markdown
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '0.
|
|
32
|
-
__version_tuple__ = version_tuple = (0,
|
|
31
|
+
__version__ = version = '0.3.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (0, 3, 0)
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Data classes for "live" stateful instance-views of the recipes.
|
|
3
|
+
|
|
4
|
+
Intended to be a common export- and communication-format for WfMS of instance-views.
|
|
5
|
+
Since they hold live, arbitrary python objects, they don't serialize trivially and are
|
|
6
|
+
not (by themselves) a valid storage format.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from flowrep.models.live import recipe2live as recipe2live
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Pydantic models, enums, and
|
|
2
|
+
Pydantic models, types and classes, enums, and constants used in constructing or
|
|
3
|
+
inspecting recipe models.
|
|
3
4
|
|
|
4
|
-
|
|
5
|
-
with recipe objects in a structured, well-typed way.
|
|
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.
|
|
6
7
|
"""
|
|
7
8
|
|
|
8
9
|
from flowrep.models.base_models import RESERVED_NAMES as RESERVED_NAMES
|
|
@@ -17,9 +18,24 @@ from flowrep.models.edge_models import InputSource as InputSource
|
|
|
17
18
|
from flowrep.models.edge_models import OutputTarget as OutputTarget
|
|
18
19
|
from flowrep.models.edge_models import SourceHandle as SourceHandle
|
|
19
20
|
from flowrep.models.edge_models import TargetHandle as TargetHandle
|
|
21
|
+
from flowrep.models.live import NOT_DATA as NOT_DATA
|
|
22
|
+
from flowrep.models.live import Atomic as Atomic
|
|
23
|
+
from flowrep.models.live import Composite as Composite
|
|
24
|
+
from flowrep.models.live import FlowControl as FlowControl
|
|
25
|
+
from flowrep.models.live import InputPort as InputPort
|
|
26
|
+
from flowrep.models.live import LiveNode as LiveNode
|
|
27
|
+
from flowrep.models.live import NotData as NotData
|
|
28
|
+
from flowrep.models.live import OutputPort as OutputPort
|
|
29
|
+
from flowrep.models.live import Workflow as Workflow
|
|
30
|
+
from flowrep.models.nodes.atomic_model import AtomicNode as AtomicNode
|
|
20
31
|
from flowrep.models.nodes.atomic_model import UnpackMode as UnpackMode
|
|
32
|
+
from flowrep.models.nodes.for_model import ForNode as ForNode
|
|
21
33
|
from flowrep.models.nodes.helper_models import ConditionalCase as ConditionalCase
|
|
22
34
|
from flowrep.models.nodes.helper_models import ExceptionCase as ExceptionCase
|
|
23
35
|
from flowrep.models.nodes.helper_models import LabeledNode as LabeledNode
|
|
36
|
+
from flowrep.models.nodes.if_model import IfNode as IfNode
|
|
37
|
+
from flowrep.models.nodes.try_model import TryNode as TryNode
|
|
24
38
|
from flowrep.models.nodes.union import Nodes as Nodes
|
|
25
39
|
from flowrep.models.nodes.union import NodeType as NodeType
|
|
40
|
+
from flowrep.models.nodes.while_model import WhileNode as WhileNode
|
|
41
|
+
from flowrep.models.nodes.workflow_model import WorkflowNode as WorkflowNode
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Flowrep recipes represent a class-view of how data can be processed via a workflow.
|
|
3
|
+
|
|
4
|
+
In this module, we provide a prototypical data structure for live, instance-view
|
|
5
|
+
workflows, which can be mutated as they are executed to be enriched with data.
|
|
6
|
+
|
|
7
|
+
Unlike the recipes, no goal is made to provide easy serialization, and these data
|
|
8
|
+
structures natively hold complex python objects.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import abc
|
|
14
|
+
import dataclasses
|
|
15
|
+
import inspect
|
|
16
|
+
import types
|
|
17
|
+
from collections.abc import Callable, MutableMapping
|
|
18
|
+
from typing import Any, get_args, get_origin, get_type_hints
|
|
19
|
+
|
|
20
|
+
from pyiron_snippets import dotdict, retrieve, singleton
|
|
21
|
+
|
|
22
|
+
from flowrep.models import base_models, edge_models
|
|
23
|
+
from flowrep.models.nodes import (
|
|
24
|
+
atomic_model,
|
|
25
|
+
for_model,
|
|
26
|
+
if_model,
|
|
27
|
+
try_model,
|
|
28
|
+
union,
|
|
29
|
+
while_model,
|
|
30
|
+
workflow_model,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NotData(metaclass=singleton.Singleton):
|
|
35
|
+
"""
|
|
36
|
+
This class exists purely to initialize data channel values where no default value
|
|
37
|
+
is provided; it lets the channel know that it has _no data in it_ and thus should
|
|
38
|
+
not identify as ready.
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def __repr__(cls):
|
|
43
|
+
# We use the class directly (not instances of it) where there is not yet data
|
|
44
|
+
# So give it a decent repr, even as just a class
|
|
45
|
+
return "NOT_DATA"
|
|
46
|
+
|
|
47
|
+
def __reduce__(self):
|
|
48
|
+
return "NOT_DATA"
|
|
49
|
+
|
|
50
|
+
def __bool__(self):
|
|
51
|
+
return False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
NOT_DATA = NotData()
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclasses.dataclass(frozen=False)
|
|
58
|
+
class _Port:
|
|
59
|
+
value: object | NotData = NOT_DATA
|
|
60
|
+
annotation: Any | None = None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclasses.dataclass(frozen=False)
|
|
64
|
+
class InputPort(_Port):
|
|
65
|
+
default: object | NotData = NOT_DATA
|
|
66
|
+
|
|
67
|
+
def get_data(self) -> object | NotData:
|
|
68
|
+
"""A shortcut for falling back on the default"""
|
|
69
|
+
return self.default if self.value is NOT_DATA else self.value
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclasses.dataclass(frozen=False)
|
|
73
|
+
class OutputPort(_Port): ...
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclasses.dataclass(frozen=False)
|
|
77
|
+
class LiveNode(abc.ABC):
|
|
78
|
+
recipe: union.NodeType
|
|
79
|
+
input_ports: MutableMapping[base_models.Label, InputPort]
|
|
80
|
+
output_ports: MutableMapping[base_models.Label, OutputPort]
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def recipe2live(recipe: union.NodeType) -> LiveNode:
|
|
84
|
+
match recipe:
|
|
85
|
+
case atomic_model.AtomicNode():
|
|
86
|
+
return Atomic.from_recipe(recipe)
|
|
87
|
+
case for_model.ForNode():
|
|
88
|
+
return FlowControl.from_recipe(recipe)
|
|
89
|
+
case if_model.IfNode():
|
|
90
|
+
return FlowControl.from_recipe(recipe)
|
|
91
|
+
case try_model.TryNode():
|
|
92
|
+
return FlowControl.from_recipe(recipe)
|
|
93
|
+
case while_model.WhileNode():
|
|
94
|
+
return FlowControl.from_recipe(recipe)
|
|
95
|
+
case workflow_model.WorkflowNode():
|
|
96
|
+
return Workflow.from_recipe(recipe)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@dataclasses.dataclass(frozen=False)
|
|
100
|
+
class Atomic(LiveNode):
|
|
101
|
+
function: Callable
|
|
102
|
+
|
|
103
|
+
@classmethod
|
|
104
|
+
def from_recipe(cls, recipe: atomic_model.AtomicNode) -> Atomic:
|
|
105
|
+
function, input_ports, output_ports = _parse_function(
|
|
106
|
+
recipe.reference.info.fully_qualified_name,
|
|
107
|
+
recipe.inputs,
|
|
108
|
+
recipe.outputs,
|
|
109
|
+
recipe.unpack_mode,
|
|
110
|
+
)
|
|
111
|
+
return Atomic(
|
|
112
|
+
recipe=recipe,
|
|
113
|
+
input_ports=dotdict.DotDict(input_ports),
|
|
114
|
+
output_ports=dotdict.DotDict(output_ports),
|
|
115
|
+
function=function,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@dataclasses.dataclass(frozen=False)
|
|
120
|
+
class Composite(LiveNode, abc.ABC):
|
|
121
|
+
nodes: MutableMapping[base_models.Label, LiveNode]
|
|
122
|
+
input_edges: edge_models.InputEdges
|
|
123
|
+
edges: edge_models.Edges
|
|
124
|
+
output_edges: edge_models.OutputEdges
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclasses.dataclass(frozen=False)
|
|
128
|
+
class Workflow(Composite):
|
|
129
|
+
@classmethod
|
|
130
|
+
def from_recipe(cls, recipe: workflow_model.WorkflowNode) -> Workflow:
|
|
131
|
+
if recipe.reference:
|
|
132
|
+
function, input_ports, output_ports = _parse_function(
|
|
133
|
+
recipe.reference.info.fully_qualified_name,
|
|
134
|
+
recipe.inputs,
|
|
135
|
+
recipe.outputs,
|
|
136
|
+
)
|
|
137
|
+
else:
|
|
138
|
+
input_ports = {label: InputPort() for label in recipe.inputs}
|
|
139
|
+
output_ports = {label: OutputPort() for label in recipe.outputs}
|
|
140
|
+
nodes = {label: recipe2live(child) for label, child in recipe.nodes.items()}
|
|
141
|
+
return Workflow(
|
|
142
|
+
recipe=recipe,
|
|
143
|
+
input_ports=dotdict.DotDict(input_ports),
|
|
144
|
+
output_ports=dotdict.DotDict(output_ports),
|
|
145
|
+
nodes=dotdict.DotDict(nodes),
|
|
146
|
+
input_edges=dict(recipe.input_edges),
|
|
147
|
+
edges=dict(recipe.edges),
|
|
148
|
+
output_edges=dict(recipe.output_edges),
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# TODO: add/remove_node/edge/input/output methods, each guarded that they are
|
|
152
|
+
# unavailable if the underlying recipe has a reference, and otherwise mutatiting
|
|
153
|
+
# the underlying recipe at the same time
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclasses.dataclass(frozen=False)
|
|
157
|
+
class FlowControl(Composite):
|
|
158
|
+
@classmethod
|
|
159
|
+
def from_recipe(
|
|
160
|
+
cls,
|
|
161
|
+
recipe: (
|
|
162
|
+
for_model.ForNode
|
|
163
|
+
| if_model.IfNode
|
|
164
|
+
| try_model.TryNode
|
|
165
|
+
| while_model.WhileNode
|
|
166
|
+
),
|
|
167
|
+
) -> FlowControl:
|
|
168
|
+
"""
|
|
169
|
+
Flow control nodes are composite with dynamic bodies; WfMS must populate the
|
|
170
|
+
nodes and edges at runtime according to recipe execution.
|
|
171
|
+
"""
|
|
172
|
+
return FlowControl(
|
|
173
|
+
recipe=recipe,
|
|
174
|
+
input_ports=dotdict.DotDict(
|
|
175
|
+
{label: InputPort() for label in recipe.inputs}
|
|
176
|
+
),
|
|
177
|
+
output_ports=dotdict.DotDict(
|
|
178
|
+
{label: OutputPort() for label in recipe.outputs}
|
|
179
|
+
),
|
|
180
|
+
nodes=dotdict.DotDict(),
|
|
181
|
+
input_edges={},
|
|
182
|
+
edges={},
|
|
183
|
+
output_edges={},
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _parse_function(
|
|
188
|
+
fully_qualified_name: str,
|
|
189
|
+
inputs: list[str],
|
|
190
|
+
outputs: list[str],
|
|
191
|
+
unpack_mode: atomic_model.UnpackMode = atomic_model.UnpackMode.TUPLE,
|
|
192
|
+
) -> tuple[
|
|
193
|
+
types.FunctionType,
|
|
194
|
+
dict[base_models.Label, InputPort],
|
|
195
|
+
dict[base_models.Label, OutputPort],
|
|
196
|
+
]:
|
|
197
|
+
function = retrieve.import_from_string(fully_qualified_name)
|
|
198
|
+
hints = get_type_hints(function, include_extras=True)
|
|
199
|
+
sig = inspect.signature(function)
|
|
200
|
+
|
|
201
|
+
# --- input ports ---
|
|
202
|
+
available = set(sig.parameters)
|
|
203
|
+
missing = set(inputs) - available
|
|
204
|
+
if missing:
|
|
205
|
+
raise ValueError(
|
|
206
|
+
f"Requested inputs {missing} not found in signature of {fully_qualified_name!r}"
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
input_ports: dict[str, InputPort] = {}
|
|
210
|
+
for name in inputs:
|
|
211
|
+
param = sig.parameters[name]
|
|
212
|
+
input_port = InputPort()
|
|
213
|
+
input_port.annotation = hints.get(name, None)
|
|
214
|
+
input_port.default = (
|
|
215
|
+
param.default if param.default is not inspect.Parameter.empty else NOT_DATA
|
|
216
|
+
)
|
|
217
|
+
input_ports[name] = input_port
|
|
218
|
+
|
|
219
|
+
# --- output ports ---
|
|
220
|
+
return_annotation = hints.get("return", None)
|
|
221
|
+
if unpack_mode == atomic_model.UnpackMode.NONE:
|
|
222
|
+
output_ports = _parse_return_without_unpacking(return_annotation, outputs)
|
|
223
|
+
elif unpack_mode == atomic_model.UnpackMode.TUPLE:
|
|
224
|
+
output_ports = _parse_return_tuple(return_annotation, outputs)
|
|
225
|
+
elif unpack_mode == atomic_model.UnpackMode.DATACLASS:
|
|
226
|
+
output_ports = _parse_return_dataclass(return_annotation, outputs)
|
|
227
|
+
|
|
228
|
+
return function, input_ports, output_ports
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _parse_return_without_unpacking(
|
|
232
|
+
return_annotation, outputs: list[str]
|
|
233
|
+
) -> dict[str, OutputPort]:
|
|
234
|
+
if len(outputs) != 1: # pragma: no cover
|
|
235
|
+
raise ValueError(
|
|
236
|
+
f"Without return unpacking, only one output is allowed, but got {outputs}. "
|
|
237
|
+
f"This should have been caught by the underlying recipe validation. Please "
|
|
238
|
+
f"raise a GitHub issue reporting how you got here!"
|
|
239
|
+
)
|
|
240
|
+
return {outputs[0]: OutputPort(annotation=return_annotation)}
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _parse_return_tuple(return_annotation, outputs: list[str]) -> dict[str, OutputPort]:
|
|
244
|
+
output_ports: dict[str, OutputPort]
|
|
245
|
+
if len(outputs) > 1:
|
|
246
|
+
origin = get_origin(return_annotation)
|
|
247
|
+
args = get_args(return_annotation)
|
|
248
|
+
|
|
249
|
+
if return_annotation is not None:
|
|
250
|
+
unpacking_hint = (
|
|
251
|
+
f"To collect the entire tuple in a single port use "
|
|
252
|
+
f"{atomic_model.UnpackMode.NONE} unpacking mode."
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
if origin is not tuple:
|
|
256
|
+
raise ValueError(
|
|
257
|
+
f"Multiple outputs {outputs} requested but return annotation "
|
|
258
|
+
f"{return_annotation!r} is not splittable -- only tuple return "
|
|
259
|
+
f"hints are splittable. {unpacking_hint}"
|
|
260
|
+
)
|
|
261
|
+
if len(args) != len(outputs):
|
|
262
|
+
raise ValueError(
|
|
263
|
+
f"Output labels {outputs} (n={len(outputs)}) do not match "
|
|
264
|
+
f"length of return annotation {return_annotation} (n={len(args)}). "
|
|
265
|
+
f"Tuple return hint unpacking requires one hint element per output."
|
|
266
|
+
f" {unpacking_hint}"
|
|
267
|
+
)
|
|
268
|
+
output_ports = {
|
|
269
|
+
label: OutputPort(annotation=annotation)
|
|
270
|
+
for label, annotation in zip(outputs, args, strict=True)
|
|
271
|
+
}
|
|
272
|
+
else:
|
|
273
|
+
output_ports = {label: OutputPort() for label in outputs}
|
|
274
|
+
else:
|
|
275
|
+
output_ports = {outputs[0]: OutputPort(annotation=return_annotation)}
|
|
276
|
+
return output_ports
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _parse_return_dataclass(
|
|
280
|
+
return_annotation, outputs: list[str]
|
|
281
|
+
) -> dict[str, OutputPort]:
|
|
282
|
+
if not dataclasses.is_dataclass(return_annotation): # pragma: no cover
|
|
283
|
+
raise TypeError(
|
|
284
|
+
f"Return annotation {return_annotation!r} is not a dataclass. This should "
|
|
285
|
+
f"have been caught by the underlying recipe validation. Please raise a "
|
|
286
|
+
f"GitHub issue reporting how you got here!"
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
fields = dataclasses.fields(return_annotation)
|
|
290
|
+
if len(outputs) != len(fields): # pragma: no cover
|
|
291
|
+
raise ValueError(
|
|
292
|
+
f"Return dataclass {return_annotation!r} has {len(fields)} fields, "
|
|
293
|
+
f"{[f.name for f in fields]}, but {len(outputs)} outputs, {outputs} were "
|
|
294
|
+
f"requested. This should have been caught by the underlying recipe "
|
|
295
|
+
f"validation. Please raise a GitHub issue reporting how you got here!"
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
label: OutputPort(
|
|
300
|
+
annotation=(field.type if field.type is not dataclasses.MISSING else None),
|
|
301
|
+
)
|
|
302
|
+
for label, field in zip(outputs, fields, strict=True)
|
|
303
|
+
}
|
|
@@ -59,7 +59,12 @@ class ForNode(base_models.NodeModel):
|
|
|
59
59
|
zipped_ports: The body node ports over which to do zipped iteration. Input
|
|
60
60
|
edges will map parent input elements to each child node accordingly.
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
Notes:
|
|
63
|
+
At runtime, iterated input values should themselves be iterable. It is
|
|
64
|
+
recommended to pass values conforming to `collections.abc.Collection`. This is
|
|
65
|
+
a runtime behaviour, and is thus not enforced here at the recipe level in any
|
|
66
|
+
way.
|
|
67
|
+
|
|
63
68
|
All iterated output — whether collected from body executions or forwarded from
|
|
64
69
|
scattered inputs — should have the same length. Thus, forwarded inputs empower
|
|
65
70
|
the node output to precisely provide which input was used to produce each
|
|
@@ -119,9 +119,10 @@ def parse_atomic(
|
|
|
119
119
|
scraped_output_labels = _get_output_labels(func, unpack_mode)
|
|
120
120
|
if len(output_labels) > 0 and len(output_labels) != len(scraped_output_labels):
|
|
121
121
|
raise ValueError(
|
|
122
|
-
|
|
123
|
-
f"
|
|
124
|
-
f"
|
|
122
|
+
"Explicitly provided output labels must match the function analysis and "
|
|
123
|
+
f"unpack_mode: expected {len(scraped_output_labels)} labels for "
|
|
124
|
+
f"unpack_mode='{unpack_mode}', got {len(output_labels)} labels "
|
|
125
|
+
f"{output_labels}; inferred labels were {scraped_output_labels}."
|
|
125
126
|
)
|
|
126
127
|
|
|
127
128
|
return atomic_model.AtomicNode(
|
|
@@ -4,7 +4,7 @@ from collections.abc import Callable
|
|
|
4
4
|
|
|
5
5
|
from pyiron_snippets import versions
|
|
6
6
|
|
|
7
|
-
from flowrep.models.parsers import object_scope, parser_helpers
|
|
7
|
+
from flowrep.models.parsers import import_parser, object_scope, parser_helpers
|
|
8
8
|
|
|
9
9
|
CallDependencies = dict[versions.VersionInfo, Callable]
|
|
10
10
|
|
|
@@ -43,10 +43,13 @@ def get_call_dependencies(
|
|
|
43
43
|
return call_dependencies
|
|
44
44
|
visited.add(func_fqn)
|
|
45
45
|
|
|
46
|
-
scope = object_scope.get_scope(func)
|
|
47
46
|
tree = parser_helpers.get_ast_function_node(func)
|
|
48
47
|
collector = CallCollector()
|
|
49
48
|
collector.visit(tree)
|
|
49
|
+
local_modules = import_parser.build_scope(collector.imports, collector.import_froms)
|
|
50
|
+
scope = object_scope.get_scope(func)
|
|
51
|
+
for name, obj in local_modules.items():
|
|
52
|
+
scope.register(name=name, obj=obj)
|
|
50
53
|
|
|
51
54
|
for call in collector.calls:
|
|
52
55
|
try:
|
|
@@ -105,7 +108,17 @@ def split_by_version_availability(
|
|
|
105
108
|
class CallCollector(ast.NodeVisitor):
|
|
106
109
|
def __init__(self):
|
|
107
110
|
self.calls: list[ast.expr] = []
|
|
111
|
+
self.imports: list[ast.Import] = []
|
|
112
|
+
self.import_froms: list[ast.ImportFrom] = []
|
|
108
113
|
|
|
109
114
|
def visit_Call(self, node: ast.Call) -> None:
|
|
110
115
|
self.calls.append(node.func)
|
|
111
116
|
self.generic_visit(node)
|
|
117
|
+
|
|
118
|
+
def visit_Import(self, node: ast.Import) -> None:
|
|
119
|
+
self.imports.append(node)
|
|
120
|
+
self.generic_visit(node)
|
|
121
|
+
|
|
122
|
+
def visit_ImportFrom(self, node: ast.ImportFrom) -> None:
|
|
123
|
+
self.import_froms.append(node)
|
|
124
|
+
self.generic_visit(node)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import importlib
|
|
3
|
+
|
|
4
|
+
from flowrep.models.parsers import object_scope
|
|
5
|
+
from flowrep.models.parsers.object_scope import ScopeProxy
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def build_scope(
|
|
9
|
+
imports: list[ast.Import] | None = None,
|
|
10
|
+
import_froms: list[ast.ImportFrom] | None = None,
|
|
11
|
+
) -> object_scope.ScopeProxy:
|
|
12
|
+
"""
|
|
13
|
+
Build a scope dictionary from a list of `import` and `from ... import ...` statements.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
imports (list | None): A list of `ast.Import` nodes.
|
|
17
|
+
import_froms (list | None): A list of `ast.ImportFrom` nodes.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
object_scope.ScopeProxy: A mutable mapping representing the scope with imported
|
|
21
|
+
modules and objects.
|
|
22
|
+
"""
|
|
23
|
+
scope = ScopeProxy()
|
|
24
|
+
|
|
25
|
+
imports = imports or []
|
|
26
|
+
import_froms = import_froms or []
|
|
27
|
+
|
|
28
|
+
# Handle `import` statements
|
|
29
|
+
for imp in imports:
|
|
30
|
+
for alias in imp.names:
|
|
31
|
+
asname = alias.asname or alias.name
|
|
32
|
+
module = importlib.import_module(alias.name)
|
|
33
|
+
scope.register(name=asname, obj=module)
|
|
34
|
+
|
|
35
|
+
# Handle `from ... import ...` statements
|
|
36
|
+
for imp_from in import_froms:
|
|
37
|
+
level = imp_from.level
|
|
38
|
+
# Dynamically import the module (absolute or relative)
|
|
39
|
+
if imp_from.module is None or level > 0:
|
|
40
|
+
raise ValueError(
|
|
41
|
+
f"Relative imports are not supported in dependency parsing. "
|
|
42
|
+
f"Encountered importing from {imp_from.module}."
|
|
43
|
+
)
|
|
44
|
+
module = importlib.import_module(imp_from.module)
|
|
45
|
+
for alias in imp_from.names:
|
|
46
|
+
name = alias.name
|
|
47
|
+
asname = alias.asname or name
|
|
48
|
+
obj = getattr(module, name)
|
|
49
|
+
scope.register(name=asname, obj=obj)
|
|
50
|
+
|
|
51
|
+
return scope
|
|
@@ -3,24 +3,63 @@ from __future__ import annotations
|
|
|
3
3
|
import ast
|
|
4
4
|
import builtins
|
|
5
5
|
import inspect
|
|
6
|
-
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Callable, MutableMapping
|
|
8
|
+
from typing import Any
|
|
7
9
|
|
|
8
10
|
|
|
9
|
-
class
|
|
11
|
+
class _EmptyValue: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ScopeProxy(MutableMapping[str, object]):
|
|
10
15
|
"""
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
A mutable mapping to connect symbols to python objects.
|
|
17
|
+
|
|
18
|
+
By default, does not allow re-registration of existing symbols to new values.
|
|
13
19
|
"""
|
|
14
20
|
|
|
15
|
-
def __init__(
|
|
16
|
-
self
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
d: MutableMapping[str, object] | None = None,
|
|
24
|
+
allow_overwrite: bool = False,
|
|
25
|
+
):
|
|
26
|
+
self._d = {} if d is None else {k: v for k, v in d.items()}
|
|
27
|
+
self.allow_overwrite = allow_overwrite
|
|
28
|
+
|
|
29
|
+
def __getitem__(self, name: str):
|
|
30
|
+
return self._d[name]
|
|
31
|
+
|
|
32
|
+
def __setitem__(self, name: str, value: object):
|
|
33
|
+
if not self.allow_overwrite:
|
|
34
|
+
old_value = self._d.get(name, _EmptyValue)
|
|
35
|
+
if old_value is not _EmptyValue and value is not old_value:
|
|
36
|
+
raise ValueError(
|
|
37
|
+
f"Variable {name} already exists as {old_value!r} in this "
|
|
38
|
+
f"scope. It cannot be reassigned to a new value of {value!r} "
|
|
39
|
+
f"while allow_overwrite is False."
|
|
40
|
+
)
|
|
41
|
+
self._d[name] = value
|
|
42
|
+
else:
|
|
43
|
+
self._d[name] = value
|
|
44
|
+
|
|
45
|
+
def __delitem__(self, name: str):
|
|
46
|
+
self._d.__delitem__(name)
|
|
47
|
+
|
|
48
|
+
def __iter__(self):
|
|
49
|
+
return iter(self._d)
|
|
50
|
+
|
|
51
|
+
def __len__(self):
|
|
52
|
+
return len(self._d)
|
|
17
53
|
|
|
18
54
|
def __getattr__(self, name: str):
|
|
19
55
|
try:
|
|
20
|
-
return self.
|
|
56
|
+
return self.__getitem__(name)
|
|
21
57
|
except KeyError:
|
|
22
58
|
raise AttributeError(name) from None
|
|
23
59
|
|
|
60
|
+
def __str__(self):
|
|
61
|
+
return str(self._d)
|
|
62
|
+
|
|
24
63
|
def register(self, name: str, obj: object) -> None:
|
|
25
64
|
"""
|
|
26
65
|
Add a name → object binding to this scope.
|
|
@@ -30,7 +69,7 @@ class ScopeProxy:
|
|
|
30
69
|
resolutions within this scope (or any scope that shares the same
|
|
31
70
|
backing namespace).
|
|
32
71
|
"""
|
|
33
|
-
self
|
|
72
|
+
self[name] = obj
|
|
34
73
|
|
|
35
74
|
def fork(self) -> ScopeProxy:
|
|
36
75
|
"""
|
|
@@ -40,11 +79,22 @@ class ScopeProxy:
|
|
|
40
79
|
affect the parent. Used when walking conditional branches so that
|
|
41
80
|
branch-local imports don't leak into sibling branches.
|
|
42
81
|
"""
|
|
43
|
-
return ScopeProxy(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def get_scope(func:
|
|
47
|
-
|
|
82
|
+
return ScopeProxy(self, allow_overwrite=self.allow_overwrite)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def get_scope(func: Callable[..., Any] | type[Any]) -> ScopeProxy:
|
|
86
|
+
module = inspect.getmodule(func)
|
|
87
|
+
if module is None:
|
|
88
|
+
module_name = getattr(func, "__module__", None)
|
|
89
|
+
if module_name is not None:
|
|
90
|
+
module = sys.modules.get(module_name)
|
|
91
|
+
if module is None:
|
|
92
|
+
raise ValueError(
|
|
93
|
+
f"Cannot determine the module for {func!r}. "
|
|
94
|
+
"inspect.getmodule() returned None and no resolvable __module__ "
|
|
95
|
+
"attribute was found."
|
|
96
|
+
)
|
|
97
|
+
return ScopeProxy(module.__dict__ | vars(builtins))
|
|
48
98
|
|
|
49
99
|
|
|
50
100
|
def resolve_attribute_to_object(attribute: str, scope: ScopeProxy | object) -> object:
|