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.
Files changed (56) hide show
  1. flowgraph/__init__.py +9 -0
  2. flowgraph/__main__.py +6 -0
  3. flowgraph/adapters/__init__.py +141 -0
  4. flowgraph/adapters/controls.py +96 -0
  5. flowgraph/adapters/cosapp.py +195 -0
  6. flowgraph/adapters/data_types.py +59 -0
  7. flowgraph/adapters/documentation.py +45 -0
  8. flowgraph/adapters/field_ops.py +121 -0
  9. flowgraph/adapters/file_conversion.py +393 -0
  10. flowgraph/adapters/files.py +191 -0
  11. flowgraph/adapters/filters.py +173 -0
  12. flowgraph/adapters/image_tools.py +394 -0
  13. flowgraph/adapters/mesh_creation_tools.py +431 -0
  14. flowgraph/adapters/mesh_generations.py +58 -0
  15. flowgraph/adapters/mesh_ops.py +362 -0
  16. flowgraph/adapters/paraview.py +39 -0
  17. flowgraph/adapters/plaid_bridge.py +205 -0
  18. flowgraph/adapters/readers.py +215 -0
  19. flowgraph/adapters/remote_files.py +118 -0
  20. flowgraph/adapters/simple_sources.py +199 -0
  21. flowgraph/adapters/sinks.py +80 -0
  22. flowgraph/adapters/user_code.py +196 -0
  23. flowgraph/adapters/workflow.py +176 -0
  24. flowgraph/adapters/workflow_interfaces.py +78 -0
  25. flowgraph/adapters/writers.py +159 -0
  26. flowgraph/application/__init__.py +1 -0
  27. flowgraph/application/node_registry.py +14 -0
  28. flowgraph/application/port_conversions.py +63 -0
  29. flowgraph/application/wasm_execution.py +132 -0
  30. flowgraph/application/workflow_cli.py +417 -0
  31. flowgraph/application/workflow_core.py +1193 -0
  32. flowgraph/application/workflow_history.py +93 -0
  33. flowgraph/application/workflow_io.py +393 -0
  34. flowgraph/application/workflow_layout.py +170 -0
  35. flowgraph/application/workflow_web_catalog.py +143 -0
  36. flowgraph/application/workflows.py +7 -0
  37. flowgraph/core_cli.py +81 -0
  38. flowgraph/domain/__init__.py +5 -0
  39. flowgraph/domain/mesh_document.py +91 -0
  40. flowgraph/examples/AllFilters.py +19 -0
  41. flowgraph/examples/convert_format.py +21 -0
  42. flowgraph/resources.py +29 -0
  43. flowgraph/testdata/AllFilters.json +390 -0
  44. flowgraph/testdata/FlowGraph.h5 +0 -0
  45. flowgraph/testdata/FlowGraph.stl +0 -0
  46. flowgraph/testdata/FlowGraph.xdmf +22 -0
  47. flowgraph/testdata/SimpleReadMesh.json +70 -0
  48. flowgraph/testdata/SpherePoints.csv +83 -0
  49. flowgraph/testdata/muscat-muscat-mesh-conversion.json +101 -0
  50. flowgraph/version.py +3 -0
  51. flowgraph-0.1.2.dist-info/METADATA +150 -0
  52. flowgraph-0.1.2.dist-info/RECORD +56 -0
  53. flowgraph-0.1.2.dist-info/WHEEL +5 -0
  54. flowgraph-0.1.2.dist-info/entry_points.txt +2 -0
  55. flowgraph-0.1.2.dist-info/licenses/LICENSE +29 -0
  56. 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()
@@ -0,0 +1,7 @@
1
+ """Compatibility exports for headless workflow composition."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from flowgraph.application.node_registry import create_node_registry
6
+
7
+ __all__ = ["create_node_registry"]
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,5 @@
1
+ """UI-independent mesh domain objects."""
2
+
3
+ from flowgraph.domain.mesh_document import MeshDocument
4
+
5
+ __all__ = ["MeshDocument"]
@@ -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