stepfunction 0.0.5__tar.gz → 0.0.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. {stepfunction-0.0.5/src/StepFunction.egg-info → stepfunction-0.0.7}/PKG-INFO +1 -1
  2. {stepfunction-0.0.5 → stepfunction-0.0.7}/pyproject.toml +1 -1
  3. {stepfunction-0.0.5 → stepfunction-0.0.7/src/StepFunction.egg-info}/PKG-INFO +1 -1
  4. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/core/step_function/step_function.py +85 -49
  5. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/core/visualizer/visualizer.py +4 -6
  6. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/exceptions/step_errors.py +1 -1
  7. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/steps/retry_step.py +12 -2
  8. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/steps/timeout_step.py +4 -0
  9. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/steps/wait_step.py +4 -0
  10. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/types/step_types.py +2 -2
  11. {stepfunction-0.0.5 → stepfunction-0.0.7}/LICENSE +0 -0
  12. {stepfunction-0.0.5 → stepfunction-0.0.7}/README.md +0 -0
  13. {stepfunction-0.0.5 → stepfunction-0.0.7}/setup.cfg +0 -0
  14. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/StepFunction.egg-info/SOURCES.txt +0 -0
  15. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/StepFunction.egg-info/dependency_links.txt +0 -0
  16. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/StepFunction.egg-info/requires.txt +0 -0
  17. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/StepFunction.egg-info/top_level.txt +0 -0
  18. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/constants/__init__.py +0 -0
  19. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/constants/enums.py +0 -0
  20. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/constants/visualizer.py +0 -0
  21. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/core/step_function/__init__.py +0 -0
  22. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/core/visualizer/__init__.py +0 -0
  23. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/exceptions/__init__.py +0 -0
  24. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/steps/__init__.py +0 -0
  25. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/steps/base/__init__.py +0 -0
  26. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/steps/base/base_step.py +0 -0
  27. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/steps/exceptions/__init__.py +0 -0
  28. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/steps/exceptions/step_exceptions.py +0 -0
  29. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/types/__init__.py +0 -0
  30. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/types/visualizer_types.py +0 -0
  31. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/utils/__init__.py +0 -0
  32. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/utils/constants.py +0 -0
  33. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/utils/logger.py +0 -0
  34. {stepfunction-0.0.5 → stepfunction-0.0.7}/src/stepfunction/utils/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stepfunction
3
- Version: 0.0.5
3
+ Version: 0.0.7
4
4
  Summary: Step Function Workflow Orchestration Library
5
5
  Author: Vineeth Penugonda
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "stepfunction"
3
- version = "0.0.5"
3
+ version = "0.0.7"
4
4
  authors = [{ name = "Vineeth Penugonda" }]
5
5
  description = "Step Function Workflow Orchestration Library"
6
6
  readme = "README.md"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: stepfunction
3
- Version: 0.0.5
3
+ Version: 0.0.7
4
4
  Summary: Step Function Workflow Orchestration Library
5
5
  Author: Vineeth Penugonda
6
6
  License-Expression: MIT
@@ -3,10 +3,9 @@
3
3
  Author: Vineeth Penugonda
4
4
  """
5
5
 
6
- from asyncio import run as asyncio_run
7
- from concurrent.futures import ThreadPoolExecutor, as_completed
6
+ from asyncio import gather, get_running_loop
8
7
  from inspect import iscoroutinefunction
9
- from typing import Any, Callable, Dict, Optional, Union
8
+ from typing import Any, Callable, Dict, Optional, Union, cast
10
9
 
11
10
  from stepfunction.constants.enums import StepFunctionStatus
12
11
  from stepfunction.exceptions.step_errors import (
@@ -34,7 +33,7 @@ class StepFunction:
34
33
 
35
34
  Properties:
36
35
  name (str): The name of the step function.
37
- steps (StepParams): A dictionary containing the steps of the workflow.
36
+ steps (Dict[str, StepParams]): A dictionary containing the steps of the workflow.
38
37
  last_result (Any): The result of the last step.
39
38
  context (Dict[str, Any]): Stores step names and results, including exceptions if any occur.
40
39
  status (StepFunctionStatus): The current status of the workflow. Possible values are:
@@ -53,6 +52,10 @@ class StepFunction:
53
52
  add_sub_step_function(name, sub_step_function, next_step=None, on_failure=None):
54
53
  Add a sub-step function to be executed as a step.
55
54
 
55
+ validate():
56
+ Validate the workflow configuration. Raises ValueError if the start step is not set
57
+ or if any next_step or on_failure reference an unknown step.
58
+
56
59
  execute(initial_input=None):
57
60
  Execute the workflow starting from the specified start step. This will run each step in sequence or in parallel,
58
61
  depending on the step configuration.
@@ -91,11 +94,11 @@ class StepFunction:
91
94
  await step_function.execute()
92
95
 
93
96
  Parallel Example:
94
- step_function.add_step("Step1", func1, parallel=True)
97
+ step_function.add_step("Step1", func1, next_step="ParallelStep")
95
98
  step_function.add_step("ParallelStep", {
96
99
  "task1": func2,
97
100
  "task2": func3
98
- }, next_step="Step2", parallel=True)
101
+ }, parallel=True)
99
102
 
100
103
  Sub-step function example:
101
104
  sub_step_function = StepFunction("SubStepFunction")
@@ -110,11 +113,13 @@ class StepFunction:
110
113
  def __init__(self, name: str):
111
114
  self.__name = name # Name of the step function
112
115
 
113
- self.__steps: StepParams = {} # Steps of the workflow
116
+ self.__steps: Dict[str, StepParams] = {} # Steps of the workflow
114
117
  self.__current_step = None # The current step being executed
115
118
 
116
119
  self.__last_result = None # To hold the result of the last step
117
- self.__context = {} # To hold the step names and results of those steps
120
+ self.__context: Dict[
121
+ str, Any
122
+ ] = {} # To hold the step names and results of those steps
118
123
 
119
124
  # Status of the step function
120
125
  self.__status: StepFunctionStatus = StepFunctionStatus.INITIALIZED
@@ -131,7 +136,7 @@ class StepFunction:
131
136
  func: Union[Callable[[Any], Any], Dict[str, Callable[[Any], Any]], BaseStep],
132
137
  next_step: Optional[str] = None,
133
138
  on_failure: Optional[str] = None,
134
- branch: Optional[Dict[Any, str]] = None,
139
+ branch: Optional[Union[Dict[Any, str], Callable[[Any], str]]] = None,
135
140
  parallel: bool = False,
136
141
  stop_on_failure: bool = False,
137
142
  ):
@@ -140,6 +145,12 @@ class StepFunction:
140
145
  if name in self.__steps:
141
146
  raise ValueError(f"Step '{name}' already exists in steps")
142
147
 
148
+ if branch is not None and next_step is not None:
149
+ raise ValueError(
150
+ f"Step '{name}' cannot have both 'branch' and 'next_step' set. "
151
+ "Use 'branch' to control routing or 'next_step' for a fixed transition, not both."
152
+ )
153
+
143
154
  step_type = None
144
155
  if isinstance(func, BaseStep):
145
156
  step_type = func.step_type
@@ -152,6 +163,7 @@ class StepFunction:
152
163
  "branch": branch,
153
164
  "parallel": parallel,
154
165
  "stop_on_failure": stop_on_failure,
166
+ "is_sub_step_function": False,
155
167
  "step_type": step_type,
156
168
  }
157
169
 
@@ -184,6 +196,7 @@ class StepFunction:
184
196
  "parallel": False,
185
197
  "stop_on_failure": False,
186
198
  "is_sub_step_function": True,
199
+ "step_type": None,
187
200
  }
188
201
 
189
202
  def set_start_step(self, name: str):
@@ -193,9 +206,32 @@ class StepFunction:
193
206
 
194
207
  self.__current_step = name
195
208
 
209
+ def validate(self):
210
+ """Validate the workflow configuration before execution."""
211
+
212
+ if self.__current_step is None:
213
+ raise ValueError(
214
+ "No start step set. Call set_start_step() before executing the workflow."
215
+ )
216
+
217
+ for step_name, step in self.__steps.items():
218
+ if step["next_step"] is not None and step["next_step"] not in self.__steps:
219
+ raise ValueError(
220
+ f"Step '{step_name}' has unknown next_step '{step['next_step']}'."
221
+ )
222
+ if (
223
+ step["on_failure"] is not None
224
+ and step["on_failure"] not in self.__steps
225
+ ):
226
+ raise ValueError(
227
+ f"Step '{step_name}' has unknown on_failure '{step['on_failure']}'."
228
+ )
229
+
196
230
  async def execute(self, initial_input: Any = None):
197
231
  """Execute the workflow."""
198
232
 
233
+ self.validate()
234
+
199
235
  self.__status = StepFunctionStatus.RUNNING
200
236
 
201
237
  self.__logger.debug(
@@ -208,8 +244,9 @@ class StepFunction:
208
244
  step = self.__steps[self.__current_step]
209
245
  try:
210
246
  if step["parallel"]:
211
- results = self._execute_parallel(
212
- step["func"], step["stop_on_failure"]
247
+ results = await self._execute_parallel(
248
+ cast(Dict[str, Callable[[Any], Any]], step["func"]),
249
+ step["stop_on_failure"],
213
250
  )
214
251
 
215
252
  self.__last_result = results
@@ -219,7 +256,9 @@ class StepFunction:
219
256
  f"Parallel step '{self.__current_step}' succeeded with results: {results}"
220
257
  )
221
258
  else:
222
- result = await self._execute_step(step["func"], self.__last_result)
259
+ result = await self._execute_step(
260
+ cast(Callable[[Any], Any], step["func"]), self.__last_result
261
+ )
223
262
 
224
263
  self.__last_result = result
225
264
  self.__context[self.__current_step] = result
@@ -234,6 +273,17 @@ class StepFunction:
234
273
  else:
235
274
  next_step = step["branch"].get(self.__last_result)
236
275
 
276
+ if next_step is None:
277
+ self.__logger.warning(
278
+ f"Step '{self.__current_step}': branch did not resolve to a next step "
279
+ f"for result '{self.__last_result}'. Workflow will end."
280
+ )
281
+ elif next_step not in self.__steps:
282
+ self.__logger.warning(
283
+ f"Step '{self.__current_step}': branch resolved to '{next_step}' "
284
+ "which does not exist in steps. This will cause a failure."
285
+ )
286
+
237
287
  self.__current_step = next_step or step["next_step"]
238
288
 
239
289
  except Exception as exc:
@@ -241,7 +291,9 @@ class StepFunction:
241
291
  f"Step '{self.__current_step}' failed. Exception: {exc}"
242
292
  )
243
293
 
244
- self.__context[self.__current_step] = exc.args[0]
294
+ exc_value = exc.args[0] if exc.args else exc
295
+
296
+ self.__context[cast(str, self.__current_step)] = exc_value
245
297
 
246
298
  if step["on_failure"]:
247
299
  self.__logger.exception(
@@ -249,7 +301,7 @@ class StepFunction:
249
301
  )
250
302
 
251
303
  self.__current_step = step["on_failure"]
252
- self.__last_result = exc.args[0]
304
+ self.__last_result = exc_value
253
305
 
254
306
  self.__status = StepFunctionStatus.FAILED
255
307
 
@@ -285,47 +337,31 @@ class StepFunction:
285
337
  else:
286
338
  return func(input_value)
287
339
 
288
- def _execute_parallel(
340
+ async def _execute_parallel(
289
341
  self, func_dict: Dict[str, Callable[[Any], Any]], stop_on_failure: bool = False
290
342
  ):
291
- """Execute the steps in parallel."""
343
+ """Execute the steps in parallel without blocking the event loop."""
344
+ loop = get_running_loop()
292
345
  results = {}
293
346
  errors = []
294
- should_stop_execution = False
295
347
 
296
- def _run(func, arg):
348
+ async def _run_one(func: Callable[[Any], Any]) -> Any:
297
349
  if iscoroutinefunction(func):
298
- return asyncio_run(func(arg))
299
- return func(arg)
300
-
301
- with ThreadPoolExecutor() as executor:
302
- futures = {
303
- executor.submit(_run, func, self.__last_result): step_name
304
- for step_name, func in func_dict.items()
305
- }
306
-
307
- for future in as_completed(futures):
308
- step_name = futures[future]
350
+ return await func(self.__last_result)
351
+ return await loop.run_in_executor(None, func, self.__last_result)
309
352
 
310
- if should_stop_execution:
311
- break
312
-
313
- try:
314
- result = future.result()
315
- results[step_name] = result
316
- except Exception as exc:
317
- self.__logger.exception(
318
- f"Parallel task '{step_name}' failed: {exc}"
319
- )
320
-
321
- results[step_name] = exc.args[0]
322
-
323
- errors.append((step_name, exc))
353
+ task_results = await gather(
354
+ *[_run_one(func) for func in func_dict.values()],
355
+ return_exceptions=True,
356
+ )
324
357
 
325
- if stop_on_failure:
326
- should_stop_execution = True
327
- for f in futures:
328
- f.cancel()
358
+ for step_name, result in zip(func_dict.keys(), task_results):
359
+ if isinstance(result, Exception):
360
+ self.__logger.exception(f"Parallel task '{step_name}' failed: {result}")
361
+ results[step_name] = result.args[0] if result.args else result
362
+ errors.append((step_name, result))
363
+ else:
364
+ results[step_name] = result
329
365
 
330
366
  if errors:
331
367
  self.__logger.error(f"Some parallel tasks failed: {errors}")
@@ -374,7 +410,7 @@ class StepFunction:
374
410
  @property
375
411
  def steps(self):
376
412
  """Returns the steps of the step function."""
377
- return self.__steps
413
+ return self.__steps.copy()
378
414
 
379
415
  @property
380
416
  def last_result(self):
@@ -384,7 +420,7 @@ class StepFunction:
384
420
  @property
385
421
  def context(self):
386
422
  """Returns the context of the step function."""
387
- return self.__context
423
+ return self.__context.copy()
388
424
 
389
425
  @property
390
426
  def status(self):
@@ -4,6 +4,7 @@ Author: Vineeth Penugonda
4
4
  """
5
5
 
6
6
  from os import getcwd
7
+ from typing import Dict, Optional
7
8
 
8
9
  from graphviz import Digraph
9
10
 
@@ -29,7 +30,7 @@ from stepfunction.types.visualizer_types import RenderStepFunctionParams
29
30
  class Visualizer:
30
31
  """This class is responsible for visualizing the graph model."""
31
32
 
32
- def __init__(self, graph_name: str, steps: StepParams = None):
33
+ def __init__(self, graph_name: str, steps: Optional[Dict[str, StepParams]] = None):
33
34
  """Initializes the visualizer."""
34
35
 
35
36
  self.graph_name = graph_name
@@ -47,10 +48,7 @@ class Visualizer:
47
48
  raise ValueError("No steps found to visualize.")
48
49
 
49
50
  for step_name, step_info in self.__steps.items():
50
- if (
51
- "is_sub_step_function" in step_info
52
- and step_info["is_sub_step_function"]
53
- ):
51
+ if step_info["is_sub_step_function"]:
54
52
  self.__dot.node(
55
53
  step_name,
56
54
  step_name,
@@ -100,7 +98,7 @@ class Visualizer:
100
98
  if step_info["next_step"]:
101
99
  self.__dot.edge(parallel_step_name, step_info["next_step"])
102
100
 
103
- if step_info.get("branch"):
101
+ if isinstance(step_info["branch"], dict):
104
102
  for result, next_step in step_info["branch"].items():
105
103
  self.__dot.edge(step_name, next_step, label=f"Branch: {result}")
106
104
 
@@ -9,6 +9,6 @@ class StepExecutionError(Exception):
9
9
  class ParallelStepExecutionError(Exception):
10
10
  """Error raised when a parallel step fails."""
11
11
 
12
- def __init__(self, exc: Exception):
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)
@@ -5,10 +5,11 @@ Author: Vineeth Penugonda
5
5
 
6
6
  from asyncio import sleep
7
7
  from inspect import iscoroutinefunction
8
- from typing import Any, Callable
8
+ from typing import Any, Callable, Optional
9
9
 
10
10
  from stepfunction.constants.enums import StepType
11
11
  from stepfunction.steps.base import BaseStep
12
+ from stepfunction.utils.logger import setup_logger
12
13
 
13
14
 
14
15
  class RetryStep(BaseStep):
@@ -43,6 +44,7 @@ class RetryStep(BaseStep):
43
44
  self.func = func
44
45
  self.max_retries = max_retries
45
46
  self.delay = delay
47
+ self.__logger = setup_logger(__name__)
46
48
 
47
49
  def build(self) -> Callable[[Any], Any]:
48
50
  """Return an async function that retries ``func`` on failure."""
@@ -51,7 +53,10 @@ class RetryStep(BaseStep):
51
53
  delay = self.delay
52
54
 
53
55
  async def run(input_value: Any) -> Any:
54
- last_exc: Exception = None
56
+ self.__logger.debug(
57
+ f"RetryStep - executing with max_retries={max_retries}, delay={delay}s"
58
+ )
59
+ last_exc: Optional[Exception] = None
55
60
 
56
61
  for attempt in range(max_retries + 1):
57
62
  try:
@@ -60,9 +65,14 @@ class RetryStep(BaseStep):
60
65
  return func(input_value)
61
66
  except Exception as exc:
62
67
  last_exc = exc
68
+ self.__logger.warning(
69
+ f"RetryStep - attempt {attempt + 1}/{max_retries + 1} failed: {exc}"
70
+ )
63
71
  if attempt < max_retries:
72
+ self.__logger.debug(f"RetryStep - retrying in {delay}s")
64
73
  await sleep(delay)
65
74
 
75
+ self.__logger.error(f"RetryStep - all {max_retries + 1} attempts exhausted")
66
76
  raise last_exc
67
77
 
68
78
  return run
@@ -11,6 +11,7 @@ from typing import Any, Callable
11
11
  from stepfunction.constants.enums import StepType
12
12
  from stepfunction.steps.base import BaseStep
13
13
  from stepfunction.steps.exceptions import StepTimeoutError
14
+ from stepfunction.utils.logger import setup_logger
14
15
 
15
16
 
16
17
  class TimeoutStep(BaseStep):
@@ -39,6 +40,7 @@ class TimeoutStep(BaseStep):
39
40
 
40
41
  self.func = func
41
42
  self.timeout = timeout
43
+ self.__logger = setup_logger(__name__)
42
44
 
43
45
  def build(self) -> Callable[[Any], Any]:
44
46
  """Return an async function that enforces a timeout on ``func``."""
@@ -46,6 +48,7 @@ class TimeoutStep(BaseStep):
46
48
  timeout = self.timeout
47
49
 
48
50
  async def run(input_value: Any) -> Any:
51
+ self.__logger.debug(f"TimeoutStep - executing with timeout of {timeout}s")
49
52
  try:
50
53
  if iscoroutinefunction(func):
51
54
  return await wait_for(func(input_value), timeout=timeout)
@@ -56,6 +59,7 @@ class TimeoutStep(BaseStep):
56
59
  timeout=timeout,
57
60
  )
58
61
  except AsyncTimeoutError:
62
+ self.__logger.error(f"TimeoutStep - exceeded timeout of {timeout}s")
59
63
  raise StepTimeoutError(timeout)
60
64
 
61
65
  return run
@@ -8,6 +8,7 @@ from typing import Any, Callable
8
8
 
9
9
  from stepfunction.constants.enums import StepType
10
10
  from stepfunction.steps.base import BaseStep
11
+ from stepfunction.utils.logger import setup_logger
11
12
 
12
13
 
13
14
  class WaitStep(BaseStep):
@@ -29,13 +30,16 @@ class WaitStep(BaseStep):
29
30
  if duration < 0:
30
31
  raise ValueError("duration must be a non-negative number")
31
32
  self.duration = duration
33
+ self.__logger = setup_logger(__name__)
32
34
 
33
35
  def build(self) -> Callable[[Any], Any]:
34
36
  """Return an async function that sleeps for ``duration`` seconds."""
35
37
  duration = self.duration
36
38
 
37
39
  async def wait(input_value: Any) -> Any:
40
+ self.__logger.debug(f"WaitStep - sleeping for {duration}s")
38
41
  await sleep(duration)
42
+ self.__logger.debug(f"WaitStep - resumed after {duration}s")
39
43
  return input_value
40
44
 
41
45
  return wait
@@ -3,8 +3,8 @@ from typing import Any, Callable, Dict, Optional, TypedDict, Union
3
3
  from stepfunction.constants.enums import StepType
4
4
 
5
5
 
6
- class StepParams(TypedDict, total=False):
7
- func: Callable[[Any], Any]
6
+ class StepParams(TypedDict):
7
+ func: Union[Callable[[Any], Any], Dict[str, Callable[[Any], Any]]]
8
8
  next_step: Optional[str]
9
9
  on_failure: Optional[str]
10
10
  branch: Optional[Union[Dict[Any, str], Callable[[Any], Optional[str]]]]
File without changes
File without changes
File without changes