ddeutil-workflow 0.0.6__py3-none-any.whl → 0.0.8__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 +26 -4
- ddeutil/workflow/__types.py +11 -1
- ddeutil/workflow/api.py +120 -0
- ddeutil/workflow/app.py +45 -0
- ddeutil/workflow/exceptions.py +3 -3
- ddeutil/workflow/log.py +79 -0
- ddeutil/workflow/pipeline.py +516 -120
- ddeutil/workflow/repeat.py +134 -0
- ddeutil/workflow/route.py +78 -0
- ddeutil/workflow/stage.py +209 -86
- ddeutil/workflow/utils.py +368 -66
- {ddeutil_workflow-0.0.6.dist-info → ddeutil_workflow-0.0.8.dist-info}/METADATA +48 -76
- ddeutil_workflow-0.0.8.dist-info/RECORD +20 -0
- {ddeutil_workflow-0.0.6.dist-info → ddeutil_workflow-0.0.8.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.6.dist-info/RECORD +0 -15
- {ddeutil_workflow-0.0.6.dist-info → ddeutil_workflow-0.0.8.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.6.dist-info → ddeutil_workflow-0.0.8.dist-info}/top_level.txt +0 -0
ddeutil/workflow/utils.py
CHANGED
@@ -6,28 +6,40 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import inspect
|
9
|
+
import logging
|
9
10
|
import os
|
10
11
|
import stat
|
11
12
|
from abc import ABC, abstractmethod
|
13
|
+
from ast import Call, Constant, Expr, Module, Name, parse
|
12
14
|
from collections.abc import Iterator
|
13
|
-
from dataclasses import dataclass, field
|
14
15
|
from datetime import date, datetime
|
15
16
|
from functools import wraps
|
16
17
|
from hashlib import md5
|
17
18
|
from importlib import import_module
|
19
|
+
from inspect import isfunction
|
18
20
|
from itertools import product
|
19
21
|
from pathlib import Path
|
20
22
|
from typing import Any, Callable, Literal, Optional, Protocol, Union
|
21
23
|
from zoneinfo import ZoneInfo
|
22
24
|
|
23
|
-
from ddeutil.core import getdot, hasdot, lazy
|
24
|
-
from ddeutil.io import PathData
|
25
|
+
from ddeutil.core import getdot, hasdot, hash_str, import_string, lazy, str2bool
|
26
|
+
from ddeutil.io import PathData, search_env_replace
|
25
27
|
from ddeutil.io.models.lineage import dt_now
|
26
|
-
from pydantic import BaseModel, Field
|
28
|
+
from pydantic import BaseModel, ConfigDict, Field
|
27
29
|
from pydantic.functional_validators import model_validator
|
28
30
|
from typing_extensions import Self
|
29
31
|
|
30
32
|
from .__types import DictData, Matrix, Re
|
33
|
+
from .exceptions import ParamValueException, UtilException
|
34
|
+
|
35
|
+
|
36
|
+
def get_diff_sec(dt: datetime, tz: ZoneInfo | None = None) -> int:
|
37
|
+
"""Return second value that come from diff of an input datetime and the
|
38
|
+
current datetime with specific timezone.
|
39
|
+
"""
|
40
|
+
return round(
|
41
|
+
(dt - datetime.now(tz=(tz or ZoneInfo("UTC")))).total_seconds()
|
42
|
+
)
|
31
43
|
|
32
44
|
|
33
45
|
class Engine(BaseModel):
|
@@ -35,9 +47,10 @@ class Engine(BaseModel):
|
|
35
47
|
|
36
48
|
paths: PathData = Field(default_factory=PathData)
|
37
49
|
registry: list[str] = Field(
|
38
|
-
default_factory=lambda: [
|
39
|
-
|
40
|
-
|
50
|
+
default_factory=lambda: ["ddeutil.workflow"], # pragma: no cover
|
51
|
+
)
|
52
|
+
registry_filter: list[str] = Field(
|
53
|
+
default_factory=lambda: ["ddeutil.workflow.utils"], # pragma: no cover
|
41
54
|
)
|
42
55
|
|
43
56
|
@model_validator(mode="before")
|
@@ -47,9 +60,21 @@ class Engine(BaseModel):
|
|
47
60
|
"""
|
48
61
|
if (_regis := values.get("registry")) and isinstance(_regis, str):
|
49
62
|
values["registry"] = [_regis]
|
63
|
+
if (_regis_filter := values.get("registry_filter")) and isinstance(
|
64
|
+
_regis, str
|
65
|
+
):
|
66
|
+
values["registry_filter"] = [_regis_filter]
|
50
67
|
return values
|
51
68
|
|
52
69
|
|
70
|
+
class CoreConf(BaseModel):
|
71
|
+
"""Core Config Model"""
|
72
|
+
|
73
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
74
|
+
|
75
|
+
tz: ZoneInfo = Field(default_factory=lambda: ZoneInfo("UTC"))
|
76
|
+
|
77
|
+
|
53
78
|
class ConfParams(BaseModel):
|
54
79
|
"""Params Model"""
|
55
80
|
|
@@ -57,16 +82,32 @@ class ConfParams(BaseModel):
|
|
57
82
|
default_factory=Engine,
|
58
83
|
description="A engine mapping values.",
|
59
84
|
)
|
85
|
+
core: CoreConf = Field(
|
86
|
+
default_factory=CoreConf,
|
87
|
+
description="A core config value",
|
88
|
+
)
|
60
89
|
|
61
90
|
|
62
91
|
def config() -> ConfParams:
|
63
|
-
"""Load Config data from ``workflows-conf.yaml`` file.
|
92
|
+
"""Load Config data from ``workflows-conf.yaml`` file.
|
93
|
+
|
94
|
+
Configuration Docs:
|
95
|
+
---
|
96
|
+
:var engine.registry:
|
97
|
+
:var engine.registry_filter:
|
98
|
+
:var paths.root:
|
99
|
+
:var paths.conf:
|
100
|
+
"""
|
64
101
|
root_path: str = os.getenv("WORKFLOW_ROOT_PATH", ".")
|
65
102
|
|
66
|
-
regis: list[str] = []
|
103
|
+
regis: list[str] = ["ddeutil.workflow"]
|
67
104
|
if regis_env := os.getenv("WORKFLOW_CORE_REGISTRY"):
|
68
105
|
regis = [r.strip() for r in regis_env.split(",")]
|
69
106
|
|
107
|
+
regis_filter: list[str] = ["ddeutil.workflow.utils"]
|
108
|
+
if regis_filter_env := os.getenv("WORKFLOW_CORE_REGISTRY_FILTER"):
|
109
|
+
regis_filter = [r.strip() for r in regis_filter_env.split(",")]
|
110
|
+
|
70
111
|
conf_path: str = (
|
71
112
|
f"{root_path}/{conf_env}"
|
72
113
|
if (conf_env := os.getenv("WORKFLOW_CORE_PATH_CONF"))
|
@@ -76,6 +117,7 @@ def config() -> ConfParams:
|
|
76
117
|
obj={
|
77
118
|
"engine": {
|
78
119
|
"registry": regis,
|
120
|
+
"registry_filter": regis_filter,
|
79
121
|
"paths": {
|
80
122
|
"root": root_path,
|
81
123
|
"conf": conf_path,
|
@@ -85,19 +127,31 @@ def config() -> ConfParams:
|
|
85
127
|
)
|
86
128
|
|
87
129
|
|
88
|
-
def gen_id(
|
130
|
+
def gen_id(
|
131
|
+
value: Any,
|
132
|
+
*,
|
133
|
+
sensitive: bool = True,
|
134
|
+
unique: bool = False,
|
135
|
+
) -> str:
|
89
136
|
"""Generate running ID for able to tracking. This generate process use `md5`
|
90
|
-
function.
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
:param
|
137
|
+
algorithm function if ``WORKFLOW_CORE_PIPELINE_ID_SIMPLE`` set to false.
|
138
|
+
But it will cut this hashing value length to 10 it the setting value set to
|
139
|
+
true.
|
140
|
+
|
141
|
+
:param value: A value that want to add to prefix before hashing with md5.
|
142
|
+
:param sensitive: A flag that convert the value to lower case before hashing
|
143
|
+
:param unique: A flag that add timestamp at microsecond level to value
|
144
|
+
before hashing.
|
95
145
|
:rtype: str
|
96
146
|
"""
|
97
147
|
if not isinstance(value, str):
|
98
148
|
value: str = str(value)
|
99
149
|
|
100
150
|
tz: ZoneInfo = ZoneInfo(os.getenv("WORKFLOW_CORE_TIMEZONE", "UTC"))
|
151
|
+
if str2bool(os.getenv("WORKFLOW_CORE_PIPELINE_ID_SIMPLE", "true")):
|
152
|
+
return hash_str(f"{(value if sensitive else value.lower())}", n=10) + (
|
153
|
+
f"{datetime.now(tz=tz):%Y%m%d%H%M%S%f}" if unique else ""
|
154
|
+
)
|
101
155
|
return md5(
|
102
156
|
(
|
103
157
|
f"{(value if sensitive else value.lower())}"
|
@@ -115,24 +169,24 @@ class TagFunc(Protocol):
|
|
115
169
|
def __call__(self, *args, **kwargs): ...
|
116
170
|
|
117
171
|
|
118
|
-
def tag(
|
172
|
+
def tag(name: str, alias: str | None = None):
|
119
173
|
"""Tag decorator function that set function attributes, ``tag`` and ``name``
|
120
174
|
for making registries variable.
|
121
175
|
|
122
|
-
:param:
|
123
|
-
:param:
|
176
|
+
:param: name: A tag value for make different use-case of a function.
|
177
|
+
:param: alias: A alias function name that keeping in registries. If this
|
178
|
+
value does not supply, it will use original function name from __name__.
|
124
179
|
"""
|
125
180
|
|
126
|
-
def func_internal(func:
|
127
|
-
func.tag =
|
128
|
-
func.name =
|
181
|
+
def func_internal(func: Callable[[...], Any]) -> TagFunc:
|
182
|
+
func.tag = name
|
183
|
+
func.name = alias or func.__name__.replace("_", "-")
|
129
184
|
|
130
185
|
@wraps(func)
|
131
186
|
def wrapped(*args, **kwargs):
|
187
|
+
# NOTE: Able to do anything before calling hook function.
|
132
188
|
return func(*args, **kwargs)
|
133
189
|
|
134
|
-
# TODO: pass result from a wrapped to Result model
|
135
|
-
# >>> return Result.model_validate(obj=wrapped)
|
136
190
|
return wrapped
|
137
191
|
|
138
192
|
return func_internal
|
@@ -145,6 +199,7 @@ def make_registry(submodule: str) -> dict[str, Registry]:
|
|
145
199
|
"""Return registries of all functions that able to called with task.
|
146
200
|
|
147
201
|
:param submodule: A module prefix that want to import registry.
|
202
|
+
:rtype: dict[str, Registry]
|
148
203
|
"""
|
149
204
|
rs: dict[str, Registry] = {}
|
150
205
|
for module in config().engine.registry:
|
@@ -185,7 +240,7 @@ class BaseParam(BaseModel, ABC):
|
|
185
240
|
|
186
241
|
@abstractmethod
|
187
242
|
def receive(self, value: Optional[Any] = None) -> Any:
|
188
|
-
raise
|
243
|
+
raise NotImplementedError(
|
189
244
|
"Receive value and validate typing before return valid value."
|
190
245
|
)
|
191
246
|
|
@@ -197,14 +252,14 @@ class DefaultParam(BaseParam):
|
|
197
252
|
|
198
253
|
@abstractmethod
|
199
254
|
def receive(self, value: Optional[Any] = None) -> Any:
|
200
|
-
raise
|
255
|
+
raise NotImplementedError(
|
201
256
|
"Receive value and validate typing before return valid value."
|
202
257
|
)
|
203
258
|
|
204
259
|
@model_validator(mode="after")
|
205
260
|
def check_default(self) -> Self:
|
206
261
|
if not self.required and self.default is None:
|
207
|
-
raise
|
262
|
+
raise ParamValueException(
|
208
263
|
"Default should set when this parameter does not required."
|
209
264
|
)
|
210
265
|
return self
|
@@ -218,6 +273,7 @@ class DatetimeParam(DefaultParam):
|
|
218
273
|
default: datetime = Field(default_factory=dt_now)
|
219
274
|
|
220
275
|
def receive(self, value: str | datetime | date | None = None) -> datetime:
|
276
|
+
"""Receive value that match with datetime."""
|
221
277
|
if value is None:
|
222
278
|
return self.default
|
223
279
|
|
@@ -226,7 +282,7 @@ class DatetimeParam(DefaultParam):
|
|
226
282
|
elif isinstance(value, date):
|
227
283
|
return datetime(value.year, value.month, value.day)
|
228
284
|
elif not isinstance(value, str):
|
229
|
-
raise
|
285
|
+
raise ParamValueException(
|
230
286
|
f"Value that want to convert to datetime does not support for "
|
231
287
|
f"type: {type(value)}"
|
232
288
|
)
|
@@ -239,6 +295,7 @@ class StrParam(DefaultParam):
|
|
239
295
|
type: Literal["str"] = "str"
|
240
296
|
|
241
297
|
def receive(self, value: Optional[str] = None) -> str | None:
|
298
|
+
"""Receive value that match with str."""
|
242
299
|
if value is None:
|
243
300
|
return self.default
|
244
301
|
return str(value)
|
@@ -250,13 +307,14 @@ class IntParam(DefaultParam):
|
|
250
307
|
type: Literal["int"] = "int"
|
251
308
|
|
252
309
|
def receive(self, value: Optional[int] = None) -> int | None:
|
310
|
+
"""Receive value that match with int."""
|
253
311
|
if value is None:
|
254
312
|
return self.default
|
255
313
|
if not isinstance(value, int):
|
256
314
|
try:
|
257
315
|
return int(str(value))
|
258
316
|
except TypeError as err:
|
259
|
-
raise
|
317
|
+
raise ParamValueException(
|
260
318
|
f"Value that want to convert to integer does not support "
|
261
319
|
f"for type: {type(value)}"
|
262
320
|
) from err
|
@@ -264,6 +322,8 @@ class IntParam(DefaultParam):
|
|
264
322
|
|
265
323
|
|
266
324
|
class ChoiceParam(BaseParam):
|
325
|
+
"""Choice parameter."""
|
326
|
+
|
267
327
|
type: Literal["choice"] = "choice"
|
268
328
|
options: list[str]
|
269
329
|
|
@@ -274,25 +334,72 @@ class ChoiceParam(BaseParam):
|
|
274
334
|
if value is None:
|
275
335
|
return self.options[0]
|
276
336
|
if any(value not in self.options):
|
277
|
-
raise
|
337
|
+
raise ParamValueException(
|
338
|
+
f"{value!r} does not match any value in choice options."
|
339
|
+
)
|
278
340
|
return value
|
279
341
|
|
280
342
|
|
281
343
|
Param = Union[
|
282
344
|
ChoiceParam,
|
283
345
|
DatetimeParam,
|
346
|
+
IntParam,
|
284
347
|
StrParam,
|
285
348
|
]
|
286
349
|
|
287
350
|
|
288
|
-
|
289
|
-
|
290
|
-
|
351
|
+
class Context(BaseModel):
|
352
|
+
"""Context Pydantic Model"""
|
353
|
+
|
354
|
+
params: dict = Field(default_factory=dict)
|
355
|
+
jobs: dict = Field(default_factory=dict)
|
356
|
+
error: dict = Field(default_factory=dict)
|
357
|
+
|
358
|
+
|
359
|
+
class Result(BaseModel):
|
360
|
+
"""Result Pydantic Model for passing parameter and receiving output from
|
361
|
+
the pipeline execution.
|
362
|
+
"""
|
363
|
+
|
364
|
+
# TODO: Add running ID to this result dataclass.
|
365
|
+
# ---
|
366
|
+
# parent_run_id: str
|
367
|
+
# run_id: str
|
368
|
+
#
|
369
|
+
status: int = Field(default=2)
|
370
|
+
context: DictData = Field(default_factory=dict)
|
371
|
+
|
372
|
+
def receive(self, result: Result) -> Result:
|
373
|
+
self.__dict__["status"] = result.status
|
374
|
+
self.__dict__["context"].update(result.context)
|
375
|
+
return self
|
376
|
+
|
377
|
+
def receive_jobs(self, result: Result) -> Result:
|
378
|
+
self.__dict__["status"] = result.status
|
379
|
+
if "jobs" not in self.__dict__["context"]:
|
380
|
+
self.__dict__["context"]["jobs"] = {}
|
381
|
+
self.__dict__["context"]["jobs"].update(result.context)
|
382
|
+
return self
|
383
|
+
|
384
|
+
|
385
|
+
class ReResult(BaseModel):
|
386
|
+
"""Result Pydantic Model for passing parameter and receiving output from
|
291
387
|
the pipeline execution.
|
292
388
|
"""
|
293
389
|
|
294
|
-
|
295
|
-
|
390
|
+
# TODO: Add running ID to this result dataclass.
|
391
|
+
# ---
|
392
|
+
# parent_run_id: str
|
393
|
+
# run_id: str
|
394
|
+
#
|
395
|
+
status: int = Field(default=2)
|
396
|
+
context: Context = Field(default_factory=Context)
|
397
|
+
|
398
|
+
def receive(self, result: ReResult) -> ReResult:
|
399
|
+
self.__dict__["status"] = result.status
|
400
|
+
self.__dict__["context"].__dict__["jobs"].update(result.context.jobs)
|
401
|
+
self.__dict__["context"].__dict__["error"].update(result.context.error)
|
402
|
+
return self
|
296
403
|
|
297
404
|
|
298
405
|
def make_exec(path: str | Path):
|
@@ -301,11 +408,216 @@ def make_exec(path: str | Path):
|
|
301
408
|
f.chmod(f.stat().st_mode | stat.S_IEXEC)
|
302
409
|
|
303
410
|
|
304
|
-
|
411
|
+
FILTERS: dict[str, callable] = {
|
412
|
+
"abs": abs,
|
413
|
+
"str": str,
|
414
|
+
"int": int,
|
415
|
+
"upper": lambda x: x.upper(),
|
416
|
+
"lower": lambda x: x.lower(),
|
417
|
+
"rstr": [str, repr],
|
418
|
+
}
|
419
|
+
|
420
|
+
|
421
|
+
class FilterFunc(Protocol):
|
422
|
+
"""Tag Function Protocol"""
|
423
|
+
|
424
|
+
name: str
|
425
|
+
|
426
|
+
def __call__(self, *args, **kwargs): ...
|
427
|
+
|
428
|
+
|
429
|
+
def custom_filter(name: str):
|
430
|
+
"""Custom filter decorator function that set function attributes, ``filter``
|
431
|
+
for making filter registries variable.
|
432
|
+
|
433
|
+
:param: name: A filter name for make different use-case of a function.
|
434
|
+
"""
|
435
|
+
|
436
|
+
def func_internal(func: Callable[[...], Any]) -> TagFunc:
|
437
|
+
func.filter = name
|
438
|
+
|
439
|
+
@wraps(func)
|
440
|
+
def wrapped(*args, **kwargs):
|
441
|
+
# NOTE: Able to do anything before calling custom filter function.
|
442
|
+
return func(*args, **kwargs)
|
443
|
+
|
444
|
+
return wrapped
|
445
|
+
|
446
|
+
return func_internal
|
447
|
+
|
448
|
+
|
449
|
+
FilterRegistry = Union[FilterFunc, Callable[[...], Any]]
|
450
|
+
|
451
|
+
|
452
|
+
def make_filter_registry() -> dict[str, FilterRegistry]:
|
453
|
+
"""Return registries of all functions that able to called with task.
|
454
|
+
|
455
|
+
:rtype: dict[str, Registry]
|
456
|
+
"""
|
457
|
+
rs: dict[str, Registry] = {}
|
458
|
+
for module in config().engine.registry_filter:
|
459
|
+
# NOTE: try to sequential import task functions
|
460
|
+
try:
|
461
|
+
importer = import_module(module)
|
462
|
+
except ModuleNotFoundError:
|
463
|
+
continue
|
464
|
+
|
465
|
+
for fstr, func in inspect.getmembers(importer, inspect.isfunction):
|
466
|
+
# NOTE: check function attribute that already set tag by
|
467
|
+
# ``utils.tag`` decorator.
|
468
|
+
if not hasattr(func, "filter"):
|
469
|
+
continue
|
470
|
+
|
471
|
+
rs[func.filter] = import_string(f"{module}.{fstr}")
|
472
|
+
|
473
|
+
rs.update(FILTERS)
|
474
|
+
return rs
|
475
|
+
|
476
|
+
|
477
|
+
def get_args_const(
|
478
|
+
expr: str,
|
479
|
+
) -> tuple[str, list[Constant], dict[str, Constant]]:
|
480
|
+
"""Get arguments and keyword-arguments from function calling string."""
|
481
|
+
try:
|
482
|
+
mod: Module = parse(expr)
|
483
|
+
except SyntaxError:
|
484
|
+
raise UtilException(
|
485
|
+
f"Post-filter: {expr} does not valid because it raise syntax error."
|
486
|
+
) from None
|
487
|
+
body: list[Expr] = mod.body
|
488
|
+
|
489
|
+
if len(body) > 1:
|
490
|
+
raise UtilException(
|
491
|
+
"Post-filter function should be only one calling per pipe"
|
492
|
+
)
|
493
|
+
|
494
|
+
caller: Union[Name, Call]
|
495
|
+
if isinstance((caller := body[0].value), Name):
|
496
|
+
return caller.id, [], {}
|
497
|
+
elif not isinstance(caller, Call):
|
498
|
+
raise UtilException(
|
499
|
+
f"Get arguments does not support for caller type: {type(caller)}"
|
500
|
+
)
|
501
|
+
|
502
|
+
name: Name = caller.func
|
503
|
+
args: list[Constant] = caller.args
|
504
|
+
keywords: dict[str, Constant] = {k.arg: k.value for k in caller.keywords}
|
505
|
+
|
506
|
+
if any(not isinstance(i, Constant) for i in args):
|
507
|
+
raise UtilException("Argument should be constant.")
|
508
|
+
|
509
|
+
return name.id, args, keywords
|
510
|
+
|
511
|
+
|
512
|
+
@custom_filter("fmt")
|
513
|
+
def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
514
|
+
return value.strftime(fmt)
|
515
|
+
|
516
|
+
|
517
|
+
def map_post_filter(
|
305
518
|
value: Any,
|
306
|
-
|
519
|
+
post_filter: list[str],
|
520
|
+
filters: dict[str, FilterRegistry],
|
521
|
+
) -> Any:
|
522
|
+
"""Mapping post-filter to value with sequence list of filter function name
|
523
|
+
that will get from the filter registry.
|
524
|
+
|
525
|
+
:param value: A string value that want to mapped with filter function.
|
526
|
+
:param post_filter: A list of post-filter function name.
|
527
|
+
:param filters: A filter registry.
|
528
|
+
"""
|
529
|
+
for _filter in post_filter:
|
530
|
+
func_name, _args, _kwargs = get_args_const(_filter)
|
531
|
+
args = [arg.value for arg in _args]
|
532
|
+
kwargs = {k: v.value for k, v in _kwargs.items()}
|
533
|
+
|
534
|
+
if func_name not in filters:
|
535
|
+
raise UtilException(
|
536
|
+
f"The post-filter: {func_name} does not support yet."
|
537
|
+
)
|
538
|
+
|
539
|
+
try:
|
540
|
+
if isinstance((f_func := filters[func_name]), list):
|
541
|
+
if args or kwargs:
|
542
|
+
raise UtilException(
|
543
|
+
"Chain filter function does not support for passing "
|
544
|
+
"arguments."
|
545
|
+
)
|
546
|
+
for func in f_func:
|
547
|
+
value: Any = func(value)
|
548
|
+
else:
|
549
|
+
value: Any = f_func(value, *args, **kwargs)
|
550
|
+
except Exception as err:
|
551
|
+
logging.warning(str(err))
|
552
|
+
raise UtilException(
|
553
|
+
f"The post-filter function: {func_name} does not fit with "
|
554
|
+
f"{value} (type: {type(value).__name__})."
|
555
|
+
) from None
|
556
|
+
return value
|
557
|
+
|
558
|
+
|
559
|
+
def str2template(
|
560
|
+
value: str,
|
561
|
+
params: DictData,
|
307
562
|
*,
|
308
|
-
|
563
|
+
filters: dict[str, FilterRegistry] | None = None,
|
564
|
+
) -> Any:
|
565
|
+
"""(Sub-function) Pass param to template string that can search by
|
566
|
+
``RE_CALLER`` regular expression.
|
567
|
+
|
568
|
+
The getter value that map a template should have typing support align
|
569
|
+
with the pipeline parameter types that is `str`, `int`, `datetime`, and
|
570
|
+
`list`.
|
571
|
+
|
572
|
+
:param value: A string value that want to mapped with an params
|
573
|
+
:param params: A parameter value that getting with matched regular
|
574
|
+
expression.
|
575
|
+
:param filters:
|
576
|
+
"""
|
577
|
+
filters: dict[str, FilterRegistry] = filters or make_filter_registry()
|
578
|
+
|
579
|
+
# NOTE: remove space before and after this string value.
|
580
|
+
value: str = value.strip()
|
581
|
+
for found in Re.RE_CALLER.finditer(value):
|
582
|
+
# NOTE:
|
583
|
+
# Get caller and filter values that setting inside;
|
584
|
+
#
|
585
|
+
# ... ``${{ <caller-value> [ | <filter-value>] ... }}``
|
586
|
+
#
|
587
|
+
caller: str = found.group("caller")
|
588
|
+
pfilter: list[str] = [
|
589
|
+
i.strip()
|
590
|
+
for i in (
|
591
|
+
found.group("post_filters").strip().removeprefix("|").split("|")
|
592
|
+
)
|
593
|
+
if i != ""
|
594
|
+
]
|
595
|
+
if not hasdot(caller, params):
|
596
|
+
raise UtilException(f"The params does not set caller: {caller!r}.")
|
597
|
+
|
598
|
+
# NOTE: from validate step, it guarantee that caller exists in params.
|
599
|
+
getter: Any = getdot(caller, params)
|
600
|
+
|
601
|
+
# NOTE:
|
602
|
+
# If type of getter caller is not string type and it does not use to
|
603
|
+
# concat other string value, it will return origin value from the
|
604
|
+
# ``getdot`` function.
|
605
|
+
if value.replace(found.group(0), "", 1) == "":
|
606
|
+
return map_post_filter(getter, pfilter, filters=filters)
|
607
|
+
|
608
|
+
# NOTE: map post-filter function.
|
609
|
+
getter: Any = map_post_filter(getter, pfilter, filters=filters)
|
610
|
+
if not isinstance(getter, str):
|
611
|
+
getter: str = str(getter)
|
612
|
+
|
613
|
+
value: str = value.replace(found.group(0), getter, 1)
|
614
|
+
|
615
|
+
return search_env_replace(value)
|
616
|
+
|
617
|
+
|
618
|
+
def param2template(
|
619
|
+
value: Any,
|
620
|
+
params: DictData,
|
309
621
|
) -> Any:
|
310
622
|
"""Pass param to template string that can search by ``RE_CALLER`` regular
|
311
623
|
expression.
|
@@ -313,46 +625,36 @@ def param2template(
|
|
313
625
|
:param value: A value that want to mapped with an params
|
314
626
|
:param params: A parameter value that getting with matched regular
|
315
627
|
expression.
|
316
|
-
:param repr_flag: A repr flag for using repr instead of str if it set be
|
317
|
-
true.
|
318
628
|
|
319
629
|
:rtype: Any
|
320
630
|
:returns: An any getter value from the params input.
|
321
631
|
"""
|
632
|
+
filters: dict[str, FilterRegistry] = make_filter_registry()
|
322
633
|
if isinstance(value, dict):
|
323
634
|
return {k: param2template(value[k], params) for k in value}
|
324
635
|
elif isinstance(value, (list, tuple, set)):
|
325
636
|
return type(value)([param2template(i, params) for i in value])
|
326
637
|
elif not isinstance(value, str):
|
327
638
|
return value
|
639
|
+
return str2template(value, params, filters=filters)
|
328
640
|
|
329
|
-
if not Re.RE_CALLER.search(value):
|
330
|
-
return value
|
331
|
-
|
332
|
-
for found in Re.RE_CALLER.finditer(value):
|
333
|
-
|
334
|
-
# NOTE: get caller value that setting inside; ``${{ <caller-value> }}``
|
335
|
-
caller: str = found.group("caller")
|
336
|
-
if not hasdot(caller, params):
|
337
|
-
raise ValueError(f"params does not set caller: {caller!r}")
|
338
|
-
|
339
|
-
getter: Any = getdot(caller, params)
|
340
641
|
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
642
|
+
def filter_func(value: Any):
|
643
|
+
"""Filter own created function out of any value with replace it to its
|
644
|
+
function name. If it is built-in function, it does not have any changing.
|
645
|
+
"""
|
646
|
+
if isinstance(value, dict):
|
647
|
+
return {k: filter_func(value[k]) for k in value}
|
648
|
+
elif isinstance(value, (list, tuple, set)):
|
649
|
+
return type(value)([filter_func(i) for i in value])
|
650
|
+
|
651
|
+
if isfunction(value):
|
652
|
+
# NOTE: If it want to improve to get this function, it able to save to
|
653
|
+
# some global memory storage.
|
654
|
+
# ---
|
655
|
+
# >>> GLOBAL_DICT[value.__name__] = value
|
656
|
+
#
|
657
|
+
return value.__name__
|
356
658
|
return value
|
357
659
|
|
358
660
|
|
@@ -368,7 +670,7 @@ def dash2underscore(
|
|
368
670
|
return values
|
369
671
|
|
370
672
|
|
371
|
-
def cross_product(matrix: Matrix) -> Iterator:
|
673
|
+
def cross_product(matrix: Matrix) -> Iterator[DictData]:
|
372
674
|
"""Iterator of products value from matrix."""
|
373
675
|
yield from (
|
374
676
|
{_k: _v for e in mapped for _k, _v in e.items()}
|