ddeutil-workflow 0.0.73__py3-none-any.whl → 0.0.75__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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__cron.py +20 -12
- ddeutil/workflow/__init__.py +119 -10
- ddeutil/workflow/__types.py +53 -41
- ddeutil/workflow/api/__init__.py +74 -3
- ddeutil/workflow/api/routes/job.py +15 -29
- ddeutil/workflow/api/routes/logs.py +9 -9
- ddeutil/workflow/api/routes/workflows.py +3 -3
- ddeutil/workflow/audits.py +70 -55
- ddeutil/workflow/cli.py +1 -15
- ddeutil/workflow/conf.py +71 -26
- ddeutil/workflow/errors.py +86 -19
- ddeutil/workflow/event.py +268 -169
- ddeutil/workflow/job.py +331 -192
- ddeutil/workflow/params.py +43 -11
- ddeutil/workflow/result.py +96 -70
- ddeutil/workflow/reusables.py +56 -6
- ddeutil/workflow/stages.py +1059 -572
- ddeutil/workflow/traces.py +205 -124
- ddeutil/workflow/utils.py +58 -19
- ddeutil/workflow/workflow.py +435 -296
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.75.dist-info}/METADATA +27 -17
- ddeutil_workflow-0.0.75.dist-info/RECORD +30 -0
- ddeutil_workflow-0.0.73.dist-info/RECORD +0 -30
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.75.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.75.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.75.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.73.dist-info → ddeutil_workflow-0.0.75.dist-info}/top_level.txt +0 -0
ddeutil/workflow/params.py
CHANGED
@@ -3,13 +3,43 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
6
|
+
"""Parameter Models for Workflow Validation and Processing.
|
7
|
+
|
8
|
+
This module provides comprehensive parameter models for handling validation and
|
9
|
+
preparation of input values passed to workflows and scheduled executions. The
|
10
|
+
parameter system ensures type safety and provides default value management.
|
11
|
+
|
12
|
+
The parameter models support various data types including strings, numbers,
|
13
|
+
dates, choices, and complex types like maps and arrays. Each parameter type
|
14
|
+
provides validation and transformation capabilities.
|
15
|
+
|
16
|
+
Classes:
|
17
|
+
BaseParam: Abstract base class for all parameter types
|
18
|
+
DefaultParam: Base class for parameters with default values
|
19
|
+
DateParam: Date parameter with validation
|
20
|
+
DatetimeParam: Datetime parameter with validation
|
21
|
+
StrParam: String parameter type
|
22
|
+
IntParam: Integer parameter type
|
23
|
+
FloatParam: Float parameter with precision control
|
24
|
+
DecimalParam: Decimal parameter for financial calculations
|
25
|
+
ChoiceParam: Parameter with predefined choices
|
26
|
+
MapParam: Dictionary/mapping parameter type
|
27
|
+
ArrayParam: List/array parameter type
|
28
|
+
|
29
|
+
Example:
|
30
|
+
```python
|
31
|
+
from ddeutil.workflow.params import StrParam, IntParam
|
32
|
+
|
33
|
+
# Define parameters
|
34
|
+
name_param = StrParam(desc="Username", required=True)
|
35
|
+
age_param = IntParam(desc="User age", default=18, required=False)
|
36
|
+
|
37
|
+
# Process values
|
38
|
+
name = name_param.receive("John")
|
39
|
+
age = age_param.receive(None) # Uses default value
|
40
|
+
```
|
12
41
|
"""
|
42
|
+
|
13
43
|
from __future__ import annotations
|
14
44
|
|
15
45
|
from abc import ABC, abstractmethod
|
@@ -22,7 +52,7 @@ from pydantic import BaseModel, Field
|
|
22
52
|
|
23
53
|
from .__types import StrOrInt
|
24
54
|
from .errors import ParamError
|
25
|
-
from .utils import get_d_now, get_dt_now
|
55
|
+
from .utils import UTC, get_d_now, get_dt_now
|
26
56
|
|
27
57
|
T = TypeVar("T")
|
28
58
|
|
@@ -52,7 +82,7 @@ class BaseParam(BaseModel, ABC):
|
|
52
82
|
)
|
53
83
|
|
54
84
|
|
55
|
-
class DefaultParam(BaseParam):
|
85
|
+
class DefaultParam(BaseParam, ABC):
|
56
86
|
"""Default Parameter that will check default if it required. This model do
|
57
87
|
not implement the `receive` method.
|
58
88
|
"""
|
@@ -139,16 +169,18 @@ class DatetimeParam(DefaultParam):
|
|
139
169
|
return self.default
|
140
170
|
|
141
171
|
if isinstance(value, datetime):
|
142
|
-
|
172
|
+
if value.tzinfo is None:
|
173
|
+
return value.replace(tzinfo=UTC)
|
174
|
+
return value.astimezone(UTC)
|
143
175
|
elif isinstance(value, date):
|
144
|
-
return datetime(value.year, value.month, value.day)
|
176
|
+
return datetime(value.year, value.month, value.day, tzinfo=UTC)
|
145
177
|
elif not isinstance(value, str):
|
146
178
|
raise ParamError(
|
147
179
|
f"Value that want to convert to datetime does not support for "
|
148
180
|
f"type: {type(value)}"
|
149
181
|
)
|
150
182
|
try:
|
151
|
-
return datetime.fromisoformat(value)
|
183
|
+
return datetime.fromisoformat(value).replace(tzinfo=UTC)
|
152
184
|
except ValueError:
|
153
185
|
raise ParamError(
|
154
186
|
f"Invalid the ISO format string for datetime: {value!r}"
|
ddeutil/workflow/result.py
CHANGED
@@ -3,9 +3,20 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""
|
7
|
-
|
8
|
-
|
6
|
+
"""Result and Status Management Module.
|
7
|
+
|
8
|
+
This module provides the core result and status management functionality for
|
9
|
+
workflow execution tracking. It includes the Status enumeration for execution
|
10
|
+
states and the Result dataclass for context transfer between workflow components.
|
11
|
+
|
12
|
+
Classes:
|
13
|
+
Status: Enumeration for execution status tracking
|
14
|
+
Result: Dataclass for execution context and result management
|
15
|
+
|
16
|
+
Functions:
|
17
|
+
validate_statuses: Determine final status from multiple status values
|
18
|
+
get_status_from_error: Convert exception types to appropriate status
|
19
|
+
get_dt_tznow: Get current datetime with timezone configuration
|
9
20
|
"""
|
10
21
|
from __future__ import annotations
|
11
22
|
|
@@ -13,7 +24,6 @@ from dataclasses import field
|
|
13
24
|
from datetime import datetime
|
14
25
|
from enum import Enum
|
15
26
|
from typing import Optional, Union
|
16
|
-
from zoneinfo import ZoneInfo
|
17
27
|
|
18
28
|
from pydantic import ConfigDict
|
19
29
|
from pydantic.dataclasses import dataclass
|
@@ -31,23 +41,24 @@ from . import (
|
|
31
41
|
WorkflowError,
|
32
42
|
)
|
33
43
|
from .__types import DictData
|
34
|
-
from .audits import
|
35
|
-
from .conf import dynamic
|
44
|
+
from .audits import Trace, get_trace
|
36
45
|
from .errors import ResultError
|
37
|
-
from .utils import default_gen_id,
|
38
|
-
|
39
|
-
|
40
|
-
def get_dt_tznow(tz: Optional[ZoneInfo] = None) -> datetime: # pragma: no cov
|
41
|
-
"""Return the current datetime object that passing the config timezone.
|
42
|
-
|
43
|
-
:rtype: datetime
|
44
|
-
"""
|
45
|
-
return get_dt_now(tz=dynamic("tz", f=tz))
|
46
|
+
from .utils import default_gen_id, get_dt_now
|
46
47
|
|
47
48
|
|
48
49
|
class Status(str, Enum):
|
49
|
-
"""
|
50
|
-
|
50
|
+
"""Execution status enumeration for workflow components.
|
51
|
+
|
52
|
+
Status enum provides standardized status values for tracking the execution
|
53
|
+
state of workflows, jobs, and stages. Each status includes an emoji
|
54
|
+
representation for visual feedback.
|
55
|
+
|
56
|
+
Attributes:
|
57
|
+
SUCCESS: Successful execution completion
|
58
|
+
FAILED: Execution failed with errors
|
59
|
+
WAIT: Waiting for execution or dependencies
|
60
|
+
SKIP: Execution was skipped due to conditions
|
61
|
+
CANCEL: Execution was cancelled
|
51
62
|
"""
|
52
63
|
|
53
64
|
SUCCESS = "SUCCESS"
|
@@ -58,9 +69,10 @@ class Status(str, Enum):
|
|
58
69
|
|
59
70
|
@property
|
60
71
|
def emoji(self) -> str: # pragma: no cov
|
61
|
-
"""
|
72
|
+
"""Get emoji representation of the status.
|
62
73
|
|
63
|
-
:
|
74
|
+
Returns:
|
75
|
+
str: Unicode emoji character representing the status
|
64
76
|
"""
|
65
77
|
return {
|
66
78
|
"SUCCESS": "✅",
|
@@ -90,12 +102,28 @@ ResultStatuses: list[Status] = [SUCCESS, FAILED, CANCEL, SKIP]
|
|
90
102
|
|
91
103
|
|
92
104
|
def validate_statuses(statuses: list[Status]) -> Status:
|
93
|
-
"""
|
105
|
+
"""Determine final status from multiple status values.
|
94
106
|
|
95
|
-
|
96
|
-
|
107
|
+
Applies workflow logic to determine the overall status based on a collection
|
108
|
+
of individual status values. Follows priority order: CANCEL > FAILED > WAIT >
|
109
|
+
individual status consistency.
|
97
110
|
|
98
|
-
:
|
111
|
+
Args:
|
112
|
+
statuses: List of status values to evaluate
|
113
|
+
|
114
|
+
Returns:
|
115
|
+
Status: Final consolidated status based on workflow logic
|
116
|
+
|
117
|
+
Example:
|
118
|
+
```python
|
119
|
+
# Mixed statuses - FAILED takes priority
|
120
|
+
result = validate_statuses([SUCCESS, FAILED, SUCCESS])
|
121
|
+
# Returns: FAILED
|
122
|
+
|
123
|
+
# All same status
|
124
|
+
result = validate_statuses([SUCCESS, SUCCESS, SUCCESS])
|
125
|
+
# Returns: SUCCESS
|
126
|
+
```
|
99
127
|
"""
|
100
128
|
if any(s == CANCEL for s in statuses):
|
101
129
|
return CANCEL
|
@@ -123,7 +151,11 @@ def get_status_from_error(
|
|
123
151
|
BaseException,
|
124
152
|
]
|
125
153
|
) -> Status:
|
126
|
-
"""Get the Status from the error object.
|
154
|
+
"""Get the Status from the error object.
|
155
|
+
|
156
|
+
Returns:
|
157
|
+
Status: The status from the specific exception class.
|
158
|
+
"""
|
127
159
|
if isinstance(error, (StageSkipError, JobSkipError)):
|
128
160
|
return SKIP
|
129
161
|
elif isinstance(
|
@@ -155,49 +187,13 @@ class Result:
|
|
155
187
|
|
156
188
|
status: Status = field(default=WAIT)
|
157
189
|
context: DictData = field(default_factory=default_context)
|
190
|
+
info: DictData = field(default_factory=dict)
|
158
191
|
run_id: Optional[str] = field(default_factory=default_gen_id)
|
159
192
|
parent_run_id: Optional[str] = field(default=None, compare=False)
|
160
|
-
ts: datetime = field(default_factory=
|
161
|
-
|
162
|
-
trace: Optional[TraceModel] = field(default=None, compare=False, repr=False)
|
193
|
+
ts: datetime = field(default_factory=get_dt_now, compare=False)
|
194
|
+
trace: Optional[Trace] = field(default=None, compare=False, repr=False)
|
163
195
|
extras: DictData = field(default_factory=dict, compare=False, repr=False)
|
164
196
|
|
165
|
-
@classmethod
|
166
|
-
def construct_with_rs_or_id(
|
167
|
-
cls,
|
168
|
-
result: Optional[Result] = None,
|
169
|
-
run_id: Optional[str] = None,
|
170
|
-
parent_run_id: Optional[str] = None,
|
171
|
-
id_logic: Optional[str] = None,
|
172
|
-
*,
|
173
|
-
extras: DictData | None = None,
|
174
|
-
) -> Self:
|
175
|
-
"""Create the Result object or set parent running id if passing Result
|
176
|
-
object.
|
177
|
-
|
178
|
-
:param result: A Result instance.
|
179
|
-
:param run_id: A running ID.
|
180
|
-
:param parent_run_id: A parent running ID.
|
181
|
-
:param id_logic: A logic function that use to generate a running ID.
|
182
|
-
:param extras: An extra parameter that want to override the core config.
|
183
|
-
|
184
|
-
:rtype: Self
|
185
|
-
"""
|
186
|
-
if result is None:
|
187
|
-
return cls(
|
188
|
-
run_id=(run_id or gen_id(id_logic or "", unique=True)),
|
189
|
-
parent_run_id=parent_run_id,
|
190
|
-
ts=get_dt_now(dynamic("tz", extras=extras)),
|
191
|
-
extras=(extras or {}),
|
192
|
-
)
|
193
|
-
elif parent_run_id:
|
194
|
-
result.set_parent_run_id(parent_run_id)
|
195
|
-
|
196
|
-
if extras is not None:
|
197
|
-
result.extras.update(extras)
|
198
|
-
|
199
|
-
return result
|
200
|
-
|
201
197
|
@model_validator(mode="after")
|
202
198
|
def __prepare_trace(self) -> Self:
|
203
199
|
"""Prepare trace field that want to pass after its initialize step.
|
@@ -205,7 +201,7 @@ class Result:
|
|
205
201
|
:rtype: Self
|
206
202
|
"""
|
207
203
|
if self.trace is None: # pragma: no cov
|
208
|
-
self.trace:
|
204
|
+
self.trace: Trace = get_trace(
|
209
205
|
self.run_id,
|
210
206
|
parent_run_id=self.parent_run_id,
|
211
207
|
extras=self.extras,
|
@@ -220,7 +216,7 @@ class Result:
|
|
220
216
|
:rtype: Self
|
221
217
|
"""
|
222
218
|
self.parent_run_id: str = running_id
|
223
|
-
self.trace:
|
219
|
+
self.trace: Trace = get_trace(
|
224
220
|
self.run_id, parent_run_id=running_id, extras=self.extras
|
225
221
|
)
|
226
222
|
return self
|
@@ -240,29 +236,59 @@ class Result:
|
|
240
236
|
|
241
237
|
:rtype: Self
|
242
238
|
"""
|
239
|
+
self.__dict__["context"].update(context or {})
|
243
240
|
self.__dict__["status"] = (
|
244
241
|
Status(status) if isinstance(status, int) else status
|
245
242
|
)
|
246
|
-
self.__dict__["context"].update(context or {})
|
247
243
|
self.__dict__["context"]["status"] = self.status
|
244
|
+
|
245
|
+
# NOTE: Update other context data.
|
248
246
|
if kwargs:
|
249
247
|
for k in kwargs:
|
250
248
|
if k in self.__dict__["context"]:
|
251
249
|
self.__dict__["context"][k].update(kwargs[k])
|
252
250
|
# NOTE: Exclude the `info` key for update information data.
|
253
251
|
elif k == "info":
|
254
|
-
self.__dict__["
|
252
|
+
self.__dict__["info"].update(kwargs["info"])
|
255
253
|
else:
|
256
254
|
raise ResultError(
|
257
255
|
f"The key {k!r} does not exists on context data."
|
258
256
|
)
|
259
257
|
return self
|
260
258
|
|
259
|
+
def make_info(self, data: DictData) -> Self:
|
260
|
+
"""Making information."""
|
261
|
+
self.__dict__["info"].update(data)
|
262
|
+
return self
|
263
|
+
|
261
264
|
def alive_time(self) -> float: # pragma: no cov
|
262
265
|
"""Return total seconds that this object use since it was created.
|
263
266
|
|
264
267
|
:rtype: float
|
265
268
|
"""
|
266
|
-
return (
|
267
|
-
|
268
|
-
|
269
|
+
return (get_dt_now() - self.ts).total_seconds()
|
270
|
+
|
271
|
+
|
272
|
+
def catch(
|
273
|
+
context: DictData,
|
274
|
+
status: Union[int, Status],
|
275
|
+
updated: DictData | None = None,
|
276
|
+
**kwargs,
|
277
|
+
) -> DictData:
|
278
|
+
"""Catch updated context to the current context."""
|
279
|
+
context.update(updated or {})
|
280
|
+
context["status"] = Status(status) if isinstance(status, int) else status
|
281
|
+
|
282
|
+
if not kwargs:
|
283
|
+
return context
|
284
|
+
|
285
|
+
# NOTE: Update other context data.
|
286
|
+
for k in kwargs:
|
287
|
+
if k in context:
|
288
|
+
context[k].update(kwargs[k])
|
289
|
+
# NOTE: Exclude the `info` key for update information data.
|
290
|
+
elif k == "info":
|
291
|
+
context["info"].update(kwargs["info"])
|
292
|
+
else:
|
293
|
+
raise ResultError(f"The key {k!r} does not exists on context data.")
|
294
|
+
return context
|
ddeutil/workflow/reusables.py
CHANGED
@@ -3,8 +3,49 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
|
7
|
-
|
6
|
+
"""Reusable Components and Registry System.
|
7
|
+
|
8
|
+
This module provides a registry system for reusable workflow components including
|
9
|
+
tagged functions, template operations, and utility functions for parameter
|
10
|
+
processing and template rendering.
|
11
|
+
|
12
|
+
The registry system allows developers to create custom callable functions that
|
13
|
+
can be invoked from workflows using the CallStage, enabling extensible and
|
14
|
+
modular workflow design.
|
15
|
+
|
16
|
+
Classes:
|
17
|
+
TagFunc: Tagged function wrapper for registry storage
|
18
|
+
|
19
|
+
Functions:
|
20
|
+
tag: Decorator for registering callable functions
|
21
|
+
param2template: Convert parameters to template format
|
22
|
+
has_template: Check if string contains template variables
|
23
|
+
not_in_template: Validate template restrictions
|
24
|
+
extract_call: Extract callable information from registry
|
25
|
+
create_model_from_caller: Generate Pydantic models from function signatures
|
26
|
+
|
27
|
+
Example:
|
28
|
+
```python
|
29
|
+
from ddeutil.workflow.reusables import tag
|
30
|
+
|
31
|
+
@tag("data-processing", alias="process-csv")
|
32
|
+
def process_csv_file(input_path: str, output_path: str) -> dict:
|
33
|
+
# Custom processing logic
|
34
|
+
return {"status": "completed", "rows_processed": 1000}
|
35
|
+
|
36
|
+
# Use in workflow YAML:
|
37
|
+
# stages:
|
38
|
+
# - name: "Process data"
|
39
|
+
# uses: "data-processing/process-csv@latest"
|
40
|
+
# args:
|
41
|
+
# input_path: "/data/input.csv"
|
42
|
+
# output_path: "/data/output.csv"
|
43
|
+
```
|
44
|
+
|
45
|
+
Note:
|
46
|
+
The registry system supports versioning and aliasing for better function
|
47
|
+
management and backward compatibility.
|
48
|
+
"""
|
8
49
|
from __future__ import annotations
|
9
50
|
|
10
51
|
import copy
|
@@ -277,6 +318,7 @@ def str2template(
|
|
277
318
|
value: str,
|
278
319
|
params: DictData,
|
279
320
|
*,
|
321
|
+
context: Optional[DictData] = None,
|
280
322
|
filters: Optional[dict[str, FilterRegistry]] = None,
|
281
323
|
registers: Optional[list[str]] = None,
|
282
324
|
) -> Optional[str]:
|
@@ -290,6 +332,7 @@ def str2template(
|
|
290
332
|
:param value: (str) A string value that want to map with params.
|
291
333
|
:param params: (DictData) A parameter value that getting with matched
|
292
334
|
regular expression.
|
335
|
+
:param context: (DictData)
|
293
336
|
:param filters: (dict[str, FilterRegistry]) A mapping of filter registry.
|
294
337
|
:param registers: (Optional[list[str]]) Override list of register.
|
295
338
|
|
@@ -318,7 +361,7 @@ def str2template(
|
|
318
361
|
# I recommend to avoid logging params context on this case because it
|
319
362
|
# can include secret value.
|
320
363
|
try:
|
321
|
-
getter: Any = getdot(caller, params)
|
364
|
+
getter: Any = getdot(caller, params | (context or {}))
|
322
365
|
except ValueError:
|
323
366
|
raise UtilError(
|
324
367
|
f"Parameters does not get dot with caller: {caller!r}."
|
@@ -347,6 +390,7 @@ def str2template(
|
|
347
390
|
def param2template(
|
348
391
|
value: T,
|
349
392
|
params: DictData,
|
393
|
+
context: Optional[DictData] = None,
|
350
394
|
filters: Optional[dict[str, FilterRegistry]] = None,
|
351
395
|
*,
|
352
396
|
extras: Optional[DictData] = None,
|
@@ -357,6 +401,7 @@ def param2template(
|
|
357
401
|
:param value: (Any) A value that want to map with params.
|
358
402
|
:param params: (DictData) A parameter value that getting with matched
|
359
403
|
regular expression.
|
404
|
+
:param context: (DictData)
|
360
405
|
:param filters: (dict[str, FilterRegistry]) A filter mapping for mapping
|
361
406
|
with `map_post_filter` func.
|
362
407
|
:param extras: (Optional[list[str]]) An Override extras.
|
@@ -372,16 +417,21 @@ def param2template(
|
|
372
417
|
)
|
373
418
|
if isinstance(value, dict):
|
374
419
|
return {
|
375
|
-
k: param2template(value[k], params, filters, extras=extras)
|
420
|
+
k: param2template(value[k], params, context, filters, extras=extras)
|
376
421
|
for k in value
|
377
422
|
}
|
378
423
|
elif isinstance(value, (list, tuple, set)):
|
379
424
|
return type(value)(
|
380
|
-
[
|
425
|
+
[
|
426
|
+
param2template(i, params, context, filters, extras=extras)
|
427
|
+
for i in value
|
428
|
+
]
|
381
429
|
)
|
382
430
|
elif not isinstance(value, str):
|
383
431
|
return value
|
384
|
-
return str2template(
|
432
|
+
return str2template(
|
433
|
+
value, params, context=context, filters=filters, registers=registers
|
434
|
+
)
|
385
435
|
|
386
436
|
|
387
437
|
@custom_filter("fmt") # pragma: no cov
|