ddeutil-workflow 0.0.37__tar.gz → 0.0.39__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 (71) hide show
  1. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/PKG-INFO +9 -3
  2. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/README.md +3 -1
  3. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/pyproject.toml +5 -0
  4. ddeutil_workflow-0.0.39/src/ddeutil/workflow/__about__.py +1 -0
  5. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/__init__.py +4 -1
  6. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/__types.py +2 -0
  7. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/api/routes/job.py +3 -1
  8. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/api/routes/logs.py +12 -4
  9. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/audit.py +7 -5
  10. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/caller.py +17 -12
  11. ddeutil_workflow-0.0.39/src/ddeutil/workflow/context.py +61 -0
  12. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/exceptions.py +14 -1
  13. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/job.py +224 -135
  14. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/logs.py +6 -1
  15. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/result.py +1 -1
  16. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/stages.py +403 -133
  17. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/templates.py +39 -20
  18. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/utils.py +1 -44
  19. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/workflow.py +168 -84
  20. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil_workflow.egg-info/PKG-INFO +9 -3
  21. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil_workflow.egg-info/SOURCES.txt +2 -0
  22. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil_workflow.egg-info/requires.txt +4 -0
  23. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_call_tag.py +33 -2
  24. ddeutil_workflow-0.0.39/tests/test_context.py +136 -0
  25. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_job.py +19 -9
  26. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_job_exec.py +24 -28
  27. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_job_exec_strategy.py +20 -4
  28. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_job_strategy.py +6 -0
  29. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_result.py +4 -1
  30. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_stage.py +9 -27
  31. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_stage_handler_exec.py +165 -4
  32. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_templates.py +23 -0
  33. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_utils.py +0 -9
  34. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_workflow.py +7 -2
  35. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_workflow_exec.py +58 -28
  36. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_workflow_exec_job.py +0 -1
  37. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_workflow_exec_poke.py +1 -2
  38. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_workflow_exec_release.py +0 -3
  39. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_workflow_task.py +0 -1
  40. ddeutil_workflow-0.0.37/src/ddeutil/workflow/__about__.py +0 -1
  41. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/LICENSE +0 -0
  42. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/setup.cfg +0 -0
  43. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/__cron.py +0 -0
  44. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/api/__init__.py +0 -0
  45. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/api/api.py +0 -0
  46. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/api/log.py +0 -0
  47. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/api/repeat.py +0 -0
  48. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  49. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
  50. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  51. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/conf.py +0 -0
  52. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/cron.py +0 -0
  53. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/params.py +0 -0
  54. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil/workflow/scheduler.py +0 -0
  55. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  56. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  57. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test__cron.py +0 -0
  58. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test__regex.py +0 -0
  59. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_audit.py +0 -0
  60. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_conf.py +0 -0
  61. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_cron_on.py +0 -0
  62. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_logs.py +0 -0
  63. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_params.py +0 -0
  64. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_release.py +0 -0
  65. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_release_queue.py +0 -0
  66. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_schedule.py +0 -0
  67. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_schedule_pending.py +0 -0
  68. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_schedule_tasks.py +0 -0
  69. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_schedule_workflow.py +0 -0
  70. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_scheduler_control.py +0 -0
  71. {ddeutil_workflow-0.0.37 → ddeutil_workflow-0.0.39}/tests/test_templates_filter.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.2
1
+ Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.37
3
+ Version: 0.0.39
4
4
  Summary: Lightweight workflow orchestration
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -31,6 +31,10 @@ Provides-Extra: api
31
31
  Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == "api"
32
32
  Requires-Dist: httpx; extra == "api"
33
33
  Requires-Dist: ujson; extra == "api"
34
+ Provides-Extra: async
35
+ Requires-Dist: aiofiles; extra == "async"
36
+ Requires-Dist: aiohttp; extra == "async"
37
+ Dynamic: license-file
34
38
 
35
39
  # Workflow Orchestration
36
40
 
@@ -126,7 +130,7 @@ flowchart LR
126
130
  > - [Google **Workflows**](https://cloud.google.com/workflows)
127
131
  > - [AWS **Step Functions**](https://aws.amazon.com/step-functions/)
128
132
 
129
- ## :round_pushpin: Installation
133
+ ## 📦 Installation
130
134
 
131
135
  This project need `ddeutil` and `ddeutil-io` extension namespace packages.
132
136
  If you want to install this package with application add-ons, you should add
@@ -165,6 +169,8 @@ run-py-local:
165
169
  run-date: datetime
166
170
  jobs:
167
171
  getting-api-data:
172
+ runs-on:
173
+ type: local
168
174
  stages:
169
175
  - name: "Retrieve API Data"
170
176
  id: retrieve-api
@@ -92,7 +92,7 @@ flowchart LR
92
92
  > - [Google **Workflows**](https://cloud.google.com/workflows)
93
93
  > - [AWS **Step Functions**](https://aws.amazon.com/step-functions/)
94
94
 
95
- ## :round_pushpin: Installation
95
+ ## 📦 Installation
96
96
 
97
97
  This project need `ddeutil` and `ddeutil-io` extension namespace packages.
98
98
  If you want to install this package with application add-ons, you should add
@@ -131,6 +131,8 @@ run-py-local:
131
131
  run-date: datetime
132
132
  jobs:
133
133
  getting-api-data:
134
+ runs-on:
135
+ type: local
134
136
  stages:
135
137
  - name: "Retrieve API Data"
136
138
  id: retrieve-api
@@ -40,6 +40,10 @@ api = [
40
40
  "httpx",
41
41
  "ujson",
42
42
  ]
43
+ async = [
44
+ "aiofiles",
45
+ "aiohttp",
46
+ ]
43
47
 
44
48
  [project.urls]
45
49
  Homepage = "https://github.com/ddeutils/ddeutil-workflow/"
@@ -67,6 +71,7 @@ source = ["src.ddeutil.workflow"]
67
71
  omit = [
68
72
  "src/ddeutil/workflow/__about__.py",
69
73
  "src/ddeutil/workflow/__cron.py",
74
+ "src/ddeutil/workflow/context.py",
70
75
  "src/ddeutil/workflow/api/__init__.py",
71
76
  "src/ddeutil/workflow/api/api.py",
72
77
  "src/ddeutil/workflow/api/log.py",
@@ -0,0 +1 @@
1
+ __version__: str = "0.0.39"
@@ -39,6 +39,8 @@ from .job import (
39
39
  Job,
40
40
  RunsOn,
41
41
  Strategy,
42
+ local_execute,
43
+ local_execute_strategy,
42
44
  )
43
45
  from .logs import (
44
46
  TraceData,
@@ -69,6 +71,8 @@ from .stages import (
69
71
  BashStage,
70
72
  CallStage,
71
73
  EmptyStage,
74
+ ForEachStage,
75
+ ParallelStage,
72
76
  PyStage,
73
77
  Stage,
74
78
  TriggerStage,
@@ -89,7 +93,6 @@ from .templates import (
89
93
  from .utils import (
90
94
  batch,
91
95
  cross_product,
92
- dash2underscore,
93
96
  delay,
94
97
  filter_func,
95
98
  gen_id,
@@ -61,8 +61,10 @@ class Re:
61
61
  # Regular expression:
62
62
  # - Version 1:
63
63
  # \${{\s*(?P<caller>[a-zA-Z0-9_.\s'\"\[\]\(\)\-\{}]+?)\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
64
+ #
64
65
  # - Version 2: (2024-09-30):
65
66
  # \${{\s*(?P<caller>(?P<caller_prefix>(?:[a-zA-Z_-]+\.)*)(?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+))\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
67
+ #
66
68
  # - Version 3: (2024-10-05):
67
69
  # \${{\s*(?P<caller>(?P<caller_prefix>(?:[a-zA-Z_-]+\??\.)*)(?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+\??))\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
68
70
  #
@@ -45,6 +45,7 @@ async def job_execute(
45
45
  run_id=result.run_id,
46
46
  parent_run_id=result.parent_run_id,
47
47
  )
48
+ context: DictData = {}
48
49
  try:
49
50
  job.set_outputs(
50
51
  job.execute(
@@ -52,7 +53,7 @@ async def job_execute(
52
53
  run_id=rs.run_id,
53
54
  parent_run_id=rs.parent_run_id,
54
55
  ).context,
55
- to=params,
56
+ to=context,
56
57
  )
57
58
  except JobException as err:
58
59
  rs.trace.error(f"[WORKFLOW]: {err.__class__.__name__}: {err}")
@@ -70,4 +71,5 @@ async def job_execute(
70
71
  exclude_defaults=True,
71
72
  ),
72
73
  "params": params,
74
+ "context": context,
73
75
  }
@@ -6,7 +6,7 @@
6
6
  """This route include audit and trace log paths."""
7
7
  from __future__ import annotations
8
8
 
9
- from fastapi import APIRouter
9
+ from fastapi import APIRouter, Path, Query
10
10
  from fastapi import status as st
11
11
  from fastapi.responses import UJSONResponse
12
12
 
@@ -27,12 +27,17 @@ log_route = APIRouter(
27
27
  summary="Read all trace logs.",
28
28
  tags=["trace"],
29
29
  )
30
- async def get_traces():
30
+ async def get_traces(
31
+ offset: int = Query(default=0, gt=0),
32
+ limit: int = Query(default=100, gt=0),
33
+ ):
31
34
  """Return all trace logs from the current trace log path that config with
32
35
  `WORKFLOW_LOG_PATH` environment variable name.
33
36
  """
34
37
  return {
35
- "message": "Getting trace logs",
38
+ "message": (
39
+ f"Getting trace logs with offset: {offset} and limit: {limit}"
40
+ ),
36
41
  "traces": [
37
42
  trace.model_dump(
38
43
  by_alias=True,
@@ -117,7 +122,10 @@ async def get_audit_with_workflow(workflow: str):
117
122
  summary="Read all audit logs with specific workflow name and release date.",
118
123
  tags=["audit"],
119
124
  )
120
- async def get_audit_with_workflow_release(workflow: str, release: str):
125
+ async def get_audit_with_workflow_release(
126
+ workflow: str = Path(...),
127
+ release: str = Path(...),
128
+ ):
121
129
  """Return all audit logs with specific workflow name and release date from
122
130
  the current audit log path that config with `WORKFLOW_AUDIT_PATH`
123
131
  environment variable name.
@@ -20,7 +20,7 @@ from typing_extensions import Self
20
20
 
21
21
  from .__types import DictData, TupleStr
22
22
  from .conf import config
23
- from .logs import TraceLog, get_trace
23
+ from .logs import TraceLog, get_dt_tznow, get_trace
24
24
 
25
25
  __all__: TupleStr = (
26
26
  "get_audit",
@@ -43,10 +43,12 @@ class BaseAudit(BaseModel, ABC):
43
43
  default_factory=dict,
44
44
  description="A context that receive from a workflow execution result.",
45
45
  )
46
- parent_run_id: Optional[str] = Field(default=None)
47
- run_id: str
48
- update: datetime = Field(default_factory=datetime.now)
49
- execution_time: float = Field(default=0)
46
+ parent_run_id: Optional[str] = Field(
47
+ default=None, description="A parent running ID."
48
+ )
49
+ run_id: str = Field(description="A running ID")
50
+ update: datetime = Field(default_factory=get_dt_tznow)
51
+ execution_time: float = Field(default=0, description="An execution time.")
50
52
 
51
53
  @model_validator(mode="after")
52
54
  def __model_action(self) -> Self:
@@ -26,6 +26,7 @@ T = TypeVar("T")
26
26
  P = ParamSpec("P")
27
27
 
28
28
  logger = logging.getLogger("ddeutil.workflow")
29
+ logging.getLogger("asyncio").setLevel(logging.INFO)
29
30
 
30
31
 
31
32
  class TagFunc(Protocol):
@@ -47,9 +48,10 @@ def tag(
47
48
  """Tag decorator function that set function attributes, ``tag`` and ``name``
48
49
  for making registries variable.
49
50
 
50
- :param: name: A tag name for make different use-case of a function.
51
- :param: alias: A alias function name that keeping in registries. If this
52
- value does not supply, it will use original function name from __name__.
51
+ :param: name: (str) A tag name for make different use-case of a function.
52
+ :param: alias: (str) A alias function name that keeping in registries.
53
+ If this value does not supply, it will use original function name
54
+ from `__name__` argument.
53
55
 
54
56
  :rtype: Callable[P, TagFunc]
55
57
  """
@@ -60,10 +62,13 @@ def tag(
60
62
 
61
63
  @wraps(func)
62
64
  def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
63
- # NOTE: Able to do anything before calling the call function.
64
65
  return func(*args, **kwargs)
65
66
 
66
- return wrapped
67
+ @wraps(func)
68
+ async def awrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
69
+ return await func(*args, **kwargs)
70
+
71
+ return awrapped if inspect.iscoroutinefunction(func) else wrapped
67
72
 
68
73
  return func_internal
69
74
 
@@ -74,7 +79,7 @@ Registry = dict[str, Callable[[], TagFunc]]
74
79
  def make_registry(submodule: str) -> dict[str, Registry]:
75
80
  """Return registries of all functions that able to called with task.
76
81
 
77
- :param submodule: A module prefix that want to import registry.
82
+ :param submodule: (str) A module prefix that want to import registry.
78
83
 
79
84
  :rtype: dict[str, Registry]
80
85
  """
@@ -130,12 +135,7 @@ def extract_call(call: str) -> Callable[[], TagFunc]:
130
135
  """Extract Call function from string value to call partial function that
131
136
  does run it at runtime.
132
137
 
133
- :raise NotImplementedError: When the searching call's function result does
134
- not exist in the registry.
135
- :raise NotImplementedError: When the searching call's tag result does not
136
- exist in the registry with its function key.
137
-
138
- :param call: A call value that able to match with Task regex.
138
+ :param call: (str) A call value that able to match with Task regex.
139
139
 
140
140
  The format of call value should contain 3 regular expression groups
141
141
  which match with the below config format:
@@ -148,6 +148,11 @@ def extract_call(call: str) -> Callable[[], TagFunc]:
148
148
  >>> extract_call("tasks/return-type-not-valid@raise")
149
149
  ...
150
150
 
151
+ :raise NotImplementedError: When the searching call's function result does
152
+ not exist in the registry.
153
+ :raise NotImplementedError: When the searching call's tag result does not
154
+ exist in the registry with its function key.
155
+
151
156
  :rtype: Callable[[], TagFunc]
152
157
  """
153
158
  if not (found := Re.RE_TASK_FMT.search(call)):
@@ -0,0 +1,61 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional, Union
4
+
5
+ from pydantic import BaseModel, ConfigDict, Field
6
+
7
+ from .__types import DictData
8
+
9
+
10
+ class ErrorContext(BaseModel): # pragma: no cov
11
+ model_config = ConfigDict(arbitrary_types_allowed=True)
12
+
13
+ obj: Exception = Field(alias="class")
14
+ name: str = Field(description="A name of exception class.")
15
+ message: str = Field(description="A exception message.")
16
+
17
+
18
+ class OutputContext(BaseModel): # pragma: no cov
19
+ outputs: DictData = Field(default_factory=dict)
20
+ errors: Optional[ErrorContext] = Field(default=None)
21
+ skipped: bool = Field(default=False)
22
+
23
+ def is_exception(self) -> bool:
24
+ return self.errors is not None
25
+
26
+
27
+ class StageContext(BaseModel): # pragma: no cov
28
+ stages: dict[str, OutputContext]
29
+ errors: Optional[ErrorContext] = Field(default=None)
30
+
31
+ def is_exception(self) -> bool:
32
+ return self.errors is not None
33
+
34
+
35
+ class MatrixContext(StageContext): # pragma: no cov
36
+ matrix: DictData = Field(default_factory=dict)
37
+
38
+
39
+ MatrixStageContext = dict[
40
+ str, Union[MatrixContext, StageContext]
41
+ ] # pragma: no cov
42
+
43
+
44
+ class StrategyContext(BaseModel): # pragma: no cov
45
+ strategies: MatrixStageContext
46
+ errors: Optional[ErrorContext] = Field(default=None)
47
+
48
+ def is_exception(self) -> bool:
49
+ return self.errors is not None
50
+
51
+
52
+ StrategyMatrixContext = Union[
53
+ StrategyContext, MatrixStageContext
54
+ ] # pragma: no cov
55
+
56
+
57
+ class JobContext(BaseModel): # pragma: no cov
58
+ params: DictData = Field(description="A parameterize value")
59
+ jobs: dict[str, StrategyMatrixContext]
60
+ errors: Optional[ErrorContext] = Field(default=None)
61
+ skipped: bool = Field(default=False)
@@ -9,8 +9,21 @@ annotate for handle error only.
9
9
  """
10
10
  from __future__ import annotations
11
11
 
12
+ from typing import Any
12
13
 
13
- class BaseWorkflowException(Exception): ...
14
+
15
+ def to_dict(exception: Exception) -> dict[str, Any]: # pragma: no cov
16
+ return {
17
+ "class": exception,
18
+ "name": exception.__class__.__name__,
19
+ "message": str(exception),
20
+ }
21
+
22
+
23
+ class BaseWorkflowException(Exception):
24
+
25
+ def to_dict(self) -> dict[str, Any]:
26
+ return to_dict(self)
14
27
 
15
28
 
16
29
  class UtilException(BaseWorkflowException): ...