flowrep 0.6.2__tar.gz → 0.6.4__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.6.2 → flowrep-0.6.4}/PKG-INFO +3 -3
- {flowrep-0.6.2 → flowrep-0.6.4}/pyproject.toml +2 -2
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/_version.py +2 -2
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/api/tools.py +3 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/converters/python_workflow_definition.py +131 -31
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/object_scope.py +2 -1
- {flowrep-0.6.2 → flowrep-0.6.4}/.gitignore +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/LICENSE +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/docs/README.md +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/__init__.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/api/__init__.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/api/schemas.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/base_models.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/__init__.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/annotate.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/flow_control.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/function.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/source.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/statements.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/compiler/sugar.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/converters/__init__.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/edge_models.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/__init__.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/atomic_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/attribute_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/case_helpers.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/chain_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/constant_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/dataclass_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/for_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/if_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/item_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/label_helpers.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/parser_helpers.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/parser_protocol.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/symbol_scope.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/try_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/while_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/parsers/workflow_parser.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/__init__.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/atomic_recipe.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/constant_recipe.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/for_recipe.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/helper_models.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/if_recipe.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/try_recipe.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/union_types.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/while_recipe.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/prospective/workflow_recipe.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/__init__.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/datastructures.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/storage.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/storage_widget.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/retrospective/viewer.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/std.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/subgraph_validation.py +0 -0
- {flowrep-0.6.2 → flowrep-0.6.4}/src/flowrep/wfms.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: flowrep
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.4
|
|
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
|
|
@@ -47,7 +47,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
47
47
|
Classifier: Programming Language :: Python :: 3.14
|
|
48
48
|
Classifier: Topic :: Scientific/Engineering
|
|
49
49
|
Requires-Python: <3.15,>=3.11
|
|
50
|
-
Requires-Dist: packaging<=26.
|
|
50
|
+
Requires-Dist: packaging<=26.3,>=24.2
|
|
51
51
|
Requires-Dist: pydantic<2.13.0,>=2.12.0
|
|
52
52
|
Requires-Dist: pyiron-snippets<2.0.0,>=1.3.0
|
|
53
53
|
Requires-Dist: typing-extensions<4.17.0,>=4.14.1
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
[build-system]
|
|
2
|
-
requires = [ "hatchling==1.
|
|
2
|
+
requires = [ "hatchling==1.32.0", "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 = [ "packaging>=24.2,<=26.
|
|
12
|
+
dependencies = [ "packaging>=24.2,<=26.3", "pydantic>=2.12.0,<2.13.0", "pyiron_snippets>=1.3.0,<2.0.0", "typing-extensions>=4.14.1,<4.17.0",]
|
|
13
13
|
dynamic = [ "version",]
|
|
14
14
|
[[project.authors]]
|
|
15
15
|
name = "Sam Waseda"
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '0.6.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 6,
|
|
21
|
+
__version__ = version = '0.6.4'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 6, 4)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -6,6 +6,9 @@ for downstream library developers.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
from flowrep.compiler.source import flowrep2python as flowrep2python
|
|
9
|
+
from flowrep.converters.python_workflow_definition import (
|
|
10
|
+
OutputContractError as OutputContractError,
|
|
11
|
+
)
|
|
9
12
|
from flowrep.converters.python_workflow_definition import flowrep2pwd as flowrep2pwd
|
|
10
13
|
from flowrep.converters.python_workflow_definition import pwd2flowrep as pwd2flowrep
|
|
11
14
|
from flowrep.parsers.atomic_parser import atomic as atomic
|
|
@@ -4,6 +4,24 @@ Bidirectional converter between flowrep and python-workflow-definition formats.
|
|
|
4
4
|
The ``python_workflow_definition`` (pwd) package is an **optional** dependency.
|
|
5
5
|
It represents workflows as flat, non-nested DAGs of atomic function calls with
|
|
6
6
|
explicit input/output nodes carrying JSON-serializable default values.
|
|
7
|
+
|
|
8
|
+
The two formats hold different axioms about how a function's return value maps
|
|
9
|
+
onto a node's output ports:
|
|
10
|
+
|
|
11
|
+
=========== ========================== ================================
|
|
12
|
+
whole return value component of return
|
|
13
|
+
=========== ========================== ================================
|
|
14
|
+
pwd ``sourcePort: null`` ``sourcePort: "<key>"`` →
|
|
15
|
+
``result["<key>"]``
|
|
16
|
+
flowrep exactly one output port N>1 ports unpack an N-tuple
|
|
17
|
+
=========== ========================== ================================
|
|
18
|
+
|
|
19
|
+
They overlap only on *exactly one output, meaning the whole return value*.
|
|
20
|
+
Anything outside that overlap raises :class:`OutputContractError` rather than
|
|
21
|
+
being mistranslated into a recipe that computes the wrong answer. As a
|
|
22
|
+
corollary, a flowrep node's sole output port name is not preserved across a
|
|
23
|
+
round-trip: it becomes pwd's unnamed ``sourcePort`` and returns as
|
|
24
|
+
:data:`_DEFAULT_OUTPUT_PORT`.
|
|
7
25
|
"""
|
|
8
26
|
|
|
9
27
|
from __future__ import annotations
|
|
@@ -36,6 +54,23 @@ _DEFAULT_OUTPUT_PORT: str = "__result__"
|
|
|
36
54
|
_PORT_SANITIZE_PREFIX: str = "flowrep_sanitized_"
|
|
37
55
|
|
|
38
56
|
|
|
57
|
+
class OutputContractError(ValueError):
|
|
58
|
+
"""
|
|
59
|
+
A node's output contract is not representable in the target format.
|
|
60
|
+
|
|
61
|
+
flowrep and pwd disagree about how a function's return value maps onto
|
|
62
|
+
output ports. pwd says ``sourcePort: null`` is the entire return value and
|
|
63
|
+
a named ``sourcePort`` is a key of a returned dict; flowrep says a single
|
|
64
|
+
output port is the entire return value and N>1 ports unpack an N-tuple.
|
|
65
|
+
The two overlap only on *exactly one output, meaning the whole return
|
|
66
|
+
value*. Converting a node outside that overlap would produce a recipe that
|
|
67
|
+
executes incorrectly, so we refuse rather than mistranslate.
|
|
68
|
+
|
|
69
|
+
Subclasses :class:`ValueError` so that callers already handling the
|
|
70
|
+
converter's other validation failures keep working.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
|
|
39
74
|
def _needs_sanitization(port: str) -> bool:
|
|
40
75
|
"""Return ``True`` if *port* is not a valid flowrep :class:`Label`."""
|
|
41
76
|
return not base_models.is_valid_label(port)
|
|
@@ -55,7 +90,12 @@ def _sanitize_port(port: str) -> str:
|
|
|
55
90
|
|
|
56
91
|
|
|
57
92
|
def _desanitize_port(port: str) -> str:
|
|
58
|
-
"""
|
|
93
|
+
"""
|
|
94
|
+
Reverse :func:`_sanitize_port` when converting back to pwd.
|
|
95
|
+
|
|
96
|
+
Only input (``targetPort``) names need this; output ports never round-trip
|
|
97
|
+
through pwd by name — see :func:`_build_pwd_edges`.
|
|
98
|
+
"""
|
|
59
99
|
if port.startswith(_PORT_SANITIZE_PREFIX):
|
|
60
100
|
candidate = port[len(_PORT_SANITIZE_PREFIX) :]
|
|
61
101
|
if candidate and _needs_sanitization(candidate):
|
|
@@ -77,8 +117,13 @@ def pwd2flowrep(
|
|
|
77
117
|
A ``(WorkflowRecipe, defaults)`` pair where *defaults* maps each
|
|
78
118
|
workflow-input name to the default value carried by the corresponding
|
|
79
119
|
PWD input node.
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
OutputContractError: If any function node is consumed via a named
|
|
123
|
+
``sourcePort``.
|
|
80
124
|
"""
|
|
81
125
|
input_nodes, output_nodes, function_nodes = _categorize_pwd_nodes(wf.nodes)
|
|
126
|
+
_validate_pwd_output_contracts(wf.edges, function_nodes)
|
|
82
127
|
|
|
83
128
|
label_map = _build_label_map(function_nodes)
|
|
84
129
|
node_inputs, node_outputs = _collect_function_node_ports(
|
|
@@ -136,8 +181,10 @@ def flowrep2pwd(
|
|
|
136
181
|
Raises:
|
|
137
182
|
ValueError: If any child is non-atomic, or if *terminal_inputs* does
|
|
138
183
|
not exactly cover the workflow's inputs.
|
|
184
|
+
OutputContractError: If any child has more than one output port.
|
|
139
185
|
"""
|
|
140
186
|
_validate_flat_workflow(wf)
|
|
187
|
+
_validate_flowrep_output_contract(wf)
|
|
141
188
|
_validate_terminal_inputs(wf, terminal_inputs)
|
|
142
189
|
|
|
143
190
|
id_counter = _IdCounter()
|
|
@@ -216,6 +263,45 @@ def _categorize_pwd_nodes(
|
|
|
216
263
|
return input_nodes, output_nodes, function_nodes
|
|
217
264
|
|
|
218
265
|
|
|
266
|
+
def _validate_pwd_output_contracts(
|
|
267
|
+
edges: list[pwd.PythonWorkflowDefinitionEdge],
|
|
268
|
+
function_nodes: dict[int, pwd.PythonWorkflowDefinitionFunctionNode],
|
|
269
|
+
) -> None:
|
|
270
|
+
"""
|
|
271
|
+
Raise :class:`OutputContractError` for function nodes consumed by key.
|
|
272
|
+
|
|
273
|
+
A named ``sourcePort`` tells pwd to subscript the node's single return value
|
|
274
|
+
with that key. A flowrep atomic node cannot express that: its outputs are
|
|
275
|
+
either the whole return value (one port) or a tuple unpacking (many ports).
|
|
276
|
+
Inspects raw ``sourcePort`` values, before sanitisation, so the message
|
|
277
|
+
quotes the author's own port names.
|
|
278
|
+
"""
|
|
279
|
+
named: dict[int, list[str]] = {}
|
|
280
|
+
for edge in edges:
|
|
281
|
+
if edge.source not in function_nodes:
|
|
282
|
+
continue
|
|
283
|
+
port = edge.sourcePort
|
|
284
|
+
if port is None or port == pwd.INTERNAL_DEFAULT_HANDLE:
|
|
285
|
+
continue
|
|
286
|
+
ports = named.setdefault(edge.source, [])
|
|
287
|
+
if port not in ports:
|
|
288
|
+
ports.append(port)
|
|
289
|
+
|
|
290
|
+
if named:
|
|
291
|
+
offenders = [
|
|
292
|
+
f"node {nid} ({function_nodes[nid].value}) is consumed via "
|
|
293
|
+
f"sourcePort(s) {sorted(named[nid])}"
|
|
294
|
+
for nid in sorted(named)
|
|
295
|
+
]
|
|
296
|
+
raise OutputContractError(
|
|
297
|
+
"pwd2flowrep requires every function node to be consumed via a null "
|
|
298
|
+
"sourcePort. pwd reads a named sourcePort as a key of a single "
|
|
299
|
+
"returned dict, which a flowrep atomic node cannot express — its "
|
|
300
|
+
"outputs are either the whole return value (one port) or a tuple "
|
|
301
|
+
"unpacking (many). Offending nodes: " + "; ".join(offenders) + "."
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
219
305
|
def _build_label_map(
|
|
220
306
|
function_nodes: dict[int, pwd.PythonWorkflowDefinitionFunctionNode],
|
|
221
307
|
) -> dict[int, str]:
|
|
@@ -237,13 +323,6 @@ def _build_label_map(
|
|
|
237
323
|
return label_map
|
|
238
324
|
|
|
239
325
|
|
|
240
|
-
def _resolve_source_port(port: str | None) -> str:
|
|
241
|
-
"""Map pwd's internal sourcePort to a flowrep output-port name."""
|
|
242
|
-
if port is None or port == pwd.INTERNAL_DEFAULT_HANDLE:
|
|
243
|
-
return _DEFAULT_OUTPUT_PORT
|
|
244
|
-
return _sanitize_port(port)
|
|
245
|
-
|
|
246
|
-
|
|
247
326
|
def _collect_function_node_ports(
|
|
248
327
|
edges: list[pwd.PythonWorkflowDefinitionEdge],
|
|
249
328
|
function_nodes: dict[int, pwd.PythonWorkflowDefinitionFunctionNode],
|
|
@@ -251,8 +330,9 @@ def _collect_function_node_ports(
|
|
|
251
330
|
"""
|
|
252
331
|
Collect ordered, unique input/output port names for each function node.
|
|
253
332
|
|
|
254
|
-
|
|
255
|
-
:func:`_sanitize_port`.
|
|
333
|
+
Input port names that are not valid flowrep Labels are sanitized via
|
|
334
|
+
:func:`_sanitize_port`. Output ports are always the single
|
|
335
|
+
:data:`_DEFAULT_OUTPUT_PORT`.
|
|
256
336
|
"""
|
|
257
337
|
node_inputs: dict[int, list[str]] = {nid: [] for nid in function_nodes}
|
|
258
338
|
node_outputs: dict[int, list[str]] = {nid: [] for nid in function_nodes}
|
|
@@ -265,12 +345,13 @@ def _collect_function_node_ports(
|
|
|
265
345
|
if port not in ports:
|
|
266
346
|
ports.append(port)
|
|
267
347
|
|
|
268
|
-
#
|
|
348
|
+
# Outputs of the function node. Guaranteed null-sourced by
|
|
349
|
+
# _validate_pwd_output_contracts, so every such node has exactly the
|
|
350
|
+
# one default-named output port.
|
|
269
351
|
if edge.source in function_nodes:
|
|
270
|
-
port = _resolve_source_port(edge.sourcePort)
|
|
271
352
|
ports = node_outputs[edge.source]
|
|
272
|
-
if
|
|
273
|
-
ports.append(
|
|
353
|
+
if _DEFAULT_OUTPUT_PORT not in ports:
|
|
354
|
+
ports.append(_DEFAULT_OUTPUT_PORT)
|
|
274
355
|
|
|
275
356
|
return node_inputs, node_outputs
|
|
276
357
|
|
|
@@ -345,7 +426,7 @@ def _build_flowrep_edges(
|
|
|
345
426
|
# Function node → workflow output
|
|
346
427
|
out_name = output_nodes[edge.target].name
|
|
347
428
|
source_label = label_map[edge.source]
|
|
348
|
-
source_port =
|
|
429
|
+
source_port = _DEFAULT_OUTPUT_PORT
|
|
349
430
|
fr_output_edges[edge_models.OutputTarget(port=out_name)] = (
|
|
350
431
|
edge_models.SourceHandle(node=source_label, port=source_port)
|
|
351
432
|
)
|
|
@@ -354,7 +435,7 @@ def _build_flowrep_edges(
|
|
|
354
435
|
# Function node → function node (sibling edge)
|
|
355
436
|
source_label = label_map[edge.source]
|
|
356
437
|
target_label = label_map[edge.target]
|
|
357
|
-
source_port =
|
|
438
|
+
source_port = _DEFAULT_OUTPUT_PORT
|
|
358
439
|
target_port = _sanitize_port(edge.targetPort)
|
|
359
440
|
fr_edges[edge_models.TargetHandle(node=target_label, port=target_port)] = (
|
|
360
441
|
edge_models.SourceHandle(node=source_label, port=source_port)
|
|
@@ -385,6 +466,33 @@ def _validate_flat_workflow(wf: workflow_recipe.WorkflowRecipe) -> None:
|
|
|
385
466
|
)
|
|
386
467
|
|
|
387
468
|
|
|
469
|
+
def _validate_flowrep_output_contract(wf: workflow_recipe.WorkflowRecipe) -> None:
|
|
470
|
+
"""
|
|
471
|
+
Raise :class:`OutputContractError` if any child has more than one output.
|
|
472
|
+
|
|
473
|
+
Must be called *after* :func:`_validate_flat_workflow`, which guarantees
|
|
474
|
+
every child is an :class:`AtomicRecipe`.
|
|
475
|
+
"""
|
|
476
|
+
offenders: list[str] = []
|
|
477
|
+
for label, node in wf.nodes.items():
|
|
478
|
+
# Guaranteed by _validate_flat_workflow
|
|
479
|
+
assert isinstance(node, atomic_recipe.AtomicRecipe)
|
|
480
|
+
if len(node.outputs) > 1:
|
|
481
|
+
offenders.append(
|
|
482
|
+
f"'{label}' ({node.fully_qualified_name}) has "
|
|
483
|
+
f"{len(node.outputs)} outputs {node.outputs}"
|
|
484
|
+
)
|
|
485
|
+
if offenders:
|
|
486
|
+
raise OutputContractError(
|
|
487
|
+
"flowrep2pwd requires every child to have exactly one output. "
|
|
488
|
+
"flowrep reads multiple output ports as unpacking a tuple, whereas "
|
|
489
|
+
"pwd reads them as keys of a single returned dict, so the converted "
|
|
490
|
+
"workflow would not execute correctly. Offending nodes: "
|
|
491
|
+
+ "; ".join(offenders)
|
|
492
|
+
+ "."
|
|
493
|
+
)
|
|
494
|
+
|
|
495
|
+
|
|
388
496
|
def _validate_terminal_inputs(
|
|
389
497
|
wf: workflow_recipe.WorkflowRecipe,
|
|
390
498
|
terminal_inputs: dict[str, Any],
|
|
@@ -405,19 +513,6 @@ def _validate_terminal_inputs(
|
|
|
405
513
|
)
|
|
406
514
|
|
|
407
515
|
|
|
408
|
-
def _flowrep_port_to_pwd_source_port(port: str) -> str | None:
|
|
409
|
-
"""
|
|
410
|
-
Map a flowrep output-port name back to a pwd ``sourcePort`` value.
|
|
411
|
-
|
|
412
|
-
Returns ``None`` for the default-output sentinel so that the PWD edge
|
|
413
|
-
validator stores it as :data:`pwd.INTERNAL_DEFAULT_HANDLE`. Otherwise
|
|
414
|
-
reverses any sanitisation applied by :func:`_sanitize_port`.
|
|
415
|
-
"""
|
|
416
|
-
if port == _DEFAULT_OUTPUT_PORT:
|
|
417
|
-
return None
|
|
418
|
-
return _desanitize_port(port)
|
|
419
|
-
|
|
420
|
-
|
|
421
516
|
def _build_pwd_edges(
|
|
422
517
|
wf: workflow_recipe.WorkflowRecipe,
|
|
423
518
|
input_node_ids: dict[str, int],
|
|
@@ -430,6 +525,11 @@ def _build_pwd_edges(
|
|
|
430
525
|
Edges are emitted in a deterministic order that preserves the input-port
|
|
431
526
|
ordering of each child node — this is important for consumers that rely on
|
|
432
527
|
edge-list order (e.g. ``get_list``).
|
|
528
|
+
|
|
529
|
+
Every function-node source emits ``sourcePort=None``. A flowrep child has
|
|
530
|
+
exactly one output (enforced by :func:`_validate_flowrep_output_contract`) and that
|
|
531
|
+
output *is* the whole return value, which is precisely what pwd spells as a
|
|
532
|
+
null ``sourcePort``. The flowrep port's name is therefore not carried over.
|
|
433
533
|
"""
|
|
434
534
|
pwd_edges: list[pwd.PythonWorkflowDefinitionEdge] = []
|
|
435
535
|
|
|
@@ -456,7 +556,7 @@ def _build_pwd_edges(
|
|
|
456
556
|
pwd_edges.append(
|
|
457
557
|
pwd.PythonWorkflowDefinitionEdge(
|
|
458
558
|
source=func_node_ids[source.node],
|
|
459
|
-
sourcePort=
|
|
559
|
+
sourcePort=None,
|
|
460
560
|
target=func_node_ids[label],
|
|
461
561
|
targetPort=target_port_pwd,
|
|
462
562
|
)
|
|
@@ -480,7 +580,7 @@ def _build_pwd_edges(
|
|
|
480
580
|
pwd_edges.append(
|
|
481
581
|
pwd.PythonWorkflowDefinitionEdge(
|
|
482
582
|
source=func_node_ids[source.node],
|
|
483
|
-
sourcePort=
|
|
583
|
+
sourcePort=None,
|
|
484
584
|
target=output_node_ids[port],
|
|
485
585
|
targetPort=None,
|
|
486
586
|
)
|
|
@@ -94,7 +94,8 @@ def get_scope(func: Callable[..., Any] | type[Any]) -> ScopeProxy:
|
|
|
94
94
|
"inspect.getmodule() returned None and no resolvable __module__ "
|
|
95
95
|
"attribute was found."
|
|
96
96
|
)
|
|
97
|
-
|
|
97
|
+
scope = module.__dict__ | getattr(func, "__globals__", {})
|
|
98
|
+
return ScopeProxy(scope | vars(builtins))
|
|
98
99
|
|
|
99
100
|
|
|
100
101
|
def resolve_attribute_to_object(attribute: str, scope: ScopeProxy | object) -> object:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|