ddeutil-workflow 0.0.61__py3-none-any.whl → 0.0.63__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.
- ddeutil/workflow/__about__.py +1 -1
- ddeutil/workflow/__cron.py +24 -25
- ddeutil/workflow/__init__.py +2 -24
- ddeutil/workflow/conf.py +36 -4
- ddeutil/workflow/event.py +13 -1
- ddeutil/workflow/reusables.py +131 -22
- ddeutil/workflow/stages.py +30 -24
- ddeutil/workflow/workflow.py +6 -6
- {ddeutil_workflow-0.0.61.dist-info → ddeutil_workflow-0.0.63.dist-info}/METADATA +11 -5
- {ddeutil_workflow-0.0.61.dist-info → ddeutil_workflow-0.0.63.dist-info}/RECORD +14 -14
- {ddeutil_workflow-0.0.61.dist-info → ddeutil_workflow-0.0.63.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.61.dist-info → ddeutil_workflow-0.0.63.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.61.dist-info → ddeutil_workflow-0.0.63.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.61.dist-info → ddeutil_workflow-0.0.63.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.63"
|
ddeutil/workflow/__cron.py
CHANGED
@@ -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
|
-
)
|
ddeutil/workflow/__init__.py
CHANGED
@@ -5,12 +5,7 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
from .__cron import CronJob, CronRunner
|
7
7
|
from .__types import DictData, DictStr, Matrix, Re, TupleStr
|
8
|
-
from .conf import
|
9
|
-
Config,
|
10
|
-
FileLoad,
|
11
|
-
config,
|
12
|
-
env,
|
13
|
-
)
|
8
|
+
from .conf import *
|
14
9
|
from .event import *
|
15
10
|
from .exceptions import *
|
16
11
|
from .job import *
|
@@ -37,24 +32,7 @@ from .result import (
|
|
37
32
|
Result,
|
38
33
|
Status,
|
39
34
|
)
|
40
|
-
from .reusables import
|
41
|
-
FILTERS,
|
42
|
-
FilterFunc,
|
43
|
-
FilterRegistry,
|
44
|
-
ReturnTagFunc,
|
45
|
-
TagFunc,
|
46
|
-
custom_filter,
|
47
|
-
extract_call,
|
48
|
-
get_args_const,
|
49
|
-
has_template,
|
50
|
-
make_filter_registry,
|
51
|
-
make_registry,
|
52
|
-
map_post_filter,
|
53
|
-
not_in_template,
|
54
|
-
param2template,
|
55
|
-
str2template,
|
56
|
-
tag,
|
57
|
-
)
|
35
|
+
from .reusables import *
|
58
36
|
from .scheduler import (
|
59
37
|
Schedule,
|
60
38
|
ScheduleWorkflow,
|
ddeutil/workflow/conf.py
CHANGED
@@ -18,8 +18,9 @@ from typing import Final, Optional, Protocol, TypeVar, Union
|
|
18
18
|
from zoneinfo import ZoneInfo
|
19
19
|
|
20
20
|
from ddeutil.core import str2bool
|
21
|
-
from ddeutil.io import YamlFlResolve
|
21
|
+
from ddeutil.io import YamlFlResolve, search_env_replace
|
22
22
|
from ddeutil.io.paths import glob_files, is_ignored, read_ignore
|
23
|
+
from pydantic import SecretStr
|
23
24
|
|
24
25
|
from .__types import DictData
|
25
26
|
|
@@ -321,15 +322,16 @@ class FileLoad(BaseLoad):
|
|
321
322
|
*,
|
322
323
|
path: Optional[Path] = None,
|
323
324
|
paths: Optional[list[Path]] = None,
|
324
|
-
excluded: list[str]
|
325
|
+
excluded: Optional[list[str]] = None,
|
325
326
|
extras: Optional[DictData] = None,
|
326
327
|
) -> Iterator[tuple[str, DictData]]:
|
327
328
|
"""Find all data that match with object type in config path. This class
|
328
329
|
method can use include and exclude list of identity name for filter and
|
329
330
|
adds-on.
|
330
331
|
|
331
|
-
:param obj: An object that want to validate matching before
|
332
|
-
|
332
|
+
:param obj: (object) An object that want to validate matching before
|
333
|
+
return.
|
334
|
+
:param path: (Path) A config path object.
|
333
335
|
:param paths: (list[Path]) A list of config path object.
|
334
336
|
:param excluded: An included list of data key that want to filter from
|
335
337
|
data.
|
@@ -474,3 +476,33 @@ class Loader(Protocol): # pragma: no cov
|
|
474
476
|
def finds(
|
475
477
|
cls, obj: object, *args, **kwargs
|
476
478
|
) -> Iterator[tuple[str, DictData]]: ...
|
479
|
+
|
480
|
+
|
481
|
+
def pass_env(value: T) -> T: # pragma: no cov
|
482
|
+
"""Passing environment variable to an input value.
|
483
|
+
|
484
|
+
:param value: (Any) A value that want to pass env var searching.
|
485
|
+
|
486
|
+
:rtype: Any
|
487
|
+
"""
|
488
|
+
if isinstance(value, dict):
|
489
|
+
return {k: pass_env(value[k]) for k in value}
|
490
|
+
elif isinstance(value, (list, tuple, set)):
|
491
|
+
return type(value)([pass_env(i) for i in value])
|
492
|
+
if not isinstance(value, str):
|
493
|
+
return value
|
494
|
+
|
495
|
+
rs: str = search_env_replace(value)
|
496
|
+
return None if rs == "null" else rs
|
497
|
+
|
498
|
+
|
499
|
+
class WorkflowSecret(SecretStr): # pragma: no cov
|
500
|
+
"""Workflow Secret String model."""
|
501
|
+
|
502
|
+
def get_secret_value(self) -> str:
|
503
|
+
"""Override get_secret_value by adding pass_env before return the
|
504
|
+
real-value.
|
505
|
+
|
506
|
+
:rtype: str
|
507
|
+
"""
|
508
|
+
return pass_env(super().get_secret_value())
|
ddeutil/workflow/event.py
CHANGED
@@ -17,6 +17,7 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
17
17
|
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo
|
18
18
|
from pydantic.functional_serializers import field_serializer
|
19
19
|
from pydantic.functional_validators import field_validator, model_validator
|
20
|
+
from pydantic_extra_types.timezone_name import TimeZoneName
|
20
21
|
from typing_extensions import Self
|
21
22
|
|
22
23
|
from .__cron import WEEKDAYS, CronJob, CronJobYear, CronRunner, Options
|
@@ -92,7 +93,7 @@ class Crontab(BaseModel):
|
|
92
93
|
),
|
93
94
|
]
|
94
95
|
tz: Annotated[
|
95
|
-
|
96
|
+
TimeZoneName,
|
96
97
|
Field(
|
97
98
|
description="A timezone string value",
|
98
99
|
alias="timezone",
|
@@ -316,6 +317,17 @@ class CrontabYear(Crontab):
|
|
316
317
|
)
|
317
318
|
|
318
319
|
|
320
|
+
class ReleaseEvent(BaseModel): # pragma: no cov
|
321
|
+
"""Release trigger event."""
|
322
|
+
|
323
|
+
release: list[str] = Field(
|
324
|
+
description=(
|
325
|
+
"A list of workflow name that want to receive event from release"
|
326
|
+
"trigger."
|
327
|
+
)
|
328
|
+
)
|
329
|
+
|
330
|
+
|
319
331
|
Event = Annotated[
|
320
332
|
Union[
|
321
333
|
CronJobYear,
|
ddeutil/workflow/reusables.py
CHANGED
@@ -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,16 @@ 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
|
17
|
+
from typing import (
|
18
|
+
Any,
|
19
|
+
Callable,
|
20
|
+
Literal,
|
21
|
+
Optional,
|
22
|
+
Protocol,
|
23
|
+
TypeVar,
|
24
|
+
Union,
|
25
|
+
get_type_hints,
|
26
|
+
)
|
18
27
|
|
19
28
|
try:
|
20
29
|
from typing import ParamSpec
|
@@ -23,6 +32,7 @@ except ImportError:
|
|
23
32
|
|
24
33
|
from ddeutil.core import getdot, import_string, lazy
|
25
34
|
from ddeutil.io import search_env_replace
|
35
|
+
from pydantic import BaseModel, create_model
|
26
36
|
from pydantic.dataclasses import dataclass
|
27
37
|
|
28
38
|
from .__types import DictData, Re
|
@@ -32,7 +42,7 @@ from .exceptions import UtilException
|
|
32
42
|
T = TypeVar("T")
|
33
43
|
P = ParamSpec("P")
|
34
44
|
|
35
|
-
|
45
|
+
# NOTE: Adjust logging level of the `asyncio` to INFO level.
|
36
46
|
logging.getLogger("asyncio").setLevel(logging.INFO)
|
37
47
|
|
38
48
|
|
@@ -40,12 +50,14 @@ FILTERS: dict[str, Callable] = { # pragma: no cov
|
|
40
50
|
"abs": abs,
|
41
51
|
"str": str,
|
42
52
|
"int": int,
|
53
|
+
"list": list,
|
54
|
+
"dict": dict,
|
43
55
|
"title": lambda x: x.title(),
|
44
56
|
"upper": lambda x: x.upper(),
|
45
57
|
"lower": lambda x: x.lower(),
|
46
58
|
"rstr": [str, repr],
|
47
|
-
"keys": lambda x:
|
48
|
-
"values": lambda x:
|
59
|
+
"keys": lambda x: x.keys(),
|
60
|
+
"values": lambda x: x.values(),
|
49
61
|
}
|
50
62
|
|
51
63
|
|
@@ -55,6 +67,7 @@ class FilterFunc(Protocol):
|
|
55
67
|
"""
|
56
68
|
|
57
69
|
filter: str
|
70
|
+
mark: Literal["filter"] = "filter"
|
58
71
|
|
59
72
|
def __call__(self, *args, **kwargs): ... # pragma: no cov
|
60
73
|
|
@@ -73,6 +86,7 @@ def custom_filter(name: str) -> Callable[P, FilterFunc]:
|
|
73
86
|
|
74
87
|
def func_internal(func: Callable[[...], Any]) -> FilterFunc:
|
75
88
|
func.filter = name
|
89
|
+
func.mark = "filter"
|
76
90
|
|
77
91
|
@wraps(func)
|
78
92
|
def wrapped(*args, **kwargs):
|
@@ -104,7 +118,10 @@ def make_filter_registry(
|
|
104
118
|
for fstr, func in inspect.getmembers(importer, inspect.isfunction):
|
105
119
|
# NOTE: check function attribute that already set tag by
|
106
120
|
# ``utils.tag`` decorator.
|
107
|
-
if not
|
121
|
+
if not (
|
122
|
+
hasattr(func, "filter")
|
123
|
+
and str(getattr(func, "mark", "NOT SET")) == "filter"
|
124
|
+
):
|
108
125
|
continue
|
109
126
|
|
110
127
|
func: FilterFunc
|
@@ -209,11 +226,9 @@ def map_post_filter(
|
|
209
226
|
value: T = func(value)
|
210
227
|
else:
|
211
228
|
value: T = f_func(value, *args, **kwargs)
|
212
|
-
except UtilException
|
213
|
-
logger.warning(str(err))
|
229
|
+
except UtilException:
|
214
230
|
raise
|
215
|
-
except Exception
|
216
|
-
logger.warning(str(err))
|
231
|
+
except Exception:
|
217
232
|
raise UtilException(
|
218
233
|
f"The post-filter: {func_name!r} does not fit with {value!r} "
|
219
234
|
f"(type: {type(value).__name__})."
|
@@ -226,6 +241,7 @@ def not_in_template(value: Any, *, not_in: str = "matrix.") -> bool:
|
|
226
241
|
|
227
242
|
:param value: A value that want to find parameter template prefix.
|
228
243
|
:param not_in: The not-in string that use in the `.startswith` function.
|
244
|
+
(Default is `matrix.`)
|
229
245
|
|
230
246
|
:rtype: bool
|
231
247
|
"""
|
@@ -274,7 +290,7 @@ def str2template(
|
|
274
290
|
:param value: (str) A string value that want to map with params.
|
275
291
|
:param params: (DictData) A parameter value that getting with matched
|
276
292
|
regular expression.
|
277
|
-
:param filters: A mapping of filter registry.
|
293
|
+
:param filters: (dict[str, FilterRegistry]) A mapping of filter registry.
|
278
294
|
:param registers: (Optional[list[str]]) Override list of register.
|
279
295
|
|
280
296
|
:rtype: str
|
@@ -299,12 +315,14 @@ def str2template(
|
|
299
315
|
]
|
300
316
|
|
301
317
|
# NOTE: from validate step, it guarantees that caller exists in params.
|
318
|
+
# I recommend to avoid logging params context on this case because it
|
319
|
+
# can include secret value.
|
302
320
|
try:
|
303
321
|
getter: Any = getdot(caller, params)
|
304
|
-
except ValueError
|
322
|
+
except ValueError:
|
305
323
|
raise UtilException(
|
306
|
-
f"
|
307
|
-
) from
|
324
|
+
f"Parameters does not get dot with caller: {caller!r}."
|
325
|
+
) from None
|
308
326
|
|
309
327
|
# NOTE:
|
310
328
|
# If type of getter caller is not string type, and it does not use to
|
@@ -332,17 +350,18 @@ def param2template(
|
|
332
350
|
filters: Optional[dict[str, FilterRegistry]] = None,
|
333
351
|
*,
|
334
352
|
extras: Optional[DictData] = None,
|
335
|
-
) ->
|
353
|
+
) -> Any:
|
336
354
|
"""Pass param to template string that can search by ``RE_CALLER`` regular
|
337
355
|
expression.
|
338
356
|
|
339
|
-
:param value: A value that want to map with params
|
340
|
-
:param params: A parameter value that getting with matched
|
341
|
-
expression.
|
342
|
-
:param filters: A filter mapping for mapping
|
357
|
+
:param value: (Any) A value that want to map with params.
|
358
|
+
:param params: (DictData) A parameter value that getting with matched
|
359
|
+
regular expression.
|
360
|
+
:param filters: (dict[str, FilterRegistry]) A filter mapping for mapping
|
361
|
+
with `map_post_filter` func.
|
343
362
|
:param extras: (Optional[list[str]]) An Override extras.
|
344
363
|
|
345
|
-
:rtype:
|
364
|
+
:rtype: Any
|
346
365
|
:returns: An any getter value from the params input.
|
347
366
|
"""
|
348
367
|
registers: Optional[list[str]] = (
|
@@ -369,6 +388,11 @@ def param2template(
|
|
369
388
|
def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
370
389
|
"""Format datetime object to string with the format.
|
371
390
|
|
391
|
+
Examples:
|
392
|
+
|
393
|
+
>>> "${{ start-date | fmt('%Y%m%d') }}"
|
394
|
+
>>> "${{ start-date | fmt }}"
|
395
|
+
|
372
396
|
:param value: (datetime) A datetime value that want to format to string
|
373
397
|
value.
|
374
398
|
:param fmt: (str) A format string pattern that passing to the `dt.strftime`
|
@@ -385,15 +409,68 @@ def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
|
385
409
|
|
386
410
|
@custom_filter("coalesce") # pragma: no cov
|
387
411
|
def coalesce(value: Optional[T], default: Any) -> T:
|
388
|
-
"""Coalesce with default value if the main value is None.
|
412
|
+
"""Coalesce with default value if the main value is None.
|
413
|
+
|
414
|
+
Examples:
|
415
|
+
|
416
|
+
>>> "${{ value | coalesce('foo') }}"
|
417
|
+
|
418
|
+
:param value: A value that want to check nullable.
|
419
|
+
:param default: A default value that use to returned value if an input
|
420
|
+
value was null.
|
421
|
+
"""
|
389
422
|
return default if value is None else value
|
390
423
|
|
391
424
|
|
425
|
+
@custom_filter("getitem") # pragma: no cov
|
426
|
+
def get_item(
|
427
|
+
value: DictData, key: Union[str, int], default: Optional[Any] = None
|
428
|
+
) -> Any:
|
429
|
+
"""Get a value with an input specific key.
|
430
|
+
|
431
|
+
Examples:
|
432
|
+
|
433
|
+
>>> "${{ value | getitem('key') }}"
|
434
|
+
>>> "${{ value | getitem('key', 'default') }}"
|
435
|
+
|
436
|
+
"""
|
437
|
+
if not isinstance(value, dict):
|
438
|
+
raise UtilException(
|
439
|
+
f"The value that pass to `getitem` filter should be `dict` not "
|
440
|
+
f"`{type(value)}`."
|
441
|
+
)
|
442
|
+
return value.get(key, default)
|
443
|
+
|
444
|
+
|
445
|
+
@custom_filter("getindex") # pragma: no cov
|
446
|
+
def get_index(value: list[Any], index: int) -> Any:
|
447
|
+
"""Get a value with an input specific index.
|
448
|
+
|
449
|
+
Examples:
|
450
|
+
|
451
|
+
>>> "${{ value | getindex(1) }}"
|
452
|
+
|
453
|
+
"""
|
454
|
+
if not isinstance(value, list):
|
455
|
+
raise UtilException(
|
456
|
+
f"The value that pass to `getindex` filter should be `list` not "
|
457
|
+
f"`{type(value)}`."
|
458
|
+
)
|
459
|
+
try:
|
460
|
+
return value[index]
|
461
|
+
except IndexError as e:
|
462
|
+
raise UtilException(
|
463
|
+
f"Index: {index} is out of range of value (The maximum range is "
|
464
|
+
f"{len(value)})."
|
465
|
+
) from e
|
466
|
+
|
467
|
+
|
392
468
|
class TagFunc(Protocol):
|
393
469
|
"""Tag Function Protocol"""
|
394
470
|
|
395
471
|
name: str
|
396
472
|
tag: str
|
473
|
+
mark: Literal["tag"] = "tag"
|
397
474
|
|
398
475
|
def __call__(self, *args, **kwargs): ... # pragma: no cov
|
399
476
|
|
@@ -421,6 +498,7 @@ def tag(
|
|
421
498
|
def func_internal(func: Callable[[...], Any]) -> ReturnTagFunc:
|
422
499
|
func.tag = name or "latest"
|
423
500
|
func.name = alias or func.__name__.replace("_", "-")
|
501
|
+
func.mark = "tag"
|
424
502
|
|
425
503
|
@wraps(func)
|
426
504
|
def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
|
@@ -468,7 +546,9 @@ def make_registry(
|
|
468
546
|
# NOTE: check function attribute that already set tag by
|
469
547
|
# ``utils.tag`` decorator.
|
470
548
|
if not (
|
471
|
-
hasattr(func, "tag")
|
549
|
+
hasattr(func, "tag")
|
550
|
+
and hasattr(func, "name")
|
551
|
+
and str(getattr(func, "mark", "NOT SET")) == "tag"
|
472
552
|
): # pragma: no cov
|
473
553
|
continue
|
474
554
|
|
@@ -553,3 +633,32 @@ def extract_call(
|
|
553
633
|
f"`REGISTER.{call.path}.registries.{call.func}`"
|
554
634
|
)
|
555
635
|
return rgt[call.func][call.tag]
|
636
|
+
|
637
|
+
|
638
|
+
def create_model_from_caller(func: Callable) -> BaseModel: # pragma: no cov
|
639
|
+
"""Create model from the caller function. This function will use for
|
640
|
+
validate the caller function argument typed-hint that valid with the args
|
641
|
+
field.
|
642
|
+
|
643
|
+
:param func: A caller function.
|
644
|
+
|
645
|
+
:rtype: BaseModel
|
646
|
+
"""
|
647
|
+
sig: inspect.Signature = inspect.signature(func)
|
648
|
+
type_hints: dict[str, Any] = get_type_hints(func)
|
649
|
+
fields: dict[str, Any] = {}
|
650
|
+
for name in sig.parameters:
|
651
|
+
param: inspect.Parameter = sig.parameters[name]
|
652
|
+
if param.kind in (
|
653
|
+
inspect.Parameter.VAR_KEYWORD,
|
654
|
+
inspect.Parameter.VAR_POSITIONAL,
|
655
|
+
):
|
656
|
+
continue
|
657
|
+
if param.default != inspect.Parameter.empty:
|
658
|
+
fields[name] = (type_hints[name], param.default)
|
659
|
+
else:
|
660
|
+
fields[name] = (type_hints[name], ...)
|
661
|
+
|
662
|
+
return create_model(
|
663
|
+
"".join(i.title() for i in func.__name__.split("_")), **fields
|
664
|
+
)
|
ddeutil/workflow/stages.py
CHANGED
@@ -60,7 +60,7 @@ from pydantic.functional_validators import model_validator
|
|
60
60
|
from typing_extensions import Self
|
61
61
|
|
62
62
|
from .__types import DictData, DictStr, StrOrInt, StrOrNone, TupleStr
|
63
|
-
from .conf import dynamic
|
63
|
+
from .conf import dynamic, pass_env
|
64
64
|
from .exceptions import StageException, to_dict
|
65
65
|
from .result import CANCEL, FAILED, SUCCESS, WAIT, Result, Status
|
66
66
|
from .reusables import TagFunc, extract_call, not_in_template, param2template
|
@@ -626,10 +626,10 @@ class BashStage(BaseAsyncStage):
|
|
626
626
|
await f.write(f"#!/bin/{f_shebang}\n\n")
|
627
627
|
|
628
628
|
# NOTE: add setting environment variable before bash skip statement.
|
629
|
-
await f.writelines([f"{k}='{env[k]}';\n" for k in env])
|
629
|
+
await f.writelines(pass_env([f"{k}='{env[k]}';\n" for k in env]))
|
630
630
|
|
631
631
|
# NOTE: make sure that shell script file does not have `\r` char.
|
632
|
-
await f.write("\n" + bash.replace("\r\n", "\n"))
|
632
|
+
await f.write("\n" + pass_env(bash.replace("\r\n", "\n")))
|
633
633
|
|
634
634
|
# NOTE: Make this .sh file able to executable.
|
635
635
|
make_exec(f"./{f_name}")
|
@@ -662,10 +662,10 @@ class BashStage(BaseAsyncStage):
|
|
662
662
|
f.write(f"#!/bin/{f_shebang}\n\n")
|
663
663
|
|
664
664
|
# NOTE: add setting environment variable before bash skip statement.
|
665
|
-
f.writelines([f"{k}='{env[k]}';\n" for k in env])
|
665
|
+
f.writelines(pass_env([f"{k}='{env[k]}';\n" for k in env]))
|
666
666
|
|
667
667
|
# NOTE: make sure that shell script file does not have `\r` char.
|
668
|
-
f.write("\n" + bash.replace("\r\n", "\n"))
|
668
|
+
f.write("\n" + pass_env(bash.replace("\r\n", "\n")))
|
669
669
|
|
670
670
|
# NOTE: Make this .sh file able to executable.
|
671
671
|
make_exec(f"./{f_name}")
|
@@ -895,7 +895,9 @@ class PyStage(BaseAsyncStage):
|
|
895
895
|
# WARNING: The exec build-in function is very dangerous. So, it
|
896
896
|
# should use the re module to validate exec-string before running.
|
897
897
|
exec(
|
898
|
-
|
898
|
+
pass_env(
|
899
|
+
param2template(dedent(self.run), params, extras=self.extras)
|
900
|
+
),
|
899
901
|
gb,
|
900
902
|
lc,
|
901
903
|
)
|
@@ -1060,12 +1062,12 @@ class CallStage(BaseAsyncStage):
|
|
1060
1062
|
args: DictData = {"result": result} | param2template(
|
1061
1063
|
self.args, params, extras=self.extras
|
1062
1064
|
)
|
1063
|
-
|
1065
|
+
sig = inspect.signature(call_func)
|
1064
1066
|
necessary_params: list[str] = []
|
1065
1067
|
has_keyword: bool = False
|
1066
|
-
for k in
|
1068
|
+
for k in sig.parameters:
|
1067
1069
|
if (
|
1068
|
-
v :=
|
1070
|
+
v := sig.parameters[k]
|
1069
1071
|
).default == Parameter.empty and v.kind not in (
|
1070
1072
|
Parameter.VAR_KEYWORD,
|
1071
1073
|
Parameter.VAR_POSITIONAL,
|
@@ -1083,7 +1085,7 @@ class CallStage(BaseAsyncStage):
|
|
1083
1085
|
f"does not set to args, {list(args.keys())}."
|
1084
1086
|
)
|
1085
1087
|
|
1086
|
-
if "result" not in
|
1088
|
+
if "result" not in sig.parameters and not has_keyword:
|
1087
1089
|
args.pop("result")
|
1088
1090
|
|
1089
1091
|
args = self.parse_model_args(call_func, args, result)
|
@@ -1149,12 +1151,12 @@ class CallStage(BaseAsyncStage):
|
|
1149
1151
|
args: DictData = {"result": result} | param2template(
|
1150
1152
|
self.args, params, extras=self.extras
|
1151
1153
|
)
|
1152
|
-
|
1154
|
+
sig = inspect.signature(call_func)
|
1153
1155
|
necessary_params: list[str] = []
|
1154
1156
|
has_keyword: bool = False
|
1155
|
-
for k in
|
1157
|
+
for k in sig.parameters:
|
1156
1158
|
if (
|
1157
|
-
v :=
|
1159
|
+
v := sig.parameters[k]
|
1158
1160
|
).default == Parameter.empty and v.kind not in (
|
1159
1161
|
Parameter.VAR_KEYWORD,
|
1160
1162
|
Parameter.VAR_POSITIONAL,
|
@@ -1172,7 +1174,7 @@ class CallStage(BaseAsyncStage):
|
|
1172
1174
|
f"does not set to args, {list(args.keys())}."
|
1173
1175
|
)
|
1174
1176
|
|
1175
|
-
if "result" not in
|
1177
|
+
if "result" not in sig.parameters and not has_keyword:
|
1176
1178
|
args.pop("result")
|
1177
1179
|
|
1178
1180
|
args = self.parse_model_args(call_func, args, result)
|
@@ -1295,7 +1297,7 @@ class TriggerStage(BaseStage):
|
|
1295
1297
|
_trigger: str = param2template(self.trigger, params, extras=self.extras)
|
1296
1298
|
result.trace.info(f"[STAGE]: Execute Trigger-Stage: {_trigger!r}")
|
1297
1299
|
rs: Result = Workflow.from_conf(
|
1298
|
-
name=_trigger,
|
1300
|
+
name=pass_env(_trigger),
|
1299
1301
|
extras=self.extras | {"stage_raise_error": True},
|
1300
1302
|
).execute(
|
1301
1303
|
params=param2template(self.params, params, extras=self.extras),
|
@@ -2417,9 +2419,11 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
2417
2419
|
)
|
2418
2420
|
|
2419
2421
|
resp = client.api.pull(
|
2420
|
-
repository=
|
2421
|
-
tag=self.tag,
|
2422
|
-
auth_config=
|
2422
|
+
repository=pass_env(self.image),
|
2423
|
+
tag=pass_env(self.tag),
|
2424
|
+
auth_config=pass_env(
|
2425
|
+
param2template(self.auth, params, extras=self.extras)
|
2426
|
+
),
|
2423
2427
|
stream=True,
|
2424
2428
|
decode=True,
|
2425
2429
|
)
|
@@ -2438,10 +2442,10 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
2438
2442
|
|
2439
2443
|
unique_image_name: str = f"{self.image}_{datetime.now():%Y%m%d%H%M%S%f}"
|
2440
2444
|
container = client.containers.run(
|
2441
|
-
image=f"{self.image}:{self.tag}",
|
2445
|
+
image=pass_env(f"{self.image}:{self.tag}"),
|
2442
2446
|
name=unique_image_name,
|
2443
|
-
environment=self.env,
|
2444
|
-
volumes=(
|
2447
|
+
environment=pass_env(self.env),
|
2448
|
+
volumes=pass_env(
|
2445
2449
|
{
|
2446
2450
|
Path.cwd()
|
2447
2451
|
/ f".docker.{result.run_id}.logs": {
|
@@ -2549,8 +2553,10 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
2549
2553
|
f_name: str = f"{run_id}.py"
|
2550
2554
|
with open(f"./{f_name}", mode="w", newline="\n") as f:
|
2551
2555
|
# NOTE: Create variable mapping that write before running statement.
|
2552
|
-
vars_str: str =
|
2553
|
-
|
2556
|
+
vars_str: str = pass_env(
|
2557
|
+
"\n ".join(
|
2558
|
+
f"{var} = {value!r}" for var, value in values.items()
|
2559
|
+
)
|
2554
2560
|
)
|
2555
2561
|
|
2556
2562
|
# NOTE: `uv` supports PEP 723 — inline TOML metadata.
|
@@ -2568,7 +2574,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
2568
2574
|
)
|
2569
2575
|
|
2570
2576
|
# NOTE: make sure that py script file does not have `\r` char.
|
2571
|
-
f.write("\n" + py.replace("\r\n", "\n"))
|
2577
|
+
f.write("\n" + pass_env(py.replace("\r\n", "\n")))
|
2572
2578
|
|
2573
2579
|
# NOTE: Make this .py file able to executable.
|
2574
2580
|
make_exec(f"./{f_name}")
|
ddeutil/workflow/workflow.py
CHANGED
@@ -774,7 +774,7 @@ class Workflow(BaseModel):
|
|
774
774
|
parent_run_id: Optional[str] = None,
|
775
775
|
result: Optional[Result] = None,
|
776
776
|
event: Optional[Event] = None,
|
777
|
-
timeout:
|
777
|
+
timeout: float = 3600,
|
778
778
|
max_job_parallel: int = 2,
|
779
779
|
) -> Result:
|
780
780
|
"""Execute workflow with passing a dynamic parameters to all jobs that
|
@@ -805,10 +805,10 @@ class Workflow(BaseModel):
|
|
805
805
|
:param result: (Result) A Result instance for return context and status.
|
806
806
|
:param event: (Event) An Event manager instance that use to cancel this
|
807
807
|
execution if it forces stopped by parent execution.
|
808
|
-
:param timeout: (
|
809
|
-
use for limit time of execution and waiting job dependency.
|
810
|
-
value does not force stop the task that still running more than
|
811
|
-
limit time. (Default: 60 * 60 seconds)
|
808
|
+
:param timeout: (float) A workflow execution time out in second unit
|
809
|
+
that use for limit time of execution and waiting job dependency.
|
810
|
+
This value does not force stop the task that still running more than
|
811
|
+
this limit time. (Default: 60 * 60 seconds)
|
812
812
|
:param max_job_parallel: (int) The maximum workers that use for job
|
813
813
|
execution in `PoolThreadExecutor` object. (Default: 2 workers)
|
814
814
|
|
@@ -840,7 +840,7 @@ class Workflow(BaseModel):
|
|
840
840
|
job_queue.put(job_id)
|
841
841
|
|
842
842
|
not_timeout_flag: bool = True
|
843
|
-
timeout:
|
843
|
+
timeout: float = dynamic(
|
844
844
|
"max_job_exec_timeout", f=timeout, extras=self.extras
|
845
845
|
)
|
846
846
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.63
|
4
4
|
Summary: Lightweight workflow orchestration
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -25,6 +25,7 @@ License-File: LICENSE
|
|
25
25
|
Requires-Dist: ddeutil[checksum]>=0.4.8
|
26
26
|
Requires-Dist: ddeutil-io[toml,yaml]>=0.2.13
|
27
27
|
Requires-Dist: pydantic==2.11.4
|
28
|
+
Requires-Dist: pydantic-extra-types==2.10.4
|
28
29
|
Requires-Dist: python-dotenv==1.1.0
|
29
30
|
Requires-Dist: schedule<2.0.0,==1.2.2
|
30
31
|
Provides-Extra: all
|
@@ -215,19 +216,23 @@ registry-caller/
|
|
215
216
|
This function will store as module that will import from `WORKFLOW_CORE_REGISTRY_CALLER`
|
216
217
|
value (This config can override by extra parameters with `registry_caller` key).
|
217
218
|
|
219
|
+
> [!NOTE]
|
220
|
+
> You can use Pydantic Model as argument of your caller function. The core workflow
|
221
|
+
> engine will auto use the `model_validate` method before run your caller function.
|
222
|
+
|
218
223
|
```python
|
219
|
-
from ddeutil.workflow import Result, tag
|
224
|
+
from ddeutil.workflow import Result, WorkflowSecret, tag
|
220
225
|
from ddeutil.workflow.exceptions import StageException
|
221
|
-
from pydantic import BaseModel
|
226
|
+
from pydantic import BaseModel
|
222
227
|
|
223
228
|
class AwsCredential(BaseModel):
|
224
229
|
path: str
|
225
230
|
access_client_id: str
|
226
|
-
access_client_secret:
|
231
|
+
access_client_secret: WorkflowSecret
|
227
232
|
|
228
233
|
class RestAuth(BaseModel):
|
229
234
|
type: str
|
230
|
-
keys:
|
235
|
+
keys: WorkflowSecret
|
231
236
|
|
232
237
|
@tag("requests", alias="get-api-with-oauth-to-s3")
|
233
238
|
def get_api_with_oauth_to_s3(
|
@@ -243,6 +248,7 @@ def get_api_with_oauth_to_s3(
|
|
243
248
|
result.trace.info(f"... {method}: {url}")
|
244
249
|
if method != "post":
|
245
250
|
raise StageException(f"RestAPI does not support for {method} action.")
|
251
|
+
# NOTE: If you want to use secret, you can use `auth.keys.get_secret_value()`.
|
246
252
|
return {"records": 1000}
|
247
253
|
```
|
248
254
|
|
@@ -1,20 +1,20 @@
|
|
1
|
-
ddeutil/workflow/__about__.py,sha256=
|
2
|
-
ddeutil/workflow/__cron.py,sha256=
|
3
|
-
ddeutil/workflow/__init__.py,sha256=
|
1
|
+
ddeutil/workflow/__about__.py,sha256=xnUKKC8j_qfLMiPiNf3P8eAETFo17HtTgo2_-q1Nkvw,28
|
2
|
+
ddeutil/workflow/__cron.py,sha256=BOKQcreiex0SAigrK1gnLxpvOeF3aca_rQwyz9Kfve4,28751
|
3
|
+
ddeutil/workflow/__init__.py,sha256=4QIAmK-p6Oex3VPrFCSwo8brhpL7_mEF3AQQlZdTqvs,1030
|
4
4
|
ddeutil/workflow/__main__.py,sha256=x-sYedl4T8p6054aySk-EQX6vhytvPR0HvaBNYxMzp0,364
|
5
5
|
ddeutil/workflow/__types.py,sha256=uNfoRbVmNK5O37UUMVnqcmoghD9oMS1q9fXC0APnjSI,4584
|
6
|
-
ddeutil/workflow/conf.py,sha256=
|
7
|
-
ddeutil/workflow/event.py,sha256=
|
6
|
+
ddeutil/workflow/conf.py,sha256=kjjFZmBK7SzSpQnMJ0KKzB2I0Zcn4oGA6uLdHRRroj0,15828
|
7
|
+
ddeutil/workflow/event.py,sha256=S2eJAZZx_V5TuQ0l417hFVCtjWXnfNPZBgSCICzxQ48,11041
|
8
8
|
ddeutil/workflow/exceptions.py,sha256=TKHBIlfquz3yEb8_kg6UXpxVLKxstt3QA9a1XYsLPJk,2455
|
9
9
|
ddeutil/workflow/job.py,sha256=Php1b3n6c-jddel8PTSa61kAW22QBTetzoLVR4XXM4E,35240
|
10
10
|
ddeutil/workflow/logs.py,sha256=iVtyl8i69y7t07tAuWkihc54WlkHCcBy_Ur0WtzJ_lM,31367
|
11
11
|
ddeutil/workflow/params.py,sha256=1u8gXs1ZyMq-2eD9H8L7Yjfu5t7b_OzjA0fJvhxdYWY,12505
|
12
12
|
ddeutil/workflow/result.py,sha256=4M9VCcveI8Yz6ZrnI-67SZlry-Z8G7e0hziy1k-pklk,5906
|
13
|
-
ddeutil/workflow/reusables.py,sha256=
|
13
|
+
ddeutil/workflow/reusables.py,sha256=gbSHUptdEar5HSfBx13ldHJw0IBGF6BShQB3XpPd3Wg,20812
|
14
14
|
ddeutil/workflow/scheduler.py,sha256=OsEyj2zscQ-3bDMk2z7UtKlCWLlgoGjaRFt17o1B1ew,27263
|
15
|
-
ddeutil/workflow/stages.py,sha256=
|
15
|
+
ddeutil/workflow/stages.py,sha256=4JBcgXxxpDfwUFob30ocWwVqoxuk7t-WoDlj8dh8Dqs,93482
|
16
16
|
ddeutil/workflow/utils.py,sha256=rcaDwXaEs4SCdcBKWx4ZCEtpnNfPI8du7Er6b_rg8t4,9569
|
17
|
-
ddeutil/workflow/workflow.py,sha256=
|
17
|
+
ddeutil/workflow/workflow.py,sha256=8Z_h8OtNHkaGf8MJixTHNeXsyA4mBlYtHDqj0oEVFBs,44858
|
18
18
|
ddeutil/workflow/api/__init__.py,sha256=kY30dL8HPY8tY_GBmm7y_3OdoXzB1-EA2a96PLU0AQw,5278
|
19
19
|
ddeutil/workflow/api/logs.py,sha256=NMTnOnsBrDB5129329xF2myLdrb-z9k1MQrmrP7qXJw,1818
|
20
20
|
ddeutil/workflow/api/utils.py,sha256=uTtUFVLpiYYahXvCVx8sueRQ03K2Xw1id_gW3IMmX1U,5295
|
@@ -23,9 +23,9 @@ ddeutil/workflow/api/routes/job.py,sha256=8X5VLDJH6PumyNIY6JGRNBsf2gWN0eG9DzxRPS
|
|
23
23
|
ddeutil/workflow/api/routes/logs.py,sha256=U6vOni3wd-ZTOwd3yVdSOpgyRmNdcgfngU5KlLM3Cww,5383
|
24
24
|
ddeutil/workflow/api/routes/schedules.py,sha256=14RnaJKEGMSJtncI1H_QQVZNBe_jDS40PPRO6qFc3i0,4805
|
25
25
|
ddeutil/workflow/api/routes/workflows.py,sha256=GJu5PiXEylswrXylEImpncySjeU9chrvrtjhiMCw2RQ,4529
|
26
|
-
ddeutil_workflow-0.0.
|
27
|
-
ddeutil_workflow-0.0.
|
28
|
-
ddeutil_workflow-0.0.
|
29
|
-
ddeutil_workflow-0.0.
|
30
|
-
ddeutil_workflow-0.0.
|
31
|
-
ddeutil_workflow-0.0.
|
26
|
+
ddeutil_workflow-0.0.63.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
27
|
+
ddeutil_workflow-0.0.63.dist-info/METADATA,sha256=9h5mIv3IdcYoOG9Qaz02PgCm2KOJqjkDCS09WjDCAUo,19748
|
28
|
+
ddeutil_workflow-0.0.63.dist-info/WHEEL,sha256=DnLRTWE75wApRYVsjgc6wsVswC54sMSJhAEd4xhDpBk,91
|
29
|
+
ddeutil_workflow-0.0.63.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
|
30
|
+
ddeutil_workflow-0.0.63.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
31
|
+
ddeutil_workflow-0.0.63.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|