ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.35__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 +19 -10
- ddeutil/workflow/api/api.py +13 -8
- ddeutil/workflow/api/routes/__init__.py +8 -0
- ddeutil/workflow/api/routes/logs.py +36 -0
- ddeutil/workflow/api/{route.py → routes/schedules.py} +2 -131
- ddeutil/workflow/api/routes/workflows.py +137 -0
- ddeutil/workflow/audit.py +28 -37
- ddeutil/workflow/{hook.py → caller.py} +27 -27
- ddeutil/workflow/conf.py +47 -12
- ddeutil/workflow/job.py +149 -138
- ddeutil/workflow/logs.py +214 -0
- ddeutil/workflow/params.py +40 -12
- ddeutil/workflow/result.py +40 -61
- ddeutil/workflow/scheduler.py +185 -163
- ddeutil/workflow/{stage.py → stages.py} +105 -42
- ddeutil/workflow/utils.py +20 -2
- ddeutil/workflow/workflow.py +142 -117
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.35.dist-info}/METADATA +36 -32
- ddeutil_workflow-0.0.35.dist-info/RECORD +30 -0
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.35.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.33.dist-info/RECORD +0 -26
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.35.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.35.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 .caller 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,31 @@ 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
|
-
|
175
|
-
result
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
|
177
|
+
result: Result = Result.construct_with_rs_or_id(
|
178
|
+
result,
|
179
|
+
run_id=run_id,
|
180
|
+
parent_run_id=parent_run_id,
|
181
|
+
id_logic=(self.name + (self.id or "")),
|
182
|
+
)
|
180
183
|
|
181
184
|
try:
|
182
|
-
# NOTE: Start calling origin function with a passing args.
|
183
185
|
return self.execute(params, result=result)
|
184
186
|
except Exception as err:
|
185
|
-
# NOTE: Start catching error from the stage execution.
|
186
187
|
result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
|
188
|
+
|
187
189
|
if config.stage_raise_error:
|
188
190
|
# NOTE: If error that raise from stage execution course by
|
189
191
|
# itself, it will return that error with previous
|
190
192
|
# dependency.
|
191
193
|
if isinstance(err, StageException):
|
192
|
-
raise
|
193
|
-
|
194
|
-
) from err
|
194
|
+
raise
|
195
|
+
|
195
196
|
raise StageException(
|
196
197
|
f"{self.__class__.__name__}: \n\t"
|
197
198
|
f"{err.__class__.__name__}: {err}"
|
@@ -202,8 +203,11 @@ class BaseStage(BaseModel, ABC):
|
|
202
203
|
return result.catch(
|
203
204
|
status=Status.FAILED,
|
204
205
|
context={
|
205
|
-
"
|
206
|
-
|
206
|
+
"errors": {
|
207
|
+
"class": err,
|
208
|
+
"name": err.__class__.__name__,
|
209
|
+
"message": f"{err.__class__.__name__}: {err}",
|
210
|
+
},
|
207
211
|
},
|
208
212
|
)
|
209
213
|
|
@@ -247,8 +251,12 @@ class BaseStage(BaseModel, ABC):
|
|
247
251
|
else gen_id(param2template(self.name, params=to))
|
248
252
|
)
|
249
253
|
|
254
|
+
errors: DictData = (
|
255
|
+
{"errors": output.pop("errors", {})} if "errors" in output else {}
|
256
|
+
)
|
257
|
+
|
250
258
|
# NOTE: Set the output to that stage generated ID with ``outputs`` key.
|
251
|
-
to["stages"][_id] = {"outputs": output}
|
259
|
+
to["stages"][_id] = {"outputs": output, **errors}
|
252
260
|
return to
|
253
261
|
|
254
262
|
def is_skipped(self, params: DictData | None = None) -> bool:
|
@@ -321,6 +329,11 @@ class EmptyStage(BaseStage):
|
|
321
329
|
|
322
330
|
:rtype: Result
|
323
331
|
"""
|
332
|
+
if result is None: # pragma: no cov
|
333
|
+
result: Result = Result(
|
334
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
335
|
+
)
|
336
|
+
|
324
337
|
result.trace.info(
|
325
338
|
f"[STAGE]: Empty-Execute: {self.name!r}: "
|
326
339
|
f"( {param2template(self.echo, params=params) or '...'} )"
|
@@ -413,6 +426,11 @@ class BashStage(BaseStage):
|
|
413
426
|
|
414
427
|
:rtype: Result
|
415
428
|
"""
|
429
|
+
if result is None: # pragma: no cov
|
430
|
+
result: Result = Result(
|
431
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
432
|
+
)
|
433
|
+
|
416
434
|
bash: str = param2template(dedent(self.bash), params)
|
417
435
|
|
418
436
|
result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
|
@@ -473,12 +491,24 @@ class PyStage(BaseStage):
|
|
473
491
|
)
|
474
492
|
|
475
493
|
@staticmethod
|
476
|
-
def
|
477
|
-
|
494
|
+
def filter_locals(values: DictData) -> Iterator[str]:
|
495
|
+
"""Filter a locals input values.
|
496
|
+
|
497
|
+
:param values: (DictData) A locals values that want to filter.
|
498
|
+
|
499
|
+
:rtype: Iterator[str]
|
500
|
+
"""
|
501
|
+
from inspect import isclass, ismodule
|
478
502
|
|
479
503
|
for value in values:
|
480
|
-
|
504
|
+
|
505
|
+
if (
|
506
|
+
value == "__annotations__"
|
507
|
+
or ismodule(values[value])
|
508
|
+
or isclass(values[value])
|
509
|
+
):
|
481
510
|
continue
|
511
|
+
|
482
512
|
yield value
|
483
513
|
|
484
514
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
@@ -494,7 +524,7 @@ class PyStage(BaseStage):
|
|
494
524
|
lc: DictData = output.get("locals", {})
|
495
525
|
super().set_outputs(
|
496
526
|
(
|
497
|
-
{k: lc[k] for k in self.
|
527
|
+
{k: lc[k] for k in self.filter_locals(lc)}
|
498
528
|
| {k: output[k] for k in output if k.startswith("error")}
|
499
529
|
),
|
500
530
|
to=to,
|
@@ -518,6 +548,11 @@ class PyStage(BaseStage):
|
|
518
548
|
|
519
549
|
:rtype: Result
|
520
550
|
"""
|
551
|
+
if result is None: # pragma: no cov
|
552
|
+
result: Result = Result(
|
553
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
554
|
+
)
|
555
|
+
|
521
556
|
# NOTE: Replace the run statement that has templating value.
|
522
557
|
run: str = param2template(dedent(self.run), params)
|
523
558
|
|
@@ -539,8 +574,8 @@ class PyStage(BaseStage):
|
|
539
574
|
)
|
540
575
|
|
541
576
|
|
542
|
-
class
|
543
|
-
"""
|
577
|
+
class CallStage(BaseStage):
|
578
|
+
"""Call executor that call the Python function from registry with tag
|
544
579
|
decorator function in ``utils`` module and run it with input arguments.
|
545
580
|
|
546
581
|
This stage is different with PyStage because the PyStage is just calling
|
@@ -558,23 +593,23 @@ class HookStage(BaseStage):
|
|
558
593
|
|
559
594
|
uses: str = Field(
|
560
595
|
description=(
|
561
|
-
"A pointer that want to load function from the
|
596
|
+
"A pointer that want to load function from the call registry."
|
562
597
|
),
|
563
598
|
)
|
564
599
|
args: DictData = Field(
|
565
600
|
default_factory=dict,
|
566
|
-
description="An arguments that want to pass to the
|
601
|
+
description="An arguments that want to pass to the call function.",
|
567
602
|
alias="with",
|
568
603
|
)
|
569
604
|
|
570
605
|
def execute(
|
571
606
|
self, params: DictData, *, result: Result | None = None
|
572
607
|
) -> Result:
|
573
|
-
"""Execute the
|
608
|
+
"""Execute the Call function that already in the call registry.
|
574
609
|
|
575
|
-
:raise ValueError: When the necessary arguments of
|
610
|
+
:raise ValueError: When the necessary arguments of call function do not
|
576
611
|
set from the input params argument.
|
577
|
-
:raise TypeError: When the return type of
|
612
|
+
:raise TypeError: When the return type of call function does not be
|
578
613
|
dict type.
|
579
614
|
|
580
615
|
:param params: A parameter that want to pass before run any statement.
|
@@ -585,7 +620,12 @@ class HookStage(BaseStage):
|
|
585
620
|
|
586
621
|
:rtype: Result
|
587
622
|
"""
|
588
|
-
|
623
|
+
if result is None: # pragma: no cov
|
624
|
+
result: Result = Result(
|
625
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
626
|
+
)
|
627
|
+
|
628
|
+
t_func: TagFunc = extract_call(param2template(self.uses, params))()
|
589
629
|
|
590
630
|
# VALIDATE: check input task caller parameters that exists before
|
591
631
|
# calling.
|
@@ -608,11 +648,11 @@ class HookStage(BaseStage):
|
|
608
648
|
if "result" not in ips.parameters:
|
609
649
|
args.pop("result")
|
610
650
|
|
611
|
-
result.trace.info(f"[STAGE]:
|
651
|
+
result.trace.info(f"[STAGE]: Call-Execute: {t_func.name}@{t_func.tag}")
|
612
652
|
rs: DictData = t_func(**param2template(args, params))
|
613
653
|
|
614
654
|
# VALIDATE:
|
615
|
-
# Check the result type from
|
655
|
+
# Check the result type from call function, it should be dict.
|
616
656
|
if not isinstance(rs, dict):
|
617
657
|
raise TypeError(
|
618
658
|
f"Return type: '{t_func.name}@{t_func.tag}' does not serialize "
|
@@ -659,14 +699,19 @@ class TriggerStage(BaseStage):
|
|
659
699
|
# NOTE: Lazy import this workflow object.
|
660
700
|
from . import Workflow
|
661
701
|
|
702
|
+
if result is None: # pragma: no cov
|
703
|
+
result: Result = Result(
|
704
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
705
|
+
)
|
706
|
+
|
662
707
|
# NOTE: Loading workflow object from trigger name.
|
663
708
|
_trigger: str = param2template(self.trigger, params=params)
|
664
709
|
|
665
710
|
# NOTE: Set running workflow ID from running stage ID to external
|
666
711
|
# params on Loader object.
|
667
|
-
|
712
|
+
workflow: Workflow = Workflow.from_loader(name=_trigger)
|
668
713
|
result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
|
669
|
-
return
|
714
|
+
return workflow.execute(
|
670
715
|
params=param2template(self.params, params),
|
671
716
|
result=result,
|
672
717
|
)
|
@@ -680,19 +725,37 @@ class TriggerStage(BaseStage):
|
|
680
725
|
Stage = Union[
|
681
726
|
PyStage,
|
682
727
|
BashStage,
|
683
|
-
|
728
|
+
CallStage,
|
684
729
|
TriggerStage,
|
685
730
|
EmptyStage,
|
686
731
|
]
|
687
732
|
|
688
733
|
|
689
734
|
# TODO: Not implement this stages yet
|
690
|
-
class ParallelStage(
|
735
|
+
class ParallelStage(BaseStage): # pragma: no cov
|
691
736
|
parallel: list[Stage]
|
692
737
|
max_parallel_core: int = Field(default=2)
|
693
738
|
|
739
|
+
def execute(
|
740
|
+
self, params: DictData, *, result: Result | None = None
|
741
|
+
) -> Result: ...
|
742
|
+
|
743
|
+
|
744
|
+
# TODO: Not implement this stages yet
|
745
|
+
class ForEachStage(BaseStage): # pragma: no cov
|
746
|
+
foreach: list[str]
|
747
|
+
stages: list[Stage]
|
748
|
+
|
749
|
+
def execute(
|
750
|
+
self, params: DictData, *, result: Result | None = None
|
751
|
+
) -> Result: ...
|
752
|
+
|
694
753
|
|
695
754
|
# TODO: Not implement this stages yet
|
696
|
-
class
|
755
|
+
class HookStage(BaseStage): # pragma: no cov
|
697
756
|
foreach: list[str]
|
698
757
|
stages: list[Stage]
|
758
|
+
|
759
|
+
def execute(
|
760
|
+
self, params: DictData, *, result: Result | None = None
|
761
|
+
) -> 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
|