ddeutil-workflow 0.0.40__py3-none-any.whl → 0.0.42__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,6 +3,7 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
+ # [x] Use dynamic config
6
7
  """Stage Model that use for getting stage data template from the Job Model.
7
8
  The stage handle the minimize task that run in some thread (same thread at
8
9
  its job owner) that mean it is the lowest executor of a workflow that can
@@ -41,6 +42,7 @@ from inspect import Parameter
41
42
  from pathlib import Path
42
43
  from subprocess import CompletedProcess
43
44
  from textwrap import dedent
45
+ from threading import Event
44
46
  from typing import Annotated, Optional, Union
45
47
 
46
48
  from pydantic import BaseModel, Field
@@ -48,11 +50,10 @@ from pydantic.functional_validators import model_validator
48
50
  from typing_extensions import Self
49
51
 
50
52
  from .__types import DictData, DictStr, TupleStr
51
- from .caller import TagFunc, extract_call
52
- from .conf import config
53
+ from .conf import dynamic
53
54
  from .exceptions import StageException, to_dict
54
55
  from .result import Result, Status
55
- from .templates import not_in_template, param2template
56
+ from .reusables import TagFunc, extract_call, not_in_template, param2template
56
57
  from .utils import (
57
58
  gen_id,
58
59
  make_exec,
@@ -66,6 +67,7 @@ __all__: TupleStr = (
66
67
  "TriggerStage",
67
68
  "ForEachStage",
68
69
  "ParallelStage",
70
+ "RaiseStage",
69
71
  "Stage",
70
72
  )
71
73
 
@@ -93,6 +95,10 @@ class BaseStage(BaseModel, ABC):
93
95
  description="A stage condition statement to allow stage executable.",
94
96
  alias="if",
95
97
  )
98
+ extras: DictData = Field(
99
+ default_factory=dict,
100
+ description="An extra override config values.",
101
+ )
96
102
 
97
103
  @property
98
104
  def iden(self) -> str:
@@ -126,7 +132,11 @@ class BaseStage(BaseModel, ABC):
126
132
 
127
133
  @abstractmethod
128
134
  def execute(
129
- self, params: DictData, *, result: Result | None = None
135
+ self,
136
+ params: DictData,
137
+ *,
138
+ result: Result | None = None,
139
+ event: Event | None = None,
130
140
  ) -> Result:
131
141
  """Execute abstraction method that action something by sub-model class.
132
142
  This is important method that make this class is able to be the stage.
@@ -135,6 +145,8 @@ class BaseStage(BaseModel, ABC):
135
145
  execution.
136
146
  :param result: (Result) A result object for keeping context and status
137
147
  data.
148
+ :param event: (Event) An event manager that use to track parent execute
149
+ was not force stopped.
138
150
 
139
151
  :rtype: Result
140
152
  """
@@ -147,8 +159,9 @@ class BaseStage(BaseModel, ABC):
147
159
  run_id: str | None = None,
148
160
  parent_run_id: str | None = None,
149
161
  result: Result | None = None,
150
- raise_error: bool = False,
162
+ raise_error: bool | None = None,
151
163
  to: DictData | None = None,
164
+ event: Event | None = None,
152
165
  ) -> Result:
153
166
  """Handler stage execution result from the stage `execute` method.
154
167
 
@@ -183,6 +196,7 @@ class BaseStage(BaseModel, ABC):
183
196
  :param raise_error: (bool) A flag that all this method raise error
184
197
  :param to: (DictData) A target object for auto set the return output
185
198
  after execution.
199
+ :param event: (Event) An event manager that pass to the stage execution.
186
200
 
187
201
  :rtype: Result
188
202
  """
@@ -194,14 +208,14 @@ class BaseStage(BaseModel, ABC):
194
208
  )
195
209
 
196
210
  try:
197
- rs: Result = self.execute(params, result=result)
211
+ rs: Result = self.execute(params, result=result, event=event)
198
212
  if to is not None:
199
213
  return self.set_outputs(rs.context, to=to)
200
214
  return rs
201
215
  except Exception as err:
202
216
  result.trace.error(f"[STAGE]: {err.__class__.__name__}: {err}")
203
217
 
204
- if raise_error or config.stage_raise_error:
218
+ if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
205
219
  if isinstance(err, StageException):
206
220
  raise
207
221
 
@@ -246,13 +260,17 @@ class BaseStage(BaseModel, ABC):
246
260
  if "stages" not in to:
247
261
  to["stages"] = {}
248
262
 
249
- if self.id is None and not config.stage_default_id:
263
+ if self.id is None and not dynamic(
264
+ "stage_default_id", extras=self.extras
265
+ ):
250
266
  return to
251
267
 
252
268
  _id: str = (
253
- param2template(self.id, params=to)
269
+ param2template(self.id, params=to, extras=self.extras)
254
270
  if self.id
255
- else gen_id(param2template(self.name, params=to))
271
+ else gen_id(
272
+ param2template(self.name, params=to, extras=self.extras)
273
+ )
256
274
  )
257
275
 
258
276
  errors: DictData = (
@@ -290,7 +308,9 @@ class BaseStage(BaseModel, ABC):
290
308
  # should use the `re` module to validate eval-string before
291
309
  # running.
292
310
  rs: bool = eval(
293
- param2template(self.condition, params), globals() | params, {}
311
+ param2template(self.condition, params, extras=self.extras),
312
+ globals() | params,
313
+ {},
294
314
  )
295
315
  if not isinstance(rs, bool):
296
316
  raise TypeError("Return type of condition does not be boolean")
@@ -299,7 +319,102 @@ class BaseStage(BaseModel, ABC):
299
319
  raise StageException(f"{err.__class__.__name__}: {err}") from err
300
320
 
301
321
 
302
- class EmptyStage(BaseStage):
322
+ class BaseAsyncStage(BaseStage):
323
+
324
+ @abstractmethod
325
+ def execute(
326
+ self,
327
+ params: DictData,
328
+ *,
329
+ result: Result | None = None,
330
+ event: Event | None = None,
331
+ ) -> Result: ...
332
+
333
+ @abstractmethod
334
+ async def axecute(
335
+ self,
336
+ params: DictData,
337
+ *,
338
+ result: Result | None = None,
339
+ event: Event | None = None,
340
+ ) -> Result:
341
+ """Async execution method for this Empty stage that only logging out to
342
+ stdout.
343
+
344
+ :param params: (DictData) A context data that want to add output result.
345
+ But this stage does not pass any output.
346
+ :param result: (Result) A result object for keeping context and status
347
+ data.
348
+ :param event: (Event) An event manager that use to track parent execute
349
+ was not force stopped.
350
+
351
+ :rtype: Result
352
+ """
353
+ raise NotImplementedError(
354
+ "Async Stage should implement `axecute` method."
355
+ )
356
+
357
+ async def handler_axecute(
358
+ self,
359
+ params: DictData,
360
+ *,
361
+ run_id: str | None = None,
362
+ parent_run_id: str | None = None,
363
+ result: Result | None = None,
364
+ raise_error: bool | None = None,
365
+ to: DictData | None = None,
366
+ event: Event | None = None,
367
+ ) -> Result:
368
+ """Async Handler stage execution result from the stage `execute` method.
369
+
370
+ :param params: (DictData) A parameterize value data that use in this
371
+ stage execution.
372
+ :param run_id: (str) A running stage ID for this execution.
373
+ :param parent_run_id: (str) A parent workflow running ID for this
374
+ execution.
375
+ :param result: (Result) A result object for keeping context and status
376
+ data before execution.
377
+ :param raise_error: (bool) A flag that all this method raise error
378
+ :param to: (DictData) A target object for auto set the return output
379
+ after execution.
380
+ :param event: (Event) An event manager that pass to the stage execution.
381
+
382
+ :rtype: Result
383
+ """
384
+ result: Result = Result.construct_with_rs_or_id(
385
+ result,
386
+ run_id=run_id,
387
+ parent_run_id=parent_run_id,
388
+ id_logic=self.iden,
389
+ )
390
+
391
+ try:
392
+ rs: Result = await self.axecute(params, result=result, event=event)
393
+ if to is not None:
394
+ return self.set_outputs(rs.context, to=to)
395
+ return rs
396
+ except Exception as err:
397
+ await result.trace.aerror(
398
+ f"[STAGE]: {err.__class__.__name__}: {err}"
399
+ )
400
+
401
+ if dynamic("stage_raise_error", f=raise_error, extras=self.extras):
402
+ if isinstance(err, StageException):
403
+ raise
404
+
405
+ raise StageException(
406
+ f"{self.__class__.__name__}: \n\t"
407
+ f"{err.__class__.__name__}: {err}"
408
+ ) from None
409
+
410
+ errors: DictData = {"errors": to_dict(err)}
411
+ if to is not None:
412
+ return self.set_outputs(errors, to=to)
413
+
414
+ return result.catch(status=Status.FAILED, context=errors)
415
+
416
+
417
+ class EmptyStage(BaseAsyncStage):
303
418
  """Empty stage that do nothing (context equal empty stage) and logging the
304
419
  name of stage only to stdout.
305
420
 
@@ -322,7 +437,11 @@ class EmptyStage(BaseStage):
322
437
  )
323
438
 
324
439
  def execute(
325
- self, params: DictData, *, result: Result | None = None
440
+ self,
441
+ params: DictData,
442
+ *,
443
+ result: Result | None = None,
444
+ event: Event | None = None,
326
445
  ) -> Result:
327
446
  """Execution method for the Empty stage that do only logging out to
328
447
  stdout. This method does not use the `handler_result` decorator because
@@ -335,6 +454,8 @@ class EmptyStage(BaseStage):
335
454
  But this stage does not pass any output.
336
455
  :param result: (Result) A result object for keeping context and status
337
456
  data.
457
+ :param event: (Event) An event manager that use to track parent execute
458
+ was not force stopped.
338
459
 
339
460
  :rtype: Result
340
461
  """
@@ -344,7 +465,7 @@ class EmptyStage(BaseStage):
344
465
 
345
466
  result.trace.info(
346
467
  f"[STAGE]: Empty-Execute: {self.name!r}: "
347
- f"( {param2template(self.echo, params=params) or '...'} )"
468
+ f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
348
469
  )
349
470
  if self.sleep > 0:
350
471
  if self.sleep > 5:
@@ -353,21 +474,42 @@ class EmptyStage(BaseStage):
353
474
 
354
475
  return result.catch(status=Status.SUCCESS)
355
476
 
356
- # TODO: Draft async execute method for the perf improvement.
357
- async def aexecute(
358
- self, params: DictData, *, result: Result | None = None
359
- ) -> Result: # pragma: no cov
477
+ async def axecute(
478
+ self,
479
+ params: DictData,
480
+ *,
481
+ result: Result | None = None,
482
+ event: Event | None = None,
483
+ ) -> Result:
484
+ """Async execution method for this Empty stage that only logging out to
485
+ stdout.
486
+
487
+ :param params: (DictData) A context data that want to add output result.
488
+ But this stage does not pass any output.
489
+ :param result: (Result) A result object for keeping context and status
490
+ data.
491
+ :param event: (Event) An event manager that use to track parent execute
492
+ was not force stopped.
493
+
494
+ :rtype: Result
495
+ """
360
496
  if result is None: # pragma: no cov
361
497
  result: Result = Result(
362
498
  run_id=gen_id(self.name + (self.id or ""), unique=True)
363
499
  )
364
500
 
365
- result.trace.info(
501
+ await result.trace.ainfo(
366
502
  f"[STAGE]: Empty-Execute: {self.name!r}: "
367
- f"( {param2template(self.echo, params=params) or '...'} )"
503
+ f"( {param2template(self.echo, params, extras=self.extras) or '...'} )"
368
504
  )
369
505
 
370
- await asyncio.sleep(1)
506
+ if self.sleep > 0:
507
+ if self.sleep > 5:
508
+ await result.trace.ainfo(
509
+ f"[STAGE]: ... sleep ({self.sleep} seconds)"
510
+ )
511
+ await asyncio.sleep(self.sleep)
512
+
371
513
  return result.catch(status=Status.SUCCESS)
372
514
 
373
515
 
@@ -438,7 +580,11 @@ class BashStage(BaseStage):
438
580
  Path(f"./{f_name}").unlink()
439
581
 
440
582
  def execute(
441
- self, params: DictData, *, result: Result | None = None
583
+ self,
584
+ params: DictData,
585
+ *,
586
+ result: Result | None = None,
587
+ event: Event | None = None,
442
588
  ) -> Result:
443
589
  """Execute the Bash statement with the Python build-in ``subprocess``
444
590
  package.
@@ -446,6 +592,8 @@ class BashStage(BaseStage):
446
592
  :param params: A parameter data that want to use in this execution.
447
593
  :param result: (Result) A result object for keeping context and status
448
594
  data.
595
+ :param event: (Event) An event manager that use to track parent execute
596
+ was not force stopped.
449
597
 
450
598
  :rtype: Result
451
599
  """
@@ -454,12 +602,14 @@ class BashStage(BaseStage):
454
602
  run_id=gen_id(self.name + (self.id or ""), unique=True)
455
603
  )
456
604
 
457
- bash: str = param2template(dedent(self.bash), params)
605
+ bash: str = param2template(
606
+ dedent(self.bash), params, extras=self.extras
607
+ )
458
608
 
459
609
  result.trace.info(f"[STAGE]: Shell-Execute: {self.name}")
460
610
  with self.create_sh_file(
461
611
  bash=bash,
462
- env=param2template(self.env, params),
612
+ env=param2template(self.env, params, extras=self.extras),
463
613
  run_id=result.run_id,
464
614
  ) as sh:
465
615
  result.trace.debug(f"... Start create `{sh[1]}` file.")
@@ -555,7 +705,11 @@ class PyStage(BaseStage):
555
705
  return to
556
706
 
557
707
  def execute(
558
- self, params: DictData, *, result: Result | None = None
708
+ self,
709
+ params: DictData,
710
+ *,
711
+ result: Result | None = None,
712
+ event: Event | None = None,
559
713
  ) -> Result:
560
714
  """Execute the Python statement that pass all globals and input params
561
715
  to globals argument on ``exec`` build-in function.
@@ -563,6 +717,8 @@ class PyStage(BaseStage):
563
717
  :param params: A parameter that want to pass before run any statement.
564
718
  :param result: (Result) A result object for keeping context and status
565
719
  data.
720
+ :param event: (Event) An event manager that use to track parent execute
721
+ was not force stopped.
566
722
 
567
723
  :rtype: Result
568
724
  """
@@ -575,7 +731,7 @@ class PyStage(BaseStage):
575
731
  gb: DictData = (
576
732
  globals()
577
733
  | params
578
- | param2template(self.vars, params)
734
+ | param2template(self.vars, params, extras=self.extras)
579
735
  | {"result": result}
580
736
  )
581
737
 
@@ -588,7 +744,9 @@ class PyStage(BaseStage):
588
744
 
589
745
  # WARNING: The exec build-in function is very dangerous. So, it
590
746
  # should use the re module to validate exec-string before running.
591
- exec(param2template(dedent(self.run), params), gb, lc)
747
+ exec(
748
+ param2template(dedent(self.run), params, extras=self.extras), gb, lc
749
+ )
592
750
 
593
751
  return result.catch(
594
752
  status=Status.SUCCESS, context={"locals": lc, "globals": gb}
@@ -629,7 +787,11 @@ class CallStage(BaseStage):
629
787
  )
630
788
 
631
789
  def execute(
632
- self, params: DictData, *, result: Result | None = None
790
+ self,
791
+ params: DictData,
792
+ *,
793
+ result: Result | None = None,
794
+ event: Event | None = None,
633
795
  ) -> Result:
634
796
  """Execute the Call function that already in the call registry.
635
797
 
@@ -638,11 +800,12 @@ class CallStage(BaseStage):
638
800
  :raise TypeError: When the return type of call function does not be
639
801
  dict type.
640
802
 
641
- :param params: A parameter that want to pass before run any statement.
642
- :type params: DictData
803
+ :param params: (DictData) A parameter that want to pass before run any
804
+ statement.
643
805
  :param result: (Result) A result object for keeping context and status
644
806
  data.
645
- :type: str | None
807
+ :param event: (Event) An event manager that use to track parent execute
808
+ was not force stopped.
646
809
 
647
810
  :rtype: Result
648
811
  """
@@ -651,11 +814,16 @@ class CallStage(BaseStage):
651
814
  run_id=gen_id(self.name + (self.id or ""), unique=True)
652
815
  )
653
816
 
654
- t_func: TagFunc = extract_call(param2template(self.uses, params))()
817
+ t_func: TagFunc = extract_call(
818
+ param2template(self.uses, params, extras=self.extras),
819
+ registries=self.extras.get("regis_call"),
820
+ )()
655
821
 
656
822
  # VALIDATE: check input task caller parameters that exists before
657
823
  # calling.
658
- args: DictData = {"result": result} | param2template(self.args, params)
824
+ args: DictData = {"result": result} | param2template(
825
+ self.args, params, extras=self.extras
826
+ )
659
827
  ips = inspect.signature(t_func)
660
828
  necessary_params: list[str] = [
661
829
  k
@@ -689,10 +857,12 @@ class CallStage(BaseStage):
689
857
  if inspect.iscoroutinefunction(t_func): # pragma: no cov
690
858
  loop = asyncio.get_event_loop()
691
859
  rs: DictData = loop.run_until_complete(
692
- t_func(**param2template(args, params))
860
+ t_func(**param2template(args, params, extras=self.extras))
693
861
  )
694
862
  else:
695
- rs: DictData = t_func(**param2template(args, params))
863
+ rs: DictData = t_func(
864
+ **param2template(args, params, extras=self.extras)
865
+ )
696
866
 
697
867
  # VALIDATE:
698
868
  # Check the result type from call function, it should be dict.
@@ -728,7 +898,11 @@ class TriggerStage(BaseStage):
728
898
  )
729
899
 
730
900
  def execute(
731
- self, params: DictData, *, result: Result | None = None
901
+ self,
902
+ params: DictData,
903
+ *,
904
+ result: Result | None = None,
905
+ event: Event | None = None,
732
906
  ) -> Result:
733
907
  """Trigger another workflow execution. It will wait the trigger
734
908
  workflow running complete before catching its result.
@@ -736,6 +910,8 @@ class TriggerStage(BaseStage):
736
910
  :param params: A parameter data that want to use in this execution.
737
911
  :param result: (Result) A result object for keeping context and status
738
912
  data.
913
+ :param event: (Event) An event manager that use to track parent execute
914
+ was not force stopped.
739
915
 
740
916
  :rtype: Result
741
917
  """
@@ -748,15 +924,16 @@ class TriggerStage(BaseStage):
748
924
  )
749
925
 
750
926
  # NOTE: Loading workflow object from trigger name.
751
- _trigger: str = param2template(self.trigger, params=params)
927
+ _trigger: str = param2template(self.trigger, params, extras=self.extras)
752
928
 
753
929
  # NOTE: Set running workflow ID from running stage ID to external
754
930
  # params on Loader object.
755
- workflow: Workflow = Workflow.from_loader(name=_trigger)
931
+ workflow: Workflow = Workflow.from_conf(_trigger, extras=self.extras)
756
932
  result.trace.info(f"[STAGE]: Trigger-Execute: {_trigger!r}")
757
933
  return workflow.execute(
758
- params=param2template(self.params, params),
934
+ params=param2template(self.params, params, extras=self.extras),
759
935
  result=result,
936
+ event=event,
760
937
  )
761
938
 
762
939
 
@@ -838,7 +1015,11 @@ class ParallelStage(BaseStage): # pragma: no cov
838
1015
  return context
839
1016
 
840
1017
  def execute(
841
- self, params: DictData, *, result: Result | None = None
1018
+ self,
1019
+ params: DictData,
1020
+ *,
1021
+ result: Result | None = None,
1022
+ event: Event | None = None,
842
1023
  ) -> Result:
843
1024
  """Execute the stages that parallel each branch via multi-threading mode
844
1025
  or async mode by changing `async_mode` flag.
@@ -846,6 +1027,8 @@ class ParallelStage(BaseStage): # pragma: no cov
846
1027
  :param params: A parameter that want to pass before run any statement.
847
1028
  :param result: (Result) A result object for keeping context and status
848
1029
  data.
1030
+ :param event: (Event) An event manager that use to track parent execute
1031
+ was not force stopped.
849
1032
 
850
1033
  :rtype: Result
851
1034
  """
@@ -854,6 +1037,9 @@ class ParallelStage(BaseStage): # pragma: no cov
854
1037
  run_id=gen_id(self.name + (self.id or ""), unique=True)
855
1038
  )
856
1039
 
1040
+ result.trace.info(
1041
+ f"[STAGE]: Parallel-Execute with {self.max_parallel_core} cores."
1042
+ )
857
1043
  rs: DictData = {"parallel": {}}
858
1044
  status = Status.SUCCESS
859
1045
  with ThreadPoolExecutor(
@@ -904,25 +1090,40 @@ class ForEachStage(BaseStage):
904
1090
  ... }
905
1091
  """
906
1092
 
907
- foreach: Union[list[str], list[int]] = Field(
1093
+ foreach: Union[list[str], list[int], str] = Field(
908
1094
  description=(
909
1095
  "A items for passing to each stages via ${{ item }} template."
910
1096
  ),
911
1097
  )
912
1098
  stages: list[Stage] = Field(
1099
+ default_factory=list,
913
1100
  description=(
914
1101
  "A list of stage that will run with each item in the foreach field."
915
1102
  ),
916
1103
  )
1104
+ concurrent: int = Field(
1105
+ default=1,
1106
+ gt=0,
1107
+ description=(
1108
+ "A concurrent value allow to run each item at the same time. It "
1109
+ "will be sequential mode if this value equal 1."
1110
+ ),
1111
+ )
917
1112
 
918
1113
  def execute(
919
- self, params: DictData, *, result: Result | None = None
1114
+ self,
1115
+ params: DictData,
1116
+ *,
1117
+ result: Result | None = None,
1118
+ event: Event | None = None,
920
1119
  ) -> Result:
921
1120
  """Execute the stages that pass each item form the foreach field.
922
1121
 
923
1122
  :param params: A parameter that want to pass before run any statement.
924
1123
  :param result: (Result) A result object for keeping context and status
925
1124
  data.
1125
+ :param event: (Event) An event manager that use to track parent execute
1126
+ was not force stopped.
926
1127
 
927
1128
  :rtype: Result
928
1129
  """
@@ -931,9 +1132,21 @@ class ForEachStage(BaseStage):
931
1132
  run_id=gen_id(self.name + (self.id or ""), unique=True)
932
1133
  )
933
1134
 
934
- rs: DictData = {"items": self.foreach, "foreach": {}}
1135
+ foreach: Union[list[str], list[int]] = (
1136
+ param2template(self.foreach, params, extras=self.extras)
1137
+ if isinstance(self.foreach, str)
1138
+ else self.foreach
1139
+ )
1140
+ if not isinstance(foreach, list):
1141
+ raise StageException(
1142
+ f"Foreach does not support foreach value: {foreach!r}"
1143
+ )
1144
+
1145
+ result.trace.info(f"[STAGE]: Foreach-Execute: {foreach!r}.")
1146
+ rs: DictData = {"items": foreach, "foreach": {}}
935
1147
  status = Status.SUCCESS
936
- for item in self.foreach:
1148
+ # TODO: Implement concurrent more than 1.
1149
+ for item in foreach:
937
1150
  result.trace.debug(f"[STAGE]: Execute foreach item: {item!r}")
938
1151
  params["item"] = item
939
1152
  context = {"stages": {}}
@@ -970,8 +1183,55 @@ class ForEachStage(BaseStage):
970
1183
 
971
1184
 
972
1185
  # TODO: Not implement this stages yet
973
- class IfStage(BaseStage): # pragma: no cov
974
- """If execution stage.
1186
+ class UntilStage(BaseStage): # pragma: no cov
1187
+ """Until execution stage.
1188
+
1189
+ Data Validate:
1190
+ >>> stage = {
1191
+ ... "name": "Until stage execution",
1192
+ ... "item": 1,
1193
+ ... "until": "${{ item }} > 3"
1194
+ ... "stages": [
1195
+ ... {
1196
+ ... "name": "Start increase item value.",
1197
+ ... "run": "item = ${{ item }}\\nitem += 1\\n"
1198
+ ... },
1199
+ ... ],
1200
+ ... }
1201
+ """
1202
+
1203
+ item: Union[str, int, bool] = Field(description="An initial value.")
1204
+ until: str = Field(description="A until condition.")
1205
+ stages: list[Stage] = Field(
1206
+ default_factory=list,
1207
+ description=(
1208
+ "A list of stage that will run with each item until condition "
1209
+ "correct."
1210
+ ),
1211
+ )
1212
+ max_until_not_change: int = Field(
1213
+ default=3,
1214
+ description="The maximum value of loop if condition not change.",
1215
+ )
1216
+
1217
+ def execute(
1218
+ self,
1219
+ params: DictData,
1220
+ *,
1221
+ result: Result | None = None,
1222
+ event: Event | None = None,
1223
+ ) -> Result: ...
1224
+
1225
+
1226
+ # TODO: Not implement this stages yet
1227
+ class Match(BaseModel):
1228
+ case: Union[str, int]
1229
+ stage: Stage
1230
+
1231
+
1232
+ # TODO: Not implement this stages yet
1233
+ class CaseStage(BaseStage): # pragma: no cov
1234
+ """Case execution stage.
975
1235
 
976
1236
  Data Validate:
977
1237
  >>> stage = {
@@ -1005,47 +1265,150 @@ class IfStage(BaseStage): # pragma: no cov
1005
1265
  """
1006
1266
 
1007
1267
  case: str = Field(description="A case condition for routing.")
1008
- match: list[dict[str, Union[str, Stage]]]
1268
+ match: list[Match]
1009
1269
 
1010
1270
  def execute(
1011
- self, params: DictData, *, result: Result | None = None
1012
- ) -> Result: ...
1271
+ self,
1272
+ params: DictData,
1273
+ *,
1274
+ result: Result | None = None,
1275
+ event: Event | None = None,
1276
+ ) -> Result:
1277
+ """Execute case-match condition that pass to the case field.
1278
+
1279
+ :param params: A parameter that want to pass before run any statement.
1280
+ :param result: (Result) A result object for keeping context and status
1281
+ data.
1282
+ :param event: (Event) An event manager that use to track parent execute
1283
+ was not force stopped.
1284
+
1285
+ :rtype: Result
1286
+ """
1287
+ if result is None: # pragma: no cov
1288
+ result: Result = Result(
1289
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
1290
+ )
1291
+ status = Status.SUCCESS
1292
+ _case = param2template(self.case, params, extras=self.extras)
1293
+ _else = None
1294
+ context = {}
1295
+ for match in self.match:
1296
+ if (c := match.case) != "_":
1297
+ _condition = param2template(c, params, extras=self.extras)
1298
+ else:
1299
+ _else = match
1300
+ continue
1301
+
1302
+ if match == _condition:
1303
+ stage: Stage = match.stage
1304
+ try:
1305
+ stage.set_outputs(
1306
+ stage.handler_execute(
1307
+ params=params,
1308
+ run_id=result.run_id,
1309
+ parent_run_id=result.parent_run_id,
1310
+ ).context,
1311
+ to=context,
1312
+ )
1313
+ except StageException as err: # pragma: no cov
1314
+ status = Status.FAILED
1315
+ result.trace.error(
1316
+ f"[STAGE]: Catch:\n\t{err.__class__.__name__}:"
1317
+ f"\n\t{err}"
1318
+ )
1319
+ context.update(
1320
+ {
1321
+ "errors": {
1322
+ "class": err,
1323
+ "name": err.__class__.__name__,
1324
+ "message": f"{err.__class__.__name__}: {err}",
1325
+ },
1326
+ },
1327
+ )
1328
+
1329
+ return result.catch(status=status, context=context)
1013
1330
 
1014
1331
 
1015
1332
  class RaiseStage(BaseStage): # pragma: no cov
1333
+ """Raise error stage that raise StageException that use a message field for
1334
+ making error message before raise.
1335
+
1336
+ Data Validate:
1337
+ >>> stage = {
1338
+ ... "name": "Raise stage",
1339
+ ... "raise": "raise this stage",
1340
+ ... }
1341
+
1342
+ """
1343
+
1016
1344
  message: str = Field(
1017
1345
  description="An error message that want to raise",
1018
1346
  alias="raise",
1019
1347
  )
1020
1348
 
1021
1349
  def execute(
1022
- self, params: DictData, *, result: Result | None = None
1350
+ self,
1351
+ params: DictData,
1352
+ *,
1353
+ result: Result | None = None,
1354
+ event: Event | None = None,
1023
1355
  ) -> Result:
1356
+ """Raise the stage."""
1357
+ if result is None: # pragma: no cov
1358
+ result: Result = Result(
1359
+ run_id=gen_id(self.name + (self.id or ""), unique=True)
1360
+ )
1361
+ result.trace.info(f"[STAGE]: Raise-Execute: {self.message!r}.")
1024
1362
  raise StageException(self.message)
1025
1363
 
1026
1364
 
1027
1365
  # TODO: Not implement this stages yet
1028
1366
  class HookStage(BaseStage): # pragma: no cov
1367
+ """Hook stage execution."""
1368
+
1029
1369
  hook: str
1030
1370
  args: DictData
1031
1371
  callback: str
1032
1372
 
1033
1373
  def execute(
1034
- self, params: DictData, *, result: Result | None = None
1374
+ self,
1375
+ params: DictData,
1376
+ *,
1377
+ result: Result | None = None,
1378
+ event: Event | None = None,
1035
1379
  ) -> Result: ...
1036
1380
 
1037
1381
 
1038
1382
  # TODO: Not implement this stages yet
1039
1383
  class DockerStage(BaseStage): # pragma: no cov
1040
- """Docker container stage execution."""
1384
+ """Docker container stage execution.
1385
+
1386
+ Data Validate:
1387
+ >>> stage = {
1388
+ ... "name": "Docker stage execution",
1389
+ ... "image": "image-name.pkg.com",
1390
+ ... "env": {
1391
+ ... "ENV": "dev",
1392
+ ... },
1393
+ ... "volume": {
1394
+ ... "secrets": "/secrets",
1395
+ ... },
1396
+ ... }
1397
+ """
1041
1398
 
1042
- image: str
1399
+ image: str = Field(
1400
+ description="A Docker image url with tag that want to run.",
1401
+ )
1043
1402
  env: DictData = Field(default_factory=dict)
1044
1403
  volume: DictData = Field(default_factory=dict)
1045
1404
  auth: DictData = Field(default_factory=dict)
1046
1405
 
1047
1406
  def execute(
1048
- self, params: DictData, *, result: Result | None = None
1407
+ self,
1408
+ params: DictData,
1409
+ *,
1410
+ result: Result | None = None,
1411
+ event: Event | None = None,
1049
1412
  ) -> Result: ...
1050
1413
 
1051
1414
 
@@ -1059,19 +1422,15 @@ class VirtualPyStage(PyStage): # pragma: no cov
1059
1422
  def create_py_file(self, py: str, run_id: str | None): ...
1060
1423
 
1061
1424
  def execute(
1062
- self, params: DictData, *, result: Result | None = None
1425
+ self,
1426
+ params: DictData,
1427
+ *,
1428
+ result: Result | None = None,
1429
+ event: Event | None = None,
1063
1430
  ) -> Result:
1064
1431
  return super().execute(params, result=result)
1065
1432
 
1066
1433
 
1067
- # TODO: Not implement this stages yet
1068
- class SensorStage(BaseStage): # pragma: no cov
1069
-
1070
- def execute(
1071
- self, params: DictData, *, result: Result | None = None
1072
- ) -> Result: ...
1073
-
1074
-
1075
1434
  # NOTE:
1076
1435
  # An order of parsing stage model on the Job model with ``stages`` field.
1077
1436
  # From the current build-in stages, they do not have stage that have the same
@@ -1079,7 +1438,6 @@ class SensorStage(BaseStage): # pragma: no cov
1079
1438
  #
1080
1439
  Stage = Annotated[
1081
1440
  Union[
1082
- EmptyStage,
1083
1441
  BashStage,
1084
1442
  CallStage,
1085
1443
  TriggerStage,
@@ -1087,6 +1445,7 @@ Stage = Annotated[
1087
1445
  ParallelStage,
1088
1446
  PyStage,
1089
1447
  RaiseStage,
1448
+ EmptyStage,
1090
1449
  ],
1091
1450
  Field(union_mode="smart"),
1092
1451
  ]