ddeutil-workflow 0.0.13__py3-none-any.whl → 0.0.15__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/__init__.py +4 -1
- ddeutil/workflow/__types.py +59 -10
- ddeutil/workflow/api.py +2 -2
- ddeutil/workflow/conf.py +45 -0
- ddeutil/workflow/cron.py +19 -12
- ddeutil/workflow/job.py +191 -153
- ddeutil/workflow/log.py +28 -14
- ddeutil/workflow/scheduler.py +255 -119
- ddeutil/workflow/stage.py +77 -35
- ddeutil/workflow/utils.py +129 -51
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/METADATA +6 -4
- ddeutil_workflow-0.0.15.dist-info/RECORD +22 -0
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.13.dist-info/RECORD +0 -21
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stage.py
CHANGED
@@ -12,16 +12,19 @@ can tracking logs.
|
|
12
12
|
handle stage error on this stage model. I think stage model should have a lot of
|
13
13
|
usecase and it does not worry when I want to create a new one.
|
14
14
|
|
15
|
-
Execution
|
16
|
-
|
15
|
+
Execution --> Ok --> Result with 0
|
16
|
+
--> Error --> Raise StageException
|
17
|
+
|
18
|
+
On the context I/O that pass to stage object at execute process. The execute
|
19
|
+
method receive `{"params": {...}}` for mapping to template.
|
17
20
|
"""
|
18
21
|
from __future__ import annotations
|
19
22
|
|
20
23
|
import contextlib
|
21
24
|
import inspect
|
22
|
-
import os
|
23
25
|
import subprocess
|
24
26
|
import sys
|
27
|
+
import time
|
25
28
|
import uuid
|
26
29
|
from abc import ABC, abstractmethod
|
27
30
|
from collections.abc import Iterator
|
@@ -38,12 +41,12 @@ try:
|
|
38
41
|
except ImportError:
|
39
42
|
from typing_extensions import ParamSpec
|
40
43
|
|
41
|
-
from ddeutil.core import str2bool
|
42
44
|
from pydantic import BaseModel, Field
|
43
45
|
from pydantic.functional_validators import model_validator
|
44
46
|
from typing_extensions import Self
|
45
47
|
|
46
48
|
from .__types import DictData, DictStr, Re, TupleStr
|
49
|
+
from .conf import config
|
47
50
|
from .exceptions import StageException
|
48
51
|
from .log import get_logger
|
49
52
|
from .utils import (
|
@@ -58,6 +61,8 @@ from .utils import (
|
|
58
61
|
)
|
59
62
|
|
60
63
|
P = ParamSpec("P")
|
64
|
+
ReturnResult = Callable[P, Result]
|
65
|
+
DecoratorResult = Callable[[ReturnResult], ReturnResult]
|
61
66
|
logger = get_logger("ddeutil.workflow")
|
62
67
|
|
63
68
|
|
@@ -69,13 +74,13 @@ __all__: TupleStr = (
|
|
69
74
|
"HookStage",
|
70
75
|
"TriggerStage",
|
71
76
|
"Stage",
|
72
|
-
"
|
77
|
+
"HookSearchData",
|
73
78
|
"extract_hook",
|
74
79
|
"handler_result",
|
75
80
|
)
|
76
81
|
|
77
82
|
|
78
|
-
def handler_result(message: str | None = None) ->
|
83
|
+
def handler_result(message: str | None = None) -> DecoratorResult:
|
79
84
|
"""Decorator function for handler result from the stage execution. This
|
80
85
|
function should to use with execution method only.
|
81
86
|
|
@@ -87,11 +92,21 @@ def handler_result(message: str | None = None) -> Callable[P, Result]:
|
|
87
92
|
--> Error --> Raise StageException
|
88
93
|
--> Result with 1 (if env var was set)
|
89
94
|
|
95
|
+
On the last step, it will set the running ID on a return result object
|
96
|
+
from current stage ID before release the final result.
|
97
|
+
|
90
98
|
:param message: A message that want to add at prefix of exception statement.
|
99
|
+
:rtype: Callable[P, Result]
|
91
100
|
"""
|
101
|
+
# NOTE: The prefix message string that want to add on the first exception
|
102
|
+
# message dialog.
|
103
|
+
#
|
104
|
+
# ... ValueError: {message}
|
105
|
+
# ... raise value error from the stage execution process.
|
106
|
+
#
|
92
107
|
message: str = message or ""
|
93
108
|
|
94
|
-
def decorator(func:
|
109
|
+
def decorator(func: ReturnResult) -> ReturnResult:
|
95
110
|
|
96
111
|
@wraps(func)
|
97
112
|
def wrapped(self: Stage, *args, **kwargs):
|
@@ -103,9 +118,7 @@ def handler_result(message: str | None = None) -> Callable[P, Result]:
|
|
103
118
|
logger.error(
|
104
119
|
f"({self.run_id}) [STAGE]: {err.__class__.__name__}: {err}"
|
105
120
|
)
|
106
|
-
if
|
107
|
-
os.getenv("WORKFLOW_CORE_STAGE_RAISE_ERROR", "true")
|
108
|
-
):
|
121
|
+
if config.stage_raise_error:
|
109
122
|
# NOTE: If error that raise from stage execution course by
|
110
123
|
# itself, it will return that error with previous
|
111
124
|
# dependency.
|
@@ -119,14 +132,14 @@ def handler_result(message: str | None = None) -> Callable[P, Result]:
|
|
119
132
|
) from None
|
120
133
|
|
121
134
|
# NOTE: Catching exception error object to result with
|
122
|
-
# error_message
|
123
|
-
|
135
|
+
# error_message and error keys.
|
136
|
+
return Result(
|
124
137
|
status=1,
|
125
138
|
context={
|
139
|
+
"error": err,
|
126
140
|
"error_message": f"{err.__class__.__name__}: {err}",
|
127
141
|
},
|
128
|
-
)
|
129
|
-
return rs.set_run_id(self.run_id)
|
142
|
+
).set_run_id(self.run_id)
|
130
143
|
|
131
144
|
return wrapped
|
132
145
|
|
@@ -162,10 +175,12 @@ class BaseStage(BaseModel, ABC):
|
|
162
175
|
)
|
163
176
|
|
164
177
|
@model_validator(mode="after")
|
165
|
-
def __prepare_running_id(self):
|
178
|
+
def __prepare_running_id(self) -> Self:
|
166
179
|
"""Prepare stage running ID that use default value of field and this
|
167
180
|
method will validate name and id fields should not contain any template
|
168
181
|
parameter (exclude matrix template).
|
182
|
+
|
183
|
+
:rtype: Self
|
169
184
|
"""
|
170
185
|
if self.run_id is None:
|
171
186
|
self.run_id = gen_id(self.name + (self.id or ""), unique=True)
|
@@ -199,16 +214,28 @@ class BaseStage(BaseModel, ABC):
|
|
199
214
|
raise NotImplementedError("Stage should implement ``execute`` method.")
|
200
215
|
|
201
216
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
202
|
-
"""Set an outputs from execution process to
|
217
|
+
"""Set an outputs from execution process to the receive context. The
|
218
|
+
result from execution will pass to value of ``outputs`` key.
|
219
|
+
|
220
|
+
For example of setting output method, If you receive execute output
|
221
|
+
and want to set on the `to` like;
|
222
|
+
|
223
|
+
... (i) output: {'foo': bar}
|
224
|
+
... (ii) to: {}
|
225
|
+
|
226
|
+
The result of the `to` variable will be;
|
227
|
+
|
228
|
+
... (iii) to: {
|
229
|
+
'stages': {
|
230
|
+
'<stage-id>': {'outputs': {'foo': 'bar'}}
|
231
|
+
}
|
232
|
+
}
|
203
233
|
|
204
234
|
:param output: A output data that want to extract to an output key.
|
205
235
|
:param to: A context data that want to add output result.
|
206
236
|
:rtype: DictData
|
207
237
|
"""
|
208
|
-
if not (
|
209
|
-
self.id
|
210
|
-
or str2bool(os.getenv("WORKFLOW_CORE_STAGE_DEFAULT_ID", "false"))
|
211
|
-
):
|
238
|
+
if not (self.id or config.stage_default_id):
|
212
239
|
logger.debug(
|
213
240
|
f"({self.run_id}) [STAGE]: Output does not set because this "
|
214
241
|
f"stage does not set ID or default stage ID config flag not be "
|
@@ -220,16 +247,15 @@ class BaseStage(BaseModel, ABC):
|
|
220
247
|
if "stages" not in to:
|
221
248
|
to["stages"] = {}
|
222
249
|
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
# NOTE: Set the output to that stage generated ID.
|
230
|
-
logger.debug(
|
231
|
-
f"({self.run_id}) [STAGE]: Set output complete with stage ID: {_id}"
|
250
|
+
# NOTE: If the stage ID did not set, it will use its name instead.
|
251
|
+
_id: str = (
|
252
|
+
param2template(self.id, params=to)
|
253
|
+
if self.id
|
254
|
+
else gen_id(param2template(self.name, params=to))
|
232
255
|
)
|
256
|
+
|
257
|
+
# NOTE: Set the output to that stage generated ID with ``outputs`` key.
|
258
|
+
logger.debug(f"({self.run_id}) [STAGE]: Set outputs on: {_id}")
|
233
259
|
to["stages"][_id] = {"outputs": output}
|
234
260
|
return to
|
235
261
|
|
@@ -240,10 +266,10 @@ class BaseStage(BaseModel, ABC):
|
|
240
266
|
:param params: A parameters that want to pass to condition template.
|
241
267
|
:rtype: bool
|
242
268
|
"""
|
243
|
-
params: DictData = params or {}
|
244
269
|
if self.condition is None:
|
245
270
|
return False
|
246
271
|
|
272
|
+
params: DictData = {} if params is None else params
|
247
273
|
_g: DictData = globals() | params
|
248
274
|
try:
|
249
275
|
rs: bool = eval(param2template(self.condition, params), _g, {})
|
@@ -270,6 +296,10 @@ class EmptyStage(BaseStage):
|
|
270
296
|
default=None,
|
271
297
|
description="A string statement that want to logging",
|
272
298
|
)
|
299
|
+
sleep: float = Field(
|
300
|
+
default=0,
|
301
|
+
description="A second value to sleep before finish execution",
|
302
|
+
)
|
273
303
|
|
274
304
|
def execute(self, params: DictData) -> Result:
|
275
305
|
"""Execution method for the Empty stage that do only logging out to
|
@@ -287,6 +317,8 @@ class EmptyStage(BaseStage):
|
|
287
317
|
f"({self.run_id}) [STAGE]: Empty-Execute: {self.name!r}: "
|
288
318
|
f"( {param2template(self.echo, params=params) or '...'} )"
|
289
319
|
)
|
320
|
+
if self.sleep > 0:
|
321
|
+
time.sleep(self.sleep)
|
290
322
|
return Result(status=0, context={})
|
291
323
|
|
292
324
|
|
@@ -462,12 +494,13 @@ class PyStage(BaseStage):
|
|
462
494
|
exec(run, _globals, _locals)
|
463
495
|
|
464
496
|
return Result(
|
465
|
-
status=0,
|
497
|
+
status=0,
|
498
|
+
context={"locals": _locals, "globals": _globals},
|
466
499
|
)
|
467
500
|
|
468
501
|
|
469
502
|
@dataclass(frozen=True)
|
470
|
-
class
|
503
|
+
class HookSearchData:
|
471
504
|
"""Hook Search dataclass that use for receive regular expression grouping
|
472
505
|
dict from searching hook string value.
|
473
506
|
"""
|
@@ -490,7 +523,7 @@ def extract_hook(hook: str) -> Callable[[], TagFunc]:
|
|
490
523
|
)
|
491
524
|
|
492
525
|
# NOTE: Pass the searching hook string to `path`, `func`, and `tag`.
|
493
|
-
hook:
|
526
|
+
hook: HookSearchData = HookSearchData(**found.groupdict())
|
494
527
|
|
495
528
|
# NOTE: Registry object should implement on this package only.
|
496
529
|
rgt: dict[str, Registry] = make_registry(f"{hook.path}")
|
@@ -596,7 +629,11 @@ class TriggerStage(BaseStage):
|
|
596
629
|
... }
|
597
630
|
"""
|
598
631
|
|
599
|
-
trigger: str = Field(
|
632
|
+
trigger: str = Field(
|
633
|
+
description=(
|
634
|
+
"A trigger workflow name that should already exist on the config."
|
635
|
+
),
|
636
|
+
)
|
600
637
|
params: DictData = Field(
|
601
638
|
default_factory=dict,
|
602
639
|
description="A parameter that want to pass to workflow execution.",
|
@@ -610,6 +647,7 @@ class TriggerStage(BaseStage):
|
|
610
647
|
:param params: A parameter data that want to use in this execution.
|
611
648
|
:rtype: Result
|
612
649
|
"""
|
650
|
+
# NOTE: Lazy import this workflow object.
|
613
651
|
from . import Workflow
|
614
652
|
|
615
653
|
# NOTE: Loading workflow object from trigger name.
|
@@ -624,7 +662,11 @@ class TriggerStage(BaseStage):
|
|
624
662
|
return wf.execute(params=param2template(self.params, params))
|
625
663
|
|
626
664
|
|
627
|
-
# NOTE:
|
665
|
+
# NOTE:
|
666
|
+
# An order of parsing stage model on the Job model with ``stages`` field.
|
667
|
+
# From the current build-in stages, they do not have stage that have the same
|
668
|
+
# fields that be cause of parsing on the Job's stages key.
|
669
|
+
#
|
628
670
|
Stage = Union[
|
629
671
|
PyStage,
|
630
672
|
BashStage,
|