m-strat 0.0.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.
- m_strat/__init__.py +1 -0
- m_strat/api/__init__.py +1 -0
- m_strat/api/dag/__init__.py +4 -0
- m_strat/api/dag/blueprint_register.py +197 -0
- m_strat/api/dag/executor.py +34 -0
- m_strat/api/dag/graph_manager.py +232 -0
- m_strat/api/dag/node.py +56 -0
- m_strat/api/ecs/__init__.py +3 -0
- m_strat/api/ecs/component_dispatcher.py +208 -0
- m_strat/api/ecs/ecs_register.py +375 -0
- m_strat/api/ecs/entity.py +23 -0
- m_strat/api/logger/__init__.py +5 -0
- m_strat/api/logger/log_manager.py +88 -0
- m_strat/api/project_management/__init__.py +10 -0
- m_strat/api/project_management/parameters_manager.py +335 -0
- m_strat/api/project_management/project_manager.py +563 -0
- m_strat/api/project_management/wrappers.py +18 -0
- m_strat/app.py +16 -0
- m_strat/cli.py +88 -0
- m_strat/gui/__init__.py +1 -0
- m_strat/gui/application_manager.py +187 -0
- m_strat/gui/dag/__init__.py +3 -0
- m_strat/gui/dag/bp_window.py +264 -0
- m_strat/gui/dag/exec_graph.py +288 -0
- m_strat/gui/dag/node_inspector.py +328 -0
- m_strat/gui/dag/pm_triggered_window.py +25 -0
- m_strat/gui/gui_helpers.py +128 -0
- m_strat/gui/project_window.py +483 -0
- m_strat-0.0.2.dist-info/METADATA +137 -0
- m_strat-0.0.2.dist-info/RECORD +34 -0
- m_strat-0.0.2.dist-info/WHEEL +5 -0
- m_strat-0.0.2.dist-info/entry_points.txt +2 -0
- m_strat-0.0.2.dist-info/licenses/LICENSE +21 -0
- m_strat-0.0.2.dist-info/top_level.txt +1 -0
m_strat/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Init
|
m_strat/api/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# init
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import importlib.util
|
|
2
|
+
import inspect
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
from m_strat.api.dag.node import BlueprintNode
|
|
7
|
+
from m_strat.api.logger import m_log
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class BlueprintRegister:
|
|
11
|
+
"""Manages Blueprint Nodes in a project"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, meta_file_path: str = "configs/bp_files_meta.jsonl"):
|
|
14
|
+
self.blueprints = {}
|
|
15
|
+
self.schemas = {}
|
|
16
|
+
self.files_found = []
|
|
17
|
+
|
|
18
|
+
if meta_file_path:
|
|
19
|
+
self.load_from_meta_file(meta_file_path)
|
|
20
|
+
self.meta_file_path = meta_file_path
|
|
21
|
+
|
|
22
|
+
def register_node(
|
|
23
|
+
self, name: str, func: callable, schema: dict, overwrite: bool = True
|
|
24
|
+
):
|
|
25
|
+
"""Adds a method to the project registry
|
|
26
|
+
Args:
|
|
27
|
+
name (str): The new name of the blueprint node being created (can be given via BlueprintNode decorator)
|
|
28
|
+
func (callable): The function that will be used to execute the node logic
|
|
29
|
+
schema (dict[str, type]): Used to guide the UI and create an automated form in the application, not enforced
|
|
30
|
+
overwrite (bool, optional): Allows a node to be overwritten if the name already exists. Defaults to True.
|
|
31
|
+
"""
|
|
32
|
+
assert overwrite or self.blueprints.get(name, None) is None, (
|
|
33
|
+
f"blueprint of '{name}' already exists, overwrite not allowed"
|
|
34
|
+
)
|
|
35
|
+
self.blueprints[name] = func
|
|
36
|
+
self.schemas[name] = schema
|
|
37
|
+
|
|
38
|
+
def unregister_node(self, name: str):
|
|
39
|
+
"""Remove a node from the registry by name
|
|
40
|
+
Returns:
|
|
41
|
+
tuple[dict,dict] : blueprint callable and schema respectively
|
|
42
|
+
"""
|
|
43
|
+
return (self.blueprints.pop(name, None), self.schemas.pop(name, None))
|
|
44
|
+
|
|
45
|
+
def get_blueprint(self, name: str):
|
|
46
|
+
"""Get the callable of a blueprint by name"""
|
|
47
|
+
return self.blueprints.get(name, None)
|
|
48
|
+
|
|
49
|
+
def refresh_blueprints(self):
|
|
50
|
+
self.blueprints = {}
|
|
51
|
+
self.schemas = {}
|
|
52
|
+
old_files_found = list(self.files_found)
|
|
53
|
+
self.files_found = []
|
|
54
|
+
for f in old_files_found:
|
|
55
|
+
self.add_from_file(f)
|
|
56
|
+
|
|
57
|
+
def save_meta_file(self, meta_file_path="configs/bp_files_meta.jsonl"):
|
|
58
|
+
"""Generates a jsonl file of where all the blueprints were retrieved from, so they can be loaded easily
|
|
59
|
+
Args:
|
|
60
|
+
meta_file_path (str, optional): relative path of wheree this generated file will save. Defaults to "configs/bp_files_meta.jsonl".
|
|
61
|
+
"""
|
|
62
|
+
if not self.files_found:
|
|
63
|
+
m_log.info("Attempted to save blueprints, but no blueprints registered")
|
|
64
|
+
return
|
|
65
|
+
with open(meta_file_path, "w", encoding="utf-8") as meta_f:
|
|
66
|
+
meta_f.writelines(
|
|
67
|
+
json.dumps(bp_file_path) + "\n"
|
|
68
|
+
for bp_file_path in set(self.files_found)
|
|
69
|
+
)
|
|
70
|
+
m_log.info(f"Blueprint data saved {meta_file_path}")
|
|
71
|
+
|
|
72
|
+
def load_from_meta_file(self, meta_file_path: str | None = None):
|
|
73
|
+
"""Loads and use each line as a path to retrieve saved blueprint reference from py files .
|
|
74
|
+
Args:
|
|
75
|
+
meta_file_path (str, optional): The path where the meta file resides. Defaults to "configs/bp_files_meta.jsonl".
|
|
76
|
+
Returns:
|
|
77
|
+
list[str] : Returns a list of all the names of the blueprints that were added
|
|
78
|
+
"""
|
|
79
|
+
m_log.info("Loading Blueprint Configuration Data...")
|
|
80
|
+
if meta_file_path is None:
|
|
81
|
+
meta_file_path = self.meta_file_path
|
|
82
|
+
if not os.path.exists(meta_file_path):
|
|
83
|
+
m_log.warning(f"bp_reg: path does not exist:: {meta_file_path}")
|
|
84
|
+
return []
|
|
85
|
+
added = []
|
|
86
|
+
with open(meta_file_path, "r", encoding="utf8") as meta_f:
|
|
87
|
+
for i, line in enumerate(meta_f):
|
|
88
|
+
if not line.strip():
|
|
89
|
+
continue
|
|
90
|
+
try:
|
|
91
|
+
next_path = json.loads(line.strip())
|
|
92
|
+
added.extend(self.add_from_file(next_path))
|
|
93
|
+
except (OSError, ValueError, json.JSONDecodeError) as e:
|
|
94
|
+
m_log.warning(
|
|
95
|
+
f"Line at {i} in {meta_file_path} failed to read: {e}"
|
|
96
|
+
)
|
|
97
|
+
return added
|
|
98
|
+
|
|
99
|
+
def get_schema(self, name: str):
|
|
100
|
+
"""Gets the (dict[str,type]) schema given a name of a Blueprint Node"""
|
|
101
|
+
return self.schemas.get(name, None)
|
|
102
|
+
|
|
103
|
+
def add_from_file(self, file_path: str):
|
|
104
|
+
"""uses importlib to check for any Blueprint Nodes and attempts to add them to the system
|
|
105
|
+
Args:
|
|
106
|
+
file_path (str): relative path of the .py blueprint file resides
|
|
107
|
+
Returns:
|
|
108
|
+
list[str] : Returns a list of all the names of the blueprints that were added
|
|
109
|
+
"""
|
|
110
|
+
if not os.path.exists(file_path):
|
|
111
|
+
m_log.warning(f"bp_reg: path does not exist:: {file_path}")
|
|
112
|
+
return []
|
|
113
|
+
|
|
114
|
+
module_name = os.path.splitext(os.path.basename(file_path))[0]
|
|
115
|
+
spec = importlib.util.spec_from_file_location(module_name, file_path)
|
|
116
|
+
added = []
|
|
117
|
+
assert (
|
|
118
|
+
spec is not None and spec.loader
|
|
119
|
+
), f"""Provided file does not have a spec or spec loader: ||{file_path} ||
|
|
120
|
+
This can happen because:
|
|
121
|
+
1. If the file exstension is wrong (not .py)
|
|
122
|
+
2. File permission issues such as file locked (in use elsewhere e.g )
|
|
123
|
+
3. Unfriendly path characters: path contains unusual unicode characters or file name is not python friendly
|
|
124
|
+
4. Some issue which I simply have not thought of in this moment.
|
|
125
|
+
"""
|
|
126
|
+
module = importlib.util.module_from_spec(spec)
|
|
127
|
+
module.BlueprintNode = BlueprintNode
|
|
128
|
+
try:
|
|
129
|
+
spec.loader.exec_module(module)
|
|
130
|
+
except Exception as e:
|
|
131
|
+
# flagrantly assume the error is because the module should not be run like this!
|
|
132
|
+
m_log.debug(
|
|
133
|
+
f"Spec Loader did not like something when bp register used exec_module (can ignore typically): {e}"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
for _, obj in inspect.getmembers(module, inspect.isfunction):
|
|
137
|
+
if getattr(obj, "__is_m_strat_node__", False):
|
|
138
|
+
self.register_node(
|
|
139
|
+
obj.__m_strat_node_name__, obj, obj.__m_strat_node_schema__
|
|
140
|
+
)
|
|
141
|
+
added.append(obj.__m_strat_node_name__)
|
|
142
|
+
|
|
143
|
+
if len(added) > 0:
|
|
144
|
+
self.files_found.append(file_path)
|
|
145
|
+
return added
|
|
146
|
+
|
|
147
|
+
def add_from_folder(self, folder_path, add_subfolders=True):
|
|
148
|
+
"""
|
|
149
|
+
From a directory, adds all Blueprint Nodes to the register
|
|
150
|
+
Args:
|
|
151
|
+
folder_path (str): The relative directory path
|
|
152
|
+
add_subfolders (bool, optional): Whether to add Blueprint Nodes from subdirectories (of any depth) . Defaults to True.
|
|
153
|
+
Returns:
|
|
154
|
+
list[str] : Returns a list of all the names of the blueprints that were added
|
|
155
|
+
"""
|
|
156
|
+
if not os.path.isdir(folder_path):
|
|
157
|
+
m_log.warning(f"bp_reg: path does not exist:: {folder_path}")
|
|
158
|
+
return []
|
|
159
|
+
added_nodes = []
|
|
160
|
+
m_log.info(f"Adding folder blueprint folder: {folder_path}")
|
|
161
|
+
if add_subfolders:
|
|
162
|
+
for root, _, files in os.walk(folder_path):
|
|
163
|
+
for f_name in files:
|
|
164
|
+
if f_name.endswith(".py") and not f_name.startswith("__"):
|
|
165
|
+
full_path = os.path.join(root, f_name)
|
|
166
|
+
next_added = []
|
|
167
|
+
try:
|
|
168
|
+
next_added = self.add_from_file(full_path)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
m_log.error(
|
|
171
|
+
f"Blueprint tried to add Blueprints from |{f_name}| in directory |{root}| but failed: \n {e} \n{'*' * 12}\n ...Skipping!"
|
|
172
|
+
)
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
added_nodes.extend(next_added)
|
|
176
|
+
else:
|
|
177
|
+
for f_name in os.listdir(folder_path):
|
|
178
|
+
if f_name.endswith(".py") and not f_name.startswith("__"):
|
|
179
|
+
full_path = os.path.join(folder_path, f_name)
|
|
180
|
+
if os.path.isfile(full_path):
|
|
181
|
+
added_nodes.extend(self.add_from_file(full_path))
|
|
182
|
+
m_log.info(
|
|
183
|
+
f"Finished adding blueprint director, {len(added_nodes)} blueprint(s) found"
|
|
184
|
+
)
|
|
185
|
+
return added_nodes
|
|
186
|
+
|
|
187
|
+
def get_all_blueprints(self):
|
|
188
|
+
"""Copies the blueprint callable dictionary, returning all blueprints
|
|
189
|
+
NB: Blueprints are given by reference, modifications will alter them!
|
|
190
|
+
"""
|
|
191
|
+
return dict(self.blueprints)
|
|
192
|
+
|
|
193
|
+
def get_all_schemas(self):
|
|
194
|
+
"""Copies the schema dictionary, returning all blueprint schemas
|
|
195
|
+
NB: Blueprints are given by reference, modifications will alter them!
|
|
196
|
+
"""
|
|
197
|
+
return dict(self.schemas)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from m_strat.api.logger import m_log
|
|
2
|
+
|
|
3
|
+
_EXECUTORS = {}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# Defines an exectuion method for Node system. Must have (targetnodes, init_context) as arguments. Must return context
|
|
7
|
+
# name muse be unique
|
|
8
|
+
def Executor(name: str):
|
|
9
|
+
def decorator(func):
|
|
10
|
+
assert _EXECUTORS.get(name, None) is None, f"Executor {name} already exists"
|
|
11
|
+
_EXECUTORS[name] = func
|
|
12
|
+
return func
|
|
13
|
+
|
|
14
|
+
return decorator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Standard linear-non parallelised running, executes in same thread without modification
|
|
18
|
+
@Executor(name="Sequential")
|
|
19
|
+
def execute_sequential(target_nodes: list[tuple], init_context: dict):
|
|
20
|
+
context = init_context
|
|
21
|
+
m_log.info("Started Sequential running")
|
|
22
|
+
for func, params, node_info in target_nodes:
|
|
23
|
+
m_log.debug(f"Running Node {node_info.id}")
|
|
24
|
+
try:
|
|
25
|
+
context = func(context, params)
|
|
26
|
+
except Exception as e:
|
|
27
|
+
m_log.error(
|
|
28
|
+
f"Pipeline execution failed at '{node_info.id}' using blueprint '{node_info.blueprint_name}':\n {e}"
|
|
29
|
+
)
|
|
30
|
+
raise # NB you should always raiserrors when making your own Executor!!
|
|
31
|
+
m_log.debug(f"Node{node_info.id} Completed")
|
|
32
|
+
|
|
33
|
+
m_log.info(f"Finished Executing {len(target_nodes)} node(s)")
|
|
34
|
+
return context
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from collections import defaultdict, deque
|
|
4
|
+
from dataclasses import asdict
|
|
5
|
+
|
|
6
|
+
from m_strat.api.project_management.wrappers import Serializable
|
|
7
|
+
|
|
8
|
+
from .node import ExecutionNode
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class GraphManager(Serializable):
|
|
12
|
+
"""Manages a single DAG which is used to build a ML pipeline from Blueprint Nodes"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
name: str,
|
|
17
|
+
load_path: str | None = None,
|
|
18
|
+
load: bool = False,
|
|
19
|
+
dir: str = "exec_graphs",
|
|
20
|
+
):
|
|
21
|
+
"""Instansiates a Graphmanager object used to manage a DAG
|
|
22
|
+
Args:
|
|
23
|
+
name (str): The designated label of this DAG
|
|
24
|
+
load_path (str, optional): Path to json config . Defaults to None.
|
|
25
|
+
load (bool, optional): If true attemps to load the graph by name or load path. Defaults to False.
|
|
26
|
+
dir (str, optional): Path to where this graph is stored. Defaults to "exec_graphs".
|
|
27
|
+
|
|
28
|
+
Raises:
|
|
29
|
+
ValueError: if no name or load_path is supplied and load is true as no data can be loaded.
|
|
30
|
+
"""
|
|
31
|
+
self.nodes = {"start": ExecutionNode("start", "", "")}
|
|
32
|
+
self.edges = defaultdict(list)
|
|
33
|
+
self.name = name
|
|
34
|
+
self.dir = dir
|
|
35
|
+
if load and (name or load_path):
|
|
36
|
+
self.load(name=name, load_path=load_path)
|
|
37
|
+
elif load:
|
|
38
|
+
raise ValueError("GraphManager cannot load without a valid graph name")
|
|
39
|
+
|
|
40
|
+
@staticmethod
|
|
41
|
+
def get_file_path(name: str, dir: str):
|
|
42
|
+
"""From the name and directory, returns what the graph data file should be"""
|
|
43
|
+
return os.path.join(dir, f"gm_{name}.json")
|
|
44
|
+
|
|
45
|
+
@staticmethod
|
|
46
|
+
def get_name_from_filename(path: str):
|
|
47
|
+
"""if given a standardised graph data file, extracts the name of the graph"""
|
|
48
|
+
base_name: str = os.path.splitext(os.path.basename(path))[0]
|
|
49
|
+
return base_name[3:]
|
|
50
|
+
|
|
51
|
+
def create_add_new_node(self, node_id: str, blueprint_name: str, param_id: str):
|
|
52
|
+
"""From data supplied, creates an ExecutionNode and adds it to the graph
|
|
53
|
+
Args:
|
|
54
|
+
node_id (str): human readable name of node
|
|
55
|
+
blueprint_name (str): name of the blueprint in blueprint Register
|
|
56
|
+
param_id (str): name of node params from paramater manager
|
|
57
|
+
"""
|
|
58
|
+
new_node = ExecutionNode(
|
|
59
|
+
id=node_id, blueprint_name=blueprint_name, param_id=param_id
|
|
60
|
+
)
|
|
61
|
+
self.add_node(new_node)
|
|
62
|
+
|
|
63
|
+
def add_node(self, new_node: ExecutionNode):
|
|
64
|
+
"""Adds a node to the graph"""
|
|
65
|
+
assert self.nodes.get(new_node.id, None) is None, "Node Already exists"
|
|
66
|
+
self.nodes[new_node.id] = new_node
|
|
67
|
+
|
|
68
|
+
def change_node_param_template(self, node_id: str, new_param_id: str):
|
|
69
|
+
"""Changes a node parameter template (i.e. the "params" parsed into the ExecutionNode at runtime)
|
|
70
|
+
Args:
|
|
71
|
+
node_id (str): instance name of the node within the DAG
|
|
72
|
+
new_param_id (str): the new parameter id for the node to use
|
|
73
|
+
"""
|
|
74
|
+
assert isinstance(new_param_id, str) and new_param_id, (
|
|
75
|
+
"New Parameter id Invalid"
|
|
76
|
+
)
|
|
77
|
+
node = self.nodes.get(node_id, None)
|
|
78
|
+
assert node is not None, "Node not found"
|
|
79
|
+
node.param_id = new_param_id
|
|
80
|
+
|
|
81
|
+
def remove_node(self, node_id: str):
|
|
82
|
+
"""
|
|
83
|
+
Removes a node from the DAG (and its edges)
|
|
84
|
+
Args:
|
|
85
|
+
node_id(str): the target node to be removed (DAG name not blueprint name!)
|
|
86
|
+
"""
|
|
87
|
+
assert node_id != "start", (
|
|
88
|
+
"Cannot Remove or Alter Start Node, alter it's edges, or parse different start point to build_execution instead"
|
|
89
|
+
)
|
|
90
|
+
self.edges.pop(node_id, [])
|
|
91
|
+
self.nodes.pop(node_id)
|
|
92
|
+
for e_v_lst in self.edges.values():
|
|
93
|
+
if node_id in e_v_lst:
|
|
94
|
+
e_v_lst.remove(node_id)
|
|
95
|
+
|
|
96
|
+
def add_edge(self, node_from: str, node_to: str):
|
|
97
|
+
"""Connects 2 nodes together, representing execution order
|
|
98
|
+
Args:
|
|
99
|
+
node_from (str): Node from which the connection is to be made
|
|
100
|
+
node_to (str): Node the connection ends at.
|
|
101
|
+
"""
|
|
102
|
+
assert node_to != "start", "All edges on start node must be outgoing"
|
|
103
|
+
assert node_from != node_to, "A node cannot have an edge connecting to itself"
|
|
104
|
+
assert self.nodes.get(node_from, None) is not None, (
|
|
105
|
+
f"Add Edge From Node {node_from} not present in manager"
|
|
106
|
+
)
|
|
107
|
+
assert self.nodes.get(node_to, None) is not None, (
|
|
108
|
+
f"Add Edge To Node {node_to} not present in manager"
|
|
109
|
+
)
|
|
110
|
+
neighbours = self.edges[node_from]
|
|
111
|
+
if node_to not in neighbours:
|
|
112
|
+
neighbours.append(node_to)
|
|
113
|
+
|
|
114
|
+
def remove_edge(self, node_from: str, node_to: str, allow_inverse: bool = False):
|
|
115
|
+
"""Removes 2 connecting edges of a node, allow inverse checks with the nodes in the opposite direction"""
|
|
116
|
+
self.edges[node_from].remove(node_to)
|
|
117
|
+
if allow_inverse:
|
|
118
|
+
inverse_edge = self.edges.get(node_to, None)
|
|
119
|
+
if not inverse_edge:
|
|
120
|
+
return
|
|
121
|
+
if node_from in inverse_edge:
|
|
122
|
+
inverse_edge.remove(node_from)
|
|
123
|
+
|
|
124
|
+
def get_edges(self):
|
|
125
|
+
"""Gets all edges as a set of tuples (of -> from, to) and returns them
|
|
126
|
+
Returns:
|
|
127
|
+
set: tuple of (node_from, node_to)
|
|
128
|
+
"""
|
|
129
|
+
edge_tuples = set()
|
|
130
|
+
for node_from, neighbours in self.edges.items():
|
|
131
|
+
for node_to in neighbours:
|
|
132
|
+
edge_tuples.add((node_from, node_to))
|
|
133
|
+
return edge_tuples
|
|
134
|
+
|
|
135
|
+
def get_nodes(self):
|
|
136
|
+
"""Returns a shallow copy of all nodes"""
|
|
137
|
+
return dict(self.nodes)
|
|
138
|
+
|
|
139
|
+
def get_node(self, node_id: str):
|
|
140
|
+
"""Gets a node via node_id from this dag, returns None if not present"""
|
|
141
|
+
return self.nodes.get(node_id, None)
|
|
142
|
+
|
|
143
|
+
def build_execution(self, start_node_id="start"):
|
|
144
|
+
"""Generates a linear execution of nodes from star_node_id until there are ends
|
|
145
|
+
Args:
|
|
146
|
+
start_node_id (str, optional): String of first node_id. Defaults to "start".
|
|
147
|
+
Returns:
|
|
148
|
+
list[ExecutionNode]: returns the order of nodes by execution from start->finish
|
|
149
|
+
NB: Currently does not detect loops, parallelisation is flattened currently.
|
|
150
|
+
"""
|
|
151
|
+
in_degree = {node_id: 0 for node_id in self.nodes}
|
|
152
|
+
|
|
153
|
+
for neighbors in self.edges.values():
|
|
154
|
+
for n in neighbors:
|
|
155
|
+
in_degree[n] += 1
|
|
156
|
+
|
|
157
|
+
queue = deque([start_node_id])
|
|
158
|
+
exec_order = []
|
|
159
|
+
|
|
160
|
+
while queue:
|
|
161
|
+
current = queue.popleft()
|
|
162
|
+
|
|
163
|
+
exec_order.append(self.nodes.get(current))
|
|
164
|
+
|
|
165
|
+
for neighbor in self.edges.get(current, []):
|
|
166
|
+
in_degree[neighbor] -= 1
|
|
167
|
+
if in_degree[neighbor] == 0:
|
|
168
|
+
queue.append(neighbor)
|
|
169
|
+
|
|
170
|
+
return exec_order
|
|
171
|
+
|
|
172
|
+
def load(self, name: str | None = None, load_path: str | None = None):
|
|
173
|
+
"""using the load path primarily or the name if not supplied. load selection order: load_path->name->self.name
|
|
174
|
+
Args:
|
|
175
|
+
name (str, optional): Name of graph manager. Defaults to None.
|
|
176
|
+
load_path (str, optional): Path to graph configuration file. Priortised. Defaults to None.
|
|
177
|
+
"""
|
|
178
|
+
assert load_path is not None or name is not None or self.name is not None, (
|
|
179
|
+
"At least one identifier required to find the graph manager"
|
|
180
|
+
)
|
|
181
|
+
if load_path is None:
|
|
182
|
+
load_path = self.get_file_path(name, self.dir)
|
|
183
|
+
|
|
184
|
+
with open(load_path, "r", encoding="utf-8") as load_file:
|
|
185
|
+
load_data = json.load(load_file)
|
|
186
|
+
self.edges = defaultdict(list, load_data["edges"])
|
|
187
|
+
self.name = load_data["name"] if name is None else name
|
|
188
|
+
node_data = {}
|
|
189
|
+
for node_id, exec_dict in load_data["nodes"].items():
|
|
190
|
+
node_data[node_id] = ExecutionNode(
|
|
191
|
+
id=node_id,
|
|
192
|
+
blueprint_name=exec_dict["blueprint_name"],
|
|
193
|
+
param_id=exec_dict["param_id"],
|
|
194
|
+
pos=exec_dict.get("pos", None),
|
|
195
|
+
)
|
|
196
|
+
self.nodes = node_data
|
|
197
|
+
|
|
198
|
+
def save(self):
|
|
199
|
+
"""using the name of the current graph, generates a name and saves the configuration as json"""
|
|
200
|
+
save_path = self.get_file_path(self.name, self.dir)
|
|
201
|
+
save_data = {"edges": self.edges, "name": self.name}
|
|
202
|
+
save_data["nodes"] = {
|
|
203
|
+
node_id: asdict(exec_node) for node_id, exec_node in self.nodes.items()
|
|
204
|
+
}
|
|
205
|
+
with open(save_path, "w", encoding="utf-8") as save_file:
|
|
206
|
+
json.dump(save_data, save_file)
|
|
207
|
+
|
|
208
|
+
def duplicate(self, new_name=""):
|
|
209
|
+
"""Duplicates the current graph manager into another"""
|
|
210
|
+
if not new_name:
|
|
211
|
+
new_name = f"{self.name}_copy"
|
|
212
|
+
dupe = GraphManager(name=new_name, load=False, dir=self.dir)
|
|
213
|
+
for n in self.nodes.values():
|
|
214
|
+
if n.id == "start":
|
|
215
|
+
if n.pos:
|
|
216
|
+
dupe.nodes["start"].pos = list(n.pos)
|
|
217
|
+
continue
|
|
218
|
+
n_copy = ExecutionNode(
|
|
219
|
+
id=n.id,
|
|
220
|
+
blueprint_name=n.blueprint_name,
|
|
221
|
+
param_id=n.param_id,
|
|
222
|
+
pos=list(n.pos) if n.pos else None,
|
|
223
|
+
)
|
|
224
|
+
dupe.add_node(n_copy)
|
|
225
|
+
for node_name, neighbours in self.edges.items():
|
|
226
|
+
for neigh in neighbours:
|
|
227
|
+
dupe.add_edge(node_name, neigh)
|
|
228
|
+
return dupe
|
|
229
|
+
|
|
230
|
+
@classmethod
|
|
231
|
+
def file_type(cls):
|
|
232
|
+
return "json"
|
m_strat/api/dag/node.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
|
|
3
|
+
# types that are allowed witihn blueprint params. Modification may cause error
|
|
4
|
+
__ALLOWED_SCHEMAS = {
|
|
5
|
+
int,
|
|
6
|
+
float,
|
|
7
|
+
bool,
|
|
8
|
+
str,
|
|
9
|
+
list,
|
|
10
|
+
bytes,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def BlueprintNode(name: str, schema: dict[str:type] | None = None):
|
|
15
|
+
"""Designates a function as a DAG node, Function must have arguments (context,params) ONLY and MUST always return context
|
|
16
|
+
Args:
|
|
17
|
+
name (str): Unique name of blueprint
|
|
18
|
+
schema (dict[str,type], optional): Dictionay. Defaults to {}.
|
|
19
|
+
"""
|
|
20
|
+
if schema is None:
|
|
21
|
+
schema = {}
|
|
22
|
+
|
|
23
|
+
def decorator(node_func):
|
|
24
|
+
assert isinstance(name, str), "Node Name must be a string"
|
|
25
|
+
assert len(name) > 0, "Node name is empty"
|
|
26
|
+
|
|
27
|
+
if schema:
|
|
28
|
+
assert all(
|
|
29
|
+
v in __ALLOWED_SCHEMAS and isinstance(k, str) for k, v in schema.items()
|
|
30
|
+
), "Invalid Schema keys or values"
|
|
31
|
+
|
|
32
|
+
node_func.__is_m_strat_node__ = True
|
|
33
|
+
node_func.__m_strat_node_name__ = name
|
|
34
|
+
node_func.__m_strat_node_schema__ = schema
|
|
35
|
+
return node_func
|
|
36
|
+
|
|
37
|
+
return decorator
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class ExecutionNode:
|
|
42
|
+
id: str # The human readable 'name' of the node instance
|
|
43
|
+
param_id: str # the node parmater identifier (at runtime is retireved as params)
|
|
44
|
+
blueprint_name: (
|
|
45
|
+
str # the name of the blueprint, must exactly match the BlueprintNode name
|
|
46
|
+
)
|
|
47
|
+
pos: list = None # optional positional information for GUIs
|
|
48
|
+
|
|
49
|
+
def to_dict(self):
|
|
50
|
+
"""Converts datalcass to a dictionary, used for serialization"""
|
|
51
|
+
return {
|
|
52
|
+
"id": self.id,
|
|
53
|
+
"blueprint_name": self.blueprint_name,
|
|
54
|
+
"param_id": self.param_id,
|
|
55
|
+
"pos": self.pos,
|
|
56
|
+
}
|