ddeutil-workflow 0.0.41__py3-none-any.whl → 0.0.43__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,19 +3,19 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- # [x] Use config
7
- """Stage Model that use for getting stage data template from the Job Model.
8
- The stage handle the minimize task that run in some thread (same thread at
9
- its job owner) that mean it is the lowest executor of a workflow that can
10
- tracking logs.
6
+ # [x] Use dynamic config
7
+ """Stage model. It stores all stage model that use for getting stage data template
8
+ from the Job Model. The stage handle the minimize task that run in some thread
9
+ (same thread at its job owner) that mean it is the lowest executor of a workflow
10
+ that can tracking logs.
11
11
 
12
12
  The output of stage execution only return 0 status because I do not want to
13
13
  handle stage error on this stage model. I think stage model should have a lot of
14
14
  use-case, and it does not worry when I want to create a new one.
15
15
 
16
- Execution --> Ok --> Result with 0
16
+ Execution --> Ok --> Result with SUCCESS
17
17
 
18
- --> Error ┬-> Result with 1 (if env var was set)
18
+ --> Error ┬-> Result with FAILED (if env var was set)
19
19
  ╰-> Raise StageException(...)
20
20
 
21
21
  On the context I/O that pass to a stage object at execute process. The
@@ -50,9 +50,9 @@ from pydantic.functional_validators import model_validator
50
50
  from typing_extensions import Self
51
51
 
52
52
  from .__types import DictData, DictStr, TupleStr
53
- from .conf import config
53
+ from .conf import dynamic
54
54
  from .exceptions import StageException, to_dict
55
- from .result import Result, Status
55
+ from .result import FAILED, SUCCESS, Result, Status
56
56
  from .reusables import TagFunc, extract_call, not_in_template, param2template
57
57
  from .utils import (
58
58
  gen_id,
@@ -67,6 +67,7 @@ __all__: TupleStr = (
67
67
  "TriggerStage",
68
68
  "ForEachStage",
69
69
  "ParallelStage",
70
+ "RaiseStage",
70
71
  "Stage",
71
72
  )
72
73
 
@@ -79,6 +80,10 @@ class BaseStage(BaseModel, ABC):
79
80
  This class is the abstraction class for any stage class.
80
81
  """
81
82
 
83
+ extras: DictData = Field(
84
+ default_factory=dict,
85
+ description="An extra override config values.",
86
+ )
82
87
  id: Optional[str] = Field(
83
88
  default=None,
84
89
  description=(
@@ -94,10 +99,6 @@ class BaseStage(BaseModel, ABC):
94
99
  description="A stage condition statement to allow stage executable.",
95
100
  alias="if",
96
101
  )
97
- extras: DictData = Field(
98
- default_factory=dict,
99
- description="An extra override values.",
100
- )
101
102
 
102
103
  @property
103
104
  def iden(self) -> str:
@@ -151,15 +152,6 @@ class BaseStage(BaseModel, ABC):
151
152
  """
152
153
  raise NotImplementedError("Stage should implement `execute` method.")
153
154
 
154
- async def axecute(
155
- self,
156
- params: DictData,
157
- *,
158
- result: Result | None = None,
159
- event: Event | None,
160
- ) -> Result: # pragma: no cov
161
- ...
162
-
163
155
  def handler_execute(
164
156
  self,
165
157
  params: DictData,
@@ -167,7 +159,7 @@ class BaseStage(BaseModel, ABC):
167
159
  run_id: str | None = None,
168
160
  parent_run_id: str | None = None,
169
161
  result: Result | None = None,
170
- raise_error: bool = False,
162
+ raise_error: bool | None = None,
171
163
  to: DictData | None = None,
172
164
  event: Event | None = None,
173
165
  ) -> Result:
@@ -178,12 +170,12 @@ class BaseStage(BaseModel, ABC):
178
170
  specific environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR`.
179
171
 
180
172
  Execution --> Ok --> Result
181
- |-status: Status.SUCCESS
173
+ |-status: SUCCESS
182
174
  ╰-context:
183
175
  ╰-outputs: ...
184
176
 
185
177
  --> Error --> Result (if env var was set)
186
- |-status: Status.FAILED
178
+ |-status: FAILED
187
179
  ╰-errors:
188
180
  |-class: ...
189
181
  |-name: ...
@@ -217,26 +209,24 @@ class BaseStage(BaseModel, ABC):
217
209
 
218
210
  try:
219
211
  rs: Result = self.execute(params, result=result, event=event)
220
- if to is not None:
221
- return self.set_outputs(rs.context, to=to)
222
- return rs
223
- except Exception as err:
224
- result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
212
+ return self.set_outputs(rs.context, to=to) if to is not None else rs
213
+ except Exception as e:
214
+ result.trace.error(f"[STAGE]: {e.__class__.__name__}: {e}")
225
215
 
226
- if raise_error or config.stage_raise_error:
227
- if isinstance(err, StageException):
216
+ if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
217
+ if isinstance(e, StageException):
228
218
  raise
229
219
 
230
220
  raise StageException(
231
221
  f"{self.__class__.__name__}: \n\t"
232
- f"{err.__class__.__name__}: {err}"
233
- ) from None
222
+ f"{e.__class__.__name__}: {e}"
223
+ ) from e
234
224
 
235
- errors: DictData = {"errors": to_dict(err)}
225
+ errors: DictData = {"errors": to_dict(e)}
236
226
  if to is not None:
237
227
  return self.set_outputs(errors, to=to)
238
228
 
239
- return result.catch(status=Status.FAILED, context=errors)
229
+ return result.catch(status=FAILED, context=errors)
240
230
 
241
231
  def set_outputs(self, output: DictData, to: DictData) -> DictData:
242
232
  """Set an outputs from execution process to the received context. The
@@ -268,13 +258,17 @@ class BaseStage(BaseModel, ABC):
268
258
  if "stages" not in to:
269
259
  to["stages"] = {}
270
260
 
271
- if self.id is None and not config.stage_default_id:
261
+ if self.id is None and not dynamic(
262
+ "stage_default_id", extras=self.extras
263
+ ):
272
264
  return to
273
265
 
274
266
  _id: str = (
275
- param2template(self.id, params=to)
267
+ param2template(self.id, params=to, extras=self.extras)
276
268
  if self.id
277
- else gen_id(param2template(self.name, params=to))
269
+ else gen_id(
270
+ param2template(self.name, params=to, extras=self.extras)
271
+ )
278
272
  )
279
273
 
280
274
  errors: DictData = (
@@ -312,16 +306,111 @@ class BaseStage(BaseModel, ABC):
312
306
  # should use the `re` module to validate eval-string before
313
307
  # running.
314
308
  rs: bool = eval(
315
- param2template(self.condition, params), globals() | params, {}
309
+ param2template(self.condition, params, extras=self.extras),
310
+ globals() | params,
311
+ {},
316
312
  )
317
313
  if not isinstance(rs, bool):
318
314
  raise TypeError("Return type of condition does not be boolean")
319
315
  return not rs
320
- except Exception as err:
321
- raise StageException(f"{err.__class__.__name__}: {err}") from err
316
+ except Exception as e:
317
+ raise StageException(f"{e.__class__.__name__}: {e}") from e
318
+
319
+
320
+ class BaseAsyncStage(BaseStage):
321
+
322
+ @abstractmethod
323
+ def execute(
324
+ self,
325
+ params: DictData,
326
+ *,
327
+ result: Result | None = None,
328
+ event: Event | None = None,
329
+ ) -> Result: ...
330
+
331
+ @abstractmethod
332
+ async def axecute(
333
+ self,
334
+ params: DictData,
335
+ *,
336
+ result: Result | None = None,
337
+ event: Event | None = None,
338
+ ) -> Result:
339
+ """Async execution method for this Empty stage that only logging out to
340
+ stdout.
341
+
342
+ :param params: (DictData) A context data that want to add output result.
343
+ But this stage does not pass any output.
344
+ :param result: (Result) A result object for keeping context and status
345
+ data.
346
+ :param event: (Event) An event manager that use to track parent execute
347
+ was not force stopped.
348
+
349
+ :rtype: Result
350
+ """
351
+ raise NotImplementedError(
352
+ "Async Stage should implement `axecute` method."
353
+ )
354
+
355
+ async def handler_axecute(
356
+ self,
357
+ params: DictData,
358
+ *,
359
+ run_id: str | None = None,
360
+ parent_run_id: str | None = None,
361
+ result: Result | None = None,
362
+ raise_error: bool | None = None,
363
+ to: DictData | None = None,
364
+ event: Event | None = None,
365
+ ) -> Result:
366
+ """Async Handler stage execution result from the stage `execute` method.
367
+
368
+ :param params: (DictData) A parameterize value data that use in this
369
+ stage execution.
370
+ :param run_id: (str) A running stage ID for this execution.
371
+ :param parent_run_id: (str) A parent workflow running ID for this
372
+ execution.
373
+ :param result: (Result) A result object for keeping context and status
374
+ data before execution.
375
+ :param raise_error: (bool) A flag that all this method raise error
376
+ :param to: (DictData) A target object for auto set the return output
377
+ after execution.
378
+ :param event: (Event) An event manager that pass to the stage execution.
379
+
380
+ :rtype: Result
381
+ """
382
+ result: Result = Result.construct_with_rs_or_id(
383
+ result,
384
+ run_id=run_id,
385
+ parent_run_id=parent_run_id,
386
+ id_logic=self.iden,
387
+ )
388
+
389
+ try:
390
+ rs: Result = await self.axecute(params, result=result, event=event)
391
+ if to is not None:
392
+ return self.set_outputs(rs.context, to=to)
393
+ return rs
394
+ except Exception as e:
395
+ await result.trace.aerror(f"[STAGE]: {e.__class__.__name__}: {e}")
396
+
397
+ if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
398
+ if isinstance(e, StageException):
399
+ raise
400
+
401
+ raise StageException(
402
+ f"{self.__class__.__name__}: \n\t"
403
+ f"{e.__class__.__name__}: {e}"
404
+ ) from None
405
+
406
+ errors: DictData = {"errors": to_dict(e)}
407
+ if to is not None:
408
+ return self.set_outputs(errors, to=to)
322
409
 
410
+ return result.catch(status=FAILED, context=errors)
323
411
 
324
- class EmptyStage(BaseStage):
412
+
413
+ class EmptyStage(BaseAsyncStage):
325
414
  """Empty stage that do nothing (context equal empty stage) and logging the
326
415
  name of stage only to stdout.
327
416
 
@@ -372,23 +461,22 @@ class EmptyStage(BaseStage):
372
461
 
373
462
  result.trace.info(
374
463
  f"[STAGE]: Empty-Execute: {self.name!r}: "
375
- f"( {param2template(self.echo, params=params) or '...'} )"
464
+ f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
376
465
  )
377
466
  if self.sleep > 0:
378
467
  if self.sleep > 5:
379
468
  result.trace.info(f"[STAGE]: ... sleep ({self.sleep} seconds)")
380
469
  time.sleep(self.sleep)
381
470
 
382
- return result.catch(status=Status.SUCCESS)
471
+ return result.catch(status=SUCCESS)
383
472
 
384
- # TODO: Draft async execute method for the perf improvement.
385
473
  async def axecute(
386
474
  self,
387
475
  params: DictData,
388
476
  *,
389
477
  result: Result | None = None,
390
- event: Event | None,
391
- ) -> Result: # pragma: no cov
478
+ event: Event | None = None,
479
+ ) -> Result:
392
480
  """Async execution method for this Empty stage that only logging out to
393
481
  stdout.
394
482
 
@@ -406,17 +494,19 @@ class EmptyStage(BaseStage):
406
494
  run_id=gen_id(self.name + (self.id or ""), unique=True)
407
495
  )
408
496
 
409
- result.trace.info(
497
+ await result.trace.ainfo(
410
498
  f"[STAGE]: Empty-Execute: {self.name!r}: "
411
- f"( {param2template(self.echo, params=params) or '...'} )"
499
+ f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
412
500
  )
413
501
 
414
502
  if self.sleep > 0:
415
503
  if self.sleep > 5:
416
- result.trace.info(f"[STAGE]: ... sleep ({self.sleep} seconds)")
504
+ await result.trace.ainfo(
505
+ f"[STAGE]: ... sleep ({self.sleep} seconds)"
506
+ )
417
507
  await asyncio.sleep(self.sleep)
418
508
 
419
- return result.catch(status=Status.SUCCESS)
509
+ return result.catch(status=SUCCESS)
420
510
 
421
511
 
422
512
  class BashStage(BaseStage):
@@ -508,12 +598,14 @@ class BashStage(BaseStage):
508
598
  run_id=gen_id(self.name + (self.id or ""), unique=True)
509
599
  )
510
600
 
511
- bash: str = param2template(dedent(self.bash), params)
601
+ bash: str = param2template(
602
+ dedent(self.bash), params, extras=self.extras
603
+ )
512
604
 
513
605
  result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
514
606
  with self.create_sh_file(
515
607
  bash=bash,
516
- env=param2template(self.env, params),
608
+ env=param2template(self.env, params, extras=self.extras),
517
609
  run_id=result.run_id,
518
610
  ) as sh:
519
611
  result.trace.debug(f"... Start create `{sh[1]}` file.")
@@ -523,17 +615,17 @@ class BashStage(BaseStage):
523
615
 
524
616
  if rs.returncode > 0:
525
617
  # NOTE: Prepare stderr message that returning from subprocess.
526
- err: str = (
618
+ e: str = (
527
619
  rs.stderr.encode("utf-8").decode("utf-16")
528
620
  if "\\x00" in rs.stderr
529
621
  else rs.stderr
530
622
  ).removesuffix("\n")
531
623
  raise StageException(
532
- f"Subprocess: {err}\nRunning Statement:\n---\n"
624
+ f"Subprocess: {e}\nRunning Statement:\n---\n"
533
625
  f"```bash\n{bash}\n```"
534
626
  )
535
627
  return result.catch(
536
- status=Status.SUCCESS,
628
+ status=SUCCESS,
537
629
  context={
538
630
  "return_code": rs.returncode,
539
631
  "stdout": None if (out := rs.stdout.strip("\n")) == "" else out,
@@ -635,7 +727,7 @@ class PyStage(BaseStage):
635
727
  gb: DictData = (
636
728
  globals()
637
729
  | params
638
- | param2template(self.vars, params)
730
+ | param2template(self.vars, params, extras=self.extras)
639
731
  | {"result": result}
640
732
  )
641
733
 
@@ -648,10 +740,12 @@ class PyStage(BaseStage):
648
740
 
649
741
  # WARNING: The exec build-in function is very dangerous. So, it
650
742
  # should use the re module to validate exec-string before running.
651
- exec(param2template(dedent(self.run), params), gb, lc)
743
+ exec(
744
+ param2template(dedent(self.run), params, extras=self.extras), gb, lc
745
+ )
652
746
 
653
747
  return result.catch(
654
- status=Status.SUCCESS, context={"locals": lc, "globals": gb}
748
+ status=SUCCESS, context={"locals": lc, "globals": gb}
655
749
  )
656
750
 
657
751
 
@@ -716,11 +810,16 @@ class CallStage(BaseStage):
716
810
  run_id=gen_id(self.name + (self.id or ""), unique=True)
717
811
  )
718
812
 
719
- t_func: TagFunc = extract_call(param2template(self.uses, params))()
813
+ t_func: TagFunc = extract_call(
814
+ param2template(self.uses, params, extras=self.extras),
815
+ registries=self.extras.get("regis_call"),
816
+ )()
720
817
 
721
818
  # VALIDATE: check input task caller parameters that exists before
722
819
  # calling.
723
- args: DictData = {"result": result} | param2template(self.args, params)
820
+ args: DictData = {"result": result} | param2template(
821
+ self.args, params, extras=self.extras
822
+ )
724
823
  ips = inspect.signature(t_func)
725
824
  necessary_params: list[str] = [
726
825
  k
@@ -754,10 +853,12 @@ class CallStage(BaseStage):
754
853
  if inspect.iscoroutinefunction(t_func): # pragma: no cov
755
854
  loop = asyncio.get_event_loop()
756
855
  rs: DictData = loop.run_until_complete(
757
- t_func(**param2template(args, params))
856
+ t_func(**param2template(args, params, extras=self.extras))
758
857
  )
759
858
  else:
760
- rs: DictData = t_func(**param2template(args, params))
859
+ rs: DictData = t_func(
860
+ **param2template(args, params, extras=self.extras)
861
+ )
761
862
 
762
863
  # VALIDATE:
763
864
  # Check the result type from call function, it should be dict.
@@ -766,7 +867,7 @@ class CallStage(BaseStage):
766
867
  f"Return type: '{t_func.name}@{t_func.tag}' does not serialize "
767
868
  f"to result model, you change return type to `dict`."
768
869
  )
769
- return result.catch(status=Status.SUCCESS, context=rs)
870
+ return result.catch(status=SUCCESS, context=rs)
770
871
 
771
872
 
772
873
  class TriggerStage(BaseStage):
@@ -819,15 +920,16 @@ class TriggerStage(BaseStage):
819
920
  )
820
921
 
821
922
  # NOTE: Loading workflow object from trigger name.
822
- _trigger: str = param2template(self.trigger, params=params)
923
+ _trigger: str = param2template(self.trigger, params, extras=self.extras)
823
924
 
824
925
  # NOTE: Set running workflow ID from running stage ID to external
825
926
  # params on Loader object.
826
- workflow: Workflow = Workflow.from_loader(name=_trigger)
927
+ workflow: Workflow = Workflow.from_conf(_trigger, extras=self.extras)
827
928
  result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
828
929
  return workflow.execute(
829
- params=param2template(self.params, params),
930
+ params=param2template(self.params, params, extras=self.extras),
830
931
  result=result,
932
+ event=event,
831
933
  )
832
934
 
833
935
 
@@ -893,19 +995,11 @@ class ParallelStage(BaseStage): # pragma: no cov
893
995
  ).context,
894
996
  to=context,
895
997
  )
896
- except StageException as err: # pragma: no cov
998
+ except StageException as e: # pragma: no cov
897
999
  result.trace.error(
898
- f"[STAGE]: Catch:\n\t{err.__class__.__name__}:" f"\n\t{err}"
899
- )
900
- context.update(
901
- {
902
- "errors": {
903
- "class": err,
904
- "name": err.__class__.__name__,
905
- "message": f"{err.__class__.__name__}: {err}",
906
- },
907
- },
1000
+ f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
908
1001
  )
1002
+ context.update({"errors": e.to_dict()})
909
1003
  return context
910
1004
 
911
1005
  def execute(
@@ -931,8 +1025,11 @@ class ParallelStage(BaseStage): # pragma: no cov
931
1025
  run_id=gen_id(self.name + (self.id or ""), unique=True)
932
1026
  )
933
1027
 
1028
+ result.trace.info(
1029
+ f"[STAGE]: Parallel-Execute with {self.max_parallel_core} cores."
1030
+ )
934
1031
  rs: DictData = {"parallel": {}}
935
- status = Status.SUCCESS
1032
+ status = SUCCESS
936
1033
  with ThreadPoolExecutor(
937
1034
  max_workers=self.max_parallel_core,
938
1035
  thread_name_prefix="parallel_stage_exec_",
@@ -956,7 +1053,7 @@ class ParallelStage(BaseStage): # pragma: no cov
956
1053
  rs["parallel"][context.pop("branch")] = context
957
1054
 
958
1055
  if "errors" in context:
959
- status = Status.FAILED
1056
+ status = FAILED
960
1057
 
961
1058
  return result.catch(status=status, context=rs)
962
1059
 
@@ -981,7 +1078,7 @@ class ForEachStage(BaseStage):
981
1078
  ... }
982
1079
  """
983
1080
 
984
- foreach: Union[list[str], list[int]] = Field(
1081
+ foreach: Union[list[str], list[int], str] = Field(
985
1082
  description=(
986
1083
  "A items for passing to each stages via ${{ item }} template."
987
1084
  ),
@@ -1023,10 +1120,21 @@ class ForEachStage(BaseStage):
1023
1120
  run_id=gen_id(self.name + (self.id or ""), unique=True)
1024
1121
  )
1025
1122
 
1026
- rs: DictData = {"items": self.foreach, "foreach": {}}
1027
- status = Status.SUCCESS
1123
+ foreach: Union[list[str], list[int]] = (
1124
+ param2template(self.foreach, params, extras=self.extras)
1125
+ if isinstance(self.foreach, str)
1126
+ else self.foreach
1127
+ )
1128
+ if not isinstance(foreach, list):
1129
+ raise StageException(
1130
+ f"Foreach does not support foreach value: {foreach!r}"
1131
+ )
1132
+
1133
+ result.trace.info(f"[STAGE]: Foreach-Execute: {foreach!r}.")
1134
+ rs: DictData = {"items": foreach, "foreach": {}}
1135
+ status: Status = SUCCESS
1028
1136
  # TODO: Implement concurrent more than 1.
1029
- for item in self.foreach:
1137
+ for item in foreach:
1030
1138
  result.trace.debug(f"[STAGE]: Execute foreach item: {item!r}")
1031
1139
  params["item"] = item
1032
1140
  context = {"stages": {}}
@@ -1041,21 +1149,12 @@ class ForEachStage(BaseStage):
1041
1149
  ).context,
1042
1150
  to=context,
1043
1151
  )
1044
- except StageException as err: # pragma: no cov
1045
- status = Status.FAILED
1152
+ except StageException as e: # pragma: no cov
1153
+ status = FAILED
1046
1154
  result.trace.error(
1047
- f"[STAGE]: Catch:\n\t{err.__class__.__name__}:"
1048
- f"\n\t{err}"
1049
- )
1050
- context.update(
1051
- {
1052
- "errors": {
1053
- "class": err,
1054
- "name": err.__class__.__name__,
1055
- "message": f"{err.__class__.__name__}: {err}",
1056
- },
1057
- },
1155
+ f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
1058
1156
  )
1157
+ context.update({"errors": e.to_dict()})
1059
1158
 
1060
1159
  rs["foreach"][item] = context
1061
1160
 
@@ -1064,10 +1163,24 @@ class ForEachStage(BaseStage):
1064
1163
 
1065
1164
  # TODO: Not implement this stages yet
1066
1165
  class UntilStage(BaseStage): # pragma: no cov
1067
- """Until execution stage."""
1166
+ """Until execution stage.
1167
+
1168
+ Data Validate:
1169
+ >>> stage = {
1170
+ ... "name": "Until stage execution",
1171
+ ... "item": 1,
1172
+ ... "until": "${{ item }} > 3"
1173
+ ... "stages": [
1174
+ ... {
1175
+ ... "name": "Start increase item value.",
1176
+ ... "run": "item = ${{ item }}\\nitem += 1\\n"
1177
+ ... },
1178
+ ... ],
1179
+ ... }
1180
+ """
1068
1181
 
1069
- until: str = Field(description="A until condition.")
1070
1182
  item: Union[str, int, bool] = Field(description="An initial value.")
1183
+ until: str = Field(description="A until condition.")
1071
1184
  stages: list[Stage] = Field(
1072
1185
  default_factory=list,
1073
1186
  description=(
@@ -1090,8 +1203,14 @@ class UntilStage(BaseStage): # pragma: no cov
1090
1203
 
1091
1204
 
1092
1205
  # TODO: Not implement this stages yet
1093
- class IfStage(BaseStage): # pragma: no cov
1094
- """If execution stage.
1206
+ class Match(BaseModel):
1207
+ case: Union[str, int]
1208
+ stage: Stage
1209
+
1210
+
1211
+ # TODO: Not implement this stages yet
1212
+ class CaseStage(BaseStage): # pragma: no cov
1213
+ """Case execution stage.
1095
1214
 
1096
1215
  Data Validate:
1097
1216
  >>> stage = {
@@ -1125,7 +1244,7 @@ class IfStage(BaseStage): # pragma: no cov
1125
1244
  """
1126
1245
 
1127
1246
  case: str = Field(description="A case condition for routing.")
1128
- match: list[dict[str, Union[str, Stage]]]
1247
+ match: list[Match]
1129
1248
 
1130
1249
  def execute(
1131
1250
  self,
@@ -1133,7 +1252,51 @@ class IfStage(BaseStage): # pragma: no cov
1133
1252
  *,
1134
1253
  result: Result | None = None,
1135
1254
  event: Event | None = None,
1136
- ) -> Result: ...
1255
+ ) -> Result:
1256
+ """Execute case-match condition that pass to the case field.
1257
+
1258
+ :param params: A parameter that want to pass before run any statement.
1259
+ :param result: (Result) A result object for keeping context and status
1260
+ data.
1261
+ :param event: (Event) An event manager that use to track parent execute
1262
+ was not force stopped.
1263
+
1264
+ :rtype: Result
1265
+ """
1266
+ if result is None: # pragma: no cov
1267
+ result: Result = Result(
1268
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
1269
+ )
1270
+ status = SUCCESS
1271
+ _case = param2template(self.case, params, extras=self.extras)
1272
+ _else = None
1273
+ context = {}
1274
+ for match in self.match:
1275
+ if (c := match.case) != "_":
1276
+ _condition = param2template(c, params, extras=self.extras)
1277
+ else:
1278
+ _else = match
1279
+ continue
1280
+
1281
+ if match == _condition:
1282
+ stage: Stage = match.stage
1283
+ try:
1284
+ stage.set_outputs(
1285
+ stage.handler_execute(
1286
+ params=params,
1287
+ run_id=result.run_id,
1288
+ parent_run_id=result.parent_run_id,
1289
+ ).context,
1290
+ to=context,
1291
+ )
1292
+ except StageException as e: # pragma: no cov
1293
+ status = FAILED
1294
+ result.trace.error(
1295
+ f"[STAGE]: Catch:\n\t{e.__class__.__name__}:" f"\n\t{e}"
1296
+ )
1297
+ context.update({"errors": e.to_dict()})
1298
+
1299
+ return result.catch(status=status, context=context)
1137
1300
 
1138
1301
 
1139
1302
  class RaiseStage(BaseStage): # pragma: no cov
@@ -1165,13 +1328,14 @@ class RaiseStage(BaseStage): # pragma: no cov
1165
1328
  result: Result = Result(
1166
1329
  run_id=gen_id(self.name + (self.id or ""), unique=True)
1167
1330
  )
1168
-
1169
- result.trace.error(f"[STAGE]: ... raise ( {self.message} )")
1331
+ result.trace.info(f"[STAGE]: Raise-Execute: {self.message!r}.")
1170
1332
  raise StageException(self.message)
1171
1333
 
1172
1334
 
1173
1335
  # TODO: Not implement this stages yet
1174
1336
  class HookStage(BaseStage): # pragma: no cov
1337
+ """Hook stage execution."""
1338
+
1175
1339
  hook: str
1176
1340
  args: DictData
1177
1341
  callback: str
@@ -1187,7 +1351,20 @@ class HookStage(BaseStage): # pragma: no cov
1187
1351
 
1188
1352
  # TODO: Not implement this stages yet
1189
1353
  class DockerStage(BaseStage): # pragma: no cov
1190
- """Docker container stage execution."""
1354
+ """Docker container stage execution.
1355
+
1356
+ Data Validate:
1357
+ >>> stage = {
1358
+ ... "name": "Docker stage execution",
1359
+ ... "image": "image-name.pkg.com",
1360
+ ... "env": {
1361
+ ... "ENV": "dev",
1362
+ ... },
1363
+ ... "volume": {
1364
+ ... "secrets": "/secrets",
1365
+ ... },
1366
+ ... }
1367
+ """
1191
1368
 
1192
1369
  image: str = Field(
1193
1370
  description="A Docker image url with tag that want to run.",
@@ -1224,18 +1401,6 @@ class VirtualPyStage(PyStage): # pragma: no cov
1224
1401
  return super().execute(params, result=result)
1225
1402
 
1226
1403
 
1227
- # TODO: Not implement this stages yet
1228
- class SensorStage(BaseStage): # pragma: no cov
1229
-
1230
- def execute(
1231
- self,
1232
- params: DictData,
1233
- *,
1234
- result: Result | None = None,
1235
- event: Event | None = None,
1236
- ) -> Result: ...
1237
-
1238
-
1239
1404
  # NOTE:
1240
1405
  # An order of parsing stage model on the Job model with ``stages`` field.
1241
1406
  # From the current build-in stages, they do not have stage that have the same
@@ -1243,7 +1408,6 @@ class SensorStage(BaseStage): # pragma: no cov
1243
1408
  #
1244
1409
  Stage = Annotated[
1245
1410
  Union[
1246
- EmptyStage,
1247
1411
  BashStage,
1248
1412
  CallStage,
1249
1413
  TriggerStage,
@@ -1251,6 +1415,7 @@ Stage = Annotated[
1251
1415
  ParallelStage,
1252
1416
  PyStage,
1253
1417
  RaiseStage,
1418
+ EmptyStage,
1254
1419
  ],
1255
1420
  Field(union_mode="smart"),
1256
1421
  ]