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.
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/PKG-INFO +1 -1
- ddeutil_workflow-0.0.62/src/ddeutil/workflow/__about__.py +1 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/__cron.py +24 -25
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/event.py +23 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/logs.py +12 -11
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/params.py +54 -21
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/reusables.py +67 -13
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/stages.py +26 -7
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/utils.py +23 -1
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/workflow.py +203 -207
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test__cron.py +36 -28
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_logs_trace.py +13 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_params.py +9 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_reusables_call_tag.py +2 -1
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_reusables_template.py +76 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_reusables_template_filter.py +18 -22
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_stage_handler_exec.py +18 -5
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_utils.py +48 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_exec.py +4 -2
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_poke.py +9 -9
- ddeutil_workflow-0.0.60/src/ddeutil/workflow/__about__.py +0 -1
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/LICENSE +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/README.md +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/pyproject.toml +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/__init__.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/__main__.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/__types.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/__init__.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/logs.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/job.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/logs.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/schedules.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/api/utils.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/conf.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/exceptions.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/job.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/result.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil/workflow/scheduler.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_conf.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_event.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_job.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_job_exec.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_job_exec_strategy.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_logs_audit.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_release.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_release_queue.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_result.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_schedule.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_schedule_pending.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_schedule_tasks.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_schedule_workflow.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_scheduler_control.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_stage.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_strategy.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_exec_job.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_release.py +0 -0
- {ddeutil_workflow-0.0.60 → ddeutil_workflow-0.0.62}/tests/test_workflow_task.py +0 -0
@@ -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
|
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
|
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(
|
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
|
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
|
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
|
776
|
+
cron: Union[CronJob, CronJobYear],
|
769
777
|
date: Optional[datetime] = None,
|
770
778
|
*,
|
771
|
-
tz: str
|
779
|
+
tz: Optional[Union[str, ZoneInfo]] = None,
|
772
780
|
) -> None:
|
773
|
-
self.tz: ZoneInfo
|
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
|
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:
|
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
|
901
|
-
f"The year is
|
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 =
|
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 =
|
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
|
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
|
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[
|
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
|
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[
|
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(
|
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
|
-
|
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(
|
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
|
-
|
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:
|
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
|
268
|
-
|
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):
|
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):
|
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(
|
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
|
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
|
-
|
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
|
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
|
211
|
-
logger.warning(str(err))
|
219
|
+
except UtilException:
|
212
220
|
raise
|
213
|
-
except Exception
|
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"
|
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
|
-
) ->
|
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:
|
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")
|
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
|
|