ddeutil-workflow 0.0.15__py3-none-any.whl → 0.0.17__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 → __cron.py} +12 -6
- ddeutil/workflow/__init__.py +1 -0
- ddeutil/workflow/__types.py +18 -6
- ddeutil/workflow/api.py +3 -5
- ddeutil/workflow/cli.py +2 -6
- ddeutil/workflow/conf.py +441 -3
- ddeutil/workflow/job.py +119 -62
- ddeutil/workflow/on.py +11 -8
- ddeutil/workflow/repeat.py +2 -6
- ddeutil/workflow/route.py +4 -12
- ddeutil/workflow/scheduler.py +71 -54
- ddeutil/workflow/stage.py +79 -43
- ddeutil/workflow/utils.py +96 -283
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/METADATA +44 -25
- ddeutil_workflow-0.0.17.dist-info/RECORD +21 -0
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/WHEEL +1 -1
- ddeutil/workflow/log.py +0 -198
- ddeutil_workflow-0.0.15.dist-info/RECORD +0 -22
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.15.dist-info → ddeutil_workflow-0.0.17.dist-info}/top_level.txt +0 -0
ddeutil/workflow/utils.py
CHANGED
@@ -7,7 +7,6 @@ from __future__ import annotations
|
|
7
7
|
|
8
8
|
import inspect
|
9
9
|
import logging
|
10
|
-
import os
|
11
10
|
import stat
|
12
11
|
import time
|
13
12
|
from abc import ABC, abstractmethod
|
@@ -15,7 +14,7 @@ from ast import Call, Constant, Expr, Module, Name, parse
|
|
15
14
|
from collections.abc import Iterator
|
16
15
|
from dataclasses import field
|
17
16
|
from datetime import date, datetime
|
18
|
-
from functools import
|
17
|
+
from functools import wraps
|
19
18
|
from hashlib import md5
|
20
19
|
from importlib import import_module
|
21
20
|
from inspect import isfunction
|
@@ -30,12 +29,10 @@ try:
|
|
30
29
|
except ImportError:
|
31
30
|
from typing_extensions import ParamSpec
|
32
31
|
|
33
|
-
from ddeutil.core import getdot, hasdot, hash_str, import_string, lazy
|
34
|
-
from ddeutil.io import
|
35
|
-
from
|
36
|
-
from pydantic import BaseModel, ConfigDict, Field
|
32
|
+
from ddeutil.core import getdot, hasdot, hash_str, import_string, lazy
|
33
|
+
from ddeutil.io import search_env_replace
|
34
|
+
from pydantic import BaseModel, Field
|
37
35
|
from pydantic.dataclasses import dataclass
|
38
|
-
from pydantic.functional_serializers import field_serializer
|
39
36
|
from pydantic.functional_validators import model_validator
|
40
37
|
from typing_extensions import Self
|
41
38
|
|
@@ -43,6 +40,7 @@ from .__types import DictData, Matrix, Re
|
|
43
40
|
from .conf import config
|
44
41
|
from .exceptions import ParamValueException, UtilException
|
45
42
|
|
43
|
+
T = TypeVar("T")
|
46
44
|
P = ParamSpec("P")
|
47
45
|
AnyModel = TypeVar("AnyModel", bound=BaseModel)
|
48
46
|
AnyModelType = type[AnyModel]
|
@@ -50,16 +48,30 @@ AnyModelType = type[AnyModel]
|
|
50
48
|
logger = logging.getLogger("ddeutil.workflow")
|
51
49
|
|
52
50
|
|
53
|
-
def
|
51
|
+
def get_dt_now(tz: ZoneInfo | None = None) -> datetime: # pragma: no cov
|
52
|
+
"""Return the current datetime object.
|
53
|
+
|
54
|
+
:param tz:
|
55
|
+
:return: The current datetime object that use an input timezone or UTC.
|
56
|
+
"""
|
57
|
+
return datetime.now(tz=(tz or ZoneInfo("UTC")))
|
58
|
+
|
59
|
+
|
60
|
+
def get_diff_sec(
|
61
|
+
dt: datetime, tz: ZoneInfo | None = None
|
62
|
+
) -> int: # pragma: no cov
|
54
63
|
"""Return second value that come from diff of an input datetime and the
|
55
64
|
current datetime with specific timezone.
|
65
|
+
|
66
|
+
:param dt:
|
67
|
+
:param tz:
|
56
68
|
"""
|
57
69
|
return round(
|
58
70
|
(dt - datetime.now(tz=(tz or ZoneInfo("UTC")))).total_seconds()
|
59
71
|
)
|
60
72
|
|
61
73
|
|
62
|
-
def delay(second: float = 0) -> None:
|
74
|
+
def delay(second: float = 0) -> None: # pragma: no cov
|
63
75
|
"""Delay time that use time.sleep with random second value between
|
64
76
|
0.00 - 0.99 seconds.
|
65
77
|
|
@@ -68,203 +80,6 @@ def delay(second: float = 0) -> None:
|
|
68
80
|
time.sleep(second + randrange(0, 99, step=10) / 100)
|
69
81
|
|
70
82
|
|
71
|
-
class Engine(BaseModel):
|
72
|
-
"""Engine Model"""
|
73
|
-
|
74
|
-
paths: PathData = Field(default_factory=PathData)
|
75
|
-
registry: list[str] = Field(
|
76
|
-
default_factory=lambda: ["ddeutil.workflow"], # pragma: no cover
|
77
|
-
)
|
78
|
-
registry_filter: list[str] = Field(
|
79
|
-
default_factory=lambda: ["ddeutil.workflow.utils"], # pragma: no cover
|
80
|
-
)
|
81
|
-
|
82
|
-
@model_validator(mode="before")
|
83
|
-
def __prepare_registry(cls, values: DictData) -> DictData:
|
84
|
-
"""Prepare registry value that passing with string type. It convert the
|
85
|
-
string type to list of string.
|
86
|
-
"""
|
87
|
-
if (_regis := values.get("registry")) and isinstance(_regis, str):
|
88
|
-
values["registry"] = [_regis]
|
89
|
-
if (_regis_filter := values.get("registry_filter")) and isinstance(
|
90
|
-
_regis, str
|
91
|
-
):
|
92
|
-
values["registry_filter"] = [_regis_filter]
|
93
|
-
return values
|
94
|
-
|
95
|
-
|
96
|
-
class CoreConf(BaseModel):
|
97
|
-
"""Core Config Model"""
|
98
|
-
|
99
|
-
model_config = ConfigDict(arbitrary_types_allowed=True)
|
100
|
-
|
101
|
-
tz: ZoneInfo = Field(default_factory=lambda: ZoneInfo("UTC"))
|
102
|
-
|
103
|
-
|
104
|
-
class ConfParams(BaseModel):
|
105
|
-
"""Params Model"""
|
106
|
-
|
107
|
-
engine: Engine = Field(
|
108
|
-
default_factory=Engine,
|
109
|
-
description="A engine mapping values.",
|
110
|
-
)
|
111
|
-
core: CoreConf = Field(
|
112
|
-
default_factory=CoreConf,
|
113
|
-
description="A core config value",
|
114
|
-
)
|
115
|
-
|
116
|
-
|
117
|
-
def load_config() -> ConfParams:
|
118
|
-
"""Load Config data from ``workflows-conf.yaml`` file.
|
119
|
-
|
120
|
-
Configuration Docs:
|
121
|
-
---
|
122
|
-
:var engine.registry:
|
123
|
-
:var engine.registry_filter:
|
124
|
-
:var paths.root:
|
125
|
-
:var paths.conf:
|
126
|
-
"""
|
127
|
-
root_path: str = os.getenv("WORKFLOW_ROOT_PATH", ".")
|
128
|
-
|
129
|
-
regis: list[str] = ["ddeutil.workflow"]
|
130
|
-
if regis_env := os.getenv("WORKFLOW_CORE_REGISTRY"):
|
131
|
-
regis = [r.strip() for r in regis_env.split(",")]
|
132
|
-
|
133
|
-
regis_filter: list[str] = ["ddeutil.workflow.utils"]
|
134
|
-
if regis_filter_env := os.getenv("WORKFLOW_CORE_REGISTRY_FILTER"):
|
135
|
-
regis_filter = [r.strip() for r in regis_filter_env.split(",")]
|
136
|
-
|
137
|
-
conf_path: str = (
|
138
|
-
f"{root_path}/{conf_env}"
|
139
|
-
if (conf_env := os.getenv("WORKFLOW_CORE_PATH_CONF"))
|
140
|
-
else None
|
141
|
-
)
|
142
|
-
return ConfParams.model_validate(
|
143
|
-
obj={
|
144
|
-
"engine": {
|
145
|
-
"registry": regis,
|
146
|
-
"registry_filter": regis_filter,
|
147
|
-
"paths": {
|
148
|
-
"root": root_path,
|
149
|
-
"conf": conf_path,
|
150
|
-
},
|
151
|
-
},
|
152
|
-
}
|
153
|
-
)
|
154
|
-
|
155
|
-
|
156
|
-
class SimLoad:
|
157
|
-
"""Simple Load Object that will search config data by given some identity
|
158
|
-
value like name of workflow or on.
|
159
|
-
|
160
|
-
:param name: A name of config data that will read by Yaml Loader object.
|
161
|
-
:param params: A Params model object.
|
162
|
-
:param externals: An external parameters
|
163
|
-
|
164
|
-
Noted:
|
165
|
-
|
166
|
-
The config data should have ``type`` key for modeling validation that
|
167
|
-
make this loader know what is config should to do pass to.
|
168
|
-
|
169
|
-
... <identity-key>:
|
170
|
-
... type: <importable-object>
|
171
|
-
... <key-data>: <value-data>
|
172
|
-
... ...
|
173
|
-
|
174
|
-
"""
|
175
|
-
|
176
|
-
def __init__(
|
177
|
-
self,
|
178
|
-
name: str,
|
179
|
-
params: ConfParams,
|
180
|
-
externals: DictData | None = None,
|
181
|
-
) -> None:
|
182
|
-
self.data: DictData = {}
|
183
|
-
for file in PathSearch(params.engine.paths.conf).files:
|
184
|
-
if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
|
185
|
-
data := YamlFlResolve(file).read().get(name, {})
|
186
|
-
):
|
187
|
-
self.data = data
|
188
|
-
|
189
|
-
# VALIDATE: check the data that reading should not empty.
|
190
|
-
if not self.data:
|
191
|
-
raise ValueError(f"Config {name!r} does not found on conf path")
|
192
|
-
|
193
|
-
self.conf_params: ConfParams = params
|
194
|
-
self.externals: DictData = externals or {}
|
195
|
-
self.data.update(self.externals)
|
196
|
-
|
197
|
-
@classmethod
|
198
|
-
def finds(
|
199
|
-
cls,
|
200
|
-
obj: object,
|
201
|
-
params: ConfParams,
|
202
|
-
*,
|
203
|
-
include: list[str] | None = None,
|
204
|
-
exclude: list[str] | None = None,
|
205
|
-
) -> Iterator[tuple[str, DictData]]:
|
206
|
-
"""Find all data that match with object type in config path. This class
|
207
|
-
method can use include and exclude list of identity name for filter and
|
208
|
-
adds-on.
|
209
|
-
|
210
|
-
:param obj:
|
211
|
-
:param params:
|
212
|
-
:param include:
|
213
|
-
:param exclude:
|
214
|
-
:rtype: Iterator[tuple[str, DictData]]
|
215
|
-
"""
|
216
|
-
exclude: list[str] = exclude or []
|
217
|
-
for file in PathSearch(params.engine.paths.conf).files:
|
218
|
-
if any(file.suffix.endswith(s) for s in (".yml", ".yaml")) and (
|
219
|
-
values := YamlFlResolve(file).read()
|
220
|
-
):
|
221
|
-
for key, data in values.items():
|
222
|
-
if key in exclude:
|
223
|
-
continue
|
224
|
-
if issubclass(get_type(data["type"], params), obj) and (
|
225
|
-
include is None or all(i in data for i in include)
|
226
|
-
):
|
227
|
-
yield key, data
|
228
|
-
|
229
|
-
@cached_property
|
230
|
-
def type(self) -> AnyModelType:
|
231
|
-
"""Return object of string type which implement on any registry. The
|
232
|
-
object type.
|
233
|
-
|
234
|
-
:rtype: AnyModelType
|
235
|
-
"""
|
236
|
-
if not (_typ := self.data.get("type")):
|
237
|
-
raise ValueError(
|
238
|
-
f"the 'type' value: {_typ} does not exists in config data."
|
239
|
-
)
|
240
|
-
return get_type(_typ, self.conf_params)
|
241
|
-
|
242
|
-
|
243
|
-
class Loader(SimLoad):
|
244
|
-
"""Loader Object that get the config `yaml` file from current path.
|
245
|
-
|
246
|
-
:param name: A name of config data that will read by Yaml Loader object.
|
247
|
-
:param externals: An external parameters
|
248
|
-
"""
|
249
|
-
|
250
|
-
@classmethod
|
251
|
-
def finds(
|
252
|
-
cls,
|
253
|
-
obj: object,
|
254
|
-
*,
|
255
|
-
include: list[str] | None = None,
|
256
|
-
exclude: list[str] | None = None,
|
257
|
-
**kwargs,
|
258
|
-
) -> DictData:
|
259
|
-
"""Override the find class method from the Simple Loader object."""
|
260
|
-
return super().finds(
|
261
|
-
obj=obj, params=load_config(), include=include, exclude=exclude
|
262
|
-
)
|
263
|
-
|
264
|
-
def __init__(self, name: str, externals: DictData) -> None:
|
265
|
-
super().__init__(name, load_config(), externals)
|
266
|
-
|
267
|
-
|
268
83
|
def gen_id(
|
269
84
|
value: Any,
|
270
85
|
*,
|
@@ -272,9 +87,9 @@ def gen_id(
|
|
272
87
|
unique: bool = False,
|
273
88
|
) -> str:
|
274
89
|
"""Generate running ID for able to tracking. This generate process use `md5`
|
275
|
-
algorithm function if ``
|
276
|
-
But it will cut this hashing value length to 10 it the setting value
|
277
|
-
true.
|
90
|
+
algorithm function if ``WORKFLOW_CORE_WORKFLOW_ID_SIMPLE_MODE`` set to
|
91
|
+
false. But it will cut this hashing value length to 10 it the setting value
|
92
|
+
set to true.
|
278
93
|
|
279
94
|
:param value: A value that want to add to prefix before hashing with md5.
|
280
95
|
:param sensitive: A flag that convert the value to lower case before hashing
|
@@ -285,7 +100,7 @@ def gen_id(
|
|
285
100
|
if not isinstance(value, str):
|
286
101
|
value: str = str(value)
|
287
102
|
|
288
|
-
if
|
103
|
+
if config.workflow_id_simple_mode:
|
289
104
|
return hash_str(f"{(value if sensitive else value.lower())}", n=10) + (
|
290
105
|
f"{datetime.now(tz=config.tz):%Y%m%d%H%M%S%f}" if unique else ""
|
291
106
|
)
|
@@ -297,40 +112,22 @@ def gen_id(
|
|
297
112
|
).hexdigest()
|
298
113
|
|
299
114
|
|
300
|
-
def get_type(t: str, params: ConfParams) -> AnyModelType:
|
301
|
-
"""Return import type from string importable value in the type key.
|
302
|
-
|
303
|
-
:param t: A importable type string.
|
304
|
-
:param params: A config parameters that use registry to search this
|
305
|
-
type.
|
306
|
-
:rtype: AnyModelType
|
307
|
-
"""
|
308
|
-
try:
|
309
|
-
# NOTE: Auto adding module prefix if it does not set
|
310
|
-
return import_string(f"ddeutil.workflow.{t}")
|
311
|
-
except ModuleNotFoundError:
|
312
|
-
for registry in params.engine.registry:
|
313
|
-
try:
|
314
|
-
return import_string(f"{registry}.{t}")
|
315
|
-
except ModuleNotFoundError:
|
316
|
-
continue
|
317
|
-
return import_string(f"{t}")
|
318
|
-
|
319
|
-
|
320
115
|
class TagFunc(Protocol):
|
321
116
|
"""Tag Function Protocol"""
|
322
117
|
|
323
118
|
name: str
|
324
119
|
tag: str
|
325
120
|
|
326
|
-
def __call__(self, *args, **kwargs): ... # pragma: no
|
121
|
+
def __call__(self, *args, **kwargs): ... # pragma: no cov
|
327
122
|
|
328
123
|
|
329
124
|
ReturnTagFunc = Callable[P, TagFunc]
|
330
125
|
DecoratorTagFunc = Callable[[Callable[[...], Any]], ReturnTagFunc]
|
331
126
|
|
332
127
|
|
333
|
-
def tag(
|
128
|
+
def tag(
|
129
|
+
name: str, alias: str | None = None
|
130
|
+
) -> DecoratorTagFunc: # pragma: no cov
|
334
131
|
"""Tag decorator function that set function attributes, ``tag`` and ``name``
|
335
132
|
for making registries variable.
|
336
133
|
|
@@ -364,7 +161,7 @@ def make_registry(submodule: str) -> dict[str, Registry]:
|
|
364
161
|
:rtype: dict[str, Registry]
|
365
162
|
"""
|
366
163
|
rs: dict[str, Registry] = {}
|
367
|
-
for module in
|
164
|
+
for module in config.regis_hook:
|
368
165
|
# NOTE: try to sequential import task functions
|
369
166
|
try:
|
370
167
|
importer = import_module(f"{module}.{submodule}")
|
@@ -396,9 +193,14 @@ def make_registry(submodule: str) -> dict[str, Registry]:
|
|
396
193
|
class BaseParam(BaseModel, ABC):
|
397
194
|
"""Base Parameter that use to make Params Model."""
|
398
195
|
|
399
|
-
desc: Optional[str] =
|
400
|
-
|
401
|
-
|
196
|
+
desc: Optional[str] = Field(
|
197
|
+
default=None, description="A description of parameter providing."
|
198
|
+
)
|
199
|
+
required: bool = Field(
|
200
|
+
default=True,
|
201
|
+
description="A require flag that force to pass this parameter value.",
|
202
|
+
)
|
203
|
+
type: str = Field(description="A type of parameter.")
|
402
204
|
|
403
205
|
@abstractmethod
|
404
206
|
def receive(self, value: Optional[Any] = None) -> Any:
|
@@ -406,19 +208,20 @@ class BaseParam(BaseModel, ABC):
|
|
406
208
|
"Receive value and validate typing before return valid value."
|
407
209
|
)
|
408
210
|
|
409
|
-
@field_serializer("type")
|
410
|
-
def __serializer_type(self, value: str) -> str:
|
411
|
-
"""Serialize the value of the type field.
|
412
|
-
|
413
|
-
:rtype: str
|
414
|
-
"""
|
415
|
-
return value
|
416
|
-
|
417
211
|
|
418
212
|
class DefaultParam(BaseParam):
|
419
|
-
"""Default Parameter that will check default if it required
|
213
|
+
"""Default Parameter that will check default if it required. This model do
|
214
|
+
not implement the receive method.
|
215
|
+
"""
|
420
216
|
|
421
|
-
|
217
|
+
required: bool = Field(
|
218
|
+
default=False,
|
219
|
+
description="A require flag for the default-able parameter value.",
|
220
|
+
)
|
221
|
+
default: Optional[str] = Field(
|
222
|
+
default=None,
|
223
|
+
description="A default value if parameter does not pass.",
|
224
|
+
)
|
422
225
|
|
423
226
|
@abstractmethod
|
424
227
|
def receive(self, value: Optional[Any] = None) -> Any:
|
@@ -426,22 +229,12 @@ class DefaultParam(BaseParam):
|
|
426
229
|
"Receive value and validate typing before return valid value."
|
427
230
|
)
|
428
231
|
|
429
|
-
@model_validator(mode="after")
|
430
|
-
def __check_default(self) -> Self:
|
431
|
-
"""Check default value should pass when it set required."""
|
432
|
-
if not self.required and self.default is None:
|
433
|
-
raise ParamValueException(
|
434
|
-
"Default should set when this parameter does not required."
|
435
|
-
)
|
436
|
-
return self
|
437
|
-
|
438
232
|
|
439
233
|
class DatetimeParam(DefaultParam):
|
440
234
|
"""Datetime parameter."""
|
441
235
|
|
442
236
|
type: Literal["datetime"] = "datetime"
|
443
|
-
|
444
|
-
default: datetime = Field(default_factory=dt_now)
|
237
|
+
default: datetime = Field(default_factory=get_dt_now)
|
445
238
|
|
446
239
|
def receive(self, value: str | datetime | date | None = None) -> datetime:
|
447
240
|
"""Receive value that match with datetime. If a input value pass with
|
@@ -463,7 +256,12 @@ class DatetimeParam(DefaultParam):
|
|
463
256
|
f"Value that want to convert to datetime does not support for "
|
464
257
|
f"type: {type(value)}"
|
465
258
|
)
|
466
|
-
|
259
|
+
try:
|
260
|
+
return datetime.fromisoformat(value)
|
261
|
+
except ValueError:
|
262
|
+
raise ParamValueException(
|
263
|
+
f"Invalid isoformat string: {value!r}"
|
264
|
+
) from None
|
467
265
|
|
468
266
|
|
469
267
|
class StrParam(DefaultParam):
|
@@ -471,7 +269,7 @@ class StrParam(DefaultParam):
|
|
471
269
|
|
472
270
|
type: Literal["str"] = "str"
|
473
271
|
|
474
|
-
def receive(self, value:
|
272
|
+
def receive(self, value: str | None = None) -> str | None:
|
475
273
|
"""Receive value that match with str.
|
476
274
|
|
477
275
|
:param value: A value that want to validate with string parameter type.
|
@@ -486,8 +284,12 @@ class IntParam(DefaultParam):
|
|
486
284
|
"""Integer parameter."""
|
487
285
|
|
488
286
|
type: Literal["int"] = "int"
|
287
|
+
default: Optional[int] = Field(
|
288
|
+
default=None,
|
289
|
+
description="A default value if parameter does not pass.",
|
290
|
+
)
|
489
291
|
|
490
|
-
def receive(self, value:
|
292
|
+
def receive(self, value: int | None = None) -> int | None:
|
491
293
|
"""Receive value that match with int.
|
492
294
|
|
493
295
|
:param value: A value that want to validate with integer parameter type.
|
@@ -498,10 +300,9 @@ class IntParam(DefaultParam):
|
|
498
300
|
if not isinstance(value, int):
|
499
301
|
try:
|
500
302
|
return int(str(value))
|
501
|
-
except
|
303
|
+
except ValueError as err:
|
502
304
|
raise ParamValueException(
|
503
|
-
f"Value
|
504
|
-
f"for type: {type(value)}"
|
305
|
+
f"Value can not convert to int, {value}, with base 10"
|
505
306
|
) from err
|
506
307
|
return value
|
507
308
|
|
@@ -510,15 +311,19 @@ class ChoiceParam(BaseParam):
|
|
510
311
|
"""Choice parameter."""
|
511
312
|
|
512
313
|
type: Literal["choice"] = "choice"
|
513
|
-
options: list[str]
|
314
|
+
options: list[str] = Field(description="A list of choice parameters.")
|
315
|
+
|
316
|
+
def receive(self, value: str | None = None) -> str:
|
317
|
+
"""Receive value that match with options.
|
514
318
|
|
515
|
-
|
516
|
-
|
319
|
+
:param value: A value that want to select from the options field.
|
320
|
+
:rtype: str
|
321
|
+
"""
|
517
322
|
# NOTE:
|
518
323
|
# Return the first value in options if does not pass any input value
|
519
324
|
if value is None:
|
520
325
|
return self.options[0]
|
521
|
-
if
|
326
|
+
if value not in self.options:
|
522
327
|
raise ParamValueException(
|
523
328
|
f"{value!r} does not match any value in choice options."
|
524
329
|
)
|
@@ -545,7 +350,7 @@ class Result:
|
|
545
350
|
|
546
351
|
status: int = field(default=2)
|
547
352
|
context: DictData = field(default_factory=dict)
|
548
|
-
start_at: datetime = field(default_factory=
|
353
|
+
start_at: datetime = field(default_factory=get_dt_now, compare=False)
|
549
354
|
end_at: Optional[datetime] = field(default=None, compare=False)
|
550
355
|
|
551
356
|
# NOTE: Ignore this field to compare another result model with __eq__.
|
@@ -577,15 +382,15 @@ class Result:
|
|
577
382
|
:param running_id: A running ID that want to update on this model.
|
578
383
|
:rtype: Self
|
579
384
|
"""
|
580
|
-
self._parent_run_id = running_id
|
385
|
+
self._parent_run_id: str = running_id
|
581
386
|
return self
|
582
387
|
|
583
388
|
@property
|
584
|
-
def parent_run_id(self):
|
389
|
+
def parent_run_id(self) -> str:
|
585
390
|
return self._parent_run_id
|
586
391
|
|
587
392
|
@property
|
588
|
-
def run_id(self):
|
393
|
+
def run_id(self) -> str:
|
589
394
|
return self._run_id
|
590
395
|
|
591
396
|
def catch(self, status: int, context: DictData) -> Self:
|
@@ -622,8 +427,8 @@ class Result:
|
|
622
427
|
self.__dict__["context"]["jobs"].update(result.context)
|
623
428
|
|
624
429
|
# NOTE: Update running ID from an incoming result.
|
625
|
-
self._parent_run_id = result.parent_run_id
|
626
|
-
self._run_id = result.run_id
|
430
|
+
self._parent_run_id: str = result.parent_run_id
|
431
|
+
self._run_id: str = result.run_id
|
627
432
|
return self
|
628
433
|
|
629
434
|
|
@@ -684,7 +489,7 @@ def make_filter_registry() -> dict[str, FilterRegistry]:
|
|
684
489
|
:rtype: dict[str, Registry]
|
685
490
|
"""
|
686
491
|
rs: dict[str, Registry] = {}
|
687
|
-
for module in
|
492
|
+
for module in config.regis_filter:
|
688
493
|
# NOTE: try to sequential import task functions
|
689
494
|
try:
|
690
495
|
importer = import_module(module)
|
@@ -716,11 +521,11 @@ def get_args_const(
|
|
716
521
|
raise UtilException(
|
717
522
|
f"Post-filter: {expr} does not valid because it raise syntax error."
|
718
523
|
) from None
|
719
|
-
body: list[Expr] = mod.body
|
720
524
|
|
525
|
+
body: list[Expr] = mod.body
|
721
526
|
if len(body) > 1:
|
722
527
|
raise UtilException(
|
723
|
-
"Post-filter function should be only one calling per
|
528
|
+
"Post-filter function should be only one calling per workflow."
|
724
529
|
)
|
725
530
|
|
726
531
|
caller: Union[Name, Call]
|
@@ -736,12 +541,15 @@ def get_args_const(
|
|
736
541
|
keywords: dict[str, Constant] = {k.arg: k.value for k in caller.keywords}
|
737
542
|
|
738
543
|
if any(not isinstance(i, Constant) for i in args):
|
739
|
-
raise UtilException("Argument should be constant.")
|
544
|
+
raise UtilException(f"Argument of {expr} should be constant.")
|
545
|
+
|
546
|
+
if any(not isinstance(i, Constant) for i in keywords.values()):
|
547
|
+
raise UtilException(f"Keyword argument of {expr} should be constant.")
|
740
548
|
|
741
549
|
return name.id, args, keywords
|
742
550
|
|
743
551
|
|
744
|
-
@custom_filter("fmt")
|
552
|
+
@custom_filter("fmt") # pragma: no cov
|
745
553
|
def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
746
554
|
"""Format datetime object to string with the format."""
|
747
555
|
if isinstance(value, datetime):
|
@@ -752,16 +560,18 @@ def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
|
752
560
|
|
753
561
|
|
754
562
|
def map_post_filter(
|
755
|
-
value:
|
563
|
+
value: T,
|
756
564
|
post_filter: list[str],
|
757
565
|
filters: dict[str, FilterRegistry],
|
758
|
-
) ->
|
566
|
+
) -> T:
|
759
567
|
"""Mapping post-filter to value with sequence list of filter function name
|
760
568
|
that will get from the filter registry.
|
761
569
|
|
762
570
|
:param value: A string value that want to mapped with filter function.
|
763
571
|
:param post_filter: A list of post-filter function name.
|
764
572
|
:param filters: A filter registry.
|
573
|
+
|
574
|
+
:rtype: T
|
765
575
|
"""
|
766
576
|
for _filter in post_filter:
|
767
577
|
func_name, _args, _kwargs = get_args_const(_filter)
|
@@ -784,6 +594,8 @@ def map_post_filter(
|
|
784
594
|
value: Any = func(value)
|
785
595
|
else:
|
786
596
|
value: Any = f_func(value, *args, **kwargs)
|
597
|
+
except UtilException:
|
598
|
+
raise
|
787
599
|
except Exception as err:
|
788
600
|
logger.warning(str(err))
|
789
601
|
raise UtilException(
|
@@ -796,8 +608,8 @@ def map_post_filter(
|
|
796
608
|
def not_in_template(value: Any, *, not_in: str = "matrix.") -> bool:
|
797
609
|
"""Check value should not pass template with not_in value prefix.
|
798
610
|
|
799
|
-
:param value:
|
800
|
-
:param not_in:
|
611
|
+
:param value: A value that want to find parameter template prefix.
|
612
|
+
:param not_in: The not in string that use in the `.startswith` function.
|
801
613
|
:rtype: bool
|
802
614
|
"""
|
803
615
|
if isinstance(value, dict):
|
@@ -815,7 +627,7 @@ def not_in_template(value: Any, *, not_in: str = "matrix.") -> bool:
|
|
815
627
|
def has_template(value: Any) -> bool:
|
816
628
|
"""Check value include templating string.
|
817
629
|
|
818
|
-
:param value:
|
630
|
+
:param value: A value that want to find parameter template.
|
819
631
|
:rtype: bool
|
820
632
|
"""
|
821
633
|
if isinstance(value, dict):
|
@@ -971,6 +783,7 @@ def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
|
|
971
783
|
"""
|
972
784
|
if n < 1:
|
973
785
|
raise ValueError("n must be at least one")
|
786
|
+
|
974
787
|
it: Iterator[Any] = iter(iterable)
|
975
788
|
while True:
|
976
789
|
chunk_it = islice(it, n)
|