ddeutil-workflow 0.0.19__tar.gz → 0.0.20__tar.gz
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-0.0.19/src/ddeutil_workflow.egg-info → ddeutil_workflow-0.0.20}/PKG-INFO +3 -3
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/pyproject.toml +2 -2
- ddeutil_workflow-0.0.20/src/ddeutil/workflow/__about__.py +1 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/__cron.py +28 -2
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/__init__.py +9 -4
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/conf.py +31 -25
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/exceptions.py +4 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/job.py +46 -45
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/on.py +4 -15
- ddeutil_workflow-0.0.20/src/ddeutil/workflow/scheduler.py +574 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/stage.py +92 -66
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/utils.py +29 -24
- ddeutil_workflow-0.0.20/src/ddeutil/workflow/workflow.py +1084 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20/src/ddeutil_workflow.egg-info}/PKG-INFO +3 -3
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil_workflow.egg-info/SOURCES.txt +12 -10
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil_workflow.egg-info/requires.txt +2 -2
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test__cron.py +1 -2
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_conf_log.py +3 -3
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_job.py +2 -7
- ddeutil_workflow-0.0.19/tests/test_job_strategy_run.py → ddeutil_workflow-0.0.20/tests/test_job_exec_strategy.py +1 -1
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_on.py +6 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_scheduler.py +26 -18
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_scheduler_tasks.py +20 -32
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_stage.py +11 -10
- ddeutil_workflow-0.0.19/tests/test_stage_bash.py → ddeutil_workflow-0.0.20/tests/test_stage_exec_bash.py +3 -3
- ddeutil_workflow-0.0.19/tests/test_stage_hook.py → ddeutil_workflow-0.0.20/tests/test_stage_exec_hook.py +5 -5
- ddeutil_workflow-0.0.19/tests/test_stage_py.py → ddeutil_workflow-0.0.20/tests/test_stage_exec_py.py +4 -4
- ddeutil_workflow-0.0.19/tests/test_stage_trigger.py → ddeutil_workflow-0.0.20/tests/test_stage_exec_trigger.py +2 -2
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_utils_result.py +1 -1
- ddeutil_workflow-0.0.19/tests/test_workflow_run.py → ddeutil_workflow-0.0.20/tests/test_workflow_exec.py +5 -5
- ddeutil_workflow-0.0.19/tests/test_workflow_task.py → ddeutil_workflow-0.0.20/tests/test_workflow_exec_hook.py +4 -4
- ddeutil_workflow-0.0.19/tests/test_workflow_depends.py → ddeutil_workflow-0.0.20/tests/test_workflow_exec_needs.py +2 -2
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_workflow_poke.py +17 -23
- ddeutil_workflow-0.0.20/tests/test_workflow_release.py +44 -0
- ddeutil_workflow-0.0.19/src/ddeutil/workflow/__about__.py +0 -1
- ddeutil_workflow-0.0.19/src/ddeutil/workflow/scheduler.py +0 -1477
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/LICENSE +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/README.md +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/__types.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/api.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/cli.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/repeat.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil/workflow/route.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_conf.py +0 -0
- /ddeutil_workflow-0.0.19/tests/test_job_py.py → /ddeutil_workflow-0.0.20/tests/test_job_exec_py.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_job_strategy.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_params.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_utils.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_utils_filter.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_utils_params.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_utils_tag.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_utils_template.py +0 -0
- {ddeutil_workflow-0.0.19 → ddeutil_workflow-0.0.20}/tests/test_workflow.py +0 -0
- /ddeutil_workflow-0.0.19/tests/test_workflow_job_run.py → /ddeutil_workflow-0.0.20/tests/test_workflow_job_exec.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.20
|
4
4
|
Summary: Lightweight workflow orchestration with less dependencies
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -24,9 +24,9 @@ Description-Content-Type: text/markdown
|
|
24
24
|
License-File: LICENSE
|
25
25
|
Requires-Dist: ddeutil>=0.4.3
|
26
26
|
Requires-Dist: ddeutil-io[toml,yaml]>=0.2.3
|
27
|
-
Requires-Dist: pydantic==2.
|
27
|
+
Requires-Dist: pydantic==2.10.2
|
28
28
|
Requires-Dist: python-dotenv==1.0.1
|
29
|
-
Requires-Dist: typer
|
29
|
+
Requires-Dist: typer==0.15.1
|
30
30
|
Requires-Dist: schedule<2.0.0,==1.2.2
|
31
31
|
Provides-Extra: api
|
32
32
|
Requires-Dist: fastapi<1.0.0,>=0.115.0; extra == "api"
|
@@ -28,9 +28,9 @@ requires-python = ">=3.9.13"
|
|
28
28
|
dependencies = [
|
29
29
|
"ddeutil>=0.4.3",
|
30
30
|
"ddeutil-io[yaml,toml]>=0.2.3",
|
31
|
-
"pydantic==2.
|
31
|
+
"pydantic==2.10.2",
|
32
32
|
"python-dotenv==1.0.1",
|
33
|
-
"typer==0.
|
33
|
+
"typer==0.15.1",
|
34
34
|
"schedule==1.2.2,<2.0.0",
|
35
35
|
]
|
36
36
|
dynamic = ["version"]
|
@@ -0,0 +1 @@
|
|
1
|
+
__version__: str = "0.0.20"
|
@@ -653,6 +653,20 @@ class CronJob:
|
|
653
653
|
|
654
654
|
|
655
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
|
+
|
656
670
|
cron_length = 6
|
657
671
|
cron_units = CRON_UNITS_YEAR
|
658
672
|
|
@@ -705,9 +719,17 @@ class CronRunner:
|
|
705
719
|
else:
|
706
720
|
self.date: datetime = datetime.now(tz=self.tz)
|
707
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
|
+
|
708
728
|
# NOTE: Add one minute if the second value more than 0.
|
709
729
|
if self.date.second > 0:
|
710
|
-
self.date: datetime = self.date + timedelta(
|
730
|
+
self.date: datetime = self.date.replace(second=0) + timedelta(
|
731
|
+
minutes=1
|
732
|
+
)
|
711
733
|
|
712
734
|
self.__start_date: datetime = self.date
|
713
735
|
self.cron: CronJob | CronJobYear = cron
|
@@ -753,7 +775,7 @@ class CronRunner:
|
|
753
775
|
not self.__shift_date(mode, reverse)
|
754
776
|
for mode in ("year", "month", "day", "hour", "minute")
|
755
777
|
):
|
756
|
-
return copy.deepcopy(self.date
|
778
|
+
return copy.deepcopy(self.date)
|
757
779
|
|
758
780
|
raise RecursionError("Unable to find execution time for schedule")
|
759
781
|
|
@@ -802,6 +824,10 @@ class CronRunner:
|
|
802
824
|
# NOTE: Replace date that less than it mode to zero.
|
803
825
|
self.date: datetime = replace_date(self.date, mode, reverse=reverse)
|
804
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
|
+
|
805
831
|
if current_value != getattr(self.date, switch[mode]):
|
806
832
|
return mode != "month"
|
807
833
|
|
@@ -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
|
+
)
|
@@ -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)
|
@@ -287,29 +312,7 @@ def get_type(t: str, params: Config) -> AnyModelType:
|
|
287
312
|
|
288
313
|
|
289
314
|
config = Config()
|
290
|
-
|
291
|
-
|
292
|
-
@lru_cache
|
293
|
-
def get_logger(name: str):
|
294
|
-
"""Return logger object with an input module name.
|
295
|
-
|
296
|
-
:param name: A module name that want to log.
|
297
|
-
"""
|
298
|
-
logger = logging.getLogger(name)
|
299
|
-
formatter = logging.Formatter(
|
300
|
-
fmt=(
|
301
|
-
"%(asctime)s.%(msecs)03d (%(name)-10s, %(process)-5d, "
|
302
|
-
"%(thread)-5d) [%(levelname)-7s] %(message)-120s "
|
303
|
-
"(%(filename)s:%(lineno)s)"
|
304
|
-
),
|
305
|
-
datefmt="%Y-%m-%d %H:%M:%S",
|
306
|
-
)
|
307
|
-
stream = logging.StreamHandler()
|
308
|
-
stream.setFormatter(formatter)
|
309
|
-
logger.addHandler(stream)
|
310
|
-
|
311
|
-
logger.setLevel(logging.DEBUG if config.debug else logging.INFO)
|
312
|
-
return logger
|
315
|
+
logger = get_logger("ddeutil.workflow")
|
313
316
|
|
314
317
|
|
315
318
|
class BaseLog(BaseModel, ABC):
|
@@ -319,8 +322,8 @@ class BaseLog(BaseModel, ABC):
|
|
319
322
|
"""
|
320
323
|
|
321
324
|
name: str = Field(description="A workflow name.")
|
322
|
-
on: str = Field(description="A cronjob string of this piepline schedule.")
|
323
325
|
release: datetime = Field(description="A release datetime.")
|
326
|
+
type: str = Field(description="A running type before logging.")
|
324
327
|
context: DictData = Field(
|
325
328
|
default_factory=dict,
|
326
329
|
description=(
|
@@ -462,6 +465,9 @@ class FileLog(BaseLog):
|
|
462
465
|
if not config.enable_write_log:
|
463
466
|
return self
|
464
467
|
|
468
|
+
logger.debug(
|
469
|
+
f"({self.run_id}) [LOG]: Start writing log: {self.name!r}."
|
470
|
+
)
|
465
471
|
log_file: Path = self.pointer() / f"{self.run_id}.log"
|
466
472
|
log_file.write_text(
|
467
473
|
json.dumps(
|
@@ -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
|
|
@@ -69,10 +69,14 @@ def make(
|
|
69
69
|
"""Make a list of product of matrix values that already filter with
|
70
70
|
exclude matrix and add specific matrix with include.
|
71
71
|
|
72
|
+
This function use the `lru_cache` decorator function increase
|
73
|
+
performance for duplicate matrix value scenario.
|
74
|
+
|
72
75
|
:param matrix: A matrix values that want to cross product to possible
|
73
76
|
parallelism values.
|
74
77
|
:param include: A list of additional matrix that want to adds-in.
|
75
78
|
:param exclude: A list of exclude matrix that want to filter-out.
|
79
|
+
|
76
80
|
:rtype: list[DictStr]
|
77
81
|
"""
|
78
82
|
# NOTE: If it does not set matrix, it will return list of an empty dict.
|
@@ -200,8 +204,15 @@ class Strategy(BaseModel):
|
|
200
204
|
|
201
205
|
|
202
206
|
class TriggerRules(str, Enum):
|
207
|
+
"""Trigger Rules enum object."""
|
208
|
+
|
203
209
|
all_success: str = "all_success"
|
204
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"
|
205
216
|
|
206
217
|
|
207
218
|
class Job(BaseModel):
|
@@ -264,12 +275,6 @@ class Job(BaseModel):
|
|
264
275
|
default_factory=Strategy,
|
265
276
|
description="A strategy matrix that want to generate.",
|
266
277
|
)
|
267
|
-
run_id: Optional[str] = Field(
|
268
|
-
default=None,
|
269
|
-
description="A running job ID.",
|
270
|
-
repr=False,
|
271
|
-
exclude=True,
|
272
|
-
)
|
273
278
|
|
274
279
|
@model_validator(mode="before")
|
275
280
|
def __prepare_keys__(cls, values: DictData) -> DictData:
|
@@ -310,29 +315,17 @@ class Job(BaseModel):
|
|
310
315
|
return value
|
311
316
|
|
312
317
|
@model_validator(mode="after")
|
313
|
-
def
|
314
|
-
"""
|
318
|
+
def __validate_job_id__(self) -> Self:
|
319
|
+
"""Validate job id should not have templating syntax.
|
315
320
|
|
316
321
|
:rtype: Self
|
317
322
|
"""
|
318
|
-
if self.run_id is None:
|
319
|
-
self.run_id = gen_id(self.id or "", unique=True)
|
320
|
-
|
321
323
|
# VALIDATE: Validate job id should not dynamic with params template.
|
322
324
|
if has_template(self.id):
|
323
325
|
raise ValueError("Job ID should not has any template.")
|
324
326
|
|
325
327
|
return self
|
326
328
|
|
327
|
-
def get_running_id(self, run_id: str) -> Self:
|
328
|
-
"""Return Job model object that changing job running ID with an
|
329
|
-
input running ID.
|
330
|
-
|
331
|
-
:param run_id: A replace job running ID.
|
332
|
-
:rtype: Self
|
333
|
-
"""
|
334
|
-
return self.model_copy(update={"run_id": run_id})
|
335
|
-
|
336
329
|
def stage(self, stage_id: str) -> Stage:
|
337
330
|
"""Return stage model that match with an input stage ID.
|
338
331
|
|
@@ -383,8 +376,6 @@ class Job(BaseModel):
|
|
383
376
|
# NOTE: If the job ID did not set, it will use index of jobs key
|
384
377
|
# instead.
|
385
378
|
_id: str = self.id or str(len(to["jobs"]) + 1)
|
386
|
-
|
387
|
-
logger.debug(f"({self.run_id}) [JOB]: Set outputs on: {_id}")
|
388
379
|
to["jobs"][_id] = (
|
389
380
|
{"strategies": output}
|
390
381
|
if self.strategy.is_set()
|
@@ -396,6 +387,7 @@ class Job(BaseModel):
|
|
396
387
|
self,
|
397
388
|
strategy: DictData,
|
398
389
|
params: DictData,
|
390
|
+
run_id: str | None = None,
|
399
391
|
*,
|
400
392
|
event: Event | None = None,
|
401
393
|
) -> Result:
|
@@ -411,10 +403,12 @@ class Job(BaseModel):
|
|
411
403
|
|
412
404
|
:param strategy: A metrix strategy value.
|
413
405
|
:param params: A dynamic parameters.
|
406
|
+
:param run_id: A job running ID for this strategy execution.
|
414
407
|
:param event: An manger event that pass to the PoolThreadExecutor.
|
415
408
|
|
416
409
|
:rtype: Result
|
417
410
|
"""
|
411
|
+
run_id: str = run_id or gen_id(self.id or "", unique=True)
|
418
412
|
strategy_id: str = gen_id(strategy)
|
419
413
|
|
420
414
|
# PARAGRAPH:
|
@@ -435,22 +429,17 @@ class Job(BaseModel):
|
|
435
429
|
# IMPORTANT: The stage execution only run sequentially one-by-one.
|
436
430
|
for stage in self.stages:
|
437
431
|
|
438
|
-
# IMPORTANT: Change any stage running IDs to this job running ID.
|
439
|
-
stage: Stage = stage.get_running_id(self.run_id)
|
440
|
-
|
441
|
-
name: str = stage.id or stage.name
|
442
|
-
|
443
432
|
if stage.is_skipped(params=context):
|
444
|
-
logger.info(f"({
|
433
|
+
logger.info(f"({run_id}) [JOB]: Skip stage: {stage.iden!r}")
|
445
434
|
continue
|
446
435
|
|
447
436
|
logger.info(
|
448
|
-
f"({
|
437
|
+
f"({run_id}) [JOB]: Start execute the stage: {stage.iden!r}"
|
449
438
|
)
|
450
439
|
|
451
440
|
# NOTE: Logging a matrix that pass on this stage execution.
|
452
441
|
if strategy:
|
453
|
-
logger.info(f"({
|
442
|
+
logger.info(f"({run_id}) [JOB]: Matrix: {strategy}")
|
454
443
|
|
455
444
|
# NOTE: Force stop this execution if event was set from main
|
456
445
|
# execution.
|
@@ -475,6 +464,7 @@ class Job(BaseModel):
|
|
475
464
|
),
|
476
465
|
},
|
477
466
|
},
|
467
|
+
run_id=run_id,
|
478
468
|
)
|
479
469
|
|
480
470
|
# PARAGRAPH:
|
@@ -497,12 +487,12 @@ class Job(BaseModel):
|
|
497
487
|
#
|
498
488
|
try:
|
499
489
|
stage.set_outputs(
|
500
|
-
stage.execute(params=context).context,
|
490
|
+
stage.execute(params=context, run_id=run_id).context,
|
501
491
|
to=context,
|
502
492
|
)
|
503
493
|
except (StageException, UtilException) as err:
|
504
494
|
logger.error(
|
505
|
-
f"({
|
495
|
+
f"({run_id}) [JOB]: {err.__class__.__name__}: {err}"
|
506
496
|
)
|
507
497
|
if config.job_raise_error:
|
508
498
|
raise JobException(
|
@@ -519,10 +509,10 @@ class Job(BaseModel):
|
|
519
509
|
"error_message": f"{err.__class__.__name__}: {err}",
|
520
510
|
},
|
521
511
|
},
|
512
|
+
run_id=run_id,
|
522
513
|
)
|
523
514
|
|
524
|
-
# NOTE: Remove the current stage object
|
525
|
-
# ``get_running_id`` method for saving memory.
|
515
|
+
# NOTE: Remove the current stage object.
|
526
516
|
del stage
|
527
517
|
|
528
518
|
return Result(
|
@@ -533,20 +523,23 @@ class Job(BaseModel):
|
|
533
523
|
"stages": filter_func(context.pop("stages", {})),
|
534
524
|
},
|
535
525
|
},
|
526
|
+
run_id=run_id,
|
536
527
|
)
|
537
528
|
|
538
|
-
def execute(self, params: DictData | None = None) -> Result:
|
529
|
+
def execute(self, params: DictData, run_id: str | None = None) -> Result:
|
539
530
|
"""Job execution with passing dynamic parameters from the workflow
|
540
531
|
execution. It will generate matrix values at the first step and run
|
541
532
|
multithread on this metrics to the ``stages`` field of this job.
|
542
533
|
|
543
534
|
:param params: An input parameters that use on job execution.
|
535
|
+
:param run_id: A job running ID for this execution.
|
536
|
+
|
544
537
|
:rtype: Result
|
545
538
|
"""
|
546
539
|
|
547
540
|
# NOTE: I use this condition because this method allow passing empty
|
548
541
|
# params and I do not want to create new dict object.
|
549
|
-
|
542
|
+
run_id: str = run_id or gen_id(self.id or "", unique=True)
|
550
543
|
context: DictData = {}
|
551
544
|
|
552
545
|
# NOTE: Normal Job execution without parallel strategy.
|
@@ -555,6 +548,7 @@ class Job(BaseModel):
|
|
555
548
|
rs: Result = self.execute_strategy(
|
556
549
|
strategy=strategy,
|
557
550
|
params=params,
|
551
|
+
run_id=run_id,
|
558
552
|
)
|
559
553
|
context.update(rs.context)
|
560
554
|
return Result(
|
@@ -577,6 +571,7 @@ class Job(BaseModel):
|
|
577
571
|
self.execute_strategy,
|
578
572
|
strategy=strategy,
|
579
573
|
params=params,
|
574
|
+
run_id=run_id,
|
580
575
|
event=event,
|
581
576
|
)
|
582
577
|
for strategy in self.strategy.make()
|
@@ -584,15 +579,18 @@ class Job(BaseModel):
|
|
584
579
|
|
585
580
|
# NOTE: Dynamic catching futures object with fail-fast flag.
|
586
581
|
return (
|
587
|
-
self.__catch_fail_fast(
|
582
|
+
self.__catch_fail_fast(
|
583
|
+
event=event, futures=futures, run_id=run_id
|
584
|
+
)
|
588
585
|
if self.strategy.fail_fast
|
589
|
-
else self.__catch_all_completed(futures=futures)
|
586
|
+
else self.__catch_all_completed(futures=futures, run_id=run_id)
|
590
587
|
)
|
591
588
|
|
589
|
+
@staticmethod
|
592
590
|
def __catch_fail_fast(
|
593
|
-
self,
|
594
591
|
event: Event,
|
595
592
|
futures: list[Future],
|
593
|
+
run_id: str,
|
596
594
|
*,
|
597
595
|
timeout: int = 1800,
|
598
596
|
result_timeout: int = 60,
|
@@ -604,6 +602,7 @@ class Job(BaseModel):
|
|
604
602
|
:param event: An event manager instance that able to set stopper on the
|
605
603
|
observing thread/process.
|
606
604
|
:param futures: A list of futures.
|
605
|
+
:param run_id: A job running ID from execution.
|
607
606
|
:param timeout: A timeout to waiting all futures complete.
|
608
607
|
:param result_timeout: A timeout of getting result from the future
|
609
608
|
instance when it was running completely.
|
@@ -623,7 +622,7 @@ class Job(BaseModel):
|
|
623
622
|
nd: str = (
|
624
623
|
f", the strategies do not run is {not_done}" if not_done else ""
|
625
624
|
)
|
626
|
-
logger.debug(f"({
|
625
|
+
logger.debug(f"({run_id}) [JOB]: Strategy is set Fail Fast{nd}")
|
627
626
|
|
628
627
|
# NOTE:
|
629
628
|
# Stop all running tasks with setting the event manager and cancel
|
@@ -639,7 +638,7 @@ class Job(BaseModel):
|
|
639
638
|
if err := future.exception():
|
640
639
|
status: int = 1
|
641
640
|
logger.error(
|
642
|
-
f"({
|
641
|
+
f"({run_id}) [JOB]: One stage failed with: "
|
643
642
|
f"{future.exception()}, shutting down this future."
|
644
643
|
)
|
645
644
|
context.update(
|
@@ -655,9 +654,10 @@ class Job(BaseModel):
|
|
655
654
|
|
656
655
|
return rs_final.catch(status=status, context=context)
|
657
656
|
|
657
|
+
@staticmethod
|
658
658
|
def __catch_all_completed(
|
659
|
-
self,
|
660
659
|
futures: list[Future],
|
660
|
+
run_id: str,
|
661
661
|
*,
|
662
662
|
timeout: int = 1800,
|
663
663
|
result_timeout: int = 60,
|
@@ -666,6 +666,7 @@ class Job(BaseModel):
|
|
666
666
|
|
667
667
|
:param futures: A list of futures that want to catch all completed
|
668
668
|
result.
|
669
|
+
:param run_id: A job running ID from execution.
|
669
670
|
:param timeout: A timeout to waiting all futures complete.
|
670
671
|
:param result_timeout: A timeout of getting result from the future
|
671
672
|
instance when it was running completely.
|
@@ -680,7 +681,7 @@ class Job(BaseModel):
|
|
680
681
|
except TimeoutError: # pragma: no cov
|
681
682
|
status = 1
|
682
683
|
logger.warning(
|
683
|
-
f"({
|
684
|
+
f"({run_id}) [JOB]: Task is hanging. Attempting to "
|
684
685
|
f"kill."
|
685
686
|
)
|
686
687
|
future.cancel()
|
@@ -691,11 +692,11 @@ class Job(BaseModel):
|
|
691
692
|
if not future.cancelled()
|
692
693
|
else "Task canceled successfully."
|
693
694
|
)
|
694
|
-
logger.warning(f"({
|
695
|
+
logger.warning(f"({run_id}) [JOB]: {stmt}")
|
695
696
|
except JobException as err:
|
696
697
|
status = 1
|
697
698
|
logger.error(
|
698
|
-
f"({
|
699
|
+
f"({run_id}) [JOB]: Get stage exception with "
|
699
700
|
f"fail-fast does not set;\n{err.__class__.__name__}:\n\t"
|
700
701
|
f"{err}"
|
701
702
|
)
|
@@ -184,24 +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
|
-
|
192
|
-
|
193
|
-
|
194
|
-
# """Pop the matching datetime value from list of datetime alias queue."""
|
195
|
-
# for dt in queue:
|
196
|
-
# if self.next(dt) == dt:
|
197
|
-
# return dt
|
198
|
-
#
|
199
|
-
# # NOTE: Add 1 second value to the current datetime for forcing crontab
|
200
|
-
# # runner generate the next datetime instead if current datetime be
|
201
|
-
# # valid because I already replaced second to zero before passing.
|
202
|
-
# return datetime.now(tz=config.tz).replace(
|
203
|
-
# second=0, microsecond=0
|
204
|
-
# ) + timedelta(seconds=1)
|
191
|
+
runner: CronRunner = self.generate(start=start)
|
192
|
+
_ = runner.next
|
193
|
+
return runner
|
205
194
|
|
206
195
|
|
207
196
|
class YearOn(On):
|