ddeutil-workflow 0.0.26.post0__py3-none-any.whl → 0.0.27__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/__init__.py +24 -16
- ddeutil/workflow/conf.py +169 -105
- ddeutil/workflow/exceptions.py +0 -3
- ddeutil/workflow/hook.py +153 -0
- ddeutil/workflow/job.py +1 -1
- ddeutil/workflow/scheduler.py +2 -2
- ddeutil/workflow/stage.py +3 -55
- ddeutil/workflow/templates.py +334 -0
- ddeutil/workflow/utils.py +3 -391
- ddeutil/workflow/workflow.py +6 -7
- {ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.27.dist-info}/METADATA +24 -24
- ddeutil_workflow-0.0.27.dist-info/RECORD +25 -0
- ddeutil_workflow-0.0.26.post0.dist-info/RECORD +0 -23
- {ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.27.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.27.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.26.post0.dist-info → ddeutil_workflow-0.0.27.dist-info}/top_level.txt +0 -0
ddeutil/workflow/stage.py
CHANGED
@@ -31,7 +31,6 @@ import time
|
|
31
31
|
import uuid
|
32
32
|
from abc import ABC, abstractmethod
|
33
33
|
from collections.abc import Iterator
|
34
|
-
from dataclasses import dataclass
|
35
34
|
from functools import wraps
|
36
35
|
from inspect import Parameter
|
37
36
|
from pathlib import Path
|
@@ -48,19 +47,16 @@ from pydantic import BaseModel, Field
|
|
48
47
|
from pydantic.functional_validators import model_validator
|
49
48
|
from typing_extensions import Self
|
50
49
|
|
51
|
-
from .__types import DictData, DictStr,
|
50
|
+
from .__types import DictData, DictStr, TupleStr
|
52
51
|
from .conf import config, get_logger
|
53
52
|
from .exceptions import StageException
|
53
|
+
from .hook import TagFunc, extract_hook
|
54
54
|
from .result import Result
|
55
|
+
from .templates import not_in_template, param2template
|
55
56
|
from .utils import (
|
56
|
-
Registry,
|
57
|
-
TagFunc,
|
58
57
|
cut_id,
|
59
58
|
gen_id,
|
60
59
|
make_exec,
|
61
|
-
make_registry,
|
62
|
-
not_in_template,
|
63
|
-
param2template,
|
64
60
|
)
|
65
61
|
|
66
62
|
P = ParamSpec("P")
|
@@ -76,7 +72,6 @@ __all__: TupleStr = (
|
|
76
72
|
"HookStage",
|
77
73
|
"TriggerStage",
|
78
74
|
"Stage",
|
79
|
-
"extract_hook",
|
80
75
|
)
|
81
76
|
|
82
77
|
|
@@ -558,53 +553,6 @@ class PyStage(BaseStage):
|
|
558
553
|
)
|
559
554
|
|
560
555
|
|
561
|
-
@dataclass(frozen=True)
|
562
|
-
class HookSearchData:
|
563
|
-
"""Hook Search dataclass that use for receive regular expression grouping
|
564
|
-
dict from searching hook string value.
|
565
|
-
"""
|
566
|
-
|
567
|
-
path: str
|
568
|
-
func: str
|
569
|
-
tag: str
|
570
|
-
|
571
|
-
|
572
|
-
def extract_hook(hook: str) -> Callable[[], TagFunc]:
|
573
|
-
"""Extract Hook function from string value to hook partial function that
|
574
|
-
does run it at runtime.
|
575
|
-
|
576
|
-
:raise NotImplementedError: When the searching hook's function result does
|
577
|
-
not exist in the registry.
|
578
|
-
:raise NotImplementedError: When the searching hook's tag result does not
|
579
|
-
exists in the registry with its function key.
|
580
|
-
|
581
|
-
:param hook: A hook value that able to match with Task regex.
|
582
|
-
:rtype: Callable[[], TagFunc]
|
583
|
-
"""
|
584
|
-
if not (found := Re.RE_TASK_FMT.search(hook)):
|
585
|
-
raise ValueError(
|
586
|
-
f"Hook {hook!r} does not match with hook format regex."
|
587
|
-
)
|
588
|
-
|
589
|
-
# NOTE: Pass the searching hook string to `path`, `func`, and `tag`.
|
590
|
-
hook: HookSearchData = HookSearchData(**found.groupdict())
|
591
|
-
|
592
|
-
# NOTE: Registry object should implement on this package only.
|
593
|
-
rgt: dict[str, Registry] = make_registry(f"{hook.path}")
|
594
|
-
if hook.func not in rgt:
|
595
|
-
raise NotImplementedError(
|
596
|
-
f"``REGISTER-MODULES.{hook.path}.registries`` does not "
|
597
|
-
f"implement registry: {hook.func!r}."
|
598
|
-
)
|
599
|
-
|
600
|
-
if hook.tag not in rgt[hook.func]:
|
601
|
-
raise NotImplementedError(
|
602
|
-
f"tag: {hook.tag!r} does not found on registry func: "
|
603
|
-
f"``REGISTER-MODULES.{hook.path}.registries.{hook.func}``"
|
604
|
-
)
|
605
|
-
return rgt[hook.func][hook.tag]
|
606
|
-
|
607
|
-
|
608
556
|
class HookStage(BaseStage):
|
609
557
|
"""Hook executor that hook the Python function from registry with tag
|
610
558
|
decorator function in ``utils`` module and run it with input arguments.
|
@@ -0,0 +1,334 @@
|
|
1
|
+
# ------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2022 Korawich Anuttra. All rights reserved.
|
3
|
+
# Licensed under the MIT License. See LICENSE in the project root for
|
4
|
+
# license information.
|
5
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import inspect
|
9
|
+
import logging
|
10
|
+
from ast import Call, Constant, Expr, Module, Name, parse
|
11
|
+
from datetime import datetime
|
12
|
+
from functools import wraps
|
13
|
+
from importlib import import_module
|
14
|
+
from typing import Any, Callable, Protocol, TypeVar, Union
|
15
|
+
|
16
|
+
try:
|
17
|
+
from typing import ParamSpec
|
18
|
+
except ImportError:
|
19
|
+
from typing_extensions import ParamSpec
|
20
|
+
|
21
|
+
from ddeutil.core import getdot, hasdot, import_string
|
22
|
+
from ddeutil.io import search_env_replace
|
23
|
+
|
24
|
+
from .__types import DictData, Re
|
25
|
+
from .conf import config
|
26
|
+
from .exceptions import UtilException
|
27
|
+
|
28
|
+
T = TypeVar("T")
|
29
|
+
P = ParamSpec("P")
|
30
|
+
|
31
|
+
logger = logging.getLogger("ddeutil.workflow")
|
32
|
+
|
33
|
+
|
34
|
+
FILTERS: dict[str, callable] = { # pragma: no cov
|
35
|
+
"abs": abs,
|
36
|
+
"str": str,
|
37
|
+
"int": int,
|
38
|
+
"title": lambda x: x.title(),
|
39
|
+
"upper": lambda x: x.upper(),
|
40
|
+
"lower": lambda x: x.lower(),
|
41
|
+
"rstr": [str, repr],
|
42
|
+
}
|
43
|
+
|
44
|
+
|
45
|
+
class FilterFunc(Protocol):
|
46
|
+
"""Tag Function Protocol. This protocol that use to represent any callable
|
47
|
+
object that able to access the name attribute.
|
48
|
+
"""
|
49
|
+
|
50
|
+
name: str
|
51
|
+
|
52
|
+
def __call__(self, *args, **kwargs): ... # pragma: no cov
|
53
|
+
|
54
|
+
|
55
|
+
FilterRegistry = Union[FilterFunc, Callable[[...], Any]]
|
56
|
+
|
57
|
+
|
58
|
+
def custom_filter(name: str) -> Callable[P, FilterFunc]:
|
59
|
+
"""Custom filter decorator function that set function attributes, ``filter``
|
60
|
+
for making filter registries variable.
|
61
|
+
|
62
|
+
:param: name: A filter name for make different use-case of a function.
|
63
|
+
:rtype: Callable[P, FilterFunc]
|
64
|
+
"""
|
65
|
+
|
66
|
+
def func_internal(func: Callable[[...], Any]) -> FilterFunc:
|
67
|
+
func.filter = name
|
68
|
+
|
69
|
+
@wraps(func)
|
70
|
+
def wrapped(*args, **kwargs):
|
71
|
+
# NOTE: Able to do anything before calling custom filter function.
|
72
|
+
return func(*args, **kwargs)
|
73
|
+
|
74
|
+
return wrapped
|
75
|
+
|
76
|
+
return func_internal
|
77
|
+
|
78
|
+
|
79
|
+
def make_filter_registry() -> dict[str, FilterRegistry]:
|
80
|
+
"""Return registries of all functions that able to called with task.
|
81
|
+
|
82
|
+
:rtype: dict[str, Registry]
|
83
|
+
"""
|
84
|
+
rs: dict[str, FilterRegistry] = {}
|
85
|
+
for module in config.regis_filter:
|
86
|
+
# NOTE: try to sequential import task functions
|
87
|
+
try:
|
88
|
+
importer = import_module(module)
|
89
|
+
except ModuleNotFoundError:
|
90
|
+
continue
|
91
|
+
|
92
|
+
for fstr, func in inspect.getmembers(importer, inspect.isfunction):
|
93
|
+
# NOTE: check function attribute that already set tag by
|
94
|
+
# ``utils.tag`` decorator.
|
95
|
+
if not hasattr(func, "filter"):
|
96
|
+
continue
|
97
|
+
|
98
|
+
rs[func.filter] = import_string(f"{module}.{fstr}")
|
99
|
+
|
100
|
+
rs.update(FILTERS)
|
101
|
+
return rs
|
102
|
+
|
103
|
+
|
104
|
+
def get_args_const(
|
105
|
+
expr: str,
|
106
|
+
) -> tuple[str, list[Constant], dict[str, Constant]]:
|
107
|
+
"""Get arguments and keyword-arguments from function calling string.
|
108
|
+
|
109
|
+
:rtype: tuple[str, list[Constant], dict[str, Constant]]
|
110
|
+
"""
|
111
|
+
try:
|
112
|
+
mod: Module = parse(expr)
|
113
|
+
except SyntaxError:
|
114
|
+
raise UtilException(
|
115
|
+
f"Post-filter: {expr} does not valid because it raise syntax error."
|
116
|
+
) from None
|
117
|
+
|
118
|
+
body: list[Expr] = mod.body
|
119
|
+
if len(body) > 1:
|
120
|
+
raise UtilException(
|
121
|
+
"Post-filter function should be only one calling per workflow."
|
122
|
+
)
|
123
|
+
|
124
|
+
caller: Union[Name, Call]
|
125
|
+
if isinstance((caller := body[0].value), Name):
|
126
|
+
return caller.id, [], {}
|
127
|
+
elif not isinstance(caller, Call):
|
128
|
+
raise UtilException(
|
129
|
+
f"Get arguments does not support for caller type: {type(caller)}"
|
130
|
+
)
|
131
|
+
|
132
|
+
name: Name = caller.func
|
133
|
+
args: list[Constant] = caller.args
|
134
|
+
keywords: dict[str, Constant] = {k.arg: k.value for k in caller.keywords}
|
135
|
+
|
136
|
+
if any(not isinstance(i, Constant) for i in args):
|
137
|
+
raise UtilException(f"Argument of {expr} should be constant.")
|
138
|
+
|
139
|
+
if any(not isinstance(i, Constant) for i in keywords.values()):
|
140
|
+
raise UtilException(f"Keyword argument of {expr} should be constant.")
|
141
|
+
|
142
|
+
return name.id, args, keywords
|
143
|
+
|
144
|
+
|
145
|
+
def get_args_from_filter(
|
146
|
+
ft: str,
|
147
|
+
filters: dict[str, FilterRegistry],
|
148
|
+
) -> tuple[str, FilterRegistry, list[Any], dict[Any, Any]]: # pragma: no cov
|
149
|
+
"""Get arguments and keyword-arguments from filter function calling string.
|
150
|
+
and validate it with the filter functions mapping dict.
|
151
|
+
"""
|
152
|
+
func_name, _args, _kwargs = get_args_const(ft)
|
153
|
+
args: list[Any] = [arg.value for arg in _args]
|
154
|
+
kwargs: dict[Any, Any] = {k: v.value for k, v in _kwargs.items()}
|
155
|
+
|
156
|
+
if func_name not in filters:
|
157
|
+
raise UtilException(
|
158
|
+
f"The post-filter: {func_name!r} does not support yet."
|
159
|
+
)
|
160
|
+
|
161
|
+
if isinstance((f_func := filters[func_name]), list) and (args or kwargs):
|
162
|
+
raise UtilException(
|
163
|
+
"Chain filter function does not support for passing arguments."
|
164
|
+
)
|
165
|
+
|
166
|
+
return func_name, f_func, args, kwargs
|
167
|
+
|
168
|
+
|
169
|
+
def map_post_filter(
|
170
|
+
value: T,
|
171
|
+
post_filter: list[str],
|
172
|
+
filters: dict[str, FilterRegistry],
|
173
|
+
) -> T:
|
174
|
+
"""Mapping post-filter to value with sequence list of filter function name
|
175
|
+
that will get from the filter registry.
|
176
|
+
|
177
|
+
:param value: A string value that want to mapped with filter function.
|
178
|
+
:param post_filter: A list of post-filter function name.
|
179
|
+
:param filters: A filter registry.
|
180
|
+
|
181
|
+
:rtype: T
|
182
|
+
"""
|
183
|
+
for ft in post_filter:
|
184
|
+
func_name, f_func, args, kwargs = get_args_from_filter(ft, filters)
|
185
|
+
try:
|
186
|
+
if isinstance(f_func, list):
|
187
|
+
for func in f_func:
|
188
|
+
value: T = func(value)
|
189
|
+
else:
|
190
|
+
value: T = f_func(value, *args, **kwargs)
|
191
|
+
except UtilException as err:
|
192
|
+
logger.warning(str(err))
|
193
|
+
raise
|
194
|
+
except Exception as err:
|
195
|
+
logger.warning(str(err))
|
196
|
+
raise UtilException(
|
197
|
+
f"The post-filter function: {func_name} does not fit with "
|
198
|
+
f"{value} (type: {type(value).__name__})."
|
199
|
+
) from None
|
200
|
+
return value
|
201
|
+
|
202
|
+
|
203
|
+
def not_in_template(value: Any, *, not_in: str = "matrix.") -> bool:
|
204
|
+
"""Check value should not pass template with not_in value prefix.
|
205
|
+
|
206
|
+
:param value: A value that want to find parameter template prefix.
|
207
|
+
:param not_in: The not in string that use in the `.startswith` function.
|
208
|
+
|
209
|
+
:rtype: bool
|
210
|
+
"""
|
211
|
+
if isinstance(value, dict):
|
212
|
+
return any(not_in_template(value[k], not_in=not_in) for k in value)
|
213
|
+
elif isinstance(value, (list, tuple, set)):
|
214
|
+
return any(not_in_template(i, not_in=not_in) for i in value)
|
215
|
+
elif not isinstance(value, str):
|
216
|
+
return False
|
217
|
+
return any(
|
218
|
+
(not found.caller.strip().startswith(not_in))
|
219
|
+
for found in Re.finditer_caller(value.strip())
|
220
|
+
)
|
221
|
+
|
222
|
+
|
223
|
+
def has_template(value: Any) -> bool:
|
224
|
+
"""Check value include templating string.
|
225
|
+
|
226
|
+
:param value: A value that want to find parameter template.
|
227
|
+
|
228
|
+
:rtype: bool
|
229
|
+
"""
|
230
|
+
if isinstance(value, dict):
|
231
|
+
return any(has_template(value[k]) for k in value)
|
232
|
+
elif isinstance(value, (list, tuple, set)):
|
233
|
+
return any(has_template(i) for i in value)
|
234
|
+
elif not isinstance(value, str):
|
235
|
+
return False
|
236
|
+
return bool(Re.RE_CALLER.findall(value.strip()))
|
237
|
+
|
238
|
+
|
239
|
+
def str2template(
|
240
|
+
value: str,
|
241
|
+
params: DictData,
|
242
|
+
*,
|
243
|
+
filters: dict[str, FilterRegistry] | None = None,
|
244
|
+
) -> Any:
|
245
|
+
"""(Sub-function) Pass param to template string that can search by
|
246
|
+
``RE_CALLER`` regular expression.
|
247
|
+
|
248
|
+
The getter value that map a template should have typing support align
|
249
|
+
with the workflow parameter types that is `str`, `int`, `datetime`, and
|
250
|
+
`list`.
|
251
|
+
|
252
|
+
:param value: A string value that want to mapped with an params
|
253
|
+
:param params: A parameter value that getting with matched regular
|
254
|
+
expression.
|
255
|
+
:param filters:
|
256
|
+
"""
|
257
|
+
filters: dict[str, FilterRegistry] = filters or make_filter_registry()
|
258
|
+
|
259
|
+
# NOTE: remove space before and after this string value.
|
260
|
+
value: str = value.strip()
|
261
|
+
for found in Re.finditer_caller(value):
|
262
|
+
# NOTE:
|
263
|
+
# Get caller and filter values that setting inside;
|
264
|
+
#
|
265
|
+
# ... ``${{ <caller-value> [ | <filter-value>] ... }}``
|
266
|
+
#
|
267
|
+
caller: str = found.caller
|
268
|
+
pfilter: list[str] = [
|
269
|
+
i.strip()
|
270
|
+
for i in (found.post_filters.strip().removeprefix("|").split("|"))
|
271
|
+
if i != ""
|
272
|
+
]
|
273
|
+
if not hasdot(caller, params):
|
274
|
+
raise UtilException(f"The params does not set caller: {caller!r}.")
|
275
|
+
|
276
|
+
# NOTE: from validate step, it guarantee that caller exists in params.
|
277
|
+
getter: Any = getdot(caller, params)
|
278
|
+
|
279
|
+
# NOTE:
|
280
|
+
# If type of getter caller is not string type and it does not use to
|
281
|
+
# concat other string value, it will return origin value from the
|
282
|
+
# ``getdot`` function.
|
283
|
+
if value.replace(found.full, "", 1) == "":
|
284
|
+
return map_post_filter(getter, pfilter, filters=filters)
|
285
|
+
|
286
|
+
# NOTE: map post-filter function.
|
287
|
+
getter: Any = map_post_filter(getter, pfilter, filters=filters)
|
288
|
+
if not isinstance(getter, str):
|
289
|
+
getter: str = str(getter)
|
290
|
+
|
291
|
+
value: str = value.replace(found.full, getter, 1)
|
292
|
+
|
293
|
+
return search_env_replace(value)
|
294
|
+
|
295
|
+
|
296
|
+
def param2template(
|
297
|
+
value: Any,
|
298
|
+
params: DictData,
|
299
|
+
) -> Any:
|
300
|
+
"""Pass param to template string that can search by ``RE_CALLER`` regular
|
301
|
+
expression.
|
302
|
+
|
303
|
+
:param value: A value that want to mapped with an params
|
304
|
+
:param params: A parameter value that getting with matched regular
|
305
|
+
expression.
|
306
|
+
|
307
|
+
:rtype: Any
|
308
|
+
:returns: An any getter value from the params input.
|
309
|
+
"""
|
310
|
+
filters: dict[str, FilterRegistry] = make_filter_registry()
|
311
|
+
if isinstance(value, dict):
|
312
|
+
return {k: param2template(value[k], params) for k in value}
|
313
|
+
elif isinstance(value, (list, tuple, set)):
|
314
|
+
return type(value)([param2template(i, params) for i in value])
|
315
|
+
elif not isinstance(value, str):
|
316
|
+
return value
|
317
|
+
return str2template(value, params, filters=filters)
|
318
|
+
|
319
|
+
|
320
|
+
@custom_filter("fmt") # pragma: no cov
|
321
|
+
def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
322
|
+
"""Format datetime object to string with the format.
|
323
|
+
|
324
|
+
:param value: A datetime value that want to format to string value.
|
325
|
+
:param fmt: A format string pattern that passing to the `dt.strftime`
|
326
|
+
method.
|
327
|
+
|
328
|
+
:rtype: str
|
329
|
+
"""
|
330
|
+
if isinstance(value, datetime):
|
331
|
+
return value.strftime(fmt)
|
332
|
+
raise UtilException(
|
333
|
+
"This custom function should pass input value with datetime type."
|
334
|
+
)
|