ddeutil-workflow 0.0.32__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 +2 -2
- ddeutil/workflow/api/route.py +4 -3
- ddeutil/workflow/audit.py +261 -0
- ddeutil/workflow/conf.py +122 -265
- ddeutil/workflow/job.py +59 -52
- ddeutil/workflow/result.py +89 -37
- ddeutil/workflow/scheduler.py +7 -6
- ddeutil/workflow/stage.py +73 -54
- ddeutil/workflow/workflow.py +63 -64
- {ddeutil_workflow-0.0.32.dist-info → ddeutil_workflow-0.0.33.dist-info}/METADATA +29 -25
- ddeutil_workflow-0.0.33.dist-info/RECORD +26 -0
- ddeutil_workflow-0.0.32.dist-info/RECORD +0 -25
- {ddeutil_workflow-0.0.32.dist-info → ddeutil_workflow-0.0.33.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.32.dist-info → ddeutil_workflow-0.0.33.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.32.dist-info → ddeutil_workflow-0.0.33.dist-info}/top_level.txt +0 -0
ddeutil/workflow/job.py
CHANGED
@@ -38,12 +38,11 @@ from .exceptions import (
|
|
38
38
|
StageException,
|
39
39
|
UtilException,
|
40
40
|
)
|
41
|
-
from .result import Result
|
41
|
+
from .result import Result, Status
|
42
42
|
from .stage import Stage
|
43
43
|
from .templates import has_template
|
44
44
|
from .utils import (
|
45
45
|
cross_product,
|
46
|
-
cut_id,
|
47
46
|
dash2underscore,
|
48
47
|
filter_func,
|
49
48
|
gen_id,
|
@@ -222,6 +221,8 @@ class RunsOn(str, Enum):
|
|
222
221
|
|
223
222
|
local: str = "local"
|
224
223
|
docker: str = "docker"
|
224
|
+
self_hosted: str = "self_hosted"
|
225
|
+
k8s: str = "k8s"
|
225
226
|
|
226
227
|
|
227
228
|
class Job(BaseModel):
|
@@ -412,6 +413,7 @@ class Job(BaseModel):
|
|
412
413
|
params: DictData,
|
413
414
|
*,
|
414
415
|
run_id: str | None = None,
|
416
|
+
result: Result | None = None,
|
415
417
|
event: Event | None = None,
|
416
418
|
) -> Result:
|
417
419
|
"""Job Strategy execution with passing dynamic parameters from the
|
@@ -431,13 +433,18 @@ class Job(BaseModel):
|
|
431
433
|
This value will pass to the `matrix` key for templating.
|
432
434
|
:param params: A dynamic parameters that will deepcopy to the context.
|
433
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.
|
434
438
|
:param event: An event manager that pass to the PoolThreadExecutor.
|
435
439
|
|
436
440
|
:rtype: Result
|
437
441
|
"""
|
438
|
-
|
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
|
+
|
439
447
|
strategy_id: str = gen_id(strategy)
|
440
|
-
rs: Result = Result(run_id=run_id)
|
441
448
|
|
442
449
|
# PARAGRAPH:
|
443
450
|
#
|
@@ -458,18 +465,14 @@ class Job(BaseModel):
|
|
458
465
|
for stage in self.stages:
|
459
466
|
|
460
467
|
if stage.is_skipped(params=context):
|
461
|
-
|
462
|
-
f"({cut_id(run_id)}) [JOB]: Skip stage: {stage.iden!r}"
|
463
|
-
)
|
468
|
+
result.trace.info(f"[JOB]: Skip stage: {stage.iden!r}")
|
464
469
|
continue
|
465
470
|
|
466
|
-
|
467
|
-
f"({cut_id(run_id)}) [JOB]: Execute stage: {stage.iden!r}"
|
468
|
-
)
|
471
|
+
result.trace.info(f"[JOB]: Execute stage: {stage.iden!r}")
|
469
472
|
|
470
473
|
# NOTE: Logging a matrix that pass on this stage execution.
|
471
474
|
if strategy:
|
472
|
-
|
475
|
+
result.trace.info(f"[JOB]: ... Matrix: {strategy}")
|
473
476
|
|
474
477
|
# NOTE: Force stop this execution if event was set from main
|
475
478
|
# execution.
|
@@ -478,7 +481,7 @@ class Job(BaseModel):
|
|
478
481
|
"Job strategy was canceled from event that had set before "
|
479
482
|
"strategy execution."
|
480
483
|
)
|
481
|
-
return
|
484
|
+
return result.catch(
|
482
485
|
status=1,
|
483
486
|
context={
|
484
487
|
strategy_id: {
|
@@ -516,20 +519,18 @@ class Job(BaseModel):
|
|
516
519
|
try:
|
517
520
|
stage.set_outputs(
|
518
521
|
stage.handler_execute(
|
519
|
-
params=context, run_id=run_id
|
522
|
+
params=context, run_id=result.run_id
|
520
523
|
).context,
|
521
524
|
to=context,
|
522
525
|
)
|
523
526
|
except (StageException, UtilException) as err:
|
524
|
-
|
525
|
-
f"({cut_id(run_id)}) [JOB]: {err.__class__.__name__}: {err}"
|
526
|
-
)
|
527
|
+
result.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
|
527
528
|
if config.job_raise_error:
|
528
529
|
raise JobException(
|
529
530
|
f"Get stage execution error: {err.__class__.__name__}: "
|
530
531
|
f"{err}"
|
531
532
|
) from None
|
532
|
-
return
|
533
|
+
return result.catch(
|
533
534
|
status=1,
|
534
535
|
context={
|
535
536
|
strategy_id: {
|
@@ -544,8 +545,8 @@ class Job(BaseModel):
|
|
544
545
|
# NOTE: Remove the current stage object for saving memory.
|
545
546
|
del stage
|
546
547
|
|
547
|
-
return
|
548
|
-
status=
|
548
|
+
return result.catch(
|
549
|
+
status=Status.SUCCESS,
|
549
550
|
context={
|
550
551
|
strategy_id: {
|
551
552
|
"matrix": strategy,
|
@@ -554,36 +555,43 @@ class Job(BaseModel):
|
|
554
555
|
},
|
555
556
|
)
|
556
557
|
|
557
|
-
def execute(
|
558
|
+
def execute(
|
559
|
+
self,
|
560
|
+
params: DictData,
|
561
|
+
*,
|
562
|
+
run_id: str | None = None,
|
563
|
+
result: Result | None = None,
|
564
|
+
) -> Result:
|
558
565
|
"""Job execution with passing dynamic parameters from the workflow
|
559
566
|
execution. It will generate matrix values at the first step and run
|
560
567
|
multithread on this metrics to the ``stages`` field of this job.
|
561
568
|
|
562
569
|
:param params: An input parameters that use on job execution.
|
563
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.
|
564
573
|
|
565
574
|
:rtype: Result
|
566
575
|
"""
|
567
576
|
|
568
577
|
# NOTE: I use this condition because this method allow passing empty
|
569
578
|
# params and I do not want to create new dict object.
|
570
|
-
|
571
|
-
|
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)
|
572
582
|
|
573
583
|
# NOTE: Normal Job execution without parallel strategy matrix. It uses
|
574
584
|
# for-loop to control strategy execution sequentially.
|
575
585
|
if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
|
586
|
+
|
576
587
|
for strategy in self.strategy.make():
|
577
|
-
|
588
|
+
result: Result = self.execute_strategy(
|
578
589
|
strategy=strategy,
|
579
590
|
params=params,
|
580
|
-
|
591
|
+
result=result,
|
581
592
|
)
|
582
|
-
|
583
|
-
return
|
584
|
-
status=0,
|
585
|
-
context=context,
|
586
|
-
)
|
593
|
+
|
594
|
+
return result.catch(status=Status.SUCCESS)
|
587
595
|
|
588
596
|
# NOTE: Create event for cancel executor by trigger stop running event.
|
589
597
|
event: Event = Event()
|
@@ -600,23 +608,23 @@ class Job(BaseModel):
|
|
600
608
|
self.execute_strategy,
|
601
609
|
strategy=strategy,
|
602
610
|
params=params,
|
603
|
-
|
611
|
+
result=result,
|
604
612
|
event=event,
|
605
613
|
)
|
606
614
|
for strategy in self.strategy.make()
|
607
615
|
]
|
608
616
|
|
609
617
|
return (
|
610
|
-
self.__catch_fail_fast(event, futures=futures,
|
618
|
+
self.__catch_fail_fast(event, futures=futures, result=result)
|
611
619
|
if self.strategy.fail_fast
|
612
|
-
else self.__catch_all_completed(futures=futures,
|
620
|
+
else self.__catch_all_completed(futures=futures, result=result)
|
613
621
|
)
|
614
622
|
|
615
623
|
@staticmethod
|
616
624
|
def __catch_fail_fast(
|
617
625
|
event: Event,
|
618
626
|
futures: list[Future],
|
619
|
-
|
627
|
+
result: Result,
|
620
628
|
*,
|
621
629
|
timeout: int = 1800,
|
622
630
|
) -> Result:
|
@@ -627,14 +635,14 @@ class Job(BaseModel):
|
|
627
635
|
:param event: An event manager instance that able to set stopper on the
|
628
636
|
observing multithreading.
|
629
637
|
:param futures: A list of futures.
|
630
|
-
:param
|
638
|
+
:param result: (Result) A result object for keeping context and status
|
639
|
+
data.
|
631
640
|
:param timeout: A timeout to waiting all futures complete.
|
632
641
|
|
633
642
|
:rtype: Result
|
634
643
|
"""
|
635
|
-
rs_final: Result = Result(run_id=run_id)
|
636
644
|
context: DictData = {}
|
637
|
-
status:
|
645
|
+
status: Status = Status.SUCCESS
|
638
646
|
|
639
647
|
# NOTE: Get results from a collection of tasks with a timeout that has
|
640
648
|
# the first exception.
|
@@ -644,7 +652,7 @@ class Job(BaseModel):
|
|
644
652
|
nd: str = (
|
645
653
|
f", the strategies do not run is {not_done}" if not_done else ""
|
646
654
|
)
|
647
|
-
|
655
|
+
result.trace.debug(f"[JOB]: Strategy is set Fail Fast{nd}")
|
648
656
|
|
649
657
|
# NOTE:
|
650
658
|
# Stop all running tasks with setting the event manager and cancel
|
@@ -660,10 +668,9 @@ class Job(BaseModel):
|
|
660
668
|
|
661
669
|
# NOTE: Handle the first exception from feature
|
662
670
|
if err := future.exception():
|
663
|
-
status:
|
664
|
-
|
665
|
-
f"
|
666
|
-
f"{future.exception()}"
|
671
|
+
status: Status = Status.FAILED
|
672
|
+
result.trace.error(
|
673
|
+
f"[JOB]: Fail-fast catching:\n\t{future.exception()}"
|
667
674
|
)
|
668
675
|
context.update(
|
669
676
|
{
|
@@ -674,36 +681,36 @@ class Job(BaseModel):
|
|
674
681
|
continue
|
675
682
|
|
676
683
|
# NOTE: Update the result context to main job context.
|
677
|
-
|
684
|
+
future.result()
|
678
685
|
|
679
|
-
return
|
686
|
+
return result.catch(status=status, context=context)
|
680
687
|
|
681
688
|
@staticmethod
|
682
689
|
def __catch_all_completed(
|
683
690
|
futures: list[Future],
|
684
|
-
|
691
|
+
result: Result,
|
685
692
|
*,
|
686
693
|
timeout: int = 1800,
|
687
694
|
) -> Result:
|
688
695
|
"""Job parallel pool futures catching with all-completed mode.
|
689
696
|
|
690
697
|
:param futures: A list of futures.
|
691
|
-
:param
|
698
|
+
:param result: (Result) A result object for keeping context and status
|
699
|
+
data.
|
692
700
|
:param timeout: A timeout to waiting all futures complete.
|
693
701
|
|
694
702
|
:rtype: Result
|
695
703
|
"""
|
696
|
-
rs_final: Result = Result(run_id=run_id)
|
697
704
|
context: DictData = {}
|
698
|
-
status:
|
705
|
+
status: Status = Status.SUCCESS
|
699
706
|
|
700
707
|
for future in as_completed(futures, timeout=timeout):
|
701
708
|
try:
|
702
|
-
|
709
|
+
future.result()
|
703
710
|
except JobException as err:
|
704
|
-
status =
|
705
|
-
|
706
|
-
f"
|
711
|
+
status = Status.FAILED
|
712
|
+
result.trace.error(
|
713
|
+
f"[JOB]: All-completed catching:\n\t"
|
707
714
|
f"{err.__class__.__name__}:\n\t{err}"
|
708
715
|
)
|
709
716
|
context.update(
|
@@ -713,4 +720,4 @@ class Job(BaseModel):
|
|
713
720
|
},
|
714
721
|
)
|
715
722
|
|
716
|
-
return
|
723
|
+
return result.catch(status=status, context=context)
|
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()
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -51,7 +51,8 @@ except ImportError: # pragma: no cov
|
|
51
51
|
|
52
52
|
from .__cron import CronRunner
|
53
53
|
from .__types import DictData, TupleStr
|
54
|
-
from .
|
54
|
+
from .audit import Audit, get_audit
|
55
|
+
from .conf import Loader, config, get_logger
|
55
56
|
from .cron import On
|
56
57
|
from .exceptions import ScheduleException, WorkflowException
|
57
58
|
from .result import Result
|
@@ -313,7 +314,7 @@ class Schedule(BaseModel):
|
|
313
314
|
*,
|
314
315
|
stop: datetime | None = None,
|
315
316
|
externals: DictData | None = None,
|
316
|
-
log: type[
|
317
|
+
log: type[Audit] | None = None,
|
317
318
|
) -> None: # pragma: no cov
|
318
319
|
"""Pending this schedule tasks with the schedule package.
|
319
320
|
|
@@ -330,7 +331,7 @@ class Schedule(BaseModel):
|
|
330
331
|
) from None
|
331
332
|
|
332
333
|
# NOTE: Get default logging.
|
333
|
-
log: type[
|
334
|
+
log: type[Audit] = log or get_audit()
|
334
335
|
scheduler: Scheduler = Scheduler()
|
335
336
|
|
336
337
|
# NOTE: Create the start and stop datetime.
|
@@ -450,7 +451,7 @@ def schedule_task(
|
|
450
451
|
stop: datetime,
|
451
452
|
queue: dict[str, ReleaseQueue],
|
452
453
|
threads: ReleaseThreads,
|
453
|
-
log: type[
|
454
|
+
log: type[Audit],
|
454
455
|
) -> type[CancelJob] | None:
|
455
456
|
"""Schedule task function that generate thread of workflow task release
|
456
457
|
method in background. This function do the same logic as the workflow poke
|
@@ -573,7 +574,7 @@ def schedule_control(
|
|
573
574
|
stop: datetime | None = None,
|
574
575
|
externals: DictData | None = None,
|
575
576
|
*,
|
576
|
-
log: type[
|
577
|
+
log: type[Audit] | None = None,
|
577
578
|
) -> list[str]: # pragma: no cov
|
578
579
|
"""Scheduler control function that run the chuck of schedules every minute
|
579
580
|
and this function release monitoring thread for tracking undead thread in
|
@@ -596,7 +597,7 @@ def schedule_control(
|
|
596
597
|
) from None
|
597
598
|
|
598
599
|
# NOTE: Get default logging.
|
599
|
-
log: type[
|
600
|
+
log: type[Audit] = log or get_audit()
|
600
601
|
scheduler: Scheduler = Scheduler()
|
601
602
|
|
602
603
|
# NOTE: Create the start and stop datetime.
|