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.
@@ -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 ---( handler )--> Result with `SUCCESS` or `CANCEL`
20
-
21
- --> Error ┬--( handler )-> Result with `FAILED` (Set `raise_error` flag)
18
+ Execution --> Ok ┬--( handler )--> Result with `SUCCESS` or `CANCEL`
22
19
  |
23
- ╰--( handler )-> Raise StageException(...)
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, UtilException, to_dict
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
- --> Error --> Result (if `raise_error` was set)
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:{NEWLINE}{e_name}: {e}")
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__}: {NEWLINE}{e_name}: {e}"
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"][_id] = {
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
- _id: str = (
314
- param2template(self.id, params=output, extras=self.extras)
315
- if self.id
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: # pragma: no cov
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__}: {NEWLINE}{e_name}: {e}"
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
- if not self.echo:
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 "\n" in message:
510
- message: str = NEWLINE + message.replace("\n", NEWLINE)
514
+ if self.echo
515
+ else "..."
516
+ )
511
517
 
512
518
  result.trace.info(
513
- f"[STAGE]: Empty-Execute: {self.name!r}: ( {message} )"
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
- if not self.echo:
544
- message: str = "..."
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 "\n" in message:
550
- message: str = NEWLINE + message.replace("\n", NEWLINE)
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(BaseStage):
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: # pragma no cov
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 [f_shebang, f_name]
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
- """Return context of prepared bash statement that want to execute. This
629
- step will write the `.sh` file before giving this file name to context.
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 [f_shebang, f_name]
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-Execute: {self.name}")
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, shell=False, capture_output=True, text=True
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
- # NOTE: Prepare stderr message that returning from subprocess.
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(BaseStage):
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-Execute: {self.name}")
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
- """Async execution method.
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(BaseStage):
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-Execute: {call_func.name}@{call_func.tag}"
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}' does not "
989
- f"serialize to result model, you change return type to `dict`."
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-Execute: {_trigger!r}")
1091
- try:
1092
- rs: Result = Workflow.from_conf(
1093
- name=_trigger,
1094
- extras=self.extras | {"stage_raise_error": True},
1095
- ).execute(
1096
- params=param2template(self.params, params, extras=self.extras),
1097
- parent_run_id=result.run_id,
1098
- event=event,
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 failed status{err_msg}"
1307
+ f"Trigger workflow return `FAILED` status{err_msg}"
1111
1308
  )
1112
1309
  return rs
1113
1310
 
1114
1311
 
1115
- class ParallelStage(BaseStage): # pragma: no cov
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
- return result.catch(
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 (StageException, UtilException) as e: # pragma: no cov
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() if event is None else event
1506
+ event: Event = event or Event()
1283
1507
  result.trace.info(
1284
- f"[STAGE]: Parallel-Execute: {self.max_workers} workers."
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(): # pragma: no cov
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"].append(e.to_dict())
1546
+ context["errors"][e.refs] = e.to_dict()
1326
1547
  else:
1327
- context["errors"] = [e.to_dict()]
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(BaseStage):
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 item.
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 the stage execution raise errors.
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(): # pragma: no cov
1641
+ if event and event.is_set():
1408
1642
  error_msg: str = (
1409
- "Item-Stage was canceled from event that had set before "
1410
- "stage item execution."
1643
+ "Item-Stage was canceled because event was set."
1411
1644
  )
1412
- return result.catch(
1645
+ result.catch(
1413
1646
  status=CANCEL,
1414
1647
  foreach={
1415
- item: {
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 (StageException, UtilException) as e:
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
- item: {
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
- item: {
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
- item: {
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() if event is None else 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-Execute: {foreach!r}.")
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(): # pragma: no cov
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 future."
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 = f", item not run: {not_done}" if not_done else ""
1552
- result.trace.debug(f"... Foreach set Fail-Fast{nd}")
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
- result.trace.error(
1560
- f"[STAGE]: {e.__class__.__name__}:{NEWLINE}{e}"
1561
- )
1562
- context.update({"errors": e.to_dict()})
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(BaseStage):
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 (StageException, UtilException) as e:
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 StageException(
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-Execution: {self.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(BaseStage):
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(): # pragma: no cov
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 (StageException, UtilException) as e: # pragma: no cov
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-Execute: {_case!r}.")
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(): # pragma: no cov
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(BaseStage): # pragma: no cov
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-Execute: {message!r}.")
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(f"[STAGE]: Docker-Execute: {self.image}:{self.tag}")
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]: Py-Virtual-Execute: {self.name}")
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