ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.34__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 +16 -10
- ddeutil/workflow/api/route.py +2 -2
- ddeutil/workflow/audit.py +28 -37
- ddeutil/workflow/{hook.py → call.py} +27 -27
- ddeutil/workflow/conf.py +47 -12
- ddeutil/workflow/job.py +80 -118
- ddeutil/workflow/result.py +126 -25
- ddeutil/workflow/scheduler.py +165 -150
- ddeutil/workflow/{stage.py → stages.py} +103 -37
- ddeutil/workflow/utils.py +20 -2
- ddeutil/workflow/workflow.py +137 -112
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.34.dist-info}/METADATA +18 -17
- ddeutil_workflow-0.0.34.dist-info/RECORD +26 -0
- ddeutil_workflow-0.0.33.dist-info/RECORD +0 -26
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.34.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.34.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.34.dist-info}/top_level.txt +0 -0
@@ -42,9 +42,9 @@ from pydantic.functional_validators import model_validator
|
|
42
42
|
from typing_extensions import Self
|
43
43
|
|
44
44
|
from .__types import DictData, DictStr, TupleStr
|
45
|
+
from .call import TagFunc, extract_call
|
45
46
|
from .conf import config, get_logger
|
46
47
|
from .exceptions import StageException
|
47
|
-
from .hook import TagFunc, extract_hook
|
48
48
|
from .result import Result, Status
|
49
49
|
from .templates import not_in_template, param2template
|
50
50
|
from .utils import (
|
@@ -60,7 +60,7 @@ __all__: TupleStr = (
|
|
60
60
|
"EmptyStage",
|
61
61
|
"BashStage",
|
62
62
|
"PyStage",
|
63
|
-
"
|
63
|
+
"CallStage",
|
64
64
|
"TriggerStage",
|
65
65
|
"Stage",
|
66
66
|
)
|
@@ -100,7 +100,7 @@ class BaseStage(BaseModel, ABC):
|
|
100
100
|
return self.id or self.name
|
101
101
|
|
102
102
|
@model_validator(mode="after")
|
103
|
-
def
|
103
|
+
def __prepare_running_id(self) -> Self:
|
104
104
|
"""Prepare stage running ID that use default value of field and this
|
105
105
|
method will validate name and id fields should not contain any template
|
106
106
|
parameter (exclude matrix template).
|
@@ -140,24 +140,26 @@ class BaseStage(BaseModel, ABC):
|
|
140
140
|
params: DictData,
|
141
141
|
*,
|
142
142
|
run_id: str | None = None,
|
143
|
+
parent_run_id: str | None = None,
|
143
144
|
result: Result | None = None,
|
144
145
|
) -> Result:
|
145
|
-
"""Handler result from the stage
|
146
|
+
"""Handler execution result from the stage `execute` method.
|
146
147
|
|
147
148
|
This stage exception handler still use ok-error concept, but it
|
148
149
|
allows you force catching an output result with error message by
|
149
150
|
specific environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR`.
|
150
151
|
|
151
152
|
Execution --> Ok --> Result
|
152
|
-
|-status:
|
153
|
+
|-status: Status.SUCCESS
|
153
154
|
|-context:
|
154
155
|
|-outputs: ...
|
155
156
|
|
156
157
|
--> Error --> Result (if env var was set)
|
157
|
-
|-status:
|
158
|
-
|-
|
159
|
-
|-
|
160
|
-
|-
|
158
|
+
|-status: Status.FAILED
|
159
|
+
|-errors:
|
160
|
+
|-class: ...
|
161
|
+
|-name: ...
|
162
|
+
|-message: ...
|
161
163
|
|
162
164
|
--> Error --> Raise StageException(...)
|
163
165
|
|
@@ -166,32 +168,34 @@ class BaseStage(BaseModel, ABC):
|
|
166
168
|
|
167
169
|
:param params: A parameter data that want to use in this execution.
|
168
170
|
:param run_id: (str) A running stage ID for this execution.
|
171
|
+
:param parent_run_id: A parent workflow running ID for this release.
|
169
172
|
:param result: (Result) A result object for keeping context and status
|
170
173
|
data.
|
171
174
|
|
172
175
|
:rtype: Result
|
173
176
|
"""
|
174
|
-
if result is None:
|
177
|
+
if result is None:
|
175
178
|
result: Result = Result(
|
176
179
|
run_id=(
|
177
180
|
run_id or gen_id(self.name + (self.id or ""), unique=True)
|
178
181
|
),
|
182
|
+
parent_run_id=parent_run_id,
|
179
183
|
)
|
184
|
+
elif parent_run_id:
|
185
|
+
result.set_parent_run_id(parent_run_id)
|
180
186
|
|
181
187
|
try:
|
182
|
-
# NOTE: Start calling origin function with a passing args.
|
183
188
|
return self.execute(params, result=result)
|
184
189
|
except Exception as err:
|
185
|
-
# NOTE: Start catching error from the stage execution.
|
186
190
|
result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
|
191
|
+
|
187
192
|
if config.stage_raise_error:
|
188
193
|
# NOTE: If error that raise from stage execution course by
|
189
194
|
# itself, it will return that error with previous
|
190
195
|
# dependency.
|
191
196
|
if isinstance(err, StageException):
|
192
|
-
raise
|
193
|
-
|
194
|
-
) from err
|
197
|
+
raise
|
198
|
+
|
195
199
|
raise StageException(
|
196
200
|
f"{self.__class__.__name__}: \n\t"
|
197
201
|
f"{err.__class__.__name__}: {err}"
|
@@ -202,8 +206,11 @@ class BaseStage(BaseModel, ABC):
|
|
202
206
|
return result.catch(
|
203
207
|
status=Status.FAILED,
|
204
208
|
context={
|
205
|
-
"
|
206
|
-
|
209
|
+
"errors": {
|
210
|
+
"class": err,
|
211
|
+
"name": err.__class__.__name__,
|
212
|
+
"message": f"{err.__class__.__name__}: {err}",
|
213
|
+
},
|
207
214
|
},
|
208
215
|
)
|
209
216
|
|
@@ -247,8 +254,12 @@ class BaseStage(BaseModel, ABC):
|
|
247
254
|
else gen_id(param2template(self.name, params=to))
|
248
255
|
)
|
249
256
|
|
257
|
+
errors: DictData = (
|
258
|
+
{"errors": output.pop("errors", {})} if "errors" in output else {}
|
259
|
+
)
|
260
|
+
|
250
261
|
# NOTE: Set the output to that stage generated ID with ``outputs`` key.
|
251
|
-
to["stages"][_id] = {"outputs": output}
|
262
|
+
to["stages"][_id] = {"outputs": output, **errors}
|
252
263
|
return to
|
253
264
|
|
254
265
|
def is_skipped(self, params: DictData | None = None) -> bool:
|
@@ -321,6 +332,11 @@ class EmptyStage(BaseStage):
|
|
321
332
|
|
322
333
|
:rtype: Result
|
323
334
|
"""
|
335
|
+
if result is None: # pragma: no cov
|
336
|
+
result: Result = Result(
|
337
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
338
|
+
)
|
339
|
+
|
324
340
|
result.trace.info(
|
325
341
|
f"[STAGE]: Empty-Execute: {self.name!r}: "
|
326
342
|
f"( {param2template(self.echo, params=params) or '...'} )"
|
@@ -413,6 +429,11 @@ class BashStage(BaseStage):
|
|
413
429
|
|
414
430
|
:rtype: Result
|
415
431
|
"""
|
432
|
+
if result is None: # pragma: no cov
|
433
|
+
result: Result = Result(
|
434
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
435
|
+
)
|
436
|
+
|
416
437
|
bash: str = param2template(dedent(self.bash), params)
|
417
438
|
|
418
439
|
result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
|
@@ -473,12 +494,24 @@ class PyStage(BaseStage):
|
|
473
494
|
)
|
474
495
|
|
475
496
|
@staticmethod
|
476
|
-
def
|
477
|
-
|
497
|
+
def filter_locals(values: DictData) -> Iterator[str]:
|
498
|
+
"""Filter a locals input values.
|
499
|
+
|
500
|
+
:param values: (DictData) A locals values that want to filter.
|
501
|
+
|
502
|
+
:rtype: Iterator[str]
|
503
|
+
"""
|
504
|
+
from inspect import isclass, ismodule
|
478
505
|
|
479
506
|
for value in values:
|
480
|
-
|
507
|
+
|
508
|
+
if (
|
509
|
+
value == "__annotations__"
|
510
|
+
or ismodule(values[value])
|
511
|
+
or isclass(values[value])
|
512
|
+
):
|
481
513
|
continue
|
514
|
+
|
482
515
|
yield value
|
483
516
|
|
484
517
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
@@ -494,7 +527,7 @@ class PyStage(BaseStage):
|
|
494
527
|
lc: DictData = output.get("locals", {})
|
495
528
|
super().set_outputs(
|
496
529
|
(
|
497
|
-
{k: lc[k] for k in self.
|
530
|
+
{k: lc[k] for k in self.filter_locals(lc)}
|
498
531
|
| {k: output[k] for k in output if k.startswith("error")}
|
499
532
|
),
|
500
533
|
to=to,
|
@@ -518,6 +551,11 @@ class PyStage(BaseStage):
|
|
518
551
|
|
519
552
|
:rtype: Result
|
520
553
|
"""
|
554
|
+
if result is None: # pragma: no cov
|
555
|
+
result: Result = Result(
|
556
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
557
|
+
)
|
558
|
+
|
521
559
|
# NOTE: Replace the run statement that has templating value.
|
522
560
|
run: str = param2template(dedent(self.run), params)
|
523
561
|
|
@@ -539,8 +577,8 @@ class PyStage(BaseStage):
|
|
539
577
|
)
|
540
578
|
|
541
579
|
|
542
|
-
class
|
543
|
-
"""
|
580
|
+
class CallStage(BaseStage):
|
581
|
+
"""Call executor that call the Python function from registry with tag
|
544
582
|
decorator function in ``utils`` module and run it with input arguments.
|
545
583
|
|
546
584
|
This stage is different with PyStage because the PyStage is just calling
|
@@ -558,23 +596,23 @@ class HookStage(BaseStage):
|
|
558
596
|
|
559
597
|
uses: str = Field(
|
560
598
|
description=(
|
561
|
-
"A pointer that want to load function from the
|
599
|
+
"A pointer that want to load function from the call registry."
|
562
600
|
),
|
563
601
|
)
|
564
602
|
args: DictData = Field(
|
565
603
|
default_factory=dict,
|
566
|
-
description="An arguments that want to pass to the
|
604
|
+
description="An arguments that want to pass to the call function.",
|
567
605
|
alias="with",
|
568
606
|
)
|
569
607
|
|
570
608
|
def execute(
|
571
609
|
self, params: DictData, *, result: Result | None = None
|
572
610
|
) -> Result:
|
573
|
-
"""Execute the
|
611
|
+
"""Execute the Call function that already in the call registry.
|
574
612
|
|
575
|
-
:raise ValueError: When the necessary arguments of
|
613
|
+
:raise ValueError: When the necessary arguments of call function do not
|
576
614
|
set from the input params argument.
|
577
|
-
:raise TypeError: When the return type of
|
615
|
+
:raise TypeError: When the return type of call function does not be
|
578
616
|
dict type.
|
579
617
|
|
580
618
|
:param params: A parameter that want to pass before run any statement.
|
@@ -585,7 +623,12 @@ class HookStage(BaseStage):
|
|
585
623
|
|
586
624
|
:rtype: Result
|
587
625
|
"""
|
588
|
-
|
626
|
+
if result is None: # pragma: no cov
|
627
|
+
result: Result = Result(
|
628
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
629
|
+
)
|
630
|
+
|
631
|
+
t_func: TagFunc = extract_call(param2template(self.uses, params))()
|
589
632
|
|
590
633
|
# VALIDATE: check input task caller parameters that exists before
|
591
634
|
# calling.
|
@@ -608,11 +651,11 @@ class HookStage(BaseStage):
|
|
608
651
|
if "result" not in ips.parameters:
|
609
652
|
args.pop("result")
|
610
653
|
|
611
|
-
result.trace.info(f"[STAGE]:
|
654
|
+
result.trace.info(f"[STAGE]: Call-Execute: {t_func.name}@{t_func.tag}")
|
612
655
|
rs: DictData = t_func(**param2template(args, params))
|
613
656
|
|
614
657
|
# VALIDATE:
|
615
|
-
# Check the result type from
|
658
|
+
# Check the result type from call function, it should be dict.
|
616
659
|
if not isinstance(rs, dict):
|
617
660
|
raise TypeError(
|
618
661
|
f"Return type: '{t_func.name}@{t_func.tag}' does not serialize "
|
@@ -659,14 +702,19 @@ class TriggerStage(BaseStage):
|
|
659
702
|
# NOTE: Lazy import this workflow object.
|
660
703
|
from . import Workflow
|
661
704
|
|
705
|
+
if result is None: # pragma: no cov
|
706
|
+
result: Result = Result(
|
707
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
708
|
+
)
|
709
|
+
|
662
710
|
# NOTE: Loading workflow object from trigger name.
|
663
711
|
_trigger: str = param2template(self.trigger, params=params)
|
664
712
|
|
665
713
|
# NOTE: Set running workflow ID from running stage ID to external
|
666
714
|
# params on Loader object.
|
667
|
-
|
715
|
+
workflow: Workflow = Workflow.from_loader(name=_trigger)
|
668
716
|
result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
|
669
|
-
return
|
717
|
+
return workflow.execute(
|
670
718
|
params=param2template(self.params, params),
|
671
719
|
result=result,
|
672
720
|
)
|
@@ -680,19 +728,37 @@ class TriggerStage(BaseStage):
|
|
680
728
|
Stage = Union[
|
681
729
|
PyStage,
|
682
730
|
BashStage,
|
683
|
-
|
731
|
+
CallStage,
|
684
732
|
TriggerStage,
|
685
733
|
EmptyStage,
|
686
734
|
]
|
687
735
|
|
688
736
|
|
689
737
|
# TODO: Not implement this stages yet
|
690
|
-
class ParallelStage(
|
738
|
+
class ParallelStage(BaseStage): # pragma: no cov
|
691
739
|
parallel: list[Stage]
|
692
740
|
max_parallel_core: int = Field(default=2)
|
693
741
|
|
742
|
+
def execute(
|
743
|
+
self, params: DictData, *, result: Result | None = None
|
744
|
+
) -> Result: ...
|
745
|
+
|
694
746
|
|
695
747
|
# TODO: Not implement this stages yet
|
696
|
-
class ForEachStage(
|
748
|
+
class ForEachStage(BaseStage): # pragma: no cov
|
697
749
|
foreach: list[str]
|
698
750
|
stages: list[Stage]
|
751
|
+
|
752
|
+
def execute(
|
753
|
+
self, params: DictData, *, result: Result | None = None
|
754
|
+
) -> Result: ...
|
755
|
+
|
756
|
+
|
757
|
+
# TODO: Not implement this stages yet
|
758
|
+
class HookStage(BaseStage): # pragma: no cov
|
759
|
+
foreach: list[str]
|
760
|
+
stages: list[Stage]
|
761
|
+
|
762
|
+
def execute(
|
763
|
+
self, params: DictData, *, result: Result | None = None
|
764
|
+
) -> Result: ...
|
ddeutil/workflow/utils.py
CHANGED
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
8
8
|
import logging
|
9
9
|
import stat
|
10
10
|
import time
|
11
|
-
from collections.abc import Iterator
|
11
|
+
from collections.abc import Iterator, Mapping
|
12
12
|
from datetime import datetime, timedelta
|
13
13
|
from hashlib import md5
|
14
14
|
from inspect import isfunction
|
@@ -199,7 +199,7 @@ def cross_product(matrix: Matrix) -> Iterator[DictData]:
|
|
199
199
|
)
|
200
200
|
|
201
201
|
|
202
|
-
def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
|
202
|
+
def batch(iterable: Iterator[Any] | range, n: int) -> Iterator[Any]:
|
203
203
|
"""Batch data into iterators of length n. The last batch may be shorter.
|
204
204
|
|
205
205
|
Example:
|
@@ -240,3 +240,21 @@ def cut_id(run_id: str, *, num: int = 6) -> str:
|
|
240
240
|
:rtype: str
|
241
241
|
"""
|
242
242
|
return run_id[-num:]
|
243
|
+
|
244
|
+
|
245
|
+
def deep_update(origin: DictData, u: Mapping) -> DictData:
|
246
|
+
"""Deep update dict.
|
247
|
+
|
248
|
+
Example:
|
249
|
+
>>> deep_update(
|
250
|
+
... origin={"jobs": {"job01": "foo"}},
|
251
|
+
... u={"jobs": {"job02": "bar"}},
|
252
|
+
... )
|
253
|
+
{"jobs": {"job01": "foo", "job02": "bar"}}
|
254
|
+
"""
|
255
|
+
for k, value in u.items():
|
256
|
+
if isinstance(value, Mapping) and value:
|
257
|
+
origin[k] = deep_update(origin.get(k, {}), value)
|
258
|
+
else:
|
259
|
+
origin[k] = value
|
260
|
+
return origin
|