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.
@@ -2,14 +2,11 @@
2
2
 
3
3
  # Render Configuration
4
4
 
5
- DEFAULT_VISUALIZER_EXTENSION = "gv"
6
- """str: The default extension for the visualizer renderer."""
5
+ DEFAULT_VISUALIZER_DIRECTION = "TD"
6
+ """str: The default flowchart direction for the Mermaid visualizer (e.g. TD, LR)."""
7
7
 
8
- DEFAULT_VISUALIZER_FORMAT = "png"
9
- """str: The default format for the visualizer renderer."""
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
- DEFAULT_VISUALIZER_PARALLEL_STEP_EDGE_STYLE = "dashed"
41
- """str: The default edge style for parallel steps in the visualizer."""
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
- DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_NODE_SHAPE = "boxed"
44
- """str: The default node shape for sub-step functions in the visualizer."""
42
+ DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS = "subStepFunction"
43
+ """str: The Mermaid classDef name applied to sub-step function nodes."""
45
44
 
46
- DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_NODE_STYLE = "dotted"
47
- """ str: The default node style for sub-step functions in the visualizer."""
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
- raise ValueError(
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
- raise ValueError(
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
- raise ValueError(
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 os import getcwd
7
- from typing import Dict, Optional
8
-
9
- from graphviz import Digraph
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
- DEFAULT_VISUALIZER_FORMAT,
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
- DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_NODE_SHAPE,
23
- DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_NODE_STYLE,
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
- from stepfunction.types.visualizer_types import RenderStepFunctionParams
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.__dot = Digraph(comment=self.graph_name)
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
- """Visualizes the graph model."""
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
- if step_info["is_sub_step_function"]:
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.__dot.edge(
194
+ self.__add_edge(
63
195
  step_name,
64
196
  step_info["next_step"],
65
- label=DEFAULT_VISUALIZER_SUCCESS_EDGE_LABEL,
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
- failure_edge_label = DEFAULT_VISUALIZER_STOP_ON_FAILURE_EDGE_LABEL
74
- failure_edge_color = DEFAULT_VISUALIZER_STOP_ON_FAILURE_EDGE_COLOR
75
-
76
- self.__dot.edge(
77
- step_name,
78
- step_info["on_failure"],
79
- label=failure_edge_label,
80
- color=failure_edge_color,
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 = step_info["func"]
86
-
87
- with self.__dot.subgraph() as s:
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
- if isinstance(step_info["branch"], dict):
102
- for result, next_step in step_info["branch"].items():
103
- self.__dot.edge(step_name, next_step, label=f"Branch: {result}")
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
- format = kwargs.get("format", DEFAULT_VISUALIZER_FORMAT)
110
- renderer = kwargs.get("renderer", DEFAULT_VISUALIZER_RENDERER)
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 = file_path
120
- self.__output_file_name = file_name
345
+ self.__output_file_path = resolved_file_path
346
+ self.__output_file_name = resolved_file_name
121
347
 
122
- self.__dot.render(
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
- def render_step_function_to_string(self, **kwargs: RenderStepFunctionParams):
129
- """Renders the graph model as a string."""
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
- format = kwargs.get("format", DEFAULT_VISUALIZER_FORMAT)
132
- renderer = kwargs.get("renderer", DEFAULT_VISUALIZER_RENDERER)
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.__dot.pipe(format=format, renderer=renderer).decode(
135
- DEFAULT_VISUALIZER_STRING_ENCODING
136
- )
361
+ return self.__build_mermaid(direction)
137
362
 
138
363
  @property
139
364
  def output_file_name(self):
@@ -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.7
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 requires the following dependencies:
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=9KEtxlwGuFI9NGxM6jNb9NMqnhSYUwsomspQNtl-C0M,1764
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=eWCe-aocFShuXrYahQP5K0K9AAC6OHX0NRztCt4TX4M,16536
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=u2oUZdImZBg91U5GiP3h2kZzY9XGs_o2xxq2eYQgc98,5256
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=j4XqT5sXM7VIDwT9H6gFXBLn7jJ5CqPIE_P68dUo_nA,465
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.7.dist-info/licenses/LICENSE,sha256=sLQZUABreUtT56S4w592eAmWiFONtLKZG-KZWf-_dvg,1074
26
- stepfunction-0.0.7.dist-info/METADATA,sha256=rqge2fKFh9fa-PHg3MdiV9l0ixzucqcH4AlICpDrrS8,2723
27
- stepfunction-0.0.7.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
28
- stepfunction-0.0.7.dist-info/top_level.txt,sha256=XqLJSUeYSmzTNMiPj-IESxp1om-LdZjefT4Un2orQD4,13
29
- stepfunction-0.0.7.dist-info/RECORD,,
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,,
@@ -1,8 +0,0 @@
1
- from typing import Optional, TypedDict
2
-
3
-
4
- class RenderStepFunctionParams(TypedDict, total=False):
5
- file_path: Optional[str]
6
- file_name: Optional[str]
7
- format: str
8
- renderer: str