ddeutil-workflow 0.0.26.post1__py3-none-any.whl → 0.0.28__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 +19 -14
- ddeutil/workflow/api/api.py +1 -53
- ddeutil/workflow/conf.py +44 -23
- ddeutil/workflow/cron.py +7 -7
- ddeutil/workflow/exceptions.py +1 -4
- ddeutil/workflow/hook.py +168 -0
- ddeutil/workflow/job.py +18 -17
- ddeutil/workflow/params.py +3 -3
- ddeutil/workflow/result.py +3 -3
- ddeutil/workflow/scheduler.py +9 -9
- ddeutil/workflow/stage.py +87 -170
- ddeutil/workflow/templates.py +336 -0
- ddeutil/workflow/utils.py +23 -404
- ddeutil/workflow/workflow.py +22 -23
- ddeutil_workflow-0.0.28.dist-info/METADATA +284 -0
- ddeutil_workflow-0.0.28.dist-info/RECORD +25 -0
- {ddeutil_workflow-0.0.26.post1.dist-info → ddeutil_workflow-0.0.28.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.26.post1.dist-info/METADATA +0 -230
- ddeutil_workflow-0.0.26.post1.dist-info/RECORD +0 -23
- {ddeutil_workflow-0.0.26.post1.dist-info → ddeutil_workflow-0.0.28.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.26.post1.dist-info → ddeutil_workflow-0.0.28.dist-info}/top_level.txt +0 -0
ddeutil/workflow/utils.py
CHANGED
@@ -5,40 +5,25 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
-
import inspect
|
9
8
|
import logging
|
10
9
|
import stat
|
11
10
|
import time
|
12
|
-
from ast import Call, Constant, Expr, Module, Name, parse
|
13
11
|
from collections.abc import Iterator
|
14
12
|
from datetime import datetime, timedelta
|
15
|
-
from functools import wraps
|
16
13
|
from hashlib import md5
|
17
|
-
from importlib import import_module
|
18
14
|
from inspect import isfunction
|
19
15
|
from itertools import chain, islice, product
|
20
16
|
from pathlib import Path
|
21
17
|
from random import randrange
|
22
|
-
from typing import Any,
|
18
|
+
from typing import Any, TypeVar
|
23
19
|
from zoneinfo import ZoneInfo
|
24
20
|
|
25
|
-
|
26
|
-
from typing import ParamSpec
|
27
|
-
except ImportError:
|
28
|
-
from typing_extensions import ParamSpec
|
21
|
+
from ddeutil.core import hash_str
|
29
22
|
|
30
|
-
from
|
31
|
-
from ddeutil.io import search_env_replace
|
32
|
-
from pydantic import BaseModel
|
33
|
-
|
34
|
-
from .__types import DictData, Matrix, Re
|
23
|
+
from .__types import DictData, Matrix
|
35
24
|
from .conf import config
|
36
|
-
from .exceptions import UtilException
|
37
25
|
|
38
26
|
T = TypeVar("T")
|
39
|
-
P = ParamSpec("P")
|
40
|
-
AnyModel = TypeVar("AnyModel", bound=BaseModel)
|
41
|
-
AnyModelType = type[AnyModel]
|
42
27
|
|
43
28
|
logger = logging.getLogger("ddeutil.workflow")
|
44
29
|
|
@@ -95,7 +80,7 @@ def gen_id(
|
|
95
80
|
sensitive: bool = True,
|
96
81
|
unique: bool = False,
|
97
82
|
) -> str:
|
98
|
-
"""Generate running ID for able to tracking. This
|
83
|
+
"""Generate running ID for able to tracking. This generates process use `md5`
|
99
84
|
algorithm function if ``WORKFLOW_CORE_WORKFLOW_ID_SIMPLE_MODE`` set to
|
100
85
|
false. But it will cut this hashing value length to 10 it the setting value
|
101
86
|
set to true.
|
@@ -104,6 +89,7 @@ def gen_id(
|
|
104
89
|
:param sensitive: A flag that convert the value to lower case before hashing
|
105
90
|
:param unique: A flag that add timestamp at microsecond level to value
|
106
91
|
before hashing.
|
92
|
+
|
107
93
|
:rtype: str
|
108
94
|
"""
|
109
95
|
if not isinstance(value, str):
|
@@ -121,84 +107,6 @@ def gen_id(
|
|
121
107
|
).hexdigest()
|
122
108
|
|
123
109
|
|
124
|
-
class TagFunc(Protocol):
|
125
|
-
"""Tag Function Protocol"""
|
126
|
-
|
127
|
-
name: str
|
128
|
-
tag: str
|
129
|
-
|
130
|
-
def __call__(self, *args, **kwargs): ... # pragma: no cov
|
131
|
-
|
132
|
-
|
133
|
-
ReturnTagFunc = Callable[P, TagFunc]
|
134
|
-
DecoratorTagFunc = Callable[[Callable[[...], Any]], ReturnTagFunc]
|
135
|
-
|
136
|
-
|
137
|
-
def tag(
|
138
|
-
name: str, alias: str | None = None
|
139
|
-
) -> DecoratorTagFunc: # pragma: no cov
|
140
|
-
"""Tag decorator function that set function attributes, ``tag`` and ``name``
|
141
|
-
for making registries variable.
|
142
|
-
|
143
|
-
:param: name: A tag name for make different use-case of a function.
|
144
|
-
:param: alias: A alias function name that keeping in registries. If this
|
145
|
-
value does not supply, it will use original function name from __name__.
|
146
|
-
:rtype: Callable[P, TagFunc]
|
147
|
-
"""
|
148
|
-
|
149
|
-
def func_internal(func: Callable[[...], Any]) -> ReturnTagFunc:
|
150
|
-
func.tag = name
|
151
|
-
func.name = alias or func.__name__.replace("_", "-")
|
152
|
-
|
153
|
-
@wraps(func)
|
154
|
-
def wrapped(*args, **kwargs):
|
155
|
-
# NOTE: Able to do anything before calling hook function.
|
156
|
-
return func(*args, **kwargs)
|
157
|
-
|
158
|
-
return wrapped
|
159
|
-
|
160
|
-
return func_internal
|
161
|
-
|
162
|
-
|
163
|
-
Registry = dict[str, Callable[[], TagFunc]]
|
164
|
-
|
165
|
-
|
166
|
-
def make_registry(submodule: str) -> dict[str, Registry]:
|
167
|
-
"""Return registries of all functions that able to called with task.
|
168
|
-
|
169
|
-
:param submodule: A module prefix that want to import registry.
|
170
|
-
:rtype: dict[str, Registry]
|
171
|
-
"""
|
172
|
-
rs: dict[str, Registry] = {}
|
173
|
-
for module in config.regis_hook:
|
174
|
-
# NOTE: try to sequential import task functions
|
175
|
-
try:
|
176
|
-
importer = import_module(f"{module}.{submodule}")
|
177
|
-
except ModuleNotFoundError:
|
178
|
-
continue
|
179
|
-
|
180
|
-
for fstr, func in inspect.getmembers(importer, inspect.isfunction):
|
181
|
-
# NOTE: check function attribute that already set tag by
|
182
|
-
# ``utils.tag`` decorator.
|
183
|
-
if not hasattr(func, "tag"):
|
184
|
-
continue
|
185
|
-
|
186
|
-
# NOTE: Create new register name if it not exists
|
187
|
-
if func.name not in rs:
|
188
|
-
rs[func.name] = {func.tag: lazy(f"{module}.{submodule}.{fstr}")}
|
189
|
-
continue
|
190
|
-
|
191
|
-
if func.tag in rs[func.name]:
|
192
|
-
raise ValueError(
|
193
|
-
f"The tag {func.tag!r} already exists on "
|
194
|
-
f"{module}.{submodule}, you should change this tag name or "
|
195
|
-
f"change it func name."
|
196
|
-
)
|
197
|
-
rs[func.name][func.tag] = lazy(f"{module}.{submodule}.{fstr}")
|
198
|
-
|
199
|
-
return rs
|
200
|
-
|
201
|
-
|
202
110
|
def make_exec(path: str | Path) -> None:
|
203
111
|
"""Change mode of file to be executable file.
|
204
112
|
|
@@ -208,314 +116,13 @@ def make_exec(path: str | Path) -> None:
|
|
208
116
|
f.chmod(f.stat().st_mode | stat.S_IEXEC)
|
209
117
|
|
210
118
|
|
211
|
-
|
212
|
-
"abs": abs,
|
213
|
-
"str": str,
|
214
|
-
"int": int,
|
215
|
-
"title": lambda x: x.title(),
|
216
|
-
"upper": lambda x: x.upper(),
|
217
|
-
"lower": lambda x: x.lower(),
|
218
|
-
"rstr": [str, repr],
|
219
|
-
}
|
220
|
-
|
221
|
-
|
222
|
-
class FilterFunc(Protocol):
|
223
|
-
"""Tag Function Protocol. This protocol that use to represent any callable
|
224
|
-
object that able to access the name attribute.
|
225
|
-
"""
|
226
|
-
|
227
|
-
name: str
|
228
|
-
|
229
|
-
def __call__(self, *args, **kwargs): ... # pragma: no cov
|
230
|
-
|
231
|
-
|
232
|
-
def custom_filter(name: str) -> Callable[P, FilterFunc]:
|
233
|
-
"""Custom filter decorator function that set function attributes, ``filter``
|
234
|
-
for making filter registries variable.
|
235
|
-
|
236
|
-
:param: name: A filter name for make different use-case of a function.
|
237
|
-
:rtype: Callable[P, FilterFunc]
|
238
|
-
"""
|
239
|
-
|
240
|
-
def func_internal(func: Callable[[...], Any]) -> FilterFunc:
|
241
|
-
func.filter = name
|
242
|
-
|
243
|
-
@wraps(func)
|
244
|
-
def wrapped(*args, **kwargs):
|
245
|
-
# NOTE: Able to do anything before calling custom filter function.
|
246
|
-
return func(*args, **kwargs)
|
247
|
-
|
248
|
-
return wrapped
|
249
|
-
|
250
|
-
return func_internal
|
251
|
-
|
252
|
-
|
253
|
-
FilterRegistry = Union[FilterFunc, Callable[[...], Any]]
|
254
|
-
|
255
|
-
|
256
|
-
def make_filter_registry() -> dict[str, FilterRegistry]:
|
257
|
-
"""Return registries of all functions that able to called with task.
|
258
|
-
|
259
|
-
:rtype: dict[str, Registry]
|
260
|
-
"""
|
261
|
-
rs: dict[str, Registry] = {}
|
262
|
-
for module in config.regis_filter:
|
263
|
-
# NOTE: try to sequential import task functions
|
264
|
-
try:
|
265
|
-
importer = import_module(module)
|
266
|
-
except ModuleNotFoundError:
|
267
|
-
continue
|
268
|
-
|
269
|
-
for fstr, func in inspect.getmembers(importer, inspect.isfunction):
|
270
|
-
# NOTE: check function attribute that already set tag by
|
271
|
-
# ``utils.tag`` decorator.
|
272
|
-
if not hasattr(func, "filter"):
|
273
|
-
continue
|
274
|
-
|
275
|
-
rs[func.filter] = import_string(f"{module}.{fstr}")
|
276
|
-
|
277
|
-
rs.update(FILTERS)
|
278
|
-
return rs
|
279
|
-
|
280
|
-
|
281
|
-
def get_args_const(
|
282
|
-
expr: str,
|
283
|
-
) -> tuple[str, list[Constant], dict[str, Constant]]:
|
284
|
-
"""Get arguments and keyword-arguments from function calling string.
|
285
|
-
|
286
|
-
:rtype: tuple[str, list[Constant], dict[str, Constant]]
|
287
|
-
"""
|
288
|
-
try:
|
289
|
-
mod: Module = parse(expr)
|
290
|
-
except SyntaxError:
|
291
|
-
raise UtilException(
|
292
|
-
f"Post-filter: {expr} does not valid because it raise syntax error."
|
293
|
-
) from None
|
294
|
-
|
295
|
-
body: list[Expr] = mod.body
|
296
|
-
if len(body) > 1:
|
297
|
-
raise UtilException(
|
298
|
-
"Post-filter function should be only one calling per workflow."
|
299
|
-
)
|
300
|
-
|
301
|
-
caller: Union[Name, Call]
|
302
|
-
if isinstance((caller := body[0].value), Name):
|
303
|
-
return caller.id, [], {}
|
304
|
-
elif not isinstance(caller, Call):
|
305
|
-
raise UtilException(
|
306
|
-
f"Get arguments does not support for caller type: {type(caller)}"
|
307
|
-
)
|
308
|
-
|
309
|
-
name: Name = caller.func
|
310
|
-
args: list[Constant] = caller.args
|
311
|
-
keywords: dict[str, Constant] = {k.arg: k.value for k in caller.keywords}
|
312
|
-
|
313
|
-
if any(not isinstance(i, Constant) for i in args):
|
314
|
-
raise UtilException(f"Argument of {expr} should be constant.")
|
315
|
-
|
316
|
-
if any(not isinstance(i, Constant) for i in keywords.values()):
|
317
|
-
raise UtilException(f"Keyword argument of {expr} should be constant.")
|
318
|
-
|
319
|
-
return name.id, args, keywords
|
320
|
-
|
321
|
-
|
322
|
-
def get_args_from_filter(
|
323
|
-
ft: str,
|
324
|
-
filters: dict[str, FilterRegistry],
|
325
|
-
) -> tuple[str, FilterRegistry, list[Any], dict[Any, Any]]: # pragma: no cov
|
326
|
-
"""Get arguments and keyword-arguments from filter function calling string.
|
327
|
-
and validate it with the filter functions mapping dict.
|
328
|
-
"""
|
329
|
-
func_name, _args, _kwargs = get_args_const(ft)
|
330
|
-
args: list[Any] = [arg.value for arg in _args]
|
331
|
-
kwargs: dict[Any, Any] = {k: v.value for k, v in _kwargs.items()}
|
332
|
-
|
333
|
-
if func_name not in filters:
|
334
|
-
raise UtilException(
|
335
|
-
f"The post-filter: {func_name!r} does not support yet."
|
336
|
-
)
|
337
|
-
|
338
|
-
if isinstance((f_func := filters[func_name]), list) and (args or kwargs):
|
339
|
-
raise UtilException(
|
340
|
-
"Chain filter function does not support for passing arguments."
|
341
|
-
)
|
342
|
-
|
343
|
-
return func_name, f_func, args, kwargs
|
344
|
-
|
345
|
-
|
346
|
-
@custom_filter("fmt") # pragma: no cov
|
347
|
-
def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
348
|
-
"""Format datetime object to string with the format.
|
349
|
-
|
350
|
-
:param value: A datetime value that want to format to string value.
|
351
|
-
:param fmt: A format string pattern that passing to the `dt.strftime`
|
352
|
-
method.
|
353
|
-
|
354
|
-
:rtype: str
|
355
|
-
"""
|
356
|
-
if isinstance(value, datetime):
|
357
|
-
return value.strftime(fmt)
|
358
|
-
raise UtilException(
|
359
|
-
"This custom function should pass input value with datetime type."
|
360
|
-
)
|
361
|
-
|
362
|
-
|
363
|
-
def map_post_filter(
|
364
|
-
value: T,
|
365
|
-
post_filter: list[str],
|
366
|
-
filters: dict[str, FilterRegistry],
|
367
|
-
) -> T:
|
368
|
-
"""Mapping post-filter to value with sequence list of filter function name
|
369
|
-
that will get from the filter registry.
|
370
|
-
|
371
|
-
:param value: A string value that want to mapped with filter function.
|
372
|
-
:param post_filter: A list of post-filter function name.
|
373
|
-
:param filters: A filter registry.
|
374
|
-
|
375
|
-
:rtype: T
|
376
|
-
"""
|
377
|
-
for ft in post_filter:
|
378
|
-
func_name, f_func, args, kwargs = get_args_from_filter(ft, filters)
|
379
|
-
try:
|
380
|
-
if isinstance(f_func, list):
|
381
|
-
for func in f_func:
|
382
|
-
value: T = func(value)
|
383
|
-
else:
|
384
|
-
value: T = f_func(value, *args, **kwargs)
|
385
|
-
except UtilException as err:
|
386
|
-
logger.warning(str(err))
|
387
|
-
raise
|
388
|
-
except Exception as err:
|
389
|
-
logger.warning(str(err))
|
390
|
-
raise UtilException(
|
391
|
-
f"The post-filter function: {func_name} does not fit with "
|
392
|
-
f"{value} (type: {type(value).__name__})."
|
393
|
-
) from None
|
394
|
-
return value
|
395
|
-
|
396
|
-
|
397
|
-
def not_in_template(value: Any, *, not_in: str = "matrix.") -> bool:
|
398
|
-
"""Check value should not pass template with not_in value prefix.
|
399
|
-
|
400
|
-
:param value: A value that want to find parameter template prefix.
|
401
|
-
:param not_in: The not in string that use in the `.startswith` function.
|
402
|
-
:rtype: bool
|
403
|
-
"""
|
404
|
-
if isinstance(value, dict):
|
405
|
-
return any(not_in_template(value[k], not_in=not_in) for k in value)
|
406
|
-
elif isinstance(value, (list, tuple, set)):
|
407
|
-
return any(not_in_template(i, not_in=not_in) for i in value)
|
408
|
-
elif not isinstance(value, str):
|
409
|
-
return False
|
410
|
-
return any(
|
411
|
-
(not found.caller.strip().startswith(not_in))
|
412
|
-
for found in Re.finditer_caller(value.strip())
|
413
|
-
)
|
414
|
-
|
415
|
-
|
416
|
-
def has_template(value: Any) -> bool:
|
417
|
-
"""Check value include templating string.
|
418
|
-
|
419
|
-
:param value: A value that want to find parameter template.
|
420
|
-
:rtype: bool
|
421
|
-
"""
|
422
|
-
if isinstance(value, dict):
|
423
|
-
return any(has_template(value[k]) for k in value)
|
424
|
-
elif isinstance(value, (list, tuple, set)):
|
425
|
-
return any(has_template(i) for i in value)
|
426
|
-
elif not isinstance(value, str):
|
427
|
-
return False
|
428
|
-
return bool(Re.RE_CALLER.findall(value.strip()))
|
429
|
-
|
430
|
-
|
431
|
-
def str2template(
|
432
|
-
value: str,
|
433
|
-
params: DictData,
|
434
|
-
*,
|
435
|
-
filters: dict[str, FilterRegistry] | None = None,
|
436
|
-
) -> Any:
|
437
|
-
"""(Sub-function) Pass param to template string that can search by
|
438
|
-
``RE_CALLER`` regular expression.
|
439
|
-
|
440
|
-
The getter value that map a template should have typing support align
|
441
|
-
with the workflow parameter types that is `str`, `int`, `datetime`, and
|
442
|
-
`list`.
|
443
|
-
|
444
|
-
:param value: A string value that want to mapped with an params
|
445
|
-
:param params: A parameter value that getting with matched regular
|
446
|
-
expression.
|
447
|
-
:param filters:
|
448
|
-
"""
|
449
|
-
filters: dict[str, FilterRegistry] = filters or make_filter_registry()
|
450
|
-
|
451
|
-
# NOTE: remove space before and after this string value.
|
452
|
-
value: str = value.strip()
|
453
|
-
for found in Re.finditer_caller(value):
|
454
|
-
# NOTE:
|
455
|
-
# Get caller and filter values that setting inside;
|
456
|
-
#
|
457
|
-
# ... ``${{ <caller-value> [ | <filter-value>] ... }}``
|
458
|
-
#
|
459
|
-
caller: str = found.caller
|
460
|
-
pfilter: list[str] = [
|
461
|
-
i.strip()
|
462
|
-
for i in (found.post_filters.strip().removeprefix("|").split("|"))
|
463
|
-
if i != ""
|
464
|
-
]
|
465
|
-
if not hasdot(caller, params):
|
466
|
-
raise UtilException(f"The params does not set caller: {caller!r}.")
|
467
|
-
|
468
|
-
# NOTE: from validate step, it guarantee that caller exists in params.
|
469
|
-
getter: Any = getdot(caller, params)
|
470
|
-
|
471
|
-
# NOTE:
|
472
|
-
# If type of getter caller is not string type and it does not use to
|
473
|
-
# concat other string value, it will return origin value from the
|
474
|
-
# ``getdot`` function.
|
475
|
-
if value.replace(found.full, "", 1) == "":
|
476
|
-
return map_post_filter(getter, pfilter, filters=filters)
|
477
|
-
|
478
|
-
# NOTE: map post-filter function.
|
479
|
-
getter: Any = map_post_filter(getter, pfilter, filters=filters)
|
480
|
-
if not isinstance(getter, str):
|
481
|
-
getter: str = str(getter)
|
482
|
-
|
483
|
-
value: str = value.replace(found.full, getter, 1)
|
484
|
-
|
485
|
-
return search_env_replace(value)
|
486
|
-
|
487
|
-
|
488
|
-
def param2template(
|
489
|
-
value: Any,
|
490
|
-
params: DictData,
|
491
|
-
) -> Any:
|
492
|
-
"""Pass param to template string that can search by ``RE_CALLER`` regular
|
493
|
-
expression.
|
494
|
-
|
495
|
-
:param value: A value that want to mapped with an params
|
496
|
-
:param params: A parameter value that getting with matched regular
|
497
|
-
expression.
|
498
|
-
|
499
|
-
:rtype: Any
|
500
|
-
:returns: An any getter value from the params input.
|
501
|
-
"""
|
502
|
-
filters: dict[str, FilterRegistry] = make_filter_registry()
|
503
|
-
if isinstance(value, dict):
|
504
|
-
return {k: param2template(value[k], params) for k in value}
|
505
|
-
elif isinstance(value, (list, tuple, set)):
|
506
|
-
return type(value)([param2template(i, params) for i in value])
|
507
|
-
elif not isinstance(value, str):
|
508
|
-
return value
|
509
|
-
return str2template(value, params, filters=filters)
|
510
|
-
|
511
|
-
|
512
|
-
def filter_func(value: Any) -> Any:
|
119
|
+
def filter_func(value: T) -> T:
|
513
120
|
"""Filter out an own created function of any value of mapping context by
|
514
121
|
replacing it to its function name. If it is built-in function, it does not
|
515
122
|
have any changing.
|
516
123
|
|
517
124
|
:param value: A value context data that want to filter out function value.
|
518
|
-
:type: The same type of
|
125
|
+
:type: The same type of input ``value``.
|
519
126
|
"""
|
520
127
|
if isinstance(value, dict):
|
521
128
|
return {k: filter_func(value[k]) for k in value}
|
@@ -523,8 +130,8 @@ def filter_func(value: Any) -> Any:
|
|
523
130
|
return type(value)([filter_func(i) for i in value])
|
524
131
|
|
525
132
|
if isfunction(value):
|
526
|
-
# NOTE: If it
|
527
|
-
#
|
133
|
+
# NOTE: If it wants to improve to get this function, it is able to save
|
134
|
+
# to some global memory storage.
|
528
135
|
# ---
|
529
136
|
# >>> GLOBAL_DICT[value.__name__] = value
|
530
137
|
#
|
@@ -540,6 +147,10 @@ def dash2underscore(
|
|
540
147
|
) -> DictData:
|
541
148
|
"""Change key name that has dash to underscore.
|
542
149
|
|
150
|
+
:param key
|
151
|
+
:param values
|
152
|
+
:param fixed
|
153
|
+
|
543
154
|
:rtype: DictData
|
544
155
|
"""
|
545
156
|
if key in values:
|
@@ -550,6 +161,8 @@ def dash2underscore(
|
|
550
161
|
def cross_product(matrix: Matrix) -> Iterator[DictData]:
|
551
162
|
"""Iterator of products value from matrix.
|
552
163
|
|
164
|
+
:param matrix:
|
165
|
+
|
553
166
|
:rtype: Iterator[DictData]
|
554
167
|
"""
|
555
168
|
yield from (
|
@@ -569,6 +182,11 @@ def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
|
|
569
182
|
['A', 'B', 'C']
|
570
183
|
['D', 'E', 'F']
|
571
184
|
['G']
|
185
|
+
|
186
|
+
:param iterable:
|
187
|
+
:param n:
|
188
|
+
|
189
|
+
:rtype: Iterator[Any]
|
572
190
|
"""
|
573
191
|
if n < 1:
|
574
192
|
raise ValueError("n must be at least one")
|
@@ -583,7 +201,7 @@ def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
|
|
583
201
|
yield chain((first_el,), chunk_it)
|
584
202
|
|
585
203
|
|
586
|
-
def cut_id(run_id: str, *, num: int = 6):
|
204
|
+
def cut_id(run_id: str, *, num: int = 6) -> str:
|
587
205
|
"""Cutting running ID with length.
|
588
206
|
|
589
207
|
Example:
|
@@ -592,6 +210,7 @@ def cut_id(run_id: str, *, num: int = 6):
|
|
592
210
|
|
593
211
|
:param run_id:
|
594
212
|
:param num:
|
595
|
-
|
213
|
+
|
214
|
+
:rtype: str
|
596
215
|
"""
|
597
216
|
return run_id[-num:]
|
ddeutil/workflow/workflow.py
CHANGED
@@ -48,12 +48,11 @@ from .exceptions import JobException, WorkflowException
|
|
48
48
|
from .job import Job
|
49
49
|
from .params import Param
|
50
50
|
from .result import Result
|
51
|
+
from .templates import has_template, param2template
|
51
52
|
from .utils import (
|
52
53
|
cut_id,
|
53
54
|
gen_id,
|
54
55
|
get_dt_now,
|
55
|
-
has_template,
|
56
|
-
param2template,
|
57
56
|
wait_a_minute,
|
58
57
|
)
|
59
58
|
|
@@ -139,7 +138,7 @@ class WorkflowQueue:
|
|
139
138
|
"""Construct WorkflowQueue object from an input queue value that passing
|
140
139
|
with list of datetime or list of WorkflowRelease.
|
141
140
|
|
142
|
-
:raise TypeError: If the type of
|
141
|
+
:raise TypeError: If the type of input queue does not valid.
|
143
142
|
|
144
143
|
:rtype: Self
|
145
144
|
"""
|
@@ -227,9 +226,9 @@ class WorkflowQueue:
|
|
227
226
|
class Workflow(BaseModel):
|
228
227
|
"""Workflow Pydantic model.
|
229
228
|
|
230
|
-
This is the main future of this project because it
|
229
|
+
This is the main future of this project because it uses to be workflow
|
231
230
|
data for running everywhere that you want or using it to scheduler task in
|
232
|
-
background. It
|
231
|
+
background. It uses lightweight coding line from Pydantic Model and enhance
|
233
232
|
execute method on it.
|
234
233
|
"""
|
235
234
|
|
@@ -318,7 +317,7 @@ class Workflow(BaseModel):
|
|
318
317
|
@model_validator(mode="before")
|
319
318
|
def __prepare_model_before__(cls, values: DictData) -> DictData:
|
320
319
|
"""Prepare the params key in the data model before validating."""
|
321
|
-
# NOTE: Prepare params type if it passing with only type value.
|
320
|
+
# NOTE: Prepare params type if it is passing with only type value.
|
322
321
|
if params := values.pop("params", {}):
|
323
322
|
values["params"] = {
|
324
323
|
p: (
|
@@ -342,7 +341,7 @@ class Workflow(BaseModel):
|
|
342
341
|
@field_validator("on", mode="after")
|
343
342
|
def __on_no_dup_and_reach_limit__(cls, value: list[On]) -> list[On]:
|
344
343
|
"""Validate the on fields should not contain duplicate values and if it
|
345
|
-
|
344
|
+
contains the every minute value more than one value, it will remove to
|
346
345
|
only one value.
|
347
346
|
|
348
347
|
:raise ValueError: If it has some duplicate value.
|
@@ -360,8 +359,8 @@ class Workflow(BaseModel):
|
|
360
359
|
# WARNING:
|
361
360
|
# if '* * * * *' in set_ons and len(set_ons) > 1:
|
362
361
|
# raise ValueError(
|
363
|
-
# "If it has every minute cronjob on value, it should
|
364
|
-
# "one value in the on field."
|
362
|
+
# "If it has every minute cronjob on value, it should have "
|
363
|
+
# "only one value in the on field."
|
365
364
|
# )
|
366
365
|
|
367
366
|
if len(set_ons) > config.max_on_per_workflow:
|
@@ -373,7 +372,7 @@ class Workflow(BaseModel):
|
|
373
372
|
|
374
373
|
@model_validator(mode="after")
|
375
374
|
def __validate_jobs_need__(self) -> Self:
|
376
|
-
"""Validate each need job in any jobs should
|
375
|
+
"""Validate each need job in any jobs should exist.
|
377
376
|
|
378
377
|
:raise WorkflowException: If it has not exists need value in this
|
379
378
|
workflow job.
|
@@ -486,7 +485,7 @@ class Workflow(BaseModel):
|
|
486
485
|
- Initialize WorkflowQueue and WorkflowRelease if they do not pass.
|
487
486
|
- Create release data for pass to parameter templating function.
|
488
487
|
- Execute this workflow with mapping release data to its parameters.
|
489
|
-
- Writing log
|
488
|
+
- Writing result log
|
490
489
|
- Remove this release on the running queue
|
491
490
|
- Push this release to complete queue
|
492
491
|
|
@@ -592,10 +591,10 @@ class Workflow(BaseModel):
|
|
592
591
|
"""Generate queue of datetime from the cron runner that initialize from
|
593
592
|
the on field. with offset value.
|
594
593
|
|
595
|
-
:param offset:
|
594
|
+
:param offset: An offset in second unit for time travel.
|
596
595
|
:param end_date: An end datetime object.
|
597
596
|
:param queue: A workflow queue object.
|
598
|
-
:param log: A log class that want to
|
597
|
+
:param log: A log class that want to make log object.
|
599
598
|
:param force_run: A flag that allow to release workflow if the log with
|
600
599
|
that release was pointed.
|
601
600
|
|
@@ -697,7 +696,7 @@ class Workflow(BaseModel):
|
|
697
696
|
start_date: datetime = current_date
|
698
697
|
offset: float = 0
|
699
698
|
|
700
|
-
# NOTE: End date is
|
699
|
+
# NOTE: End date is using to stop generate queue with an input periods
|
701
700
|
# value.
|
702
701
|
end_date: datetime = start_date + timedelta(minutes=periods)
|
703
702
|
|
@@ -813,7 +812,7 @@ class Workflow(BaseModel):
|
|
813
812
|
:param params: A params that was parameterized from workflow execution.
|
814
813
|
:param run_id: A workflow running ID for this job execution.
|
815
814
|
:param raise_error: A flag that raise error instead catching to result
|
816
|
-
if it
|
815
|
+
if it gets exception from job execution.
|
817
816
|
|
818
817
|
:rtype: Result
|
819
818
|
:return: Return the result object that receive the job execution result
|
@@ -869,8 +868,8 @@ class Workflow(BaseModel):
|
|
869
868
|
"""Execute workflow with passing a dynamic parameters to all jobs that
|
870
869
|
included in this workflow model with ``jobs`` field.
|
871
870
|
|
872
|
-
The result of execution process for each
|
873
|
-
workflow will
|
871
|
+
The result of execution process for each job and stages on this
|
872
|
+
workflow will keep in dict which able to catch out with all jobs and
|
874
873
|
stages by dot annotation.
|
875
874
|
|
876
875
|
For example, when I want to use the output from previous stage, I
|
@@ -908,8 +907,8 @@ class Workflow(BaseModel):
|
|
908
907
|
)
|
909
908
|
return rs.catch(status=0, context=params)
|
910
909
|
|
911
|
-
# NOTE: Create a job queue that keep the job that want to
|
912
|
-
#
|
910
|
+
# NOTE: Create a job queue that keep the job that want to run after
|
911
|
+
# its dependency condition.
|
913
912
|
jq: Queue = Queue()
|
914
913
|
for job_id in self.jobs:
|
915
914
|
jq.put(job_id)
|
@@ -968,7 +967,7 @@ class Workflow(BaseModel):
|
|
968
967
|
|
969
968
|
:param context: A context workflow data that want to downstream passing.
|
970
969
|
:param ts: A start timestamp that use for checking execute time should
|
971
|
-
|
970
|
+
time out.
|
972
971
|
:param job_queue: A job queue object.
|
973
972
|
:param timeout: A second value unit that bounding running time.
|
974
973
|
:param thread_timeout: A timeout to waiting all futures complete.
|
@@ -1065,7 +1064,7 @@ class Workflow(BaseModel):
|
|
1065
1064
|
|
1066
1065
|
:param context: A context workflow data that want to downstream passing.
|
1067
1066
|
:param ts: A start timestamp that use for checking execute time should
|
1068
|
-
|
1067
|
+
time out.
|
1069
1068
|
:param timeout: A second value unit that bounding running time.
|
1070
1069
|
|
1071
1070
|
:rtype: DictData
|
@@ -1091,7 +1090,7 @@ class Workflow(BaseModel):
|
|
1091
1090
|
continue
|
1092
1091
|
|
1093
1092
|
# NOTE: Start workflow job execution with deep copy context data
|
1094
|
-
# before release. This job execution process will
|
1093
|
+
# before release. This job execution process will run until
|
1095
1094
|
# done before checking all execution timeout or not.
|
1096
1095
|
#
|
1097
1096
|
# {
|
@@ -1183,7 +1182,7 @@ class WorkflowTask:
|
|
1183
1182
|
|
1184
1183
|
:param end_date: An end datetime object.
|
1185
1184
|
:param queue: A workflow queue object.
|
1186
|
-
:param log: A log class that want to
|
1185
|
+
:param log: A log class that want to make log object.
|
1187
1186
|
:param force_run: A flag that allow to release workflow if the log with
|
1188
1187
|
that release was pointed.
|
1189
1188
|
|