ddeutil-workflow 0.0.63__py3-none-any.whl → 0.0.65__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,9 +3,9 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """Stages module include all stage model that use be the minimum execution layer
7
- of this workflow engine. The stage handle the minimize task that run in some
8
- thread (same thread at its job owner) that mean it is the lowest executor that
6
+ """Stages module include all stage model that implemented to be the minimum execution
7
+ layer of this workflow core engine. The stage handle the minimize task that run
8
+ in a thread (same thread at its job owner) that mean it is the lowest executor that
9
9
  you can track logs.
10
10
 
11
11
  The output of stage execution only return SUCCESS or CANCEL status because
@@ -17,9 +17,11 @@ the stage execution method.
17
17
 
18
18
  Execution --> Ok ┬--( handler )--> Result with `SUCCESS` or `CANCEL`
19
19
  |
20
- ╰--( handler )--> Result with `FAILED` (Set `raise_error` flag)
20
+ ├--( handler )--> Result with `FAILED` (Set `raise_error` flag)
21
+ |
22
+ ╰--( handler )---> Result with `SKIP`
21
23
 
22
- --> Error ---( handler )--> Raise StageException(...)
24
+ --> Error ---( handler )--> Raise StageError(...)
23
25
 
24
26
  On the context I/O that pass to a stage object at execute process. The
25
27
  execute method receives a `params={"params": {...}}` value for passing template
@@ -55,21 +57,40 @@ from textwrap import dedent
55
57
  from threading import Event
56
58
  from typing import Annotated, Any, Optional, TypeVar, Union, get_type_hints
57
59
 
58
- from pydantic import BaseModel, Field
59
- from pydantic.functional_validators import model_validator
60
+ from ddeutil.core import str2list
61
+ from pydantic import BaseModel, Field, ValidationError
62
+ from pydantic.functional_validators import field_validator, model_validator
60
63
  from typing_extensions import Self
61
64
 
65
+ from . import StageCancelError, StageRetryError
62
66
  from .__types import DictData, DictStr, StrOrInt, StrOrNone, TupleStr
63
67
  from .conf import dynamic, pass_env
64
- from .exceptions import StageException, to_dict
65
- from .result import CANCEL, FAILED, SUCCESS, WAIT, Result, Status
66
- from .reusables import TagFunc, extract_call, not_in_template, param2template
68
+ from .errors import StageError, StageSkipError, to_dict
69
+ from .result import (
70
+ CANCEL,
71
+ FAILED,
72
+ SKIP,
73
+ SUCCESS,
74
+ WAIT,
75
+ Result,
76
+ Status,
77
+ get_status_from_error,
78
+ validate_statuses,
79
+ )
80
+ from .reusables import (
81
+ TagFunc,
82
+ create_model_from_caller,
83
+ extract_call,
84
+ not_in_template,
85
+ param2template,
86
+ )
67
87
  from .utils import (
68
88
  delay,
69
89
  dump_all,
70
90
  filter_func,
71
91
  gen_id,
72
92
  make_exec,
93
+ to_train,
73
94
  )
74
95
 
75
96
  T = TypeVar("T")
@@ -100,6 +121,12 @@ class BaseStage(BaseModel, ABC):
100
121
  name: str = Field(
101
122
  description="A stage name that want to logging when start execution.",
102
123
  )
124
+ desc: StrOrNone = Field(
125
+ default=None,
126
+ description=(
127
+ "A stage description that use to logging when start execution."
128
+ ),
129
+ )
103
130
  condition: StrOrNone = Field(
104
131
  default=None,
105
132
  description=(
@@ -118,6 +145,14 @@ class BaseStage(BaseModel, ABC):
118
145
  """
119
146
  return self.id or self.name
120
147
 
148
+ @field_validator("desc", mode="after")
149
+ def ___prepare_desc__(cls, value: str) -> str:
150
+ """Prepare description string that was created on a template.
151
+
152
+ :rtype: str
153
+ """
154
+ return dedent(value.lstrip("\n"))
155
+
121
156
  @model_validator(mode="after")
122
157
  def __prepare_running_id(self) -> Self:
123
158
  """Prepare stage running ID that use default value of field and this
@@ -129,14 +164,12 @@ class BaseStage(BaseModel, ABC):
129
164
 
130
165
  :rtype: Self
131
166
  """
132
-
133
167
  # VALIDATE: Validate stage id and name should not dynamic with params
134
168
  # template. (allow only matrix)
135
169
  if not_in_template(self.id) or not_in_template(self.name):
136
170
  raise ValueError(
137
171
  "Stage name and ID should only template with 'matrix.'"
138
172
  )
139
-
140
173
  return self
141
174
 
142
175
  @abstractmethod
@@ -169,47 +202,41 @@ class BaseStage(BaseModel, ABC):
169
202
  parent_run_id: StrOrNone = None,
170
203
  result: Optional[Result] = None,
171
204
  event: Optional[Event] = None,
172
- raise_error: Optional[bool] = None,
173
205
  ) -> Union[Result, DictData]:
174
206
  """Handler stage execution result from the stage `execute` method.
175
207
 
176
- This stage exception handler still use ok-error concept, but it
177
- allows you force catching an output result with error message by
178
- specific environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR` or set
179
- `raise_error` parameter to True.
208
+ This handler strategy will catch and mapping message to the result
209
+ context data before returning. All possible status that will return from
210
+ this method be:
180
211
 
181
- Execution --> Ok --> Result
212
+ Handler --> Ok --> Result
182
213
  |-status: SUCCESS
183
214
  ╰-context:
184
215
  ╰-outputs: ...
185
216
 
186
217
  --> Ok --> Result
187
- |-status: CANCEL
188
- ╰-errors:
189
- |-name: ...
190
- ╰-message: ...
218
+ ╰-status: CANCEL
191
219
 
192
- --> Ok --> Result (if `raise_error` was set)
220
+ --> Ok --> Result
221
+ ╰-status: SKIP
222
+
223
+ --> Ok --> Result
193
224
  |-status: FAILED
194
225
  ╰-errors:
195
226
  |-name: ...
196
227
  ╰-message: ...
197
228
 
198
- --> Error --> Raise StageException(...)
199
-
200
229
  On the last step, it will set the running ID on a return result
201
230
  object from the current stage ID before release the final result.
202
231
 
203
232
  :param params: (DictData) A parameter data.
204
- :param run_id: (str) A running stage ID.
205
- :param parent_run_id: (str) A parent running ID.
233
+ :param run_id: (str) A running stage ID. (Default is None)
234
+ :param parent_run_id: (str) A parent running ID. (Default is None)
206
235
  :param result: (Result) A result object for keeping context and status
207
236
  data before execution.
237
+ (Default is None)
208
238
  :param event: (Event) An event manager that pass to the stage execution.
209
- :param raise_error: (bool) A flag that all this method raise error
210
-
211
- :raise StageException: If the raise_error was set and the execution
212
- raise any error.
239
+ (Default is None)
213
240
 
214
241
  :rtype: Result
215
242
  """
@@ -221,21 +248,71 @@ class BaseStage(BaseModel, ABC):
221
248
  extras=self.extras,
222
249
  )
223
250
  try:
224
- return self.execute(params, result=result, event=event)
251
+ result.trace.info(
252
+ f"[STAGE]: Handler {to_train(self.__class__.__name__)}: "
253
+ f"{self.name!r}."
254
+ )
255
+ if self.desc:
256
+ result.trace.debug(f"[STAGE]: Description:||{self.desc}||")
257
+
258
+ if self.is_skipped(params):
259
+ raise StageSkipError(
260
+ f"Skip because condition {self.condition} was valid."
261
+ )
262
+ # NOTE: Start call wrapped execution method that will use custom
263
+ # execution before the real execution from inherit stage model.
264
+ result_caught: Result = self.__execute(
265
+ params, result=result, event=event
266
+ )
267
+ if result_caught.status == WAIT:
268
+ raise StageError(
269
+ "Status from execution should not return waiting status."
270
+ )
271
+ return result_caught
272
+
273
+ # NOTE: Catch this error in this line because the execution can raise
274
+ # this exception class at other location.
275
+ except (
276
+ StageSkipError,
277
+ StageCancelError,
278
+ StageError,
279
+ ) as e: # pragma: no cov
280
+ result.trace.info(
281
+ f"[STAGE]: Handler:||{e.__class__.__name__}: {e}||"
282
+ f"{traceback.format_exc()}"
283
+ )
284
+ return result.catch(
285
+ status=get_status_from_error(e),
286
+ context=(
287
+ None
288
+ if isinstance(e, StageSkipError)
289
+ else {"errors": e.to_dict()}
290
+ ),
291
+ )
225
292
  except Exception as e:
226
- e_name: str = e.__class__.__name__
227
293
  result.trace.error(
228
- f"[STAGE]: Error Handler:||{e_name}:||{e}||"
294
+ f"[STAGE]: Error Handler:||{e.__class__.__name__}: {e}||"
229
295
  f"{traceback.format_exc()}"
230
296
  )
231
- if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
232
- if isinstance(e, StageException):
233
- raise
234
- raise StageException(
235
- f"{self.__class__.__name__}: {e_name}: {e}"
236
- ) from e
237
297
  return result.catch(status=FAILED, context={"errors": to_dict(e)})
238
298
 
299
+ def __execute(
300
+ self, params: DictData, result: Result, event: Optional[Event]
301
+ ) -> Result:
302
+ """Wrapped the execute method before returning to handler execution.
303
+
304
+ :param params: (DictData) A parameter data that want to use in this
305
+ execution.
306
+ :param result: (Result) A result object for keeping context and status
307
+ data.
308
+ :param event: (Event) An event manager that use to track parent execute
309
+ was not force stopped.
310
+
311
+ :rtype: Result
312
+ """
313
+ result.catch(status=WAIT)
314
+ return self.execute(params, result=result, event=event)
315
+
239
316
  def set_outputs(self, output: DictData, to: DictData) -> DictData:
240
317
  """Set an outputs from execution result context to the received context
241
318
  with a `to` input parameter. The result context from stage execution
@@ -283,20 +360,15 @@ class BaseStage(BaseModel, ABC):
283
360
  ):
284
361
  return to
285
362
 
286
- output: DictData = output.copy()
363
+ _id: str = self.gen_id(params=to)
364
+ output: DictData = copy.deepcopy(output)
287
365
  errors: DictData = (
288
- {"errors": output.pop("errors", {})} if "errors" in output else {}
366
+ {"errors": output.pop("errors")} if "errors" in output else {}
289
367
  )
290
- skipping: dict[str, bool] = (
291
- {"skipped": output.pop("skipped", False)}
292
- if "skipped" in output
293
- else {}
368
+ status: dict[str, Status] = (
369
+ {"status": output.pop("status")} if "status" in output else {}
294
370
  )
295
- to["stages"][self.gen_id(params=to)] = {
296
- "outputs": copy.deepcopy(output),
297
- **skipping,
298
- **errors,
299
- }
371
+ to["stages"][_id] = {"outputs": output} | errors | status
300
372
  return to
301
373
 
302
374
  def get_outputs(self, output: DictData) -> DictData:
@@ -325,14 +397,15 @@ class BaseStage(BaseModel, ABC):
325
397
  :param params: (DictData) A parameters that want to pass to condition
326
398
  template.
327
399
 
328
- :raise StageException: When it has any error raise from the eval
400
+ :raise StageError: When it has any error raise from the eval
329
401
  condition statement.
330
- :raise StageException: When return type of the eval condition statement
402
+ :raise StageError: When return type of the eval condition statement
331
403
  does not return with boolean type.
332
404
 
333
405
  :rtype: bool
334
406
  """
335
- if self.condition is None:
407
+ # NOTE: Support for condition value is empty string.
408
+ if not self.condition:
336
409
  return False
337
410
 
338
411
  try:
@@ -348,13 +421,13 @@ class BaseStage(BaseModel, ABC):
348
421
  raise TypeError("Return type of condition does not be boolean")
349
422
  return not rs
350
423
  except Exception as e:
351
- raise StageException(f"{e.__class__.__name__}: {e}") from e
424
+ raise StageError(f"{e.__class__.__name__}: {e}") from e
352
425
 
353
426
  def gen_id(self, params: DictData) -> str:
354
427
  """Generate stage ID that dynamic use stage's name if it ID does not
355
428
  set.
356
429
 
357
- :param params: A parameter data.
430
+ :param params: (DictData) A parameter or context data.
358
431
 
359
432
  :rtype: str
360
433
  """
@@ -366,8 +439,16 @@ class BaseStage(BaseModel, ABC):
366
439
  )
367
440
  )
368
441
 
442
+ @property
443
+ def is_nested(self) -> bool:
444
+ """Return true if this stage is nested stage.
445
+
446
+ :rtype: bool
447
+ """
448
+ return False
369
449
 
370
- class BaseAsyncStage(BaseStage):
450
+
451
+ class BaseAsyncStage(BaseStage, ABC):
371
452
  """Base Async Stage model to make any stage model allow async execution for
372
453
  optimize CPU and Memory on the current node. If you want to implement any
373
454
  custom async stage, you can inherit this class and implement
@@ -377,18 +458,6 @@ class BaseAsyncStage(BaseStage):
377
458
  model.
378
459
  """
379
460
 
380
- @abstractmethod
381
- def execute(
382
- self,
383
- params: DictData,
384
- *,
385
- result: Optional[Result] = None,
386
- event: Optional[Event] = None,
387
- ) -> Result:
388
- raise NotImplementedError(
389
- "Async Stage should implement `execute` method."
390
- )
391
-
392
461
  @abstractmethod
393
462
  async def axecute(
394
463
  self,
@@ -421,7 +490,6 @@ class BaseAsyncStage(BaseStage):
421
490
  parent_run_id: StrOrNone = None,
422
491
  result: Optional[Result] = None,
423
492
  event: Optional[Event] = None,
424
- raise_error: Optional[bool] = None,
425
493
  ) -> Result:
426
494
  """Async Handler stage execution result from the stage `execute` method.
427
495
 
@@ -431,7 +499,6 @@ class BaseAsyncStage(BaseStage):
431
499
  :param result: (Result) A Result instance for return context and status.
432
500
  :param event: (Event) An Event manager instance that use to cancel this
433
501
  execution if it forces stopped by parent execution.
434
- :param raise_error: (bool) A flag that all this method raise error
435
502
 
436
503
  :rtype: Result
437
504
  """
@@ -442,22 +509,142 @@ class BaseAsyncStage(BaseStage):
442
509
  id_logic=self.iden,
443
510
  extras=self.extras,
444
511
  )
445
-
446
512
  try:
447
- rs: Result = await self.axecute(params, result=result, event=event)
448
- return rs
449
- except Exception as e:
450
- e_name: str = e.__class__.__name__
451
- await result.trace.aerror(f"[STAGE]: Handler {e_name}: {e}")
452
- if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
453
- if isinstance(e, StageException):
454
- raise
455
- raise StageException(
456
- f"{self.__class__.__name__}: {e_name}: {e}"
457
- ) from None
513
+ await result.trace.ainfo(
514
+ f"[STAGE]: Handler {to_train(self.__class__.__name__)}: "
515
+ f"{self.name!r}."
516
+ )
517
+ if self.desc:
518
+ await result.trace.adebug(
519
+ f"[STAGE]: Description:||{self.desc}||"
520
+ )
458
521
 
522
+ if self.is_skipped(params=params):
523
+ raise StageSkipError(
524
+ f"Skip because condition {self.condition} was valid."
525
+ )
526
+
527
+ # NOTE: Start call wrapped execution method that will use custom
528
+ # execution before the real execution from inherit stage model.
529
+ result_caught: Result = await self.__axecute(
530
+ params, result=result, event=event
531
+ )
532
+ if result_caught.status == WAIT:
533
+ raise StageError(
534
+ "Status from execution should not return waiting status."
535
+ )
536
+ return result_caught
537
+
538
+ # NOTE: Catch this error in this line because the execution can raise
539
+ # this exception class at other location.
540
+ except (
541
+ StageSkipError,
542
+ StageCancelError,
543
+ StageError,
544
+ ) as e: # pragma: no cov
545
+ await result.trace.ainfo(
546
+ f"[STAGE]: Skip Handler:||{e.__class__.__name__}: {e}||"
547
+ f"{traceback.format_exc()}"
548
+ )
549
+ return result.catch(
550
+ status=get_status_from_error(e),
551
+ context=(
552
+ {"errors": e.to_dict()}
553
+ if isinstance(e, StageError)
554
+ else None
555
+ ),
556
+ )
557
+ except Exception as e:
558
+ await result.trace.aerror(
559
+ f"[STAGE]: Error Handler:||{e.__class__.__name__}: {e}||"
560
+ f"{traceback.format_exc()}"
561
+ )
459
562
  return result.catch(status=FAILED, context={"errors": to_dict(e)})
460
563
 
564
+ async def __axecute(
565
+ self, params: DictData, result: Result, event: Optional[Event]
566
+ ) -> Result:
567
+ """Wrapped the axecute method before returning to handler axecute.
568
+
569
+ :param params: (DictData) A parameter data that want to use in this
570
+ execution.
571
+ :param result: (Result) A result object for keeping context and status
572
+ data.
573
+ :param event: (Event) An event manager that use to track parent execute
574
+ was not force stopped.
575
+
576
+ :rtype: Result
577
+ """
578
+ result.catch(status=WAIT)
579
+ return await self.axecute(params, result=result, event=event)
580
+
581
+
582
+ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
583
+ """Base Retry Stage model that will execute again when it raises with the
584
+ `StageRetryError`.
585
+ """
586
+
587
+ retry: int = Field(
588
+ default=0,
589
+ ge=0,
590
+ lt=20,
591
+ description="Retry number if stage execution get the error.",
592
+ )
593
+
594
+ def __execute(
595
+ self,
596
+ params: DictData,
597
+ result: Result,
598
+ event: Optional[Event],
599
+ ) -> Result:
600
+ """Wrapped the execute method with retry strategy before returning to
601
+ handler execute.
602
+
603
+ :param params: (DictData) A parameter data that want to use in this
604
+ execution.
605
+ :param result: (Result) A result object for keeping context and status
606
+ data.
607
+ :param event: (Event) An event manager that use to track parent execute
608
+ was not force stopped.
609
+
610
+ :rtype: Result
611
+ """
612
+ current_retry: int = 0
613
+ with current_retry < (self.retry + 1):
614
+ try:
615
+ result.catch(status=WAIT, context={"retry": current_retry})
616
+ return self.execute(params, result=result, event=event)
617
+ except StageRetryError:
618
+ current_retry += 1
619
+ raise StageError(f"Reach the maximum of retry number: {self.retry}.")
620
+
621
+ async def __axecute(
622
+ self,
623
+ params: DictData,
624
+ result: Result,
625
+ event: Optional[Event],
626
+ ) -> Result:
627
+ """Wrapped the axecute method with retry strategy before returning to
628
+ handler axecute.
629
+
630
+ :param params: (DictData) A parameter data that want to use in this
631
+ execution.
632
+ :param result: (Result) A result object for keeping context and status
633
+ data.
634
+ :param event: (Event) An event manager that use to track parent execute
635
+ was not force stopped.
636
+
637
+ :rtype: Result
638
+ """
639
+ current_retry: int = 0
640
+ with current_retry < (self.retry + 1):
641
+ try:
642
+ result.catch(status=WAIT, context={"retry": current_retry})
643
+ return await self.axecute(params, result=result, event=event)
644
+ except StageRetryError:
645
+ current_retry += 1
646
+ raise StageError(f"Reach the maximum of retry number: {self.retry}.")
647
+
461
648
 
462
649
  class EmptyStage(BaseAsyncStage):
463
650
  """Empty stage executor that do nothing and log the `message` field to
@@ -521,12 +708,15 @@ class EmptyStage(BaseAsyncStage):
521
708
  else "..."
522
709
  )
523
710
 
524
- result.trace.info(
525
- f"[STAGE]: Execute Empty-Stage: {self.name!r}: ( {message} )"
526
- )
711
+ if event and event.is_set():
712
+ raise StageCancelError(
713
+ "Execution was canceled from the event before start parallel."
714
+ )
715
+
716
+ result.trace.info(f"[STAGE]: Message: ( {message} )")
527
717
  if self.sleep > 0:
528
718
  if self.sleep > 5:
529
- result.trace.info(f"[STAGE]: ... sleep ({self.sleep} sec)")
719
+ result.trace.info(f"[STAGE]: Sleep ... ({self.sleep} sec)")
530
720
  time.sleep(self.sleep)
531
721
  return result.catch(status=SUCCESS)
532
722
 
@@ -560,11 +750,16 @@ class EmptyStage(BaseAsyncStage):
560
750
  else "..."
561
751
  )
562
752
 
563
- result.trace.info(f"[STAGE]: Empty-Stage: {self.name!r}: ( {message} )")
753
+ if event and event.is_set():
754
+ raise StageCancelError(
755
+ "Execution was canceled from the event before start parallel."
756
+ )
757
+
758
+ result.trace.info(f"[STAGE]: Message: ( {message} )")
564
759
  if self.sleep > 0:
565
760
  if self.sleep > 5:
566
761
  await result.trace.ainfo(
567
- f"[STAGE]: ... sleep ({self.sleep} sec)"
762
+ f"[STAGE]: Sleep ... ({self.sleep} sec)"
568
763
  )
569
764
  await asyncio.sleep(self.sleep)
570
765
  return result.catch(status=SUCCESS)
@@ -697,19 +892,15 @@ class BashStage(BaseAsyncStage):
697
892
  run_id=gen_id(self.name + (self.id or ""), unique=True),
698
893
  extras=self.extras,
699
894
  )
700
-
701
- result.trace.info(f"[STAGE]: Execute Shell-Stage: {self.name}")
702
-
703
895
  bash: str = param2template(
704
896
  dedent(self.bash.strip("\n")), params, extras=self.extras
705
897
  )
706
-
707
898
  with self.create_sh_file(
708
899
  bash=bash,
709
900
  env=param2template(self.env, params, extras=self.extras),
710
901
  run_id=result.run_id,
711
902
  ) as sh:
712
- result.trace.debug(f"[STAGE]: ... Create `{sh[1]}` file.")
903
+ result.trace.debug(f"[STAGE]: Create `{sh[1]}` file.")
713
904
  rs: CompletedProcess = subprocess.run(
714
905
  sh,
715
906
  shell=False,
@@ -720,7 +911,7 @@ class BashStage(BaseAsyncStage):
720
911
  )
721
912
  if rs.returncode > 0:
722
913
  e: str = rs.stderr.removesuffix("\n")
723
- raise StageException(
914
+ raise StageError(
724
915
  f"Subprocess: {e}\n---( statement )---\n```bash\n{bash}\n```"
725
916
  )
726
917
  return result.catch(
@@ -753,17 +944,15 @@ class BashStage(BaseAsyncStage):
753
944
  run_id=gen_id(self.name + (self.id or ""), unique=True),
754
945
  extras=self.extras,
755
946
  )
756
- await result.trace.ainfo(f"[STAGE]: Execute Shell-Stage: {self.name}")
757
947
  bash: str = param2template(
758
948
  dedent(self.bash.strip("\n")), params, extras=self.extras
759
949
  )
760
-
761
950
  async with self.async_create_sh_file(
762
951
  bash=bash,
763
952
  env=param2template(self.env, params, extras=self.extras),
764
953
  run_id=result.run_id,
765
954
  ) as sh:
766
- await result.trace.adebug(f"[STAGE]: ... Create `{sh[1]}` file.")
955
+ await result.trace.adebug(f"[STAGE]: Create `{sh[1]}` file.")
767
956
  rs: CompletedProcess = subprocess.run(
768
957
  sh,
769
958
  shell=False,
@@ -775,7 +964,7 @@ class BashStage(BaseAsyncStage):
775
964
 
776
965
  if rs.returncode > 0:
777
966
  e: str = rs.stderr.removesuffix("\n")
778
- raise StageException(
967
+ raise StageError(
779
968
  f"Subprocess: {e}\n---( statement )---\n```bash\n{bash}\n```"
780
969
  )
781
970
  return result.catch(
@@ -882,7 +1071,6 @@ class PyStage(BaseAsyncStage):
882
1071
  run_id=gen_id(self.name + (self.id or ""), unique=True),
883
1072
  extras=self.extras,
884
1073
  )
885
-
886
1074
  lc: DictData = {}
887
1075
  gb: DictData = (
888
1076
  globals()
@@ -890,8 +1078,6 @@ class PyStage(BaseAsyncStage):
890
1078
  | {"result": result}
891
1079
  )
892
1080
 
893
- result.trace.info(f"[STAGE]: Execute Py-Stage: {self.name}")
894
-
895
1081
  # WARNING: The exec build-in function is very dangerous. So, it
896
1082
  # should use the re module to validate exec-string before running.
897
1083
  exec(
@@ -915,6 +1101,7 @@ class PyStage(BaseAsyncStage):
915
1101
  and not ismodule(gb[k])
916
1102
  and not isclass(gb[k])
917
1103
  and not isfunction(gb[k])
1104
+ and k in params
918
1105
  )
919
1106
  },
920
1107
  },
@@ -950,8 +1137,6 @@ class PyStage(BaseAsyncStage):
950
1137
  | param2template(self.vars, params, extras=self.extras)
951
1138
  | {"result": result}
952
1139
  )
953
- await result.trace.ainfo(f"[STAGE]: Execute Py-Stage: {self.name}")
954
-
955
1140
  # WARNING: The exec build-in function is very dangerous. So, it
956
1141
  # should use the re module to validate exec-string before running.
957
1142
  exec(
@@ -972,6 +1157,7 @@ class PyStage(BaseAsyncStage):
972
1157
  and not ismodule(gb[k])
973
1158
  and not isclass(gb[k])
974
1159
  and not isfunction(gb[k])
1160
+ and k in params
975
1161
  )
976
1162
  },
977
1163
  },
@@ -1054,7 +1240,7 @@ class CallStage(BaseAsyncStage):
1054
1240
  )()
1055
1241
 
1056
1242
  result.trace.info(
1057
- f"[STAGE]: Execute Call-Stage: {call_func.name}@{call_func.tag}"
1243
+ f"[STAGE]: Caller Func: '{call_func.name}@{call_func.tag}'"
1058
1244
  )
1059
1245
 
1060
1246
  # VALIDATE: check input task caller parameters that exists before
@@ -1088,8 +1274,12 @@ class CallStage(BaseAsyncStage):
1088
1274
  if "result" not in sig.parameters and not has_keyword:
1089
1275
  args.pop("result")
1090
1276
 
1091
- args = self.parse_model_args(call_func, args, result)
1277
+ if event and event.is_set():
1278
+ raise StageCancelError(
1279
+ "Execution was canceled from the event before start parallel."
1280
+ )
1092
1281
 
1282
+ args = self.validate_model_args(call_func, args, result)
1093
1283
  if inspect.iscoroutinefunction(call_func):
1094
1284
  loop = asyncio.get_event_loop()
1095
1285
  rs: DictData = loop.run_until_complete(
@@ -1143,7 +1333,7 @@ class CallStage(BaseAsyncStage):
1143
1333
  )()
1144
1334
 
1145
1335
  await result.trace.ainfo(
1146
- f"[STAGE]: Execute Call-Stage: {call_func.name}@{call_func.tag}"
1336
+ f"[STAGE]: Caller Func: '{call_func.name}@{call_func.tag}'"
1147
1337
  )
1148
1338
 
1149
1339
  # VALIDATE: check input task caller parameters that exists before
@@ -1177,7 +1367,7 @@ class CallStage(BaseAsyncStage):
1177
1367
  if "result" not in sig.parameters and not has_keyword:
1178
1368
  args.pop("result")
1179
1369
 
1180
- args = self.parse_model_args(call_func, args, result)
1370
+ args: DictData = self.validate_model_args(call_func, args, result)
1181
1371
  if inspect.iscoroutinefunction(call_func):
1182
1372
  rs: DictOrModel = await call_func(
1183
1373
  **param2template(args, params, extras=self.extras)
@@ -1200,23 +1390,41 @@ class CallStage(BaseAsyncStage):
1200
1390
  return result.catch(status=SUCCESS, context=dump_all(rs, by_alias=True))
1201
1391
 
1202
1392
  @staticmethod
1203
- def parse_model_args(
1393
+ def validate_model_args(
1204
1394
  func: TagFunc,
1205
1395
  args: DictData,
1206
1396
  result: Result,
1207
1397
  ) -> DictData:
1208
- """Parse Pydantic model from any dict data before parsing to target
1209
- caller function.
1398
+ """Validate an input arguments before passing to the caller function.
1210
1399
 
1211
- :param func: A tag function that want to get typing.
1212
- :param args: An arguments before passing to this tag function.
1400
+ :param func: (TagFunc) A tag function that want to get typing.
1401
+ :param args: (DictData) An arguments before passing to this tag func.
1213
1402
  :param result: (Result) A result object for keeping context and status
1214
1403
  data.
1215
1404
 
1216
1405
  :rtype: DictData
1217
1406
  """
1218
1407
  try:
1408
+ model_instance: BaseModel = create_model_from_caller(
1409
+ func
1410
+ ).model_validate(args)
1411
+ override: DictData = dict(model_instance)
1412
+ args.update(override)
1219
1413
  type_hints: dict[str, Any] = get_type_hints(func)
1414
+ for arg in type_hints:
1415
+
1416
+ if arg == "return":
1417
+ continue
1418
+
1419
+ if arg.removeprefix("_") in args:
1420
+ args[arg] = args.pop(arg.removeprefix("_"))
1421
+ continue
1422
+
1423
+ return args
1424
+ except ValidationError as e:
1425
+ raise StageError(
1426
+ "Validate argument from the caller function raise invalid type."
1427
+ ) from e
1220
1428
  except TypeError as e:
1221
1429
  result.trace.warning(
1222
1430
  f"[STAGE]: Get type hint raise TypeError: {e}, so, it skip "
@@ -1224,28 +1432,43 @@ class CallStage(BaseAsyncStage):
1224
1432
  )
1225
1433
  return args
1226
1434
 
1227
- for arg in type_hints:
1228
1435
 
1229
- if arg == "return":
1230
- continue
1436
+ class BaseNestedStage(BaseStage, ABC):
1437
+ """Base Nested Stage model. This model is use for checking the child stage
1438
+ is the nested stage or not.
1439
+ """
1231
1440
 
1232
- if arg.removeprefix("_") in args:
1233
- args[arg] = args.pop(arg.removeprefix("_"))
1441
+ def set_outputs(self, output: DictData, to: DictData) -> DictData:
1442
+ """Override the set outputs method that support for nested-stage."""
1443
+ return super().set_outputs(output, to=to)
1234
1444
 
1235
- t: Any = type_hints[arg]
1445
+ def get_outputs(self, output: DictData) -> DictData:
1446
+ """Override the get outputs method that support for nested-stage"""
1447
+ return super().get_outputs(output)
1236
1448
 
1237
- # NOTE: Check Result argument was passed to this caller function.
1238
- #
1239
- # if is_dataclass(t) and t.__name__ == "Result" and arg not in args:
1240
- # args[arg] = result
1449
+ @property
1450
+ def is_nested(self) -> bool:
1451
+ """Check if this stage is a nested stage or not.
1241
1452
 
1242
- if issubclass(t, BaseModel) and arg in args:
1243
- args[arg] = t.model_validate(obj=args[arg])
1453
+ :rtype: bool
1454
+ """
1455
+ return True
1244
1456
 
1245
- return args
1457
+ @staticmethod
1458
+ def mark_errors(context: DictData, error: StageError) -> None:
1459
+ """Make the errors context result with the refs value depends on the nested
1460
+ execute func.
1246
1461
 
1462
+ :param context: (DictData) A context data.
1463
+ :param error: (StageError) A stage exception object.
1464
+ """
1465
+ if "errors" in context:
1466
+ context["errors"][error.refs] = error.to_dict()
1467
+ else:
1468
+ context["errors"] = error.to_dict(with_refs=True)
1247
1469
 
1248
- class TriggerStage(BaseStage):
1470
+
1471
+ class TriggerStage(BaseNestedStage):
1249
1472
  """Trigger workflow executor stage that run an input trigger Workflow
1250
1473
  execute method. This is the stage that allow you to create the reusable
1251
1474
  Workflow template with dynamic parameters.
@@ -1277,13 +1500,14 @@ class TriggerStage(BaseStage):
1277
1500
  event: Optional[Event] = None,
1278
1501
  ) -> Result:
1279
1502
  """Trigger another workflow execution. It will wait the trigger
1280
- workflow running complete before catching its result.
1503
+ workflow running complete before catching its result and raise error
1504
+ when the result status does not be SUCCESS.
1281
1505
 
1282
1506
  :param params: (DictData) A parameter data.
1283
1507
  :param result: (Result) A result object for keeping context and status
1284
- data.
1508
+ data. (Default is None)
1285
1509
  :param event: (Event) An event manager that use to track parent execute
1286
- was not force stopped.
1510
+ was not force stopped. (Default is None)
1287
1511
 
1288
1512
  :rtype: Result
1289
1513
  """
@@ -1295,57 +1519,28 @@ class TriggerStage(BaseStage):
1295
1519
  )
1296
1520
 
1297
1521
  _trigger: str = param2template(self.trigger, params, extras=self.extras)
1298
- result.trace.info(f"[STAGE]: Execute Trigger-Stage: {_trigger!r}")
1299
- rs: Result = Workflow.from_conf(
1522
+ result: Result = Workflow.from_conf(
1300
1523
  name=pass_env(_trigger),
1301
- extras=self.extras | {"stage_raise_error": True},
1524
+ extras=self.extras,
1302
1525
  ).execute(
1526
+ # NOTE: Should not use the `pass_env` function on this params parameter.
1303
1527
  params=param2template(self.params, params, extras=self.extras),
1304
1528
  run_id=None,
1305
1529
  parent_run_id=result.parent_run_id,
1306
1530
  event=event,
1307
1531
  )
1308
- if rs.status == FAILED:
1532
+ if result.status == FAILED:
1309
1533
  err_msg: StrOrNone = (
1310
1534
  f" with:\n{msg}"
1311
- if (msg := rs.context.get("errors", {}).get("message"))
1535
+ if (msg := result.context.get("errors", {}).get("message"))
1312
1536
  else "."
1313
1537
  )
1314
- raise StageException(
1315
- f"Trigger workflow return `FAILED` status{err_msg}"
1316
- )
1317
- return rs
1318
-
1319
-
1320
- class BaseNestedStage(BaseStage):
1321
- """Base Nested Stage model. This model is use for checking the child stage
1322
- is the nested stage or not.
1323
- """
1324
-
1325
- @abstractmethod
1326
- def execute(
1327
- self,
1328
- params: DictData,
1329
- *,
1330
- result: Optional[Result] = None,
1331
- event: Optional[Event] = None,
1332
- ) -> Result:
1333
- """Execute abstraction method that action something by sub-model class.
1334
- This is important method that make this class is able to be the nested
1335
- stage.
1336
-
1337
- :param params: (DictData) A parameter data that want to use in this
1338
- execution.
1339
- :param result: (Result) A result object for keeping context and status
1340
- data.
1341
- :param event: (Event) An event manager that use to track parent execute
1342
- was not force stopped.
1343
-
1344
- :rtype: Result
1345
- """
1346
- raise NotImplementedError(
1347
- "Nested-Stage should implement `execute` method."
1348
- )
1538
+ raise StageError(f"Trigger workflow was failed{err_msg}")
1539
+ elif result.status == CANCEL:
1540
+ raise StageCancelError("Trigger workflow was cancel.")
1541
+ elif result.status == SKIP:
1542
+ raise StageSkipError("Trigger workflow was skipped.")
1543
+ return result
1349
1544
 
1350
1545
 
1351
1546
  class ParallelStage(BaseNestedStage):
@@ -1366,10 +1561,14 @@ class ParallelStage(BaseNestedStage):
1366
1561
  ... "echo": "Start run with branch 1",
1367
1562
  ... "sleep": 3,
1368
1563
  ... },
1564
+ ... {
1565
+ ... "name": "Echo second stage",
1566
+ ... "echo": "Start run with branch 1",
1567
+ ... },
1369
1568
  ... ],
1370
1569
  ... "branch02": [
1371
1570
  ... {
1372
- ... "name": "Echo second stage",
1571
+ ... "name": "Echo first stage",
1373
1572
  ... "echo": "Start run with branch 2",
1374
1573
  ... "sleep": 1,
1375
1574
  ... },
@@ -1399,94 +1598,116 @@ class ParallelStage(BaseNestedStage):
1399
1598
  result: Result,
1400
1599
  *,
1401
1600
  event: Optional[Event] = None,
1402
- ) -> Result:
1403
- """Execute all stage with specific branch ID.
1601
+ ) -> tuple[Status, Result]:
1602
+ """Execute branch that will execute all nested-stage that was set in
1603
+ this stage with specific branch ID.
1404
1604
 
1405
1605
  :param branch: (str) A branch ID.
1406
1606
  :param params: (DictData) A parameter data.
1407
1607
  :param result: (Result) A Result instance for return context and status.
1408
1608
  :param event: (Event) An Event manager instance that use to cancel this
1409
1609
  execution if it forces stopped by parent execution.
1610
+ (Default is None)
1410
1611
 
1411
- :rtype: Result
1612
+ :raise StageCancelError: If event was set.
1613
+
1614
+ :rtype: tuple[Status, Result]
1412
1615
  """
1413
1616
  result.trace.debug(f"[STAGE]: Execute Branch: {branch!r}")
1617
+
1618
+ # NOTE: Create nested-context
1414
1619
  context: DictData = copy.deepcopy(params)
1415
1620
  context.update({"branch": branch})
1416
- output: DictData = {"branch": branch, "stages": {}}
1417
- for stage in self.parallel[branch]:
1621
+ nestet_context: DictData = {"branch": branch, "stages": {}}
1622
+
1623
+ total_stage: int = len(self.parallel[branch])
1624
+ skips: list[bool] = [False] * total_stage
1625
+ for i, stage in enumerate(self.parallel[branch], start=0):
1418
1626
 
1419
1627
  if self.extras:
1420
1628
  stage.extras = self.extras
1421
1629
 
1422
- if stage.is_skipped(params=context):
1423
- result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
1424
- stage.set_outputs(output={"skipped": True}, to=output)
1425
- continue
1426
-
1427
1630
  if event and event.is_set():
1428
1631
  error_msg: str = (
1429
- "Branch-Stage was canceled from event that had set before "
1430
- "stage branch execution."
1632
+ "Branch execution was canceled from the event before "
1633
+ "start branch execution."
1431
1634
  )
1432
1635
  result.catch(
1433
1636
  status=CANCEL,
1434
1637
  parallel={
1435
1638
  branch: {
1639
+ "status": CANCEL,
1436
1640
  "branch": branch,
1437
- "stages": filter_func(output.pop("stages", {})),
1438
- "errors": StageException(error_msg).to_dict(),
1641
+ "stages": filter_func(
1642
+ nestet_context.pop("stages", {})
1643
+ ),
1644
+ "errors": StageCancelError(error_msg).to_dict(),
1439
1645
  }
1440
1646
  },
1441
1647
  )
1442
- raise StageException(error_msg, refs=branch)
1648
+ raise StageCancelError(error_msg, refs=branch)
1443
1649
 
1444
- try:
1445
- rs: Result = stage.handler_execute(
1446
- params=context,
1447
- run_id=result.run_id,
1448
- parent_run_id=result.parent_run_id,
1449
- raise_error=True,
1450
- event=event,
1650
+ rs: Result = stage.handler_execute(
1651
+ params=context,
1652
+ run_id=result.run_id,
1653
+ parent_run_id=result.parent_run_id,
1654
+ event=event,
1655
+ )
1656
+ stage.set_outputs(rs.context, to=nestet_context)
1657
+ stage.set_outputs(stage.get_outputs(nestet_context), to=context)
1658
+
1659
+ if rs.status == SKIP:
1660
+ skips[i] = True
1661
+ continue
1662
+
1663
+ elif rs.status == FAILED: # pragma: no cov
1664
+ error_msg: str = (
1665
+ f"Branch execution was break because its nested-stage, "
1666
+ f"{stage.iden!r}, failed."
1451
1667
  )
1452
- stage.set_outputs(rs.context, to=output)
1453
- stage.set_outputs(stage.get_outputs(output), to=context)
1454
- except StageException as e:
1455
1668
  result.catch(
1456
1669
  status=FAILED,
1457
1670
  parallel={
1458
1671
  branch: {
1672
+ "status": FAILED,
1459
1673
  "branch": branch,
1460
- "stages": filter_func(output.pop("stages", {})),
1461
- "errors": e.to_dict(),
1674
+ "stages": filter_func(
1675
+ nestet_context.pop("stages", {})
1676
+ ),
1677
+ "errors": StageError(error_msg).to_dict(),
1462
1678
  },
1463
1679
  },
1464
1680
  )
1465
- raise StageException(str(e), refs=branch) from e
1681
+ raise StageError(error_msg, refs=branch)
1466
1682
 
1467
- if rs.status == FAILED:
1683
+ elif rs.status == CANCEL:
1468
1684
  error_msg: str = (
1469
- f"Branch-Stage was break because it has a sub stage, "
1470
- f"{stage.iden}, failed without raise error."
1685
+ "Branch execution was canceled from the event after "
1686
+ "end branch execution."
1471
1687
  )
1472
1688
  result.catch(
1473
- status=FAILED,
1689
+ status=CANCEL,
1474
1690
  parallel={
1475
1691
  branch: {
1692
+ "status": CANCEL,
1476
1693
  "branch": branch,
1477
- "stages": filter_func(output.pop("stages", {})),
1478
- "errors": StageException(error_msg).to_dict(),
1479
- },
1694
+ "stages": filter_func(
1695
+ nestet_context.pop("stages", {})
1696
+ ),
1697
+ "errors": StageCancelError(error_msg).to_dict(),
1698
+ }
1480
1699
  },
1481
1700
  )
1482
- raise StageException(error_msg, refs=branch)
1701
+ raise StageCancelError(error_msg, refs=branch)
1483
1702
 
1484
- return result.catch(
1485
- status=SUCCESS,
1703
+ status: Status = SKIP if sum(skips) == total_stage else SUCCESS
1704
+ return status, result.catch(
1705
+ status=status,
1486
1706
  parallel={
1487
1707
  branch: {
1708
+ "status": status,
1488
1709
  "branch": branch,
1489
- "stages": filter_func(output.pop("stages", {})),
1710
+ "stages": filter_func(nestet_context.pop("stages", {})),
1490
1711
  },
1491
1712
  },
1492
1713
  )
@@ -1504,6 +1725,7 @@ class ParallelStage(BaseNestedStage):
1504
1725
  :param result: (Result) A Result instance for return context and status.
1505
1726
  :param event: (Event) An Event manager instance that use to cancel this
1506
1727
  execution if it forces stopped by parent execution.
1728
+ (Default is None)
1507
1729
 
1508
1730
  :rtype: Result
1509
1731
  """
@@ -1512,28 +1734,18 @@ class ParallelStage(BaseNestedStage):
1512
1734
  extras=self.extras,
1513
1735
  )
1514
1736
  event: Event = event or Event()
1515
- result.trace.info(
1516
- f"[STAGE]: Execute Parallel-Stage: {self.max_workers} workers."
1737
+ result.trace.info(f"[STAGE]: Parallel with {self.max_workers} workers.")
1738
+ result.catch(
1739
+ status=WAIT,
1740
+ context={"workers": self.max_workers, "parallel": {}},
1517
1741
  )
1518
- result.catch(status=WAIT, context={"parallel": {}})
1742
+ len_parallel: int = len(self.parallel)
1519
1743
  if event and event.is_set():
1520
- return result.catch(
1521
- status=CANCEL,
1522
- context={
1523
- "errors": StageException(
1524
- "Stage was canceled from event that had set "
1525
- "before stage parallel execution."
1526
- ).to_dict()
1527
- },
1744
+ raise StageCancelError(
1745
+ "Execution was canceled from the event before start parallel."
1528
1746
  )
1529
1747
 
1530
- with ThreadPoolExecutor(
1531
- max_workers=self.max_workers, thread_name_prefix="stage_parallel_"
1532
- ) as executor:
1533
-
1534
- context: DictData = {}
1535
- status: Status = SUCCESS
1536
-
1748
+ with ThreadPoolExecutor(self.max_workers, "stp") as executor:
1537
1749
  futures: list[Future] = [
1538
1750
  executor.submit(
1539
1751
  self.execute_branch,
@@ -1544,17 +1756,18 @@ class ParallelStage(BaseNestedStage):
1544
1756
  )
1545
1757
  for branch in self.parallel
1546
1758
  ]
1547
-
1548
- for future in as_completed(futures):
1759
+ context: DictData = {}
1760
+ statuses: list[Status] = [WAIT] * len_parallel
1761
+ for i, future in enumerate(as_completed(futures), start=0):
1549
1762
  try:
1550
- future.result()
1551
- except StageException as e:
1552
- status = FAILED
1553
- if "errors" in context:
1554
- context["errors"][e.refs] = e.to_dict()
1555
- else:
1556
- context["errors"] = e.to_dict(with_refs=True)
1557
- return result.catch(status=status, context=context)
1763
+ statuses[i], _ = future.result()
1764
+ except StageError as e:
1765
+ statuses[i] = get_status_from_error(e)
1766
+ self.mark_errors(context, e)
1767
+ return result.catch(
1768
+ status=validate_statuses(statuses),
1769
+ context=context,
1770
+ )
1558
1771
 
1559
1772
 
1560
1773
  class ForEachStage(BaseNestedStage):
@@ -1614,9 +1827,12 @@ class ForEachStage(BaseNestedStage):
1614
1827
  result: Result,
1615
1828
  *,
1616
1829
  event: Optional[Event] = None,
1617
- ) -> Result:
1618
- """Execute all nested stage that set on this stage with specific foreach
1619
- item parameter.
1830
+ ) -> tuple[Status, Result]:
1831
+ """Execute item that will execute all nested-stage that was set in this
1832
+ stage with specific foreach item.
1833
+
1834
+ This method will create the nested-context from an input context
1835
+ data and use it instead the context data.
1620
1836
 
1621
1837
  :param index: (int) An index value of foreach loop.
1622
1838
  :param item: (str | int) An item that want to execution.
@@ -1624,91 +1840,112 @@ class ForEachStage(BaseNestedStage):
1624
1840
  :param result: (Result) A Result instance for return context and status.
1625
1841
  :param event: (Event) An Event manager instance that use to cancel this
1626
1842
  execution if it forces stopped by parent execution.
1843
+ (Default is None)
1627
1844
 
1628
- :raise StageException: If event was set.
1629
- :raise StageException: If the stage execution raise any Exception error.
1630
- :raise StageException: If the result from execution has `FAILED` status.
1845
+ This method should raise error when it wants to stop the foreach
1846
+ loop such as cancel event or getting the failed status.
1631
1847
 
1632
- :rtype: Result
1848
+ :raise StageCancelError: If event was set.
1849
+ :raise StageError: If the stage execution raise any Exception error.
1850
+ :raise StageError: If the result from execution has `FAILED` status.
1851
+
1852
+ :rtype: tuple[Status, Result]
1633
1853
  """
1634
1854
  result.trace.debug(f"[STAGE]: Execute Item: {item!r}")
1635
1855
  key: StrOrInt = index if self.use_index_as_key else item
1856
+
1636
1857
  context: DictData = copy.deepcopy(params)
1637
1858
  context.update({"item": item, "loop": index})
1638
- output: DictData = {"item": item, "stages": {}}
1639
- for stage in self.stages:
1859
+ nestet_context: DictData = {"item": item, "stages": {}}
1860
+ total_stage: int = len(self.stages)
1861
+ skips: list[bool] = [False] * total_stage
1862
+ for i, stage in enumerate(self.stages, start=0):
1640
1863
 
1641
1864
  if self.extras:
1642
1865
  stage.extras = self.extras
1643
1866
 
1644
- if stage.is_skipped(params=context):
1645
- result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
1646
- stage.set_outputs(output={"skipped": True}, to=output)
1647
- continue
1648
-
1649
1867
  if event and event.is_set():
1650
1868
  error_msg: str = (
1651
- "Item-Stage was canceled because event was set."
1869
+ "Item execution was canceled from the event before start "
1870
+ "item execution."
1652
1871
  )
1653
1872
  result.catch(
1654
1873
  status=CANCEL,
1655
1874
  foreach={
1656
1875
  key: {
1876
+ "status": CANCEL,
1657
1877
  "item": item,
1658
- "stages": filter_func(output.pop("stages", {})),
1659
- "errors": StageException(error_msg).to_dict(),
1878
+ "stages": filter_func(
1879
+ nestet_context.pop("stages", {})
1880
+ ),
1881
+ "errors": StageCancelError(error_msg).to_dict(),
1660
1882
  }
1661
1883
  },
1662
1884
  )
1663
- raise StageException(error_msg, refs=key)
1885
+ raise StageCancelError(error_msg, refs=key)
1664
1886
 
1665
- try:
1666
- rs: Result = stage.handler_execute(
1667
- params=context,
1668
- run_id=result.run_id,
1669
- parent_run_id=result.parent_run_id,
1670
- raise_error=True,
1671
- event=event,
1887
+ rs: Result = stage.handler_execute(
1888
+ params=context,
1889
+ run_id=result.run_id,
1890
+ parent_run_id=result.parent_run_id,
1891
+ event=event,
1892
+ )
1893
+ stage.set_outputs(rs.context, to=nestet_context)
1894
+ stage.set_outputs(stage.get_outputs(nestet_context), to=context)
1895
+
1896
+ if rs.status == SKIP:
1897
+ skips[i] = True
1898
+ continue
1899
+
1900
+ elif rs.status == FAILED: # pragma: no cov
1901
+ error_msg: str = (
1902
+ f"Item execution was break because its nested-stage, "
1903
+ f"{stage.iden!r}, failed."
1672
1904
  )
1673
- stage.set_outputs(rs.context, to=output)
1674
- stage.set_outputs(stage.get_outputs(output), to=context)
1675
- except StageException as e:
1905
+ result.trace.warning(f"[STAGE]: {error_msg}")
1676
1906
  result.catch(
1677
1907
  status=FAILED,
1678
1908
  foreach={
1679
1909
  key: {
1910
+ "status": FAILED,
1680
1911
  "item": item,
1681
- "stages": filter_func(output.pop("stages", {})),
1682
- "errors": e.to_dict(),
1912
+ "stages": filter_func(
1913
+ nestet_context.pop("stages", {})
1914
+ ),
1915
+ "errors": StageError(error_msg).to_dict(),
1683
1916
  },
1684
1917
  },
1685
1918
  )
1686
- raise StageException(str(e), refs=key) from e
1919
+ raise StageError(error_msg, refs=key)
1687
1920
 
1688
- if rs.status == FAILED:
1921
+ elif rs.status == CANCEL:
1689
1922
  error_msg: str = (
1690
- f"Item-Stage was break because it has a sub stage, "
1691
- f"{stage.iden}, failed without raise error."
1923
+ "Item execution was canceled from the event after "
1924
+ "end item execution."
1692
1925
  )
1693
- result.trace.warning(f"[STAGE]: {error_msg}")
1694
1926
  result.catch(
1695
- status=FAILED,
1927
+ status=CANCEL,
1696
1928
  foreach={
1697
1929
  key: {
1930
+ "status": CANCEL,
1698
1931
  "item": item,
1699
- "stages": filter_func(output.pop("stages", {})),
1700
- "errors": StageException(error_msg).to_dict(),
1701
- },
1932
+ "stages": filter_func(
1933
+ nestet_context.pop("stages", {})
1934
+ ),
1935
+ "errors": StageCancelError(error_msg).to_dict(),
1936
+ }
1702
1937
  },
1703
1938
  )
1704
- raise StageException(error_msg, refs=key)
1939
+ raise StageCancelError(error_msg, refs=key)
1705
1940
 
1706
- return result.catch(
1707
- status=SUCCESS,
1941
+ status: Status = SKIP if sum(skips) == total_stage else SUCCESS
1942
+ return status, result.catch(
1943
+ status=status,
1708
1944
  foreach={
1709
1945
  key: {
1946
+ "status": status,
1710
1947
  "item": item,
1711
- "stages": filter_func(output.pop("stages", {})),
1948
+ "stages": filter_func(nestet_context.pop("stages", {})),
1712
1949
  },
1713
1950
  },
1714
1951
  )
@@ -1736,38 +1973,42 @@ class ForEachStage(BaseNestedStage):
1736
1973
  extras=self.extras,
1737
1974
  )
1738
1975
  event: Event = event or Event()
1739
- foreach: Union[list[str], list[int]] = (
1976
+ foreach: Union[list[str], list[int]] = pass_env(
1740
1977
  param2template(self.foreach, params, extras=self.extras)
1741
- if isinstance(self.foreach, str)
1742
- else self.foreach
1743
1978
  )
1744
1979
 
1980
+ # [NOTE]: Force convert str to list.
1981
+ if isinstance(foreach, str):
1982
+ try:
1983
+ foreach: list[Any] = str2list(foreach)
1984
+ except ValueError as e:
1985
+ raise TypeError(
1986
+ f"Does not support string foreach: {foreach!r} that can "
1987
+ f"not convert to list."
1988
+ ) from e
1989
+
1745
1990
  # [VALIDATE]: Type of the foreach should be `list` type.
1746
- if not isinstance(foreach, list):
1747
- raise TypeError(f"Does not support foreach: {foreach!r}")
1991
+ elif not isinstance(foreach, list):
1992
+ raise TypeError(
1993
+ f"Does not support foreach: {foreach!r} ({type(foreach)})"
1994
+ )
1995
+ # [Validate]: Value in the foreach item should not be duplicate when the
1996
+ # `use_index_as_key` field did not set.
1748
1997
  elif len(set(foreach)) != len(foreach) and not self.use_index_as_key:
1749
1998
  raise ValueError(
1750
1999
  "Foreach item should not duplicate. If this stage must to pass "
1751
2000
  "duplicate item, it should set `use_index_as_key: true`."
1752
2001
  )
1753
2002
 
1754
- result.trace.info(f"[STAGE]: Execute Foreach-Stage: {foreach!r}.")
2003
+ result.trace.info(f"[STAGE]: Foreach: {foreach!r}.")
1755
2004
  result.catch(status=WAIT, context={"items": foreach, "foreach": {}})
2005
+ len_foreach: int = len(foreach)
1756
2006
  if event and event.is_set():
1757
- return result.catch(
1758
- status=CANCEL,
1759
- context={
1760
- "errors": StageException(
1761
- "Stage was canceled from event that had set "
1762
- "before stage foreach execution."
1763
- ).to_dict()
1764
- },
2007
+ raise StageCancelError(
2008
+ "Execution was canceled from the event before start foreach."
1765
2009
  )
1766
2010
 
1767
- with ThreadPoolExecutor(
1768
- max_workers=self.concurrent, thread_name_prefix="stage_foreach_"
1769
- ) as executor:
1770
-
2011
+ with ThreadPoolExecutor(self.concurrent, "stf") as executor:
1771
2012
  futures: list[Future] = [
1772
2013
  executor.submit(
1773
2014
  self.execute_item,
@@ -1779,19 +2020,21 @@ class ForEachStage(BaseNestedStage):
1779
2020
  )
1780
2021
  for i, item in enumerate(foreach, start=0)
1781
2022
  ]
2023
+
1782
2024
  context: DictData = {}
1783
- status: Status = SUCCESS
2025
+ statuses: list[Status] = [WAIT] * len_foreach
2026
+ fail_fast: bool = False
1784
2027
 
1785
2028
  done, not_done = wait(futures, return_when=FIRST_EXCEPTION)
1786
2029
  if len(list(done)) != len(futures):
1787
2030
  result.trace.warning(
1788
- "[STAGE]: Set event for stop pending for-each stage."
2031
+ "[STAGE]: Set the event for stop pending for-each stage."
1789
2032
  )
1790
2033
  event.set()
1791
2034
  for future in not_done:
1792
2035
  future.cancel()
1793
- time.sleep(0.075)
1794
2036
 
2037
+ time.sleep(0.025)
1795
2038
  nd: str = (
1796
2039
  (
1797
2040
  f", {len(not_done)} item"
@@ -1804,18 +2047,24 @@ class ForEachStage(BaseNestedStage):
1804
2047
  f"[STAGE]: ... Foreach-Stage set failed event{nd}"
1805
2048
  )
1806
2049
  done: Iterator[Future] = as_completed(futures)
2050
+ fail_fast = True
1807
2051
 
1808
- for future in done:
2052
+ for i, future in enumerate(done, start=0):
1809
2053
  try:
1810
- future.result()
1811
- except StageException as e:
1812
- status = FAILED
1813
- if "errors" in context:
1814
- context["errors"][e.refs] = e.to_dict()
1815
- else:
1816
- context["errors"] = e.to_dict(with_refs=True)
2054
+ statuses[i], _ = future.result()
2055
+ except StageError as e:
2056
+ statuses[i] = get_status_from_error(e)
2057
+ self.mark_errors(context, e)
1817
2058
  except CancelledError:
1818
2059
  pass
2060
+
2061
+ status: Status = validate_statuses(statuses)
2062
+
2063
+ # NOTE: Prepare status because it does not cancel from parent event but
2064
+ # cancel from failed item execution.
2065
+ if fail_fast and status == CANCEL:
2066
+ status = FAILED
2067
+
1819
2068
  return result.catch(status=status, context=context)
1820
2069
 
1821
2070
 
@@ -1874,8 +2123,9 @@ class UntilStage(BaseNestedStage):
1874
2123
  params: DictData,
1875
2124
  result: Result,
1876
2125
  event: Optional[Event] = None,
1877
- ) -> tuple[Result, T]:
1878
- """Execute all stage with specific loop and item.
2126
+ ) -> tuple[Status, Result, T]:
2127
+ """Execute loop that will execute all nested-stage that was set in this
2128
+ stage with specific loop and item.
1879
2129
 
1880
2130
  :param item: (T) An item that want to execution.
1881
2131
  :param loop: (int) A number of loop.
@@ -1884,98 +2134,115 @@ class UntilStage(BaseNestedStage):
1884
2134
  :param event: (Event) An Event manager instance that use to cancel this
1885
2135
  execution if it forces stopped by parent execution.
1886
2136
 
1887
- :rtype: tuple[Result, T]
2137
+ :rtype: tuple[Status, Result, T]
1888
2138
  :return: Return a pair of Result and changed item.
1889
2139
  """
1890
- result.trace.debug(f"[STAGE]: ... Execute until item: {item!r}")
2140
+ result.trace.debug(f"[STAGE]: Execute Loop: {loop} (Item {item!r})")
2141
+
2142
+ # NOTE: Create nested-context
1891
2143
  context: DictData = copy.deepcopy(params)
1892
- context.update({"item": item})
1893
- output: DictData = {"loop": loop, "item": item, "stages": {}}
1894
- next_item: T = None
1895
- for stage in self.stages:
2144
+ context.update({"item": item, "loop": loop})
2145
+ nestet_context: DictData = {"loop": loop, "item": item, "stages": {}}
2146
+
2147
+ next_item: Optional[T] = None
2148
+ total_stage: int = len(self.stages)
2149
+ skips: list[bool] = [False] * total_stage
2150
+ for i, stage in enumerate(self.stages, start=0):
1896
2151
 
1897
2152
  if self.extras:
1898
2153
  stage.extras = self.extras
1899
2154
 
1900
- if stage.is_skipped(params=context):
1901
- result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
1902
- stage.set_outputs(output={"skipped": True}, to=output)
1903
- continue
1904
-
1905
2155
  if event and event.is_set():
1906
2156
  error_msg: str = (
1907
- "Loop-Stage was canceled from event that had set before "
1908
- "stage loop execution."
2157
+ "Loop execution was canceled from the event before start "
2158
+ "loop execution."
1909
2159
  )
1910
- return (
1911
- result.catch(
1912
- status=CANCEL,
1913
- until={
1914
- loop: {
1915
- "loop": loop,
1916
- "item": item,
1917
- "stages": filter_func(output.pop("stages", {})),
1918
- "errors": StageException(error_msg).to_dict(),
1919
- }
1920
- },
1921
- ),
1922
- next_item,
2160
+ result.catch(
2161
+ status=CANCEL,
2162
+ until={
2163
+ loop: {
2164
+ "status": CANCEL,
2165
+ "loop": loop,
2166
+ "item": item,
2167
+ "stages": filter_func(
2168
+ nestet_context.pop("stages", {})
2169
+ ),
2170
+ "errors": StageCancelError(error_msg).to_dict(),
2171
+ }
2172
+ },
1923
2173
  )
2174
+ raise StageCancelError(error_msg, refs=loop)
1924
2175
 
1925
- try:
1926
- rs: Result = stage.handler_execute(
1927
- params=context,
1928
- run_id=result.run_id,
1929
- parent_run_id=result.parent_run_id,
1930
- raise_error=True,
1931
- event=event,
1932
- )
1933
- stage.set_outputs(rs.context, to=output)
2176
+ rs: Result = stage.handler_execute(
2177
+ params=context,
2178
+ run_id=result.run_id,
2179
+ parent_run_id=result.parent_run_id,
2180
+ event=event,
2181
+ )
2182
+ stage.set_outputs(rs.context, to=nestet_context)
2183
+
2184
+ if "item" in (_output := stage.get_outputs(nestet_context)):
2185
+ next_item = _output["item"]
1934
2186
 
1935
- if "item" in (_output := stage.get_outputs(output)):
1936
- next_item = _output["item"]
2187
+ stage.set_outputs(_output, to=context)
2188
+
2189
+ if rs.status == SKIP:
2190
+ skips[i] = True
2191
+ continue
1937
2192
 
1938
- stage.set_outputs(_output, to=context)
1939
- except StageException as e:
2193
+ elif rs.status == FAILED:
2194
+ error_msg: str = (
2195
+ f"Loop execution was break because its nested-stage, "
2196
+ f"{stage.iden!r}, failed."
2197
+ )
1940
2198
  result.catch(
1941
2199
  status=FAILED,
1942
2200
  until={
1943
2201
  loop: {
2202
+ "status": FAILED,
1944
2203
  "loop": loop,
1945
2204
  "item": item,
1946
- "stages": filter_func(output.pop("stages", {})),
1947
- "errors": e.to_dict(),
2205
+ "stages": filter_func(
2206
+ nestet_context.pop("stages", {})
2207
+ ),
2208
+ "errors": StageError(error_msg).to_dict(),
1948
2209
  }
1949
2210
  },
1950
2211
  )
1951
- raise
2212
+ raise StageError(error_msg, refs=loop)
1952
2213
 
1953
- if rs.status == FAILED:
2214
+ elif rs.status == CANCEL:
1954
2215
  error_msg: str = (
1955
- f"Loop-Stage was break because it has a sub stage, "
1956
- f"{stage.iden}, failed without raise error."
2216
+ "Loop execution was canceled from the event after "
2217
+ "end loop execution."
1957
2218
  )
1958
2219
  result.catch(
1959
- status=FAILED,
2220
+ status=CANCEL,
1960
2221
  until={
1961
2222
  loop: {
2223
+ "status": CANCEL,
1962
2224
  "loop": loop,
1963
2225
  "item": item,
1964
- "stages": filter_func(output.pop("stages", {})),
1965
- "errors": StageException(error_msg).to_dict(),
2226
+ "stages": filter_func(
2227
+ nestet_context.pop("stages", {})
2228
+ ),
2229
+ "errors": StageCancelError(error_msg).to_dict(),
1966
2230
  }
1967
2231
  },
1968
2232
  )
1969
- raise StageException(error_msg)
2233
+ raise StageCancelError(error_msg, refs=loop)
1970
2234
 
2235
+ status: Status = SKIP if sum(skips) == total_stage else SUCCESS
1971
2236
  return (
2237
+ status,
1972
2238
  result.catch(
1973
- status=SUCCESS,
2239
+ status=status,
1974
2240
  until={
1975
2241
  loop: {
2242
+ "status": status,
1976
2243
  "loop": loop,
1977
2244
  "item": item,
1978
- "stages": filter_func(output.pop("stages", {})),
2245
+ "stages": filter_func(nestet_context.pop("stages", {})),
1979
2246
  }
1980
2247
  },
1981
2248
  ),
@@ -1989,12 +2256,14 @@ class UntilStage(BaseNestedStage):
1989
2256
  result: Optional[Result] = None,
1990
2257
  event: Optional[Event] = None,
1991
2258
  ) -> Result:
1992
- """Execute until loop with checking until condition.
2259
+ """Execute until loop with checking the until condition before release
2260
+ the next loop.
1993
2261
 
1994
2262
  :param params: (DictData) A parameter data.
1995
2263
  :param result: (Result) A Result instance for return context and status.
1996
2264
  :param event: (Event) An Event manager instance that use to cancel this
1997
2265
  execution if it forces stopped by parent execution.
2266
+ (Default is None)
1998
2267
 
1999
2268
  :rtype: Result
2000
2269
  """
@@ -2002,29 +2271,24 @@ class UntilStage(BaseNestedStage):
2002
2271
  run_id=gen_id(self.name + (self.id or ""), unique=True),
2003
2272
  extras=self.extras,
2004
2273
  )
2005
-
2006
- result.trace.info(f"[STAGE]: Execute Until-Stage: {self.until}")
2007
- item: Union[str, int, bool] = param2template(
2008
- self.item, params, extras=self.extras
2274
+ event: Event = event or Event()
2275
+ result.trace.info(f"[STAGE]: Until: {self.until!r}")
2276
+ item: Union[str, int, bool] = pass_env(
2277
+ param2template(self.item, params, extras=self.extras)
2009
2278
  )
2010
2279
  loop: int = 1
2011
- track: bool = True
2280
+ until_rs: bool = True
2012
2281
  exceed_loop: bool = False
2013
2282
  result.catch(status=WAIT, context={"until": {}})
2014
- while track and not (exceed_loop := loop >= self.max_loop):
2283
+ statuses: list[Status] = []
2284
+ while until_rs and not (exceed_loop := (loop > self.max_loop)):
2015
2285
 
2016
2286
  if event and event.is_set():
2017
- return result.catch(
2018
- status=CANCEL,
2019
- context={
2020
- "errors": StageException(
2021
- "Stage was canceled from event that had set "
2022
- "before stage loop execution."
2023
- ).to_dict()
2024
- },
2287
+ raise StageCancelError(
2288
+ "Execution was canceled from the event before start loop."
2025
2289
  )
2026
2290
 
2027
- result, item = self.execute_loop(
2291
+ status, result, item = self.execute_loop(
2028
2292
  item=item,
2029
2293
  loop=loop,
2030
2294
  params=params,
@@ -2034,34 +2298,39 @@ class UntilStage(BaseNestedStage):
2034
2298
 
2035
2299
  loop += 1
2036
2300
  if item is None:
2301
+ item: int = loop
2037
2302
  result.trace.warning(
2038
- f"[STAGE]: ... Loop-Execute not set item. It use loop: {loop} by "
2039
- f"default."
2303
+ f"[STAGE]: Return loop not set the item. It uses loop: "
2304
+ f"{loop} by default."
2040
2305
  )
2041
- item: int = loop
2042
2306
 
2043
2307
  next_track: bool = eval(
2044
- param2template(
2045
- self.until,
2046
- params | {"item": item, "loop": loop},
2047
- extras=self.extras,
2308
+ pass_env(
2309
+ param2template(
2310
+ self.until,
2311
+ params | {"item": item, "loop": loop},
2312
+ extras=self.extras,
2313
+ ),
2048
2314
  ),
2049
2315
  globals() | params | {"item": item},
2050
2316
  {},
2051
2317
  )
2052
2318
  if not isinstance(next_track, bool):
2053
- raise StageException(
2319
+ raise TypeError(
2054
2320
  "Return type of until condition not be `boolean`, getting"
2055
2321
  f": {next_track!r}"
2056
2322
  )
2057
- track: bool = not next_track
2058
- delay(0.025)
2323
+ until_rs: bool = not next_track
2324
+ statuses.append(status)
2325
+ delay(0.005)
2059
2326
 
2060
2327
  if exceed_loop:
2061
- raise StageException(
2062
- f"The until loop was exceed {self.max_loop} loops"
2328
+ error_msg: str = (
2329
+ f"Loop was exceed the maximum {self.max_loop} "
2330
+ f"loop{'s' if self.max_loop > 1 else ''}."
2063
2331
  )
2064
- return result.catch(status=SUCCESS)
2332
+ raise StageError(error_msg)
2333
+ return result.catch(status=validate_statuses(statuses))
2065
2334
 
2066
2335
 
2067
2336
  class Match(BaseModel):
@@ -2145,11 +2414,6 @@ class CaseStage(BaseNestedStage):
2145
2414
  if self.extras:
2146
2415
  stage.extras = self.extras
2147
2416
 
2148
- if stage.is_skipped(params=context):
2149
- result.trace.info(f"[STAGE]: ... Skip stage: {stage.iden!r}")
2150
- stage.set_outputs(output={"skipped": True}, to=output)
2151
- continue
2152
-
2153
2417
  if event and event.is_set():
2154
2418
  error_msg: str = (
2155
2419
  "Case-Stage was canceled from event that had set before "
@@ -2160,29 +2424,18 @@ class CaseStage(BaseNestedStage):
2160
2424
  context={
2161
2425
  "case": case,
2162
2426
  "stages": filter_func(output.pop("stages", {})),
2163
- "errors": StageException(error_msg).to_dict(),
2427
+ "errors": StageError(error_msg).to_dict(),
2164
2428
  },
2165
2429
  )
2166
2430
 
2167
- try:
2168
- rs: Result = stage.handler_execute(
2169
- params=context,
2170
- run_id=result.run_id,
2171
- parent_run_id=result.parent_run_id,
2172
- raise_error=True,
2173
- event=event,
2174
- )
2175
- stage.set_outputs(rs.context, to=output)
2176
- stage.set_outputs(stage.get_outputs(output), to=context)
2177
- except StageException as e:
2178
- return result.catch(
2179
- status=FAILED,
2180
- context={
2181
- "case": case,
2182
- "stages": filter_func(output.pop("stages", {})),
2183
- "errors": e.to_dict(),
2184
- },
2185
- )
2431
+ rs: Result = stage.handler_execute(
2432
+ params=context,
2433
+ run_id=result.run_id,
2434
+ parent_run_id=result.parent_run_id,
2435
+ event=event,
2436
+ )
2437
+ stage.set_outputs(rs.context, to=output)
2438
+ stage.set_outputs(stage.get_outputs(output), to=context)
2186
2439
 
2187
2440
  if rs.status == FAILED:
2188
2441
  error_msg: str = (
@@ -2194,7 +2447,7 @@ class CaseStage(BaseNestedStage):
2194
2447
  context={
2195
2448
  "case": case,
2196
2449
  "stages": filter_func(output.pop("stages", {})),
2197
- "errors": StageException(error_msg).to_dict(),
2450
+ "errors": StageError(error_msg).to_dict(),
2198
2451
  },
2199
2452
  )
2200
2453
  return result.catch(
@@ -2228,7 +2481,7 @@ class CaseStage(BaseNestedStage):
2228
2481
 
2229
2482
  _case: StrOrNone = param2template(self.case, params, extras=self.extras)
2230
2483
 
2231
- result.trace.info(f"[STAGE]: Execute Case-Stage: {_case!r}.")
2484
+ result.trace.info(f"[STAGE]: Case: {_case!r}.")
2232
2485
  _else: Optional[Match] = None
2233
2486
  stages: Optional[list[Stage]] = None
2234
2487
  for match in self.match:
@@ -2237,39 +2490,28 @@ class CaseStage(BaseNestedStage):
2237
2490
  continue
2238
2491
 
2239
2492
  _condition: str = param2template(c, params, extras=self.extras)
2240
- if stages is None and _case == _condition:
2493
+ if stages is None and pass_env(_case) == pass_env(_condition):
2241
2494
  stages: list[Stage] = match.stages
2242
2495
 
2243
2496
  if stages is None:
2244
2497
  if _else is None:
2245
2498
  if not self.skip_not_match:
2246
- raise StageException(
2499
+ raise StageError(
2247
2500
  "This stage does not set else for support not match "
2248
2501
  "any case."
2249
2502
  )
2250
- result.trace.info(
2251
- "[STAGE]: ... Skip this stage because it does not match."
2252
- )
2253
- error_msg: str = (
2254
- "Case-Stage was canceled because it does not match any "
2255
- "case and else condition does not set too."
2256
- )
2257
- return result.catch(
2258
- status=CANCEL,
2259
- context={"errors": StageException(error_msg).to_dict()},
2503
+ raise StageSkipError(
2504
+ "Execution was skipped because it does not match any "
2505
+ "case and the else condition does not set too."
2260
2506
  )
2507
+
2261
2508
  _case: str = "_"
2262
2509
  stages: list[Stage] = _else.stages
2263
2510
 
2264
2511
  if event and event.is_set():
2265
- return result.catch(
2266
- status=CANCEL,
2267
- context={
2268
- "errors": StageException(
2269
- "Stage was canceled from event that had set before "
2270
- "case-stage execution."
2271
- ).to_dict()
2272
- },
2512
+ raise StageCancelError(
2513
+ "Execution was canceled from the event before start "
2514
+ "case execution."
2273
2515
  )
2274
2516
 
2275
2517
  return self.execute_case(
@@ -2278,7 +2520,7 @@ class CaseStage(BaseNestedStage):
2278
2520
 
2279
2521
 
2280
2522
  class RaiseStage(BaseAsyncStage):
2281
- """Raise error stage executor that raise `StageException` that use a message
2523
+ """Raise error stage executor that raise `StageError` that use a message
2282
2524
  field for making error message before raise.
2283
2525
 
2284
2526
  Data Validate:
@@ -2291,7 +2533,7 @@ class RaiseStage(BaseAsyncStage):
2291
2533
 
2292
2534
  message: str = Field(
2293
2535
  description=(
2294
- "An error message that want to raise with `StageException` class"
2536
+ "An error message that want to raise with `StageError` class"
2295
2537
  ),
2296
2538
  alias="raise",
2297
2539
  )
@@ -2303,7 +2545,7 @@ class RaiseStage(BaseAsyncStage):
2303
2545
  result: Optional[Result] = None,
2304
2546
  event: Optional[Event] = None,
2305
2547
  ) -> Result:
2306
- """Raise the StageException object with the message field execution.
2548
+ """Raise the StageError object with the message field execution.
2307
2549
 
2308
2550
  :param params: (DictData) A parameter data.
2309
2551
  :param result: (Result) A Result instance for return context and status.
@@ -2315,8 +2557,8 @@ class RaiseStage(BaseAsyncStage):
2315
2557
  extras=self.extras,
2316
2558
  )
2317
2559
  message: str = param2template(self.message, params, extras=self.extras)
2318
- result.trace.info(f"[STAGE]: Execute Raise-Stage: ( {message} )")
2319
- raise StageException(message)
2560
+ result.trace.info(f"[STAGE]: Message: ( {message} )")
2561
+ raise StageError(message)
2320
2562
 
2321
2563
  async def axecute(
2322
2564
  self,
@@ -2343,7 +2585,7 @@ class RaiseStage(BaseAsyncStage):
2343
2585
  )
2344
2586
  message: str = param2template(self.message, params, extras=self.extras)
2345
2587
  await result.trace.ainfo(f"[STAGE]: Execute Raise-Stage: ( {message} )")
2346
- raise StageException(message)
2588
+ raise StageError(message)
2347
2589
 
2348
2590
 
2349
2591
  class DockerStage(BaseStage): # pragma: no cov
@@ -2437,7 +2679,7 @@ class DockerStage(BaseStage): # pragma: no cov
2437
2679
  )
2438
2680
  return result.catch(
2439
2681
  status=CANCEL,
2440
- context={"errors": StageException(error_msg).to_dict()},
2682
+ context={"errors": StageError(error_msg).to_dict()},
2441
2683
  )
2442
2684
 
2443
2685
  unique_image_name: str = f"{self.image}_{datetime.now():%Y%m%d%H%M%S%f}"
@@ -2507,9 +2749,7 @@ class DockerStage(BaseStage): # pragma: no cov
2507
2749
  extras=self.extras,
2508
2750
  )
2509
2751
 
2510
- result.trace.info(
2511
- f"[STAGE]: Execute Docker-Stage: {self.image}:{self.tag}"
2512
- )
2752
+ result.trace.info(f"[STAGE]: Docker: {self.image}:{self.tag}")
2513
2753
  raise NotImplementedError("Docker Stage does not implement yet.")
2514
2754
 
2515
2755
 
@@ -2608,8 +2848,6 @@ class VirtualPyStage(PyStage): # pragma: no cov
2608
2848
  run_id=gen_id(self.name + (self.id or ""), unique=True),
2609
2849
  extras=self.extras,
2610
2850
  )
2611
-
2612
- result.trace.info(f"[STAGE]: Execute VirtualPy-Stage: {self.name}")
2613
2851
  run: str = param2template(dedent(self.run), params, extras=self.extras)
2614
2852
  with self.create_py_file(
2615
2853
  py=run,
@@ -2617,19 +2855,9 @@ class VirtualPyStage(PyStage): # pragma: no cov
2617
2855
  deps=param2template(self.deps, params, extras=self.extras),
2618
2856
  run_id=result.run_id,
2619
2857
  ) as py:
2620
- result.trace.debug(f"[STAGE]: ... Create `{py}` file.")
2621
- try:
2622
- import uv
2623
-
2624
- _ = uv
2625
- except ImportError:
2626
- raise ImportError(
2627
- "The VirtualPyStage need you to install `uv` before"
2628
- "execution."
2629
- ) from None
2630
-
2858
+ result.trace.debug(f"[STAGE]: Create `{py}` file.")
2631
2859
  rs: CompletedProcess = subprocess.run(
2632
- ["uv", "run", py, "--no-cache"],
2860
+ ["python", "-m", "uv", "run", py, "--no-cache"],
2633
2861
  # ["uv", "run", "--python", "3.9", py],
2634
2862
  shell=False,
2635
2863
  capture_output=True,
@@ -2643,7 +2871,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
2643
2871
  if "\\x00" in rs.stderr
2644
2872
  else rs.stderr
2645
2873
  ).removesuffix("\n")
2646
- raise StageException(
2874
+ raise StageError(
2647
2875
  f"Subprocess: {e}\nRunning Statement:\n---\n"
2648
2876
  f"```python\n{run}\n```"
2649
2877
  )