ddeutil-workflow 0.0.31__py3-none-any.whl → 0.0.33__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 -2
- ddeutil/workflow/api/api.py +4 -6
- ddeutil/workflow/api/route.py +8 -7
- ddeutil/workflow/audit.py +261 -0
- ddeutil/workflow/conf.py +122 -265
- ddeutil/workflow/job.py +61 -52
- ddeutil/workflow/params.py +5 -2
- ddeutil/workflow/result.py +89 -37
- ddeutil/workflow/scheduler.py +118 -45
- ddeutil/workflow/stage.py +75 -56
- ddeutil/workflow/templates.py +13 -4
- ddeutil/workflow/workflow.py +63 -64
- {ddeutil_workflow-0.0.31.dist-info → ddeutil_workflow-0.0.33.dist-info}/METADATA +46 -27
- ddeutil_workflow-0.0.33.dist-info/RECORD +26 -0
- ddeutil_workflow-0.0.31.dist-info/RECORD +0 -25
- {ddeutil_workflow-0.0.31.dist-info → ddeutil_workflow-0.0.33.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.31.dist-info → ddeutil_workflow-0.0.33.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.31.dist-info → ddeutil_workflow-0.0.33.dist-info}/top_level.txt +0 -0
ddeutil/workflow/job.py
CHANGED
@@ -7,6 +7,8 @@
|
|
7
7
|
The job handle the lineage of stages and location of execution of stages that
|
8
8
|
mean the job model able to define ``runs-on`` key that allow you to run this
|
9
9
|
job.
|
10
|
+
|
11
|
+
This module include Strategy Model that use on the job strategy field.
|
10
12
|
"""
|
11
13
|
from __future__ import annotations
|
12
14
|
|
@@ -36,12 +38,11 @@ from .exceptions import (
|
|
36
38
|
StageException,
|
37
39
|
UtilException,
|
38
40
|
)
|
39
|
-
from .result import Result
|
41
|
+
from .result import Result, Status
|
40
42
|
from .stage import Stage
|
41
43
|
from .templates import has_template
|
42
44
|
from .utils import (
|
43
45
|
cross_product,
|
44
|
-
cut_id,
|
45
46
|
dash2underscore,
|
46
47
|
filter_func,
|
47
48
|
gen_id,
|
@@ -220,6 +221,8 @@ class RunsOn(str, Enum):
|
|
220
221
|
|
221
222
|
local: str = "local"
|
222
223
|
docker: str = "docker"
|
224
|
+
self_hosted: str = "self_hosted"
|
225
|
+
k8s: str = "k8s"
|
223
226
|
|
224
227
|
|
225
228
|
class Job(BaseModel):
|
@@ -410,6 +413,7 @@ class Job(BaseModel):
|
|
410
413
|
params: DictData,
|
411
414
|
*,
|
412
415
|
run_id: str | None = None,
|
416
|
+
result: Result | None = None,
|
413
417
|
event: Event | None = None,
|
414
418
|
) -> Result:
|
415
419
|
"""Job Strategy execution with passing dynamic parameters from the
|
@@ -429,13 +433,18 @@ class Job(BaseModel):
|
|
429
433
|
This value will pass to the `matrix` key for templating.
|
430
434
|
:param params: A dynamic parameters that will deepcopy to the context.
|
431
435
|
:param run_id: A job running ID for this strategy execution.
|
436
|
+
:param result: (Result) A result object for keeping context and status
|
437
|
+
data.
|
432
438
|
:param event: An event manager that pass to the PoolThreadExecutor.
|
433
439
|
|
434
440
|
:rtype: Result
|
435
441
|
"""
|
436
|
-
|
442
|
+
if result is None: # pragma: no cov
|
443
|
+
result: Result = Result(
|
444
|
+
run_id=(run_id or gen_id(self.id or "", unique=True))
|
445
|
+
)
|
446
|
+
|
437
447
|
strategy_id: str = gen_id(strategy)
|
438
|
-
rs: Result = Result(run_id=run_id)
|
439
448
|
|
440
449
|
# PARAGRAPH:
|
441
450
|
#
|
@@ -456,18 +465,14 @@ class Job(BaseModel):
|
|
456
465
|
for stage in self.stages:
|
457
466
|
|
458
467
|
if stage.is_skipped(params=context):
|
459
|
-
|
460
|
-
f"({cut_id(run_id)}) [JOB]: Skip stage: {stage.iden!r}"
|
461
|
-
)
|
468
|
+
result.trace.info(f"[JOB]: Skip stage: {stage.iden!r}")
|
462
469
|
continue
|
463
470
|
|
464
|
-
|
465
|
-
f"({cut_id(run_id)}) [JOB]: Execute stage: {stage.iden!r}"
|
466
|
-
)
|
471
|
+
result.trace.info(f"[JOB]: Execute stage: {stage.iden!r}")
|
467
472
|
|
468
473
|
# NOTE: Logging a matrix that pass on this stage execution.
|
469
474
|
if strategy:
|
470
|
-
|
475
|
+
result.trace.info(f"[JOB]: ... Matrix: {strategy}")
|
471
476
|
|
472
477
|
# NOTE: Force stop this execution if event was set from main
|
473
478
|
# execution.
|
@@ -476,7 +481,7 @@ class Job(BaseModel):
|
|
476
481
|
"Job strategy was canceled from event that had set before "
|
477
482
|
"strategy execution."
|
478
483
|
)
|
479
|
-
return
|
484
|
+
return result.catch(
|
480
485
|
status=1,
|
481
486
|
context={
|
482
487
|
strategy_id: {
|
@@ -514,20 +519,18 @@ class Job(BaseModel):
|
|
514
519
|
try:
|
515
520
|
stage.set_outputs(
|
516
521
|
stage.handler_execute(
|
517
|
-
params=context, run_id=run_id
|
522
|
+
params=context, run_id=result.run_id
|
518
523
|
).context,
|
519
524
|
to=context,
|
520
525
|
)
|
521
526
|
except (StageException, UtilException) as err:
|
522
|
-
|
523
|
-
f"({cut_id(run_id)}) [JOB]: {err.__class__.__name__}: {err}"
|
524
|
-
)
|
527
|
+
result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
|
525
528
|
if config.job_raise_error:
|
526
529
|
raise JobException(
|
527
530
|
f"Get stage execution error: {err.__class__.__name__}: "
|
528
531
|
f"{err}"
|
529
532
|
) from None
|
530
|
-
return
|
533
|
+
return result.catch(
|
531
534
|
status=1,
|
532
535
|
context={
|
533
536
|
strategy_id: {
|
@@ -542,8 +545,8 @@ class Job(BaseModel):
|
|
542
545
|
# NOTE: Remove the current stage object for saving memory.
|
543
546
|
del stage
|
544
547
|
|
545
|
-
return
|
546
|
-
status=
|
548
|
+
return result.catch(
|
549
|
+
status=Status.SUCCESS,
|
547
550
|
context={
|
548
551
|
strategy_id: {
|
549
552
|
"matrix": strategy,
|
@@ -552,36 +555,43 @@ class Job(BaseModel):
|
|
552
555
|
},
|
553
556
|
)
|
554
557
|
|
555
|
-
def execute(
|
558
|
+
def execute(
|
559
|
+
self,
|
560
|
+
params: DictData,
|
561
|
+
*,
|
562
|
+
run_id: str | None = None,
|
563
|
+
result: Result | None = None,
|
564
|
+
) -> Result:
|
556
565
|
"""Job execution with passing dynamic parameters from the workflow
|
557
566
|
execution. It will generate matrix values at the first step and run
|
558
567
|
multithread on this metrics to the ``stages`` field of this job.
|
559
568
|
|
560
569
|
:param params: An input parameters that use on job execution.
|
561
570
|
:param run_id: A job running ID for this execution.
|
571
|
+
:param result: (Result) A result object for keeping context and status
|
572
|
+
data.
|
562
573
|
|
563
574
|
:rtype: Result
|
564
575
|
"""
|
565
576
|
|
566
577
|
# NOTE: I use this condition because this method allow passing empty
|
567
578
|
# params and I do not want to create new dict object.
|
568
|
-
|
569
|
-
|
579
|
+
if result is None: # pragma: no cov
|
580
|
+
run_id: str = run_id or gen_id(self.id or "", unique=True)
|
581
|
+
result: Result = Result(run_id=run_id)
|
570
582
|
|
571
583
|
# NOTE: Normal Job execution without parallel strategy matrix. It uses
|
572
584
|
# for-loop to control strategy execution sequentially.
|
573
585
|
if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
|
586
|
+
|
574
587
|
for strategy in self.strategy.make():
|
575
|
-
|
588
|
+
result: Result = self.execute_strategy(
|
576
589
|
strategy=strategy,
|
577
590
|
params=params,
|
578
|
-
|
591
|
+
result=result,
|
579
592
|
)
|
580
|
-
|
581
|
-
return
|
582
|
-
status=0,
|
583
|
-
context=context,
|
584
|
-
)
|
593
|
+
|
594
|
+
return result.catch(status=Status.SUCCESS)
|
585
595
|
|
586
596
|
# NOTE: Create event for cancel executor by trigger stop running event.
|
587
597
|
event: Event = Event()
|
@@ -598,23 +608,23 @@ class Job(BaseModel):
|
|
598
608
|
self.execute_strategy,
|
599
609
|
strategy=strategy,
|
600
610
|
params=params,
|
601
|
-
|
611
|
+
result=result,
|
602
612
|
event=event,
|
603
613
|
)
|
604
614
|
for strategy in self.strategy.make()
|
605
615
|
]
|
606
616
|
|
607
617
|
return (
|
608
|
-
self.__catch_fail_fast(event, futures=futures,
|
618
|
+
self.__catch_fail_fast(event, futures=futures, result=result)
|
609
619
|
if self.strategy.fail_fast
|
610
|
-
else self.__catch_all_completed(futures=futures,
|
620
|
+
else self.__catch_all_completed(futures=futures, result=result)
|
611
621
|
)
|
612
622
|
|
613
623
|
@staticmethod
|
614
624
|
def __catch_fail_fast(
|
615
625
|
event: Event,
|
616
626
|
futures: list[Future],
|
617
|
-
|
627
|
+
result: Result,
|
618
628
|
*,
|
619
629
|
timeout: int = 1800,
|
620
630
|
) -> Result:
|
@@ -625,14 +635,14 @@ class Job(BaseModel):
|
|
625
635
|
:param event: An event manager instance that able to set stopper on the
|
626
636
|
observing multithreading.
|
627
637
|
:param futures: A list of futures.
|
628
|
-
:param
|
638
|
+
:param result: (Result) A result object for keeping context and status
|
639
|
+
data.
|
629
640
|
:param timeout: A timeout to waiting all futures complete.
|
630
641
|
|
631
642
|
:rtype: Result
|
632
643
|
"""
|
633
|
-
rs_final: Result = Result(run_id=run_id)
|
634
644
|
context: DictData = {}
|
635
|
-
status:
|
645
|
+
status: Status = Status.SUCCESS
|
636
646
|
|
637
647
|
# NOTE: Get results from a collection of tasks with a timeout that has
|
638
648
|
# the first exception.
|
@@ -642,7 +652,7 @@ class Job(BaseModel):
|
|
642
652
|
nd: str = (
|
643
653
|
f", the strategies do not run is {not_done}" if not_done else ""
|
644
654
|
)
|
645
|
-
|
655
|
+
result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
|
646
656
|
|
647
657
|
# NOTE:
|
648
658
|
# Stop all running tasks with setting the event manager and cancel
|
@@ -658,10 +668,9 @@ class Job(BaseModel):
|
|
658
668
|
|
659
669
|
# NOTE: Handle the first exception from feature
|
660
670
|
if err := future.exception():
|
661
|
-
status:
|
662
|
-
|
663
|
-
f"
|
664
|
-
f"{future.exception()}"
|
671
|
+
status: Status = Status.FAILED
|
672
|
+
result.trace.error(
|
673
|
+
f"[JOB]: Fail-fast catching:\n\t{future.exception()}"
|
665
674
|
)
|
666
675
|
context.update(
|
667
676
|
{
|
@@ -672,36 +681,36 @@ class Job(BaseModel):
|
|
672
681
|
continue
|
673
682
|
|
674
683
|
# NOTE: Update the result context to main job context.
|
675
|
-
|
684
|
+
future.result()
|
676
685
|
|
677
|
-
return
|
686
|
+
return result.catch(status=status, context=context)
|
678
687
|
|
679
688
|
@staticmethod
|
680
689
|
def __catch_all_completed(
|
681
690
|
futures: list[Future],
|
682
|
-
|
691
|
+
result: Result,
|
683
692
|
*,
|
684
693
|
timeout: int = 1800,
|
685
694
|
) -> Result:
|
686
695
|
"""Job parallel pool futures catching with all-completed mode.
|
687
696
|
|
688
697
|
:param futures: A list of futures.
|
689
|
-
:param
|
698
|
+
:param result: (Result) A result object for keeping context and status
|
699
|
+
data.
|
690
700
|
:param timeout: A timeout to waiting all futures complete.
|
691
701
|
|
692
702
|
:rtype: Result
|
693
703
|
"""
|
694
|
-
rs_final: Result = Result(run_id=run_id)
|
695
704
|
context: DictData = {}
|
696
|
-
status:
|
705
|
+
status: Status = Status.SUCCESS
|
697
706
|
|
698
707
|
for future in as_completed(futures, timeout=timeout):
|
699
708
|
try:
|
700
|
-
|
709
|
+
future.result()
|
701
710
|
except JobException as err:
|
702
|
-
status =
|
703
|
-
|
704
|
-
f"
|
711
|
+
status = Status.FAILED
|
712
|
+
result.trace.error(
|
713
|
+
f"[JOB]: All-completed catching:\n\t"
|
705
714
|
f"{err.__class__.__name__}:\n\t{err}"
|
706
715
|
)
|
707
716
|
context.update(
|
@@ -711,4 +720,4 @@ class Job(BaseModel):
|
|
711
720
|
},
|
712
721
|
)
|
713
722
|
|
714
|
-
return
|
723
|
+
return result.catch(status=status, context=context)
|
ddeutil/workflow/params.py
CHANGED
@@ -3,6 +3,9 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
+
"""Param Model that use for parsing incoming parameters that pass to the
|
7
|
+
Workflow and Schedule objects.
|
8
|
+
"""
|
6
9
|
from __future__ import annotations
|
7
10
|
|
8
11
|
import decimal
|
@@ -70,7 +73,7 @@ class DefaultParam(BaseParam):
|
|
70
73
|
|
71
74
|
|
72
75
|
# TODO: Not implement this parameter yet
|
73
|
-
class DateParam(DefaultParam):
|
76
|
+
class DateParam(DefaultParam): # pragma: no cov
|
74
77
|
"""Date parameter."""
|
75
78
|
|
76
79
|
type: Literal["date"] = "date"
|
@@ -156,7 +159,7 @@ class IntParam(DefaultParam):
|
|
156
159
|
|
157
160
|
|
158
161
|
# TODO: Not implement this parameter yet
|
159
|
-
class DecimalParam(DefaultParam):
|
162
|
+
class DecimalParam(DefaultParam): # pragma: no cov
|
160
163
|
type: Literal["decimal"] = "decimal"
|
161
164
|
|
162
165
|
def receive(self, value: float | None = None) -> decimal.Decimal: ...
|
ddeutil/workflow/result.py
CHANGED
@@ -6,19 +6,76 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
from dataclasses import field
|
9
|
+
from datetime import datetime
|
10
|
+
from enum import IntEnum
|
11
|
+
from threading import Event
|
9
12
|
from typing import Optional
|
10
13
|
|
14
|
+
from pydantic import ConfigDict
|
11
15
|
from pydantic.dataclasses import dataclass
|
12
|
-
from pydantic.functional_validators import model_validator
|
13
16
|
from typing_extensions import Self
|
14
17
|
|
15
18
|
from .__types import DictData, TupleStr
|
16
|
-
from .
|
19
|
+
from .conf import config, get_logger
|
20
|
+
from .utils import cut_id, gen_id, get_dt_now
|
17
21
|
|
18
|
-
|
22
|
+
logger = get_logger("ddeutil.workflow.audit")
|
19
23
|
|
24
|
+
__all__: TupleStr = (
|
25
|
+
"Result",
|
26
|
+
"Status",
|
27
|
+
)
|
20
28
|
|
21
|
-
|
29
|
+
|
30
|
+
def default_gen_id() -> str:
|
31
|
+
"""Return running ID which use for making default ID for the Result model if
|
32
|
+
a run_id field initializes at the first time.
|
33
|
+
|
34
|
+
:rtype: str
|
35
|
+
"""
|
36
|
+
return gen_id("manual", unique=True)
|
37
|
+
|
38
|
+
|
39
|
+
def get_dt_tznow() -> datetime:
|
40
|
+
"""Return the current datetime object that passing the config timezone.
|
41
|
+
|
42
|
+
:rtype: datetime
|
43
|
+
"""
|
44
|
+
return get_dt_now(tz=config.tz)
|
45
|
+
|
46
|
+
|
47
|
+
class Status(IntEnum):
|
48
|
+
"""Status Int Enum object."""
|
49
|
+
|
50
|
+
SUCCESS: int = 0
|
51
|
+
FAILED: int = 1
|
52
|
+
WAIT: int = 2
|
53
|
+
|
54
|
+
|
55
|
+
class TraceLog: # pragma: no cov
|
56
|
+
"""Trace Log object."""
|
57
|
+
|
58
|
+
__slots__: TupleStr = ("run_id",)
|
59
|
+
|
60
|
+
def __init__(self, run_id: str):
|
61
|
+
self.run_id: str = run_id
|
62
|
+
|
63
|
+
def debug(self, message: str):
|
64
|
+
logger.debug(f"({cut_id(self.run_id)}) {message}")
|
65
|
+
|
66
|
+
def info(self, message: str):
|
67
|
+
logger.info(f"({cut_id(self.run_id)}) {message}")
|
68
|
+
|
69
|
+
def warning(self, message: str):
|
70
|
+
logger.warning(f"({cut_id(self.run_id)}) {message}")
|
71
|
+
|
72
|
+
def error(self, message: str):
|
73
|
+
logger.error(f"({cut_id(self.run_id)}) {message}")
|
74
|
+
|
75
|
+
|
76
|
+
@dataclass(
|
77
|
+
config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True)
|
78
|
+
)
|
22
79
|
class Result:
|
23
80
|
"""Result Pydantic Model for passing and receiving data context from any
|
24
81
|
module execution process like stage execution, job execution, or workflow
|
@@ -28,22 +85,14 @@ class Result:
|
|
28
85
|
and ``_run_id`` fields to comparing with other result instance.
|
29
86
|
"""
|
30
87
|
|
31
|
-
status:
|
88
|
+
status: Status = field(default=Status.WAIT)
|
32
89
|
context: DictData = field(default_factory=dict)
|
33
|
-
run_id: Optional[str] = field(
|
90
|
+
run_id: Optional[str] = field(default_factory=default_gen_id)
|
34
91
|
|
35
92
|
# NOTE: Ignore this field to compare another result model with __eq__.
|
36
93
|
parent_run_id: Optional[str] = field(default=None, compare=False)
|
37
|
-
|
38
|
-
|
39
|
-
def __prepare_run_id(self) -> Self:
|
40
|
-
"""Prepare running ID which use default ID if it initializes at the
|
41
|
-
first time.
|
42
|
-
|
43
|
-
:rtype: Self
|
44
|
-
"""
|
45
|
-
self._run_id = gen_id("manual", unique=True)
|
46
|
-
return self
|
94
|
+
event: Event = field(default_factory=Event, compare=False)
|
95
|
+
ts: datetime = field(default_factory=get_dt_tznow, compare=False)
|
47
96
|
|
48
97
|
def set_run_id(self, running_id: str) -> Self:
|
49
98
|
"""Set a running ID.
|
@@ -51,7 +100,7 @@ class Result:
|
|
51
100
|
:param running_id: A running ID that want to update on this model.
|
52
101
|
:rtype: Self
|
53
102
|
"""
|
54
|
-
self.run_id = running_id
|
103
|
+
self.run_id: str = running_id
|
55
104
|
return self
|
56
105
|
|
57
106
|
def set_parent_run_id(self, running_id: str) -> Self:
|
@@ -63,10 +112,22 @@ class Result:
|
|
63
112
|
self.parent_run_id: str = running_id
|
64
113
|
return self
|
65
114
|
|
66
|
-
def catch(
|
67
|
-
|
68
|
-
|
69
|
-
|
115
|
+
def catch(
|
116
|
+
self,
|
117
|
+
status: int | Status,
|
118
|
+
context: DictData | None = None,
|
119
|
+
) -> Self:
|
120
|
+
"""Catch the status and context to this Result object. This method will
|
121
|
+
use between a child execution return a result, and it wants to pass
|
122
|
+
status and context to this object.
|
123
|
+
|
124
|
+
:param status:
|
125
|
+
:param context:
|
126
|
+
"""
|
127
|
+
self.__dict__["status"] = (
|
128
|
+
Status(status) if isinstance(status, int) else status
|
129
|
+
)
|
130
|
+
self.__dict__["context"].update(context or {})
|
70
131
|
return self
|
71
132
|
|
72
133
|
def receive(self, result: Result) -> Self:
|
@@ -82,22 +143,13 @@ class Result:
|
|
82
143
|
self.run_id = result.run_id
|
83
144
|
return self
|
84
145
|
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
exist.
|
146
|
+
@property
|
147
|
+
def trace(self) -> TraceLog:
|
148
|
+
"""Return TraceLog object that passing its running ID.
|
89
149
|
|
90
|
-
:rtype:
|
150
|
+
:rtype: TraceLog
|
91
151
|
"""
|
92
|
-
self.
|
152
|
+
return TraceLog(self.run_id)
|
93
153
|
|
94
|
-
|
95
|
-
|
96
|
-
self.__dict__["context"]["jobs"] = {}
|
97
|
-
|
98
|
-
self.__dict__["context"]["jobs"].update(result.context)
|
99
|
-
|
100
|
-
# NOTE: Update running ID from an incoming result.
|
101
|
-
self.parent_run_id: str = result.parent_run_id
|
102
|
-
self.run_id: str = result.run_id
|
103
|
-
return self
|
154
|
+
def alive_time(self) -> float: # pragma: no cov
|
155
|
+
return (get_dt_tznow() - self.ts).total_seconds()
|