ddeutil-workflow 0.0.63__py3-none-any.whl → 0.0.65__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -21,7 +21,7 @@ from ddeutil.core import str2dict, str2list
21
21
  from pydantic import BaseModel, Field
22
22
 
23
23
  from .__types import StrOrInt
24
- from .exceptions import ParamValueException
24
+ from .errors import ParamError
25
25
  from .utils import get_d_now, get_dt_now
26
26
 
27
27
  T = TypeVar("T")
@@ -101,14 +101,14 @@ class DateParam(DefaultParam): # pragma: no cov
101
101
  elif isinstance(value, date):
102
102
  return value
103
103
  elif not isinstance(value, str):
104
- raise ParamValueException(
104
+ raise ParamError(
105
105
  f"Value that want to convert to date does not support for "
106
106
  f"type: {type(value)}"
107
107
  )
108
108
  try:
109
109
  return date.fromisoformat(value)
110
110
  except ValueError:
111
- raise ParamValueException(
111
+ raise ParamError(
112
112
  f"Invalid the ISO format string for date: {value!r}"
113
113
  ) from None
114
114
 
@@ -143,14 +143,14 @@ class DatetimeParam(DefaultParam):
143
143
  elif isinstance(value, date):
144
144
  return datetime(value.year, value.month, value.day)
145
145
  elif not isinstance(value, str):
146
- raise ParamValueException(
146
+ raise ParamError(
147
147
  f"Value that want to convert to datetime does not support for "
148
148
  f"type: {type(value)}"
149
149
  )
150
150
  try:
151
151
  return datetime.fromisoformat(value)
152
152
  except ValueError:
153
- raise ParamValueException(
153
+ raise ParamError(
154
154
  f"Invalid the ISO format string for datetime: {value!r}"
155
155
  ) from None
156
156
 
@@ -189,7 +189,7 @@ class IntParam(DefaultParam):
189
189
  try:
190
190
  return int(str(value))
191
191
  except ValueError as err:
192
- raise ParamValueException(
192
+ raise ParamError(
193
193
  f"Value can not convert to int, {value}, with base 10"
194
194
  ) from err
195
195
  return value
@@ -299,7 +299,7 @@ class ChoiceParam(BaseParam):
299
299
  if value is None:
300
300
  return self.options[0]
301
301
  if value not in self.options:
302
- raise ParamValueException(
302
+ raise ParamError(
303
303
  f"{value!r} does not match any value in choice options."
304
304
  )
305
305
  return value
@@ -331,12 +331,12 @@ class MapParam(DefaultParam):
331
331
  try:
332
332
  value: dict[Any, Any] = str2dict(value)
333
333
  except ValueError as e:
334
- raise ParamValueException(
334
+ raise ParamError(
335
335
  f"Value that want to convert to map does not support for "
336
336
  f"type: {type(value)}"
337
337
  ) from e
338
338
  elif not isinstance(value, dict):
339
- raise ParamValueException(
339
+ raise ParamError(
340
340
  f"Value of map param support only string-dict or dict type, "
341
341
  f"not {type(value)}"
342
342
  )
@@ -366,14 +366,14 @@ class ArrayParam(DefaultParam):
366
366
  try:
367
367
  value: list[T] = str2list(value)
368
368
  except ValueError as e:
369
- raise ParamValueException(
369
+ raise ParamError(
370
370
  f"Value that want to convert to array does not support for "
371
371
  f"type: {type(value)}"
372
372
  ) from e
373
373
  elif isinstance(value, (tuple, set)):
374
374
  return list(value)
375
375
  elif not isinstance(value, list):
376
- raise ParamValueException(
376
+ raise ParamError(
377
377
  f"Value of map param support only string-list or list type, "
378
378
  f"not {type(value)}"
379
379
  )
@@ -11,7 +11,7 @@ from __future__ import annotations
11
11
 
12
12
  from dataclasses import field
13
13
  from datetime import datetime
14
- from enum import IntEnum
14
+ from enum import IntEnum, auto
15
15
  from typing import Optional, Union
16
16
 
17
17
  from pydantic import ConfigDict
@@ -19,9 +19,20 @@ from pydantic.dataclasses import dataclass
19
19
  from pydantic.functional_validators import model_validator
20
20
  from typing_extensions import Self
21
21
 
22
+ from . import (
23
+ JobCancelError,
24
+ JobError,
25
+ JobSkipError,
26
+ StageCancelError,
27
+ StageError,
28
+ StageSkipError,
29
+ WorkflowCancelError,
30
+ WorkflowError,
31
+ WorkflowSkipError,
32
+ )
22
33
  from .__types import DictData
23
34
  from .conf import dynamic
24
- from .exceptions import ResultException
35
+ from .errors import ResultError
25
36
  from .logs import TraceModel, get_dt_tznow, get_trace
26
37
  from .utils import default_gen_id, gen_id, get_dt_now
27
38
 
@@ -31,11 +42,11 @@ class Status(IntEnum):
31
42
  Result dataclass object.
32
43
  """
33
44
 
34
- SUCCESS = 0
35
- FAILED = 1
36
- WAIT = 2
37
- SKIP = 3
38
- CANCEL = 4
45
+ SUCCESS = auto()
46
+ FAILED = auto()
47
+ WAIT = auto()
48
+ SKIP = auto()
49
+ CANCEL = auto()
39
50
 
40
51
  @property
41
52
  def emoji(self) -> str: # pragma: no cov
@@ -43,7 +54,19 @@ class Status(IntEnum):
43
54
 
44
55
  :rtype: str
45
56
  """
46
- return {0: "✅", 1: "❌", 2: "🟡", 3: "⏩", 4: "🚫"}[self.value]
57
+ return {
58
+ "SUCCESS": "✅",
59
+ "FAILED": "❌",
60
+ "WAIT": "🟡",
61
+ "SKIP": "⏩",
62
+ "CANCEL": "🚫",
63
+ }[self.name]
64
+
65
+ def __repr__(self) -> str:
66
+ return self.name
67
+
68
+ def __str__(self) -> str:
69
+ return self.name
47
70
 
48
71
 
49
72
  SUCCESS = Status.SUCCESS
@@ -53,6 +76,55 @@ SKIP = Status.SKIP
53
76
  CANCEL = Status.CANCEL
54
77
 
55
78
 
79
+ def validate_statuses(statuses: list[Status]) -> Status:
80
+ """Validate the final status from list of Status object.
81
+
82
+ :param statuses: (list[Status]) A list of status that want to validate the
83
+ final status.
84
+
85
+ :rtype: Status
86
+ """
87
+ if any(s == CANCEL for s in statuses):
88
+ return CANCEL
89
+ elif any(s == FAILED for s in statuses):
90
+ return FAILED
91
+ elif any(s == WAIT for s in statuses):
92
+ return WAIT
93
+ for status in (SUCCESS, SKIP):
94
+ if all(s == status for s in statuses):
95
+ return status
96
+ return FAILED if FAILED in statuses else SUCCESS
97
+
98
+
99
+ def get_status_from_error(
100
+ error: Union[
101
+ StageError,
102
+ StageCancelError,
103
+ StageSkipError,
104
+ JobError,
105
+ JobCancelError,
106
+ JobSkipError,
107
+ WorkflowError,
108
+ WorkflowCancelError,
109
+ WorkflowSkipError,
110
+ Exception,
111
+ BaseException,
112
+ ]
113
+ ) -> Status:
114
+ """Get the Status from the error object."""
115
+ if isinstance(error, (StageSkipError, JobSkipError, WorkflowSkipError)):
116
+ return SKIP
117
+ elif isinstance(
118
+ error, (StageCancelError, JobCancelError, WorkflowCancelError)
119
+ ):
120
+ return CANCEL
121
+ return FAILED
122
+
123
+
124
+ def default_context() -> DictData:
125
+ return {"status": WAIT}
126
+
127
+
56
128
  @dataclass(
57
129
  config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True),
58
130
  )
@@ -70,7 +142,7 @@ class Result:
70
142
  """
71
143
 
72
144
  status: Status = field(default=WAIT)
73
- context: DictData = field(default_factory=dict)
145
+ context: DictData = field(default_factory=default_context)
74
146
  run_id: Optional[str] = field(default_factory=default_gen_id)
75
147
  parent_run_id: Optional[str] = field(default=None, compare=False)
76
148
  ts: datetime = field(default_factory=get_dt_tznow, compare=False)
@@ -160,12 +232,16 @@ class Result:
160
232
  Status(status) if isinstance(status, int) else status
161
233
  )
162
234
  self.__dict__["context"].update(context or {})
235
+ self.__dict__["context"]["status"] = self.status
163
236
  if kwargs:
164
237
  for k in kwargs:
165
238
  if k in self.__dict__["context"]:
166
239
  self.__dict__["context"][k].update(kwargs[k])
240
+ # NOTE: Exclude the `info` key for update information data.
241
+ elif k == "info":
242
+ self.__dict__["context"][k].update(kwargs[k])
167
243
  else:
168
- raise ResultException(
244
+ raise ResultError(
169
245
  f"The key {k!r} does not exists on context data."
170
246
  )
171
247
  return self
@@ -15,6 +15,7 @@ from datetime import datetime
15
15
  from functools import wraps
16
16
  from importlib import import_module
17
17
  from typing import (
18
+ Annotated,
18
19
  Any,
19
20
  Callable,
20
21
  Literal,
@@ -32,12 +33,13 @@ except ImportError:
32
33
 
33
34
  from ddeutil.core import getdot, import_string, lazy
34
35
  from ddeutil.io import search_env_replace
35
- from pydantic import BaseModel, create_model
36
+ from pydantic import BaseModel, ConfigDict, Field, create_model
37
+ from pydantic.alias_generators import to_pascal
36
38
  from pydantic.dataclasses import dataclass
37
39
 
38
40
  from .__types import DictData, Re
39
41
  from .conf import dynamic
40
- from .exceptions import UtilException
42
+ from .errors import UtilError
41
43
 
42
44
  T = TypeVar("T")
43
45
  P = ParamSpec("P")
@@ -121,7 +123,7 @@ def make_filter_registry(
121
123
  if not (
122
124
  hasattr(func, "filter")
123
125
  and str(getattr(func, "mark", "NOT SET")) == "filter"
124
- ):
126
+ ): # pragma: no cov
125
127
  continue
126
128
 
127
129
  func: FilterFunc
@@ -144,13 +146,13 @@ def get_args_const(
144
146
  try:
145
147
  mod: Module = parse(expr)
146
148
  except SyntaxError:
147
- raise UtilException(
149
+ raise UtilError(
148
150
  f"Post-filter: {expr} does not valid because it raise syntax error."
149
151
  ) from None
150
152
 
151
153
  body: list[Expr] = mod.body
152
154
  if len(body) > 1:
153
- raise UtilException(
155
+ raise UtilError(
154
156
  "Post-filter function should be only one calling per workflow."
155
157
  )
156
158
 
@@ -158,7 +160,7 @@ def get_args_const(
158
160
  if isinstance((caller := body[0].value), Name):
159
161
  return caller.id, [], {}
160
162
  elif not isinstance(caller, Call):
161
- raise UtilException(
163
+ raise UtilError(
162
164
  f"Get arguments does not support for caller type: {type(caller)}"
163
165
  )
164
166
 
@@ -167,10 +169,10 @@ def get_args_const(
167
169
  keywords: dict[str, Constant] = {k.arg: k.value for k in caller.keywords}
168
170
 
169
171
  if any(not isinstance(i, Constant) for i in args):
170
- raise UtilException(f"Argument of {expr} should be constant.")
172
+ raise UtilError(f"Argument of {expr} should be constant.")
171
173
 
172
174
  if any(not isinstance(i, Constant) for i in keywords.values()):
173
- raise UtilException(f"Keyword argument of {expr} should be constant.")
175
+ raise UtilError(f"Keyword argument of {expr} should be constant.")
174
176
 
175
177
  return name.id, args, keywords
176
178
 
@@ -192,12 +194,10 @@ def get_args_from_filter(
192
194
  kwargs: dict[Any, Any] = {k: v.value for k, v in _kwargs.items()}
193
195
 
194
196
  if func_name not in filters:
195
- raise UtilException(
196
- f"The post-filter: {func_name!r} does not support yet."
197
- )
197
+ raise UtilError(f"The post-filter: {func_name!r} does not support yet.")
198
198
 
199
199
  if isinstance((f_func := filters[func_name]), list) and (args or kwargs):
200
- raise UtilException(
200
+ raise UtilError(
201
201
  "Chain filter function does not support for passing arguments."
202
202
  )
203
203
 
@@ -226,10 +226,10 @@ def map_post_filter(
226
226
  value: T = func(value)
227
227
  else:
228
228
  value: T = f_func(value, *args, **kwargs)
229
- except UtilException:
229
+ except UtilError:
230
230
  raise
231
231
  except Exception:
232
- raise UtilException(
232
+ raise UtilError(
233
233
  f"The post-filter: {func_name!r} does not fit with {value!r} "
234
234
  f"(type: {type(value).__name__})."
235
235
  ) from None
@@ -320,7 +320,7 @@ def str2template(
320
320
  try:
321
321
  getter: Any = getdot(caller, params)
322
322
  except ValueError:
323
- raise UtilException(
323
+ raise UtilError(
324
324
  f"Parameters does not get dot with caller: {caller!r}."
325
325
  ) from None
326
326
 
@@ -402,7 +402,7 @@ def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
402
402
  """
403
403
  if isinstance(value, datetime):
404
404
  return value.strftime(fmt)
405
- raise UtilException(
405
+ raise UtilError(
406
406
  "This custom function should pass input value with datetime type."
407
407
  )
408
408
 
@@ -435,7 +435,7 @@ def get_item(
435
435
 
436
436
  """
437
437
  if not isinstance(value, dict):
438
- raise UtilException(
438
+ raise UtilError(
439
439
  f"The value that pass to `getitem` filter should be `dict` not "
440
440
  f"`{type(value)}`."
441
441
  )
@@ -452,14 +452,14 @@ def get_index(value: list[Any], index: int) -> Any:
452
452
 
453
453
  """
454
454
  if not isinstance(value, list):
455
- raise UtilException(
455
+ raise UtilError(
456
456
  f"The value that pass to `getindex` filter should be `list` not "
457
457
  f"`{type(value)}`."
458
458
  )
459
459
  try:
460
460
  return value[index]
461
461
  except IndexError as e:
462
- raise UtilException(
462
+ raise UtilError(
463
463
  f"Index: {index} is out of range of value (The maximum range is "
464
464
  f"{len(value)})."
465
465
  ) from e
@@ -635,12 +635,25 @@ def extract_call(
635
635
  return rgt[call.func][call.tag]
636
636
 
637
637
 
638
+ class BaseCallerArgs(BaseModel): # pragma: no cov
639
+ """Base Caller Args model."""
640
+
641
+ model_config = ConfigDict(
642
+ arbitrary_types_allowed=True,
643
+ use_enum_values=True,
644
+ )
645
+
646
+
638
647
  def create_model_from_caller(func: Callable) -> BaseModel: # pragma: no cov
639
648
  """Create model from the caller function. This function will use for
640
649
  validate the caller function argument typed-hint that valid with the args
641
650
  field.
642
651
 
643
- :param func: A caller function.
652
+ Reference:
653
+ - https://github.com/lmmx/pydantic-function-models
654
+ - https://docs.pydantic.dev/1.10/usage/models/#dynamic-model-creation
655
+
656
+ :param func: (Callable) A caller function.
644
657
 
645
658
  :rtype: BaseModel
646
659
  """
@@ -649,16 +662,34 @@ def create_model_from_caller(func: Callable) -> BaseModel: # pragma: no cov
649
662
  fields: dict[str, Any] = {}
650
663
  for name in sig.parameters:
651
664
  param: inspect.Parameter = sig.parameters[name]
665
+
666
+ # NOTE: Skip all `*args` and `**kwargs` parameters.
652
667
  if param.kind in (
653
668
  inspect.Parameter.VAR_KEYWORD,
654
669
  inspect.Parameter.VAR_POSITIONAL,
655
670
  ):
656
671
  continue
672
+
673
+ if name.startswith("_"):
674
+ kwargs = {"serialization_alias": name}
675
+ rename: str = name.removeprefix("_")
676
+ else:
677
+ kwargs = {}
678
+ rename: str = name
679
+
657
680
  if param.default != inspect.Parameter.empty:
658
- fields[name] = (type_hints[name], param.default)
681
+ fields[rename] = Annotated[
682
+ type_hints[name],
683
+ Field(default=param.default, **kwargs),
684
+ ]
659
685
  else:
660
- fields[name] = (type_hints[name], ...)
686
+ fields[rename] = Annotated[
687
+ type_hints[name],
688
+ Field(..., **kwargs),
689
+ ]
661
690
 
662
691
  return create_model(
663
- "".join(i.title() for i in func.__name__.split("_")), **fields
692
+ to_pascal(func.__name__),
693
+ __base__=BaseCallerArgs,
694
+ **fields,
664
695
  )