flowgraph 0.1.2__py3-none-any.whl
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.
- flowgraph/__init__.py +9 -0
- flowgraph/__main__.py +6 -0
- flowgraph/adapters/__init__.py +141 -0
- flowgraph/adapters/controls.py +96 -0
- flowgraph/adapters/cosapp.py +195 -0
- flowgraph/adapters/data_types.py +59 -0
- flowgraph/adapters/documentation.py +45 -0
- flowgraph/adapters/field_ops.py +121 -0
- flowgraph/adapters/file_conversion.py +393 -0
- flowgraph/adapters/files.py +191 -0
- flowgraph/adapters/filters.py +173 -0
- flowgraph/adapters/image_tools.py +394 -0
- flowgraph/adapters/mesh_creation_tools.py +431 -0
- flowgraph/adapters/mesh_generations.py +58 -0
- flowgraph/adapters/mesh_ops.py +362 -0
- flowgraph/adapters/paraview.py +39 -0
- flowgraph/adapters/plaid_bridge.py +205 -0
- flowgraph/adapters/readers.py +215 -0
- flowgraph/adapters/remote_files.py +118 -0
- flowgraph/adapters/simple_sources.py +199 -0
- flowgraph/adapters/sinks.py +80 -0
- flowgraph/adapters/user_code.py +196 -0
- flowgraph/adapters/workflow.py +176 -0
- flowgraph/adapters/workflow_interfaces.py +78 -0
- flowgraph/adapters/writers.py +159 -0
- flowgraph/application/__init__.py +1 -0
- flowgraph/application/node_registry.py +14 -0
- flowgraph/application/port_conversions.py +63 -0
- flowgraph/application/wasm_execution.py +132 -0
- flowgraph/application/workflow_cli.py +417 -0
- flowgraph/application/workflow_core.py +1193 -0
- flowgraph/application/workflow_history.py +93 -0
- flowgraph/application/workflow_io.py +393 -0
- flowgraph/application/workflow_layout.py +170 -0
- flowgraph/application/workflow_web_catalog.py +143 -0
- flowgraph/application/workflows.py +7 -0
- flowgraph/core_cli.py +81 -0
- flowgraph/domain/__init__.py +5 -0
- flowgraph/domain/mesh_document.py +91 -0
- flowgraph/examples/AllFilters.py +19 -0
- flowgraph/examples/convert_format.py +21 -0
- flowgraph/resources.py +29 -0
- flowgraph/testdata/AllFilters.json +390 -0
- flowgraph/testdata/FlowGraph.h5 +0 -0
- flowgraph/testdata/FlowGraph.stl +0 -0
- flowgraph/testdata/FlowGraph.xdmf +22 -0
- flowgraph/testdata/SimpleReadMesh.json +70 -0
- flowgraph/testdata/SpherePoints.csv +83 -0
- flowgraph/testdata/muscat-muscat-mesh-conversion.json +101 -0
- flowgraph/version.py +3 -0
- flowgraph-0.1.2.dist-info/METADATA +150 -0
- flowgraph-0.1.2.dist-info/RECORD +56 -0
- flowgraph-0.1.2.dist-info/WHEEL +5 -0
- flowgraph-0.1.2.dist-info/entry_points.txt +2 -0
- flowgraph-0.1.2.dist-info/licenses/LICENSE +29 -0
- flowgraph-0.1.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Export the non-executable workflow catalog consumed by the static web editor."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
from collections.abc import Mapping
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from flowgraph.adapters import ADAPTERS, AdapterGroup
|
|
13
|
+
from flowgraph.application.port_conversions import PORT_CONVERSIONS
|
|
14
|
+
from flowgraph.application.workflow_core import NodeDefinition, ParameterDefinition, PortDefinition
|
|
15
|
+
from flowgraph.application.workflow_io import WORKFLOW_FORMAT, WORKFLOW_FORMAT_VERSION
|
|
16
|
+
|
|
17
|
+
WEB_CATALOG_FORMAT = "flowgraph-web-node-catalog"
|
|
18
|
+
WEB_CATALOG_VERSION = 1
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def workflow_web_catalog() -> dict[str, Any]:
|
|
22
|
+
"""Return a JSON-compatible, non-executable description of the workflow catalog.
|
|
23
|
+
|
|
24
|
+
The result intentionally excludes node executors, port resolvers, runtime Python
|
|
25
|
+
types, and all other callable behavior. A static editor may use it to author and
|
|
26
|
+
structurally validate workflow JSON, but it cannot execute a workflow from it.
|
|
27
|
+
"""
|
|
28
|
+
catalog = {
|
|
29
|
+
"format": WEB_CATALOG_FORMAT,
|
|
30
|
+
"version": WEB_CATALOG_VERSION,
|
|
31
|
+
"workflow_format": WORKFLOW_FORMAT,
|
|
32
|
+
"workflow_format_version": WORKFLOW_FORMAT_VERSION,
|
|
33
|
+
"groups": [_group_to_dict(group) for group in ADAPTERS.groups],
|
|
34
|
+
"conversions": [
|
|
35
|
+
{
|
|
36
|
+
"source_type_id": conversion.source_type_id,
|
|
37
|
+
"target_type_id": conversion.target_type_id,
|
|
38
|
+
"label": conversion.label,
|
|
39
|
+
}
|
|
40
|
+
for conversion in PORT_CONVERSIONS
|
|
41
|
+
],
|
|
42
|
+
}
|
|
43
|
+
_assert_json_compatible(catalog)
|
|
44
|
+
return catalog
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def write_workflow_web_catalog(path: str | Path) -> Path:
|
|
48
|
+
"""Write the deterministic static-editor catalog JSON to *path*.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
path:
|
|
53
|
+
Destination JSON file. Its parent directory must already exist.
|
|
54
|
+
|
|
55
|
+
Returns
|
|
56
|
+
-------
|
|
57
|
+
pathlib.Path
|
|
58
|
+
The resolved output path.
|
|
59
|
+
"""
|
|
60
|
+
destination = Path(path)
|
|
61
|
+
if not destination.parent.is_dir():
|
|
62
|
+
raise ValueError(f"Web catalog output directory does not exist: {destination.parent}")
|
|
63
|
+
destination.write_text(
|
|
64
|
+
json.dumps(workflow_web_catalog(), indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
65
|
+
)
|
|
66
|
+
return destination.resolve()
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _group_to_dict(group: AdapterGroup) -> dict[str, Any]:
|
|
70
|
+
"""Serialize an ordered catalog group and its nested groups."""
|
|
71
|
+
return {
|
|
72
|
+
"label": group.label,
|
|
73
|
+
"nodes": [_node_to_dict(definition) for definition in group.node_definitions],
|
|
74
|
+
"subgroups": [_group_to_dict(subgroup) for subgroup in group.subgroups],
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _node_to_dict(definition: NodeDefinition) -> dict[str, Any]:
|
|
79
|
+
"""Serialize static authoring metadata for one node definition."""
|
|
80
|
+
return {
|
|
81
|
+
"id": definition.id,
|
|
82
|
+
"icon": definition.icon,
|
|
83
|
+
"label": definition.label,
|
|
84
|
+
"description": definition.description,
|
|
85
|
+
"color": definition.color,
|
|
86
|
+
"presentation": definition.presentation,
|
|
87
|
+
"ports": [_port_to_dict(port) for port in definition.ports],
|
|
88
|
+
"parameters": [_parameter_to_dict(parameter) for parameter in definition.parameters],
|
|
89
|
+
"creates_subworkflow": definition.subworkflow_factory is not None,
|
|
90
|
+
"has_dynamic_ports": (
|
|
91
|
+
definition.port_resolver is not None or definition.instance_port_resolver is not None
|
|
92
|
+
),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _port_to_dict(port: PortDefinition) -> dict[str, Any]:
|
|
97
|
+
"""Serialize one named typed node endpoint."""
|
|
98
|
+
return {
|
|
99
|
+
"name": port.name,
|
|
100
|
+
"label": port.display_label,
|
|
101
|
+
"direction": port.direction.value,
|
|
102
|
+
"data_type_id": port.data_type.id,
|
|
103
|
+
"data_type_label": port.data_type.label,
|
|
104
|
+
"required": port.required,
|
|
105
|
+
"is_param": port.is_param,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _parameter_to_dict(parameter: ParameterDefinition) -> dict[str, Any]:
|
|
110
|
+
"""Serialize one editable parameter schema without runtime type objects."""
|
|
111
|
+
return {
|
|
112
|
+
"name": parameter.name,
|
|
113
|
+
"kind_id": parameter.kind.id,
|
|
114
|
+
"kind_label": parameter.kind.label,
|
|
115
|
+
"label": parameter.label,
|
|
116
|
+
"default": deepcopy(parameter.default),
|
|
117
|
+
"placeholder": parameter.placeholder,
|
|
118
|
+
"options": [
|
|
119
|
+
{"value": deepcopy(option.value), "label": option.label} for option in parameter.options
|
|
120
|
+
],
|
|
121
|
+
"file_patterns": list(parameter.file_patterns),
|
|
122
|
+
"port": parameter.port,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _assert_json_compatible(payload: Mapping[str, Any]) -> None:
|
|
127
|
+
"""Raise a useful error if a catalog implementation adds non-JSON metadata."""
|
|
128
|
+
try:
|
|
129
|
+
json.dumps(payload)
|
|
130
|
+
except (TypeError, ValueError) as error:
|
|
131
|
+
raise ValueError(f"Workflow web catalog is not JSON-compatible: {error}") from error
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def main() -> None:
|
|
135
|
+
"""Write the web-editor catalog to a command-line-selected JSON file."""
|
|
136
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
137
|
+
parser.add_argument("output", type=Path, help="Destination node-catalog.json path.")
|
|
138
|
+
args = parser.parse_args()
|
|
139
|
+
print(write_workflow_web_catalog(args.output))
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
if __name__ == "__main__":
|
|
143
|
+
main()
|
flowgraph/core_cli.py
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Headless command-line entry point for the binary FlowGraph core wheel."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import sys
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
|
|
9
|
+
from flowgraph.application.workflow_cli import (
|
|
10
|
+
WorkflowCliError,
|
|
11
|
+
execute_cli_workflow,
|
|
12
|
+
format_workflow_outputs,
|
|
13
|
+
inspect_workflow,
|
|
14
|
+
parse_inputs,
|
|
15
|
+
parse_overrides,
|
|
16
|
+
workflow_outputs,
|
|
17
|
+
write_json_output,
|
|
18
|
+
)
|
|
19
|
+
from flowgraph.application.workflow_core import WorkflowError
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
23
|
+
"""Build the command-line parser for headless workflow execution."""
|
|
24
|
+
parser = argparse.ArgumentParser(description="Inspect or execute FlowGraph workflows.")
|
|
25
|
+
commands = parser.add_subparsers(dest="command", required=True)
|
|
26
|
+
|
|
27
|
+
inspect_parser = commands.add_parser(
|
|
28
|
+
"inspect", help="print workflow inputs, outputs, and its directed ASCII graph"
|
|
29
|
+
)
|
|
30
|
+
inspect_parser.add_argument("workflow", type=str, help="workflow JSON file")
|
|
31
|
+
|
|
32
|
+
run_parser = commands.add_parser("run", help="execute a workflow without opening the UI")
|
|
33
|
+
run_parser.add_argument("workflow", type=str, help="workflow JSON file")
|
|
34
|
+
run_parser.add_argument(
|
|
35
|
+
"--input",
|
|
36
|
+
action="append",
|
|
37
|
+
default=[],
|
|
38
|
+
metavar="NAME=VALUE",
|
|
39
|
+
help="set a published workflow input; JSON values are decoded when possible",
|
|
40
|
+
)
|
|
41
|
+
run_parser.add_argument(
|
|
42
|
+
"--override",
|
|
43
|
+
action="append",
|
|
44
|
+
default=[],
|
|
45
|
+
metavar="NODE-ID.PARAMETER=VALUE",
|
|
46
|
+
help="override a node parameter for this run",
|
|
47
|
+
)
|
|
48
|
+
run_parser.add_argument(
|
|
49
|
+
"--json-output",
|
|
50
|
+
type=str,
|
|
51
|
+
metavar="FILE",
|
|
52
|
+
help="write public workflow outputs as JSON to FILE",
|
|
53
|
+
)
|
|
54
|
+
return parser
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(argv: Sequence[str] | None = None) -> int:
|
|
58
|
+
"""Validate arguments and run a headless FlowGraph workflow command."""
|
|
59
|
+
args = build_parser().parse_args(argv)
|
|
60
|
+
try:
|
|
61
|
+
if args.command == "inspect":
|
|
62
|
+
print(inspect_workflow(args.workflow))
|
|
63
|
+
else:
|
|
64
|
+
workflow, result = execute_cli_workflow(
|
|
65
|
+
args.workflow,
|
|
66
|
+
inputs=parse_inputs(args.input),
|
|
67
|
+
overrides=parse_overrides(args.override),
|
|
68
|
+
)
|
|
69
|
+
outputs = workflow_outputs(workflow, result)
|
|
70
|
+
if args.json_output:
|
|
71
|
+
write_json_output(outputs, args.json_output)
|
|
72
|
+
else:
|
|
73
|
+
print(format_workflow_outputs(outputs))
|
|
74
|
+
except (OSError, ValueError, KeyError, WorkflowError, WorkflowCliError) as error:
|
|
75
|
+
print(f"flowgraph: {error}", file=sys.stderr)
|
|
76
|
+
return 2
|
|
77
|
+
return 0
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
if __name__ == "__main__":
|
|
81
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""The canonical, editable mesh document."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
from Muscat.MeshContainers.Mesh import Mesh
|
|
7
|
+
from Muscat.Types import MuscatIndex
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class MeshDocument:
|
|
12
|
+
"""Own a Muscat mesh and application-level state without UI dependencies."""
|
|
13
|
+
|
|
14
|
+
mesh: Mesh # type: ignore # this is only a type hint
|
|
15
|
+
|
|
16
|
+
_mesh: Mesh = field(init=False, repr=False)
|
|
17
|
+
|
|
18
|
+
def __str__(self):
|
|
19
|
+
return str(self._mesh)
|
|
20
|
+
|
|
21
|
+
@property
|
|
22
|
+
def mesh(self) -> Mesh:
|
|
23
|
+
return self._mesh
|
|
24
|
+
|
|
25
|
+
@mesh.setter
|
|
26
|
+
def mesh(self, mesh: Mesh):
|
|
27
|
+
self._mesh = mesh
|
|
28
|
+
# ensure we have the correct FlowGraph Fields
|
|
29
|
+
self.get_muscat_node_ids()
|
|
30
|
+
self.get_muscat_elements_ids()
|
|
31
|
+
|
|
32
|
+
def get_muscat_node_ids(self):
|
|
33
|
+
|
|
34
|
+
if "flowgraph_node_id" not in self.mesh.nodeFields:
|
|
35
|
+
res = np.arange(self.mesh.GetNumberOfNodes(), dtype=MuscatIndex)
|
|
36
|
+
self.mesh.nodeFields["flowgraph_node_id"] = res
|
|
37
|
+
else:
|
|
38
|
+
res = self.mesh.nodeFields["flowgraph_node_id"]
|
|
39
|
+
|
|
40
|
+
if len(res) != self.mesh.GetNumberOfNodes():
|
|
41
|
+
raise ValueError("MeshDocument node IDs do not match its Muscat mesh")
|
|
42
|
+
return res
|
|
43
|
+
|
|
44
|
+
def get_muscat_elements_ids(self):
|
|
45
|
+
if "flowgraph_element_id" not in self.mesh.elemFields:
|
|
46
|
+
res = np.arange(self.mesh.GetNumberOfElements(), dtype=MuscatIndex)
|
|
47
|
+
self.mesh.elemFields["flowgraph_element_id"] = res
|
|
48
|
+
else:
|
|
49
|
+
res = self.mesh.elemFields["flowgraph_element_id"]
|
|
50
|
+
|
|
51
|
+
if len(res) != self._mesh.GetNumberOfElements():
|
|
52
|
+
raise ValueError("MeshDocument element IDs do not match its Muscat mesh")
|
|
53
|
+
return res
|
|
54
|
+
|
|
55
|
+
def to_vtk_unstructured_grid(self): # type: ignore[no-untyped-def]
|
|
56
|
+
"""Create a VTK projection with stable Muscat node and element ID arrays."""
|
|
57
|
+
from Muscat.Bridges.vtkBridge import MeshToVtk
|
|
58
|
+
|
|
59
|
+
return MeshToVtk(self.mesh)
|
|
60
|
+
|
|
61
|
+
def __repr__(self):
|
|
62
|
+
mesh = self.mesh
|
|
63
|
+
res = f"""Mesh details :
|
|
64
|
+
Number Of Nodes : {mesh.GetNumberOfNodes()}
|
|
65
|
+
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
if len(mesh.nodesTags):
|
|
69
|
+
res += " Nodes named selections (name: size):\n"
|
|
70
|
+
for x in mesh.nodesTags:
|
|
71
|
+
res += f" {x.name}: {len(x)} \n"
|
|
72
|
+
|
|
73
|
+
res += f"Total Number Of Elements : {mesh.GetNumberOfElements()} \n"
|
|
74
|
+
|
|
75
|
+
for data in mesh.elements.values():
|
|
76
|
+
res += f" Element type: {data.elementType} \n"
|
|
77
|
+
res += f" Number of elements: {data.GetNumberOfElements()}\n"
|
|
78
|
+
if len(data.tags):
|
|
79
|
+
res += " Named selections (name: size):\n"
|
|
80
|
+
for x in data.tags:
|
|
81
|
+
res += f" {x.name}: {len(x)} \n"
|
|
82
|
+
|
|
83
|
+
if len(mesh.nodeFields.keys()) > 0:
|
|
84
|
+
res += "\n Node Fields (name: type, shape):\n"
|
|
85
|
+
for k, v in mesh.nodeFields.items():
|
|
86
|
+
res += f" {k}: {v.dtype}, {v.shape}\n"
|
|
87
|
+
if len(mesh.elemFields.keys()) > 0:
|
|
88
|
+
res += "\n Element Fields (name: type, shape):\n"
|
|
89
|
+
for k, v in mesh.elemFields.items():
|
|
90
|
+
res += f" {k}: {v.dtype}, {v.shape}\n"
|
|
91
|
+
return res
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from flowgraph.application.node_registry import create_node_registry
|
|
4
|
+
from flowgraph.application.workflow_io import execute_workflow, load_workflow
|
|
5
|
+
|
|
6
|
+
registry = create_node_registry()
|
|
7
|
+
my_workflow = load_workflow(Path(__file__).parent / ".." / "testdata" / "AllFilters.json", registry)
|
|
8
|
+
|
|
9
|
+
print(my_workflow)
|
|
10
|
+
|
|
11
|
+
for node in my_workflow.nodes:
|
|
12
|
+
node.pprint()
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
parameter_overrides = {}
|
|
16
|
+
|
|
17
|
+
result = execute_workflow(my_workflow, registry=registry, parameter_overrides=parameter_overrides)
|
|
18
|
+
|
|
19
|
+
print(result)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from flowgraph.application.node_registry import create_node_registry
|
|
4
|
+
from flowgraph.application.workflow_io import execute_workflow, load_workflow
|
|
5
|
+
|
|
6
|
+
registry = create_node_registry()
|
|
7
|
+
my_workflow = load_workflow(
|
|
8
|
+
Path(__file__).parent / ".." / "testdata" / "workflowtest.json", registry
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
print(my_workflow)
|
|
12
|
+
|
|
13
|
+
for node in my_workflow.nodes:
|
|
14
|
+
node.pprint()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
parameter_overrides = {"create-muscat-element-filter-2": {"dimensionality": [0]}}
|
|
18
|
+
|
|
19
|
+
result = execute_workflow(my_workflow, registry=registry, parameter_overrides=parameter_overrides)
|
|
20
|
+
|
|
21
|
+
print(result.node_outputs["filter-mesh-muscat-2"]["mesh"].mesh)
|
flowgraph/resources.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Current-working-directory-independent access to packaged FlowGraph resources."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
import flowgraph
|
|
8
|
+
|
|
9
|
+
PACKAGE_ROOT = Path(__file__).resolve().parent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def package_resource_path(*parts: str, required: bool = True) -> Path:
|
|
13
|
+
"""Return a path inside the installed package and optionally require it to exist.
|
|
14
|
+
|
|
15
|
+
PyInstaller gives bundled modules meaningful ``__file__`` paths inside its
|
|
16
|
+
collected application directory, so this works in source, wheel, and onedir
|
|
17
|
+
executions without depending on the process working directory.
|
|
18
|
+
"""
|
|
19
|
+
candidates = [Path(root).joinpath(*parts) for root in flowgraph.__path__]
|
|
20
|
+
path = next(
|
|
21
|
+
(candidate for candidate in candidates if candidate.exists()), PACKAGE_ROOT.joinpath(*parts)
|
|
22
|
+
)
|
|
23
|
+
if required and not path.exists():
|
|
24
|
+
resource = "/".join(parts) or "."
|
|
25
|
+
raise FileNotFoundError(
|
|
26
|
+
f"Required FlowGraph resource {resource!r} was not included at {path}. "
|
|
27
|
+
"Install the matching FlowGraph core and desktop distributions, then rebuild the artifact."
|
|
28
|
+
)
|
|
29
|
+
return path
|