ddeutil-workflow 0.0.49__py3-none-any.whl → 0.0.51__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 +8 -26
- ddeutil/workflow/conf.py +11 -11
- ddeutil/workflow/cron.py +46 -20
- ddeutil/workflow/exceptions.py +3 -3
- ddeutil/workflow/job.py +269 -145
- ddeutil/workflow/logs.py +23 -19
- ddeutil/workflow/params.py +56 -16
- ddeutil/workflow/result.py +12 -4
- ddeutil/workflow/reusables.py +4 -2
- ddeutil/workflow/scheduler.py +5 -1
- ddeutil/workflow/stages.py +580 -217
- ddeutil/workflow/utils.py +42 -38
- ddeutil/workflow/workflow.py +92 -95
- {ddeutil_workflow-0.0.49.dist-info → ddeutil_workflow-0.0.51.dist-info}/METADATA +71 -14
- ddeutil_workflow-0.0.51.dist-info/RECORD +31 -0
- ddeutil_workflow-0.0.49.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.49.dist-info → ddeutil_workflow-0.0.51.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.49.dist-info → ddeutil_workflow-0.0.51.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.49.dist-info → ddeutil_workflow-0.0.51.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stages.py
CHANGED
@@ -26,6 +26,7 @@ from __future__ import annotations
|
|
26
26
|
|
27
27
|
import asyncio
|
28
28
|
import contextlib
|
29
|
+
import copy
|
29
30
|
import inspect
|
30
31
|
import subprocess
|
31
32
|
import sys
|
@@ -34,10 +35,13 @@ import uuid
|
|
34
35
|
from abc import ABC, abstractmethod
|
35
36
|
from collections.abc import Iterator
|
36
37
|
from concurrent.futures import (
|
38
|
+
FIRST_EXCEPTION,
|
37
39
|
Future,
|
38
40
|
ThreadPoolExecutor,
|
39
41
|
as_completed,
|
42
|
+
wait,
|
40
43
|
)
|
44
|
+
from datetime import datetime
|
41
45
|
from inspect import Parameter
|
42
46
|
from pathlib import Path
|
43
47
|
from subprocess import CompletedProcess
|
@@ -51,10 +55,12 @@ from typing_extensions import Self
|
|
51
55
|
|
52
56
|
from .__types import DictData, DictStr, TupleStr
|
53
57
|
from .conf import dynamic
|
54
|
-
from .exceptions import StageException, to_dict
|
55
|
-
from .result import FAILED, SUCCESS, Result, Status
|
58
|
+
from .exceptions import StageException, UtilException, to_dict
|
59
|
+
from .result import CANCEL, FAILED, SUCCESS, WAIT, Result, Status
|
56
60
|
from .reusables import TagFunc, extract_call, not_in_template, param2template
|
57
61
|
from .utils import (
|
62
|
+
delay,
|
63
|
+
filter_func,
|
58
64
|
gen_id,
|
59
65
|
make_exec,
|
60
66
|
)
|
@@ -67,7 +73,8 @@ class BaseStage(BaseModel, ABC):
|
|
67
73
|
metadata. If you want to implement any custom stage, you can use this class
|
68
74
|
to parent and implement ``self.execute()`` method only.
|
69
75
|
|
70
|
-
This class is the abstraction class for any stage
|
76
|
+
This class is the abstraction class for any stage model that want to
|
77
|
+
implement to workflow model.
|
71
78
|
"""
|
72
79
|
|
73
80
|
extras: DictData = Field(
|
@@ -150,9 +157,8 @@ class BaseStage(BaseModel, ABC):
|
|
150
157
|
parent_run_id: str | None = None,
|
151
158
|
result: Result | None = None,
|
152
159
|
raise_error: bool | None = None,
|
153
|
-
to: DictData | None = None,
|
154
160
|
event: Event | None = None,
|
155
|
-
) -> Result:
|
161
|
+
) -> Result | DictData:
|
156
162
|
"""Handler stage execution result from the stage `execute` method.
|
157
163
|
|
158
164
|
This stage exception handler still use ok-error concept, but it
|
@@ -184,8 +190,6 @@ class BaseStage(BaseModel, ABC):
|
|
184
190
|
:param result: (Result) A result object for keeping context and status
|
185
191
|
data before execution.
|
186
192
|
:param raise_error: (bool) A flag that all this method raise error
|
187
|
-
:param to: (DictData) A target object for auto set the return output
|
188
|
-
after execution.
|
189
193
|
:param event: (Event) An event manager that pass to the stage execution.
|
190
194
|
|
191
195
|
:rtype: Result
|
@@ -200,7 +204,7 @@ class BaseStage(BaseModel, ABC):
|
|
200
204
|
|
201
205
|
try:
|
202
206
|
rs: Result = self.execute(params, result=result, event=event)
|
203
|
-
return
|
207
|
+
return rs
|
204
208
|
except Exception as e:
|
205
209
|
result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
|
206
210
|
|
@@ -214,11 +218,7 @@ class BaseStage(BaseModel, ABC):
|
|
214
218
|
) from e
|
215
219
|
|
216
220
|
errors: DictData = {"errors": to_dict(e)}
|
217
|
-
return (
|
218
|
-
self.set_outputs(errors, to=to)
|
219
|
-
if to is not None
|
220
|
-
else result.catch(status=FAILED, context=errors)
|
221
|
-
)
|
221
|
+
return result.catch(status=FAILED, context=errors)
|
222
222
|
|
223
223
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
224
224
|
"""Set an outputs from execution process to the received context. The
|
@@ -228,7 +228,7 @@ class BaseStage(BaseModel, ABC):
|
|
228
228
|
and want to set on the `to` like;
|
229
229
|
|
230
230
|
... (i) output: {'foo': bar}
|
231
|
-
... (ii) to: {}
|
231
|
+
... (ii) to: {'stages': {}}
|
232
232
|
|
233
233
|
The result of the `to` argument will be;
|
234
234
|
|
@@ -236,11 +236,15 @@ class BaseStage(BaseModel, ABC):
|
|
236
236
|
'stages': {
|
237
237
|
'<stage-id>': {
|
238
238
|
'outputs': {'foo': 'bar'},
|
239
|
-
'skipped': False
|
239
|
+
'skipped': False,
|
240
240
|
}
|
241
241
|
}
|
242
242
|
}
|
243
243
|
|
244
|
+
Important:
|
245
|
+
This method is use for reconstruct the result context and transfer
|
246
|
+
to the `to` argument.
|
247
|
+
|
244
248
|
:param output: (DictData) An output data that want to extract to an
|
245
249
|
output key.
|
246
250
|
:param to: (DictData) A context data that want to add output result.
|
@@ -271,11 +275,18 @@ class BaseStage(BaseModel, ABC):
|
|
271
275
|
if "skipped" in output
|
272
276
|
else {}
|
273
277
|
)
|
274
|
-
to["stages"][_id] = {
|
278
|
+
to["stages"][_id] = {
|
279
|
+
"outputs": copy.deepcopy(output),
|
280
|
+
**skipping,
|
281
|
+
**errors,
|
282
|
+
}
|
275
283
|
return to
|
276
284
|
|
277
285
|
def get_outputs(self, outputs: DictData) -> DictData:
|
278
|
-
"""Get the outputs from stages data.
|
286
|
+
"""Get the outputs from stages data. It will get this stage ID from
|
287
|
+
the stage outputs mapping.
|
288
|
+
|
289
|
+
:param outputs: (DictData) A stage outputs that want to get by stage ID.
|
279
290
|
|
280
291
|
:rtype: DictData
|
281
292
|
"""
|
@@ -329,6 +340,7 @@ class BaseStage(BaseModel, ABC):
|
|
329
340
|
|
330
341
|
|
331
342
|
class BaseAsyncStage(BaseStage):
|
343
|
+
"""Base Async Stage model."""
|
332
344
|
|
333
345
|
@abstractmethod
|
334
346
|
def execute(
|
@@ -374,7 +386,6 @@ class BaseAsyncStage(BaseStage):
|
|
374
386
|
parent_run_id: str | None = None,
|
375
387
|
result: Result | None = None,
|
376
388
|
raise_error: bool | None = None,
|
377
|
-
to: DictData | None = None,
|
378
389
|
event: Event | None = None,
|
379
390
|
) -> Result:
|
380
391
|
"""Async Handler stage execution result from the stage `execute` method.
|
@@ -387,8 +398,6 @@ class BaseAsyncStage(BaseStage):
|
|
387
398
|
:param result: (Result) A result object for keeping context and status
|
388
399
|
data before execution.
|
389
400
|
:param raise_error: (bool) A flag that all this method raise error
|
390
|
-
:param to: (DictData) A target object for auto set the return output
|
391
|
-
after execution.
|
392
401
|
:param event: (Event) An event manager that pass to the stage execution.
|
393
402
|
|
394
403
|
:rtype: Result
|
@@ -403,8 +412,6 @@ class BaseAsyncStage(BaseStage):
|
|
403
412
|
|
404
413
|
try:
|
405
414
|
rs: Result = await self.axecute(params, result=result, event=event)
|
406
|
-
if to is not None: # pragma: no cov
|
407
|
-
return self.set_outputs(rs.context, to=to)
|
408
415
|
return rs
|
409
416
|
except Exception as e: # pragma: no cov
|
410
417
|
await result.trace.aerror(f"[STAGE]: {e.__class__.__name__}: {e}")
|
@@ -419,9 +426,6 @@ class BaseAsyncStage(BaseStage):
|
|
419
426
|
) from None
|
420
427
|
|
421
428
|
errors: DictData = {"errors": to_dict(e)}
|
422
|
-
if to is not None:
|
423
|
-
return self.set_outputs(errors, to=to)
|
424
|
-
|
425
429
|
return result.catch(status=FAILED, context=errors)
|
426
430
|
|
427
431
|
|
@@ -439,12 +443,13 @@ class EmptyStage(BaseAsyncStage):
|
|
439
443
|
|
440
444
|
echo: Optional[str] = Field(
|
441
445
|
default=None,
|
442
|
-
description="A string
|
446
|
+
description="A string message that want to show on the stdout.",
|
443
447
|
)
|
444
448
|
sleep: float = Field(
|
445
449
|
default=0,
|
446
|
-
description="A second value to sleep before start execution",
|
450
|
+
description="A second value to sleep before start execution.",
|
447
451
|
ge=0,
|
452
|
+
lt=1800,
|
448
453
|
)
|
449
454
|
|
450
455
|
def execute(
|
@@ -471,12 +476,23 @@ class EmptyStage(BaseAsyncStage):
|
|
471
476
|
:rtype: Result
|
472
477
|
"""
|
473
478
|
result: Result = result or Result(
|
474
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
479
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
480
|
+
extras=self.extras,
|
475
481
|
)
|
476
482
|
|
483
|
+
if not self.echo:
|
484
|
+
message: str = "..."
|
485
|
+
else:
|
486
|
+
message: str = param2template(
|
487
|
+
dedent(self.echo), params, extras=self.extras
|
488
|
+
)
|
489
|
+
if "\n" in self.echo:
|
490
|
+
message: str = "\n\t" + message.replace("\n", "\n\t").strip(
|
491
|
+
"\n"
|
492
|
+
)
|
493
|
+
|
477
494
|
result.trace.info(
|
478
|
-
f"[STAGE]: Empty-Execute: {self.name!r}: "
|
479
|
-
f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
|
495
|
+
f"[STAGE]: Empty-Execute: {self.name!r}: ( {message} )"
|
480
496
|
)
|
481
497
|
if self.sleep > 0:
|
482
498
|
if self.sleep > 5:
|
@@ -504,9 +520,10 @@ class EmptyStage(BaseAsyncStage):
|
|
504
520
|
|
505
521
|
:rtype: Result
|
506
522
|
"""
|
507
|
-
if result is None:
|
523
|
+
if result is None:
|
508
524
|
result: Result = Result(
|
509
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
525
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
526
|
+
extras=self.extras,
|
510
527
|
)
|
511
528
|
|
512
529
|
await result.trace.ainfo(
|
@@ -547,8 +564,8 @@ class BashStage(BaseStage):
|
|
547
564
|
env: DictStr = Field(
|
548
565
|
default_factory=dict,
|
549
566
|
description=(
|
550
|
-
"An environment
|
551
|
-
"
|
567
|
+
"An environment variables that set before start execute by adding "
|
568
|
+
"on the header of the `.sh` file."
|
552
569
|
),
|
553
570
|
)
|
554
571
|
|
@@ -610,7 +627,8 @@ class BashStage(BaseStage):
|
|
610
627
|
"""
|
611
628
|
if result is None: # pragma: no cov
|
612
629
|
result: Result = Result(
|
613
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
630
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
631
|
+
extras=self.extras,
|
614
632
|
)
|
615
633
|
|
616
634
|
bash: str = param2template(
|
@@ -623,7 +641,7 @@ class BashStage(BaseStage):
|
|
623
641
|
env=param2template(self.env, params, extras=self.extras),
|
624
642
|
run_id=result.run_id,
|
625
643
|
) as sh:
|
626
|
-
result.trace.debug(f"...
|
644
|
+
result.trace.debug(f"... Create `{sh[1]}` file.")
|
627
645
|
rs: CompletedProcess = subprocess.run(
|
628
646
|
sh, shell=False, capture_output=True, text=True
|
629
647
|
)
|
@@ -667,18 +685,20 @@ class PyStage(BaseStage):
|
|
667
685
|
"""
|
668
686
|
|
669
687
|
run: str = Field(
|
670
|
-
description="A Python string statement that want to run with exec
|
688
|
+
description="A Python string statement that want to run with `exec`.",
|
671
689
|
)
|
672
690
|
vars: DictData = Field(
|
673
691
|
default_factory=dict,
|
674
692
|
description=(
|
675
|
-
"A mapping
|
693
|
+
"A variable mapping that want to pass to globals parameter in the "
|
694
|
+
"`exec` func."
|
676
695
|
),
|
677
696
|
)
|
678
697
|
|
679
698
|
@staticmethod
|
680
699
|
def filter_locals(values: DictData) -> Iterator[str]:
|
681
|
-
"""Filter a locals
|
700
|
+
"""Filter a locals mapping values that be module, class, or
|
701
|
+
__annotations__.
|
682
702
|
|
683
703
|
:param values: (DictData) A locals values that want to filter.
|
684
704
|
|
@@ -735,7 +755,8 @@ class PyStage(BaseStage):
|
|
735
755
|
"""
|
736
756
|
if result is None: # pragma: no cov
|
737
757
|
result: Result = Result(
|
738
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
758
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
759
|
+
extras=self.extras,
|
739
760
|
)
|
740
761
|
|
741
762
|
lc: DictData = {}
|
@@ -748,11 +769,11 @@ class PyStage(BaseStage):
|
|
748
769
|
|
749
770
|
# NOTE: Start exec the run statement.
|
750
771
|
result.trace.info(f"[STAGE]: Py-Execute: {self.name}")
|
751
|
-
result.trace.warning(
|
752
|
-
|
753
|
-
|
754
|
-
)
|
755
|
-
|
772
|
+
# result.trace.warning(
|
773
|
+
# "[STAGE]: This stage allow use `eval` function, so, please "
|
774
|
+
# "check your statement be safe before execute."
|
775
|
+
# )
|
776
|
+
#
|
756
777
|
# WARNING: The exec build-in function is very dangerous. So, it
|
757
778
|
# should use the re module to validate exec-string before running.
|
758
779
|
exec(
|
@@ -818,11 +839,17 @@ class CallStage(BaseStage):
|
|
818
839
|
:param event: (Event) An event manager that use to track parent execute
|
819
840
|
was not force stopped.
|
820
841
|
|
842
|
+
:raise ValueError: If necessary arguments does not pass from the `args`
|
843
|
+
field.
|
844
|
+
:raise TypeError: If the result from the caller function does not by
|
845
|
+
a `dict` type.
|
846
|
+
|
821
847
|
:rtype: Result
|
822
848
|
"""
|
823
849
|
if result is None: # pragma: no cov
|
824
850
|
result: Result = Result(
|
825
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
851
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
852
|
+
extras=self.extras,
|
826
853
|
)
|
827
854
|
|
828
855
|
has_keyword: bool = False
|
@@ -880,7 +907,7 @@ class CallStage(BaseStage):
|
|
880
907
|
# VALIDATE:
|
881
908
|
# Check the result type from call function, it should be dict.
|
882
909
|
if isinstance(rs, BaseModel):
|
883
|
-
rs: DictData = rs.model_dump()
|
910
|
+
rs: DictData = rs.model_dump(by_alias=True)
|
884
911
|
elif not isinstance(rs, dict):
|
885
912
|
raise TypeError(
|
886
913
|
f"Return type: '{call_func.name}@{call_func.tag}' does not "
|
@@ -896,6 +923,13 @@ class CallStage(BaseStage):
|
|
896
923
|
) -> DictData:
|
897
924
|
"""Parse Pydantic model from any dict data before parsing to target
|
898
925
|
caller function.
|
926
|
+
|
927
|
+
:param func: A tag function that want to get typing.
|
928
|
+
:param args: An arguments before passing to this tag function.
|
929
|
+
:param result: (Result) A result object for keeping context and status
|
930
|
+
data.
|
931
|
+
|
932
|
+
:rtype: DictData
|
899
933
|
"""
|
900
934
|
try:
|
901
935
|
type_hints: dict[str, Any] = get_type_hints(func)
|
@@ -942,7 +976,7 @@ class TriggerStage(BaseStage):
|
|
942
976
|
|
943
977
|
trigger: str = Field(
|
944
978
|
description=(
|
945
|
-
"A trigger workflow name that should
|
979
|
+
"A trigger workflow name that should exist on the config path."
|
946
980
|
),
|
947
981
|
)
|
948
982
|
params: DictData = Field(
|
@@ -968,26 +1002,37 @@ class TriggerStage(BaseStage):
|
|
968
1002
|
|
969
1003
|
:rtype: Result
|
970
1004
|
"""
|
971
|
-
|
972
|
-
from . import Workflow
|
1005
|
+
from .exceptions import WorkflowException
|
1006
|
+
from .workflow import Workflow
|
973
1007
|
|
974
|
-
if result is None:
|
1008
|
+
if result is None:
|
975
1009
|
result: Result = Result(
|
976
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1010
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
1011
|
+
extras=self.extras,
|
977
1012
|
)
|
978
1013
|
|
979
|
-
# NOTE: Loading workflow object from trigger name.
|
980
1014
|
_trigger: str = param2template(self.trigger, params, extras=self.extras)
|
981
|
-
|
982
|
-
# NOTE: Set running workflow ID from running stage ID to external
|
983
|
-
# params on Loader object.
|
984
|
-
workflow: Workflow = Workflow.from_conf(_trigger, extras=self.extras)
|
985
1015
|
result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
|
986
|
-
|
987
|
-
|
988
|
-
|
989
|
-
|
990
|
-
|
1016
|
+
try:
|
1017
|
+
rs: Result = Workflow.from_conf(
|
1018
|
+
name=_trigger,
|
1019
|
+
extras=self.extras | {"stage_raise_error": True},
|
1020
|
+
).execute(
|
1021
|
+
params=param2template(self.params, params, extras=self.extras),
|
1022
|
+
parent_run_id=result.run_id,
|
1023
|
+
event=event,
|
1024
|
+
)
|
1025
|
+
except WorkflowException as e:
|
1026
|
+
raise StageException("Trigger workflow stage was failed") from e
|
1027
|
+
|
1028
|
+
if rs.status == FAILED:
|
1029
|
+
err_msg: str | None = (
|
1030
|
+
f" with:\n{msg}"
|
1031
|
+
if (msg := rs.context.get("errors", {}).get("message"))
|
1032
|
+
else ""
|
1033
|
+
)
|
1034
|
+
raise StageException(f"Trigger workflow was failed{err_msg}.")
|
1035
|
+
return rs
|
991
1036
|
|
992
1037
|
|
993
1038
|
class ParallelStage(BaseStage): # pragma: no cov
|
@@ -1019,17 +1064,25 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1019
1064
|
"""
|
1020
1065
|
|
1021
1066
|
parallel: dict[str, list[Stage]] = Field(
|
1022
|
-
description="A mapping of parallel branch
|
1067
|
+
description="A mapping of parallel branch name and stages.",
|
1068
|
+
)
|
1069
|
+
max_workers: int = Field(
|
1070
|
+
default=2,
|
1071
|
+
ge=1,
|
1072
|
+
lt=20,
|
1073
|
+
description=(
|
1074
|
+
"The maximum thread pool worker size for execution parallel."
|
1075
|
+
),
|
1076
|
+
alias="max-workers",
|
1023
1077
|
)
|
1024
|
-
max_parallel_core: int = Field(default=2)
|
1025
1078
|
|
1026
|
-
|
1027
|
-
|
1079
|
+
def execute_task(
|
1080
|
+
self,
|
1028
1081
|
branch: str,
|
1029
1082
|
params: DictData,
|
1030
1083
|
result: Result,
|
1031
|
-
stages: list[Stage],
|
1032
1084
|
*,
|
1085
|
+
event: Event | None = None,
|
1033
1086
|
extras: DictData | None = None,
|
1034
1087
|
) -> DictData:
|
1035
1088
|
"""Task execution method for passing a branch to each thread.
|
@@ -1038,32 +1091,82 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1038
1091
|
:param params: A parameter data that want to use in this execution.
|
1039
1092
|
:param result: (Result) A result object for keeping context and status
|
1040
1093
|
data.
|
1041
|
-
:param
|
1042
|
-
|
1094
|
+
:param event: (Event) An event manager that use to track parent execute
|
1095
|
+
was not force stopped.
|
1096
|
+
:param extras: (DictData) An extra parameters that want to override
|
1097
|
+
config values.
|
1043
1098
|
|
1044
1099
|
:rtype: DictData
|
1045
1100
|
"""
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1101
|
+
result.trace.debug(f"... Execute branch: {branch!r}")
|
1102
|
+
context: DictData = copy.deepcopy(params)
|
1103
|
+
context.update({"branch": branch, "stages": {}})
|
1104
|
+
for stage in self.parallel[branch]:
|
1105
|
+
|
1049
1106
|
if extras:
|
1050
1107
|
stage.extras = extras
|
1051
1108
|
|
1109
|
+
if stage.is_skipped(params=context):
|
1110
|
+
result.trace.info(f"... Skip stage: {stage.iden!r}")
|
1111
|
+
stage.set_outputs(output={"skipped": True}, to=context)
|
1112
|
+
continue
|
1113
|
+
|
1114
|
+
if event and event.is_set():
|
1115
|
+
error_msg: str = (
|
1116
|
+
"Branch-Stage was canceled from event that had set before "
|
1117
|
+
"stage item execution."
|
1118
|
+
)
|
1119
|
+
return result.catch(
|
1120
|
+
status=CANCEL,
|
1121
|
+
parallel={
|
1122
|
+
branch: {
|
1123
|
+
"branch": branch,
|
1124
|
+
"stages": filter_func(context.pop("stages", {})),
|
1125
|
+
"errors": StageException(error_msg).to_dict(),
|
1126
|
+
}
|
1127
|
+
},
|
1128
|
+
)
|
1129
|
+
|
1052
1130
|
try:
|
1053
|
-
stage.
|
1054
|
-
|
1055
|
-
|
1056
|
-
|
1057
|
-
|
1058
|
-
|
1059
|
-
to=context,
|
1131
|
+
rs: Result = stage.handler_execute(
|
1132
|
+
params=context,
|
1133
|
+
run_id=result.run_id,
|
1134
|
+
parent_run_id=result.parent_run_id,
|
1135
|
+
raise_error=True,
|
1136
|
+
event=event,
|
1060
1137
|
)
|
1138
|
+
stage.set_outputs(rs.context, to=context)
|
1061
1139
|
except StageException as e: # pragma: no cov
|
1062
|
-
result.trace.error(
|
1063
|
-
|
1140
|
+
result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
|
1141
|
+
raise StageException(
|
1142
|
+
f"Sub-Stage execution error: {e.__class__.__name__}: {e}"
|
1143
|
+
) from None
|
1144
|
+
|
1145
|
+
if rs.status == FAILED:
|
1146
|
+
error_msg: str = (
|
1147
|
+
f"Item-Stage was break because it has a sub stage, "
|
1148
|
+
f"{stage.iden}, failed without raise error."
|
1149
|
+
)
|
1150
|
+
return result.catch(
|
1151
|
+
status=FAILED,
|
1152
|
+
parallel={
|
1153
|
+
branch: {
|
1154
|
+
"branch": branch,
|
1155
|
+
"stages": filter_func(context.pop("stages", {})),
|
1156
|
+
"errors": StageException(error_msg).to_dict(),
|
1157
|
+
},
|
1158
|
+
},
|
1064
1159
|
)
|
1065
|
-
|
1066
|
-
return
|
1160
|
+
|
1161
|
+
return result.catch(
|
1162
|
+
status=SUCCESS,
|
1163
|
+
parallel={
|
1164
|
+
branch: {
|
1165
|
+
"branch": branch,
|
1166
|
+
"stages": filter_func(context.pop("stages", {})),
|
1167
|
+
},
|
1168
|
+
},
|
1169
|
+
)
|
1067
1170
|
|
1068
1171
|
def execute(
|
1069
1172
|
self,
|
@@ -1085,49 +1188,54 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1085
1188
|
"""
|
1086
1189
|
if result is None: # pragma: no cov
|
1087
1190
|
result: Result = Result(
|
1088
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1191
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
1192
|
+
extras=self.extras,
|
1089
1193
|
)
|
1090
|
-
|
1194
|
+
event: Event = Event() if event is None else event
|
1091
1195
|
result.trace.info(
|
1092
|
-
f"[STAGE]: Parallel-Execute
|
1196
|
+
f"[STAGE]: Parallel-Execute: {self.max_workers} workers."
|
1093
1197
|
)
|
1094
|
-
|
1095
|
-
status = SUCCESS
|
1198
|
+
result.catch(status=WAIT, context={"parallel": {}})
|
1096
1199
|
with ThreadPoolExecutor(
|
1097
|
-
max_workers=self.
|
1200
|
+
max_workers=self.max_workers,
|
1098
1201
|
thread_name_prefix="parallel_stage_exec_",
|
1099
1202
|
) as executor:
|
1100
1203
|
|
1101
|
-
|
1102
|
-
|
1103
|
-
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
|
1108
|
-
|
1109
|
-
|
1110
|
-
|
1111
|
-
|
1204
|
+
context: DictData = {}
|
1205
|
+
status: Status = SUCCESS
|
1206
|
+
|
1207
|
+
futures: list[Future] = (
|
1208
|
+
executor.submit(
|
1209
|
+
self.execute_task,
|
1210
|
+
branch=branch,
|
1211
|
+
params=params,
|
1212
|
+
result=result,
|
1213
|
+
event=event,
|
1214
|
+
extras=self.extras,
|
1112
1215
|
)
|
1216
|
+
for branch in self.parallel
|
1217
|
+
)
|
1113
1218
|
|
1114
1219
|
done = as_completed(futures, timeout=1800)
|
1115
1220
|
for future in done:
|
1116
|
-
|
1117
|
-
|
1118
|
-
|
1119
|
-
if "errors" in context:
|
1221
|
+
try:
|
1222
|
+
future.result()
|
1223
|
+
except StageException as e:
|
1120
1224
|
status = FAILED
|
1225
|
+
result.trace.error(
|
1226
|
+
f"[STAGE]: {e.__class__.__name__}:\n\t{e}"
|
1227
|
+
)
|
1228
|
+
context.update({"errors": e.to_dict()})
|
1121
1229
|
|
1122
|
-
return result.catch(status=status, context=
|
1230
|
+
return result.catch(status=status, context=context)
|
1123
1231
|
|
1124
1232
|
|
1125
1233
|
class ForEachStage(BaseStage):
|
1126
1234
|
"""For-Each execution stage that execute child stages with an item in list
|
1127
|
-
of item values.
|
1235
|
+
of item values. This stage is not the low-level stage model because it runs
|
1236
|
+
muti-stages in this stage execution.
|
1128
1237
|
|
1129
|
-
|
1130
|
-
in this stage execution.
|
1238
|
+
The concept of this stage use the same logic of the Job execution.
|
1131
1239
|
|
1132
1240
|
Data Validate:
|
1133
1241
|
>>> stage = {
|
@@ -1167,46 +1275,93 @@ class ForEachStage(BaseStage):
|
|
1167
1275
|
self,
|
1168
1276
|
item: Union[str, int],
|
1169
1277
|
params: DictData,
|
1170
|
-
context: DictData,
|
1171
1278
|
result: Result,
|
1172
|
-
|
1279
|
+
*,
|
1280
|
+
event: Event | None = None,
|
1281
|
+
) -> Result:
|
1173
1282
|
"""Execute foreach item from list of item.
|
1174
1283
|
|
1175
1284
|
:param item: (str | int) An item that want to execution.
|
1176
1285
|
:param params: (DictData) A parameter that want to pass to stage
|
1177
1286
|
execution.
|
1178
|
-
:param
|
1179
|
-
|
1287
|
+
:param result: (Result) A result object for keeping context and status
|
1288
|
+
data.
|
1289
|
+
:param event: (Event) An event manager that use to track parent execute
|
1290
|
+
was not force stopped.
|
1180
1291
|
|
1181
|
-
:
|
1292
|
+
:raise StageException: If the stage execution raise errors.
|
1293
|
+
|
1294
|
+
:rtype: Result
|
1182
1295
|
"""
|
1183
|
-
result.trace.debug(f"
|
1184
|
-
|
1185
|
-
|
1186
|
-
status: Status = SUCCESS
|
1296
|
+
result.trace.debug(f"... Execute item: {item!r}")
|
1297
|
+
context: DictData = copy.deepcopy(params)
|
1298
|
+
context.update({"item": item, "stages": {}})
|
1187
1299
|
for stage in self.stages:
|
1188
1300
|
|
1189
1301
|
if self.extras:
|
1190
1302
|
stage.extras = self.extras
|
1191
1303
|
|
1304
|
+
if stage.is_skipped(params=context):
|
1305
|
+
result.trace.info(f"... Skip stage: {stage.iden!r}")
|
1306
|
+
stage.set_outputs(output={"skipped": True}, to=context)
|
1307
|
+
continue
|
1308
|
+
|
1309
|
+
if event and event.is_set(): # pragma: no cov
|
1310
|
+
error_msg: str = (
|
1311
|
+
"Item-Stage was canceled from event that had set before "
|
1312
|
+
"stage item execution."
|
1313
|
+
)
|
1314
|
+
return result.catch(
|
1315
|
+
status=CANCEL,
|
1316
|
+
foreach={
|
1317
|
+
item: {
|
1318
|
+
"item": item,
|
1319
|
+
"stages": filter_func(context.pop("stages", {})),
|
1320
|
+
"errors": StageException(error_msg).to_dict(),
|
1321
|
+
}
|
1322
|
+
},
|
1323
|
+
)
|
1324
|
+
|
1192
1325
|
try:
|
1193
|
-
stage.
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
to=to,
|
1326
|
+
rs: Result = stage.handler_execute(
|
1327
|
+
params=context,
|
1328
|
+
run_id=result.run_id,
|
1329
|
+
parent_run_id=result.parent_run_id,
|
1330
|
+
raise_error=True,
|
1331
|
+
event=event,
|
1200
1332
|
)
|
1201
|
-
|
1202
|
-
|
1203
|
-
result.trace.error(
|
1204
|
-
|
1333
|
+
stage.set_outputs(rs.context, to=context)
|
1334
|
+
except (StageException, UtilException) as e:
|
1335
|
+
result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
|
1336
|
+
raise StageException(
|
1337
|
+
f"Sub-Stage execution error: {e.__class__.__name__}: {e}"
|
1338
|
+
) from None
|
1339
|
+
|
1340
|
+
if rs.status == FAILED:
|
1341
|
+
error_msg: str = (
|
1342
|
+
f"Item-Stage was break because it has a sub stage, "
|
1343
|
+
f"{stage.iden}, failed without raise error."
|
1344
|
+
)
|
1345
|
+
return result.catch(
|
1346
|
+
status=FAILED,
|
1347
|
+
foreach={
|
1348
|
+
item: {
|
1349
|
+
"item": item,
|
1350
|
+
"stages": filter_func(context.pop("stages", {})),
|
1351
|
+
"errors": StageException(error_msg).to_dict(),
|
1352
|
+
},
|
1353
|
+
},
|
1205
1354
|
)
|
1206
|
-
to.update({"errors": e.to_dict()})
|
1207
1355
|
|
1208
|
-
|
1209
|
-
|
1356
|
+
return result.catch(
|
1357
|
+
status=SUCCESS,
|
1358
|
+
foreach={
|
1359
|
+
item: {
|
1360
|
+
"item": item,
|
1361
|
+
"stages": filter_func(context.pop("stages", {})),
|
1362
|
+
},
|
1363
|
+
},
|
1364
|
+
)
|
1210
1365
|
|
1211
1366
|
def execute(
|
1212
1367
|
self,
|
@@ -1227,9 +1382,10 @@ class ForEachStage(BaseStage):
|
|
1227
1382
|
"""
|
1228
1383
|
if result is None: # pragma: no cov
|
1229
1384
|
result: Result = Result(
|
1230
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1385
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
1386
|
+
extras=self.extras,
|
1231
1387
|
)
|
1232
|
-
|
1388
|
+
event: Event = Event() if event is None else event
|
1233
1389
|
foreach: Union[list[str], list[int]] = (
|
1234
1390
|
param2template(self.foreach, params, extras=self.extras)
|
1235
1391
|
if isinstance(self.foreach, str)
|
@@ -1241,24 +1397,58 @@ class ForEachStage(BaseStage):
|
|
1241
1397
|
)
|
1242
1398
|
|
1243
1399
|
result.trace.info(f"[STAGE]: Foreach-Execute: {foreach!r}.")
|
1244
|
-
context
|
1245
|
-
|
1246
|
-
|
1400
|
+
result.catch(status=WAIT, context={"items": foreach, "foreach": {}})
|
1401
|
+
if event and event.is_set(): # pragma: no cov
|
1402
|
+
return result.catch(
|
1403
|
+
status=CANCEL,
|
1404
|
+
context={
|
1405
|
+
"errors": StageException(
|
1406
|
+
"Stage was canceled from event that had set "
|
1407
|
+
"before stage foreach execution."
|
1408
|
+
).to_dict()
|
1409
|
+
},
|
1410
|
+
)
|
1411
|
+
|
1412
|
+
with ThreadPoolExecutor(
|
1413
|
+
max_workers=self.concurrent, thread_name_prefix="stage_foreach_"
|
1414
|
+
) as executor:
|
1415
|
+
|
1247
1416
|
futures: list[Future] = [
|
1248
1417
|
executor.submit(
|
1249
1418
|
self.execute_item,
|
1250
1419
|
item=item,
|
1251
|
-
params=params
|
1252
|
-
context=context,
|
1420
|
+
params=params,
|
1253
1421
|
result=result,
|
1422
|
+
event=event,
|
1254
1423
|
)
|
1255
1424
|
for item in foreach
|
1256
1425
|
]
|
1257
|
-
|
1258
|
-
|
1259
|
-
|
1426
|
+
context: DictData = {}
|
1427
|
+
status: Status = SUCCESS
|
1428
|
+
|
1429
|
+
done, not_done = wait(
|
1430
|
+
futures, timeout=1800, return_when=FIRST_EXCEPTION
|
1431
|
+
)
|
1432
|
+
|
1433
|
+
if len(done) != len(futures):
|
1434
|
+
result.trace.warning(
|
1435
|
+
"[STAGE]: Set the event for stop running stage."
|
1436
|
+
)
|
1437
|
+
event.set()
|
1438
|
+
for future in not_done:
|
1439
|
+
future.cancel()
|
1260
1440
|
|
1261
|
-
|
1441
|
+
for future in done:
|
1442
|
+
try:
|
1443
|
+
future.result()
|
1444
|
+
except StageException as e:
|
1445
|
+
status = FAILED
|
1446
|
+
result.trace.error(
|
1447
|
+
f"[STAGE]: {e.__class__.__name__}:\n\t{e}"
|
1448
|
+
)
|
1449
|
+
context.update({"errors": e.to_dict()})
|
1450
|
+
|
1451
|
+
return result.catch(status=status, context=context)
|
1262
1452
|
|
1263
1453
|
|
1264
1454
|
class UntilStage(BaseStage): # pragma: no cov
|
@@ -1278,7 +1468,12 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1278
1468
|
... }
|
1279
1469
|
"""
|
1280
1470
|
|
1281
|
-
item: Union[str, int, bool] = Field(
|
1471
|
+
item: Union[str, int, bool] = Field(
|
1472
|
+
default=0,
|
1473
|
+
description=(
|
1474
|
+
"An initial value that can be any value in str, int, or bool type."
|
1475
|
+
),
|
1476
|
+
)
|
1282
1477
|
until: str = Field(description="A until condition.")
|
1283
1478
|
stages: list[Stage] = Field(
|
1284
1479
|
default_factory=list,
|
@@ -1287,11 +1482,12 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1287
1482
|
"correct."
|
1288
1483
|
),
|
1289
1484
|
)
|
1290
|
-
|
1485
|
+
max_loop: int = Field(
|
1291
1486
|
default=10,
|
1292
1487
|
ge=1,
|
1293
1488
|
lt=100,
|
1294
1489
|
description="The maximum value of loop for this until stage.",
|
1490
|
+
alias="max-loop",
|
1295
1491
|
)
|
1296
1492
|
|
1297
1493
|
def execute_item(
|
@@ -1299,9 +1495,9 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1299
1495
|
item: T,
|
1300
1496
|
loop: int,
|
1301
1497
|
params: DictData,
|
1302
|
-
context: DictData,
|
1303
1498
|
result: Result,
|
1304
|
-
|
1499
|
+
event: Event | None = None,
|
1500
|
+
) -> tuple[Result, T]:
|
1305
1501
|
"""Execute until item set item by some stage or by default loop
|
1306
1502
|
variable.
|
1307
1503
|
|
@@ -1309,43 +1505,81 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1309
1505
|
:param loop: (int) A number of loop.
|
1310
1506
|
:param params: (DictData) A parameter that want to pass to stage
|
1311
1507
|
execution.
|
1312
|
-
:param
|
1313
|
-
|
1508
|
+
:param result: (Result) A result object for keeping context and status
|
1509
|
+
data.
|
1510
|
+
:param event: (Event) An event manager that use to track parent execute
|
1511
|
+
was not force stopped.
|
1314
1512
|
|
1315
|
-
:rtype: tuple[
|
1513
|
+
:rtype: tuple[Result, T]
|
1316
1514
|
"""
|
1317
|
-
result.trace.debug(f"
|
1318
|
-
|
1319
|
-
|
1320
|
-
status: Status = SUCCESS
|
1515
|
+
result.trace.debug(f"... Execute until item: {item!r}")
|
1516
|
+
context: DictData = copy.deepcopy(params)
|
1517
|
+
context.update({"loop": loop, "item": item, "stages": {}})
|
1321
1518
|
next_item: T = None
|
1322
1519
|
for stage in self.stages:
|
1323
1520
|
|
1324
1521
|
if self.extras:
|
1325
1522
|
stage.extras = self.extras
|
1326
1523
|
|
1524
|
+
if stage.is_skipped(params=context):
|
1525
|
+
result.trace.info(f"... Skip stage: {stage.iden!r}")
|
1526
|
+
stage.set_outputs(output={"skipped": True}, to=context)
|
1527
|
+
continue
|
1528
|
+
|
1529
|
+
if event and event.is_set():
|
1530
|
+
error_msg: str = (
|
1531
|
+
"Item-Stage was canceled from event that had set before "
|
1532
|
+
"stage item execution."
|
1533
|
+
)
|
1534
|
+
return (
|
1535
|
+
result.catch(
|
1536
|
+
status=CANCEL,
|
1537
|
+
until={
|
1538
|
+
loop: {
|
1539
|
+
"loop": loop,
|
1540
|
+
"item": item,
|
1541
|
+
"stages": filter_func(
|
1542
|
+
context.pop("stages", {})
|
1543
|
+
),
|
1544
|
+
"errors": StageException(error_msg).to_dict(),
|
1545
|
+
}
|
1546
|
+
},
|
1547
|
+
),
|
1548
|
+
next_item,
|
1549
|
+
)
|
1550
|
+
|
1327
1551
|
try:
|
1328
|
-
stage.
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
|
1334
|
-
to=to,
|
1552
|
+
rs: Result = stage.handler_execute(
|
1553
|
+
params=context,
|
1554
|
+
run_id=result.run_id,
|
1555
|
+
parent_run_id=result.parent_run_id,
|
1556
|
+
raise_error=True,
|
1557
|
+
event=event,
|
1335
1558
|
)
|
1559
|
+
stage.set_outputs(rs.context, to=context)
|
1336
1560
|
if "item" in (
|
1337
|
-
outputs := stage.get_outputs(
|
1561
|
+
outputs := stage.get_outputs(context).get("outputs", {})
|
1338
1562
|
):
|
1339
1563
|
next_item = outputs["item"]
|
1340
|
-
except StageException as e:
|
1341
|
-
|
1342
|
-
|
1343
|
-
f"
|
1344
|
-
)
|
1345
|
-
to.update({"errors": e.to_dict()})
|
1564
|
+
except (StageException, UtilException) as e:
|
1565
|
+
result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
|
1566
|
+
raise StageException(
|
1567
|
+
f"Sub-Stage execution error: {e.__class__.__name__}: {e}"
|
1568
|
+
) from None
|
1346
1569
|
|
1347
|
-
|
1348
|
-
|
1570
|
+
return (
|
1571
|
+
result.catch(
|
1572
|
+
status=SUCCESS,
|
1573
|
+
until={
|
1574
|
+
loop: {
|
1575
|
+
"loop": loop,
|
1576
|
+
"item": item,
|
1577
|
+
"stages": filter_func(context.pop("stages", {})),
|
1578
|
+
}
|
1579
|
+
},
|
1580
|
+
),
|
1581
|
+
next_item,
|
1582
|
+
)
|
1349
1583
|
|
1350
1584
|
def execute(
|
1351
1585
|
self,
|
@@ -1370,22 +1604,33 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1370
1604
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1371
1605
|
)
|
1372
1606
|
|
1607
|
+
result.trace.info(f"[STAGE]: Until-Execution: {self.until}")
|
1373
1608
|
item: Union[str, int, bool] = param2template(
|
1374
1609
|
self.item, params, extras=self.extras
|
1375
1610
|
)
|
1376
|
-
|
1611
|
+
loop: int = 1
|
1377
1612
|
track: bool = True
|
1378
1613
|
exceed_loop: bool = False
|
1379
|
-
|
1380
|
-
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1614
|
+
result.catch(status=WAIT, context={"until": {}})
|
1615
|
+
while track and not (exceed_loop := loop >= self.max_loop):
|
1616
|
+
|
1617
|
+
if event and event.is_set():
|
1618
|
+
return result.catch(
|
1619
|
+
status=CANCEL,
|
1620
|
+
context={
|
1621
|
+
"errors": StageException(
|
1622
|
+
"Stage was canceled from event that had set "
|
1623
|
+
"before stage until execution."
|
1624
|
+
).to_dict()
|
1625
|
+
},
|
1626
|
+
)
|
1627
|
+
|
1628
|
+
result, item = self.execute_item(
|
1384
1629
|
item=item,
|
1385
1630
|
loop=loop,
|
1386
|
-
params=params
|
1387
|
-
context=context,
|
1631
|
+
params=params,
|
1388
1632
|
result=result,
|
1633
|
+
event=event,
|
1389
1634
|
)
|
1390
1635
|
|
1391
1636
|
loop += 1
|
@@ -1398,34 +1643,36 @@ class UntilStage(BaseStage): # pragma: no cov
|
|
1398
1643
|
|
1399
1644
|
next_track: bool = eval(
|
1400
1645
|
param2template(
|
1401
|
-
self.until,
|
1646
|
+
self.until,
|
1647
|
+
params | {"item": item, "loop": loop},
|
1648
|
+
extras=self.extras,
|
1402
1649
|
),
|
1403
1650
|
globals() | params | {"item": item},
|
1404
1651
|
{},
|
1405
1652
|
)
|
1406
1653
|
if not isinstance(next_track, bool):
|
1407
|
-
raise
|
1654
|
+
raise StageException(
|
1408
1655
|
"Return type of until condition does not be boolean, it"
|
1409
1656
|
f"return: {next_track!r}"
|
1410
1657
|
)
|
1411
|
-
track = not next_track
|
1412
|
-
|
1658
|
+
track: bool = not next_track
|
1659
|
+
delay(0.025)
|
1413
1660
|
|
1414
1661
|
if exceed_loop:
|
1415
1662
|
raise StageException(
|
1416
|
-
f"The until loop was exceed {self.
|
1663
|
+
f"The until loop was exceed {self.max_loop} loops"
|
1417
1664
|
)
|
1418
|
-
return result.catch(status=
|
1665
|
+
return result.catch(status=SUCCESS)
|
1419
1666
|
|
1420
1667
|
|
1421
1668
|
class Match(BaseModel):
|
1422
1669
|
"""Match model for the Case Stage."""
|
1423
1670
|
|
1424
|
-
case: Union[str, int]
|
1425
|
-
stage: Stage
|
1671
|
+
case: Union[str, int] = Field(description="A match case.")
|
1672
|
+
stage: Stage = Field(description="A stage to execution for this case.")
|
1426
1673
|
|
1427
1674
|
|
1428
|
-
class CaseStage(BaseStage):
|
1675
|
+
class CaseStage(BaseStage):
|
1429
1676
|
"""Case execution stage.
|
1430
1677
|
|
1431
1678
|
Data Validate:
|
@@ -1463,6 +1710,14 @@ class CaseStage(BaseStage): # pragma: no cov
|
|
1463
1710
|
match: list[Match] = Field(
|
1464
1711
|
description="A list of Match model that should not be an empty list.",
|
1465
1712
|
)
|
1713
|
+
skip_not_match: bool = Field(
|
1714
|
+
default=False,
|
1715
|
+
description=(
|
1716
|
+
"A flag for making skip if it does not match and else condition "
|
1717
|
+
"does not set too."
|
1718
|
+
),
|
1719
|
+
alias="skip-not-match",
|
1720
|
+
)
|
1466
1721
|
|
1467
1722
|
def execute(
|
1468
1723
|
self,
|
@@ -1483,58 +1738,78 @@ class CaseStage(BaseStage): # pragma: no cov
|
|
1483
1738
|
"""
|
1484
1739
|
if result is None: # pragma: no cov
|
1485
1740
|
result: Result = Result(
|
1486
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1741
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
1742
|
+
extras=self.extras,
|
1487
1743
|
)
|
1488
1744
|
|
1489
|
-
_case = param2template(
|
1745
|
+
_case: Optional[str] = param2template(
|
1746
|
+
self.case, params, extras=self.extras
|
1747
|
+
)
|
1490
1748
|
|
1491
1749
|
result.trace.info(f"[STAGE]: Case-Execute: {_case!r}.")
|
1492
|
-
_else = None
|
1750
|
+
_else: Optional[Match] = None
|
1493
1751
|
stage: Optional[Stage] = None
|
1494
|
-
context = {}
|
1495
|
-
status = SUCCESS
|
1496
1752
|
for match in self.match:
|
1497
|
-
if (c := match.case)
|
1498
|
-
|
1499
|
-
else:
|
1500
|
-
_else = match
|
1753
|
+
if (c := match.case) == "_":
|
1754
|
+
_else: Match = match
|
1501
1755
|
continue
|
1502
1756
|
|
1757
|
+
_condition: str = param2template(c, params, extras=self.extras)
|
1503
1758
|
if stage is None and _case == _condition:
|
1504
1759
|
stage: Stage = match.stage
|
1505
1760
|
|
1506
1761
|
if stage is None:
|
1507
1762
|
if _else is None:
|
1508
|
-
|
1509
|
-
|
1510
|
-
|
1763
|
+
if not self.skip_not_match:
|
1764
|
+
raise StageException(
|
1765
|
+
"This stage does not set else for support not match "
|
1766
|
+
"any case."
|
1767
|
+
)
|
1768
|
+
result.trace.info(
|
1769
|
+
"... Skip this stage because it does not match."
|
1770
|
+
)
|
1771
|
+
error_msg: str = (
|
1772
|
+
"Case-Stage was canceled because it does not match any "
|
1773
|
+
"case and else condition does not set too."
|
1774
|
+
)
|
1775
|
+
return result.catch(
|
1776
|
+
status=CANCEL,
|
1777
|
+
context={"errors": StageException(error_msg).to_dict()},
|
1511
1778
|
)
|
1512
|
-
|
1513
1779
|
stage: Stage = _else.stage
|
1514
1780
|
|
1515
1781
|
if self.extras:
|
1516
1782
|
stage.extras = self.extras
|
1517
1783
|
|
1784
|
+
if event and event.is_set(): # pragma: no cov
|
1785
|
+
return result.catch(
|
1786
|
+
status=CANCEL,
|
1787
|
+
context={
|
1788
|
+
"errors": StageException(
|
1789
|
+
"Stage was canceled from event that had set before "
|
1790
|
+
"case-stage execution."
|
1791
|
+
).to_dict()
|
1792
|
+
},
|
1793
|
+
)
|
1794
|
+
|
1518
1795
|
try:
|
1519
|
-
|
1520
|
-
|
1796
|
+
return result.catch(
|
1797
|
+
status=SUCCESS,
|
1798
|
+
context=stage.handler_execute(
|
1521
1799
|
params=params,
|
1522
1800
|
run_id=result.run_id,
|
1523
1801
|
parent_run_id=result.parent_run_id,
|
1524
|
-
|
1802
|
+
event=event,
|
1803
|
+
).context,
|
1525
1804
|
)
|
1526
1805
|
except StageException as e: # pragma: no cov
|
1527
|
-
|
1528
|
-
result.
|
1529
|
-
f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
|
1530
|
-
)
|
1531
|
-
context.update({"errors": e.to_dict()})
|
1532
|
-
return result.catch(status=status, context=context)
|
1806
|
+
result.trace.error(f"[STAGE]: {e.__class__.__name__}:" f"\n\t{e}")
|
1807
|
+
return result.catch(status=FAILED, context={"errors": e.to_dict()})
|
1533
1808
|
|
1534
1809
|
|
1535
1810
|
class RaiseStage(BaseStage): # pragma: no cov
|
1536
|
-
"""Raise error stage that raise StageException that use a message
|
1537
|
-
making error message before raise.
|
1811
|
+
"""Raise error stage execution that raise StageException that use a message
|
1812
|
+
field for making error message before raise.
|
1538
1813
|
|
1539
1814
|
Data Validate:
|
1540
1815
|
>>> stage = {
|
@@ -1545,7 +1820,9 @@ class RaiseStage(BaseStage): # pragma: no cov
|
|
1545
1820
|
"""
|
1546
1821
|
|
1547
1822
|
message: str = Field(
|
1548
|
-
description=
|
1823
|
+
description=(
|
1824
|
+
"An error message that want to raise with StageException class"
|
1825
|
+
),
|
1549
1826
|
alias="raise",
|
1550
1827
|
)
|
1551
1828
|
|
@@ -1556,13 +1833,22 @@ class RaiseStage(BaseStage): # pragma: no cov
|
|
1556
1833
|
result: Result | None = None,
|
1557
1834
|
event: Event | None = None,
|
1558
1835
|
) -> Result:
|
1559
|
-
"""Raise the
|
1836
|
+
"""Raise the StageException object with the message field execution.
|
1837
|
+
|
1838
|
+
:param params: A parameter that want to pass before run any statement.
|
1839
|
+
:param result: (Result) A result object for keeping context and status
|
1840
|
+
data.
|
1841
|
+
:param event: (Event) An event manager that use to track parent execute
|
1842
|
+
was not force stopped.
|
1843
|
+
"""
|
1560
1844
|
if result is None: # pragma: no cov
|
1561
1845
|
result: Result = Result(
|
1562
|
-
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
1846
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
1847
|
+
extras=self.extras,
|
1563
1848
|
)
|
1564
|
-
|
1565
|
-
|
1849
|
+
message: str = param2template(self.message, params, extras=self.extras)
|
1850
|
+
result.trace.info(f"[STAGE]: Raise-Execute: {message!r}.")
|
1851
|
+
raise StageException(message)
|
1566
1852
|
|
1567
1853
|
|
1568
1854
|
# TODO: Not implement this stages yet
|
@@ -1570,7 +1856,7 @@ class HookStage(BaseStage): # pragma: no cov
|
|
1570
1856
|
"""Hook stage execution."""
|
1571
1857
|
|
1572
1858
|
hook: str
|
1573
|
-
args: DictData
|
1859
|
+
args: DictData = Field(default_factory=dict)
|
1574
1860
|
callback: str
|
1575
1861
|
|
1576
1862
|
def execute(
|
@@ -1593,19 +1879,91 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
1593
1879
|
... "image": "image-name.pkg.com",
|
1594
1880
|
... "env": {
|
1595
1881
|
... "ENV": "dev",
|
1882
|
+
... "DEBUG": "true",
|
1596
1883
|
... },
|
1597
1884
|
... "volume": {
|
1598
1885
|
... "secrets": "/secrets",
|
1599
1886
|
... },
|
1887
|
+
... "auth": {
|
1888
|
+
... "username": "__json_key",
|
1889
|
+
... "password": "${GOOGLE_CREDENTIAL_JSON_STRING}",
|
1890
|
+
... },
|
1600
1891
|
... }
|
1601
1892
|
"""
|
1602
1893
|
|
1603
1894
|
image: str = Field(
|
1604
1895
|
description="A Docker image url with tag that want to run.",
|
1605
1896
|
)
|
1897
|
+
tag: str = Field(default="latest", description="An Docker image tag.")
|
1606
1898
|
env: DictData = Field(default_factory=dict)
|
1607
1899
|
volume: DictData = Field(default_factory=dict)
|
1608
|
-
auth: DictData = Field(
|
1900
|
+
auth: DictData = Field(
|
1901
|
+
default_factory=dict,
|
1902
|
+
description=(
|
1903
|
+
"An authentication of the Docker registry that use in pulling step."
|
1904
|
+
),
|
1905
|
+
)
|
1906
|
+
|
1907
|
+
def execute_task(
|
1908
|
+
self,
|
1909
|
+
params: DictData,
|
1910
|
+
result: Result,
|
1911
|
+
):
|
1912
|
+
from docker import DockerClient
|
1913
|
+
from docker.errors import ContainerError
|
1914
|
+
|
1915
|
+
client = DockerClient(
|
1916
|
+
base_url="unix://var/run/docker.sock", version="auto"
|
1917
|
+
)
|
1918
|
+
|
1919
|
+
resp = client.api.pull(
|
1920
|
+
repository=f"{self.image}",
|
1921
|
+
tag=self.tag,
|
1922
|
+
auth_config=param2template(self.auth, params, extras=self.extras),
|
1923
|
+
stream=True,
|
1924
|
+
decode=True,
|
1925
|
+
)
|
1926
|
+
for line in resp:
|
1927
|
+
result.trace.info(f"... {line}")
|
1928
|
+
|
1929
|
+
unique_image_name: str = f"{self.image}_{datetime.now():%Y%m%d%H%M%S%f}"
|
1930
|
+
container = client.containers.run(
|
1931
|
+
image=f"{self.image}:{self.tag}",
|
1932
|
+
name=unique_image_name,
|
1933
|
+
environment=self.env,
|
1934
|
+
volumes=(
|
1935
|
+
{
|
1936
|
+
Path.cwd()
|
1937
|
+
/ f".docker.{result.run_id}.logs": {
|
1938
|
+
"bind": "/logs",
|
1939
|
+
"mode": "rw",
|
1940
|
+
},
|
1941
|
+
}
|
1942
|
+
| {
|
1943
|
+
Path.cwd() / source: {"bind": target, "mode": "rw"}
|
1944
|
+
for source, target in (
|
1945
|
+
volume.split(":", maxsplit=1) for volume in self.volume
|
1946
|
+
)
|
1947
|
+
}
|
1948
|
+
),
|
1949
|
+
detach=True,
|
1950
|
+
)
|
1951
|
+
|
1952
|
+
for line in container.logs(stream=True, timestamps=True):
|
1953
|
+
result.trace.info(f"... {line.strip().decode()}")
|
1954
|
+
|
1955
|
+
# NOTE: This code copy from the docker package.
|
1956
|
+
exit_status: int = container.wait()["StatusCode"]
|
1957
|
+
if exit_status != 0:
|
1958
|
+
out = container.logs(stdout=False, stderr=True)
|
1959
|
+
container.remove()
|
1960
|
+
raise ContainerError(
|
1961
|
+
container,
|
1962
|
+
exit_status,
|
1963
|
+
None,
|
1964
|
+
f"{self.image}:{self.tag}",
|
1965
|
+
out,
|
1966
|
+
)
|
1609
1967
|
|
1610
1968
|
def execute(
|
1611
1969
|
self,
|
@@ -1639,6 +1997,11 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
1639
1997
|
) -> Result:
|
1640
1998
|
"""Execute the Python statement via Python virtual environment.
|
1641
1999
|
|
2000
|
+
Steps:
|
2001
|
+
- Create python file.
|
2002
|
+
- Create `.venv` and install necessary Python deps.
|
2003
|
+
- Execution python file with uv and specific `.venv`.
|
2004
|
+
|
1642
2005
|
:param params: A parameter that want to pass before run any statement.
|
1643
2006
|
:param result: (Result) A result object for keeping context and status
|
1644
2007
|
data.
|