stepfunction 0.1.0__py3-none-any.whl → 0.2.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.
File without changes
@@ -46,3 +46,8 @@ DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS_STYLE = (
46
46
  "fill:#f5f5f5,stroke:#333,stroke-dasharray: 5 5"
47
47
  )
48
48
  """str: The Mermaid classDef style applied to sub-step function nodes."""
49
+
50
+ # Node ID sanitization
51
+
52
+ VISUALIZER_INVALID_NODE_ID_CHARS = r"[^0-9A-Za-z_]"
53
+ """str: Regex matching characters not allowed in a Mermaid node ID; matches are replaced with "_"."""
@@ -0,0 +1,3 @@
1
+ from .serializer import decode_step_function, encode_step_function
2
+
3
+ __all__ = ["decode_step_function", "encode_step_function"]
@@ -0,0 +1,170 @@
1
+ """Encode/decode logic for declarative (JSON-able dict) StepFunction specs.
2
+
3
+ Author: Vineeth Penugonda
4
+ """
5
+
6
+ from typing import Any, Callable, Dict, Optional, cast
7
+
8
+ from stepfunction.core.step_function.step_function import StepFunction
9
+ from stepfunction.exceptions.step_errors import (
10
+ UnregisteredFunctionError,
11
+ UnserializableStepError,
12
+ )
13
+ from stepfunction.registry.step_registry import StepRegistry
14
+ from stepfunction.registry.step_registry import registry as default_registry
15
+ from stepfunction.types.step_types import StepParams
16
+
17
+
18
+ def _name_for_or_raise(
19
+ step_registry: StepRegistry, func: Callable[[Any], Any], step_name: str
20
+ ) -> str:
21
+ name = step_registry.name_for(func)
22
+ if name is None:
23
+ raise UnregisteredFunctionError(
24
+ f"The function used in step '{step_name}' is not registered in the "
25
+ "given registry, so its name can't be determined for export. "
26
+ "Register it with stepfunction.registry.step_registry.register_step() first."
27
+ )
28
+ return name
29
+
30
+
31
+ def _encode_step(
32
+ step_name: str, step: StepParams, step_registry: StepRegistry
33
+ ) -> Dict[str, Any]:
34
+ if step["step_type"] is not None:
35
+ raise UnserializableStepError(step_name, step["step_type"])
36
+
37
+ encoded: Dict[str, Any] = {
38
+ "next_step": step["next_step"],
39
+ "on_failure": step["on_failure"],
40
+ "parallel": step["parallel"],
41
+ "stop_on_failure": step["stop_on_failure"],
42
+ }
43
+
44
+ if step["is_sub_step_function"]:
45
+ sub_step_function = cast(StepFunction, step["sub_step_function"])
46
+ encoded["sub_step_function"] = encode_step_function(
47
+ sub_step_function, step_registry
48
+ )
49
+ return encoded
50
+
51
+ if step["parallel"]:
52
+ func_map = cast(Dict[str, Callable[[Any], Any]], step["func"])
53
+ encoded["func"] = {
54
+ slot: _name_for_or_raise(step_registry, fn, step_name)
55
+ for slot, fn in func_map.items()
56
+ }
57
+ else:
58
+ func = cast(Callable[[Any], Any], step["func"])
59
+ encoded["func"] = _name_for_or_raise(step_registry, func, step_name)
60
+
61
+ branch = step["branch"]
62
+ if branch is not None:
63
+ if callable(branch):
64
+ encoded["branch"] = _name_for_or_raise(step_registry, branch, step_name)
65
+ else:
66
+ encoded["branch"] = {str(key): value for key, value in branch.items()}
67
+
68
+ return encoded
69
+
70
+
71
+ def encode_step_function(
72
+ sf: StepFunction, step_registry: Optional[StepRegistry] = None
73
+ ) -> Dict[str, Any]:
74
+ """Export ``sf`` as a JSON-able dict.
75
+
76
+ Recurses into nested sub-step-functions by calling itself again, so
77
+ arbitrary nesting depth is handled without special-casing.
78
+
79
+ Raises:
80
+ UnserializableStepError: If any step was built from a BaseStep
81
+ instance (RetryStep, TimeoutStep, WaitStep, or a custom
82
+ BaseStep subclass) — not yet supported.
83
+ UnregisteredFunctionError: If a step or branch function used in
84
+ ``sf`` has no registered name in ``step_registry``.
85
+ """
86
+ step_registry = step_registry or default_registry
87
+
88
+ return {
89
+ "name": sf.name,
90
+ "start_step": sf.current_step,
91
+ "steps": {
92
+ step_name: _encode_step(step_name, step, step_registry)
93
+ for step_name, step in sf.steps.items()
94
+ },
95
+ }
96
+
97
+
98
+ def decode_step_function(
99
+ data: Dict[str, Any],
100
+ step_registry: Optional[StepRegistry] = None,
101
+ _validate: bool = True,
102
+ ) -> StepFunction:
103
+ """Reconstruct a StepFunction from a dict produced by ``encode_step_function``.
104
+
105
+ Rebuilds the workflow purely through ``add_step``/``add_sub_step_function``/
106
+ ``set_start_step`` — the same public API a user would call by hand — and
107
+ recurses into nested "sub_step_function" entries by calling itself again.
108
+ Validates exactly once, at the very end, at the outermost level only:
109
+ ``StepFunction.validate()`` already recurses into sub-step functions and
110
+ reports a readable breadcrumb across nesting levels, so a malformed
111
+ nested spec still fails fast without the decoder needing its own
112
+ recursive validation pass.
113
+
114
+ Raises:
115
+ UnregisteredFunctionError: If a referenced function name isn't
116
+ registered in ``step_registry`` (defaults to the package's
117
+ default singleton registry if not given).
118
+ ValueError: If the reconstructed workflow fails validate().
119
+ """
120
+ step_registry = step_registry or default_registry
121
+
122
+ sf = StepFunction(data["name"])
123
+
124
+ for step_name, step_data in data["steps"].items():
125
+ if "sub_step_function" in step_data:
126
+ sub_step_function = decode_step_function(
127
+ step_data["sub_step_function"], step_registry, _validate=False
128
+ )
129
+ sf.add_sub_step_function(
130
+ step_name,
131
+ sub_step_function=sub_step_function,
132
+ next_step=step_data.get("next_step"),
133
+ on_failure=step_data.get("on_failure"),
134
+ )
135
+ continue
136
+
137
+ func_spec = step_data["func"]
138
+ if isinstance(func_spec, dict):
139
+ func: Any = {
140
+ slot: step_registry.get(ref) for slot, ref in func_spec.items()
141
+ }
142
+ else:
143
+ func = step_registry.get(func_spec)
144
+
145
+ branch_spec = step_data.get("branch")
146
+ if branch_spec is None:
147
+ branch: Any = None
148
+ elif isinstance(branch_spec, dict):
149
+ branch = dict(branch_spec)
150
+ else:
151
+ branch = step_registry.get(branch_spec)
152
+
153
+ sf.add_step(
154
+ step_name,
155
+ func,
156
+ next_step=step_data.get("next_step"),
157
+ on_failure=step_data.get("on_failure"),
158
+ branch=branch,
159
+ parallel=step_data.get("parallel", False),
160
+ stop_on_failure=step_data.get("stop_on_failure", False),
161
+ )
162
+
163
+ start_step = data.get("start_step")
164
+ if start_step is not None:
165
+ sf.set_start_step(start_step)
166
+
167
+ if _validate:
168
+ sf.validate()
169
+
170
+ return sf
@@ -3,18 +3,24 @@
3
3
  Author: Vineeth Penugonda
4
4
  """
5
5
 
6
- from asyncio import gather, get_running_loop
7
- from inspect import iscoroutinefunction
8
- from typing import Any, Callable, Dict, List, Optional, Union, cast
6
+ from asyncio import CancelledError, gather, get_running_loop
7
+ from inspect import isawaitable, iscoroutinefunction
8
+ from time import monotonic
9
+ from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union, cast
9
10
 
10
11
  from stepfunction.constants.enums import StepFunctionStatus
11
12
  from stepfunction.exceptions.step_errors import (
12
13
  ParallelStepExecutionError,
13
14
  StepExecutionError,
14
15
  )
16
+ from stepfunction.hooks import StepEvent, StepFunctionHooks, WorkflowEvent
15
17
  from stepfunction.steps.base import BaseStep
16
18
  from stepfunction.types.step_types import StepParams
17
19
  from stepfunction.utils.logger import setup_logger
20
+ from stepfunction.utils.utils import utc_now
21
+
22
+ if TYPE_CHECKING:
23
+ from stepfunction.registry.step_registry import StepRegistry
18
24
 
19
25
 
20
26
  class StepFunction:
@@ -33,6 +39,7 @@ class StepFunction:
33
39
 
34
40
  Properties:
35
41
  name (str): The name of the step function.
42
+ hooks (Optional[StepFunctionHooks]): The lifecycle hooks set on this step function, if any.
36
43
  steps (Dict[str, StepParams]): A dictionary containing the steps of the workflow.
37
44
  last_result (Any): The result of the last step.
38
45
  context (Dict[str, Any]): Stores step names and results, including exceptions if any occur.
@@ -49,6 +56,9 @@ class StepFunction:
49
56
  set_start_step(name):
50
57
  Set the start step of the workflow.
51
58
 
59
+ set_hooks(hooks):
60
+ Set the lifecycle hooks (a StepFunctionHooks) notified as steps start, succeed and fail.
61
+
52
62
  add_sub_step_function(name, sub_step_function, next_step=None, on_failure=None):
53
63
  Add a sub-step function to be executed as a step.
54
64
 
@@ -66,6 +76,14 @@ class StepFunction:
66
76
  visualize_to_string():
67
77
  Return a string representation of the workflow for visualization.
68
78
 
79
+ to_dict(step_registry=None) / to_json(indent=2, step_registry=None):
80
+ Export the workflow as a JSON-able dict / JSON string, resolving step and
81
+ branch functions to their registered name via stepfunction.registry.step_registry.
82
+
83
+ from_dict(data, step_registry=None) / from_json(json_str, step_registry=None):
84
+ Classmethods that reconstruct a StepFunction from a dict/JSON string
85
+ produced by to_dict()/to_json(), resolving function names via the registry.
86
+
69
87
  Protected Methods:
70
88
  _execute_step(func, input_value):
71
89
  Execute a single step, handling asynchronous and synchronous functions appropriately.
@@ -108,11 +126,25 @@ class StepFunction:
108
126
 
109
127
  Status Example:
110
128
  status = step_function.status # Will be StepFunctionStatus.INITIALIZED, StepFunctionStatus.RUNNING, StepFunctionStatus.COMPLETED, or StepFunctionStatus.FAILED.
129
+
130
+ Hooks Example:
131
+ class RunRecorder(StepFunctionHooks):
132
+ async def on_step_success(self, event):
133
+ print(event.step, event.duration, event.next_step)
134
+
135
+ step_function = StepFunction("MyStepFunction", hooks=RunRecorder())
111
136
  """
112
137
 
113
- def __init__(self, name: str):
138
+ def __init__(self, name: str, hooks: Optional[StepFunctionHooks] = None):
114
139
  self.__name = name # Name of the step function
115
140
 
141
+ self.__hooks = hooks # Lifecycle hooks set on this step function
142
+
143
+ # Hooks and workflow path for the current run; a sub-step function
144
+ # without hooks of its own inherits its parent's
145
+ self.__run_hooks: Optional[StepFunctionHooks] = None
146
+ self.__run_path: List[str] = [name]
147
+
116
148
  self.__steps: Dict[str, StepParams] = {} # Steps of the workflow
117
149
  self.__current_step = None # The current step being executed
118
150
 
@@ -181,7 +213,13 @@ class StepFunction:
181
213
  raise ValueError(f"Step '{name}' already exists in steps")
182
214
 
183
215
  async def sub_func(last_result):
184
- await sub_step_function.execute(initial_input=last_result)
216
+ await sub_step_function._execute(
217
+ initial_input=last_result,
218
+ hooks=sub_step_function.hooks
219
+ if sub_step_function.hooks is not None
220
+ else self.__run_hooks,
221
+ path=self.__run_path + [_sub_path_label(name, sub_step_function)],
222
+ )
185
223
  self.__context.update(sub_step_function.context)
186
224
 
187
225
  if sub_step_function.status == StepFunctionStatus.FAILED:
@@ -208,6 +246,10 @@ class StepFunction:
208
246
 
209
247
  self.__current_step = name
210
248
 
249
+ def set_hooks(self, hooks: Optional[StepFunctionHooks]):
250
+ """Set the lifecycle hooks notified as the workflow runs (None removes them)."""
251
+ self.__hooks = hooks
252
+
211
253
  def validate(self):
212
254
  """Validate the workflow configuration before execution."""
213
255
  self._validate(path=[self.__name])
@@ -246,16 +288,33 @@ class StepFunction:
246
288
  if step["is_sub_step_function"]:
247
289
  sub_step_function = cast("StepFunction", step["sub_step_function"])
248
290
  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]
291
+ path=path + [_sub_path_label(step_name, sub_step_function)]
252
292
  )
253
293
 
254
294
  async def execute(self, initial_input: Any = None):
255
295
  """Execute the workflow."""
256
296
 
297
+ await self._execute(initial_input, hooks=self.__hooks, path=[self.__name])
298
+
299
+ async def _execute(
300
+ self,
301
+ initial_input: Any,
302
+ hooks: Optional[StepFunctionHooks],
303
+ path: List[str],
304
+ ):
305
+ """Execute the workflow, reporting to ``hooks`` under the workflow ``path``.
306
+
307
+ ``path`` is the chain of step-function names from the outermost
308
+ workflow down to this one, as in _validate(); sub-step functions are
309
+ run through here so their events carry the full path.
310
+ """
311
+
257
312
  self.validate()
258
313
 
314
+ self.__run_hooks = hooks
315
+ self.__run_path = path
316
+ workflow = " -> ".join(path)
317
+
259
318
  self.__status = StepFunctionStatus.RUNNING
260
319
 
261
320
  self.__logger.debug(
@@ -264,96 +323,174 @@ class StepFunction:
264
323
 
265
324
  self.__last_result = initial_input
266
325
 
267
- while self.__current_step:
268
- step = self.__steps[self.__current_step]
269
- try:
270
- if step["parallel"]:
271
- results = await self._execute_parallel(
272
- cast(Dict[str, Callable[[Any], Any]], step["func"]),
273
- step["stop_on_failure"],
274
- )
275
-
276
- self.__last_result = results
277
- self.__context[self.__current_step] = results
326
+ run_started_at = utc_now()
327
+ run_started = monotonic()
328
+ await self._emit(
329
+ "on_workflow_start",
330
+ WorkflowEvent(
331
+ workflow=workflow,
332
+ status=self.__status,
333
+ input=initial_input,
334
+ started_at=run_started_at,
335
+ ),
336
+ )
278
337
 
279
- self.__logger.info(
280
- f"Parallel step '{self.__current_step}' succeeded with results: {results}"
281
- )
282
- else:
283
- result = await self._execute_step(
284
- cast(Callable[[Any], Any], step["func"]), self.__last_result
285
- )
338
+ run_error: Optional[BaseException] = None
339
+ try:
340
+ while self.__current_step:
341
+ await self._execute_current_step(workflow)
342
+ except BaseException as exc:
343
+ run_error = exc
344
+ self.__status = StepFunctionStatus.FAILED
345
+ raise
346
+ finally:
347
+ await self._emit(
348
+ "on_workflow_end",
349
+ WorkflowEvent(
350
+ workflow=workflow,
351
+ status=self.__status,
352
+ input=initial_input,
353
+ started_at=run_started_at,
354
+ output=self.__last_result,
355
+ error=run_error,
356
+ finished_at=utc_now(),
357
+ duration=monotonic() - run_started,
358
+ ),
359
+ )
286
360
 
287
- self.__last_result = result
288
- self.__context[self.__current_step] = result
361
+ async def _execute_current_step(self, workflow: str):
362
+ """Execute the current step, route to the next one and report both to the hooks."""
363
+
364
+ step_name = cast(str, self.__current_step)
365
+ step = self.__steps[step_name]
366
+ step_input = self.__last_result
367
+
368
+ started_at = utc_now()
369
+ started = monotonic()
370
+ await self._emit(
371
+ "on_step_start",
372
+ StepEvent(
373
+ workflow=workflow,
374
+ step=step_name,
375
+ task=None,
376
+ input=step_input,
377
+ started_at=started_at,
378
+ ),
379
+ )
289
380
 
290
- self.__logger.info(f"Step '{self.__current_step}' succeeded")
381
+ def _ended(**fields: Any) -> StepEvent:
382
+ return StepEvent(
383
+ workflow=workflow,
384
+ step=step_name,
385
+ task=None,
386
+ input=step_input,
387
+ started_at=started_at,
388
+ finished_at=utc_now(),
389
+ duration=monotonic() - started,
390
+ **fields,
391
+ )
291
392
 
292
- next_step = None
393
+ try:
394
+ if step["parallel"]:
395
+ results = await self._execute_parallel(
396
+ cast(Dict[str, Callable[[Any], Any]], step["func"]),
397
+ step["stop_on_failure"],
398
+ workflow=workflow,
399
+ step_name=step_name,
400
+ )
293
401
 
294
- if step["branch"]:
295
- if callable(step["branch"]):
296
- next_step = step["branch"](self.__last_result)
297
- else:
298
- next_step = step["branch"].get(self.__last_result)
402
+ self.__last_result = results
403
+ self.__context[step_name] = results
299
404
 
300
- if next_step is None:
301
- self.__logger.warning(
302
- f"Step '{self.__current_step}': branch did not resolve to a next step "
303
- f"for result '{self.__last_result}'. Workflow will end."
304
- )
305
- elif next_step not in self.__steps:
306
- self.__logger.warning(
307
- f"Step '{self.__current_step}': branch resolved to '{next_step}' "
308
- "which does not exist in steps. This will cause a failure."
309
- )
405
+ self.__logger.info(
406
+ f"Parallel step '{step_name}' succeeded with results: {results}"
407
+ )
408
+ else:
409
+ result = await self._execute_step(
410
+ cast(Callable[[Any], Any], step["func"]), self.__last_result
411
+ )
310
412
 
311
- self.__current_step = next_step or step["next_step"]
413
+ self.__last_result = result
414
+ self.__context[step_name] = result
312
415
 
313
- except Exception as exc:
314
- self.__logger.exception(
315
- f"Step '{self.__current_step}' failed. Exception: {exc}"
316
- )
416
+ self.__logger.info(f"Step '{step_name}' succeeded")
317
417
 
318
- exc_value = exc.args[0] if exc.args else exc
418
+ next_step = None
319
419
 
320
- self.__context[cast(str, self.__current_step)] = exc_value
420
+ if step["branch"]:
421
+ if callable(step["branch"]):
422
+ next_step = step["branch"](self.__last_result)
423
+ else:
424
+ next_step = step["branch"].get(self.__last_result)
321
425
 
322
- if step["on_failure"]:
323
- self.__logger.exception(
324
- f"Executing failure step: {step['on_failure']} for '{self.__current_step}'"
426
+ if next_step is None:
427
+ self.__logger.warning(
428
+ f"Step '{step_name}': branch did not resolve to a next step "
429
+ f"for result '{self.__last_result}'. Workflow will end."
430
+ )
431
+ elif next_step not in self.__steps:
432
+ self.__logger.warning(
433
+ f"Step '{step_name}': branch resolved to '{next_step}' "
434
+ "which does not exist in steps. This will cause a failure."
325
435
  )
326
436
 
327
- self.__current_step = step["on_failure"]
328
- self.__last_result = exc_value
437
+ self.__current_step = next_step or step["next_step"]
329
438
 
330
- self.__status = StepFunctionStatus.FAILED
439
+ except CancelledError as exc:
440
+ self.__logger.warning(f"Step '{step_name}' was cancelled")
331
441
 
332
- self.__logger.debug(
333
- f"StepFunction - {self.__name} - Status - {self.__status.value}"
334
- )
335
- else:
336
- self.__logger.exception(
337
- f"No failure step defined for '{self.__current_step}'. Raising Exception."
338
- )
442
+ self.__status = StepFunctionStatus.FAILED
339
443
 
340
- self.__status = StepFunctionStatus.FAILED
444
+ await self._emit("on_step_failure", _ended(error=exc))
341
445
 
342
- self.__logger.debug(
343
- f"StepFunction - {self.__name} - Status - {self.__status.value}"
344
- )
446
+ raise
447
+
448
+ except Exception as exc:
449
+ self.__logger.exception(f"Step '{step_name}' failed. Exception: {exc}")
450
+
451
+ exc_value = exc.args[0] if exc.args else exc
452
+
453
+ self.__context[step_name] = exc_value
454
+
455
+ self.__status = StepFunctionStatus.FAILED
345
456
 
346
- raise StepExecutionError(exc)
457
+ self.__logger.debug(
458
+ f"StepFunction - {self.__name} - Status - {self.__status.value}"
459
+ )
347
460
 
348
- if not self.__current_step and self.__status != StepFunctionStatus.FAILED:
349
- # If no more steps, mark as COMPLETED
461
+ await self._emit(
462
+ "on_step_failure", _ended(error=exc, next_step=step["on_failure"])
463
+ )
350
464
 
351
- self.__status = StepFunctionStatus.COMPLETED
465
+ if step["on_failure"]:
466
+ self.__logger.exception(
467
+ f"Executing failure step: {step['on_failure']} for '{step_name}'"
468
+ )
352
469
 
353
- self.__logger.debug(
354
- f"StepFunction - {self.__name} - Status - {self.__status.value}"
470
+ self.__current_step = step["on_failure"]
471
+ self.__last_result = exc_value
472
+ else:
473
+ self.__logger.exception(
474
+ f"No failure step defined for '{step_name}'. Raising Exception."
355
475
  )
356
476
 
477
+ raise StepExecutionError(exc)
478
+
479
+ else:
480
+ await self._emit(
481
+ "on_step_success",
482
+ _ended(output=self.__last_result, next_step=self.__current_step),
483
+ )
484
+
485
+ if not self.__current_step and self.__status != StepFunctionStatus.FAILED:
486
+ # If no more steps, mark as COMPLETED
487
+
488
+ self.__status = StepFunctionStatus.COMPLETED
489
+
490
+ self.__logger.debug(
491
+ f"StepFunction - {self.__name} - Status - {self.__status.value}"
492
+ )
493
+
357
494
  async def _execute_step(self, func: Callable, input_value: Any):
358
495
  """Execute a single step, handling async functions."""
359
496
  if iscoroutinefunction(func):
@@ -362,30 +499,72 @@ class StepFunction:
362
499
  return func(input_value)
363
500
 
364
501
  async def _execute_parallel(
365
- self, func_dict: Dict[str, Callable[[Any], Any]], stop_on_failure: bool = False
502
+ self,
503
+ func_dict: Dict[str, Callable[[Any], Any]],
504
+ stop_on_failure: bool = False,
505
+ workflow: Optional[str] = None,
506
+ step_name: Optional[str] = None,
366
507
  ):
367
- """Execute the steps in parallel without blocking the event loop."""
508
+ """Execute the steps in parallel without blocking the event loop.
509
+
510
+ Each task is reported to the hooks as its own step event, with
511
+ ``task`` set to its name.
512
+ """
368
513
  loop = get_running_loop()
369
514
  results = {}
370
515
  errors = []
516
+ step_input = self.__last_result
517
+
518
+ async def _run_one(task: str, func: Callable[[Any], Any]) -> Any:
519
+ started_at = utc_now()
520
+ started = monotonic()
521
+ await self._emit(
522
+ "on_step_start",
523
+ StepEvent(
524
+ workflow=cast(str, workflow),
525
+ step=cast(str, step_name),
526
+ task=task,
527
+ input=step_input,
528
+ started_at=started_at,
529
+ ),
530
+ )
371
531
 
372
- async def _run_one(func: Callable[[Any], Any]) -> Any:
373
- if iscoroutinefunction(func):
374
- return await func(self.__last_result)
375
- return await loop.run_in_executor(None, func, self.__last_result)
532
+ def _ended(**fields: Any) -> StepEvent:
533
+ return StepEvent(
534
+ workflow=cast(str, workflow),
535
+ step=cast(str, step_name),
536
+ task=task,
537
+ input=step_input,
538
+ started_at=started_at,
539
+ finished_at=utc_now(),
540
+ duration=monotonic() - started,
541
+ **fields,
542
+ )
543
+
544
+ try:
545
+ if iscoroutinefunction(func):
546
+ result = await func(step_input)
547
+ else:
548
+ result = await loop.run_in_executor(None, func, step_input)
549
+ except BaseException as exc:
550
+ await self._emit("on_step_failure", _ended(error=exc))
551
+ raise
552
+
553
+ await self._emit("on_step_success", _ended(output=result))
554
+ return result
376
555
 
377
556
  task_results = await gather(
378
- *[_run_one(func) for func in func_dict.values()],
557
+ *[_run_one(task, func) for task, func in func_dict.items()],
379
558
  return_exceptions=True,
380
559
  )
381
560
 
382
- for step_name, result in zip(func_dict.keys(), task_results):
561
+ for task, result in zip(func_dict.keys(), task_results):
383
562
  if isinstance(result, Exception):
384
- self.__logger.exception(f"Parallel task '{step_name}' failed: {result}")
385
- results[step_name] = result.args[0] if result.args else result
386
- errors.append((step_name, result))
563
+ self.__logger.exception(f"Parallel task '{task}' failed: {result}")
564
+ results[task] = result.args[0] if result.args else result
565
+ errors.append((task, result))
387
566
  else:
388
- results[step_name] = result
567
+ results[task] = result
389
568
 
390
569
  if errors:
391
570
  self.__logger.error(f"Some parallel tasks failed: {errors}")
@@ -396,6 +575,25 @@ class StepFunction:
396
575
 
397
576
  return results
398
577
 
578
+ async def _emit(self, hook_name: str, event: Union[StepEvent, WorkflowEvent]):
579
+ """Call a hook for this run, awaiting it if it's a coroutine.
580
+
581
+ A hook that raises is logged and ignored: hooks observe the workflow,
582
+ they never change its routing or status.
583
+ """
584
+ hooks = self.__run_hooks
585
+ if hooks is None:
586
+ return
587
+
588
+ try:
589
+ result = getattr(hooks, hook_name)(event)
590
+ if isawaitable(result):
591
+ await result
592
+ except Exception:
593
+ self.__logger.exception(
594
+ f"StepFunction - {self.__name} - Hook '{hook_name}' raised; ignoring"
595
+ )
596
+
399
597
  def visualize(self):
400
598
  """Visualize the workflow."""
401
599
  from stepfunction.core.visualizer import Visualizer
@@ -430,11 +628,80 @@ class StepFunction:
430
628
 
431
629
  return visualizer.render_step_function_to_string()
432
630
 
631
+ def to_dict(self, step_registry: Optional["StepRegistry"] = None) -> Dict[str, Any]:
632
+ """Export this workflow as a JSON-able dict.
633
+
634
+ Function references (step funcs, parallel-step funcs, and branch
635
+ callables) are encoded as their registered string name — see
636
+ ``stepfunction.registry.step_registry``. Nested sub-step-functions are
637
+ encoded recursively.
638
+
639
+ Call this before execute(): if the workflow has already run,
640
+ "start_step" reflects wherever the execution cursor ended up
641
+ (often None after a completed run), not the original start step.
642
+
643
+ Raises:
644
+ UnserializableStepError: If any step was built from a BaseStep
645
+ instance (RetryStep, TimeoutStep, WaitStep, or a custom
646
+ BaseStep subclass) — not yet supported.
647
+ UnregisteredFunctionError: If a step or branch function used in
648
+ this workflow has no registered name in ``step_registry``.
649
+ """
650
+ from stepfunction.core.serializer import encode_step_function
651
+
652
+ return encode_step_function(self, step_registry)
653
+
654
+ def to_json(
655
+ self, indent: Optional[int] = 2, step_registry: Optional["StepRegistry"] = None
656
+ ) -> str:
657
+ """Export this workflow as a JSON string. See to_dict() for details
658
+ and caveats."""
659
+ from json import dumps
660
+
661
+ return dumps(self.to_dict(step_registry), indent=indent)
662
+
663
+ @classmethod
664
+ def from_dict(
665
+ cls, data: Dict[str, Any], step_registry: Optional["StepRegistry"] = None
666
+ ) -> "StepFunction":
667
+ """Reconstruct a StepFunction from a dict produced by to_dict().
668
+
669
+ Function name references in ``data`` are resolved against
670
+ ``step_registry`` (defaults to the package's default registry,
671
+ ``stepfunction.registry.step_registry.registry``, if not given). Validates the
672
+ reconstructed workflow before returning, so a malformed spec fails
673
+ fast with a readable breadcrumb error rather than only failing
674
+ later at execute() time.
675
+
676
+ Raises:
677
+ UnregisteredFunctionError: If a referenced function name isn't
678
+ registered.
679
+ ValueError: If the reconstructed workflow fails validate().
680
+ """
681
+ from stepfunction.core.serializer import decode_step_function
682
+
683
+ return decode_step_function(data, step_registry)
684
+
685
+ @classmethod
686
+ def from_json(
687
+ cls, json_str: str, step_registry: Optional["StepRegistry"] = None
688
+ ) -> "StepFunction":
689
+ """Reconstruct a StepFunction from a JSON string produced by to_json().
690
+ See from_dict() for details."""
691
+ from json import loads
692
+
693
+ return cls.from_dict(loads(json_str), step_registry)
694
+
433
695
  @property
434
696
  def name(self):
435
697
  """Returns the name of the step function."""
436
698
  return self.__name
437
699
 
700
+ @property
701
+ def hooks(self) -> Optional[StepFunctionHooks]:
702
+ """Returns the lifecycle hooks set on the step function, if any."""
703
+ return self.__hooks
704
+
438
705
  @property
439
706
  def steps(self):
440
707
  """Returns the steps of the step function."""
@@ -455,8 +722,22 @@ class StepFunction:
455
722
  """Returns the status of the step function."""
456
723
  return self.__status
457
724
 
725
+ @property
726
+ def current_step(self):
727
+ """Returns the step set by set_start_step(), advanced by execute()
728
+ as the workflow progresses."""
729
+ return self.__current_step
730
+
458
731
  def __str__(self):
459
732
  return f"StepFunction(name={self.__name}, NoOfSteps={len(self.__steps)}, CurrentStep={self.__current_step}, Status={self.__status})"
460
733
 
461
734
  def __repr__(self):
462
735
  return str(self)
736
+
737
+
738
+ def _sub_path_label(step_name: str, sub_step_function: StepFunction) -> str:
739
+ """How a sub-step function appears in a workflow path: its step name, plus
740
+ its own name when the two differ (e.g. "SubStep (SUB_FLOW)")."""
741
+ if step_name != sub_step_function.name:
742
+ return f"{step_name} ({sub_step_function.name})"
743
+ return step_name
@@ -36,11 +36,10 @@ from stepfunction.constants.visualizer import (
36
36
  DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS,
37
37
  DEFAULT_VISUALIZER_SUB_STEP_FUNCTION_CLASS_STYLE,
38
38
  DEFAULT_VISUALIZER_SUCCESS_EDGE_LABEL,
39
+ VISUALIZER_INVALID_NODE_ID_CHARS,
39
40
  )
40
41
  from stepfunction.types.step_types import StepParams
41
42
 
42
- _INVALID_NODE_ID_CHARS = r"[^0-9A-Za-z_]"
43
-
44
43
 
45
44
  def _quote(label: str) -> str:
46
45
  """Escape and quote a string for use as a Mermaid edge label."""
@@ -261,7 +260,7 @@ class Visualizer:
261
260
  if step_name in self.__node_ids:
262
261
  return self.__node_ids[step_name]
263
262
 
264
- sanitized = re_sub(_INVALID_NODE_ID_CHARS, "_", step_name) or "step"
263
+ sanitized = re_sub(VISUALIZER_INVALID_NODE_ID_CHARS, "_", step_name) or "step"
265
264
  if sanitized[0].isdigit():
266
265
  sanitized = f"_{sanitized}"
267
266
 
@@ -12,3 +12,39 @@ class ParallelStepExecutionError(Exception):
12
12
  def __init__(self, exc: list[tuple[str, Exception]]):
13
13
  self.message = f"Parallel step generated an exception: {exc}"
14
14
  super().__init__(self.message)
15
+
16
+
17
+ class DuplicateRegistrationError(Exception):
18
+ """Error raised when a function is registered under a name that is already taken."""
19
+
20
+ def __init__(self, name: str):
21
+ self.message = f"A function is already registered under the name '{name}'."
22
+ super().__init__(self.message)
23
+
24
+
25
+ class UnregisteredFunctionError(Exception):
26
+ """Error raised when a step spec references a function that has no
27
+ registered name, in either direction: decoding a name that was never
28
+ registered, or encoding a function that was never given a name."""
29
+
30
+ def __init__(self, message: str):
31
+ self.message = message
32
+ super().__init__(self.message)
33
+
34
+
35
+ class UnserializableStepError(Exception):
36
+ """Error raised when a step cannot be represented in a declarative spec.
37
+
38
+ Currently applies to any step built from a BaseStep instance (e.g.
39
+ RetryStep, TimeoutStep, WaitStep) — only the built closure survives
40
+ add_step(), not the original instance/config needed to reconstruct it.
41
+ """
42
+
43
+ def __init__(self, step_name: str, step_type: object):
44
+ self.message = (
45
+ f"Step '{step_name}' cannot be serialized: it was built from a "
46
+ f"BaseStep instance (step_type={step_type}). Serializing "
47
+ "BaseStep-derived steps (RetryStep, TimeoutStep, WaitStep, or "
48
+ "custom BaseStep subclasses) is not yet supported."
49
+ )
50
+ super().__init__(self.message)
@@ -0,0 +1,4 @@
1
+ from .events import StepEvent, WorkflowEvent
2
+ from .step_function_hooks import StepFunctionHooks
3
+
4
+ __all__ = ["StepEvent", "StepFunctionHooks", "WorkflowEvent"]
@@ -0,0 +1,73 @@
1
+ """Events passed to lifecycle hooks.
2
+
3
+ Author: Vineeth Penugonda
4
+ """
5
+
6
+ from dataclasses import dataclass
7
+ from datetime import datetime
8
+ from typing import Any, Optional
9
+
10
+ from stepfunction.constants.enums import StepFunctionStatus
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class StepEvent:
15
+ """Describes one step (or one task of a parallel step) as it starts or ends.
16
+
17
+ Attributes:
18
+ workflow (str): Path of the step function running the step, from the
19
+ outermost workflow down, e.g. "FLOW -> SubStep (SUB_FLOW)".
20
+ step (str): Name of the step.
21
+ task (Optional[str]): Name of the task within a parallel step; None for
22
+ ordinary steps and for the parallel step itself.
23
+ input (Any): The value the step was called with.
24
+ started_at (datetime): When the step started (UTC).
25
+ output (Any): The step's result. Set on success only.
26
+ error (Optional[BaseException]): The exception the step raised. Set on
27
+ failure only — the original exception, not the value stored in context.
28
+ next_step (Optional[str]): Where the workflow goes next: the branch or
29
+ next_step target on success, the on_failure target on failure, or
30
+ None if the workflow ends (or raises) here.
31
+ finished_at (Optional[datetime]): When the step ended (UTC). None on start.
32
+ duration (Optional[float]): Seconds the step took, from a monotonic
33
+ clock. None on start.
34
+ """
35
+
36
+ workflow: str
37
+ step: str
38
+ task: Optional[str]
39
+ input: Any
40
+ started_at: datetime
41
+ output: Any = None
42
+ error: Optional[BaseException] = None
43
+ next_step: Optional[str] = None
44
+ finished_at: Optional[datetime] = None
45
+ duration: Optional[float] = None
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class WorkflowEvent:
50
+ """Describes a step function run as it starts or ends.
51
+
52
+ Attributes:
53
+ workflow (str): Path of the step function, from the outermost workflow down.
54
+ status (StepFunctionStatus): RUNNING on start; COMPLETED or FAILED on end.
55
+ input (Any): The initial input the run was called with.
56
+ started_at (datetime): When the run started (UTC).
57
+ output (Any): The last step's result. Set on end only.
58
+ error (Optional[BaseException]): The exception execute() raised (e.g. a
59
+ StepExecutionError wrapping the step's error — the step's own failure
60
+ event has the original). None when the run completed, or failed into
61
+ an on_failure step.
62
+ finished_at (Optional[datetime]): When the run ended (UTC). None on start.
63
+ duration (Optional[float]): Seconds the run took. None on start.
64
+ """
65
+
66
+ workflow: str
67
+ status: StepFunctionStatus
68
+ input: Any
69
+ started_at: datetime
70
+ output: Any = None
71
+ error: Optional[BaseException] = None
72
+ finished_at: Optional[datetime] = None
73
+ duration: Optional[float] = None
@@ -0,0 +1,51 @@
1
+ """Base class for observing a step function's lifecycle.
2
+
3
+ Author: Vineeth Penugonda
4
+ """
5
+
6
+ from stepfunction.hooks.events import StepEvent, WorkflowEvent
7
+
8
+
9
+ class StepFunctionHooks:
10
+ """Receives events as a step function runs.
11
+
12
+ Subclass it and override the methods you need; the rest do nothing. Each
13
+ method may be a regular function or a coroutine function. Pass an instance
14
+ to ``StepFunction(name, hooks=...)`` or ``set_hooks()``.
15
+
16
+ Hooks observe; they don't steer. An exception raised by a hook is logged
17
+ and ignored, so it never changes the workflow's routing or status. A
18
+ sub-step function without hooks of its own uses its parent's.
19
+
20
+ Order of events for one run:
21
+ on_workflow_start
22
+ for each step: on_step_start, then on_step_success or on_step_failure
23
+ (a parallel step also reports each task between its own start and end,
24
+ with ``event.task`` set)
25
+ on_workflow_end
26
+
27
+ If the run is cancelled, the running step and the workflow end with
28
+ ``error`` set to the ``asyncio.CancelledError``, which is then re-raised.
29
+
30
+ Example:
31
+ class PrintHooks(StepFunctionHooks):
32
+ async def on_step_success(self, event: StepEvent):
33
+ print(f"{event.step} took {event.duration:.3f}s -> {event.next_step}")
34
+
35
+ sf = StepFunction("MyStepFunction", hooks=PrintHooks())
36
+ """
37
+
38
+ def on_workflow_start(self, event: WorkflowEvent):
39
+ """Called once the workflow is validated, before its first step."""
40
+
41
+ def on_workflow_end(self, event: WorkflowEvent):
42
+ """Called when the workflow completes, fails, raises or is cancelled."""
43
+
44
+ def on_step_start(self, event: StepEvent):
45
+ """Called before a step (or a parallel task) runs."""
46
+
47
+ def on_step_success(self, event: StepEvent):
48
+ """Called after a step (or a parallel task) returns and its next step is resolved."""
49
+
50
+ def on_step_failure(self, event: StepEvent):
51
+ """Called after a step (or a parallel task) raises."""
File without changes
@@ -0,0 +1,97 @@
1
+ """Function registry for declarative (JSON) workflow loading.
2
+
3
+ Author: Vineeth Penugonda
4
+ """
5
+
6
+ from typing import Any, Callable, Dict, Optional
7
+
8
+ from stepfunction.exceptions.step_errors import (
9
+ DuplicateRegistrationError,
10
+ UnregisteredFunctionError,
11
+ )
12
+
13
+
14
+ class StepRegistry:
15
+ """A name -> callable registry used to resolve string function references
16
+ in a declarative (JSON) workflow spec back into real Python callables.
17
+
18
+ Step functions and branch-router functions share one namespace, since
19
+ both are simply ``Callable[[Any], Any]`` — the calling context
20
+ (``add_step`` vs. ``branch=``), not the function itself, determines the
21
+ role a registered callable plays in a given workflow.
22
+
23
+ A module-level default instance (``registry``) covers the common case of
24
+ a single process-wide registry. Construct your own ``StepRegistry()`` for
25
+ isolated namespaces (e.g. per-tenant, or test isolation).
26
+ """
27
+
28
+ def __init__(self):
29
+ self.__functions: Dict[str, Callable[[Any], Any]] = {}
30
+
31
+ def register(
32
+ self, name: str, func: Optional[Callable[[Any], Any]] = None
33
+ ) -> Callable[[Any], Any]:
34
+ """Register ``func`` under ``name``.
35
+
36
+ Usable as a direct call (``registry.register("my_step", my_step)``)
37
+ or as a decorator (``@registry.register("my_step")``).
38
+
39
+ Raises:
40
+ DuplicateRegistrationError: If ``name`` is already registered.
41
+ """
42
+ if func is not None:
43
+ self.__add(name, func)
44
+ return func
45
+
46
+ def decorator(fn: Callable[[Any], Any]) -> Callable[[Any], Any]:
47
+ self.__add(name, fn)
48
+ return fn
49
+
50
+ return decorator
51
+
52
+ def __add(self, name: str, func: Callable[[Any], Any]) -> None:
53
+ if name in self.__functions:
54
+ raise DuplicateRegistrationError(name)
55
+ self.__functions[name] = func
56
+
57
+ def unregister(self, name: str) -> None:
58
+ """Remove a registration if present. No error if ``name`` isn't registered."""
59
+ self.__functions.pop(name, None)
60
+
61
+ def get(self, name: str) -> Callable[[Any], Any]:
62
+ """Look up a registered callable by name.
63
+
64
+ Raises:
65
+ UnregisteredFunctionError: If ``name`` has not been registered.
66
+ """
67
+ try:
68
+ return self.__functions[name]
69
+ except KeyError:
70
+ raise UnregisteredFunctionError(
71
+ f"No function is registered under the name '{name}'. Register it "
72
+ "with stepfunction.registry.step_registry.register_step() (or your "
73
+ "own StepRegistry instance) before loading this workflow spec."
74
+ ) from None
75
+
76
+ def name_for(self, func: Callable[[Any], Any]) -> Optional[str]:
77
+ """Reverse lookup: the registered name for ``func``, or ``None`` if it
78
+ isn't registered under this registry."""
79
+ for registered_name, registered_func in self.__functions.items():
80
+ if registered_func is func:
81
+ return registered_name
82
+ return None
83
+
84
+ def __contains__(self, name: str) -> bool:
85
+ return name in self.__functions
86
+
87
+ def clear(self) -> None:
88
+ """Remove all registrations. Primarily useful for test isolation."""
89
+ self.__functions.clear()
90
+
91
+
92
+ # Module-level default singleton, covering the common case of one
93
+ # process-wide registry.
94
+ registry = StepRegistry()
95
+
96
+ register_step = registry.register
97
+ get_step = registry.get
@@ -1,5 +1,6 @@
1
1
  """This module contains utility functions for the stepfunction package."""
2
2
 
3
+ from datetime import datetime, timezone
3
4
  from os import getenv
4
5
  from typing import Optional
5
6
 
@@ -16,3 +17,8 @@ def get_environment_variable(name: str, default: Optional[str] = None) -> Option
16
17
  str: The value of the environment variable.
17
18
  """
18
19
  return getenv(name, default)
20
+
21
+
22
+ def utc_now() -> datetime:
23
+ """Returns the current time as a timezone-aware UTC datetime."""
24
+ return datetime.now(timezone.utc)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stepfunction
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Step Function Workflow Orchestration Library
5
5
  Author: Vineeth Penugonda
6
6
  License-Expression: MIT
@@ -33,6 +33,39 @@ Dynamic: license-file
33
33
  - **Branching Logic**: Direct workflows based on conditions.
34
34
  - **Sub-Step Functions**: Modularize workflows by embedding sub-step functions.
35
35
  - **Visualization**: Integrated support for visualizing the workflow graph as a Mermaid flowchart.
36
+ - **Lifecycle Hooks**: Get notified as the workflow and each step start, succeed or fail — with inputs, outputs, errors, routing and timings — to record run history, metrics or traces.
37
+
38
+ ## Lifecycle Hooks
39
+
40
+ Subclass `StepFunctionHooks`, override the events you need (sync or async), and pass an instance to the step function:
41
+
42
+ ```python
43
+ from stepfunction.core.step_function import StepFunction
44
+ from stepfunction.hooks import StepEvent, StepFunctionHooks
45
+
46
+
47
+ class RunRecorder(StepFunctionHooks):
48
+ async def on_step_start(self, event: StepEvent):
49
+ print(f"{event.workflow}: {event.step} started with {event.input!r}")
50
+
51
+ async def on_step_success(self, event: StepEvent):
52
+ print(f"{event.step} -> {event.output!r} in {event.duration:.3f}s, next: {event.next_step}")
53
+
54
+ async def on_step_failure(self, event: StepEvent):
55
+ print(f"{event.step} failed: {event.error!r}, next: {event.next_step}")
56
+
57
+
58
+ sf = StepFunction("MyStepFunction", hooks=RunRecorder())
59
+ ```
60
+
61
+ Events: `on_workflow_start`, `on_step_start`, `on_step_success`, `on_step_failure`, `on_workflow_end`.
62
+
63
+ - `StepEvent` carries the workflow path, step name, parallel `task` name (if any), `input`, `output`, the original `error`, the resolved `next_step`, UTC `started_at` / `finished_at` and `duration` in seconds.
64
+ - `WorkflowEvent` carries the workflow path, `status`, initial `input`, final `output`, the `error` that ended the run (if `execute()` raised) and timings.
65
+ - Each task of a parallel step is reported as its own step event, with `task` set.
66
+ - A sub-step function without hooks of its own reports to its parent's hooks; its events carry the nested path, e.g. `"FLOW -> SubStep (SUB_FLOW)"`.
67
+ - Hooks observe; they don't steer. An exception raised by a hook is logged and ignored.
68
+ - If the run is cancelled, the running step and the workflow are reported with the `CancelledError`, which is then re-raised.
36
69
 
37
70
  ## Installation
38
71
 
@@ -1,12 +1,20 @@
1
+ stepfunction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1
2
  stepfunction/constants/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
3
  stepfunction/constants/enums.py,sha256=K4_xKGa0NApw6Ai4VkfGW1Sc5fEqwFPpuyezy6YexzY,240
3
- stepfunction/constants/visualizer.py,sha256=impswU0JpqgpzTu4_a26FVLK4tII4ptbUaopFb2ZObM,1815
4
+ stepfunction/constants/visualizer.py,sha256=MHi7MafiXGr6wb2ug7-czRG-CiM-xO5wniN6EFZBRzA,1994
5
+ stepfunction/core/serializer/__init__.py,sha256=PvxFmVDK8EnFBqZXgYBELO1uobT5vkFFntRzlGx7cJo,127
6
+ stepfunction/core/serializer/serializer.py,sha256=k5Uu881Amoy0Kv2mPwiv7kcNOnydtfOEQeBRu3aloj4,6006
4
7
  stepfunction/core/step_function/__init__.py,sha256=4L5ufP13A7hXBDBYrD45pfEsV9bsa6zbSkSnHpi-tyM,68
5
- stepfunction/core/step_function/step_function.py,sha256=qPfHSEP1LFLR6_hRVxxD6duALaDcTkIRWO_HrdYAg7s,17666
8
+ stepfunction/core/step_function/step_function.py,sha256=whoKdcjayIHnu8wSEnB06ufhhXLnQNrmvb_7I2qXcT4,27809
6
9
  stepfunction/core/visualizer/__init__.py,sha256=pgNI6LPnY9Pm9e_YhMn-5CVmUzp8mvoytbDZ_b3McSw,61
7
- stepfunction/core/visualizer/visualizer.py,sha256=XM-kSoIPha-rYM2T-ZYYoo6-ttcnodn-832MX9Gg6d0,12892
10
+ stepfunction/core/visualizer/visualizer.py,sha256=hhgZt5y98KiKLFtxhc_W7U0NwQ48F6WmAmN_N2_0GKc,12897
8
11
  stepfunction/exceptions/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- stepfunction/exceptions/step_errors.py,sha256=yNJt6tiO8LkiSxeHoSZBpaKA7qkqdsXPnOd3AMt3C8A,486
12
+ stepfunction/exceptions/step_errors.py,sha256=jN71QXMtuekNy9YDMrwkcOxY9RR9Wssbk-zKeGVW0I4,1923
13
+ stepfunction/hooks/__init__.py,sha256=omNFwha6-07ggGZHnDTZCbZNEM26qvffIxtRvdGp8RU,159
14
+ stepfunction/hooks/events.py,sha256=gIqaEJqQRro7VPyqfDEgdLQNK627iX3E6fOYz2Cf9lg,2886
15
+ stepfunction/hooks/step_function_hooks.py,sha256=JK6wLb_uns3DoECt6tQJGCiSNMhbgZ71HEcNADq4pig,2011
16
+ stepfunction/registry/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ stepfunction/registry/step_registry.py,sha256=znpqXkgJQnOwCg7K3h6BmHdZrrDIFhsnyOYzdW8iVQE,3430
10
18
  stepfunction/steps/__init__.py,sha256=DvzYMJiMY-5O2tWhCa82LSbqIa80VXfUuLgFYyAOdi8,256
11
19
  stepfunction/steps/retry_step.py,sha256=Y5cIWi4MqVSeXUVxHKXW0TkYPBl7bOcGGL1cxH_26wY,2855
12
20
  stepfunction/steps/timeout_step.py,sha256=0_wwAAZcwC61fKSkT4TWRef-mUOT3A2T_-Y1pTXuhd8,2379
@@ -20,9 +28,9 @@ stepfunction/types/step_types.py,sha256=pMA8xY_a3yNmpDul8BBqa-0_Fpp3QNIMFpxAgn_t
20
28
  stepfunction/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
29
  stepfunction/utils/constants.py,sha256=VZefE-OhNXOyg28wPu62VZPvReublyqWS515-TbYWo0,391
22
30
  stepfunction/utils/logger.py,sha256=0L8F8oO5HiYNqcuV8G8UD4cTdCpmObQYrkvwvrF1S-8,1789
23
- stepfunction/utils/utils.py,sha256=Zw9-sKE2LOghxyiKImqGWSv4nHZ9nI8yEptp1xaYfHk,563
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,,
31
+ stepfunction/utils/utils.py,sha256=CrJuiWQrVs3re569wlJFODkmZ2fZcQaUS7Vxm56188c,739
32
+ stepfunction-0.2.0.dist-info/licenses/LICENSE,sha256=sLQZUABreUtT56S4w592eAmWiFONtLKZG-KZWf-_dvg,1074
33
+ stepfunction-0.2.0.dist-info/METADATA,sha256=ojVNltqxMd6afHuj6RpORW05ldaJ-AB6SI_fO59Kc_8,4737
34
+ stepfunction-0.2.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
35
+ stepfunction-0.2.0.dist-info/top_level.txt,sha256=XqLJSUeYSmzTNMiPj-IESxp1om-LdZjefT4Un2orQD4,13
36
+ stepfunction-0.2.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (82.0.1)
2
+ Generator: setuptools (84.0.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5