ddeutil-workflow 0.0.56__py3-none-any.whl → 0.0.58__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__cron.py +26 -12
- ddeutil/workflow/__types.py +1 -0
- ddeutil/workflow/conf.py +21 -9
- ddeutil/workflow/event.py +11 -10
- ddeutil/workflow/exceptions.py +33 -12
- ddeutil/workflow/job.py +89 -58
- ddeutil/workflow/logs.py +59 -37
- ddeutil/workflow/params.py +4 -0
- ddeutil/workflow/result.py +9 -4
- ddeutil/workflow/scheduler.py +15 -9
- ddeutil/workflow/stages.py +441 -171
- ddeutil/workflow/utils.py +37 -6
- ddeutil/workflow/workflow.py +218 -243
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/METADATA +41 -35
- ddeutil_workflow-0.0.58.dist-info/RECORD +31 -0
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.56.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stages.py
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
# [x] Use dynamic config
|
7
6
|
"""Stages module include all stage model that use be the minimum execution layer
|
8
7
|
of this workflow engine. The stage handle the minimize task that run in some
|
9
8
|
thread (same thread at its job owner) that mean it is the lowest executor that
|
@@ -16,11 +15,11 @@ have a lot of use-case, and it should does not worry about it error output.
|
|
16
15
|
So, I will create `handler_execute` for any exception class that raise from
|
17
16
|
the stage execution method.
|
18
17
|
|
19
|
-
Execution --> Ok
|
20
|
-
|
21
|
-
--> Error ┬--( handler )-> Result with `FAILED` (Set `raise_error` flag)
|
18
|
+
Execution --> Ok ┬--( handler )--> Result with `SUCCESS` or `CANCEL`
|
22
19
|
|
|
23
|
-
╰--( handler )
|
20
|
+
╰--( handler )--> Result with `FAILED` (Set `raise_error` flag)
|
21
|
+
|
22
|
+
--> Error ---( handler )--> Raise StageException(...)
|
24
23
|
|
25
24
|
On the context I/O that pass to a stage object at execute process. The
|
26
25
|
execute method receives a `params={"params": {...}}` value for passing template
|
@@ -41,6 +40,7 @@ from abc import ABC, abstractmethod
|
|
41
40
|
from collections.abc import AsyncIterator, Iterator
|
42
41
|
from concurrent.futures import (
|
43
42
|
FIRST_EXCEPTION,
|
43
|
+
CancelledError,
|
44
44
|
Future,
|
45
45
|
ThreadPoolExecutor,
|
46
46
|
as_completed,
|
@@ -58,13 +58,12 @@ from pydantic import BaseModel, Field
|
|
58
58
|
from pydantic.functional_validators import model_validator
|
59
59
|
from typing_extensions import Self
|
60
60
|
|
61
|
-
from .__types import DictData, DictStr, TupleStr
|
61
|
+
from .__types import DictData, DictStr, StrOrInt, TupleStr
|
62
62
|
from .conf import dynamic
|
63
|
-
from .exceptions import StageException,
|
63
|
+
from .exceptions import StageException, to_dict
|
64
64
|
from .result import CANCEL, FAILED, SUCCESS, WAIT, Result, Status
|
65
65
|
from .reusables import TagFunc, extract_call, not_in_template, param2template
|
66
66
|
from .utils import (
|
67
|
-
NEWLINE,
|
68
67
|
delay,
|
69
68
|
filter_func,
|
70
69
|
gen_id,
|
@@ -72,7 +71,6 @@ from .utils import (
|
|
72
71
|
)
|
73
72
|
|
74
73
|
T = TypeVar("T")
|
75
|
-
StrOrInt = Union[str, int]
|
76
74
|
|
77
75
|
|
78
76
|
class BaseStage(BaseModel, ABC):
|
@@ -174,17 +172,23 @@ class BaseStage(BaseModel, ABC):
|
|
174
172
|
|
175
173
|
This stage exception handler still use ok-error concept, but it
|
176
174
|
allows you force catching an output result with error message by
|
177
|
-
specific environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR
|
175
|
+
specific environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR` or set
|
176
|
+
`raise_error` parameter to True.
|
178
177
|
|
179
178
|
Execution --> Ok --> Result
|
180
179
|
|-status: SUCCESS
|
181
180
|
╰-context:
|
182
181
|
╰-outputs: ...
|
183
182
|
|
184
|
-
-->
|
183
|
+
--> Ok --> Result
|
184
|
+
|-status: CANCEL
|
185
|
+
╰-errors:
|
186
|
+
|-name: ...
|
187
|
+
╰-message: ...
|
188
|
+
|
189
|
+
--> Ok --> Result (if `raise_error` was set)
|
185
190
|
|-status: FAILED
|
186
191
|
╰-errors:
|
187
|
-
|-class: ...
|
188
192
|
|-name: ...
|
189
193
|
╰-message: ...
|
190
194
|
|
@@ -201,6 +205,9 @@ class BaseStage(BaseModel, ABC):
|
|
201
205
|
:param event: (Event) An event manager that pass to the stage execution.
|
202
206
|
:param raise_error: (bool) A flag that all this method raise error
|
203
207
|
|
208
|
+
:raise StageException: If the raise_error was set and the execution
|
209
|
+
raise any error.
|
210
|
+
|
204
211
|
:rtype: Result
|
205
212
|
"""
|
206
213
|
result: Result = Result.construct_with_rs_or_id(
|
@@ -210,20 +217,17 @@ class BaseStage(BaseModel, ABC):
|
|
210
217
|
id_logic=self.iden,
|
211
218
|
extras=self.extras,
|
212
219
|
)
|
213
|
-
|
214
220
|
try:
|
215
221
|
return self.execute(params, result=result, event=event)
|
216
222
|
except Exception as e:
|
217
223
|
e_name: str = e.__class__.__name__
|
218
|
-
result.trace.error(f"[STAGE]: Handler
|
224
|
+
result.trace.error(f"[STAGE]: Error Handler:||{e_name}:||{e}")
|
219
225
|
if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
|
220
226
|
if isinstance(e, StageException):
|
221
227
|
raise
|
222
|
-
|
223
228
|
raise StageException(
|
224
|
-
f"{self.__class__.__name__}: {
|
229
|
+
f"{self.__class__.__name__}: {e_name}: {e}"
|
225
230
|
) from e
|
226
|
-
|
227
231
|
return result.catch(status=FAILED, context={"errors": to_dict(e)})
|
228
232
|
|
229
233
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
@@ -273,13 +277,6 @@ class BaseStage(BaseModel, ABC):
|
|
273
277
|
):
|
274
278
|
return to
|
275
279
|
|
276
|
-
_id: str = (
|
277
|
-
param2template(self.id, params=to, extras=self.extras)
|
278
|
-
if self.id
|
279
|
-
else gen_id(
|
280
|
-
param2template(self.name, params=to, extras=self.extras)
|
281
|
-
)
|
282
|
-
)
|
283
280
|
output: DictData = output.copy()
|
284
281
|
errors: DictData = (
|
285
282
|
{"errors": output.pop("errors", {})} if "errors" in output else {}
|
@@ -289,7 +286,7 @@ class BaseStage(BaseModel, ABC):
|
|
289
286
|
if "skipped" in output
|
290
287
|
else {}
|
291
288
|
)
|
292
|
-
to["stages"][
|
289
|
+
to["stages"][self.gen_id(params=to)] = {
|
293
290
|
"outputs": copy.deepcopy(output),
|
294
291
|
**skipping,
|
295
292
|
**errors,
|
@@ -309,15 +306,11 @@ class BaseStage(BaseModel, ABC):
|
|
309
306
|
"stage_default_id", extras=self.extras
|
310
307
|
):
|
311
308
|
return {}
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
else gen_id(
|
317
|
-
param2template(self.name, params=output, extras=self.extras)
|
318
|
-
)
|
309
|
+
return (
|
310
|
+
output.get("stages", {})
|
311
|
+
.get(self.gen_id(params=output), {})
|
312
|
+
.get("outputs", {})
|
319
313
|
)
|
320
|
-
return output.get("stages", {}).get(_id, {}).get("outputs", {})
|
321
314
|
|
322
315
|
def is_skipped(self, params: DictData) -> bool:
|
323
316
|
"""Return true if condition of this stage do not correct. This process
|
@@ -351,6 +344,22 @@ class BaseStage(BaseModel, ABC):
|
|
351
344
|
except Exception as e:
|
352
345
|
raise StageException(f"{e.__class__.__name__}: {e}") from e
|
353
346
|
|
347
|
+
def gen_id(self, params: DictData) -> str:
|
348
|
+
"""Generate stage ID that dynamic use stage's name if it ID does not
|
349
|
+
set.
|
350
|
+
|
351
|
+
:param params: A parameter data.
|
352
|
+
|
353
|
+
:rtype: str
|
354
|
+
"""
|
355
|
+
return (
|
356
|
+
param2template(self.id, params=params, extras=self.extras)
|
357
|
+
if self.id
|
358
|
+
else gen_id(
|
359
|
+
param2template(self.name, params=params, extras=self.extras)
|
360
|
+
)
|
361
|
+
)
|
362
|
+
|
354
363
|
|
355
364
|
class BaseAsyncStage(BaseStage):
|
356
365
|
"""Base Async Stage model to make any stage model allow async execution for
|
@@ -431,15 +440,14 @@ class BaseAsyncStage(BaseStage):
|
|
431
440
|
try:
|
432
441
|
rs: Result = await self.axecute(params, result=result, event=event)
|
433
442
|
return rs
|
434
|
-
except Exception as e:
|
443
|
+
except Exception as e:
|
435
444
|
e_name: str = e.__class__.__name__
|
436
445
|
await result.trace.aerror(f"[STAGE]: Handler {e_name}: {e}")
|
437
446
|
if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
|
438
447
|
if isinstance(e, StageException):
|
439
448
|
raise
|
440
|
-
|
441
449
|
raise StageException(
|
442
|
-
f"{self.__class__.__name__}: {
|
450
|
+
f"{self.__class__.__name__}: {e_name}: {e}"
|
443
451
|
) from None
|
444
452
|
|
445
453
|
return result.catch(status=FAILED, context={"errors": to_dict(e)})
|
@@ -499,18 +507,16 @@ class EmptyStage(BaseAsyncStage):
|
|
499
507
|
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
500
508
|
extras=self.extras,
|
501
509
|
)
|
502
|
-
|
503
|
-
|
504
|
-
message: str = "..."
|
505
|
-
else:
|
506
|
-
message: str = param2template(
|
510
|
+
message: str = (
|
511
|
+
param2template(
|
507
512
|
dedent(self.echo.strip("\n")), params, extras=self.extras
|
508
513
|
)
|
509
|
-
if
|
510
|
-
|
514
|
+
if self.echo
|
515
|
+
else "..."
|
516
|
+
)
|
511
517
|
|
512
518
|
result.trace.info(
|
513
|
-
f"[STAGE]: Empty-
|
519
|
+
f"[STAGE]: Execute Empty-Stage: {self.name!r}: ( {message} )"
|
514
520
|
)
|
515
521
|
if self.sleep > 0:
|
516
522
|
if self.sleep > 5:
|
@@ -540,18 +546,15 @@ class EmptyStage(BaseAsyncStage):
|
|
540
546
|
extras=self.extras,
|
541
547
|
)
|
542
548
|
|
543
|
-
|
544
|
-
|
545
|
-
else:
|
546
|
-
message: str = param2template(
|
549
|
+
message: str = (
|
550
|
+
param2template(
|
547
551
|
dedent(self.echo.strip("\n")), params, extras=self.extras
|
548
552
|
)
|
549
|
-
if
|
550
|
-
|
551
|
-
|
552
|
-
result.trace.info(
|
553
|
-
f"[STAGE]: Empty-Execute: {self.name!r}: ( {message} )"
|
553
|
+
if self.echo
|
554
|
+
else "..."
|
554
555
|
)
|
556
|
+
|
557
|
+
result.trace.info(f"[STAGE]: Empty-Stage: {self.name!r}: ( {message} )")
|
555
558
|
if self.sleep > 0:
|
556
559
|
if self.sleep > 5:
|
557
560
|
await result.trace.ainfo(
|
@@ -561,7 +564,7 @@ class EmptyStage(BaseAsyncStage):
|
|
561
564
|
return result.catch(status=SUCCESS)
|
562
565
|
|
563
566
|
|
564
|
-
class BashStage(
|
567
|
+
class BashStage(BaseAsyncStage):
|
565
568
|
"""Bash stage executor that execute bash script on the current OS.
|
566
569
|
If your current OS is Windows, it will run on the bash from the current WSL.
|
567
570
|
It will use `bash` for Windows OS and use `sh` for Linux OS.
|
@@ -597,7 +600,16 @@ class BashStage(BaseStage):
|
|
597
600
|
@contextlib.asynccontextmanager
|
598
601
|
async def acreate_sh_file(
|
599
602
|
self, bash: str, env: DictStr, run_id: str | None = None
|
600
|
-
) -> AsyncIterator:
|
603
|
+
) -> AsyncIterator[TupleStr]:
|
604
|
+
"""Async create and write `.sh` file with the `aiofiles` package.
|
605
|
+
|
606
|
+
:param bash: (str) A bash statement.
|
607
|
+
:param env: (DictStr) An environment variable that set before run bash.
|
608
|
+
:param run_id: (str | None) A running stage ID that use for writing sh
|
609
|
+
file instead generate by UUID4.
|
610
|
+
|
611
|
+
:rtype: AsyncIterator[TupleStr]
|
612
|
+
"""
|
601
613
|
import aiofiles
|
602
614
|
|
603
615
|
f_name: str = f"{run_id or uuid.uuid4()}.sh"
|
@@ -616,7 +628,7 @@ class BashStage(BaseStage):
|
|
616
628
|
# NOTE: Make this .sh file able to executable.
|
617
629
|
make_exec(f"./{f_name}")
|
618
630
|
|
619
|
-
yield
|
631
|
+
yield f_shebang, f_name
|
620
632
|
|
621
633
|
# Note: Remove .sh file that use to run bash.
|
622
634
|
Path(f"./{f_name}").unlink()
|
@@ -625,9 +637,8 @@ class BashStage(BaseStage):
|
|
625
637
|
def create_sh_file(
|
626
638
|
self, bash: str, env: DictStr, run_id: str | None = None
|
627
639
|
) -> Iterator[TupleStr]:
|
628
|
-
"""
|
629
|
-
|
630
|
-
After that, it will auto delete this file automatic.
|
640
|
+
"""Create and write the `.sh` file before giving this file name to
|
641
|
+
context. After that, it will auto delete this file automatic.
|
631
642
|
|
632
643
|
:param bash: (str) A bash statement.
|
633
644
|
:param env: (DictStr) An environment variable that set before run bash.
|
@@ -635,6 +646,7 @@ class BashStage(BaseStage):
|
|
635
646
|
file instead generate by UUID4.
|
636
647
|
|
637
648
|
:rtype: Iterator[TupleStr]
|
649
|
+
:return: Return context of prepared bash statement that want to execute.
|
638
650
|
"""
|
639
651
|
f_name: str = f"{run_id or uuid.uuid4()}.sh"
|
640
652
|
f_shebang: str = "bash" if sys.platform.startswith("win") else "sh"
|
@@ -652,7 +664,7 @@ class BashStage(BaseStage):
|
|
652
664
|
# NOTE: Make this .sh file able to executable.
|
653
665
|
make_exec(f"./{f_name}")
|
654
666
|
|
655
|
-
yield
|
667
|
+
yield f_shebang, f_name
|
656
668
|
|
657
669
|
# Note: Remove .sh file that use to run bash.
|
658
670
|
Path(f"./{f_name}").unlink()
|
@@ -680,7 +692,7 @@ class BashStage(BaseStage):
|
|
680
692
|
extras=self.extras,
|
681
693
|
)
|
682
694
|
|
683
|
-
result.trace.info(f"[STAGE]: Shell-
|
695
|
+
result.trace.info(f"[STAGE]: Execute Shell-Stage: {self.name}")
|
684
696
|
|
685
697
|
bash: str = param2template(
|
686
698
|
dedent(self.bash.strip("\n")), params, extras=self.extras
|
@@ -691,21 +703,74 @@ class BashStage(BaseStage):
|
|
691
703
|
env=param2template(self.env, params, extras=self.extras),
|
692
704
|
run_id=result.run_id,
|
693
705
|
) as sh:
|
694
|
-
result.trace.debug(f"... Create `{sh[1]}` file.")
|
706
|
+
result.trace.debug(f"[STAGE]: ... Create `{sh[1]}` file.")
|
695
707
|
rs: CompletedProcess = subprocess.run(
|
696
|
-
sh,
|
708
|
+
sh,
|
709
|
+
shell=False,
|
710
|
+
check=False,
|
711
|
+
capture_output=True,
|
712
|
+
text=True,
|
713
|
+
encoding="utf-8",
|
714
|
+
)
|
715
|
+
if rs.returncode > 0:
|
716
|
+
e: str = rs.stderr.removesuffix("\n")
|
717
|
+
raise StageException(
|
718
|
+
f"Subprocess: {e}\n---( statement )---\n```bash\n{bash}\n```"
|
719
|
+
)
|
720
|
+
return result.catch(
|
721
|
+
status=SUCCESS,
|
722
|
+
context={
|
723
|
+
"return_code": rs.returncode,
|
724
|
+
"stdout": None if (out := rs.stdout.strip("\n")) == "" else out,
|
725
|
+
"stderr": None if (out := rs.stderr.strip("\n")) == "" else out,
|
726
|
+
},
|
727
|
+
)
|
728
|
+
|
729
|
+
async def axecute(
|
730
|
+
self,
|
731
|
+
params: DictData,
|
732
|
+
*,
|
733
|
+
result: Result | None = None,
|
734
|
+
event: Event | None = None,
|
735
|
+
) -> Result:
|
736
|
+
"""Async execution method for this Bash stage that only logging out to
|
737
|
+
stdout.
|
738
|
+
|
739
|
+
:param params: (DictData) A parameter data.
|
740
|
+
:param result: (Result) A Result instance for return context and status.
|
741
|
+
:param event: (Event) An Event manager instance that use to cancel this
|
742
|
+
execution if it forces stopped by parent execution.
|
743
|
+
|
744
|
+
:rtype: Result
|
745
|
+
"""
|
746
|
+
result: Result = result or Result(
|
747
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
748
|
+
extras=self.extras,
|
749
|
+
)
|
750
|
+
await result.trace.ainfo(f"[STAGE]: Execute Shell-Stage: {self.name}")
|
751
|
+
bash: str = param2template(
|
752
|
+
dedent(self.bash.strip("\n")), params, extras=self.extras
|
753
|
+
)
|
754
|
+
|
755
|
+
async with self.acreate_sh_file(
|
756
|
+
bash=bash,
|
757
|
+
env=param2template(self.env, params, extras=self.extras),
|
758
|
+
run_id=result.run_id,
|
759
|
+
) as sh:
|
760
|
+
await result.trace.adebug(f"[STAGE]: ... Create `{sh[1]}` file.")
|
761
|
+
rs: CompletedProcess = subprocess.run(
|
762
|
+
sh,
|
763
|
+
shell=False,
|
764
|
+
check=False,
|
765
|
+
capture_output=True,
|
766
|
+
text=True,
|
767
|
+
encoding="utf-8",
|
697
768
|
)
|
698
769
|
|
699
770
|
if rs.returncode > 0:
|
700
|
-
|
701
|
-
e: str = (
|
702
|
-
rs.stderr.encode("utf-8").decode("utf-16")
|
703
|
-
if "\\x00" in rs.stderr
|
704
|
-
else rs.stderr
|
705
|
-
).removesuffix("\n")
|
771
|
+
e: str = rs.stderr.removesuffix("\n")
|
706
772
|
raise StageException(
|
707
|
-
f"Subprocess: {e}\n---( statement )---\n"
|
708
|
-
f"```bash\n{bash}\n```"
|
773
|
+
f"Subprocess: {e}\n---( statement )---\n```bash\n{bash}\n```"
|
709
774
|
)
|
710
775
|
return result.catch(
|
711
776
|
status=SUCCESS,
|
@@ -717,7 +782,7 @@ class BashStage(BaseStage):
|
|
717
782
|
)
|
718
783
|
|
719
784
|
|
720
|
-
class PyStage(
|
785
|
+
class PyStage(BaseAsyncStage):
|
721
786
|
"""Python stage that running the Python statement with the current globals
|
722
787
|
and passing an input additional variables via `exec` built-in function.
|
723
788
|
|
@@ -819,7 +884,7 @@ class PyStage(BaseStage):
|
|
819
884
|
| {"result": result}
|
820
885
|
)
|
821
886
|
|
822
|
-
result.trace.info(f"[STAGE]: Py-
|
887
|
+
result.trace.info(f"[STAGE]: Execute Py-Stage: {self.name}")
|
823
888
|
|
824
889
|
# WARNING: The exec build-in function is very dangerous. So, it
|
825
890
|
# should use the re module to validate exec-string before running.
|
@@ -849,15 +914,63 @@ class PyStage(BaseStage):
|
|
849
914
|
|
850
915
|
async def axecute(
|
851
916
|
self,
|
852
|
-
|
853
|
-
|
917
|
+
params: DictData,
|
918
|
+
*,
|
919
|
+
result: Result | None = None,
|
920
|
+
event: Event | None = None,
|
921
|
+
) -> Result:
|
922
|
+
"""Async execution method for this Bash stage that only logging out to
|
923
|
+
stdout.
|
924
|
+
|
925
|
+
:param params: (DictData) A parameter data.
|
926
|
+
:param result: (Result) A Result instance for return context and status.
|
927
|
+
:param event: (Event) An Event manager instance that use to cancel this
|
928
|
+
execution if it forces stopped by parent execution.
|
854
929
|
|
855
930
|
References:
|
856
931
|
- https://stackoverflow.com/questions/44859165/async-exec-in-python
|
932
|
+
|
933
|
+
:rtype: Result
|
857
934
|
"""
|
935
|
+
result: Result = result or Result(
|
936
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
937
|
+
extras=self.extras,
|
938
|
+
)
|
939
|
+
lc: DictData = {}
|
940
|
+
gb: DictData = (
|
941
|
+
globals()
|
942
|
+
| param2template(self.vars, params, extras=self.extras)
|
943
|
+
| {"result": result}
|
944
|
+
)
|
945
|
+
await result.trace.ainfo(f"[STAGE]: Execute Py-Stage: {self.name}")
|
946
|
+
|
947
|
+
# WARNING: The exec build-in function is very dangerous. So, it
|
948
|
+
# should use the re module to validate exec-string before running.
|
949
|
+
exec(
|
950
|
+
param2template(dedent(self.run), params, extras=self.extras),
|
951
|
+
gb,
|
952
|
+
lc,
|
953
|
+
)
|
954
|
+
return result.catch(
|
955
|
+
status=SUCCESS,
|
956
|
+
context={
|
957
|
+
"locals": {k: lc[k] for k in self.filter_locals(lc)},
|
958
|
+
"globals": {
|
959
|
+
k: gb[k]
|
960
|
+
for k in gb
|
961
|
+
if (
|
962
|
+
not k.startswith("__")
|
963
|
+
and k != "annotations"
|
964
|
+
and not ismodule(gb[k])
|
965
|
+
and not isclass(gb[k])
|
966
|
+
and not isfunction(gb[k])
|
967
|
+
)
|
968
|
+
},
|
969
|
+
},
|
970
|
+
)
|
858
971
|
|
859
972
|
|
860
|
-
class CallStage(
|
973
|
+
class CallStage(BaseAsyncStage):
|
861
974
|
"""Call stage executor that call the Python function from registry with tag
|
862
975
|
decorator function in `reusables` module and run it with input arguments.
|
863
976
|
|
@@ -933,7 +1046,7 @@ class CallStage(BaseStage):
|
|
933
1046
|
)()
|
934
1047
|
|
935
1048
|
result.trace.info(
|
936
|
-
f"[STAGE]: Call-
|
1049
|
+
f"[STAGE]: Execute Call-Stage: {call_func.name}@{call_func.tag}"
|
937
1050
|
)
|
938
1051
|
|
939
1052
|
# VALIDATE: check input task caller parameters that exists before
|
@@ -985,8 +1098,97 @@ class CallStage(BaseStage):
|
|
985
1098
|
rs: DictData = rs.model_dump(by_alias=True)
|
986
1099
|
elif not isinstance(rs, dict):
|
987
1100
|
raise TypeError(
|
988
|
-
f"Return type: '{call_func.name}@{call_func.tag}'
|
989
|
-
f"serialize
|
1101
|
+
f"Return type: '{call_func.name}@{call_func.tag}' can not "
|
1102
|
+
f"serialize, you must set return be `dict` or Pydantic "
|
1103
|
+
f"model."
|
1104
|
+
)
|
1105
|
+
return result.catch(status=SUCCESS, context=rs)
|
1106
|
+
|
1107
|
+
async def axecute(
|
1108
|
+
self,
|
1109
|
+
params: DictData,
|
1110
|
+
*,
|
1111
|
+
result: Result | None = None,
|
1112
|
+
event: Event | None = None,
|
1113
|
+
) -> Result:
|
1114
|
+
"""Async execution method for this Bash stage that only logging out to
|
1115
|
+
stdout.
|
1116
|
+
|
1117
|
+
:param params: (DictData) A parameter data.
|
1118
|
+
:param result: (Result) A Result instance for return context and status.
|
1119
|
+
:param event: (Event) An Event manager instance that use to cancel this
|
1120
|
+
execution if it forces stopped by parent execution.
|
1121
|
+
|
1122
|
+
References:
|
1123
|
+
- https://stackoverflow.com/questions/44859165/async-exec-in-python
|
1124
|
+
|
1125
|
+
:rtype: Result
|
1126
|
+
"""
|
1127
|
+
result: Result = result or Result(
|
1128
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
1129
|
+
extras=self.extras,
|
1130
|
+
)
|
1131
|
+
|
1132
|
+
call_func: TagFunc = extract_call(
|
1133
|
+
param2template(self.uses, params, extras=self.extras),
|
1134
|
+
registries=self.extras.get("registry_caller"),
|
1135
|
+
)()
|
1136
|
+
|
1137
|
+
await result.trace.ainfo(
|
1138
|
+
f"[STAGE]: Execute Call-Stage: {call_func.name}@{call_func.tag}"
|
1139
|
+
)
|
1140
|
+
|
1141
|
+
# VALIDATE: check input task caller parameters that exists before
|
1142
|
+
# calling.
|
1143
|
+
args: DictData = {"result": result} | param2template(
|
1144
|
+
self.args, params, extras=self.extras
|
1145
|
+
)
|
1146
|
+
ips = inspect.signature(call_func)
|
1147
|
+
necessary_params: list[str] = []
|
1148
|
+
has_keyword: bool = False
|
1149
|
+
for k in ips.parameters:
|
1150
|
+
if (
|
1151
|
+
v := ips.parameters[k]
|
1152
|
+
).default == Parameter.empty and v.kind not in (
|
1153
|
+
Parameter.VAR_KEYWORD,
|
1154
|
+
Parameter.VAR_POSITIONAL,
|
1155
|
+
):
|
1156
|
+
necessary_params.append(k)
|
1157
|
+
elif v.kind == Parameter.VAR_KEYWORD:
|
1158
|
+
has_keyword = True
|
1159
|
+
|
1160
|
+
if any(
|
1161
|
+
(k.removeprefix("_") not in args and k not in args)
|
1162
|
+
for k in necessary_params
|
1163
|
+
):
|
1164
|
+
raise ValueError(
|
1165
|
+
f"Necessary params, ({', '.join(necessary_params)}, ), "
|
1166
|
+
f"does not set to args, {list(args.keys())}."
|
1167
|
+
)
|
1168
|
+
|
1169
|
+
if "result" not in ips.parameters and not has_keyword:
|
1170
|
+
args.pop("result")
|
1171
|
+
|
1172
|
+
args = self.parse_model_args(call_func, args, result)
|
1173
|
+
|
1174
|
+
if inspect.iscoroutinefunction(call_func):
|
1175
|
+
rs: DictData = await call_func(
|
1176
|
+
**param2template(args, params, extras=self.extras)
|
1177
|
+
)
|
1178
|
+
else:
|
1179
|
+
rs: DictData = call_func(
|
1180
|
+
**param2template(args, params, extras=self.extras)
|
1181
|
+
)
|
1182
|
+
|
1183
|
+
# VALIDATE:
|
1184
|
+
# Check the result type from call function, it should be dict.
|
1185
|
+
if isinstance(rs, BaseModel):
|
1186
|
+
rs: DictData = rs.model_dump(by_alias=True)
|
1187
|
+
elif not isinstance(rs, dict):
|
1188
|
+
raise TypeError(
|
1189
|
+
f"Return type: '{call_func.name}@{call_func.tag}' can not "
|
1190
|
+
f"serialize, you must set return be `dict` or Pydantic "
|
1191
|
+
f"model."
|
990
1192
|
)
|
991
1193
|
return result.catch(status=SUCCESS, context=rs)
|
992
1194
|
|
@@ -1078,7 +1280,6 @@ class TriggerStage(BaseStage):
|
|
1078
1280
|
|
1079
1281
|
:rtype: Result
|
1080
1282
|
"""
|
1081
|
-
from .exceptions import WorkflowException
|
1082
1283
|
from .workflow import Workflow
|
1083
1284
|
|
1084
1285
|
result: Result = result or Result(
|
@@ -1087,19 +1288,15 @@ class TriggerStage(BaseStage):
|
|
1087
1288
|
)
|
1088
1289
|
|
1089
1290
|
_trigger: str = param2template(self.trigger, params, extras=self.extras)
|
1090
|
-
result.trace.info(f"[STAGE]: Trigger-
|
1091
|
-
|
1092
|
-
|
1093
|
-
|
1094
|
-
|
1095
|
-
)
|
1096
|
-
|
1097
|
-
|
1098
|
-
|
1099
|
-
)
|
1100
|
-
except WorkflowException as e:
|
1101
|
-
raise StageException("Trigger workflow stage was failed") from e
|
1102
|
-
|
1291
|
+
result.trace.info(f"[STAGE]: Execute Trigger-Stage: {_trigger!r}")
|
1292
|
+
rs: Result = Workflow.from_conf(
|
1293
|
+
name=_trigger,
|
1294
|
+
extras=self.extras | {"stage_raise_error": True},
|
1295
|
+
).execute(
|
1296
|
+
params=param2template(self.params, params, extras=self.extras),
|
1297
|
+
parent_run_id=result.run_id,
|
1298
|
+
event=event,
|
1299
|
+
)
|
1103
1300
|
if rs.status == FAILED:
|
1104
1301
|
err_msg: str | None = (
|
1105
1302
|
f" with:\n{msg}"
|
@@ -1107,12 +1304,43 @@ class TriggerStage(BaseStage):
|
|
1107
1304
|
else "."
|
1108
1305
|
)
|
1109
1306
|
raise StageException(
|
1110
|
-
f"Trigger workflow return
|
1307
|
+
f"Trigger workflow return `FAILED` status{err_msg}"
|
1111
1308
|
)
|
1112
1309
|
return rs
|
1113
1310
|
|
1114
1311
|
|
1115
|
-
class
|
1312
|
+
class BaseNestedStage(BaseStage):
|
1313
|
+
"""Base Nested Stage model. This model is use for checking the child stage
|
1314
|
+
is the nested stage or not.
|
1315
|
+
"""
|
1316
|
+
|
1317
|
+
@abstractmethod
|
1318
|
+
def execute(
|
1319
|
+
self,
|
1320
|
+
params: DictData,
|
1321
|
+
*,
|
1322
|
+
result: Result | None = None,
|
1323
|
+
event: Event | None = None,
|
1324
|
+
) -> Result:
|
1325
|
+
"""Execute abstraction method that action something by sub-model class.
|
1326
|
+
This is important method that make this class is able to be the nested
|
1327
|
+
stage.
|
1328
|
+
|
1329
|
+
:param params: (DictData) A parameter data that want to use in this
|
1330
|
+
execution.
|
1331
|
+
:param result: (Result) A result object for keeping context and status
|
1332
|
+
data.
|
1333
|
+
:param event: (Event) An event manager that use to track parent execute
|
1334
|
+
was not force stopped.
|
1335
|
+
|
1336
|
+
:rtype: Result
|
1337
|
+
"""
|
1338
|
+
raise NotImplementedError(
|
1339
|
+
"Nested-Stage should implement `execute` method."
|
1340
|
+
)
|
1341
|
+
|
1342
|
+
|
1343
|
+
class ParallelStage(BaseNestedStage):
|
1116
1344
|
"""Parallel stage executor that execute branch stages with multithreading.
|
1117
1345
|
This stage let you set the fix branches for running child stage inside it on
|
1118
1346
|
multithread pool.
|
@@ -1193,7 +1421,7 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1193
1421
|
"Branch-Stage was canceled from event that had set before "
|
1194
1422
|
"stage branch execution."
|
1195
1423
|
)
|
1196
|
-
|
1424
|
+
result.catch(
|
1197
1425
|
status=CANCEL,
|
1198
1426
|
parallel={
|
1199
1427
|
branch: {
|
@@ -1203,6 +1431,7 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1203
1431
|
}
|
1204
1432
|
},
|
1205
1433
|
)
|
1434
|
+
raise StageException(error_msg, refs=branch)
|
1206
1435
|
|
1207
1436
|
try:
|
1208
1437
|
rs: Result = stage.handler_execute(
|
@@ -1214,10 +1443,7 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1214
1443
|
)
|
1215
1444
|
stage.set_outputs(rs.context, to=output)
|
1216
1445
|
stage.set_outputs(stage.get_outputs(output), to=context)
|
1217
|
-
except
|
1218
|
-
result.trace.error(
|
1219
|
-
f"[STAGE]: {e.__class__.__name__}:{NEWLINE}{e}"
|
1220
|
-
)
|
1446
|
+
except StageException as e:
|
1221
1447
|
result.catch(
|
1222
1448
|
status=FAILED,
|
1223
1449
|
parallel={
|
@@ -1228,9 +1454,7 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1228
1454
|
},
|
1229
1455
|
},
|
1230
1456
|
)
|
1231
|
-
raise StageException(
|
1232
|
-
f"Sub-Stage raise: {e.__class__.__name__}: {e}"
|
1233
|
-
) from None
|
1457
|
+
raise StageException(str(e), refs=branch) from e
|
1234
1458
|
|
1235
1459
|
if rs.status == FAILED:
|
1236
1460
|
error_msg: str = (
|
@@ -1247,7 +1471,7 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1247
1471
|
},
|
1248
1472
|
},
|
1249
1473
|
)
|
1250
|
-
raise StageException(error_msg)
|
1474
|
+
raise StageException(error_msg, refs=branch)
|
1251
1475
|
|
1252
1476
|
return result.catch(
|
1253
1477
|
status=SUCCESS,
|
@@ -1279,12 +1503,12 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1279
1503
|
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
1280
1504
|
extras=self.extras,
|
1281
1505
|
)
|
1282
|
-
event: Event = Event()
|
1506
|
+
event: Event = event or Event()
|
1283
1507
|
result.trace.info(
|
1284
|
-
f"[STAGE]: Parallel-
|
1508
|
+
f"[STAGE]: Execute Parallel-Stage: {self.max_workers} workers."
|
1285
1509
|
)
|
1286
1510
|
result.catch(status=WAIT, context={"parallel": {}})
|
1287
|
-
if event and event.is_set():
|
1511
|
+
if event and event.is_set():
|
1288
1512
|
return result.catch(
|
1289
1513
|
status=CANCEL,
|
1290
1514
|
context={
|
@@ -1318,17 +1542,14 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
1318
1542
|
future.result()
|
1319
1543
|
except StageException as e:
|
1320
1544
|
status = FAILED
|
1321
|
-
result.trace.error(
|
1322
|
-
f"[STAGE]: {e.__class__.__name__}:{NEWLINE}{e}"
|
1323
|
-
)
|
1324
1545
|
if "errors" in context:
|
1325
|
-
context["errors"].
|
1546
|
+
context["errors"][e.refs] = e.to_dict()
|
1326
1547
|
else:
|
1327
|
-
context["errors"] =
|
1548
|
+
context["errors"] = e.to_dict(with_refs=True)
|
1328
1549
|
return result.catch(status=status, context=context)
|
1329
1550
|
|
1330
1551
|
|
1331
|
-
class ForEachStage(
|
1552
|
+
class ForEachStage(BaseNestedStage):
|
1332
1553
|
"""For-Each stage executor that execute all stages with each item in the
|
1333
1554
|
foreach list.
|
1334
1555
|
|
@@ -1369,30 +1590,43 @@ class ForEachStage(BaseStage):
|
|
1369
1590
|
"will be sequential mode if this value equal 1."
|
1370
1591
|
),
|
1371
1592
|
)
|
1593
|
+
use_index_as_key: bool = Field(
|
1594
|
+
default=False,
|
1595
|
+
description=(
|
1596
|
+
"A flag for using the loop index as a key instead item value. "
|
1597
|
+
"This flag allow to skip checking duplicate item step."
|
1598
|
+
),
|
1599
|
+
)
|
1372
1600
|
|
1373
1601
|
def execute_item(
|
1374
1602
|
self,
|
1603
|
+
index: int,
|
1375
1604
|
item: StrOrInt,
|
1376
1605
|
params: DictData,
|
1377
1606
|
result: Result,
|
1378
1607
|
*,
|
1379
1608
|
event: Event | None = None,
|
1380
1609
|
) -> Result:
|
1381
|
-
"""Execute all stage with specific foreach
|
1610
|
+
"""Execute all nested stage that set on this stage with specific foreach
|
1611
|
+
item parameter.
|
1382
1612
|
|
1613
|
+
:param index: (int) An index value of foreach loop.
|
1383
1614
|
:param item: (str | int) An item that want to execution.
|
1384
1615
|
:param params: (DictData) A parameter data.
|
1385
1616
|
:param result: (Result) A Result instance for return context and status.
|
1386
1617
|
:param event: (Event) An Event manager instance that use to cancel this
|
1387
1618
|
execution if it forces stopped by parent execution.
|
1388
1619
|
|
1389
|
-
:raise StageException: If
|
1620
|
+
:raise StageException: If event was set.
|
1621
|
+
:raise StageException: If the stage execution raise any Exception error.
|
1622
|
+
:raise StageException: If the result from execution has `FAILED` status.
|
1390
1623
|
|
1391
1624
|
:rtype: Result
|
1392
1625
|
"""
|
1393
1626
|
result.trace.debug(f"[STAGE]: Execute Item: {item!r}")
|
1627
|
+
key: StrOrInt = index if self.use_index_as_key else item
|
1394
1628
|
context: DictData = copy.deepcopy(params)
|
1395
|
-
context.update({"item": item})
|
1629
|
+
context.update({"item": item, "loop": index})
|
1396
1630
|
output: DictData = {"item": item, "stages": {}}
|
1397
1631
|
for stage in self.stages:
|
1398
1632
|
|
@@ -1404,21 +1638,21 @@ class ForEachStage(BaseStage):
|
|
1404
1638
|
stage.set_outputs(output={"skipped": True}, to=output)
|
1405
1639
|
continue
|
1406
1640
|
|
1407
|
-
if event and event.is_set():
|
1641
|
+
if event and event.is_set():
|
1408
1642
|
error_msg: str = (
|
1409
|
-
"Item-Stage was canceled
|
1410
|
-
"stage item execution."
|
1643
|
+
"Item-Stage was canceled because event was set."
|
1411
1644
|
)
|
1412
|
-
|
1645
|
+
result.catch(
|
1413
1646
|
status=CANCEL,
|
1414
1647
|
foreach={
|
1415
|
-
|
1648
|
+
key: {
|
1416
1649
|
"item": item,
|
1417
1650
|
"stages": filter_func(output.pop("stages", {})),
|
1418
1651
|
"errors": StageException(error_msg).to_dict(),
|
1419
1652
|
}
|
1420
1653
|
},
|
1421
1654
|
)
|
1655
|
+
raise StageException(error_msg, refs=key)
|
1422
1656
|
|
1423
1657
|
try:
|
1424
1658
|
rs: Result = stage.handler_execute(
|
@@ -1430,23 +1664,18 @@ class ForEachStage(BaseStage):
|
|
1430
1664
|
)
|
1431
1665
|
stage.set_outputs(rs.context, to=output)
|
1432
1666
|
stage.set_outputs(stage.get_outputs(output), to=context)
|
1433
|
-
except
|
1434
|
-
result.trace.error(
|
1435
|
-
f"[STAGE]: {e.__class__.__name__}:{NEWLINE}{e}"
|
1436
|
-
)
|
1667
|
+
except StageException as e:
|
1437
1668
|
result.catch(
|
1438
1669
|
status=FAILED,
|
1439
1670
|
foreach={
|
1440
|
-
|
1671
|
+
key: {
|
1441
1672
|
"item": item,
|
1442
1673
|
"stages": filter_func(output.pop("stages", {})),
|
1443
1674
|
"errors": e.to_dict(),
|
1444
1675
|
},
|
1445
1676
|
},
|
1446
1677
|
)
|
1447
|
-
raise StageException(
|
1448
|
-
f"Sub-Stage raise: {e.__class__.__name__}: {e}"
|
1449
|
-
) from None
|
1678
|
+
raise StageException(str(e), refs=key) from e
|
1450
1679
|
|
1451
1680
|
if rs.status == FAILED:
|
1452
1681
|
error_msg: str = (
|
@@ -1457,19 +1686,19 @@ class ForEachStage(BaseStage):
|
|
1457
1686
|
result.catch(
|
1458
1687
|
status=FAILED,
|
1459
1688
|
foreach={
|
1460
|
-
|
1689
|
+
key: {
|
1461
1690
|
"item": item,
|
1462
1691
|
"stages": filter_func(output.pop("stages", {})),
|
1463
1692
|
"errors": StageException(error_msg).to_dict(),
|
1464
1693
|
},
|
1465
1694
|
},
|
1466
1695
|
)
|
1467
|
-
raise StageException(error_msg)
|
1696
|
+
raise StageException(error_msg, refs=key)
|
1468
1697
|
|
1469
1698
|
return result.catch(
|
1470
1699
|
status=SUCCESS,
|
1471
1700
|
foreach={
|
1472
|
-
|
1701
|
+
key: {
|
1473
1702
|
"item": item,
|
1474
1703
|
"stages": filter_func(output.pop("stages", {})),
|
1475
1704
|
},
|
@@ -1498,7 +1727,7 @@ class ForEachStage(BaseStage):
|
|
1498
1727
|
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
1499
1728
|
extras=self.extras,
|
1500
1729
|
)
|
1501
|
-
event: Event = Event()
|
1730
|
+
event: Event = event or Event()
|
1502
1731
|
foreach: Union[list[str], list[int]] = (
|
1503
1732
|
param2template(self.foreach, params, extras=self.extras)
|
1504
1733
|
if isinstance(self.foreach, str)
|
@@ -1508,10 +1737,15 @@ class ForEachStage(BaseStage):
|
|
1508
1737
|
# [VALIDATE]: Type of the foreach should be `list` type.
|
1509
1738
|
if not isinstance(foreach, list):
|
1510
1739
|
raise TypeError(f"Does not support foreach: {foreach!r}")
|
1740
|
+
elif len(set(foreach)) != len(foreach) and not self.use_index_as_key:
|
1741
|
+
raise ValueError(
|
1742
|
+
"Foreach item should not duplicate. If this stage must to pass "
|
1743
|
+
"duplicate item, it should set `use_index_as_key: true`."
|
1744
|
+
)
|
1511
1745
|
|
1512
|
-
result.trace.info(f"[STAGE]: Foreach-
|
1746
|
+
result.trace.info(f"[STAGE]: Execute Foreach-Stage: {foreach!r}.")
|
1513
1747
|
result.catch(status=WAIT, context={"items": foreach, "foreach": {}})
|
1514
|
-
if event and event.is_set():
|
1748
|
+
if event and event.is_set():
|
1515
1749
|
return result.catch(
|
1516
1750
|
status=CANCEL,
|
1517
1751
|
context={
|
@@ -1529,12 +1763,13 @@ class ForEachStage(BaseStage):
|
|
1529
1763
|
futures: list[Future] = [
|
1530
1764
|
executor.submit(
|
1531
1765
|
self.execute_item,
|
1766
|
+
index=i,
|
1532
1767
|
item=item,
|
1533
1768
|
params=params,
|
1534
1769
|
result=result,
|
1535
1770
|
event=event,
|
1536
1771
|
)
|
1537
|
-
for item in foreach
|
1772
|
+
for i, item in enumerate(foreach, start=0)
|
1538
1773
|
]
|
1539
1774
|
context: DictData = {}
|
1540
1775
|
status: Status = SUCCESS
|
@@ -1542,28 +1777,41 @@ class ForEachStage(BaseStage):
|
|
1542
1777
|
done, not_done = wait(futures, return_when=FIRST_EXCEPTION)
|
1543
1778
|
if len(done) != len(futures):
|
1544
1779
|
result.trace.warning(
|
1545
|
-
"[STAGE]: Set event for stop pending stage
|
1780
|
+
"[STAGE]: Set event for stop pending for-each stage."
|
1546
1781
|
)
|
1547
1782
|
event.set()
|
1548
1783
|
for future in not_done:
|
1549
1784
|
future.cancel()
|
1785
|
+
time.sleep(0.075)
|
1550
1786
|
|
1551
|
-
nd: str =
|
1552
|
-
|
1787
|
+
nd: str = (
|
1788
|
+
(
|
1789
|
+
f", {len(not_done)} item"
|
1790
|
+
f"{'s' if len(not_done) > 1 else ''} not run!!!"
|
1791
|
+
)
|
1792
|
+
if not_done
|
1793
|
+
else ""
|
1794
|
+
)
|
1795
|
+
result.trace.debug(
|
1796
|
+
f"[STAGE]: ... Foreach-Stage set failed event{nd}"
|
1797
|
+
)
|
1798
|
+
done: list[Future] = as_completed(futures)
|
1553
1799
|
|
1554
1800
|
for future in done:
|
1555
1801
|
try:
|
1556
1802
|
future.result()
|
1557
1803
|
except StageException as e:
|
1558
1804
|
status = FAILED
|
1559
|
-
|
1560
|
-
|
1561
|
-
|
1562
|
-
|
1805
|
+
if "errors" in context:
|
1806
|
+
context["errors"][e.refs] = e.to_dict()
|
1807
|
+
else:
|
1808
|
+
context["errors"] = e.to_dict(with_refs=True)
|
1809
|
+
except CancelledError:
|
1810
|
+
pass
|
1563
1811
|
return result.catch(status=status, context=context)
|
1564
1812
|
|
1565
1813
|
|
1566
|
-
class UntilStage(
|
1814
|
+
class UntilStage(BaseNestedStage):
|
1567
1815
|
"""Until stage executor that will run stages in each loop until it valid
|
1568
1816
|
with stop loop condition.
|
1569
1817
|
|
@@ -1628,7 +1876,7 @@ class UntilStage(BaseStage):
|
|
1628
1876
|
:rtype: tuple[Result, T]
|
1629
1877
|
:return: Return a pair of Result and changed item.
|
1630
1878
|
"""
|
1631
|
-
result.trace.debug(f"... Execute until item: {item!r}")
|
1879
|
+
result.trace.debug(f"[STAGE]: ... Execute until item: {item!r}")
|
1632
1880
|
context: DictData = copy.deepcopy(params)
|
1633
1881
|
context.update({"item": item})
|
1634
1882
|
output: DictData = {"loop": loop, "item": item, "stages": {}}
|
@@ -1677,10 +1925,7 @@ class UntilStage(BaseStage):
|
|
1677
1925
|
next_item = _output["item"]
|
1678
1926
|
|
1679
1927
|
stage.set_outputs(_output, to=context)
|
1680
|
-
except
|
1681
|
-
result.trace.error(
|
1682
|
-
f"[STAGE]: {e.__class__.__name__}:{NEWLINE}{e}"
|
1683
|
-
)
|
1928
|
+
except StageException as e:
|
1684
1929
|
result.catch(
|
1685
1930
|
status=FAILED,
|
1686
1931
|
until={
|
@@ -1692,9 +1937,7 @@ class UntilStage(BaseStage):
|
|
1692
1937
|
}
|
1693
1938
|
},
|
1694
1939
|
)
|
1695
|
-
raise
|
1696
|
-
f"Sub-Stage execution error: {e.__class__.__name__}: {e}"
|
1697
|
-
) from None
|
1940
|
+
raise
|
1698
1941
|
|
1699
1942
|
if rs.status == FAILED:
|
1700
1943
|
error_msg: str = (
|
@@ -1749,7 +1992,7 @@ class UntilStage(BaseStage):
|
|
1749
1992
|
extras=self.extras,
|
1750
1993
|
)
|
1751
1994
|
|
1752
|
-
result.trace.info(f"[STAGE]: Until-
|
1995
|
+
result.trace.info(f"[STAGE]: Execute Until-Stage: {self.until}")
|
1753
1996
|
item: Union[str, int, bool] = param2template(
|
1754
1997
|
self.item, params, extras=self.extras
|
1755
1998
|
)
|
@@ -1781,7 +2024,7 @@ class UntilStage(BaseStage):
|
|
1781
2024
|
loop += 1
|
1782
2025
|
if item is None:
|
1783
2026
|
result.trace.warning(
|
1784
|
-
f"... Loop-Execute not set item. It use loop: {loop} by "
|
2027
|
+
f"[STAGE]: ... Loop-Execute not set item. It use loop: {loop} by "
|
1785
2028
|
f"default."
|
1786
2029
|
)
|
1787
2030
|
item: int = loop
|
@@ -1819,7 +2062,7 @@ class Match(BaseModel):
|
|
1819
2062
|
)
|
1820
2063
|
|
1821
2064
|
|
1822
|
-
class CaseStage(
|
2065
|
+
class CaseStage(BaseNestedStage):
|
1823
2066
|
"""Case stage executor that execute all stages if the condition was matched.
|
1824
2067
|
|
1825
2068
|
Data Validate:
|
@@ -1892,11 +2135,11 @@ class CaseStage(BaseStage):
|
|
1892
2135
|
stage.extras = self.extras
|
1893
2136
|
|
1894
2137
|
if stage.is_skipped(params=context):
|
1895
|
-
result.trace.info(f"... Skip stage: {stage.iden!r}")
|
2138
|
+
result.trace.info(f"[STAGE]: ... Skip stage: {stage.iden!r}")
|
1896
2139
|
stage.set_outputs(output={"skipped": True}, to=output)
|
1897
2140
|
continue
|
1898
2141
|
|
1899
|
-
if event and event.is_set():
|
2142
|
+
if event and event.is_set():
|
1900
2143
|
error_msg: str = (
|
1901
2144
|
"Case-Stage was canceled from event that had set before "
|
1902
2145
|
"stage case execution."
|
@@ -1920,8 +2163,7 @@ class CaseStage(BaseStage):
|
|
1920
2163
|
)
|
1921
2164
|
stage.set_outputs(rs.context, to=output)
|
1922
2165
|
stage.set_outputs(stage.get_outputs(output), to=context)
|
1923
|
-
except
|
1924
|
-
result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
|
2166
|
+
except StageException as e:
|
1925
2167
|
return result.catch(
|
1926
2168
|
status=FAILED,
|
1927
2169
|
context={
|
@@ -1977,7 +2219,7 @@ class CaseStage(BaseStage):
|
|
1977
2219
|
self.case, params, extras=self.extras
|
1978
2220
|
)
|
1979
2221
|
|
1980
|
-
result.trace.info(f"[STAGE]: Case-
|
2222
|
+
result.trace.info(f"[STAGE]: Execute Case-Stage: {_case!r}.")
|
1981
2223
|
_else: Optional[Match] = None
|
1982
2224
|
stages: Optional[list[Stage]] = None
|
1983
2225
|
for match in self.match:
|
@@ -1997,7 +2239,7 @@ class CaseStage(BaseStage):
|
|
1997
2239
|
"any case."
|
1998
2240
|
)
|
1999
2241
|
result.trace.info(
|
2000
|
-
"... Skip this stage because it does not match."
|
2242
|
+
"[STAGE]: ... Skip this stage because it does not match."
|
2001
2243
|
)
|
2002
2244
|
error_msg: str = (
|
2003
2245
|
"Case-Stage was canceled because it does not match any "
|
@@ -2010,7 +2252,7 @@ class CaseStage(BaseStage):
|
|
2010
2252
|
_case: str = "_"
|
2011
2253
|
stages: list[Stage] = _else.stages
|
2012
2254
|
|
2013
|
-
if event and event.is_set():
|
2255
|
+
if event and event.is_set():
|
2014
2256
|
return result.catch(
|
2015
2257
|
status=CANCEL,
|
2016
2258
|
context={
|
@@ -2026,7 +2268,7 @@ class CaseStage(BaseStage):
|
|
2026
2268
|
)
|
2027
2269
|
|
2028
2270
|
|
2029
|
-
class RaiseStage(
|
2271
|
+
class RaiseStage(BaseAsyncStage):
|
2030
2272
|
"""Raise error stage executor that raise `StageException` that use a message
|
2031
2273
|
field for making error message before raise.
|
2032
2274
|
|
@@ -2064,7 +2306,34 @@ class RaiseStage(BaseStage): # pragma: no cov
|
|
2064
2306
|
extras=self.extras,
|
2065
2307
|
)
|
2066
2308
|
message: str = param2template(self.message, params, extras=self.extras)
|
2067
|
-
result.trace.info(f"[STAGE]: Raise-
|
2309
|
+
result.trace.info(f"[STAGE]: Execute Raise-Stage: ( {message} )")
|
2310
|
+
raise StageException(message)
|
2311
|
+
|
2312
|
+
async def axecute(
|
2313
|
+
self,
|
2314
|
+
params: DictData,
|
2315
|
+
*,
|
2316
|
+
result: Result | None = None,
|
2317
|
+
event: Event | None = None,
|
2318
|
+
) -> Result:
|
2319
|
+
"""Async execution method for this Empty stage that only logging out to
|
2320
|
+
stdout.
|
2321
|
+
|
2322
|
+
:param params: (DictData) A context data that want to add output result.
|
2323
|
+
But this stage does not pass any output.
|
2324
|
+
:param result: (Result) A result object for keeping context and status
|
2325
|
+
data.
|
2326
|
+
:param event: (Event) An event manager that use to track parent execute
|
2327
|
+
was not force stopped.
|
2328
|
+
|
2329
|
+
:rtype: Result
|
2330
|
+
"""
|
2331
|
+
result: Result = result or Result(
|
2332
|
+
run_id=gen_id(self.name + (self.id or ""), unique=True),
|
2333
|
+
extras=self.extras,
|
2334
|
+
)
|
2335
|
+
message: str = param2template(self.message, params, extras=self.extras)
|
2336
|
+
await result.trace.ainfo(f"[STAGE]: Execute Raise-Stage: ( {message} )")
|
2068
2337
|
raise StageException(message)
|
2069
2338
|
|
2070
2339
|
|
@@ -2142,7 +2411,7 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
2142
2411
|
decode=True,
|
2143
2412
|
)
|
2144
2413
|
for line in resp:
|
2145
|
-
result.trace.info(f"... {line}")
|
2414
|
+
result.trace.info(f"[STAGE]: ... {line}")
|
2146
2415
|
|
2147
2416
|
if event and event.is_set():
|
2148
2417
|
error_msg: str = (
|
@@ -2178,7 +2447,7 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
2178
2447
|
)
|
2179
2448
|
|
2180
2449
|
for line in container.logs(stream=True, timestamps=True):
|
2181
|
-
result.trace.info(f"... {line.strip().decode()}")
|
2450
|
+
result.trace.info(f"[STAGE]: ... {line.strip().decode()}")
|
2182
2451
|
|
2183
2452
|
# NOTE: This code copy from the docker package.
|
2184
2453
|
exit_status: int = container.wait()["StatusCode"]
|
@@ -2221,8 +2490,9 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
2221
2490
|
extras=self.extras,
|
2222
2491
|
)
|
2223
2492
|
|
2224
|
-
result.trace.info(
|
2225
|
-
|
2493
|
+
result.trace.info(
|
2494
|
+
f"[STAGE]: Execute Docker-Stage: {self.image}:{self.tag}"
|
2495
|
+
)
|
2226
2496
|
raise NotImplementedError("Docker Stage does not implement yet.")
|
2227
2497
|
|
2228
2498
|
|
@@ -2316,7 +2586,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
2316
2586
|
extras=self.extras,
|
2317
2587
|
)
|
2318
2588
|
|
2319
|
-
result.trace.info(f"[STAGE]:
|
2589
|
+
result.trace.info(f"[STAGE]: Execute VirtualPy-Stage: {self.name}")
|
2320
2590
|
run: str = param2template(dedent(self.run), params, extras=self.extras)
|
2321
2591
|
with self.create_py_file(
|
2322
2592
|
py=run,
|
@@ -2324,7 +2594,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
2324
2594
|
deps=param2template(self.deps, params, extras=self.extras),
|
2325
2595
|
run_id=result.run_id,
|
2326
2596
|
) as py:
|
2327
|
-
result.trace.debug(f"... Create `{py}` file.")
|
2597
|
+
result.trace.debug(f"[STAGE]: ... Create `{py}` file.")
|
2328
2598
|
rs: CompletedProcess = subprocess.run(
|
2329
2599
|
["uv", "run", py, "--no-cache"],
|
2330
2600
|
# ["uv", "run", "--python", "3.9", py],
|
@@ -2375,4 +2645,4 @@ Stage = Annotated[
|
|
2375
2645
|
EmptyStage,
|
2376
2646
|
],
|
2377
2647
|
Field(union_mode="smart"),
|
2378
|
-
]
|
2648
|
+
] # pragma: no cov
|