stepfunction 0.0.7__py3-none-any.whl → 0.1.0__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.
- stepfunction/constants/visualizer.py +20 -19
- stepfunction/core/step_function/step_function.py +32 -4
- stepfunction/core/visualizer/visualizer.py +304 -79
- stepfunction/types/step_types.py +5 -1
- {stepfunction-0.0.7.dist-info → stepfunction-0.1.0.dist-info}/METADATA +3 -8
- {stepfunction-0.0.7.dist-info → stepfunction-0.1.0.dist-info}/RECORD +9 -10
- stepfunction/types/visualizer_types.py +0 -8
- {stepfunction-0.0.7.dist-info → stepfunction-0.1.0.dist-info}/WHEEL +0 -0
- {stepfunction-0.0.7.dist-info → stepfunction-0.1.0.dist-info}/licenses/LICENSE +0 -0
- {stepfunction-0.0.7.dist-info → stepfunction-0.1.0.dist-info}/top_level.txt +0 -0
|
@@ -2,14 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
# Render Configuration
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
"""str: The default
|
|
5
|
+
DEFAULT_VISUALIZER_DIRECTION = "TD"
|
|
6
|
+
"""str: The default flowchart direction for the Mermaid visualizer (e.g. TD, LR)."""
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
"""str: The default
|
|
10
|
-
|
|
11
|
-
DEFAULT_VISUALIZER_RENDERER = "cairo"
|
|
12
|
-
"""str: The default renderer for the visualizer."""
|
|
8
|
+
DEFAULT_VISUALIZER_EXTENSION = "mmd"
|
|
9
|
+
"""str: The default file extension for the visualizer renderer output."""
|
|
13
10
|
|
|
14
11
|
DEFAULT_VISUALIZER_FOLDER = "workflow_renders"
|
|
15
12
|
"""str: The default folder where visualizer renders are stored."""
|
|
@@ -22,26 +19,30 @@ DEFAULT_VISUALIZER_STRING_ENCODING = "utf-8"
|
|
|
22
19
|
DEFAULT_VISUALIZER_SUCCESS_EDGE_LABEL = "Success"
|
|
23
20
|
"""str: The default edge label for success transitions in the visualizer."""
|
|
24
21
|
|
|
25
|
-
DEFAULT_VISUALIZER_SUCCESS_EDGE_COLOR = "black"
|
|
26
|
-
"""str: The default edge color for success transitions in the visualizer."""
|
|
27
|
-
|
|
28
22
|
DEFAULT_VISUALIZER_FAILURE_EDGE_LABEL = "Failure"
|
|
29
23
|
"""str: The default edge label for failure transitions in the visualizer."""
|
|
30
24
|
|
|
31
|
-
DEFAULT_VISUALIZER_FAILURE_EDGE_COLOR = "black"
|
|
32
|
-
"""str: The default edge color for failure transitions in the visualizer."""
|
|
33
|
-
|
|
34
25
|
DEFAULT_VISUALIZER_STOP_ON_FAILURE_EDGE_LABEL = "Stop on Failure"
|
|
35
26
|
"""str: The default edge label for stop on failure transitions in the visualizer."""
|
|
36
27
|
|
|
37
28
|
DEFAULT_VISUALIZER_STOP_ON_FAILURE_EDGE_COLOR = "red"
|
|
38
29
|
"""str: The default edge color for stop on failure transitions in the visualizer."""
|
|
39
30
|
|
|
40
|
-
|
|
41
|
-
"""str: The default edge
|
|
31
|
+
DEFAULT_VISUALIZER_BRANCH_EDGE_LABEL_PREFIX = "Branch"
|
|
32
|
+
"""str: The default edge label prefix for branch transitions in the visualizer."""
|
|
33
|
+
|
|
34
|
+
DEFAULT_VISUALIZER_BRANCH_DEFAULT_LABEL = "else"
|
|
35
|
+
"""str: The label used for an unconditional (fallback) branch return."""
|
|
36
|
+
|
|
37
|
+
DEFAULT_VISUALIZER_MAX_BRANCH_LABEL_LENGTH = 60
|
|
38
|
+
"""int: The maximum length of a branch condition label before it's truncated."""
|
|
39
|
+
|
|
40
|
+
# Node styling (Mermaid classDef names)
|
|
42
41
|
|
|
43
|
-
|
|
44
|
-
"""str: The
|
|
42
|
+
DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS = "subStepFunction"
|
|
43
|
+
"""str: The Mermaid classDef name applied to sub-step function nodes."""
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
"
|
|
45
|
+
DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS_STYLE = (
|
|
46
|
+
"fill:#f5f5f5,stroke:#333,stroke-dasharray: 5 5"
|
|
47
|
+
)
|
|
48
|
+
"""str: The Mermaid classDef style applied to sub-step function nodes."""
|
|
@@ -5,7 +5,7 @@ Author: Vineeth Penugonda
|
|
|
5
5
|
|
|
6
6
|
from asyncio import gather, get_running_loop
|
|
7
7
|
from inspect import iscoroutinefunction
|
|
8
|
-
from typing import Any, Callable, Dict, Optional, Union, cast
|
|
8
|
+
from typing import Any, Callable, Dict, List, Optional, Union, cast
|
|
9
9
|
|
|
10
10
|
from stepfunction.constants.enums import StepFunctionStatus
|
|
11
11
|
from stepfunction.exceptions.step_errors import (
|
|
@@ -165,6 +165,7 @@ class StepFunction:
|
|
|
165
165
|
"stop_on_failure": stop_on_failure,
|
|
166
166
|
"is_sub_step_function": False,
|
|
167
167
|
"step_type": step_type,
|
|
168
|
+
"sub_step_function": None,
|
|
168
169
|
}
|
|
169
170
|
|
|
170
171
|
def add_sub_step_function(
|
|
@@ -197,6 +198,7 @@ class StepFunction:
|
|
|
197
198
|
"stop_on_failure": False,
|
|
198
199
|
"is_sub_step_function": True,
|
|
199
200
|
"step_type": None,
|
|
201
|
+
"sub_step_function": sub_step_function,
|
|
200
202
|
}
|
|
201
203
|
|
|
202
204
|
def set_start_step(self, name: str):
|
|
@@ -208,24 +210,46 @@ class StepFunction:
|
|
|
208
210
|
|
|
209
211
|
def validate(self):
|
|
210
212
|
"""Validate the workflow configuration before execution."""
|
|
213
|
+
self._validate(path=[self.__name])
|
|
214
|
+
|
|
215
|
+
def _validate(self, path: List[str]):
|
|
216
|
+
"""Validate this step function, recursing into sub-step functions.
|
|
217
|
+
|
|
218
|
+
``path`` tracks the chain of step-function names from the outermost
|
|
219
|
+
workflow down to this one, so a failure several sub-step functions
|
|
220
|
+
deep is reported as a single readable breadcrumb (e.g.
|
|
221
|
+
"FLOW -> SUB_FLOW_3 -> SUB_FLOW_4") instead of a wall of nested
|
|
222
|
+
"invalid sub-step function" messages.
|
|
223
|
+
"""
|
|
224
|
+
|
|
225
|
+
def _fail(message: str) -> None:
|
|
226
|
+
location = " -> ".join(path)
|
|
227
|
+
raise ValueError(f"[{location}] {message}")
|
|
211
228
|
|
|
212
229
|
if self.__current_step is None:
|
|
213
|
-
|
|
230
|
+
_fail(
|
|
214
231
|
"No start step set. Call set_start_step() before executing the workflow."
|
|
215
232
|
)
|
|
216
233
|
|
|
217
234
|
for step_name, step in self.__steps.items():
|
|
218
235
|
if step["next_step"] is not None and step["next_step"] not in self.__steps:
|
|
219
|
-
|
|
236
|
+
_fail(
|
|
220
237
|
f"Step '{step_name}' has unknown next_step '{step['next_step']}'."
|
|
221
238
|
)
|
|
222
239
|
if (
|
|
223
240
|
step["on_failure"] is not None
|
|
224
241
|
and step["on_failure"] not in self.__steps
|
|
225
242
|
):
|
|
226
|
-
|
|
243
|
+
_fail(
|
|
227
244
|
f"Step '{step_name}' has unknown on_failure '{step['on_failure']}'."
|
|
228
245
|
)
|
|
246
|
+
if step["is_sub_step_function"]:
|
|
247
|
+
sub_step_function = cast("StepFunction", step["sub_step_function"])
|
|
248
|
+
sub_step_function._validate(
|
|
249
|
+
path=path + [f"{step_name} ({sub_step_function.name})"]
|
|
250
|
+
if step_name != sub_step_function.name
|
|
251
|
+
else path + [step_name]
|
|
252
|
+
)
|
|
229
253
|
|
|
230
254
|
async def execute(self, initial_input: Any = None):
|
|
231
255
|
"""Execute the workflow."""
|
|
@@ -376,6 +400,8 @@ class StepFunction:
|
|
|
376
400
|
"""Visualize the workflow."""
|
|
377
401
|
from stepfunction.core.visualizer import Visualizer
|
|
378
402
|
|
|
403
|
+
self.validate()
|
|
404
|
+
|
|
379
405
|
visualizer = Visualizer(self.__name, self.__steps)
|
|
380
406
|
|
|
381
407
|
self.__logger.debug("Visualizing the step function")
|
|
@@ -394,6 +420,8 @@ class StepFunction:
|
|
|
394
420
|
"""Visualize the workflow as a string."""
|
|
395
421
|
from stepfunction.core.visualizer import Visualizer
|
|
396
422
|
|
|
423
|
+
self.validate()
|
|
424
|
+
|
|
397
425
|
visualizer = Visualizer(self.__name, self.__steps)
|
|
398
426
|
|
|
399
427
|
self.__logger.debug("Visualizing the step function")
|
|
@@ -3,32 +3,166 @@
|
|
|
3
3
|
Author: Vineeth Penugonda
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
from ast import (
|
|
7
|
+
AST,
|
|
8
|
+
AsyncFunctionDef,
|
|
9
|
+
Constant,
|
|
10
|
+
FunctionDef,
|
|
11
|
+
If,
|
|
12
|
+
Lambda,
|
|
13
|
+
Return,
|
|
14
|
+
iter_child_nodes,
|
|
15
|
+
parse,
|
|
16
|
+
unparse,
|
|
17
|
+
walk,
|
|
18
|
+
)
|
|
19
|
+
from inspect import getsource
|
|
20
|
+
from os import getcwd, makedirs
|
|
21
|
+
from re import sub as re_sub
|
|
22
|
+
from textwrap import dedent
|
|
23
|
+
from typing import Any, Callable, Dict, List, Mapping, Optional, Set, Tuple, cast
|
|
10
24
|
|
|
11
25
|
from stepfunction.constants.visualizer import (
|
|
26
|
+
DEFAULT_VISUALIZER_BRANCH_DEFAULT_LABEL,
|
|
27
|
+
DEFAULT_VISUALIZER_BRANCH_EDGE_LABEL_PREFIX,
|
|
28
|
+
DEFAULT_VISUALIZER_DIRECTION,
|
|
12
29
|
DEFAULT_VISUALIZER_EXTENSION,
|
|
13
|
-
DEFAULT_VISUALIZER_FAILURE_EDGE_COLOR,
|
|
14
30
|
DEFAULT_VISUALIZER_FAILURE_EDGE_LABEL,
|
|
15
31
|
DEFAULT_VISUALIZER_FOLDER,
|
|
16
|
-
|
|
17
|
-
DEFAULT_VISUALIZER_PARALLEL_STEP_EDGE_STYLE,
|
|
18
|
-
DEFAULT_VISUALIZER_RENDERER,
|
|
32
|
+
DEFAULT_VISUALIZER_MAX_BRANCH_LABEL_LENGTH,
|
|
19
33
|
DEFAULT_VISUALIZER_STOP_ON_FAILURE_EDGE_COLOR,
|
|
20
34
|
DEFAULT_VISUALIZER_STOP_ON_FAILURE_EDGE_LABEL,
|
|
21
35
|
DEFAULT_VISUALIZER_STRING_ENCODING,
|
|
22
|
-
|
|
23
|
-
|
|
36
|
+
DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS,
|
|
37
|
+
DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS_STYLE,
|
|
24
38
|
DEFAULT_VISUALIZER_SUCCESS_EDGE_LABEL,
|
|
25
39
|
)
|
|
26
40
|
from stepfunction.types.step_types import StepParams
|
|
27
|
-
|
|
41
|
+
|
|
42
|
+
_INVALID_NODE_ID_CHARS = r"[^0-9A-Za-z_]"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _quote(label: str) -> str:
|
|
46
|
+
"""Escape and quote a string for use as a Mermaid edge label."""
|
|
47
|
+
|
|
48
|
+
escaped = label.replace('"', "'").replace("|", "/")
|
|
49
|
+
return f'"{escaped}"'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _truncate(
|
|
53
|
+
label: str, max_length: int = DEFAULT_VISUALIZER_MAX_BRANCH_LABEL_LENGTH
|
|
54
|
+
) -> str:
|
|
55
|
+
"""Truncate a label to a maximum length, preserving readability."""
|
|
56
|
+
|
|
57
|
+
if len(label) <= max_length:
|
|
58
|
+
return label
|
|
59
|
+
|
|
60
|
+
return f"{label[: max_length - 3]}..."
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _extract_callable_branch_edges(
|
|
64
|
+
branch_func: Callable[[Any], Optional[str]], known_steps: Set[str]
|
|
65
|
+
) -> List[Tuple[str, Optional[str]]]:
|
|
66
|
+
"""Statically inspects a branch callable's source for ``return "<step_name>"``
|
|
67
|
+
statements, pairing each target with the nearest enclosing ``if``/``else``
|
|
68
|
+
condition.
|
|
69
|
+
|
|
70
|
+
Router callables in this library are plain functions that inspect the prior
|
|
71
|
+
step's result and return the name of the next step. Since the callable isn't
|
|
72
|
+
executed at visualization time, the runtime outcome can't be known - but most
|
|
73
|
+
routers are simple if/elif/return chains, so a light AST scan recovers a
|
|
74
|
+
faithful approximation of the possible branches without executing the
|
|
75
|
+
callable. Falls back to no edges if the source isn't available or isn't a
|
|
76
|
+
recognizable if/return shape, rather than guessing.
|
|
77
|
+
|
|
78
|
+
Returns a list of ``(target_step, condition_label)`` tuples, where
|
|
79
|
+
``condition_label`` is ``None`` for an unconditional/fallback return.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
source = dedent(getsource(branch_func))
|
|
84
|
+
tree = parse(source)
|
|
85
|
+
except (OSError, TypeError, SyntaxError):
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
func_node = next(
|
|
89
|
+
(
|
|
90
|
+
node
|
|
91
|
+
for node in walk(tree)
|
|
92
|
+
if isinstance(node, (FunctionDef, AsyncFunctionDef))
|
|
93
|
+
),
|
|
94
|
+
None,
|
|
95
|
+
)
|
|
96
|
+
if func_node is None:
|
|
97
|
+
return []
|
|
98
|
+
|
|
99
|
+
# Map each node in the function body to the nearest enclosing `if` *and*
|
|
100
|
+
# which side of it the node is on (the `test` body vs. the `else`/`elif`
|
|
101
|
+
# body) - a plain `if/else` shares one `If` node for both branches, so
|
|
102
|
+
# without tracking the side, a return in the `else` would be mislabeled
|
|
103
|
+
# with the same (un-negated) condition as the `if` branch.
|
|
104
|
+
branch_context: Dict[AST, Optional[Tuple[If, bool]]] = {}
|
|
105
|
+
|
|
106
|
+
def _walk(node: AST, current_branch: Optional[Tuple[If, bool]]) -> None:
|
|
107
|
+
if isinstance(node, If):
|
|
108
|
+
branch_context[node] = current_branch
|
|
109
|
+
|
|
110
|
+
for child in node.body:
|
|
111
|
+
branch_context[child] = (node, True)
|
|
112
|
+
_walk(child, (node, True))
|
|
113
|
+
|
|
114
|
+
for child in node.orelse:
|
|
115
|
+
branch_context[child] = (node, False)
|
|
116
|
+
_walk(child, (node, False))
|
|
117
|
+
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
for child in iter_child_nodes(node):
|
|
121
|
+
if isinstance(child, (FunctionDef, AsyncFunctionDef, Lambda)):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
branch_context[child] = current_branch
|
|
125
|
+
_walk(child, current_branch)
|
|
126
|
+
|
|
127
|
+
_walk(func_node, None)
|
|
128
|
+
|
|
129
|
+
edges: List[Tuple[str, Optional[str]]] = []
|
|
130
|
+
seen: Set[Tuple[str, Optional[str]]] = set()
|
|
131
|
+
|
|
132
|
+
for node in walk(func_node):
|
|
133
|
+
if not isinstance(node, Return) or node.value is None:
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
value = node.value
|
|
137
|
+
if not (isinstance(value, Constant) and isinstance(value.value, str)):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
target = value.value
|
|
141
|
+
if target not in known_steps:
|
|
142
|
+
continue
|
|
143
|
+
|
|
144
|
+
context = branch_context.get(node)
|
|
145
|
+
label = None
|
|
146
|
+
if context is not None:
|
|
147
|
+
enclosing_if, is_true_branch = context
|
|
148
|
+
try:
|
|
149
|
+
condition = unparse(enclosing_if.test)
|
|
150
|
+
label = condition if is_true_branch else f"not ({condition})"
|
|
151
|
+
except Exception:
|
|
152
|
+
label = None
|
|
153
|
+
|
|
154
|
+
key = (target, label)
|
|
155
|
+
if key in seen:
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
seen.add(key)
|
|
159
|
+
edges.append((target, label))
|
|
160
|
+
|
|
161
|
+
return edges
|
|
28
162
|
|
|
29
163
|
|
|
30
164
|
class Visualizer:
|
|
31
|
-
"""This class is responsible for visualizing the graph model."""
|
|
165
|
+
"""This class is responsible for visualizing the graph model as a Mermaid flowchart."""
|
|
32
166
|
|
|
33
167
|
def __init__(self, graph_name: str, steps: Optional[Dict[str, StepParams]] = None):
|
|
34
168
|
"""Initializes the visualizer."""
|
|
@@ -39,101 +173,192 @@ class Visualizer:
|
|
|
39
173
|
self.__output_file_name = None
|
|
40
174
|
self.__output_file_path = None
|
|
41
175
|
|
|
42
|
-
self.
|
|
176
|
+
self.__node_lines: List[str] = []
|
|
177
|
+
self.__edge_lines: List[str] = []
|
|
178
|
+
self.__link_styles: List[str] = []
|
|
179
|
+
self.__sub_step_function_nodes: Set[str] = set()
|
|
180
|
+
self.__node_ids: Dict[str, str] = {}
|
|
43
181
|
|
|
44
182
|
def visualize_step_function(self):
|
|
45
|
-
"""
|
|
183
|
+
"""Builds the Mermaid flowchart definition from the step function's steps."""
|
|
46
184
|
|
|
47
185
|
if not self.__steps:
|
|
48
186
|
raise ValueError("No steps found to visualize.")
|
|
49
187
|
|
|
188
|
+
known_steps = set(self.__steps.keys())
|
|
189
|
+
|
|
50
190
|
for step_name, step_info in self.__steps.items():
|
|
51
|
-
|
|
52
|
-
self.__dot.node(
|
|
53
|
-
step_name,
|
|
54
|
-
step_name,
|
|
55
|
-
shape=DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_NODE_SHAPE,
|
|
56
|
-
style=DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_NODE_STYLE,
|
|
57
|
-
)
|
|
58
|
-
else:
|
|
59
|
-
self.__dot.node(step_name, step_name)
|
|
191
|
+
self.__add_node(step_name, step_info)
|
|
60
192
|
|
|
61
193
|
if step_info["next_step"]:
|
|
62
|
-
self.
|
|
194
|
+
self.__add_edge(
|
|
63
195
|
step_name,
|
|
64
196
|
step_info["next_step"],
|
|
65
|
-
|
|
197
|
+
DEFAULT_VISUALIZER_SUCCESS_EDGE_LABEL,
|
|
66
198
|
)
|
|
67
199
|
|
|
68
200
|
if step_info["on_failure"]:
|
|
69
|
-
failure_edge_label = DEFAULT_VISUALIZER_FAILURE_EDGE_LABEL
|
|
70
|
-
failure_edge_color = DEFAULT_VISUALIZER_FAILURE_EDGE_COLOR
|
|
71
|
-
|
|
72
201
|
if step_info.get("stop_on_failure"):
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
202
|
+
self.__add_edge(
|
|
203
|
+
step_name,
|
|
204
|
+
step_info["on_failure"],
|
|
205
|
+
DEFAULT_VISUALIZER_STOP_ON_FAILURE_EDGE_LABEL,
|
|
206
|
+
dashed=True,
|
|
207
|
+
color=DEFAULT_VISUALIZER_STOP_ON_FAILURE_EDGE_COLOR,
|
|
208
|
+
)
|
|
209
|
+
else:
|
|
210
|
+
self.__add_edge(
|
|
211
|
+
step_name,
|
|
212
|
+
step_info["on_failure"],
|
|
213
|
+
DEFAULT_VISUALIZER_FAILURE_EDGE_LABEL,
|
|
214
|
+
dashed=True,
|
|
215
|
+
)
|
|
82
216
|
|
|
83
217
|
if step_info["parallel"]:
|
|
84
218
|
# func is a dictionary for parallel steps
|
|
85
|
-
parallel_function_names =
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
s.attr(rank="same")
|
|
89
|
-
|
|
90
|
-
for parallel_step_name, func in parallel_function_names.items():
|
|
91
|
-
self.__dot.node(parallel_step_name, parallel_step_name)
|
|
92
|
-
self.__dot.edge(
|
|
93
|
-
step_name,
|
|
94
|
-
parallel_step_name,
|
|
95
|
-
style=DEFAULT_VISUALIZER_PARALLEL_STEP_EDGE_STYLE,
|
|
96
|
-
)
|
|
97
|
-
|
|
98
|
-
if step_info["next_step"]:
|
|
99
|
-
self.__dot.edge(parallel_step_name, step_info["next_step"])
|
|
219
|
+
parallel_function_names = cast(
|
|
220
|
+
Dict[str, Callable[[Any], Any]], step_info["func"]
|
|
221
|
+
)
|
|
100
222
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
self.
|
|
223
|
+
for parallel_step_name in parallel_function_names:
|
|
224
|
+
self.__add_node(parallel_step_name, {})
|
|
225
|
+
self.__add_edge(step_name, parallel_step_name, dashed=True)
|
|
226
|
+
|
|
227
|
+
if step_info["next_step"]:
|
|
228
|
+
self.__add_edge(parallel_step_name, step_info["next_step"])
|
|
229
|
+
|
|
230
|
+
branch = step_info["branch"]
|
|
231
|
+
if isinstance(branch, dict):
|
|
232
|
+
for result, next_step in branch.items():
|
|
233
|
+
self.__add_edge(
|
|
234
|
+
step_name,
|
|
235
|
+
next_step,
|
|
236
|
+
f"{DEFAULT_VISUALIZER_BRANCH_EDGE_LABEL_PREFIX}: {result}",
|
|
237
|
+
)
|
|
238
|
+
elif callable(branch):
|
|
239
|
+
for target, condition in _extract_callable_branch_edges(
|
|
240
|
+
branch, known_steps
|
|
241
|
+
):
|
|
242
|
+
label_suffix = condition or DEFAULT_VISUALIZER_BRANCH_DEFAULT_LABEL
|
|
243
|
+
label = (
|
|
244
|
+
f"{DEFAULT_VISUALIZER_BRANCH_EDGE_LABEL_PREFIX}: "
|
|
245
|
+
f"{_truncate(label_suffix)}"
|
|
246
|
+
)
|
|
247
|
+
self.__add_edge(step_name, target, label)
|
|
248
|
+
|
|
249
|
+
def __node_id(self, step_name: str) -> str:
|
|
250
|
+
"""Maps a step name to a Mermaid-safe node ID.
|
|
251
|
+
|
|
252
|
+
Step names are free text and may contain spaces or other characters
|
|
253
|
+
that Mermaid doesn't allow in a bare node ID (only the bracketed label
|
|
254
|
+
accepts arbitrary text). The original name is preserved as the node's
|
|
255
|
+
label; this only affects the identifier used to reference the node in
|
|
256
|
+
edges. Cached so the same step name always resolves to the same ID,
|
|
257
|
+
and disambiguated on collision (e.g. "First Step" and "First_Step"
|
|
258
|
+
would otherwise both sanitize to "First_Step").
|
|
259
|
+
"""
|
|
260
|
+
|
|
261
|
+
if step_name in self.__node_ids:
|
|
262
|
+
return self.__node_ids[step_name]
|
|
263
|
+
|
|
264
|
+
sanitized = re_sub(_INVALID_NODE_ID_CHARS, "_", step_name) or "step"
|
|
265
|
+
if sanitized[0].isdigit():
|
|
266
|
+
sanitized = f"_{sanitized}"
|
|
267
|
+
|
|
268
|
+
existing_ids = set(self.__node_ids.values())
|
|
269
|
+
candidate = sanitized
|
|
270
|
+
suffix = 2
|
|
271
|
+
while candidate in existing_ids:
|
|
272
|
+
candidate = f"{sanitized}_{suffix}"
|
|
273
|
+
suffix += 1
|
|
274
|
+
|
|
275
|
+
self.__node_ids[step_name] = candidate
|
|
276
|
+
return candidate
|
|
277
|
+
|
|
278
|
+
def __add_node(self, step_name: str, step_info: Mapping[str, Any]) -> None:
|
|
279
|
+
node_id = self.__node_id(step_name)
|
|
280
|
+
|
|
281
|
+
if step_info.get("is_sub_step_function"):
|
|
282
|
+
self.__node_lines.append(f"{node_id}([{_quote(step_name)}])")
|
|
283
|
+
self.__sub_step_function_nodes.add(node_id)
|
|
284
|
+
else:
|
|
285
|
+
self.__node_lines.append(f"{node_id}[{_quote(step_name)}]")
|
|
286
|
+
|
|
287
|
+
def __add_edge(
|
|
288
|
+
self,
|
|
289
|
+
source: str,
|
|
290
|
+
target: str,
|
|
291
|
+
label: Optional[str] = None,
|
|
292
|
+
dashed: bool = False,
|
|
293
|
+
color: Optional[str] = None,
|
|
294
|
+
) -> None:
|
|
295
|
+
arrow = "-.->" if dashed else "-->"
|
|
296
|
+
source_id = self.__node_id(source)
|
|
297
|
+
target_id = self.__node_id(target)
|
|
298
|
+
|
|
299
|
+
if label:
|
|
300
|
+
self.__edge_lines.append(
|
|
301
|
+
f"{source_id} {arrow}|{_quote(label)}| {target_id}"
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
self.__edge_lines.append(f"{source_id} {arrow} {target_id}")
|
|
305
|
+
|
|
306
|
+
if color:
|
|
307
|
+
edge_index = len(self.__edge_lines) - 1
|
|
308
|
+
self.__link_styles.append(f"linkStyle {edge_index} stroke:{color}")
|
|
309
|
+
|
|
310
|
+
def __build_mermaid(self, direction: str) -> str:
|
|
311
|
+
lines = [f"flowchart {direction}"]
|
|
312
|
+
|
|
313
|
+
lines.extend(f" {line}" for line in self.__node_lines)
|
|
314
|
+
lines.extend(f" {line}" for line in self.__edge_lines)
|
|
315
|
+
lines.extend(f" {line}" for line in self.__link_styles)
|
|
316
|
+
|
|
317
|
+
if self.__sub_step_function_nodes:
|
|
318
|
+
lines.append(
|
|
319
|
+
f" classDef {DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS} "
|
|
320
|
+
f"{DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS_STYLE}"
|
|
321
|
+
)
|
|
322
|
+
node_list = ",".join(sorted(self.__sub_step_function_nodes))
|
|
323
|
+
lines.append(
|
|
324
|
+
f" class {node_list} {DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS}"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
return "\n".join(lines) + "\n"
|
|
328
|
+
|
|
329
|
+
def render_step_function(
|
|
330
|
+
self,
|
|
331
|
+
*,
|
|
332
|
+
file_path: Optional[str] = None,
|
|
333
|
+
file_name: Optional[str] = None,
|
|
334
|
+
direction: str = DEFAULT_VISUALIZER_DIRECTION,
|
|
335
|
+
) -> None:
|
|
336
|
+
"""Writes the Mermaid flowchart definition to a .mmd file."""
|
|
104
337
|
|
|
105
|
-
def render_step_function(self, **kwargs: RenderStepFunctionParams):
|
|
106
|
-
"""Renders the graph model."""
|
|
107
338
|
current_dir = getcwd()
|
|
108
339
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
file_path = kwargs.get(
|
|
113
|
-
"file_path", f"{current_dir}/{DEFAULT_VISUALIZER_FOLDER}"
|
|
114
|
-
)
|
|
115
|
-
file_name = kwargs.get(
|
|
116
|
-
"file_name", f"{self.graph_name}.{DEFAULT_VISUALIZER_EXTENSION}"
|
|
340
|
+
resolved_file_path = file_path or f"{current_dir}/{DEFAULT_VISUALIZER_FOLDER}"
|
|
341
|
+
resolved_file_name = (
|
|
342
|
+
file_name or f"{self.graph_name}.{DEFAULT_VISUALIZER_EXTENSION}"
|
|
117
343
|
)
|
|
118
344
|
|
|
119
|
-
self.__output_file_path =
|
|
120
|
-
self.__output_file_name =
|
|
345
|
+
self.__output_file_path = resolved_file_path
|
|
346
|
+
self.__output_file_name = resolved_file_name
|
|
121
347
|
|
|
122
|
-
|
|
123
|
-
filename=f"{self.__output_file_path}/{self.__output_file_name}",
|
|
124
|
-
format=format,
|
|
125
|
-
renderer=renderer,
|
|
126
|
-
)
|
|
348
|
+
makedirs(resolved_file_path, exist_ok=True)
|
|
127
349
|
|
|
128
|
-
|
|
129
|
-
|
|
350
|
+
output_file = f"{resolved_file_path}/{resolved_file_name}"
|
|
351
|
+
with open(
|
|
352
|
+
output_file, "w", encoding=DEFAULT_VISUALIZER_STRING_ENCODING
|
|
353
|
+
) as handle:
|
|
354
|
+
handle.write(self.__build_mermaid(direction))
|
|
130
355
|
|
|
131
|
-
|
|
132
|
-
|
|
356
|
+
def render_step_function_to_string(
|
|
357
|
+
self, *, direction: str = DEFAULT_VISUALIZER_DIRECTION
|
|
358
|
+
) -> str:
|
|
359
|
+
"""Renders the Mermaid flowchart definition as a string."""
|
|
133
360
|
|
|
134
|
-
return self.
|
|
135
|
-
DEFAULT_VISUALIZER_STRING_ENCODING
|
|
136
|
-
)
|
|
361
|
+
return self.__build_mermaid(direction)
|
|
137
362
|
|
|
138
363
|
@property
|
|
139
364
|
def output_file_name(self):
|
stepfunction/types/step_types.py
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
from typing import Any, Callable, Dict, Optional, TypedDict, Union
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, Optional, TypedDict, Union
|
|
2
2
|
|
|
3
3
|
from stepfunction.constants.enums import StepType
|
|
4
4
|
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from stepfunction.core.step_function.step_function import StepFunction
|
|
7
|
+
|
|
5
8
|
|
|
6
9
|
class StepParams(TypedDict):
|
|
7
10
|
func: Union[Callable[[Any], Any], Dict[str, Callable[[Any], Any]]]
|
|
@@ -12,3 +15,4 @@ class StepParams(TypedDict):
|
|
|
12
15
|
stop_on_failure: bool
|
|
13
16
|
is_sub_step_function: bool
|
|
14
17
|
step_type: Optional[StepType]
|
|
18
|
+
sub_step_function: Optional["StepFunction"]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: stepfunction
|
|
3
|
-
Version: 0.0
|
|
3
|
+
Version: 0.1.0
|
|
4
4
|
Summary: Step Function Workflow Orchestration Library
|
|
5
5
|
Author: Vineeth Penugonda
|
|
6
6
|
License-Expression: MIT
|
|
@@ -16,7 +16,6 @@ Classifier: Topic :: Software Development :: Libraries
|
|
|
16
16
|
Requires-Python: >=3.9
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
License-File: LICENSE
|
|
19
|
-
Requires-Dist: graphviz===0.20.3
|
|
20
19
|
Dynamic: license-file
|
|
21
20
|
|
|
22
21
|
# StepFunction
|
|
@@ -33,7 +32,7 @@ Dynamic: license-file
|
|
|
33
32
|
- **Error Handling**: Define failure paths and handle exceptions gracefully.
|
|
34
33
|
- **Branching Logic**: Direct workflows based on conditions.
|
|
35
34
|
- **Sub-Step Functions**: Modularize workflows by embedding sub-step functions.
|
|
36
|
-
- **Visualization**: Integrated support for visualizing the workflow graph.
|
|
35
|
+
- **Visualization**: Integrated support for visualizing the workflow graph as a Mermaid flowchart.
|
|
37
36
|
|
|
38
37
|
## Installation
|
|
39
38
|
|
|
@@ -49,11 +48,7 @@ For more detailed usage examples and advanced use cases, please read the [blog p
|
|
|
49
48
|
|
|
50
49
|
## Dependencies
|
|
51
50
|
|
|
52
|
-
The StepFunction package
|
|
53
|
-
|
|
54
|
-
- `graphviz == 0.20.3`
|
|
55
|
-
|
|
56
|
-
These will be automatically installed when you install the package via pip.
|
|
51
|
+
The StepFunction package has no required runtime dependencies. Workflow visualization renders directly to [Mermaid](https://mermaid.js.org/) flowchart syntax (`.mmd`), which can be previewed in VS Code, GitHub/Bitbucket markdown, or https://mermaid.live without any additional tooling.
|
|
57
52
|
|
|
58
53
|
## Contributing
|
|
59
54
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
stepfunction/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
stepfunction/constants/enums.py,sha256=K4_xKGa0NApw6Ai4VkfGW1Sc5fEqwFPpuyezy6YexzY,240
|
|
3
|
-
stepfunction/constants/visualizer.py,sha256=
|
|
3
|
+
stepfunction/constants/visualizer.py,sha256=impswU0JpqgpzTu4_a26FVLK4tII4ptbUaopFb2ZObM,1815
|
|
4
4
|
stepfunction/core/step_function/__init__.py,sha256=4L5ufP13A7hXBDBYrD45pfEsV9bsa6zbSkSnHpi-tyM,68
|
|
5
|
-
stepfunction/core/step_function/step_function.py,sha256=
|
|
5
|
+
stepfunction/core/step_function/step_function.py,sha256=qPfHSEP1LFLR6_hRVxxD6duALaDcTkIRWO_HrdYAg7s,17666
|
|
6
6
|
stepfunction/core/visualizer/__init__.py,sha256=pgNI6LPnY9Pm9e_YhMn-5CVmUzp8mvoytbDZ_b3McSw,61
|
|
7
|
-
stepfunction/core/visualizer/visualizer.py,sha256=
|
|
7
|
+
stepfunction/core/visualizer/visualizer.py,sha256=XM-kSoIPha-rYM2T-ZYYoo6-ttcnodn-832MX9Gg6d0,12892
|
|
8
8
|
stepfunction/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
9
|
stepfunction/exceptions/step_errors.py,sha256=yNJt6tiO8LkiSxeHoSZBpaKA7qkqdsXPnOd3AMt3C8A,486
|
|
10
10
|
stepfunction/steps/__init__.py,sha256=DvzYMJiMY-5O2tWhCa82LSbqIa80VXfUuLgFYyAOdi8,256
|
|
@@ -16,14 +16,13 @@ stepfunction/steps/base/base_step.py,sha256=Gf4NHE63n-zQigK5xATH7DEcm_pBRxivxkT0
|
|
|
16
16
|
stepfunction/steps/exceptions/__init__.py,sha256=s72aRyWdZjdSiTj4wq5rpoCIpzgQprN9uMgehK5D01U,78
|
|
17
17
|
stepfunction/steps/exceptions/step_exceptions.py,sha256=oulQdYDOLltImzfSLsZa14jLImBjwJecGpT7Q9sxgYk,282
|
|
18
18
|
stepfunction/types/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
19
|
-
stepfunction/types/step_types.py,sha256=
|
|
20
|
-
stepfunction/types/visualizer_types.py,sha256=vkL25eWSTFJ89DbnowraZ_7IJIEuM1y7_udYFu29fYU,189
|
|
19
|
+
stepfunction/types/step_types.py,sha256=pMA8xY_a3yNmpDul8BBqa-0_Fpp3QNIMFpxAgn_tUAA,622
|
|
21
20
|
stepfunction/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
21
|
stepfunction/utils/constants.py,sha256=VZefE-OhNXOyg28wPu62VZPvReublyqWS515-TbYWo0,391
|
|
23
22
|
stepfunction/utils/logger.py,sha256=0L8F8oO5HiYNqcuV8G8UD4cTdCpmObQYrkvwvrF1S-8,1789
|
|
24
23
|
stepfunction/utils/utils.py,sha256=Zw9-sKE2LOghxyiKImqGWSv4nHZ9nI8yEptp1xaYfHk,563
|
|
25
|
-
stepfunction-0.0.
|
|
26
|
-
stepfunction-0.0.
|
|
27
|
-
stepfunction-0.0.
|
|
28
|
-
stepfunction-0.0.
|
|
29
|
-
stepfunction-0.0.
|
|
24
|
+
stepfunction-0.1.0.dist-info/licenses/LICENSE,sha256=sLQZUABreUtT56S4w592eAmWiFONtLKZG-KZWf-_dvg,1074
|
|
25
|
+
stepfunction-0.1.0.dist-info/METADATA,sha256=QG6O4Dkz66awYt9zyBhTBKwYbBFWmQQ4VWBuQyrr3rw,2836
|
|
26
|
+
stepfunction-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
27
|
+
stepfunction-0.1.0.dist-info/top_level.txt,sha256=XqLJSUeYSmzTNMiPj-IESxp1om-LdZjefT4Un2orQD4,13
|
|
28
|
+
stepfunction-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|