ddeutil-workflow 0.0.18__py3-none-any.whl → 0.0.20__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/__cron.py +29 -2
- ddeutil/workflow/__init__.py +9 -4
- ddeutil/workflow/conf.py +49 -40
- ddeutil/workflow/exceptions.py +4 -0
- ddeutil/workflow/job.py +58 -45
- ddeutil/workflow/on.py +4 -2
- ddeutil/workflow/scheduler.py +117 -947
- ddeutil/workflow/stage.py +92 -66
- ddeutil/workflow/utils.py +61 -43
- ddeutil/workflow/workflow.py +1084 -0
- {ddeutil_workflow-0.0.18.dist-info → ddeutil_workflow-0.0.20.dist-info}/METADATA +12 -12
- ddeutil_workflow-0.0.20.dist-info/RECORD +22 -0
- {ddeutil_workflow-0.0.18.dist-info → ddeutil_workflow-0.0.20.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.18.dist-info/RECORD +0 -21
- {ddeutil_workflow-0.0.18.dist-info → ddeutil_workflow-0.0.20.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.18.dist-info → ddeutil_workflow-0.0.20.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.18.dist-info → ddeutil_workflow-0.0.20.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.20"
|
ddeutil/workflow/__cron.py
CHANGED
@@ -646,12 +646,27 @@ class CronJob:
|
|
646
646
|
|
647
647
|
:param date: An initial date that want to mark as the start point.
|
648
648
|
:param tz: A string timezone that want to change on runner.
|
649
|
+
|
649
650
|
:rtype: CronRunner
|
650
651
|
"""
|
651
652
|
return CronRunner(self, date, tz=tz)
|
652
653
|
|
653
654
|
|
654
655
|
class CronJobYear(CronJob):
|
656
|
+
"""The Cron Job Converter with Year extension object that generate datetime
|
657
|
+
dimension of cron job schedule format,
|
658
|
+
|
659
|
+
* * * * * * <command to execute>
|
660
|
+
|
661
|
+
(i) minute (0 - 59)
|
662
|
+
(ii) hour (0 - 23)
|
663
|
+
(iii) day of the month (1 - 31)
|
664
|
+
(iv) month (1 - 12)
|
665
|
+
(v) day of the week (0 - 6) (Sunday to Saturday; 7 is also Sunday
|
666
|
+
on some systems)
|
667
|
+
(vi) year (1990 - 2100)
|
668
|
+
"""
|
669
|
+
|
655
670
|
cron_length = 6
|
656
671
|
cron_units = CRON_UNITS_YEAR
|
657
672
|
|
@@ -704,9 +719,17 @@ class CronRunner:
|
|
704
719
|
else:
|
705
720
|
self.date: datetime = datetime.now(tz=self.tz)
|
706
721
|
|
722
|
+
# NOTE: Add one second if the microsecond value more than 0.
|
723
|
+
if self.date.microsecond > 0:
|
724
|
+
self.date: datetime = self.date.replace(microsecond=0) + timedelta(
|
725
|
+
seconds=1
|
726
|
+
)
|
727
|
+
|
707
728
|
# NOTE: Add one minute if the second value more than 0.
|
708
729
|
if self.date.second > 0:
|
709
|
-
self.date: datetime = self.date + timedelta(
|
730
|
+
self.date: datetime = self.date.replace(second=0) + timedelta(
|
731
|
+
minutes=1
|
732
|
+
)
|
710
733
|
|
711
734
|
self.__start_date: datetime = self.date
|
712
735
|
self.cron: CronJob | CronJobYear = cron
|
@@ -752,7 +775,7 @@ class CronRunner:
|
|
752
775
|
not self.__shift_date(mode, reverse)
|
753
776
|
for mode in ("year", "month", "day", "hour", "minute")
|
754
777
|
):
|
755
|
-
return copy.deepcopy(self.date
|
778
|
+
return copy.deepcopy(self.date)
|
756
779
|
|
757
780
|
raise RecursionError("Unable to find execution time for schedule")
|
758
781
|
|
@@ -801,6 +824,10 @@ class CronRunner:
|
|
801
824
|
# NOTE: Replace date that less than it mode to zero.
|
802
825
|
self.date: datetime = replace_date(self.date, mode, reverse=reverse)
|
803
826
|
|
827
|
+
# NOTE: Replace second and microsecond values that change from
|
828
|
+
# the replace_date func with reverse flag.
|
829
|
+
self.date: datetime = self.date.replace(second=0, microsecond=0)
|
830
|
+
|
804
831
|
if current_value != getattr(self.date, switch[mode]):
|
805
832
|
return mode != "month"
|
806
833
|
|
ddeutil/workflow/__init__.py
CHANGED
@@ -15,7 +15,10 @@ from .exceptions import (
|
|
15
15
|
UtilException,
|
16
16
|
WorkflowException,
|
17
17
|
)
|
18
|
-
from .job import
|
18
|
+
from .job import (
|
19
|
+
Job,
|
20
|
+
Strategy,
|
21
|
+
)
|
19
22
|
from .on import (
|
20
23
|
On,
|
21
24
|
YearOn,
|
@@ -24,8 +27,6 @@ from .on import (
|
|
24
27
|
from .scheduler import (
|
25
28
|
Schedule,
|
26
29
|
ScheduleWorkflow,
|
27
|
-
Workflow,
|
28
|
-
WorkflowTaskData,
|
29
30
|
)
|
30
31
|
from .stage import (
|
31
32
|
BashStage,
|
@@ -34,7 +35,7 @@ from .stage import (
|
|
34
35
|
PyStage,
|
35
36
|
Stage,
|
36
37
|
TriggerStage,
|
37
|
-
|
38
|
+
extract_hook,
|
38
39
|
)
|
39
40
|
from .utils import (
|
40
41
|
FILTERS,
|
@@ -70,3 +71,7 @@ from .utils import (
|
|
70
71
|
str2template,
|
71
72
|
tag,
|
72
73
|
)
|
74
|
+
from .workflow import (
|
75
|
+
Workflow,
|
76
|
+
WorkflowTaskData,
|
77
|
+
)
|
ddeutil/workflow/conf.py
CHANGED
@@ -33,6 +33,29 @@ load_dotenv()
|
|
33
33
|
env = os.getenv
|
34
34
|
|
35
35
|
|
36
|
+
@lru_cache
|
37
|
+
def get_logger(name: str):
|
38
|
+
"""Return logger object with an input module name.
|
39
|
+
|
40
|
+
:param name: A module name that want to log.
|
41
|
+
"""
|
42
|
+
lg = logging.getLogger(name)
|
43
|
+
formatter = logging.Formatter(
|
44
|
+
fmt=(
|
45
|
+
"%(asctime)s.%(msecs)03d (%(name)-10s, %(process)-5d, "
|
46
|
+
"%(thread)-5d) [%(levelname)-7s] %(message)-120s "
|
47
|
+
"(%(filename)s:%(lineno)s)"
|
48
|
+
),
|
49
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
50
|
+
)
|
51
|
+
stream = logging.StreamHandler()
|
52
|
+
stream.setFormatter(formatter)
|
53
|
+
lg.addHandler(stream)
|
54
|
+
|
55
|
+
lg.setLevel(logging.DEBUG if config.debug else logging.INFO)
|
56
|
+
return lg
|
57
|
+
|
58
|
+
|
36
59
|
class Config:
|
37
60
|
"""Config object for keeping application configuration on current session
|
38
61
|
without changing when if the application still running.
|
@@ -98,12 +121,14 @@ class Config:
|
|
98
121
|
os.getenv("WORKFLOW_API_ENABLE_ROUTE_SCHEDULE", "true")
|
99
122
|
)
|
100
123
|
|
101
|
-
def __init__(self):
|
124
|
+
def __init__(self) -> None:
|
125
|
+
# VALIDATE: the MAX_JOB_PARALLEL value should not less than 0.
|
102
126
|
if self.max_job_parallel < 0:
|
103
127
|
raise ValueError(
|
104
128
|
f"``MAX_JOB_PARALLEL`` should more than 0 but got "
|
105
129
|
f"{self.max_job_parallel}."
|
106
130
|
)
|
131
|
+
|
107
132
|
try:
|
108
133
|
self.stop_boundary_delta: timedelta = timedelta(
|
109
134
|
**json.loads(self.stop_boundary_delta_str)
|
@@ -195,6 +220,7 @@ class SimLoad:
|
|
195
220
|
:param conf: A config object.
|
196
221
|
:param include:
|
197
222
|
:param exclude:
|
223
|
+
|
198
224
|
:rtype: Iterator[tuple[str, DictData]]
|
199
225
|
"""
|
200
226
|
exclude: list[str] = exclude or []
|
@@ -247,12 +273,14 @@ class Loader(SimLoad):
|
|
247
273
|
include: list[str] | None = None,
|
248
274
|
exclude: list[str] | None = None,
|
249
275
|
**kwargs,
|
250
|
-
) -> DictData:
|
276
|
+
) -> Iterator[tuple[str, DictData]]:
|
251
277
|
"""Override the find class method from the Simple Loader object.
|
252
278
|
|
253
279
|
:param obj: A object that want to validate matching before return.
|
254
280
|
:param include:
|
255
281
|
:param exclude:
|
282
|
+
|
283
|
+
:rtype: Iterator[tuple[str, DictData]]
|
256
284
|
"""
|
257
285
|
return super().finds(
|
258
286
|
obj=obj, conf=Config(), include=include, exclude=exclude
|
@@ -268,6 +296,7 @@ def get_type(t: str, params: Config) -> AnyModelType:
|
|
268
296
|
:param t: A importable type string.
|
269
297
|
:param params: A config parameters that use registry to search this
|
270
298
|
type.
|
299
|
+
|
271
300
|
:rtype: AnyModelType
|
272
301
|
"""
|
273
302
|
try:
|
@@ -283,29 +312,7 @@ def get_type(t: str, params: Config) -> AnyModelType:
|
|
283
312
|
|
284
313
|
|
285
314
|
config = Config()
|
286
|
-
|
287
|
-
|
288
|
-
@lru_cache
|
289
|
-
def get_logger(name: str):
|
290
|
-
"""Return logger object with an input module name.
|
291
|
-
|
292
|
-
:param name: A module name that want to log.
|
293
|
-
"""
|
294
|
-
logger = logging.getLogger(name)
|
295
|
-
formatter = logging.Formatter(
|
296
|
-
fmt=(
|
297
|
-
"%(asctime)s.%(msecs)03d (%(name)-10s, %(process)-5d, "
|
298
|
-
"%(thread)-5d) [%(levelname)-7s] %(message)-120s "
|
299
|
-
"(%(filename)s:%(lineno)s)"
|
300
|
-
),
|
301
|
-
datefmt="%Y-%m-%d %H:%M:%S",
|
302
|
-
)
|
303
|
-
stream = logging.StreamHandler()
|
304
|
-
stream.setFormatter(formatter)
|
305
|
-
logger.addHandler(stream)
|
306
|
-
|
307
|
-
logger.setLevel(logging.DEBUG if config.debug else logging.INFO)
|
308
|
-
return logger
|
315
|
+
logger = get_logger("ddeutil.workflow")
|
309
316
|
|
310
317
|
|
311
318
|
class BaseLog(BaseModel, ABC):
|
@@ -315,8 +322,8 @@ class BaseLog(BaseModel, ABC):
|
|
315
322
|
"""
|
316
323
|
|
317
324
|
name: str = Field(description="A workflow name.")
|
318
|
-
on: str = Field(description="A cronjob string of this piepline schedule.")
|
319
325
|
release: datetime = Field(description="A release datetime.")
|
326
|
+
type: str = Field(description="A running type before logging.")
|
320
327
|
context: DictData = Field(
|
321
328
|
default_factory=dict,
|
322
329
|
description=(
|
@@ -366,6 +373,8 @@ class FileLog(BaseLog):
|
|
366
373
|
workflow name.
|
367
374
|
|
368
375
|
:param name: A workflow name that want to search release logging data.
|
376
|
+
|
377
|
+
:rtype: Iterator[Self]
|
369
378
|
"""
|
370
379
|
pointer: Path = config.root_path / f"./logs/workflow={name}"
|
371
380
|
if not pointer.exists():
|
@@ -387,6 +396,9 @@ class FileLog(BaseLog):
|
|
387
396
|
workflow name and release values. If a release does not pass to an input
|
388
397
|
argument, it will return the latest release from the current log path.
|
389
398
|
|
399
|
+
:param name:
|
400
|
+
:param release:
|
401
|
+
|
390
402
|
:raise FileNotFoundError:
|
391
403
|
:raise NotImplementedError:
|
392
404
|
|
@@ -411,21 +423,17 @@ class FileLog(BaseLog):
|
|
411
423
|
return cls.model_validate(obj=json.load(f))
|
412
424
|
|
413
425
|
@classmethod
|
414
|
-
def is_pointed(
|
415
|
-
|
416
|
-
|
417
|
-
release: datetime,
|
418
|
-
*,
|
419
|
-
queue: list[datetime] | None = None,
|
420
|
-
) -> bool:
|
421
|
-
"""Check this log already point in the destination.
|
426
|
+
def is_pointed(cls, name: str, release: datetime) -> bool:
|
427
|
+
"""Check the release log already pointed or created at the destination
|
428
|
+
log path.
|
422
429
|
|
423
430
|
:param name: A workflow name.
|
424
431
|
:param release: A release datetime.
|
425
|
-
|
426
|
-
|
432
|
+
|
433
|
+
:rtype: bool
|
434
|
+
:return: Return False if the release log was not pointed or created.
|
427
435
|
"""
|
428
|
-
# NOTE:
|
436
|
+
# NOTE: Return False if enable writing log flag does not set.
|
429
437
|
if not config.enable_write_log:
|
430
438
|
return False
|
431
439
|
|
@@ -434,9 +442,7 @@ class FileLog(BaseLog):
|
|
434
442
|
name=name, release=release
|
435
443
|
)
|
436
444
|
|
437
|
-
|
438
|
-
return pointer.exists()
|
439
|
-
return pointer.exists() or (release in queue)
|
445
|
+
return pointer.exists()
|
440
446
|
|
441
447
|
def pointer(self) -> Path:
|
442
448
|
"""Return release directory path that was generated from model data.
|
@@ -459,6 +465,9 @@ class FileLog(BaseLog):
|
|
459
465
|
if not config.enable_write_log:
|
460
466
|
return self
|
461
467
|
|
468
|
+
logger.debug(
|
469
|
+
f"({self.run_id}) [LOG]: Start writing log: {self.name!r}."
|
470
|
+
)
|
462
471
|
log_file: Path = self.pointer() / f"{self.run_id}.log"
|
463
472
|
log_file.write_text(
|
464
473
|
json.dumps(
|
ddeutil/workflow/exceptions.py
CHANGED
@@ -3,6 +3,10 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
+
"""Exception objects for this package do not do anything because I want to
|
7
|
+
create the lightweight workflow package. So, this module do just a exception
|
8
|
+
annotate for handle error only.
|
9
|
+
"""
|
6
10
|
from __future__ import annotations
|
7
11
|
|
8
12
|
|
ddeutil/workflow/job.py
CHANGED
@@ -19,6 +19,7 @@ from concurrent.futures import (
|
|
19
19
|
as_completed,
|
20
20
|
wait,
|
21
21
|
)
|
22
|
+
from enum import Enum
|
22
23
|
from functools import lru_cache
|
23
24
|
from textwrap import dedent
|
24
25
|
from threading import Event
|
@@ -68,10 +69,14 @@ def make(
|
|
68
69
|
"""Make a list of product of matrix values that already filter with
|
69
70
|
exclude matrix and add specific matrix with include.
|
70
71
|
|
72
|
+
This function use the `lru_cache` decorator function increase
|
73
|
+
performance for duplicate matrix value scenario.
|
74
|
+
|
71
75
|
:param matrix: A matrix values that want to cross product to possible
|
72
76
|
parallelism values.
|
73
77
|
:param include: A list of additional matrix that want to adds-in.
|
74
78
|
:param exclude: A list of exclude matrix that want to filter-out.
|
79
|
+
|
75
80
|
:rtype: list[DictStr]
|
76
81
|
"""
|
77
82
|
# NOTE: If it does not set matrix, it will return list of an empty dict.
|
@@ -198,6 +203,18 @@ class Strategy(BaseModel):
|
|
198
203
|
return make(self.matrix, self.include, self.exclude)
|
199
204
|
|
200
205
|
|
206
|
+
class TriggerRules(str, Enum):
|
207
|
+
"""Trigger Rules enum object."""
|
208
|
+
|
209
|
+
all_success: str = "all_success"
|
210
|
+
all_failed: str = "all_failed"
|
211
|
+
all_done: str = "all_done"
|
212
|
+
one_failed: str = "one_failed"
|
213
|
+
one_success: str = "one_success"
|
214
|
+
none_failed: str = "none_failed"
|
215
|
+
none_skipped: str = "none_skipped"
|
216
|
+
|
217
|
+
|
201
218
|
class Job(BaseModel):
|
202
219
|
"""Job Pydantic model object (group of stages).
|
203
220
|
|
@@ -245,6 +262,11 @@ class Job(BaseModel):
|
|
245
262
|
default_factory=list,
|
246
263
|
description="A list of Stage of this job.",
|
247
264
|
)
|
265
|
+
trigger_rule: TriggerRules = Field(
|
266
|
+
default=TriggerRules.all_success,
|
267
|
+
description="A trigger rule of tracking needed jobs.",
|
268
|
+
serialization_alias="trigger-rule",
|
269
|
+
)
|
248
270
|
needs: list[str] = Field(
|
249
271
|
default_factory=list,
|
250
272
|
description="A list of the job ID that want to run before this job.",
|
@@ -253,12 +275,6 @@ class Job(BaseModel):
|
|
253
275
|
default_factory=Strategy,
|
254
276
|
description="A strategy matrix that want to generate.",
|
255
277
|
)
|
256
|
-
run_id: Optional[str] = Field(
|
257
|
-
default=None,
|
258
|
-
description="A running job ID.",
|
259
|
-
repr=False,
|
260
|
-
exclude=True,
|
261
|
-
)
|
262
278
|
|
263
279
|
@model_validator(mode="before")
|
264
280
|
def __prepare_keys__(cls, values: DictData) -> DictData:
|
@@ -269,6 +285,7 @@ class Job(BaseModel):
|
|
269
285
|
:rtype: DictData
|
270
286
|
"""
|
271
287
|
dash2underscore("runs-on", values)
|
288
|
+
dash2underscore("trigger-rule", values)
|
272
289
|
return values
|
273
290
|
|
274
291
|
@field_validator("desc", mode="after")
|
@@ -298,29 +315,17 @@ class Job(BaseModel):
|
|
298
315
|
return value
|
299
316
|
|
300
317
|
@model_validator(mode="after")
|
301
|
-
def
|
302
|
-
"""
|
318
|
+
def __validate_job_id__(self) -> Self:
|
319
|
+
"""Validate job id should not have templating syntax.
|
303
320
|
|
304
321
|
:rtype: Self
|
305
322
|
"""
|
306
|
-
if self.run_id is None:
|
307
|
-
self.run_id = gen_id(self.id or "", unique=True)
|
308
|
-
|
309
323
|
# VALIDATE: Validate job id should not dynamic with params template.
|
310
324
|
if has_template(self.id):
|
311
325
|
raise ValueError("Job ID should not has any template.")
|
312
326
|
|
313
327
|
return self
|
314
328
|
|
315
|
-
def get_running_id(self, run_id: str) -> Self:
|
316
|
-
"""Return Job model object that changing job running ID with an
|
317
|
-
input running ID.
|
318
|
-
|
319
|
-
:param run_id: A replace job running ID.
|
320
|
-
:rtype: Self
|
321
|
-
"""
|
322
|
-
return self.model_copy(update={"run_id": run_id})
|
323
|
-
|
324
329
|
def stage(self, stage_id: str) -> Stage:
|
325
330
|
"""Return stage model that match with an input stage ID.
|
326
331
|
|
@@ -371,8 +376,6 @@ class Job(BaseModel):
|
|
371
376
|
# NOTE: If the job ID did not set, it will use index of jobs key
|
372
377
|
# instead.
|
373
378
|
_id: str = self.id or str(len(to["jobs"]) + 1)
|
374
|
-
|
375
|
-
logger.debug(f"({self.run_id}) [JOB]: Set outputs on: {_id}")
|
376
379
|
to["jobs"][_id] = (
|
377
380
|
{"strategies": output}
|
378
381
|
if self.strategy.is_set()
|
@@ -384,6 +387,7 @@ class Job(BaseModel):
|
|
384
387
|
self,
|
385
388
|
strategy: DictData,
|
386
389
|
params: DictData,
|
390
|
+
run_id: str | None = None,
|
387
391
|
*,
|
388
392
|
event: Event | None = None,
|
389
393
|
) -> Result:
|
@@ -399,10 +403,12 @@ class Job(BaseModel):
|
|
399
403
|
|
400
404
|
:param strategy: A metrix strategy value.
|
401
405
|
:param params: A dynamic parameters.
|
406
|
+
:param run_id: A job running ID for this strategy execution.
|
402
407
|
:param event: An manger event that pass to the PoolThreadExecutor.
|
403
408
|
|
404
409
|
:rtype: Result
|
405
410
|
"""
|
411
|
+
run_id: str = run_id or gen_id(self.id or "", unique=True)
|
406
412
|
strategy_id: str = gen_id(strategy)
|
407
413
|
|
408
414
|
# PARAGRAPH:
|
@@ -423,22 +429,17 @@ class Job(BaseModel):
|
|
423
429
|
# IMPORTANT: The stage execution only run sequentially one-by-one.
|
424
430
|
for stage in self.stages:
|
425
431
|
|
426
|
-
# IMPORTANT: Change any stage running IDs to this job running ID.
|
427
|
-
stage: Stage = stage.get_running_id(self.run_id)
|
428
|
-
|
429
|
-
name: str = stage.id or stage.name
|
430
|
-
|
431
432
|
if stage.is_skipped(params=context):
|
432
|
-
logger.info(f"({
|
433
|
+
logger.info(f"({run_id}) [JOB]: Skip stage: {stage.iden!r}")
|
433
434
|
continue
|
434
435
|
|
435
436
|
logger.info(
|
436
|
-
f"({
|
437
|
+
f"({run_id}) [JOB]: Start execute the stage: {stage.iden!r}"
|
437
438
|
)
|
438
439
|
|
439
440
|
# NOTE: Logging a matrix that pass on this stage execution.
|
440
441
|
if strategy:
|
441
|
-
logger.info(f"({
|
442
|
+
logger.info(f"({run_id}) [JOB]: Matrix: {strategy}")
|
442
443
|
|
443
444
|
# NOTE: Force stop this execution if event was set from main
|
444
445
|
# execution.
|
@@ -463,6 +464,7 @@ class Job(BaseModel):
|
|
463
464
|
),
|
464
465
|
},
|
465
466
|
},
|
467
|
+
run_id=run_id,
|
466
468
|
)
|
467
469
|
|
468
470
|
# PARAGRAPH:
|
@@ -485,12 +487,12 @@ class Job(BaseModel):
|
|
485
487
|
#
|
486
488
|
try:
|
487
489
|
stage.set_outputs(
|
488
|
-
stage.execute(params=context).context,
|
490
|
+
stage.execute(params=context, run_id=run_id).context,
|
489
491
|
to=context,
|
490
492
|
)
|
491
493
|
except (StageException, UtilException) as err:
|
492
494
|
logger.error(
|
493
|
-
f"({
|
495
|
+
f"({run_id}) [JOB]: {err.__class__.__name__}: {err}"
|
494
496
|
)
|
495
497
|
if config.job_raise_error:
|
496
498
|
raise JobException(
|
@@ -507,10 +509,10 @@ class Job(BaseModel):
|
|
507
509
|
"error_message": f"{err.__class__.__name__}: {err}",
|
508
510
|
},
|
509
511
|
},
|
512
|
+
run_id=run_id,
|
510
513
|
)
|
511
514
|
|
512
|
-
# NOTE: Remove the current stage object
|
513
|
-
# ``get_running_id`` method for saving memory.
|
515
|
+
# NOTE: Remove the current stage object.
|
514
516
|
del stage
|
515
517
|
|
516
518
|
return Result(
|
@@ -521,20 +523,23 @@ class Job(BaseModel):
|
|
521
523
|
"stages": filter_func(context.pop("stages", {})),
|
522
524
|
},
|
523
525
|
},
|
526
|
+
run_id=run_id,
|
524
527
|
)
|
525
528
|
|
526
|
-
def execute(self, params: DictData | None = None) -> Result:
|
529
|
+
def execute(self, params: DictData, run_id: str | None = None) -> Result:
|
527
530
|
"""Job execution with passing dynamic parameters from the workflow
|
528
531
|
execution. It will generate matrix values at the first step and run
|
529
532
|
multithread on this metrics to the ``stages`` field of this job.
|
530
533
|
|
531
534
|
:param params: An input parameters that use on job execution.
|
535
|
+
:param run_id: A job running ID for this execution.
|
536
|
+
|
532
537
|
:rtype: Result
|
533
538
|
"""
|
534
539
|
|
535
540
|
# NOTE: I use this condition because this method allow passing empty
|
536
541
|
# params and I do not want to create new dict object.
|
537
|
-
|
542
|
+
run_id: str = run_id or gen_id(self.id or "", unique=True)
|
538
543
|
context: DictData = {}
|
539
544
|
|
540
545
|
# NOTE: Normal Job execution without parallel strategy.
|
@@ -543,6 +548,7 @@ class Job(BaseModel):
|
|
543
548
|
rs: Result = self.execute_strategy(
|
544
549
|
strategy=strategy,
|
545
550
|
params=params,
|
551
|
+
run_id=run_id,
|
546
552
|
)
|
547
553
|
context.update(rs.context)
|
548
554
|
return Result(
|
@@ -565,6 +571,7 @@ class Job(BaseModel):
|
|
565
571
|
self.execute_strategy,
|
566
572
|
strategy=strategy,
|
567
573
|
params=params,
|
574
|
+
run_id=run_id,
|
568
575
|
event=event,
|
569
576
|
)
|
570
577
|
for strategy in self.strategy.make()
|
@@ -572,15 +579,18 @@ class Job(BaseModel):
|
|
572
579
|
|
573
580
|
# NOTE: Dynamic catching futures object with fail-fast flag.
|
574
581
|
return (
|
575
|
-
self.__catch_fail_fast(
|
582
|
+
self.__catch_fail_fast(
|
583
|
+
event=event, futures=futures, run_id=run_id
|
584
|
+
)
|
576
585
|
if self.strategy.fail_fast
|
577
|
-
else self.__catch_all_completed(futures=futures)
|
586
|
+
else self.__catch_all_completed(futures=futures, run_id=run_id)
|
578
587
|
)
|
579
588
|
|
589
|
+
@staticmethod
|
580
590
|
def __catch_fail_fast(
|
581
|
-
self,
|
582
591
|
event: Event,
|
583
592
|
futures: list[Future],
|
593
|
+
run_id: str,
|
584
594
|
*,
|
585
595
|
timeout: int = 1800,
|
586
596
|
result_timeout: int = 60,
|
@@ -592,6 +602,7 @@ class Job(BaseModel):
|
|
592
602
|
:param event: An event manager instance that able to set stopper on the
|
593
603
|
observing thread/process.
|
594
604
|
:param futures: A list of futures.
|
605
|
+
:param run_id: A job running ID from execution.
|
595
606
|
:param timeout: A timeout to waiting all futures complete.
|
596
607
|
:param result_timeout: A timeout of getting result from the future
|
597
608
|
instance when it was running completely.
|
@@ -611,7 +622,7 @@ class Job(BaseModel):
|
|
611
622
|
nd: str = (
|
612
623
|
f", the strategies do not run is {not_done}" if not_done else ""
|
613
624
|
)
|
614
|
-
logger.debug(f"({
|
625
|
+
logger.debug(f"({run_id}) [JOB]: Strategy is set Fail Fast{nd}")
|
615
626
|
|
616
627
|
# NOTE:
|
617
628
|
# Stop all running tasks with setting the event manager and cancel
|
@@ -627,7 +638,7 @@ class Job(BaseModel):
|
|
627
638
|
if err := future.exception():
|
628
639
|
status: int = 1
|
629
640
|
logger.error(
|
630
|
-
f"({
|
641
|
+
f"({run_id}) [JOB]: One stage failed with: "
|
631
642
|
f"{future.exception()}, shutting down this future."
|
632
643
|
)
|
633
644
|
context.update(
|
@@ -643,9 +654,10 @@ class Job(BaseModel):
|
|
643
654
|
|
644
655
|
return rs_final.catch(status=status, context=context)
|
645
656
|
|
657
|
+
@staticmethod
|
646
658
|
def __catch_all_completed(
|
647
|
-
self,
|
648
659
|
futures: list[Future],
|
660
|
+
run_id: str,
|
649
661
|
*,
|
650
662
|
timeout: int = 1800,
|
651
663
|
result_timeout: int = 60,
|
@@ -654,6 +666,7 @@ class Job(BaseModel):
|
|
654
666
|
|
655
667
|
:param futures: A list of futures that want to catch all completed
|
656
668
|
result.
|
669
|
+
:param run_id: A job running ID from execution.
|
657
670
|
:param timeout: A timeout to waiting all futures complete.
|
658
671
|
:param result_timeout: A timeout of getting result from the future
|
659
672
|
instance when it was running completely.
|
@@ -668,7 +681,7 @@ class Job(BaseModel):
|
|
668
681
|
except TimeoutError: # pragma: no cov
|
669
682
|
status = 1
|
670
683
|
logger.warning(
|
671
|
-
f"({
|
684
|
+
f"({run_id}) [JOB]: Task is hanging. Attempting to "
|
672
685
|
f"kill."
|
673
686
|
)
|
674
687
|
future.cancel()
|
@@ -679,11 +692,11 @@ class Job(BaseModel):
|
|
679
692
|
if not future.cancelled()
|
680
693
|
else "Task canceled successfully."
|
681
694
|
)
|
682
|
-
logger.warning(f"({
|
695
|
+
logger.warning(f"({run_id}) [JOB]: {stmt}")
|
683
696
|
except JobException as err:
|
684
697
|
status = 1
|
685
698
|
logger.error(
|
686
|
-
f"({
|
699
|
+
f"({run_id}) [JOB]: Get stage exception with "
|
687
700
|
f"fail-fast does not set;\n{err.__class__.__name__}:\n\t"
|
688
701
|
f"{err}"
|
689
702
|
)
|
ddeutil/workflow/on.py
CHANGED
@@ -184,11 +184,13 @@ class On(BaseModel):
|
|
184
184
|
raise TypeError("start value should be str or datetime type.")
|
185
185
|
return self.cronjob.schedule(date=start, tz=self.tz)
|
186
186
|
|
187
|
-
def next(self, start: str | datetime) ->
|
187
|
+
def next(self, start: str | datetime) -> CronRunner:
|
188
188
|
"""Return a next datetime from Cron runner object that start with any
|
189
189
|
date that given from input.
|
190
190
|
"""
|
191
|
-
|
191
|
+
runner: CronRunner = self.generate(start=start)
|
192
|
+
_ = runner.next
|
193
|
+
return runner
|
192
194
|
|
193
195
|
|
194
196
|
class YearOn(On):
|