ddeutil-workflow 0.0.26.post1__py3-none-any.whl → 0.0.28__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 -14
- ddeutil/workflow/api/api.py +1 -53
- ddeutil/workflow/conf.py +44 -23
- ddeutil/workflow/cron.py +7 -7
- ddeutil/workflow/exceptions.py +1 -4
- ddeutil/workflow/hook.py +168 -0
- ddeutil/workflow/job.py +18 -17
- ddeutil/workflow/params.py +3 -3
- ddeutil/workflow/result.py +3 -3
- ddeutil/workflow/scheduler.py +9 -9
- ddeutil/workflow/stage.py +87 -170
- ddeutil/workflow/templates.py +336 -0
- ddeutil/workflow/utils.py +23 -404
- ddeutil/workflow/workflow.py +22 -23
- ddeutil_workflow-0.0.28.dist-info/METADATA +284 -0
- ddeutil_workflow-0.0.28.dist-info/RECORD +25 -0
- {ddeutil_workflow-0.0.26.post1.dist-info → ddeutil_workflow-0.0.28.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.26.post1.dist-info/METADATA +0 -230
- ddeutil_workflow-0.0.26.post1.dist-info/RECORD +0 -23
- {ddeutil_workflow-0.0.26.post1.dist-info → ddeutil_workflow-0.0.28.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.26.post1.dist-info → ddeutil_workflow-0.0.28.dist-info}/top_level.txt +0 -0
ddeutil/workflow/job.py
CHANGED
@@ -38,13 +38,13 @@ from .exceptions import (
|
|
38
38
|
)
|
39
39
|
from .result import Result
|
40
40
|
from .stage import Stage
|
41
|
+
from .templates import has_template
|
41
42
|
from .utils import (
|
42
43
|
cross_product,
|
43
44
|
cut_id,
|
44
45
|
dash2underscore,
|
45
46
|
filter_func,
|
46
47
|
gen_id,
|
47
|
-
has_template,
|
48
48
|
)
|
49
49
|
|
50
50
|
logger = get_logger("ddeutil.workflow")
|
@@ -83,7 +83,7 @@ def make(
|
|
83
83
|
if len(matrix) == 0:
|
84
84
|
return [{}]
|
85
85
|
|
86
|
-
# NOTE: Remove matrix that exists on the
|
86
|
+
# NOTE: Remove matrix that exists on the excluded.
|
87
87
|
final: list[DictStr] = []
|
88
88
|
for r in cross_product(matrix=matrix):
|
89
89
|
if any(
|
@@ -101,7 +101,7 @@ def make(
|
|
101
101
|
add: list[DictStr] = []
|
102
102
|
for inc in include:
|
103
103
|
# VALIDATE:
|
104
|
-
# Validate any key in include list should be a subset of
|
104
|
+
# Validate any key in include list should be a subset of someone
|
105
105
|
# in matrix.
|
106
106
|
if all(not (set(inc.keys()) <= set(m.keys())) for m in final):
|
107
107
|
raise ValueError(
|
@@ -128,9 +128,9 @@ class Strategy(BaseModel):
|
|
128
128
|
special job with combination of matrix data.
|
129
129
|
|
130
130
|
This model does not be the part of job only because you can use it to
|
131
|
-
any model object. The
|
132
|
-
comming from combination logic with any matrix values for running it
|
133
|
-
parallelism.
|
131
|
+
any model object. The objective of this model is generating metrix result
|
132
|
+
that comming from combination logic with any matrix values for running it
|
133
|
+
with parallelism.
|
134
134
|
|
135
135
|
[1, 2, 3] x [a, b] --> [1a], [1b], [2a], [2b], [3a], [3b]
|
136
136
|
|
@@ -180,7 +180,7 @@ class Strategy(BaseModel):
|
|
180
180
|
"""Rename key that use dash to underscore because Python does not
|
181
181
|
support this character exist in any variable name.
|
182
182
|
|
183
|
-
:param values: A parsing values to
|
183
|
+
:param values: A parsing values to these models
|
184
184
|
:rtype: DictData
|
185
185
|
"""
|
186
186
|
dash2underscore("max-parallel", values)
|
@@ -226,7 +226,7 @@ class Job(BaseModel):
|
|
226
226
|
"""Job Pydantic model object (short descripte: a group of stages).
|
227
227
|
|
228
228
|
This job model allow you to use for-loop that call matrix strategy. If
|
229
|
-
you pass matrix mapping and it able to generate, you will see it running
|
229
|
+
you pass matrix mapping, and it is able to generate, you will see it running
|
230
230
|
with loop of matrix values.
|
231
231
|
|
232
232
|
Data Validate:
|
@@ -355,7 +355,7 @@ class Job(BaseModel):
|
|
355
355
|
return all(need in jobs for need in self.needs)
|
356
356
|
|
357
357
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
358
|
-
"""Set an outputs from execution process to the
|
358
|
+
"""Set an outputs from execution process to the received context. The
|
359
359
|
result from execution will pass to value of ``strategies`` key.
|
360
360
|
|
361
361
|
For example of setting output method, If you receive execute output
|
@@ -420,7 +420,7 @@ class Job(BaseModel):
|
|
420
420
|
strategy and return with context of this strategy data.
|
421
421
|
|
422
422
|
The result of this execution will return result with strategy ID
|
423
|
-
that generated from the `gen_id` function with
|
423
|
+
that generated from the `gen_id` function with an input strategy value.
|
424
424
|
|
425
425
|
:raise JobException: If it has any error from ``StageException`` or
|
426
426
|
``UtilException``.
|
@@ -429,7 +429,7 @@ class Job(BaseModel):
|
|
429
429
|
This value will pass to the `matrix` key for templating.
|
430
430
|
:param params: A dynamic parameters that will deepcopy to the context.
|
431
431
|
:param run_id: A job running ID for this strategy execution.
|
432
|
-
:param event: An
|
432
|
+
:param event: An event manager that pass to the PoolThreadExecutor.
|
433
433
|
|
434
434
|
:rtype: Result
|
435
435
|
"""
|
@@ -496,7 +496,7 @@ class Job(BaseModel):
|
|
496
496
|
# PARAGRAPH:
|
497
497
|
#
|
498
498
|
# I do not use below syntax because `params` dict be the
|
499
|
-
# reference memory pointer and it was changed when I action
|
499
|
+
# reference memory pointer, and it was changed when I action
|
500
500
|
# anything like update or re-construct this.
|
501
501
|
#
|
502
502
|
# ... params |= stage.execute(params=params)
|
@@ -513,7 +513,9 @@ class Job(BaseModel):
|
|
513
513
|
#
|
514
514
|
try:
|
515
515
|
stage.set_outputs(
|
516
|
-
stage.
|
516
|
+
stage.handler_execute(
|
517
|
+
params=context, run_id=run_id
|
518
|
+
).context,
|
517
519
|
to=context,
|
518
520
|
)
|
519
521
|
except (StageException, UtilException) as err:
|
@@ -566,7 +568,7 @@ class Job(BaseModel):
|
|
566
568
|
run_id: str = run_id or gen_id(self.id or "", unique=True)
|
567
569
|
context: DictData = {}
|
568
570
|
|
569
|
-
# NOTE: Normal Job execution without parallel strategy matrix. It
|
571
|
+
# NOTE: Normal Job execution without parallel strategy matrix. It uses
|
570
572
|
# for-loop to control strategy execution sequentially.
|
571
573
|
if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
|
572
574
|
for strategy in self.strategy.make():
|
@@ -585,8 +587,7 @@ class Job(BaseModel):
|
|
585
587
|
event: Event = Event()
|
586
588
|
|
587
589
|
# IMPORTANT: Start running strategy execution by multithreading because
|
588
|
-
# it will
|
589
|
-
# execution.
|
590
|
+
# it will run by strategy values without waiting previous execution.
|
590
591
|
with ThreadPoolExecutor(
|
591
592
|
max_workers=self.strategy.max_parallel,
|
592
593
|
thread_name_prefix="job_strategy_exec_",
|
@@ -618,7 +619,7 @@ class Job(BaseModel):
|
|
618
619
|
timeout: int = 1800,
|
619
620
|
) -> Result:
|
620
621
|
"""Job parallel pool futures catching with fail-fast mode. That will
|
621
|
-
stop and set event on all not done futures if it
|
622
|
+
stop and set event on all not done futures if it receives the first
|
622
623
|
exception from all running futures.
|
623
624
|
|
624
625
|
:param event: An event manager instance that able to set stopper on the
|
ddeutil/workflow/params.py
CHANGED
@@ -75,7 +75,7 @@ class DatetimeParam(DefaultParam):
|
|
75
75
|
default: datetime = Field(default_factory=get_dt_now)
|
76
76
|
|
77
77
|
def receive(self, value: str | datetime | date | None = None) -> datetime:
|
78
|
-
"""Receive value that match with datetime. If
|
78
|
+
"""Receive value that match with datetime. If an input value pass with
|
79
79
|
None, it will use default value instead.
|
80
80
|
|
81
81
|
:param value: A value that want to validate with datetime parameter
|
@@ -98,7 +98,7 @@ class DatetimeParam(DefaultParam):
|
|
98
98
|
return datetime.fromisoformat(value)
|
99
99
|
except ValueError:
|
100
100
|
raise ParamValueException(
|
101
|
-
f"Invalid
|
101
|
+
f"Invalid the ISO format string: {value!r}"
|
102
102
|
) from None
|
103
103
|
|
104
104
|
|
@@ -158,7 +158,7 @@ class ChoiceParam(BaseParam):
|
|
158
158
|
:rtype: str
|
159
159
|
"""
|
160
160
|
# NOTE:
|
161
|
-
# Return the first value in options if does not pass any input value
|
161
|
+
# Return the first value in options if it does not pass any input value
|
162
162
|
if value is None:
|
163
163
|
return self.options[0]
|
164
164
|
if value not in self.options:
|
ddeutil/workflow/result.py
CHANGED
@@ -37,8 +37,8 @@ class Result:
|
|
37
37
|
|
38
38
|
@model_validator(mode="after")
|
39
39
|
def __prepare_run_id(self) -> Self:
|
40
|
-
"""Prepare running ID which use default ID if it
|
41
|
-
time
|
40
|
+
"""Prepare running ID which use default ID if it initializes at the
|
41
|
+
first time.
|
42
42
|
|
43
43
|
:rtype: Self
|
44
44
|
"""
|
@@ -84,7 +84,7 @@ class Result:
|
|
84
84
|
|
85
85
|
def receive_jobs(self, result: Result) -> Self:
|
86
86
|
"""Receive context from another result object that use on the workflow
|
87
|
-
execution which create a ``jobs`` keys on the context if it
|
87
|
+
execution which create a ``jobs`` keys on the context if it does not
|
88
88
|
exist.
|
89
89
|
|
90
90
|
:rtype: Self
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -149,7 +149,7 @@ class WorkflowSchedule(BaseModel):
|
|
149
149
|
@field_validator("on", mode="after")
|
150
150
|
def __on_no_dup__(cls, value: list[On]) -> list[On]:
|
151
151
|
"""Validate the on fields should not contain duplicate values and if it
|
152
|
-
|
152
|
+
contains every minute value, it should have only one on value.
|
153
153
|
|
154
154
|
:rtype: list[On]
|
155
155
|
"""
|
@@ -195,8 +195,8 @@ class WorkflowSchedule(BaseModel):
|
|
195
195
|
wf: Workflow = Workflow.from_loader(self.name, externals=extras)
|
196
196
|
wf_queue: WorkflowQueue = queue[self.alias]
|
197
197
|
|
198
|
-
# IMPORTANT: Create the default 'on' value if it does not
|
199
|
-
#
|
198
|
+
# IMPORTANT: Create the default 'on' value if it does not pass the `on`
|
199
|
+
# field to the Schedule object.
|
200
200
|
ons: list[On] = self.on or wf.on.copy()
|
201
201
|
|
202
202
|
for on in ons:
|
@@ -223,7 +223,7 @@ class WorkflowSchedule(BaseModel):
|
|
223
223
|
class Schedule(BaseModel):
|
224
224
|
"""Schedule Pydantic model that use to run with any scheduler package.
|
225
225
|
|
226
|
-
It does not equal the on value in Workflow model but it
|
226
|
+
It does not equal the on value in Workflow model, but it uses same logic
|
227
227
|
to running release date with crontab interval.
|
228
228
|
"""
|
229
229
|
|
@@ -368,7 +368,7 @@ def schedule_task(
|
|
368
368
|
:param stop: A stop datetime object that force stop running scheduler.
|
369
369
|
:param queue: A mapping of alias name and WorkflowQueue object.
|
370
370
|
:param threads: A mapping of alias name and Thread object.
|
371
|
-
:param log: A log class that want to
|
371
|
+
:param log: A log class that want to make log object.
|
372
372
|
|
373
373
|
:rtype: CancelJob | None
|
374
374
|
"""
|
@@ -449,7 +449,7 @@ def schedule_task(
|
|
449
449
|
|
450
450
|
|
451
451
|
def monitor(threads: ReleaseThreads) -> None: # pragma: no cov
|
452
|
-
"""Monitoring function that running every five minute for track long
|
452
|
+
"""Monitoring function that running every five minute for track long-running
|
453
453
|
thread instance from the schedule_control function that run every minute.
|
454
454
|
|
455
455
|
:param threads: A mapping of Thread object and its name.
|
@@ -479,7 +479,7 @@ def schedule_control(
|
|
479
479
|
"""Scheduler control function that running every minute.
|
480
480
|
|
481
481
|
:param schedules: A list of workflow names that want to schedule running.
|
482
|
-
:param stop:
|
482
|
+
:param stop: A datetime value that use to stop running schedule.
|
483
483
|
:param externals: An external parameters that pass to Loader.
|
484
484
|
:param log:
|
485
485
|
|
@@ -554,7 +554,7 @@ def schedule_control(
|
|
554
554
|
scheduler.run_pending()
|
555
555
|
time.sleep(1)
|
556
556
|
|
557
|
-
# NOTE: Break the scheduler when the control job does not
|
557
|
+
# NOTE: Break the scheduler when the control job does not exist.
|
558
558
|
if not scheduler.get_jobs("control"):
|
559
559
|
scheduler.clear("monitor")
|
560
560
|
|
@@ -585,7 +585,7 @@ def schedule_runner(
|
|
585
585
|
|
586
586
|
:param stop: A stop datetime object that force stop running scheduler.
|
587
587
|
:param externals:
|
588
|
-
:param excluded: A list of schedule name that want to
|
588
|
+
:param excluded: A list of schedule name that want to exclude from finding.
|
589
589
|
|
590
590
|
:rtype: list[str]
|
591
591
|
|
ddeutil/workflow/stage.py
CHANGED
@@ -5,12 +5,12 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
"""Stage Model that use for getting stage data template from the Job Model.
|
7
7
|
The stage handle the minimize task that run in some thread (same thread at
|
8
|
-
its job owner) that mean it is the lowest executor of a workflow
|
9
|
-
|
8
|
+
its job owner) that mean it is the lowest executor of a workflow that can
|
9
|
+
tracking logs.
|
10
10
|
|
11
11
|
The output of stage execution only return 0 status because I do not want to
|
12
12
|
handle stage error on this stage model. I think stage model should have a lot of
|
13
|
-
|
13
|
+
use-case, and it does not worry when I want to create a new one.
|
14
14
|
|
15
15
|
Execution --> Ok --> Result with 0
|
16
16
|
|
@@ -31,41 +31,28 @@ import time
|
|
31
31
|
import uuid
|
32
32
|
from abc import ABC, abstractmethod
|
33
33
|
from collections.abc import Iterator
|
34
|
-
from dataclasses import dataclass
|
35
|
-
from functools import wraps
|
36
34
|
from inspect import Parameter
|
37
35
|
from pathlib import Path
|
38
36
|
from subprocess import CompletedProcess
|
39
37
|
from textwrap import dedent
|
40
|
-
from typing import
|
41
|
-
|
42
|
-
try:
|
43
|
-
from typing import ParamSpec
|
44
|
-
except ImportError:
|
45
|
-
from typing_extensions import ParamSpec
|
38
|
+
from typing import Optional, Union
|
46
39
|
|
47
40
|
from pydantic import BaseModel, Field
|
48
41
|
from pydantic.functional_validators import model_validator
|
49
42
|
from typing_extensions import Self
|
50
43
|
|
51
|
-
from .__types import DictData, DictStr,
|
44
|
+
from .__types import DictData, DictStr, TupleStr
|
52
45
|
from .conf import config, get_logger
|
53
46
|
from .exceptions import StageException
|
47
|
+
from .hook import TagFunc, extract_hook
|
54
48
|
from .result import Result
|
49
|
+
from .templates import not_in_template, param2template
|
55
50
|
from .utils import (
|
56
|
-
Registry,
|
57
|
-
TagFunc,
|
58
51
|
cut_id,
|
59
52
|
gen_id,
|
60
53
|
make_exec,
|
61
|
-
make_registry,
|
62
|
-
not_in_template,
|
63
|
-
param2template,
|
64
54
|
)
|
65
55
|
|
66
|
-
P = ParamSpec("P")
|
67
|
-
ReturnResult = Callable[P, Result]
|
68
|
-
DecoratorResult = Callable[[ReturnResult], ReturnResult]
|
69
56
|
logger = get_logger("ddeutil.workflow")
|
70
57
|
|
71
58
|
|
@@ -76,94 +63,9 @@ __all__: TupleStr = (
|
|
76
63
|
"HookStage",
|
77
64
|
"TriggerStage",
|
78
65
|
"Stage",
|
79
|
-
"extract_hook",
|
80
66
|
)
|
81
67
|
|
82
68
|
|
83
|
-
def handler_result(message: str | None = None) -> DecoratorResult:
|
84
|
-
"""Decorator function for handler result from the stage execution. This
|
85
|
-
function should to use with execution method only.
|
86
|
-
|
87
|
-
This stage exception handler still use ok-error concept but it allow
|
88
|
-
you force catching an output result with error message by specific
|
89
|
-
environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR`.
|
90
|
-
|
91
|
-
Execution --> Ok --> Result
|
92
|
-
|-status: 0
|
93
|
-
|-context:
|
94
|
-
|-outputs: ...
|
95
|
-
|
96
|
-
--> Error --> Result (if env var was set)
|
97
|
-
|-status: 1
|
98
|
-
|-context:
|
99
|
-
|-error: ...
|
100
|
-
|-error_message: ...
|
101
|
-
|
102
|
-
--> Error --> Raise StageException(...)
|
103
|
-
|
104
|
-
On the last step, it will set the running ID on a return result object
|
105
|
-
from current stage ID before release the final result.
|
106
|
-
|
107
|
-
:param message: A message that want to add at prefix of exception statement.
|
108
|
-
:type message: str | None (Default=None)
|
109
|
-
:rtype: Callable[P, Result]
|
110
|
-
"""
|
111
|
-
# NOTE: The prefix message string that want to add on the first exception
|
112
|
-
# message dialog.
|
113
|
-
#
|
114
|
-
# >>> ValueError: {message}
|
115
|
-
# ... raise value error from the stage execution process.
|
116
|
-
#
|
117
|
-
message: str = message or ""
|
118
|
-
|
119
|
-
def decorator(func: ReturnResult) -> ReturnResult:
|
120
|
-
|
121
|
-
@wraps(func)
|
122
|
-
def wrapped(self: Stage, *args, **kwargs):
|
123
|
-
|
124
|
-
if not (run_id := kwargs.get("run_id")):
|
125
|
-
run_id: str = gen_id(self.name + (self.id or ""), unique=True)
|
126
|
-
kwargs["run_id"] = run_id
|
127
|
-
|
128
|
-
rs_raise: Result = Result(status=1, run_id=run_id)
|
129
|
-
|
130
|
-
try:
|
131
|
-
# NOTE: Start calling origin function with a passing args.
|
132
|
-
return func(self, *args, **kwargs)
|
133
|
-
except Exception as err:
|
134
|
-
# NOTE: Start catching error from the stage execution.
|
135
|
-
logger.error(
|
136
|
-
f"({cut_id(run_id)}) [STAGE]: {err.__class__.__name__}: "
|
137
|
-
f"{err}"
|
138
|
-
)
|
139
|
-
if config.stage_raise_error:
|
140
|
-
# NOTE: If error that raise from stage execution course by
|
141
|
-
# itself, it will return that error with previous
|
142
|
-
# dependency.
|
143
|
-
if isinstance(err, StageException):
|
144
|
-
raise StageException(
|
145
|
-
f"{self.__class__.__name__}: {message}\n\t{err}"
|
146
|
-
) from err
|
147
|
-
raise StageException(
|
148
|
-
f"{self.__class__.__name__}: {message}\n\t"
|
149
|
-
f"{err.__class__.__name__}: {err}"
|
150
|
-
) from None
|
151
|
-
|
152
|
-
# NOTE: Catching exception error object to result with
|
153
|
-
# error_message and error keys.
|
154
|
-
return rs_raise.catch(
|
155
|
-
status=1,
|
156
|
-
context={
|
157
|
-
"error": err,
|
158
|
-
"error_message": f"{err.__class__.__name__}: {err}",
|
159
|
-
},
|
160
|
-
)
|
161
|
-
|
162
|
-
return wrapped
|
163
|
-
|
164
|
-
return decorator
|
165
|
-
|
166
|
-
|
167
69
|
class BaseStage(BaseModel, ABC):
|
168
70
|
"""Base Stage Model that keep only id and name fields for the stage
|
169
71
|
metadata. If you want to implement any custom stage, you can use this class
|
@@ -230,8 +132,74 @@ class BaseStage(BaseModel, ABC):
|
|
230
132
|
"""
|
231
133
|
raise NotImplementedError("Stage should implement ``execute`` method.")
|
232
134
|
|
135
|
+
def handler_execute(
|
136
|
+
self, params: DictData, *, run_id: str | None = None
|
137
|
+
) -> Result:
|
138
|
+
"""Handler result from the stage execution.
|
139
|
+
|
140
|
+
This stage exception handler still use ok-error concept, but it
|
141
|
+
allows you force catching an output result with error message by
|
142
|
+
specific environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR`.
|
143
|
+
|
144
|
+
Execution --> Ok --> Result
|
145
|
+
|-status: 0
|
146
|
+
|-context:
|
147
|
+
|-outputs: ...
|
148
|
+
|
149
|
+
--> Error --> Result (if env var was set)
|
150
|
+
|-status: 1
|
151
|
+
|-context:
|
152
|
+
|-error: ...
|
153
|
+
|-error_message: ...
|
154
|
+
|
155
|
+
--> Error --> Raise StageException(...)
|
156
|
+
|
157
|
+
On the last step, it will set the running ID on a return result object
|
158
|
+
from current stage ID before release the final result.
|
159
|
+
|
160
|
+
:param params: A parameter data that want to use in this execution.
|
161
|
+
:param run_id: A running stage ID for this execution.
|
162
|
+
|
163
|
+
:rtype: Result
|
164
|
+
"""
|
165
|
+
if not run_id:
|
166
|
+
run_id: str = gen_id(self.name + (self.id or ""), unique=True)
|
167
|
+
|
168
|
+
rs_raise: Result = Result(status=1, run_id=run_id)
|
169
|
+
try:
|
170
|
+
# NOTE: Start calling origin function with a passing args.
|
171
|
+
return self.execute(params, run_id=run_id)
|
172
|
+
except Exception as err:
|
173
|
+
# NOTE: Start catching error from the stage execution.
|
174
|
+
logger.error(
|
175
|
+
f"({cut_id(run_id)}) [STAGE]: {err.__class__.__name__}: "
|
176
|
+
f"{err}"
|
177
|
+
)
|
178
|
+
if config.stage_raise_error:
|
179
|
+
# NOTE: If error that raise from stage execution course by
|
180
|
+
# itself, it will return that error with previous
|
181
|
+
# dependency.
|
182
|
+
if isinstance(err, StageException):
|
183
|
+
raise StageException(
|
184
|
+
f"{self.__class__.__name__}: \n\t{err}"
|
185
|
+
) from err
|
186
|
+
raise StageException(
|
187
|
+
f"{self.__class__.__name__}: \n\t"
|
188
|
+
f"{err.__class__.__name__}: {err}"
|
189
|
+
) from None
|
190
|
+
|
191
|
+
# NOTE: Catching exception error object to result with
|
192
|
+
# error_message and error keys.
|
193
|
+
return rs_raise.catch(
|
194
|
+
status=1,
|
195
|
+
context={
|
196
|
+
"error": err,
|
197
|
+
"error_message": f"{err.__class__.__name__}: {err}",
|
198
|
+
},
|
199
|
+
)
|
200
|
+
|
233
201
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
234
|
-
"""Set an outputs from execution process to the
|
202
|
+
"""Set an outputs from execution process to the received context. The
|
235
203
|
result from execution will pass to value of ``outputs`` key.
|
236
204
|
|
237
205
|
For example of setting output method, If you receive execute output
|
@@ -248,7 +216,7 @@ class BaseStage(BaseModel, ABC):
|
|
248
216
|
}
|
249
217
|
}
|
250
218
|
|
251
|
-
:param output:
|
219
|
+
:param output: An output data that want to extract to an output key.
|
252
220
|
:param to: A context data that want to add output result.
|
253
221
|
:rtype: DictData
|
254
222
|
"""
|
@@ -293,8 +261,9 @@ class BaseStage(BaseModel, ABC):
|
|
293
261
|
params: DictData = {} if params is None else params
|
294
262
|
|
295
263
|
try:
|
296
|
-
# WARNING: The eval build-in function is
|
297
|
-
# should
|
264
|
+
# WARNING: The eval build-in function is very dangerous. So, it
|
265
|
+
# should use the `re` module to validate eval-string before
|
266
|
+
# running.
|
298
267
|
rs: bool = eval(
|
299
268
|
param2template(self.condition, params), globals() | params, {}
|
300
269
|
)
|
@@ -326,7 +295,6 @@ class EmptyStage(BaseStage):
|
|
326
295
|
ge=0,
|
327
296
|
)
|
328
297
|
|
329
|
-
@handler_result()
|
330
298
|
def execute(self, params: DictData, *, run_id: str | None = None) -> Result:
|
331
299
|
"""Execution method for the Empty stage that do only logging out to
|
332
300
|
stdout. This method does not use the `handler_result` decorator because
|
@@ -357,7 +325,7 @@ class EmptyStage(BaseStage):
|
|
357
325
|
|
358
326
|
class BashStage(BaseStage):
|
359
327
|
"""Bash execution stage that execute bash script on the current OS.
|
360
|
-
|
328
|
+
If your current OS is Windows, it will run on the bash in the WSL.
|
361
329
|
|
362
330
|
I get some limitation when I run shell statement with the built-in
|
363
331
|
supprocess package. It does not good enough to use multiline statement.
|
@@ -423,7 +391,6 @@ class BashStage(BaseStage):
|
|
423
391
|
# Note: Remove .sh file that use to run bash.
|
424
392
|
Path(f"./{f_name}").unlink()
|
425
393
|
|
426
|
-
@handler_result()
|
427
394
|
def execute(self, params: DictData, *, run_id: str | None = None) -> Result:
|
428
395
|
"""Execute the Bash statement with the Python build-in ``subprocess``
|
429
396
|
package.
|
@@ -504,7 +471,7 @@ class PyStage(BaseStage):
|
|
504
471
|
"""Override set an outputs method for the Python execution process that
|
505
472
|
extract output from all the locals values.
|
506
473
|
|
507
|
-
:param output:
|
474
|
+
:param output: An output data that want to extract to an output key.
|
508
475
|
:param to: A context data that want to add output result.
|
509
476
|
|
510
477
|
:rtype: DictData
|
@@ -525,7 +492,6 @@ class PyStage(BaseStage):
|
|
525
492
|
to.update({k: gb[k] for k in to if k in gb})
|
526
493
|
return to
|
527
494
|
|
528
|
-
@handler_result()
|
529
495
|
def execute(self, params: DictData, *, run_id: str | None = None) -> Result:
|
530
496
|
"""Execute the Python statement that pass all globals and input params
|
531
497
|
to globals argument on ``exec`` build-in function.
|
@@ -547,8 +513,8 @@ class PyStage(BaseStage):
|
|
547
513
|
# NOTE: Start exec the run statement.
|
548
514
|
logger.info(f"({cut_id(run_id)}) [STAGE]: Py-Execute: {self.name}")
|
549
515
|
|
550
|
-
# WARNING: The exec build-in function is
|
551
|
-
# should
|
516
|
+
# WARNING: The exec build-in function is very dangerous. So, it
|
517
|
+
# should use the re module to validate exec-string before running.
|
552
518
|
exec(run, _globals, lc)
|
553
519
|
|
554
520
|
return Result(
|
@@ -558,53 +524,6 @@ class PyStage(BaseStage):
|
|
558
524
|
)
|
559
525
|
|
560
526
|
|
561
|
-
@dataclass(frozen=True)
|
562
|
-
class HookSearchData:
|
563
|
-
"""Hook Search dataclass that use for receive regular expression grouping
|
564
|
-
dict from searching hook string value.
|
565
|
-
"""
|
566
|
-
|
567
|
-
path: str
|
568
|
-
func: str
|
569
|
-
tag: str
|
570
|
-
|
571
|
-
|
572
|
-
def extract_hook(hook: str) -> Callable[[], TagFunc]:
|
573
|
-
"""Extract Hook function from string value to hook partial function that
|
574
|
-
does run it at runtime.
|
575
|
-
|
576
|
-
:raise NotImplementedError: When the searching hook's function result does
|
577
|
-
not exist in the registry.
|
578
|
-
:raise NotImplementedError: When the searching hook's tag result does not
|
579
|
-
exists in the registry with its function key.
|
580
|
-
|
581
|
-
:param hook: A hook value that able to match with Task regex.
|
582
|
-
:rtype: Callable[[], TagFunc]
|
583
|
-
"""
|
584
|
-
if not (found := Re.RE_TASK_FMT.search(hook)):
|
585
|
-
raise ValueError(
|
586
|
-
f"Hook {hook!r} does not match with hook format regex."
|
587
|
-
)
|
588
|
-
|
589
|
-
# NOTE: Pass the searching hook string to `path`, `func`, and `tag`.
|
590
|
-
hook: HookSearchData = HookSearchData(**found.groupdict())
|
591
|
-
|
592
|
-
# NOTE: Registry object should implement on this package only.
|
593
|
-
rgt: dict[str, Registry] = make_registry(f"{hook.path}")
|
594
|
-
if hook.func not in rgt:
|
595
|
-
raise NotImplementedError(
|
596
|
-
f"``REGISTER-MODULES.{hook.path}.registries`` does not "
|
597
|
-
f"implement registry: {hook.func!r}."
|
598
|
-
)
|
599
|
-
|
600
|
-
if hook.tag not in rgt[hook.func]:
|
601
|
-
raise NotImplementedError(
|
602
|
-
f"tag: {hook.tag!r} does not found on registry func: "
|
603
|
-
f"``REGISTER-MODULES.{hook.path}.registries.{hook.func}``"
|
604
|
-
)
|
605
|
-
return rgt[hook.func][hook.tag]
|
606
|
-
|
607
|
-
|
608
527
|
class HookStage(BaseStage):
|
609
528
|
"""Hook executor that hook the Python function from registry with tag
|
610
529
|
decorator function in ``utils`` module and run it with input arguments.
|
@@ -612,7 +531,7 @@ class HookStage(BaseStage):
|
|
612
531
|
This stage is different with PyStage because the PyStage is just calling
|
613
532
|
a Python statement with the ``eval`` and pass that locale before eval that
|
614
533
|
statement. So, you can create your function complexly that you can for your
|
615
|
-
|
534
|
+
objective to invoked by this stage object.
|
616
535
|
|
617
536
|
Data Validate:
|
618
537
|
>>> stage = {
|
@@ -633,7 +552,6 @@ class HookStage(BaseStage):
|
|
633
552
|
alias="with",
|
634
553
|
)
|
635
554
|
|
636
|
-
@handler_result()
|
637
555
|
def execute(self, params: DictData, *, run_id: str | None = None) -> Result:
|
638
556
|
"""Execute the Hook function that already in the hook registry.
|
639
557
|
|
@@ -664,7 +582,7 @@ class HookStage(BaseStage):
|
|
664
582
|
f"Necessary params, ({', '.join(ips.parameters.keys())}, ), "
|
665
583
|
f"does not set to args"
|
666
584
|
)
|
667
|
-
# NOTE: add '_' prefix if it
|
585
|
+
# NOTE: add '_' prefix if it wants to use.
|
668
586
|
for k in ips.parameters:
|
669
587
|
if k.removeprefix("_") in args:
|
670
588
|
args[k] = args.pop(k.removeprefix("_"))
|
@@ -686,7 +604,7 @@ class HookStage(BaseStage):
|
|
686
604
|
|
687
605
|
|
688
606
|
class TriggerStage(BaseStage):
|
689
|
-
"""Trigger Workflow execution stage that execute another workflow. This
|
607
|
+
"""Trigger Workflow execution stage that execute another workflow. This
|
690
608
|
the core stage that allow you to create the reusable workflow object or
|
691
609
|
dynamic parameters workflow for common usecase.
|
692
610
|
|
@@ -708,9 +626,8 @@ class TriggerStage(BaseStage):
|
|
708
626
|
description="A parameter that want to pass to workflow execution.",
|
709
627
|
)
|
710
628
|
|
711
|
-
@handler_result("Raise from TriggerStage")
|
712
629
|
def execute(self, params: DictData, *, run_id: str | None = None) -> Result:
|
713
|
-
"""Trigger another workflow execution. It will
|
630
|
+
"""Trigger another workflow execution. It will wait the trigger
|
714
631
|
workflow running complete before catching its result.
|
715
632
|
|
716
633
|
:param params: A parameter data that want to use in this execution.
|
@@ -739,7 +656,7 @@ class TriggerStage(BaseStage):
|
|
739
656
|
# NOTE:
|
740
657
|
# An order of parsing stage model on the Job model with ``stages`` field.
|
741
658
|
# From the current build-in stages, they do not have stage that have the same
|
742
|
-
# fields that
|
659
|
+
# fields that because of parsing on the Job's stages key.
|
743
660
|
#
|
744
661
|
Stage = Union[
|
745
662
|
PyStage,
|