ddeutil-workflow 0.0.85__py3-none-any.whl → 0.0.86__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.85"
1
+ __version__: str = "0.0.86"
2
2
  __python_version__: str = "3.9"
@@ -10,6 +10,7 @@ from typing import Any, Optional
10
10
 
11
11
  from fastapi import APIRouter, Body
12
12
  from fastapi import status as st
13
+ from fastapi.encoders import jsonable_encoder
13
14
  from fastapi.responses import UJSONResponse
14
15
 
15
16
  from ...__types import DictData
@@ -66,7 +67,7 @@ async def job_execute(
66
67
  exclude_unset=True,
67
68
  ),
68
69
  "params": params,
69
- "context": context,
70
+ "context": jsonable_encoder(context),
70
71
  },
71
72
  status_code=st.HTTP_500_INTERNAL_SERVER_ERROR,
72
73
  )
@@ -81,7 +82,7 @@ async def job_execute(
81
82
  exclude_unset=True,
82
83
  ),
83
84
  "params": params,
84
- "context": context,
85
+ "context": jsonable_encoder(context),
85
86
  },
86
87
  status_code=st.HTTP_200_OK,
87
88
  )
ddeutil/workflow/conf.py CHANGED
@@ -117,7 +117,7 @@ class Config: # pragma: no cov
117
117
  def debug(self) -> bool:
118
118
  """Debug flag for echo log that use DEBUG mode.
119
119
 
120
- :rtype: bool
120
+ Returns: bool
121
121
  """
122
122
  return str2bool(env("LOG_DEBUG_MODE", "true"))
123
123
 
@@ -472,9 +472,11 @@ def dynamic(
472
472
  def pass_env(value: T) -> T: # pragma: no cov
473
473
  """Passing environment variable to an input value.
474
474
 
475
- :param value: (Any) A value that want to pass env var searching.
475
+ Args:
476
+ value (Any): A value that want to pass env var searching.
476
477
 
477
- :rtype: Any
478
+ Returns:
479
+ Any: An any value that have passed environment variable.
478
480
  """
479
481
  if isinstance(value, dict):
480
482
  return {k: pass_env(value[k]) for k in value}
@@ -208,4 +208,7 @@ class WorkflowCancelError(WorkflowError): ...
208
208
  class WorkflowTimeoutError(WorkflowError): ...
209
209
 
210
210
 
211
+ class WorkflowSkipError(WorkflowError): ...
212
+
213
+
211
214
  class ParamError(WorkflowError): ...
ddeutil/workflow/job.py CHANGED
@@ -75,7 +75,7 @@ from .result import (
75
75
  from .reusables import has_template, param2template
76
76
  from .stages import Stage
77
77
  from .traces import Trace, get_trace
78
- from .utils import cross_product, extract_id, filter_func, gen_id
78
+ from .utils import cross_product, extract_id, filter_func, gen_id, get_dt_now
79
79
 
80
80
  MatrixFilter = list[dict[str, Union[str, int]]]
81
81
 
@@ -824,9 +824,14 @@ class Job(BaseModel):
824
824
  status: dict[str, Status] = (
825
825
  {"status": output.pop("status")} if "status" in output else {}
826
826
  )
827
+ info: DictData = (
828
+ {"info": output.pop("info")} if "info" in output else {}
829
+ )
827
830
  kwargs: DictData = kwargs or {}
828
831
  if self.strategy.is_set():
829
- to["jobs"][_id] = {"strategies": output} | errors | status | kwargs
832
+ to["jobs"][_id] = (
833
+ {"strategies": output} | errors | status | info | kwargs
834
+ )
830
835
  elif len(k := output.keys()) > 1: # pragma: no cov
831
836
  raise JobError(
832
837
  "Strategy output from execution return more than one ID while "
@@ -835,7 +840,7 @@ class Job(BaseModel):
835
840
  else:
836
841
  _output: DictData = {} if len(k) == 0 else output[list(k)[0]]
837
842
  _output.pop("matrix", {})
838
- to["jobs"][_id] = _output | errors | status | kwargs
843
+ to["jobs"][_id] = _output | errors | status | info | kwargs
839
844
  return to
840
845
 
841
846
  def get_outputs(
@@ -851,7 +856,8 @@ class Job(BaseModel):
851
856
  output (DictData): A job outputs data that want to extract
852
857
  job_id (StrOrNone): A job ID if the `id` field does not set.
853
858
 
854
- :rtype: DictData
859
+ Returns:
860
+ DictData: An output data.
855
861
  """
856
862
  _id: str = self.id or job_id
857
863
  if self.strategy.is_set():
@@ -878,17 +884,31 @@ class Job(BaseModel):
878
884
  params: DictData,
879
885
  run_id: str,
880
886
  context: DictData,
887
+ *,
881
888
  parent_run_id: Optional[str] = None,
882
889
  event: Optional[Event] = None,
883
890
  ) -> Result:
884
891
  """Process routing method that will route the provider function depend
885
892
  on runs-on value.
893
+
894
+ Args:
895
+ params (DictData): A parameter data that want to use in this
896
+ execution.
897
+ run_id (str): A running stage ID.
898
+ context (DictData): A context data that was passed from handler
899
+ method.
900
+ parent_run_id (str, default None): A parent running ID.
901
+ event (Event, default None): An event manager that use to track
902
+ parent process was not force stopped.
903
+
904
+ Returns:
905
+ Result: The execution result with status and context data.
886
906
  """
887
907
  trace: Trace = get_trace(
888
908
  run_id, parent_run_id=parent_run_id, extras=self.extras
889
909
  )
890
910
  trace.info(
891
- f"[JOB]: Routing for "
911
+ f"[JOB]: Routing "
892
912
  f"{''.join(self.runs_on.type.value.split('_')).title()}: "
893
913
  f"{self.id!r}"
894
914
  )
@@ -946,6 +966,7 @@ class Job(BaseModel):
946
966
  run_id=parent_run_id,
947
967
  event=event,
948
968
  )
969
+
949
970
  if rs is None:
950
971
  trace.error(
951
972
  f"[JOB]: Execution not support runs-on: {self.runs_on.type.value!r} "
@@ -964,20 +985,20 @@ class Job(BaseModel):
964
985
  },
965
986
  extras=self.extras,
966
987
  )
967
- if rs.status in (CANCEL, SKIP):
968
- trace.debug(
969
- f"[JOB]: Job process routing got result status be {rs.status}"
970
- )
988
+
989
+ if rs.status == SKIP:
990
+ raise JobSkipError("Job got skipped status.")
991
+ elif rs.status == CANCEL:
992
+ raise JobCancelError("Job got canceled status.")
971
993
  elif rs.status == FAILED:
972
- raise JobError("[JOB]: Job process error")
994
+ raise JobError("Job process error")
973
995
  return rs
974
996
 
975
997
  def _execute(
976
998
  self,
977
999
  params: DictData,
978
- run_id: str,
979
1000
  context: DictData,
980
- parent_run_id: Optional[str] = None,
1001
+ trace: Trace,
981
1002
  event: Optional[Event] = None,
982
1003
  ) -> Result:
983
1004
  """Wrapped the route execute method before returning to handler
@@ -988,45 +1009,40 @@ class Job(BaseModel):
988
1009
 
989
1010
  Args:
990
1011
  params: A parameter data that want to use in this execution
991
- run_id:
992
1012
  context:
993
- parent_run_id:
994
- event:
1013
+ trace (Trace):
1014
+ event (Event, default None):
995
1015
 
996
1016
  Returns:
997
1017
  Result: The wrapped execution result.
998
1018
  """
999
1019
  current_retry: int = 0
1020
+ maximum_retry: int = self.retry + 1
1000
1021
  exception: Exception
1001
1022
  catch(context, status=WAIT)
1002
- trace: Trace = get_trace(
1003
- run_id, parent_run_id=parent_run_id, extras=self.extras
1004
- )
1005
1023
  try:
1006
1024
  return self.process(
1007
1025
  params,
1008
- run_id,
1026
+ run_id=trace.run_id,
1009
1027
  context=context,
1010
- parent_run_id=parent_run_id,
1028
+ parent_run_id=trace.parent_run_id,
1011
1029
  event=event,
1012
1030
  )
1013
1031
  except (JobCancelError, JobSkipError):
1014
1032
  trace.debug("[JOB]: process raise skip or cancel error.")
1015
1033
  raise
1016
1034
  except Exception as e:
1035
+ if self.retry == 0:
1036
+ raise
1037
+
1017
1038
  current_retry += 1
1018
1039
  exception = e
1019
- finally:
1020
- trace.debug("[JOB]: Failed at the first execution.")
1021
-
1022
- if self.retry == 0:
1023
- raise exception
1024
1040
 
1025
1041
  trace.warning(
1026
- f"[JOB]: Retry count: {current_retry} ... "
1042
+ f"[JOB]: Retry count: {current_retry}/{maximum_retry} ... "
1027
1043
  f"( {exception.__class__.__name__} )"
1028
1044
  )
1029
- while current_retry < (self.retry + 1):
1045
+ while current_retry < maximum_retry:
1030
1046
  try:
1031
1047
  catch(
1032
1048
  context=context,
@@ -1035,18 +1051,18 @@ class Job(BaseModel):
1035
1051
  )
1036
1052
  return self.process(
1037
1053
  params,
1038
- run_id,
1054
+ run_id=trace.run_id,
1039
1055
  context=context,
1040
- parent_run_id=parent_run_id,
1056
+ parent_run_id=trace.parent_run_id,
1041
1057
  event=event,
1042
1058
  )
1043
1059
  except (JobCancelError, JobSkipError):
1044
1060
  trace.debug("[JOB]: process raise skip or cancel error.")
1045
1061
  raise
1046
- except exception as e:
1062
+ except Exception as e:
1047
1063
  current_retry += 1
1048
1064
  trace.warning(
1049
- f"[JOB]: Retry count: {current_retry} ... "
1065
+ f"[JOB]: Retry count: {current_retry}/{maximum_retry} ... "
1050
1066
  f"( {e.__class__.__name__} )"
1051
1067
  )
1052
1068
  exception = e
@@ -1083,32 +1099,40 @@ class Job(BaseModel):
1083
1099
  parent_run_id, run_id = extract_id(
1084
1100
  (self.id or "EMPTY"), run_id=run_id, extras=self.extras
1085
1101
  )
1102
+ context: DictData = {
1103
+ "status": WAIT,
1104
+ "info": {"exec_start": get_dt_now()},
1105
+ }
1086
1106
  trace: Trace = get_trace(
1087
1107
  run_id, parent_run_id=parent_run_id, extras=self.extras
1088
1108
  )
1089
- context: DictData = {"status": WAIT}
1090
1109
  try:
1091
1110
  trace.info(
1092
1111
  f"[JOB]: Handler {self.runs_on.type.name}: "
1093
1112
  f"{(self.id or 'EMPTY')!r}."
1094
1113
  )
1095
- result_caught: Result = self._execute(
1114
+ result: Result = self._execute(
1096
1115
  params,
1097
- run_id=run_id,
1098
1116
  context=context,
1099
- parent_run_id=parent_run_id,
1117
+ trace=trace,
1100
1118
  event=event,
1101
1119
  )
1102
- return result_caught.make_info(
1103
- {"execution_time": time.monotonic() - ts}
1104
- )
1105
- except JobError: # pragma: no cov
1120
+ return result
1121
+ except JobError as e: # pragma: no cov
1122
+ if isinstance(e, JobSkipError):
1123
+ trace.error(f"[JOB]: ⏭️ Skip: {e}")
1124
+
1125
+ st: Status = get_status_from_error(e)
1106
1126
  return Result.from_trace(trace).catch(
1107
- status=FAILED,
1108
- context=catch(context, status=FAILED),
1109
- info={"execution_time": time.monotonic() - ts},
1127
+ status=st, context=catch(context, status=st)
1110
1128
  )
1111
1129
  finally:
1130
+ context["info"].update(
1131
+ {
1132
+ "exec_end": get_dt_now(),
1133
+ "exec_latency": round(time.monotonic() - ts, 6),
1134
+ }
1135
+ )
1112
1136
  trace.debug("[JOB]: End Handler job execution.")
1113
1137
 
1114
1138
 
@@ -12,30 +12,29 @@ states and the Result dataclass for context transfer between workflow components
12
12
  from __future__ import annotations
13
13
 
14
14
  from dataclasses import field
15
+ from datetime import datetime
15
16
  from enum import Enum
16
17
  from typing import Any, Optional, TypedDict, Union
17
18
 
18
19
  from pydantic import ConfigDict
19
20
  from pydantic.dataclasses import dataclass
20
- from pydantic.functional_validators import model_validator
21
21
  from typing_extensions import NotRequired, Self
22
22
 
23
- from . import (
23
+ from .__types import DictData
24
+ from .errors import (
25
+ BaseError,
26
+ ErrorData,
24
27
  JobCancelError,
25
- JobError,
26
28
  JobSkipError,
29
+ ResultError,
27
30
  StageCancelError,
28
- StageError,
29
31
  StageNestedCancelError,
30
- StageNestedError,
31
32
  StageNestedSkipError,
32
33
  StageSkipError,
33
34
  WorkflowCancelError,
34
- WorkflowError,
35
+ WorkflowSkipError,
35
36
  )
36
- from .__types import DictData
37
- from .audits import Trace, get_trace
38
- from .errors import ErrorData, ResultError
37
+ from .traces import Trace, get_trace
39
38
  from .utils import default_gen_id
40
39
 
41
40
 
@@ -110,11 +109,11 @@ def validate_statuses(statuses: list[Status]) -> Status:
110
109
  Status: Final consolidated status based on workflow logic
111
110
 
112
111
  Example:
113
- >>> # Mixed statuses - FAILED takes priority
112
+ Case: Mixed statuses - FAILED takes priority
114
113
  >>> validate_statuses([SUCCESS, FAILED, SUCCESS])
115
114
  >>> # Returns: FAILED
116
115
 
117
- >>> # All same status
116
+ Case: All same status
118
117
  >>> validate_statuses([SUCCESS, SUCCESS, SUCCESS])
119
118
  >>> # Returns: SUCCESS
120
119
  """
@@ -131,21 +130,7 @@ def validate_statuses(statuses: list[Status]) -> Status:
131
130
 
132
131
 
133
132
  def get_status_from_error(
134
- error: Union[
135
- StageError,
136
- StageCancelError,
137
- StageSkipError,
138
- StageNestedCancelError,
139
- StageNestedError,
140
- StageNestedSkipError,
141
- JobError,
142
- JobCancelError,
143
- JobSkipError,
144
- WorkflowError,
145
- WorkflowCancelError,
146
- Exception,
147
- BaseException,
148
- ]
133
+ error: Union[BaseError, Exception, BaseException]
149
134
  ) -> Status:
150
135
  """Get the Status from the error object.
151
136
 
@@ -155,7 +140,10 @@ def get_status_from_error(
155
140
  Returns:
156
141
  Status: The status from the specific exception class.
157
142
  """
158
- if isinstance(error, (StageNestedSkipError, StageSkipError, JobSkipError)):
143
+ if isinstance(
144
+ error,
145
+ (StageNestedSkipError, StageSkipError, JobSkipError, WorkflowSkipError),
146
+ ):
159
147
  return SKIP
160
148
  elif isinstance(
161
149
  error,
@@ -189,25 +177,8 @@ class Result:
189
177
  extras: DictData = field(default_factory=dict, compare=False, repr=False)
190
178
  status: Status = field(default=WAIT)
191
179
  context: Optional[DictData] = field(default=None)
192
- info: DictData = field(default_factory=dict)
193
180
  run_id: str = field(default_factory=default_gen_id)
194
181
  parent_run_id: Optional[str] = field(default=None)
195
- trace: Optional[Trace] = field(default=None, compare=False, repr=False)
196
-
197
- @model_validator(mode="after")
198
- def __prepare_trace(self) -> Self:
199
- """Prepare trace field that want to pass after its initialize step.
200
-
201
- :rtype: Self
202
- """
203
- if self.trace is None: # pragma: no cov
204
- self.trace: Trace = get_trace(
205
- self.run_id,
206
- parent_run_id=self.parent_run_id,
207
- extras=self.extras,
208
- )
209
-
210
- return self
211
182
 
212
183
  @classmethod
213
184
  def from_trace(cls, trace: Trace):
@@ -216,7 +187,13 @@ class Result:
216
187
  run_id=trace.run_id,
217
188
  parent_run_id=trace.parent_run_id,
218
189
  extras=trace.extras,
219
- trace=trace,
190
+ )
191
+
192
+ def gen_trace(self) -> Trace:
193
+ return get_trace(
194
+ self.run_id,
195
+ parent_run_id=self.parent_run_id,
196
+ extras=self.extras,
220
197
  )
221
198
 
222
199
  def catch(
@@ -251,18 +228,16 @@ class Result:
251
228
  self.__dict__["context"][k].update(kwargs[k])
252
229
  # NOTE: Exclude the `info` key for update information data.
253
230
  elif k == "info":
254
- self.__dict__["info"].update(kwargs["info"])
231
+ if "info" in self.__dict__["context"]:
232
+ self.__dict__["context"].update(kwargs[k])
233
+ else:
234
+ self.__dict__["context"]["info"] = kwargs[k]
255
235
  else:
256
236
  raise ResultError(
257
237
  f"The key {k!r} does not exists on context data."
258
238
  )
259
239
  return self
260
240
 
261
- def make_info(self, data: DictData) -> Self:
262
- """Making information."""
263
- self.__dict__["info"].update(data)
264
- return self
265
-
266
241
 
267
242
  def catch(
268
243
  context: DictData,
@@ -292,19 +267,35 @@ def catch(
292
267
  context[k].update(kwargs[k])
293
268
  # NOTE: Exclude the `info` key for update information data.
294
269
  elif k == "info":
295
- context.update({"info": kwargs["info"]})
296
- else:
297
- raise ResultError(f"The key {k!r} does not exists on context data.")
270
+ if "info" in context:
271
+ context.update(kwargs[k])
272
+ else:
273
+ context["info"] = kwargs[k]
274
+ # ARCHIVE:
275
+ # else:
276
+ # raise ResultError(f"The key {k!r} does not exist on context data.")
298
277
  return context
299
278
 
300
279
 
280
+ class Info(TypedDict):
281
+ exec_start: datetime
282
+ exec_end: NotRequired[datetime]
283
+ exec_latency: NotRequired[float]
284
+
285
+
286
+ class System(TypedDict):
287
+ __sys_release_dryrun_mode: NotRequired[bool]
288
+ __sys_exec_break_circle: NotRequired[str]
289
+
290
+
301
291
  class Context(TypedDict):
302
292
  """Context dict typed."""
303
293
 
304
294
  status: Status
295
+ info: Info
296
+ sys: NotRequired[System]
305
297
  context: NotRequired[DictData]
306
298
  errors: NotRequired[Union[list[ErrorData], ErrorData]]
307
- info: NotRequired[DictData]
308
299
 
309
300
 
310
301
  class Layer(str, Enum):