ddeutil-workflow 0.0.60__tar.gz → 0.0.62__tar.gz

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.
Files changed (68) hide show
  1. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/PKG-INFO +1 -1
  2. ddeutil_workflow-0.0.62/src/ddeutil/workflow/__about__.py +1 -0
  3. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/__cron.py +24 -25
  4. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/event.py +23 -0
  5. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/logs.py +12 -11
  6. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/params.py +54 -21
  7. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/reusables.py +67 -13
  8. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/stages.py +26 -7
  9. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/utils.py +23 -1
  10. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/workflow.py +203 -207
  11. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/PKG-INFO +1 -1
  12. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test__cron.py +36 -28
  13. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_logs_trace.py +13 -0
  14. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_params.py +9 -0
  15. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_reusables_call_tag.py +2 -1
  16. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_reusables_template.py +76 -0
  17. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_reusables_template_filter.py +18 -22
  18. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_stage_handler_exec.py +18 -5
  19. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_utils.py +48 -0
  20. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_exec.py +4 -2
  21. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_poke.py +9 -9
  22. ddeutil_workflow-0.0.60/src/ddeutil/workflow/__about__.py +0 -1
  23. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/LICENSE +0 -0
  24. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/README.md +0 -0
  25. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/pyproject.toml +0 -0
  26. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/setup.cfg +0 -0
  27. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/__init__.py +0 -0
  28. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/__main__.py +0 -0
  29. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/__types.py +0 -0
  30. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/__init__.py +0 -0
  31. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/logs.py +0 -0
  32. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
  33. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/job.py +0 -0
  34. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/logs.py +0 -0
  35. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
  36. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
  37. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/utils.py +0 -0
  38. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/conf.py +0 -0
  39. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/exceptions.py +0 -0
  40. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/job.py +0 -0
  41. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/result.py +0 -0
  42. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/scheduler.py +0 -0
  43. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
  44. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
  45. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
  46. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
  47. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
  48. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test__regex.py +0 -0
  49. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_conf.py +0 -0
  50. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_event.py +0 -0
  51. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_job.py +0 -0
  52. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_job_exec.py +0 -0
  53. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_job_exec_strategy.py +0 -0
  54. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_logs_audit.py +0 -0
  55. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_release.py +0 -0
  56. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_release_queue.py +0 -0
  57. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_result.py +0 -0
  58. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_schedule.py +0 -0
  59. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_schedule_pending.py +0 -0
  60. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_schedule_tasks.py +0 -0
  61. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_schedule_workflow.py +0 -0
  62. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_scheduler_control.py +0 -0
  63. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_stage.py +0 -0
  64. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_strategy.py +0 -0
  65. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow.py +0 -0
  66. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_exec_job.py +0 -0
  67. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_release.py +0 -0
  68. {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_task.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddeutil-workflow
3
- Version: 0.0.60
3
+ Version: 0.0.62
4
4
  Summary: Lightweight workflow orchestration
5
5
  Author-email: ddeutils <korawich.anu@gmail.com>
6
6
  License: MIT
@@ -0,0 +1 @@
1
+ __version__: str = "0.0.62"
@@ -18,7 +18,7 @@ from ddeutil.core import (
18
18
  isinstance_check,
19
19
  must_split,
20
20
  )
21
- from ddeutil.core.dtutils import next_date, replace_date
21
+ from ddeutil.core.dtutils import DatetimeMode, next_date, replace_date
22
22
 
23
23
  WEEKDAYS: dict[str, int] = {
24
24
  "Sun": 0,
@@ -31,7 +31,8 @@ WEEKDAYS: dict[str, int] = {
31
31
  }
32
32
 
33
33
 
34
- class CronYearLimit(Exception): ...
34
+ class YearReachLimit(Exception):
35
+ """"""
35
36
 
36
37
 
37
38
  def str2cron(value: str) -> str: # pragma: no cov
@@ -178,7 +179,7 @@ class CronPart:
178
179
  def __init__(
179
180
  self,
180
181
  unit: Unit,
181
- values: str | list[int],
182
+ values: Union[str, list[int]],
182
183
  options: Options,
183
184
  ) -> None:
184
185
  self.unit: Unit = unit
@@ -229,19 +230,21 @@ class CronPart:
229
230
  f"(unit={self.unit}, values={self.__str__()!r})"
230
231
  )
231
232
 
232
- def __lt__(self, other) -> bool:
233
+ def __lt__(self, other: Union[CronPart, list]) -> bool:
233
234
  """Override __lt__ method."""
234
235
  if isinstance(other, CronPart):
235
236
  return self.values < other.values
236
237
  elif isinstance(other, list):
237
238
  return self.values < other
239
+ return NotImplemented
238
240
 
239
- def __eq__(self, other) -> bool:
241
+ def __eq__(self, other: Union[CronPart, list]) -> bool:
240
242
  """Override __eq__ method."""
241
243
  if isinstance(other, CronPart):
242
244
  return self.values == other.values
243
245
  elif isinstance(other, list):
244
246
  return self.values == other
247
+ return NotImplemented
245
248
 
246
249
  @property
247
250
  def min(self) -> int:
@@ -271,6 +274,7 @@ class CronPart:
271
274
  and (step := self.values[1] - self.values[0]) > 1
272
275
  ):
273
276
  return step
277
+ return None
274
278
 
275
279
  @property
276
280
  def is_full(self) -> bool:
@@ -355,6 +359,8 @@ class CronPart:
355
359
  f"Invalid interval step value {value_step!r} for "
356
360
  f"{self.unit.name!r}"
357
361
  )
362
+ elif value_step:
363
+ value_step: int = int(value_step)
358
364
 
359
365
  # NOTE: Generate interval that has step
360
366
  interval_list.append(self._interval(value_range_list, value_step))
@@ -375,7 +381,9 @@ class CronPart:
375
381
  value: str = value.replace(alt, str(self.unit.min + i))
376
382
  return value
377
383
 
378
- def replace_weekday(self, values: list[int] | Iterator[int]) -> list[int]:
384
+ def replace_weekday(
385
+ self, values: Union[list[int], Iterator[int]]
386
+ ) -> list[int]:
379
387
  """Replaces all 7 with 0 as Sunday can be represented by both.
380
388
 
381
389
  :param values: list or iter of int that want to mode by 7
@@ -433,12 +441,12 @@ class CronPart:
433
441
  def _interval(
434
442
  self,
435
443
  values: list[int],
436
- step: int | None = None,
444
+ step: Optional[int] = None,
437
445
  ) -> list[int]:
438
446
  """Applies an interval step to a collection of values.
439
447
 
440
448
  :param values:
441
- :param step:
449
+ :param step: (int) A step
442
450
 
443
451
  :rtype: list[int]
444
452
  """
@@ -515,7 +523,7 @@ class CronPart:
515
523
  start_number: Optional[int] = value
516
524
  return multi_dim_values
517
525
 
518
- def filler(self, value: int) -> int | str:
526
+ def filler(self, value: int) -> Union[int, str]:
519
527
  """Formats weekday and month names as string when the relevant options
520
528
  are set.
521
529
 
@@ -765,12 +773,12 @@ class CronRunner:
765
773
 
766
774
  def __init__(
767
775
  self,
768
- cron: CronJob | CronJobYear,
776
+ cron: Union[CronJob, CronJobYear],
769
777
  date: Optional[datetime] = None,
770
778
  *,
771
- tz: str | ZoneInfo | None = None,
779
+ tz: Optional[Union[str, ZoneInfo]] = None,
772
780
  ) -> None:
773
- self.tz: ZoneInfo | None = None
781
+ self.tz: Optional[ZoneInfo] = None
774
782
  if tz:
775
783
  if isinstance(tz, ZoneInfo):
776
784
  self.tz = tz
@@ -810,7 +818,7 @@ class CronRunner:
810
818
  )
811
819
 
812
820
  self.__start_date: datetime = self.date
813
- self.cron: CronJob | CronJobYear = cron
821
+ self.cron: Union[CronJob, CronJobYear] = cron
814
822
  self.is_year: bool = isinstance(cron, CronJobYear)
815
823
  self.reset_flag: bool = True
816
824
 
@@ -863,7 +871,7 @@ class CronRunner:
863
871
 
864
872
  raise RecursionError("Unable to find execution time for schedule")
865
873
 
866
- def __shift_date(self, mode: str, reverse: bool = False) -> bool:
874
+ def __shift_date(self, mode: DatetimeMode, reverse: bool = False) -> bool:
867
875
  """Increments the mode of date value ("month", "day", "hour", "minute")
868
876
  until matches with the schedule.
869
877
 
@@ -897,8 +905,8 @@ class CronRunner:
897
905
  getattr(self.date, mode)
898
906
  > (max_year := max(self.cron.year.values))
899
907
  ):
900
- raise CronYearLimit(
901
- f"The year is out of limit with this crontab value: "
908
+ raise YearReachLimit(
909
+ f"The year is reach the limit with this crontab setting: "
902
910
  f"{max_year}."
903
911
  )
904
912
 
@@ -917,12 +925,3 @@ class CronRunner:
917
925
 
918
926
  # NOTE: Return False if the date that match with condition.
919
927
  return False
920
-
921
-
922
- __all__ = (
923
- "CronJob",
924
- "CronJobYear",
925
- "CronRunner",
926
- "Options",
927
- "WEEKDAYS",
928
- )
@@ -314,3 +314,26 @@ class CrontabYear(Crontab):
314
314
  if isinstance(value, str)
315
315
  else value
316
316
  )
317
+
318
+
319
+ class ReleaseEvent(BaseModel): # pragma: no cov
320
+ """Release trigger event."""
321
+
322
+ release: list[str] = Field(
323
+ description=(
324
+ "A list of workflow name that want to receive event from release"
325
+ "trigger."
326
+ )
327
+ )
328
+
329
+
330
+ Event = Annotated[
331
+ Union[
332
+ CronJobYear,
333
+ CronJob,
334
+ ],
335
+ Field(
336
+ union_mode="smart",
337
+ description="An event models.",
338
+ ),
339
+ ] # pragma: no cov
@@ -23,7 +23,7 @@ from inspect import Traceback, currentframe, getframeinfo
23
23
  from pathlib import Path
24
24
  from threading import get_ident
25
25
  from types import FrameType
26
- from typing import ClassVar, Literal, Optional, TypeVar, Union
26
+ from typing import ClassVar, Final, Literal, Optional, TypeVar, Union
27
27
 
28
28
  from pydantic import BaseModel, ConfigDict, Field
29
29
  from pydantic.functional_validators import model_validator
@@ -74,7 +74,7 @@ def get_dt_tznow() -> datetime: # pragma: no cov
74
74
  return get_dt_now(tz=config.tz)
75
75
 
76
76
 
77
- PREFIX_LOGS: dict[str, dict] = {
77
+ PREFIX_LOGS: Final[dict[str, dict]] = {
78
78
  "CALLER": {
79
79
  "emoji": "📍",
80
80
  "desc": "logs from any usage from custom caller function.",
@@ -85,7 +85,7 @@ PREFIX_LOGS: dict[str, dict] = {
85
85
  "RELEASE": {"emoji": "📅", "desc": "logs from release workflow method."},
86
86
  "POKING": {"emoji": "⏰", "desc": "logs from poke workflow method."},
87
87
  } # pragma: no cov
88
- PREFIX_DEFAULT: str = "CALLER"
88
+ PREFIX_DEFAULT: Final[str] = "CALLER"
89
89
  PREFIX_LOGS_REGEX: re.Pattern[str] = re.compile(
90
90
  rf"(^\[(?P<name>{'|'.join(PREFIX_LOGS)})]:\s?)?(?P<message>.*)",
91
91
  re.MULTILINE | re.DOTALL | re.ASCII | re.VERBOSE,
@@ -103,6 +103,9 @@ class PrefixMsg(BaseModel):
103
103
  def prepare(self, extras: Optional[DictData] = None) -> str:
104
104
  """Prepare message with force add prefix before writing trace log.
105
105
 
106
+ :param extras: (DictData) An extra parameter that want to get the
107
+ `log_add_emoji` flag.
108
+
106
109
  :rtype: str
107
110
  """
108
111
  name: str = self.name or PREFIX_DEFAULT
@@ -332,9 +335,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
332
335
 
333
336
  :param message: (str) A message that want to log.
334
337
  """
335
- msg: str = prepare_newline(
336
- self.make_message(extract_msg_prefix(message).prepare(self.extras))
337
- )
338
+ msg: str = self.make_message(message)
338
339
 
339
340
  if mode != "debug" or (
340
341
  mode == "debug" and dynamic("debug", extras=self.extras)
@@ -391,9 +392,7 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
391
392
 
392
393
  :param message: (str) A message that want to log.
393
394
  """
394
- msg: str = prepare_newline(
395
- self.make_message(extract_msg_prefix(message).prepare(self.extras))
396
- )
395
+ msg: str = self.make_message(message)
397
396
 
398
397
  if mode != "debug" or (
399
398
  mode == "debug" and dynamic("debug", extras=self.extras)
@@ -514,13 +513,15 @@ class FileTrace(BaseTrace): # pragma: no cov
514
513
  return f"{cut_parent_run_id} -> {cut_run_id}"
515
514
 
516
515
  def make_message(self, message: str) -> str:
517
- """Prepare and Make a message before write and log processes.
516
+ """Prepare and Make a message before write and log steps.
518
517
 
519
518
  :param message: (str) A message that want to prepare and make before.
520
519
 
521
520
  :rtype: str
522
521
  """
523
- return f"({self.cut_id}) {message}"
522
+ return prepare_newline(
523
+ f"({self.cut_id}) {extract_msg_prefix(message).prepare(self.extras)}"
524
+ )
524
525
 
525
526
  def writer(self, message: str, level: str, is_err: bool = False) -> None:
526
527
  """Write a trace message after making to target file and write metadata
@@ -20,6 +20,7 @@ from typing import Annotated, Any, Literal, Optional, TypeVar, Union
20
20
  from ddeutil.core import str2dict, str2list
21
21
  from pydantic import BaseModel, Field
22
22
 
23
+ from .__types import StrOrInt
23
24
  from .exceptions import ParamValueException
24
25
  from .utils import get_d_now, get_dt_now
25
26
 
@@ -159,10 +160,11 @@ class StrParam(DefaultParam):
159
160
 
160
161
  type: Literal["str"] = "str"
161
162
 
162
- def receive(self, value: Optional[str] = None) -> Optional[str]:
163
+ def receive(self, value: Optional[Any] = None) -> Optional[str]:
163
164
  """Receive value that match with str.
164
165
 
165
- :param value: A value that want to validate with string parameter type.
166
+ :param value: (Any) A value that want to validate with string parameter
167
+ type.
166
168
  :rtype: Optional[str]
167
169
  """
168
170
  if value is None:
@@ -175,7 +177,7 @@ class IntParam(DefaultParam):
175
177
 
176
178
  type: Literal["int"] = "int"
177
179
 
178
- def receive(self, value: Optional[int] = None) -> Optional[int]:
180
+ def receive(self, value: Optional[StrOrInt] = None) -> Optional[int]:
179
181
  """Receive value that match with int.
180
182
 
181
183
  :param value: A value that want to validate with integer parameter type.
@@ -200,13 +202,24 @@ class FloatParam(DefaultParam): # pragma: no cov
200
202
  precision: int = 6
201
203
 
202
204
  def rounding(self, value: float) -> float:
203
- """Rounding float value with the specific precision field."""
205
+ """Rounding float value with the specific precision field.
206
+
207
+ :param value: A float value that want to round with the precision value.
208
+
209
+ :rtype: float
210
+ """
204
211
  round_str: str = f"{{0:.{self.precision}f}}"
205
212
  return float(round_str.format(round(value, self.precision)))
206
213
 
207
- def receive(self, value: Optional[Union[float, int, str]] = None) -> float:
214
+ def receive(
215
+ self, value: Optional[Union[float, int, str]] = None
216
+ ) -> Optional[float]:
217
+ """Receive value that match with float.
208
218
 
209
- if value in None:
219
+ :param value: A value that want to validate with float parameter type.
220
+ :rtype: float | None
221
+ """
222
+ if value is None:
210
223
  return self.default
211
224
 
212
225
  if isinstance(value, float):
@@ -217,11 +230,7 @@ class FloatParam(DefaultParam): # pragma: no cov
217
230
  raise TypeError(
218
231
  "Received value type does not math with str, float, or int."
219
232
  )
220
-
221
- try:
222
- return self.rounding(float(value))
223
- except Exception:
224
- raise
233
+ return self.rounding(float(value))
225
234
 
226
235
 
227
236
  class DecimalParam(DefaultParam): # pragma: no cov
@@ -231,12 +240,28 @@ class DecimalParam(DefaultParam): # pragma: no cov
231
240
  precision: int = 6
232
241
 
233
242
  def rounding(self, value: Decimal) -> Decimal:
234
- """Rounding float value with the specific precision field."""
243
+ """Rounding float value with the specific precision field.
244
+
245
+ :param value: (Decimal) A Decimal value that want to round with the
246
+ precision value.
247
+
248
+ :rtype: Decimal
249
+ """
235
250
  return value.quantize(Decimal(10) ** -self.precision)
236
251
 
237
- def receive(self, value: float | Decimal | None = None) -> Decimal:
252
+ def receive(
253
+ self, value: Optional[Union[float, int, str, Decimal]] = None
254
+ ) -> Decimal:
255
+ """Receive value that match with decimal.
238
256
 
239
- if isinstance(value, float):
257
+ :param value: (float | Decimal) A value that want to validate with
258
+ decimal parameter type.
259
+ :rtype: Decimal | None
260
+ """
261
+ if value is None:
262
+ return self.default
263
+
264
+ if isinstance(value, (float, int)):
240
265
  return self.rounding(Decimal(value))
241
266
  elif isinstance(value, Decimal):
242
267
  return self.rounding(value)
@@ -261,11 +286,12 @@ class ChoiceParam(BaseParam):
261
286
  description="A list of choice parameters that able be str or int.",
262
287
  )
263
288
 
264
- def receive(self, value: Union[str, int] | None = None) -> Union[str, int]:
289
+ def receive(self, value: Optional[StrOrInt] = None) -> StrOrInt:
265
290
  """Receive value that match with options.
266
291
 
267
- :param value: A value that want to select from the options field.
268
- :rtype: str
292
+ :param value: (str | int) A value that want to select from the options
293
+ field.
294
+ :rtype: str | int
269
295
  """
270
296
  # NOTE:
271
297
  # Return the first value in options if it does not pass any input
@@ -279,7 +305,7 @@ class ChoiceParam(BaseParam):
279
305
  return value
280
306
 
281
307
 
282
- class MapParam(DefaultParam): # pragma: no cov
308
+ class MapParam(DefaultParam):
283
309
  """Map parameter."""
284
310
 
285
311
  type: Literal["map"] = "map"
@@ -295,6 +321,7 @@ class MapParam(DefaultParam): # pragma: no cov
295
321
  """Receive value that match with map type.
296
322
 
297
323
  :param value: A value that want to validate with map parameter type.
324
+
298
325
  :rtype: dict[Any, Any]
299
326
  """
300
327
  if value is None:
@@ -316,7 +343,7 @@ class MapParam(DefaultParam): # pragma: no cov
316
343
  return value
317
344
 
318
345
 
319
- class ArrayParam(DefaultParam): # pragma: no cov
346
+ class ArrayParam(DefaultParam):
320
347
  """Array parameter."""
321
348
 
322
349
  type: Literal["array"] = "array"
@@ -326,7 +353,7 @@ class ArrayParam(DefaultParam): # pragma: no cov
326
353
  )
327
354
 
328
355
  def receive(
329
- self, value: Optional[Union[list[T], tuple[T, ...], str]] = None
356
+ self, value: Optional[Union[list[T], tuple[T, ...], set[T], str]] = None
330
357
  ) -> list[T]:
331
358
  """Receive value that match with array type.
332
359
 
@@ -365,5 +392,11 @@ Param = Annotated[
365
392
  IntParam,
366
393
  StrParam,
367
394
  ],
368
- Field(discriminator="type"),
395
+ Field(
396
+ discriminator="type",
397
+ description=(
398
+ "A parameter models that use for validate and receive on the "
399
+ "workflow execution."
400
+ ),
401
+ ),
369
402
  ]
@@ -4,7 +4,7 @@
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
6
  # [x] Use dynamic config
7
- """Reusables module that keep any templating functions."""
7
+ """Reusables module that keep any template and template filter functions."""
8
8
  from __future__ import annotations
9
9
 
10
10
  import copy
@@ -14,7 +14,7 @@ from ast import Call, Constant, Expr, Module, Name, parse
14
14
  from datetime import datetime
15
15
  from functools import wraps
16
16
  from importlib import import_module
17
- from typing import Any, Callable, Optional, Protocol, TypeVar, Union
17
+ from typing import Any, Callable, Literal, Optional, Protocol, TypeVar, Union
18
18
 
19
19
  try:
20
20
  from typing import ParamSpec
@@ -32,7 +32,7 @@ from .exceptions import UtilException
32
32
  T = TypeVar("T")
33
33
  P = ParamSpec("P")
34
34
 
35
- logger = logging.getLogger("ddeutil.workflow")
35
+ # NOTE: Adjust logging level of the `asyncio` to INFO level.
36
36
  logging.getLogger("asyncio").setLevel(logging.INFO)
37
37
 
38
38
 
@@ -40,10 +40,14 @@ FILTERS: dict[str, Callable] = { # pragma: no cov
40
40
  "abs": abs,
41
41
  "str": str,
42
42
  "int": int,
43
+ "list": list,
44
+ "dict": dict,
43
45
  "title": lambda x: x.title(),
44
46
  "upper": lambda x: x.upper(),
45
47
  "lower": lambda x: x.lower(),
46
48
  "rstr": [str, repr],
49
+ "keys": lambda x: x.keys(),
50
+ "values": lambda x: x.values(),
47
51
  }
48
52
 
49
53
 
@@ -53,6 +57,7 @@ class FilterFunc(Protocol):
53
57
  """
54
58
 
55
59
  filter: str
60
+ mark: Literal["filter"] = "filter"
56
61
 
57
62
  def __call__(self, *args, **kwargs): ... # pragma: no cov
58
63
 
@@ -71,6 +76,7 @@ def custom_filter(name: str) -> Callable[P, FilterFunc]:
71
76
 
72
77
  def func_internal(func: Callable[[...], Any]) -> FilterFunc:
73
78
  func.filter = name
79
+ func.mark = "filter"
74
80
 
75
81
  @wraps(func)
76
82
  def wrapped(*args, **kwargs):
@@ -102,7 +108,10 @@ def make_filter_registry(
102
108
  for fstr, func in inspect.getmembers(importer, inspect.isfunction):
103
109
  # NOTE: check function attribute that already set tag by
104
110
  # ``utils.tag`` decorator.
105
- if not hasattr(func, "filter"):
111
+ if not (
112
+ hasattr(func, "filter")
113
+ and str(getattr(func, "mark", "NOT SET")) == "filter"
114
+ ):
106
115
  continue
107
116
 
108
117
  func: FilterFunc
@@ -207,11 +216,9 @@ def map_post_filter(
207
216
  value: T = func(value)
208
217
  else:
209
218
  value: T = f_func(value, *args, **kwargs)
210
- except UtilException as err:
211
- logger.warning(str(err))
219
+ except UtilException:
212
220
  raise
213
- except Exception as err:
214
- logger.warning(str(err))
221
+ except Exception:
215
222
  raise UtilException(
216
223
  f"The post-filter: {func_name!r} does not fit with {value!r} "
217
224
  f"(type: {type(value).__name__})."
@@ -301,7 +308,7 @@ def str2template(
301
308
  getter: Any = getdot(caller, params)
302
309
  except ValueError as err:
303
310
  raise UtilException(
304
- f"Params does not set caller: {caller!r}."
311
+ f"Parameters does not get dot with caller: {caller!r}."
305
312
  ) from err
306
313
 
307
314
  # NOTE:
@@ -330,7 +337,7 @@ def param2template(
330
337
  filters: Optional[dict[str, FilterRegistry]] = None,
331
338
  *,
332
339
  extras: Optional[DictData] = None,
333
- ) -> T:
340
+ ) -> Any:
334
341
  """Pass param to template string that can search by ``RE_CALLER`` regular
335
342
  expression.
336
343
 
@@ -340,7 +347,7 @@ def param2template(
340
347
  :param filters: A filter mapping for mapping with `map_post_filter` func.
341
348
  :param extras: (Optional[list[str]]) An Override extras.
342
349
 
343
- :rtype: T
350
+ :rtype: Any
344
351
  :returns: An any getter value from the params input.
345
352
  """
346
353
  registers: Optional[list[str]] = (
@@ -367,6 +374,11 @@ def param2template(
367
374
  def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
368
375
  """Format datetime object to string with the format.
369
376
 
377
+ Examples:
378
+
379
+ > ${{ start-date | fmt('%Y%m%d') }}
380
+ > ${{ start-date | fmt }}
381
+
370
382
  :param value: (datetime) A datetime value that want to format to string
371
383
  value.
372
384
  :param fmt: (str) A format string pattern that passing to the `dt.strftime`
@@ -383,15 +395,54 @@ def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
383
395
 
384
396
  @custom_filter("coalesce") # pragma: no cov
385
397
  def coalesce(value: Optional[T], default: Any) -> T:
386
- """Coalesce with default value if the main value is None."""
398
+ """Coalesce with default value if the main value is None.
399
+
400
+ Examples:
401
+
402
+ > ${{ value | coalesce("foo") }}
403
+
404
+ :param value: A value that want to check nullable.
405
+ :param default: A default value that use to returned value if an input
406
+ value was null.
407
+ """
387
408
  return default if value is None else value
388
409
 
389
410
 
411
+ @custom_filter("getitem") # pragma: no cov
412
+ def get_item(
413
+ value: DictData, key: Union[str, int], default: Optional[Any] = None
414
+ ) -> Any:
415
+ """Get a value with an input specific key."""
416
+ if not isinstance(value, dict):
417
+ raise UtilException(
418
+ f"The value that pass to `getitem` filter should be `dict` not "
419
+ f"`{type(value)}`."
420
+ )
421
+ return value.get(key, default)
422
+
423
+
424
+ @custom_filter("getindex") # pragma: no cov
425
+ def get_index(value: list[Any], index: int):
426
+ if not isinstance(value, list):
427
+ raise UtilException(
428
+ f"The value that pass to `getindex` filter should be `list` not "
429
+ f"`{type(value)}`."
430
+ )
431
+ try:
432
+ return value[index]
433
+ except IndexError as e:
434
+ raise UtilException(
435
+ f"Index: {index} is out of range of value (The maximum range is "
436
+ f"{len(value)})."
437
+ ) from e
438
+
439
+
390
440
  class TagFunc(Protocol):
391
441
  """Tag Function Protocol"""
392
442
 
393
443
  name: str
394
444
  tag: str
445
+ mark: Literal["tag"] = "tag"
395
446
 
396
447
  def __call__(self, *args, **kwargs): ... # pragma: no cov
397
448
 
@@ -419,6 +470,7 @@ def tag(
419
470
  def func_internal(func: Callable[[...], Any]) -> ReturnTagFunc:
420
471
  func.tag = name or "latest"
421
472
  func.name = alias or func.__name__.replace("_", "-")
473
+ func.mark = "tag"
422
474
 
423
475
  @wraps(func)
424
476
  def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
@@ -466,7 +518,9 @@ def make_registry(
466
518
  # NOTE: check function attribute that already set tag by
467
519
  # ``utils.tag`` decorator.
468
520
  if not (
469
- hasattr(func, "tag") and hasattr(func, "name")
521
+ hasattr(func, "tag")
522
+ and hasattr(func, "name")
523
+ and str(getattr(func, "mark", "NOT SET")) == "tag"
470
524
  ): # pragma: no cov
471
525
  continue
472
526