ddeutil-workflow 0.0.83__py3-none-any.whl → 0.0.84__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.
@@ -1,2 +1,2 @@
1
- __version__: str = "0.0.83"
1
+ __version__: str = "0.0.84"
2
2
  __python_version__: str = "3.9"
@@ -206,13 +206,13 @@ class BaseStage(BaseModel, ABC):
206
206
  return self.id or self.name
207
207
 
208
208
  @field_validator("desc", mode="after")
209
- def ___prepare_desc__(cls, value: str) -> str:
209
+ def ___prepare_desc__(cls, value: Optional[str]) -> Optional[str]:
210
210
  """Prepare description string that was created on a template.
211
211
 
212
212
  Returns:
213
213
  str: A dedent and left strip newline of description string.
214
214
  """
215
- return dedent(value.lstrip("\n"))
215
+ return value if value is None else dedent(value.lstrip("\n"))
216
216
 
217
217
  @model_validator(mode="after")
218
218
  def __prepare_running_id(self) -> Self:
@@ -552,7 +552,7 @@ class BaseStage(BaseModel, ABC):
552
552
  # should use the `re` module to validate eval-string before
553
553
  # running.
554
554
  rs: bool = eval(
555
- param2template(self.condition, params, extras=self.extras),
555
+ self.pass_template(self.condition, params),
556
556
  globals() | params,
557
557
  {},
558
558
  )
@@ -583,7 +583,8 @@ class BaseStage(BaseModel, ABC):
583
583
  def is_nested(self) -> bool:
584
584
  """Return true if this stage is nested stage.
585
585
 
586
- :rtype: bool
586
+ Returns:
587
+ bool: True if this stage is nested stage.
587
588
  """
588
589
  return False
589
590
 
@@ -593,14 +594,46 @@ class BaseStage(BaseModel, ABC):
593
594
  Returns:
594
595
  DictData: A dict that was dumped from this model with alias mode.
595
596
  """
596
- return self.model_dump(by_alias=True)
597
+ return self.model_dump(
598
+ by_alias=True,
599
+ exclude_defaults=True,
600
+ exclude={"extras", "id", "name", "desc"},
601
+ )
597
602
 
598
- def md(self) -> str: # pragma: no cov
603
+ def md(self, level: int = 1) -> str: # pragma: no cov
599
604
  """Return generated document that will be the interface of this stage.
600
605
 
601
- :rtype: str
606
+ Args:
607
+ level (int, default 0): A header level that want to generate
608
+ markdown content.
609
+
610
+ Returns:
611
+ str
602
612
  """
603
- return self.desc
613
+ assert level >= 1, "Header level should gather than 0"
614
+
615
+ def align_newline(value: Optional[str]) -> str:
616
+ space: str = " " * 16
617
+ if value is None:
618
+ return ""
619
+ return value.rstrip("\n").replace("\n", f"\n{space}")
620
+
621
+ header: str = "#" * level
622
+ return dedent(
623
+ f"""
624
+ {header} Stage: {self.iden}\n
625
+ {align_newline(self.desc)}\n
626
+ #{header} Parameters\n
627
+ | name | type | default | description |
628
+ | --- | --- | --- | : --- : |\n\n
629
+ #{header} Details\n
630
+ ```json
631
+ {self.detail()}
632
+ ```
633
+ """.lstrip(
634
+ "\n"
635
+ )
636
+ )
604
637
 
605
638
  def dryrun(
606
639
  self,
@@ -610,26 +643,73 @@ class BaseStage(BaseModel, ABC):
610
643
  *,
611
644
  parent_run_id: Optional[str] = None,
612
645
  event: Optional[Event] = None,
613
- ) -> Optional[Result]: # pragma: no cov
646
+ ) -> Optional[Result]:
614
647
  """Pre-process method that will use to run with dry-run mode, and it
615
- should be used before process method.
648
+ should be used replace of process method when workflow release set with
649
+ DRYRUN mode.
650
+
651
+ By default, this method will set logic to convert this stage model
652
+ to am EmptyStage if it is action stage before use process method
653
+ instead process itself.
654
+
655
+ Args:
656
+ params (DictData): A parameter data that want to use in this
657
+ execution.
658
+ run_id (str): A running stage ID.
659
+ context (DictData): A context data.
660
+ parent_run_id (str, default None): A parent running ID.
661
+ event (Event, default None): An event manager that use to track
662
+ parent process was not force stopped.
663
+
664
+ Returns:
665
+ Result: The execution result with status and context data.
616
666
  """
667
+ trace: Trace = get_trace(
668
+ run_id, parent_run_id=parent_run_id, extras=self.extras
669
+ )
670
+ trace.debug("[STAGE]: Start Dryrun ...")
671
+ if self.action_stage:
672
+ return self.to_empty().process(
673
+ params,
674
+ run_id,
675
+ context,
676
+ parent_run_id=parent_run_id,
677
+ event=event,
678
+ )
679
+ return self.process(
680
+ params, run_id, context, parent_run_id=parent_run_id, event=event
681
+ )
617
682
 
618
- def to_empty(self, sleep: int = 0.35) -> EmptyStage: # pragma: no cov
683
+ def to_empty(
684
+ self,
685
+ sleep: int = 0.35,
686
+ *,
687
+ message: Optional[str] = None,
688
+ ) -> EmptyStage:
619
689
  """Convert the current Stage model to the EmptyStage model for dry-run
620
690
  mode if the `action_stage` class attribute has set.
621
691
 
692
+ Args:
693
+ sleep (int, default 0.35): An adjustment sleep time.
694
+ message (str, default None): A message that want to override default
695
+ message on EmptyStage model.
696
+
622
697
  Returns:
623
698
  EmptyStage: An EmptyStage model that passing itself model data to
624
699
  message.
625
700
  """
701
+ if isinstance(self, EmptyStage):
702
+ return self.model_copy(update={"sleep": sleep})
626
703
  return EmptyStage.model_validate(
627
704
  {
628
705
  "name": self.name,
629
706
  "id": self.id,
630
707
  "desc": self.desc,
631
708
  "if": self.condition,
632
- "echo": f"Convert from {self.__class__.__name__}",
709
+ "echo": (
710
+ message
711
+ or f"Convert from {self.__class__.__name__} to EmptyStage"
712
+ ),
633
713
  "sleep": sleep,
634
714
  }
635
715
  )
@@ -794,12 +874,14 @@ class BaseAsyncStage(BaseStage, ABC):
794
874
  ) -> Result:
795
875
  """Wrapped the axecute method before returning to handler axecute.
796
876
 
797
- :param params: (DictData) A parameter data that want to use in this
798
- execution.
799
- :param event: (Event) An event manager that use to track parent execute
800
- was not force stopped.
877
+ Args:
878
+ params: (DictData) A parameter data that want to use in this
879
+ execution.
880
+ event: (Event) An event manager that use to track parent execute
881
+ was not force stopped.
801
882
 
802
- :rtype: Result
883
+ Returns:
884
+ Result: A Result object.
803
885
  """
804
886
  catch(context, status=WAIT)
805
887
  return await self.async_process(
@@ -820,7 +902,10 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
820
902
  default=0,
821
903
  ge=0,
822
904
  lt=20,
823
- description="A retry number if stage execution get the error.",
905
+ description=(
906
+ "A retry number if stage process got the error exclude skip and "
907
+ "cancel exception class."
908
+ ),
824
909
  )
825
910
 
826
911
  def _execute(
@@ -834,12 +919,14 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
834
919
  """Wrapped the execute method with retry strategy before returning to
835
920
  handler execute.
836
921
 
837
- :param params: (DictData) A parameter data that want to use in this
838
- execution.
839
- :param event: (Event) An event manager that use to track parent execute
840
- was not force stopped.
922
+ Args:
923
+ params: (DictData) A parameter data that want to use in this
924
+ execution.
925
+ event: (Event) An event manager that use to track parent execute
926
+ was not force stopped.
841
927
 
842
- :rtype: Result
928
+ Returns:
929
+ Result: A Result object.
843
930
  """
844
931
  current_retry: int = 0
845
932
  exception: Exception
@@ -847,9 +934,19 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
847
934
  trace: Trace = get_trace(
848
935
  run_id, parent_run_id=parent_run_id, extras=self.extras
849
936
  )
850
-
851
937
  # NOTE: First execution for not pass to retry step if it passes.
852
938
  try:
939
+ if (
940
+ self.extras.get("__sys_release_dryrun_mode", False)
941
+ and self.action_stage
942
+ ):
943
+ return self.dryrun(
944
+ params | {"retry": current_retry},
945
+ run_id=run_id,
946
+ context=context,
947
+ parent_run_id=parent_run_id,
948
+ event=event,
949
+ )
853
950
  return self.process(
854
951
  params | {"retry": current_retry},
855
952
  run_id=run_id,
@@ -876,6 +973,17 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
876
973
  status=WAIT,
877
974
  updated={"retry": current_retry},
878
975
  )
976
+ if (
977
+ self.extras.get("__sys_release_dryrun_mode", False)
978
+ and self.action_stage
979
+ ):
980
+ return self.dryrun(
981
+ params | {"retry": current_retry},
982
+ run_id=run_id,
983
+ context=context,
984
+ parent_run_id=parent_run_id,
985
+ event=event,
986
+ )
879
987
  return self.process(
880
988
  params | {"retry": current_retry},
881
989
  run_id=run_id,
@@ -884,10 +992,10 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
884
992
  event=event,
885
993
  )
886
994
  except (
887
- StageSkipError,
888
995
  StageNestedSkipError,
889
- StageCancelError,
890
996
  StageNestedCancelError,
997
+ StageSkipError,
998
+ StageCancelError,
891
999
  ):
892
1000
  trace.debug("[STAGE]: process raise skip or cancel error.")
893
1001
  raise
@@ -916,12 +1024,14 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
916
1024
  """Wrapped the axecute method with retry strategy before returning to
917
1025
  handler axecute.
918
1026
 
919
- :param params: (DictData) A parameter data that want to use in this
920
- execution.
921
- :param event: (Event) An event manager that use to track parent execute
922
- was not force stopped.
1027
+ Args:
1028
+ params: (DictData) A parameter data that want to use in this
1029
+ execution.
1030
+ event: (Event) An event manager that use to track parent execute
1031
+ was not force stopped.
923
1032
 
924
- :rtype: Result
1033
+ Returns:
1034
+ Result: A Result object.
925
1035
  """
926
1036
  current_retry: int = 0
927
1037
  exception: Exception
@@ -932,6 +1042,17 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
932
1042
 
933
1043
  # NOTE: First execution for not pass to retry step if it passes.
934
1044
  try:
1045
+ if (
1046
+ self.extras.get("__sys_release_dryrun_mode", False)
1047
+ and self.action_stage
1048
+ ):
1049
+ return self.dryrun(
1050
+ params | {"retry": current_retry},
1051
+ run_id=run_id,
1052
+ context=context,
1053
+ parent_run_id=parent_run_id,
1054
+ event=event,
1055
+ )
935
1056
  return await self.async_process(
936
1057
  params | {"retry": current_retry},
937
1058
  run_id=run_id,
@@ -958,6 +1079,17 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
958
1079
  status=WAIT,
959
1080
  updated={"retry": current_retry},
960
1081
  )
1082
+ if (
1083
+ self.extras.get("__sys_release_dryrun_mode", False)
1084
+ and self.action_stage
1085
+ ):
1086
+ return self.dryrun(
1087
+ params | {"retry": current_retry},
1088
+ run_id=run_id,
1089
+ context=context,
1090
+ parent_run_id=parent_run_id,
1091
+ event=event,
1092
+ )
961
1093
  return await self.async_process(
962
1094
  params | {"retry": current_retry},
963
1095
  run_id=run_id,
@@ -966,10 +1098,10 @@ class BaseRetryStage(BaseAsyncStage, ABC): # pragma: no cov
966
1098
  event=event,
967
1099
  )
968
1100
  except (
969
- StageSkipError,
970
1101
  StageNestedSkipError,
971
- StageCancelError,
972
1102
  StageNestedCancelError,
1103
+ StageSkipError,
1104
+ StageCancelError,
973
1105
  ):
974
1106
  await trace.adebug(
975
1107
  "[STAGE]: process raise skip or cancel error."
@@ -1165,6 +1297,7 @@ class BashStage(BaseRetryStage):
1165
1297
  ... }
1166
1298
  """
1167
1299
 
1300
+ action_stage: ClassVar[bool] = True
1168
1301
  bash: str = Field(
1169
1302
  description=(
1170
1303
  "A bash statement that want to execute via Python subprocess."
@@ -1409,6 +1542,7 @@ class PyStage(BaseRetryStage):
1409
1542
  ... }
1410
1543
  """
1411
1544
 
1545
+ action_stage: ClassVar[bool] = True
1412
1546
  run: str = Field(
1413
1547
  description="A Python string statement that want to run with `exec`.",
1414
1548
  )
@@ -1647,6 +1781,7 @@ class CallStage(BaseRetryStage):
1647
1781
  ... }
1648
1782
  """
1649
1783
 
1784
+ action_stage: ClassVar[bool] = True
1650
1785
  uses: str = Field(
1651
1786
  description=(
1652
1787
  "A caller function with registry importer syntax that use to load "
@@ -1728,7 +1863,7 @@ class CallStage(BaseRetryStage):
1728
1863
  extras=self.extras,
1729
1864
  ),
1730
1865
  "extras": self.extras,
1731
- } | param2template(self.args, params, extras=self.extras)
1866
+ } | self.pass_template(self.args, params)
1732
1867
  sig = inspect.signature(call_func)
1733
1868
  necessary_params: list[str] = []
1734
1869
  has_keyword: bool = False
@@ -1743,6 +1878,7 @@ class CallStage(BaseRetryStage):
1743
1878
  elif v.kind == Parameter.VAR_KEYWORD:
1744
1879
  has_keyword = True
1745
1880
 
1881
+ # NOTE: Validate private parameter should exist in the args field.
1746
1882
  if any(
1747
1883
  (k.removeprefix("_") not in args and k not in args)
1748
1884
  for k in necessary_params
@@ -1760,11 +1896,12 @@ class CallStage(BaseRetryStage):
1760
1896
  f"does not set to args. It already set {list(args.keys())}."
1761
1897
  )
1762
1898
 
1763
- if "result" not in sig.parameters and not has_keyword:
1764
- args.pop("result")
1899
+ if not has_keyword:
1900
+ if "result" not in sig.parameters:
1901
+ args.pop("result")
1765
1902
 
1766
- if "extras" not in sig.parameters and not has_keyword:
1767
- args.pop("extras")
1903
+ if "extras" not in sig.parameters: # pragma: no cov
1904
+ args.pop("extras")
1768
1905
 
1769
1906
  if event and event.is_set():
1770
1907
  raise StageCancelError("Cancel before start call process.")
@@ -1847,7 +1984,7 @@ class CallStage(BaseRetryStage):
1847
1984
  extras=self.extras,
1848
1985
  ),
1849
1986
  "extras": self.extras,
1850
- } | param2template(self.args, params, extras=self.extras)
1987
+ } | self.pass_template(self.args, params)
1851
1988
  sig = inspect.signature(call_func)
1852
1989
  necessary_params: list[str] = []
1853
1990
  has_keyword: bool = False
@@ -1878,11 +2015,13 @@ class CallStage(BaseRetryStage):
1878
2015
  f"Necessary params, ({', '.join(necessary_params)}, ), "
1879
2016
  f"does not set to args. It already set {list(args.keys())}."
1880
2017
  )
1881
- if "result" not in sig.parameters and not has_keyword:
1882
- args.pop("result")
1883
2018
 
1884
- if "extras" not in sig.parameters and not has_keyword:
1885
- args.pop("extras")
2019
+ if not has_keyword:
2020
+ if "result" not in sig.parameters:
2021
+ args.pop("result")
2022
+
2023
+ if "extras" not in sig.parameters: # pragma: no cov
2024
+ args.pop("extras")
1886
2025
 
1887
2026
  if event and event.is_set():
1888
2027
  raise StageCancelError("Cancel before start call process.")
@@ -1932,11 +2071,14 @@ class CallStage(BaseRetryStage):
1932
2071
  """Validate an input arguments before passing to the caller function.
1933
2072
 
1934
2073
  Args:
1935
- func: (TagFunc) A tag function that want to get typing.
1936
- args: (DictData) An arguments before passing to this tag func.
1937
- run_id: A running stage ID.
2074
+ func (TagFunc): A tag function object that want to get typing.
2075
+ args (DictData): An arguments before passing to this tag func.
2076
+ run_id (str): A running ID.
2077
+ parent_run_id (str, default None): A parent running ID.
2078
+ extras (DictData, default None): An extra parameters.
1938
2079
 
1939
- :rtype: DictData
2080
+ Returns:
2081
+ DictData: A prepared args paramter that validate with model args.
1940
2082
  """
1941
2083
  try:
1942
2084
  override: DictData = dict(
@@ -1969,8 +2111,87 @@ class CallStage(BaseRetryStage):
1969
2111
  )
1970
2112
  return args
1971
2113
 
2114
+ def dryrun(
2115
+ self,
2116
+ params: DictData,
2117
+ run_id: str,
2118
+ context: DictData,
2119
+ *,
2120
+ parent_run_id: Optional[str] = None,
2121
+ event: Optional[Event] = None,
2122
+ ) -> Optional[Result]: # pragma: no cov
2123
+ """Override the dryrun method for this CallStage.
1972
2124
 
1973
- class BaseNestedStage(BaseRetryStage, ABC):
2125
+ Steps:
2126
+ - Pre-hook caller function that exist.
2127
+ - Show function parameters
2128
+ """
2129
+ trace: Trace = get_trace(
2130
+ run_id, parent_run_id=parent_run_id, extras=self.extras
2131
+ )
2132
+ call_func: TagFunc = self.get_caller(params=params)()
2133
+ trace.info(f"[STAGE]: Caller Func: '{call_func.name}@{call_func.tag}'")
2134
+
2135
+ args: DictData = {
2136
+ "result": Result(
2137
+ run_id=run_id,
2138
+ parent_run_id=parent_run_id,
2139
+ status=WAIT,
2140
+ context=context,
2141
+ extras=self.extras,
2142
+ ),
2143
+ "extras": self.extras,
2144
+ } | self.pass_template(self.args, params)
2145
+ sig = inspect.signature(call_func)
2146
+ trace.debug(f"[STAGE]: {sig.parameters}")
2147
+ necessary_params: list[str] = []
2148
+ has_keyword: bool = False
2149
+ for k in sig.parameters:
2150
+ if (
2151
+ v := sig.parameters[k]
2152
+ ).default == Parameter.empty and v.kind not in (
2153
+ Parameter.VAR_KEYWORD,
2154
+ Parameter.VAR_POSITIONAL,
2155
+ ):
2156
+ necessary_params.append(k)
2157
+ elif v.kind == Parameter.VAR_KEYWORD:
2158
+ has_keyword = True
2159
+
2160
+ func_typed: dict[str, Any] = get_type_hints(call_func)
2161
+ map_type: str = "||".join(
2162
+ f"\t{p}: {func_typed[p]}"
2163
+ for p in necessary_params
2164
+ if p in func_typed
2165
+ )
2166
+ map_type_args: str = "||".join(f"\t{a}: {type(a)}" for a in args)
2167
+ if not has_keyword:
2168
+ if "result" not in sig.parameters:
2169
+ args.pop("result")
2170
+
2171
+ if "extras" not in sig.parameters:
2172
+ args.pop("extras")
2173
+
2174
+ trace.debug(
2175
+ f"[STAGE]: Details"
2176
+ f"||Necessary Params:"
2177
+ f"||{map_type}"
2178
+ f"||Return Type: {func_typed['return']}"
2179
+ f"||Argument Params:"
2180
+ f"||{map_type_args}"
2181
+ f"||"
2182
+ )
2183
+ if has_keyword:
2184
+ trace.debug("[STAGE]: This caller function support keyword param.")
2185
+ return Result(
2186
+ run_id=run_id,
2187
+ parent_run_id=parent_run_id,
2188
+ status=SUCCESS,
2189
+ context=catch(context=context, status=SUCCESS),
2190
+ extras=self.extras,
2191
+ )
2192
+
2193
+
2194
+ class BaseNestedStage(BaseAsyncStage, ABC):
1974
2195
  """Base Nested Stage model. This model is use for checking the child stage
1975
2196
  is the nested stage or not.
1976
2197
  """
@@ -2035,7 +2256,7 @@ class BaseNestedStage(BaseRetryStage, ABC):
2035
2256
  )
2036
2257
 
2037
2258
 
2038
- class TriggerStage(BaseNestedStage):
2259
+ class TriggerStage(BaseRetryStage):
2039
2260
  """Trigger workflow executor stage that run an input trigger Workflow
2040
2261
  execute method. This is the stage that allow you to create the reusable
2041
2262
  Workflow template with dynamic parameters.
@@ -2093,7 +2314,7 @@ class TriggerStage(BaseNestedStage):
2093
2314
  run_id, parent_run_id=parent_run_id, extras=self.extras
2094
2315
  )
2095
2316
  _trigger: str = param2template(self.trigger, params, extras=self.extras)
2096
- if _trigger == self.extras.get("__sys_break_circle_exec", "NOTSET"):
2317
+ if _trigger == self.extras.get("__sys_exec_break_circle", "NOTSET"):
2097
2318
  raise StageError("Circle execute via trigger itself workflow name.")
2098
2319
  trace.info(f"[NESTED]: Load Workflow Config: {_trigger!r}")
2099
2320
  result: Result = Workflow.from_conf(
@@ -2142,6 +2363,33 @@ class TriggerStage(BaseNestedStage):
2142
2363
  )
2143
2364
  return result
2144
2365
 
2366
+ async def async_process(
2367
+ self,
2368
+ params: DictData,
2369
+ run_id: str,
2370
+ context: DictData,
2371
+ *,
2372
+ parent_run_id: Optional[str] = None,
2373
+ event: Optional[Event] = None,
2374
+ ) -> Result: # pragma: no cov
2375
+ """Async process for nested-stage do not implement yet.
2376
+
2377
+ Args:
2378
+ params: A parameter data that want to use in this
2379
+ execution.
2380
+ run_id: A running stage ID.
2381
+ context: A context data.
2382
+ parent_run_id: A parent running ID. (Default is None)
2383
+ event: An event manager that use to track parent process
2384
+ was not force stopped.
2385
+
2386
+ Returns:
2387
+ Result: The execution result with status and context data.
2388
+ """
2389
+ raise NotImplementedError(
2390
+ "The Trigger stage does not implement the `axecute` method yet."
2391
+ )
2392
+
2145
2393
 
2146
2394
  class ParallelContext(TypedDict):
2147
2395
  branch: str
@@ -2776,7 +3024,7 @@ class UntilStage(BaseNestedStage):
2776
3024
  ),
2777
3025
  )
2778
3026
  until: str = Field(description="A until condition for stop the while loop.")
2779
- stages: list[NestedStage] = Field(
3027
+ stages: list[SubStage] = Field(
2780
3028
  default_factory=list,
2781
3029
  description=(
2782
3030
  "A list of stage that will run with each item in until loop."
@@ -3414,7 +3662,7 @@ class RaiseStage(BaseAsyncStage):
3414
3662
  raise StageError(message)
3415
3663
 
3416
3664
 
3417
- class DockerStage(BaseStage): # pragma: no cov
3665
+ class DockerStage(BaseRetryStage): # pragma: no cov
3418
3666
  """Docker container stage execution that will pull the specific Docker image
3419
3667
  with custom authentication and run this image by passing environment
3420
3668
  variables and mounting local volume to this Docker container.
@@ -3437,6 +3685,7 @@ class DockerStage(BaseStage): # pragma: no cov
3437
3685
  ... }
3438
3686
  """
3439
3687
 
3688
+ action_stage: ClassVar[bool] = True
3440
3689
  image: str = Field(
3441
3690
  description="A Docker image url with tag that want to run.",
3442
3691
  )
@@ -3593,6 +3842,33 @@ class DockerStage(BaseStage): # pragma: no cov
3593
3842
  trace.info(f"[STAGE]: Docker: {self.image}:{self.tag}")
3594
3843
  raise NotImplementedError("Docker Stage does not implement yet.")
3595
3844
 
3845
+ async def async_process(
3846
+ self,
3847
+ params: DictData,
3848
+ run_id: str,
3849
+ context: DictData,
3850
+ *,
3851
+ parent_run_id: Optional[str] = None,
3852
+ event: Optional[Event] = None,
3853
+ ) -> Result: # pragma: no cov
3854
+ """Async process for nested-stage do not implement yet.
3855
+
3856
+ Args:
3857
+ params: A parameter data that want to use in this
3858
+ execution.
3859
+ run_id: A running stage ID.
3860
+ context: A context data.
3861
+ parent_run_id: A parent running ID. (Default is None)
3862
+ event: An event manager that use to track parent process
3863
+ was not force stopped.
3864
+
3865
+ Returns:
3866
+ Result: The execution result with status and context data.
3867
+ """
3868
+ raise NotImplementedError(
3869
+ "The Docker stage does not implement the `axecute` method yet."
3870
+ )
3871
+
3596
3872
 
3597
3873
  class VirtualPyStage(PyStage): # pragma: no cov
3598
3874
  """Virtual Python stage executor that run Python statement on the dependent
@@ -3629,7 +3905,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
3629
3905
 
3630
3906
  Args:
3631
3907
  py: A Python string statement.
3632
- values: A variable that want to set before running this
3908
+ values: A variable that want to set before running these
3633
3909
  deps: An additional Python dependencies that want install before
3634
3910
  run this python stage.
3635
3911
  run_id: (StrOrNone) A running ID of this stage execution.
@@ -3761,7 +4037,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
3761
4037
  )
3762
4038
 
3763
4039
 
3764
- NestedStage = Annotated[
4040
+ SubStage = Annotated[
3765
4041
  Union[
3766
4042
  BashStage,
3767
4043
  CallStage,
@@ -3777,7 +4053,10 @@ NestedStage = Annotated[
3777
4053
  ],
3778
4054
  Field(
3779
4055
  union_mode="smart",
3780
- description="A nested-stage allow list",
4056
+ description=(
4057
+ "A nested-stage allow list that able to use on the NestedStage "
4058
+ "model."
4059
+ ),
3781
4060
  ),
3782
4061
  ] # pragma: no cov
3783
4062
 
@@ -3790,6 +4069,7 @@ ActionStage = Annotated[
3790
4069
  PyStage,
3791
4070
  RaiseStage,
3792
4071
  DockerStage,
4072
+ TriggerStage,
3793
4073
  EmptyStage,
3794
4074
  ],
3795
4075
  Field(
ddeutil/workflow/utils.py CHANGED
@@ -405,6 +405,17 @@ def obj_name(obj: Optional[Union[str, object]] = None) -> Optional[str]:
405
405
  return obj_type
406
406
 
407
407
 
408
- def remove_sys_extras(extras: DictData) -> DictData:
409
- """Remove key that starts with `__sys_` from the extra dict parameter."""
410
- return {k: extras[k] for k in extras if not k.startswith("__sys_")}
408
+ def pop_sys_extras(extras: DictData, scope: str = "exec") -> DictData:
409
+ """Remove key that starts with `__sys_` from the extra dict parameter.
410
+
411
+ Args:
412
+ extras:
413
+ scope (str):
414
+
415
+ Returns:
416
+ DictData:
417
+ """
418
+ keys: list[str] = [k for k in extras if not k.startswith(f"__sys_{scope}")]
419
+ for k in keys:
420
+ extras.pop(k)
421
+ return extras
@@ -41,6 +41,7 @@ from pydantic.functional_serializers import field_serializer
41
41
  from pydantic.functional_validators import field_validator, model_validator
42
42
  from typing_extensions import Self
43
43
 
44
+ from . import DRYRUN
44
45
  from .__types import DictData
45
46
  from .audits import NORMAL, RERUN, Audit, ReleaseType, get_audit
46
47
  from .conf import YamlParser, dynamic
@@ -66,7 +67,7 @@ from .utils import (
66
67
  extract_id,
67
68
  gen_id,
68
69
  get_dt_now,
69
- remove_sys_extras,
70
+ pop_sys_extras,
70
71
  )
71
72
 
72
73
 
@@ -244,14 +245,12 @@ class Workflow(BaseModel):
244
245
  f"{self.name!r}."
245
246
  )
246
247
 
247
- # NOTE: Force update internal extras for handler circle execution.
248
- self.extras.update({"__sys_break_circle_exec": self.name})
249
-
250
248
  return self
251
249
 
252
250
  @field_serializer("extras")
253
251
  def __serialize_extras(self, extras: DictData) -> DictData:
254
- return remove_sys_extras(extras)
252
+ """Serialize extra parameter."""
253
+ return {k: extras[k] for k in extras if not k.startswith("__sys_")}
255
254
 
256
255
  def detail(self) -> DictData: # pragma: no cov
257
256
  """Return the detail of this workflow for generate markdown."""
@@ -264,8 +263,10 @@ class Workflow(BaseModel):
264
263
  author (str | None, default None): An author name.
265
264
  """
266
265
 
267
- def align_newline(value: str) -> str:
266
+ def align_newline(value: Optional[str]) -> str:
268
267
  space: str = " " * 16
268
+ if value is None:
269
+ return ""
269
270
  return value.rstrip("\n").replace("\n", f"\n{space}")
270
271
 
271
272
  info: str = (
@@ -452,7 +453,17 @@ class Workflow(BaseModel):
452
453
  extras=self.extras,
453
454
  )
454
455
 
455
- if release_type == NORMAL and audit.is_pointed(data=audit_data):
456
+ if release_type == RERUN:
457
+ # TODO: It will load previous audit and use this data to run with
458
+ # the `rerun` method.
459
+ raise NotImplementedError(
460
+ "Release does not support for rerun type yet. Please use the "
461
+ "`rerun` method instead."
462
+ )
463
+ elif release_type == DRYRUN:
464
+ self.extras.update({"__sys_release_dryrun_mode": True})
465
+ trace.debug("[RELEASE]: Mark dryrun mode to the extra params.")
466
+ elif release_type == NORMAL and audit.is_pointed(data=audit_data):
456
467
  trace.info("[RELEASE]: Skip this release because it already audit.")
457
468
  return Result(
458
469
  run_id=run_id,
@@ -462,14 +473,6 @@ class Workflow(BaseModel):
462
473
  extras=self.extras,
463
474
  )
464
475
 
465
- if release_type == RERUN:
466
- # TODO: It will load previous audit and use this data to run with
467
- # the `rerun` method.
468
- raise NotImplementedError(
469
- "Release does not support for rerun type yet. Please use the "
470
- "`rerun` method instead."
471
- )
472
-
473
476
  rs: Result = self.execute(
474
477
  params=values,
475
478
  run_id=parent_run_id,
@@ -478,27 +481,29 @@ class Workflow(BaseModel):
478
481
  catch(context, status=rs.status, updated=rs.context)
479
482
  trace.info(f"[RELEASE]: End {name!r} : {release:%Y-%m-%d %H:%M:%S}")
480
483
  trace.debug(f"[RELEASE]: Writing audit: {name!r}.")
481
- (
482
- audit.save(
483
- data=audit_data
484
- | {
485
- "context": context,
486
- "runs_metadata": (
487
- (runs_metadata or {})
488
- | rs.info
489
- | {
490
- "timeout": timeout,
491
- "original_name": self.name,
492
- "audit_excluded": audit_excluded,
493
- }
494
- ),
495
- },
496
- excluded=audit_excluded,
484
+ if release_type != DRYRUN:
485
+ (
486
+ audit.save(
487
+ data=audit_data
488
+ | {
489
+ "context": context,
490
+ "runs_metadata": (
491
+ (runs_metadata or {})
492
+ | rs.info
493
+ | {
494
+ "timeout": timeout,
495
+ "original_name": self.name,
496
+ "audit_excluded": audit_excluded,
497
+ }
498
+ ),
499
+ },
500
+ excluded=audit_excluded,
501
+ )
497
502
  )
498
- )
499
- return Result(
500
- run_id=run_id,
501
- parent_run_id=parent_run_id,
503
+
504
+ # NOTE: Pop system extra parameters.
505
+ pop_sys_extras(self.extras, scope="release")
506
+ return Result.from_trace(trace).catch(
502
507
  status=rs.status,
503
508
  context=catch(
504
509
  context,
@@ -513,7 +518,6 @@ class Workflow(BaseModel):
513
518
  **(context["errors"] if "errors" in context else {}),
514
519
  },
515
520
  ),
516
- extras=remove_sys_extras(self.extras),
517
521
  )
518
522
 
519
523
  def execute_job(
@@ -719,6 +723,8 @@ class Workflow(BaseModel):
719
723
  extras=self.extras,
720
724
  )
721
725
 
726
+ # NOTE: Force update internal extras for handler circle execution.
727
+ self.extras.update({"__sys_exec_break_circle": self.name})
722
728
  with ThreadPoolExecutor(max_job_parallel, "wf") as executor:
723
729
  futures: list[Future] = []
724
730
 
@@ -747,6 +753,7 @@ class Workflow(BaseModel):
747
753
  backoff_sleep = 0.01
748
754
 
749
755
  if check == FAILED: # pragma: no cov
756
+ pop_sys_extras(self.extras)
750
757
  return Result(
751
758
  run_id=run_id,
752
759
  parent_run_id=parent_run_id,
@@ -841,6 +848,7 @@ class Workflow(BaseModel):
841
848
  for i, s in enumerate(sequence_statuses, start=0):
842
849
  statuses[total + 1 + skip_count + i] = s
843
850
 
851
+ pop_sys_extras(self.extras)
844
852
  st: Status = validate_statuses(statuses)
845
853
  return Result(
846
854
  run_id=run_id,
@@ -862,6 +870,7 @@ class Workflow(BaseModel):
862
870
 
863
871
  time.sleep(0.0025)
864
872
 
873
+ pop_sys_extras(self.extras)
865
874
  return Result(
866
875
  run_id=run_id,
867
876
  parent_run_id=parent_run_id,
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.83
3
+ Version: 0.0.84
4
4
  Summary: Lightweight workflow orchestration with YAML template
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -1,4 +1,4 @@
1
- ddeutil/workflow/__about__.py,sha256=6n_dCde_BwTTJlP4Yu4HNgiRti7QydfShki703lt6Ro,60
1
+ ddeutil/workflow/__about__.py,sha256=zX5wSNry4MbxSK_Ujk9sD5ooaxsvpp2kdHcvlV5NcWY,60
2
2
  ddeutil/workflow/__cron.py,sha256=-1tqZG7GtUmusdl6NTy_Ck7nM_tGYTXYB7TB7tKeO60,29184
3
3
  ddeutil/workflow/__init__.py,sha256=Dvfjs7LpLerGCYGnbqKwznViTw7ire_6LR8obC1I4aM,3456
4
4
  ddeutil/workflow/__main__.py,sha256=Nqk5aO-HsZVKV2BmuJYeJEufJluipvCD9R1k2kMoJ3Y,8581
@@ -11,10 +11,10 @@ ddeutil/workflow/job.py,sha256=VVTpxVR2iVEkjvP8r0O0LRtAPnrbsguYbKzHpe2TAVo,48146
11
11
  ddeutil/workflow/params.py,sha256=y9f6DEIyae1j4awbj3Kbeq75-U2UPFlKv9K57Hdo_Go,17188
12
12
  ddeutil/workflow/result.py,sha256=0W3z5wAs3Dyr8r2vRMY5hl1MkvdsyXWJmQD4NmsDDOM,10194
13
13
  ddeutil/workflow/reusables.py,sha256=SBLJSxR8ELoWJErBfSMZS3Rr1O_93T-fFBpfn2AvxuA,25007
14
- ddeutil/workflow/stages.py,sha256=lWlzvpJ6YyhDf0ks5q_fzHjm4-o6UZfhiYp9CG-ffro,129661
14
+ ddeutil/workflow/stages.py,sha256=zoTtD2w6ZqaAsi-ilH14Wvg_Wx_AJlZqKZCCOzSprj0,139303
15
15
  ddeutil/workflow/traces.py,sha256=pq1lOg2UMgDiSDmjHxXPoTaBHnfc7uzzlo1u2TCwN2Q,74733
16
- ddeutil/workflow/utils.py,sha256=XsH8DkcTiMmWt1e59b4bFQofsBdo7uW1-7gC2rghuW8,12128
17
- ddeutil/workflow/workflow.py,sha256=uc71PJh7e-Bjb3Xg7T83wlLrKTPGvmX7Qjsn6SJ1GDI,42544
16
+ ddeutil/workflow/utils.py,sha256=Hqdh6I3OD4N0Kuehn9qWVOg54pO3BG3rgIacCrN7jqk,12298
17
+ ddeutil/workflow/workflow.py,sha256=1db85LamgwFlYtOJv6ghd3WLREfsP4zQpNOlPXbeuy4,43124
18
18
  ddeutil/workflow/api/__init__.py,sha256=5DzYL3ngceoRshh5HYCSVWChqNJSiP01E1bEd8XxPi0,4799
19
19
  ddeutil/workflow/api/log_conf.py,sha256=WfS3udDLSyrP-C80lWOvxxmhd_XWKvQPkwDqKblcH3E,1834
20
20
  ddeutil/workflow/api/routes/__init__.py,sha256=JRaJZB0D6mgR17MbZo8yLtdYDtD62AA8MdKlFqhG84M,420
@@ -27,9 +27,9 @@ ddeutil/workflow/plugins/providers/aws.py,sha256=61uIFBEWt-_D5Sui24qUPier1Hiqlw_
27
27
  ddeutil/workflow/plugins/providers/az.py,sha256=o3dh011lEtmr7-d7FPZJPgXdT0ytFzKfc5xnVxSyXGU,34867
28
28
  ddeutil/workflow/plugins/providers/container.py,sha256=DSN0RWxMjTJN5ANheeMauDaPa3X6Z2E1eGUcctYkENw,22134
29
29
  ddeutil/workflow/plugins/providers/gcs.py,sha256=KgAOdMBvdbMLTH_z_FwVriBFtZfKEYx8_34jzUOVjTY,27460
30
- ddeutil_workflow-0.0.83.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
31
- ddeutil_workflow-0.0.83.dist-info/METADATA,sha256=pxD6FyTSV4ra5DujoPvWQnb2Z9WWHHqi97HBADqeeAo,16087
32
- ddeutil_workflow-0.0.83.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
- ddeutil_workflow-0.0.83.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
34
- ddeutil_workflow-0.0.83.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
35
- ddeutil_workflow-0.0.83.dist-info/RECORD,,
30
+ ddeutil_workflow-0.0.84.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
31
+ ddeutil_workflow-0.0.84.dist-info/METADATA,sha256=3GFTGHqYEocyCDqwG7-RcaVfrJ61Es9iiC_tP3Aiy04,16087
32
+ ddeutil_workflow-0.0.84.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
33
+ ddeutil_workflow-0.0.84.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
34
+ ddeutil_workflow-0.0.84.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
35
+ ddeutil_workflow-0.0.84.dist-info/RECORD,,