ddeutil-workflow 0.0.38__py3-none-any.whl → 0.0.40__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 +89 -25
- ddeutil/workflow/__types.py +2 -0
- ddeutil/workflow/audit.py +7 -5
- ddeutil/workflow/caller.py +11 -10
- ddeutil/workflow/conf.py +8 -23
- ddeutil/workflow/context.py +2 -0
- ddeutil/workflow/job.py +127 -17
- ddeutil/workflow/scheduler.py +40 -1
- ddeutil/workflow/stages.py +55 -38
- ddeutil/workflow/templates.py +39 -20
- ddeutil/workflow/workflow.py +81 -23
- {ddeutil_workflow-0.0.38.dist-info → ddeutil_workflow-0.0.40.dist-info}/METADATA +5 -5
- {ddeutil_workflow-0.0.38.dist-info → ddeutil_workflow-0.0.40.dist-info}/RECORD +17 -17
- {ddeutil_workflow-0.0.38.dist-info → ddeutil_workflow-0.0.40.dist-info}/WHEEL +1 -1
- {ddeutil_workflow-0.0.38.dist-info → ddeutil_workflow-0.0.40.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.38.dist-info → ddeutil_workflow-0.0.40.dist-info}/top_level.txt +0 -0
ddeutil/workflow/job.py
CHANGED
@@ -40,12 +40,8 @@ from .exceptions import (
|
|
40
40
|
)
|
41
41
|
from .result import Result, Status
|
42
42
|
from .stages import Stage
|
43
|
-
from .templates import has_template
|
44
|
-
from .utils import
|
45
|
-
cross_product,
|
46
|
-
filter_func,
|
47
|
-
gen_id,
|
48
|
-
)
|
43
|
+
from .templates import has_template, param2template
|
44
|
+
from .utils import cross_product, filter_func, gen_id
|
49
45
|
|
50
46
|
MatrixFilter = list[dict[str, Union[str, int]]]
|
51
47
|
|
@@ -54,6 +50,7 @@ __all__: TupleStr = (
|
|
54
50
|
"Strategy",
|
55
51
|
"Job",
|
56
52
|
"TriggerRules",
|
53
|
+
"TriggerState",
|
57
54
|
"RunsOn",
|
58
55
|
"RunsOnLocal",
|
59
56
|
"RunsOnSelfHosted",
|
@@ -208,6 +205,16 @@ class TriggerRules(str, Enum):
|
|
208
205
|
none_skipped: str = "none_skipped"
|
209
206
|
|
210
207
|
|
208
|
+
class TriggerState(str, Enum):
|
209
|
+
waiting: str = "waiting"
|
210
|
+
passed: str = "passed"
|
211
|
+
skipped: str = "skipped"
|
212
|
+
failed: str = "failed"
|
213
|
+
|
214
|
+
def is_waiting(self):
|
215
|
+
return self.value == "waiting"
|
216
|
+
|
217
|
+
|
211
218
|
class RunsOnType(str, Enum):
|
212
219
|
"""Runs-On enum object."""
|
213
220
|
|
@@ -312,13 +319,21 @@ class Job(BaseModel):
|
|
312
319
|
description="A target node for this job to use for execution.",
|
313
320
|
alias="runs-on",
|
314
321
|
)
|
322
|
+
condition: Optional[str] = Field(
|
323
|
+
default=None,
|
324
|
+
description="A job condition statement to allow job executable.",
|
325
|
+
alias="if",
|
326
|
+
)
|
315
327
|
stages: list[Stage] = Field(
|
316
328
|
default_factory=list,
|
317
329
|
description="A list of Stage of this job.",
|
318
330
|
)
|
319
331
|
trigger_rule: TriggerRules = Field(
|
320
332
|
default=TriggerRules.all_success,
|
321
|
-
description=
|
333
|
+
description=(
|
334
|
+
"A trigger rule of tracking needed jobs if feature will use when "
|
335
|
+
"the `raise_error` did not set from job and stage executions."
|
336
|
+
),
|
322
337
|
alias="trigger-rule",
|
323
338
|
)
|
324
339
|
needs: list[str] = Field(
|
@@ -382,12 +397,87 @@ class Job(BaseModel):
|
|
382
397
|
return stage
|
383
398
|
raise ValueError(f"Stage ID {stage_id} does not exists")
|
384
399
|
|
385
|
-
def check_needs(
|
400
|
+
def check_needs(
|
401
|
+
self, jobs: dict[str, Any]
|
402
|
+
) -> TriggerState: # pragma: no cov
|
386
403
|
"""Return True if job's need exists in an input list of job's ID.
|
387
404
|
|
405
|
+
:param jobs: A mapping of job model and its ID.
|
406
|
+
|
407
|
+
:rtype: TriggerState
|
408
|
+
"""
|
409
|
+
if not self.needs:
|
410
|
+
return TriggerState.passed
|
411
|
+
|
412
|
+
def make_return(result: bool) -> TriggerState:
|
413
|
+
return TriggerState.passed if result else TriggerState.failed
|
414
|
+
|
415
|
+
need_exist: dict[str, Any] = {
|
416
|
+
need: jobs[need] for need in self.needs if need in jobs
|
417
|
+
}
|
418
|
+
if len(need_exist) != len(self.needs):
|
419
|
+
return TriggerState.waiting
|
420
|
+
elif all("skipped" in need_exist[job] for job in need_exist):
|
421
|
+
return TriggerState.skipped
|
422
|
+
elif self.trigger_rule == TriggerRules.all_done:
|
423
|
+
return TriggerState.passed
|
424
|
+
elif self.trigger_rule == TriggerRules.all_success:
|
425
|
+
rs = all(
|
426
|
+
k not in need_exist[job]
|
427
|
+
for k in ("errors", "skipped")
|
428
|
+
for job in need_exist
|
429
|
+
)
|
430
|
+
elif self.trigger_rule == TriggerRules.all_failed:
|
431
|
+
rs = all("errors" in need_exist[job] for job in need_exist)
|
432
|
+
elif self.trigger_rule == TriggerRules.one_success:
|
433
|
+
rs = sum(
|
434
|
+
k not in need_exist[job]
|
435
|
+
for k in ("errors", "skipped")
|
436
|
+
for job in need_exist
|
437
|
+
) + 1 == len(self.needs)
|
438
|
+
elif self.trigger_rule == TriggerRules.one_failed:
|
439
|
+
rs = sum("errors" in need_exist[job] for job in need_exist) == 1
|
440
|
+
elif self.trigger_rule == TriggerRules.none_skipped:
|
441
|
+
rs = all("skipped" not in need_exist[job] for job in need_exist)
|
442
|
+
elif self.trigger_rule == TriggerRules.none_failed:
|
443
|
+
rs = all("errors" not in need_exist[job] for job in need_exist)
|
444
|
+
else: # pragma: no cov
|
445
|
+
raise NotImplementedError(
|
446
|
+
f"Trigger rule: {self.trigger_rule} does not support yet."
|
447
|
+
)
|
448
|
+
return make_return(rs)
|
449
|
+
|
450
|
+
def is_skipped(self, params: DictData | None = None) -> bool:
|
451
|
+
"""Return true if condition of this job do not correct. This process
|
452
|
+
use build-in eval function to execute the if-condition.
|
453
|
+
|
454
|
+
:raise JobException: When it has any error raise from the eval
|
455
|
+
condition statement.
|
456
|
+
:raise JobException: When return type of the eval condition statement
|
457
|
+
does not return with boolean type.
|
458
|
+
|
459
|
+
:param params: (DictData) A parameters that want to pass to condition
|
460
|
+
template.
|
461
|
+
|
388
462
|
:rtype: bool
|
389
463
|
"""
|
390
|
-
|
464
|
+
if self.condition is None:
|
465
|
+
return False
|
466
|
+
|
467
|
+
params: DictData = {} if params is None else params
|
468
|
+
|
469
|
+
try:
|
470
|
+
# WARNING: The eval build-in function is very dangerous. So, it
|
471
|
+
# should use the `re` module to validate eval-string before
|
472
|
+
# running.
|
473
|
+
rs: bool = eval(
|
474
|
+
param2template(self.condition, params), globals() | params, {}
|
475
|
+
)
|
476
|
+
if not isinstance(rs, bool):
|
477
|
+
raise TypeError("Return type of condition does not be boolean")
|
478
|
+
return not rs
|
479
|
+
except Exception as err:
|
480
|
+
raise JobException(f"{err.__class__.__name__}: {err}") from err
|
391
481
|
|
392
482
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
393
483
|
"""Set an outputs from execution process to the received context. The
|
@@ -436,7 +526,9 @@ class Job(BaseModel):
|
|
436
526
|
{"errors": output.pop("errors", {})} if "errors" in output else {}
|
437
527
|
)
|
438
528
|
|
439
|
-
if
|
529
|
+
if "SKIP" in output: # pragma: no cov
|
530
|
+
to["jobs"][_id] = output["SKIP"]
|
531
|
+
elif self.strategy.is_set():
|
440
532
|
to["jobs"][_id] = {"strategies": output, **errors}
|
441
533
|
else:
|
442
534
|
_output = output.get(next(iter(output), "FIRST"), {})
|
@@ -458,8 +550,8 @@ class Job(BaseModel):
|
|
458
550
|
multithread on this metrics to the `stages` field of this job.
|
459
551
|
|
460
552
|
:param params: An input parameters that use on job execution.
|
461
|
-
:param run_id: A job running ID
|
462
|
-
:param parent_run_id: A parent workflow running ID
|
553
|
+
:param run_id: (str) A job running ID.
|
554
|
+
:param parent_run_id: (str) A parent workflow running ID.
|
463
555
|
:param result: (Result) A result object for keeping context and status
|
464
556
|
data.
|
465
557
|
:param event: (Event) An event manager that pass to the
|
@@ -559,6 +651,7 @@ def local_execute_strategy(
|
|
559
651
|
|
560
652
|
if stage.is_skipped(params=context):
|
561
653
|
result.trace.info(f"[STAGE]: Skip stage: {stage.iden!r}")
|
654
|
+
stage.set_outputs(output={"skipped": True}, to=context)
|
562
655
|
continue
|
563
656
|
|
564
657
|
if event and event.is_set():
|
@@ -623,9 +716,6 @@ def local_execute_strategy(
|
|
623
716
|
},
|
624
717
|
)
|
625
718
|
|
626
|
-
# NOTE: Remove the current stage object for saving memory.
|
627
|
-
del stage
|
628
|
-
|
629
719
|
return result.catch(
|
630
720
|
status=Status.SUCCESS,
|
631
721
|
context={
|
@@ -680,7 +770,17 @@ def local_execute(
|
|
680
770
|
|
681
771
|
for strategy in job.strategy.make():
|
682
772
|
|
683
|
-
|
773
|
+
if event and event.is_set(): # pragma: no cov
|
774
|
+
return result.catch(
|
775
|
+
status=Status.FAILED,
|
776
|
+
context={
|
777
|
+
"errors": JobException(
|
778
|
+
"Job strategy was canceled from event that had set "
|
779
|
+
"before strategy execution."
|
780
|
+
).to_dict()
|
781
|
+
},
|
782
|
+
)
|
783
|
+
|
684
784
|
local_execute_strategy(
|
685
785
|
job=job,
|
686
786
|
strategy=strategy,
|
@@ -694,12 +794,22 @@ def local_execute(
|
|
694
794
|
|
695
795
|
fail_fast_flag: bool = job.strategy.fail_fast
|
696
796
|
ls: str = "Fail-Fast" if fail_fast_flag else "All-Completed"
|
697
|
-
|
698
797
|
result.trace.info(
|
699
798
|
f"[JOB]: Start multithreading: {job.strategy.max_parallel} threads "
|
700
799
|
f"with {ls} mode."
|
701
800
|
)
|
702
801
|
|
802
|
+
if event and event.is_set(): # pragma: no cov
|
803
|
+
return result.catch(
|
804
|
+
status=Status.FAILED,
|
805
|
+
context={
|
806
|
+
"errors": JobException(
|
807
|
+
"Job strategy was canceled from event that had set "
|
808
|
+
"before strategy execution."
|
809
|
+
).to_dict()
|
810
|
+
},
|
811
|
+
)
|
812
|
+
|
703
813
|
# IMPORTANT: Start running strategy execution by multithreading because
|
704
814
|
# it will run by strategy values without waiting previous execution.
|
705
815
|
with ThreadPoolExecutor(
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -31,6 +31,7 @@ from concurrent.futures import (
|
|
31
31
|
from datetime import datetime, timedelta
|
32
32
|
from functools import wraps
|
33
33
|
from heapq import heappop, heappush
|
34
|
+
from pathlib import Path
|
34
35
|
from textwrap import dedent
|
35
36
|
from threading import Thread
|
36
37
|
from typing import Callable, Optional, TypedDict, Union
|
@@ -52,7 +53,7 @@ except ImportError: # pragma: no cov
|
|
52
53
|
from .__cron import CronRunner
|
53
54
|
from .__types import DictData, TupleStr
|
54
55
|
from .audit import Audit, get_audit
|
55
|
-
from .conf import Loader, config, get_logger
|
56
|
+
from .conf import Loader, SimLoad, config, get_logger
|
56
57
|
from .cron import On
|
57
58
|
from .exceptions import ScheduleException, WorkflowException
|
58
59
|
from .result import Result, Status
|
@@ -266,6 +267,8 @@ class Schedule(BaseModel):
|
|
266
267
|
:param externals: An external parameters that want to pass to Loader
|
267
268
|
object.
|
268
269
|
|
270
|
+
:raise ValueError: If the type does not match with current object.
|
271
|
+
|
269
272
|
:rtype: Self
|
270
273
|
"""
|
271
274
|
loader: Loader = Loader(name, externals=(externals or {}))
|
@@ -281,6 +284,42 @@ class Schedule(BaseModel):
|
|
281
284
|
|
282
285
|
return cls.model_validate(obj=loader_data)
|
283
286
|
|
287
|
+
@classmethod
|
288
|
+
def from_path(
|
289
|
+
cls,
|
290
|
+
name: str,
|
291
|
+
path: Path,
|
292
|
+
externals: DictData | None = None,
|
293
|
+
) -> Self:
|
294
|
+
"""Create Schedule instance from the SimLoad object that receive an
|
295
|
+
input schedule name and conf path. The loader object will use this
|
296
|
+
schedule name to searching configuration data of this schedule model
|
297
|
+
in conf path.
|
298
|
+
|
299
|
+
:param name: (str) A schedule name that want to pass to Loader object.
|
300
|
+
:param path: (Path) A config path that want to search.
|
301
|
+
:param externals: An external parameters that want to pass to Loader
|
302
|
+
object.
|
303
|
+
|
304
|
+
:raise ValueError: If the type does not match with current object.
|
305
|
+
|
306
|
+
:rtype: Self
|
307
|
+
"""
|
308
|
+
loader: SimLoad = SimLoad(
|
309
|
+
name, conf_path=path, externals=(externals or {})
|
310
|
+
)
|
311
|
+
|
312
|
+
# NOTE: Validate the config type match with current connection model
|
313
|
+
if loader.type != cls.__name__:
|
314
|
+
raise ValueError(f"Type {loader.type} does not match with {cls}")
|
315
|
+
|
316
|
+
loader_data: DictData = copy.deepcopy(loader.data)
|
317
|
+
|
318
|
+
# NOTE: Add name to loader data
|
319
|
+
loader_data["name"] = name.replace(" ", "_")
|
320
|
+
|
321
|
+
return cls.model_validate(obj=loader_data)
|
322
|
+
|
284
323
|
def tasks(
|
285
324
|
self,
|
286
325
|
start_date: datetime,
|
ddeutil/workflow/stages.py
CHANGED
@@ -41,7 +41,7 @@ from inspect import Parameter
|
|
41
41
|
from pathlib import Path
|
42
42
|
from subprocess import CompletedProcess
|
43
43
|
from textwrap import dedent
|
44
|
-
from typing import Optional, Union
|
44
|
+
from typing import Annotated, Optional, Union
|
45
45
|
|
46
46
|
from pydantic import BaseModel, Field
|
47
47
|
from pydantic.functional_validators import model_validator
|
@@ -230,7 +230,10 @@ class BaseStage(BaseModel, ABC):
|
|
230
230
|
|
231
231
|
... (iii) to: {
|
232
232
|
'stages': {
|
233
|
-
'<stage-id>': {
|
233
|
+
'<stage-id>': {
|
234
|
+
'outputs': {'foo': 'bar'},
|
235
|
+
'skipped': False
|
236
|
+
}
|
234
237
|
}
|
235
238
|
}
|
236
239
|
|
@@ -255,8 +258,12 @@ class BaseStage(BaseModel, ABC):
|
|
255
258
|
errors: DictData = (
|
256
259
|
{"errors": output.pop("errors", {})} if "errors" in output else {}
|
257
260
|
)
|
258
|
-
|
259
|
-
|
261
|
+
skipping: dict[str, bool] = (
|
262
|
+
{"skipped": output.pop("skipped", False)}
|
263
|
+
if "skipped" in output
|
264
|
+
else {}
|
265
|
+
)
|
266
|
+
to["stages"][_id] = {"outputs": output, **skipping, **errors}
|
260
267
|
return to
|
261
268
|
|
262
269
|
def is_skipped(self, params: DictData | None = None) -> bool:
|
@@ -539,19 +546,11 @@ class PyStage(BaseStage):
|
|
539
546
|
|
540
547
|
:rtype: DictData
|
541
548
|
"""
|
542
|
-
|
543
|
-
|
549
|
+
lc: DictData = output.pop("locals", {})
|
550
|
+
gb: DictData = output.pop("globals", {})
|
544
551
|
super().set_outputs(
|
545
|
-
(
|
546
|
-
{k: lc[k] for k in self.filter_locals(lc)}
|
547
|
-
| ({"errors": output["errors"]} if "errors" in output else {})
|
548
|
-
),
|
549
|
-
to=to,
|
552
|
+
{k: lc[k] for k in self.filter_locals(lc)} | output, to=to
|
550
553
|
)
|
551
|
-
|
552
|
-
# NOTE: Override value that changing from the globals that pass via the
|
553
|
-
# exec function.
|
554
|
-
gb: DictData = output.get("globals", {})
|
555
554
|
to.update({k: gb[k] for k in to if k in gb})
|
556
555
|
return to
|
557
556
|
|
@@ -572,17 +571,13 @@ class PyStage(BaseStage):
|
|
572
571
|
run_id=gen_id(self.name + (self.id or ""), unique=True)
|
573
572
|
)
|
574
573
|
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
# NOTE: create custom globals value that will pass to exec function.
|
579
|
-
_globals: DictData = (
|
574
|
+
lc: DictData = {}
|
575
|
+
gb: DictData = (
|
580
576
|
globals()
|
581
577
|
| params
|
582
578
|
| param2template(self.vars, params)
|
583
579
|
| {"result": result}
|
584
580
|
)
|
585
|
-
lc: DictData = {}
|
586
581
|
|
587
582
|
# NOTE: Start exec the run statement.
|
588
583
|
result.trace.info(f"[STAGE]: Py-Execute: {self.name}")
|
@@ -591,14 +586,12 @@ class PyStage(BaseStage):
|
|
591
586
|
"check your statement be safe before execute."
|
592
587
|
)
|
593
588
|
|
594
|
-
# TODO: Add Python systax wrapper for checking dangerous code before run
|
595
|
-
# this statement.
|
596
589
|
# WARNING: The exec build-in function is very dangerous. So, it
|
597
590
|
# should use the re module to validate exec-string before running.
|
598
|
-
exec(run,
|
591
|
+
exec(param2template(dedent(self.run), params), gb, lc)
|
599
592
|
|
600
593
|
return result.catch(
|
601
|
-
status=Status.SUCCESS, context={"locals": lc, "globals":
|
594
|
+
status=Status.SUCCESS, context={"locals": lc, "globals": gb}
|
602
595
|
)
|
603
596
|
|
604
597
|
|
@@ -795,7 +788,9 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
795
788
|
... }
|
796
789
|
"""
|
797
790
|
|
798
|
-
parallel: dict[str, list[Stage]] = Field(
|
791
|
+
parallel: dict[str, list[Stage]] = Field(
|
792
|
+
description="A mapping of parallel branch ID.",
|
793
|
+
)
|
799
794
|
max_parallel_core: int = Field(default=2)
|
800
795
|
|
801
796
|
@staticmethod
|
@@ -807,9 +802,10 @@ class ParallelStage(BaseStage): # pragma: no cov
|
|
807
802
|
) -> DictData:
|
808
803
|
"""Task execution method for passing a branch to each thread.
|
809
804
|
|
810
|
-
:param branch:
|
811
|
-
:param params:
|
812
|
-
:param result:
|
805
|
+
:param branch: A branch ID.
|
806
|
+
:param params: A parameter data that want to use in this execution.
|
807
|
+
:param result: (Result) A result object for keeping context and status
|
808
|
+
data.
|
813
809
|
:param stages:
|
814
810
|
|
815
811
|
:rtype: DictData
|
@@ -1008,7 +1004,7 @@ class IfStage(BaseStage): # pragma: no cov
|
|
1008
1004
|
|
1009
1005
|
"""
|
1010
1006
|
|
1011
|
-
case: str
|
1007
|
+
case: str = Field(description="A case condition for routing.")
|
1012
1008
|
match: list[dict[str, Union[str, Stage]]]
|
1013
1009
|
|
1014
1010
|
def execute(
|
@@ -1016,6 +1012,18 @@ class IfStage(BaseStage): # pragma: no cov
|
|
1016
1012
|
) -> Result: ...
|
1017
1013
|
|
1018
1014
|
|
1015
|
+
class RaiseStage(BaseStage): # pragma: no cov
|
1016
|
+
message: str = Field(
|
1017
|
+
description="An error message that want to raise",
|
1018
|
+
alias="raise",
|
1019
|
+
)
|
1020
|
+
|
1021
|
+
def execute(
|
1022
|
+
self, params: DictData, *, result: Result | None = None
|
1023
|
+
) -> Result:
|
1024
|
+
raise StageException(self.message)
|
1025
|
+
|
1026
|
+
|
1019
1027
|
# TODO: Not implement this stages yet
|
1020
1028
|
class HookStage(BaseStage): # pragma: no cov
|
1021
1029
|
hook: str
|
@@ -1050,6 +1058,11 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
1050
1058
|
|
1051
1059
|
def create_py_file(self, py: str, run_id: str | None): ...
|
1052
1060
|
|
1061
|
+
def execute(
|
1062
|
+
self, params: DictData, *, result: Result | None = None
|
1063
|
+
) -> Result:
|
1064
|
+
return super().execute(params, result=result)
|
1065
|
+
|
1053
1066
|
|
1054
1067
|
# TODO: Not implement this stages yet
|
1055
1068
|
class SensorStage(BaseStage): # pragma: no cov
|
@@ -1064,12 +1077,16 @@ class SensorStage(BaseStage): # pragma: no cov
|
|
1064
1077
|
# From the current build-in stages, they do not have stage that have the same
|
1065
1078
|
# fields that because of parsing on the Job's stages key.
|
1066
1079
|
#
|
1067
|
-
Stage =
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
1071
|
-
|
1072
|
-
|
1073
|
-
|
1074
|
-
|
1080
|
+
Stage = Annotated[
|
1081
|
+
Union[
|
1082
|
+
EmptyStage,
|
1083
|
+
BashStage,
|
1084
|
+
CallStage,
|
1085
|
+
TriggerStage,
|
1086
|
+
ForEachStage,
|
1087
|
+
ParallelStage,
|
1088
|
+
PyStage,
|
1089
|
+
RaiseStage,
|
1090
|
+
],
|
1091
|
+
Field(union_mode="smart"),
|
1075
1092
|
]
|
ddeutil/workflow/templates.py
CHANGED
@@ -18,7 +18,7 @@ try:
|
|
18
18
|
except ImportError:
|
19
19
|
from typing_extensions import ParamSpec
|
20
20
|
|
21
|
-
from ddeutil.core import getdot,
|
21
|
+
from ddeutil.core import getdot, import_string
|
22
22
|
from ddeutil.io import search_env_replace
|
23
23
|
|
24
24
|
from .__types import DictData, Re
|
@@ -59,7 +59,8 @@ def custom_filter(name: str) -> Callable[P, FilterFunc]:
|
|
59
59
|
"""Custom filter decorator function that set function attributes, ``filter``
|
60
60
|
for making filter registries variable.
|
61
61
|
|
62
|
-
:param: name: A filter name for make different use-case of a function.
|
62
|
+
:param: name: (str) A filter name for make different use-case of a function.
|
63
|
+
|
63
64
|
:rtype: Callable[P, FilterFunc]
|
64
65
|
"""
|
65
66
|
|
@@ -108,7 +109,7 @@ def get_args_const(
|
|
108
109
|
) -> tuple[str, list[Constant], dict[str, Constant]]:
|
109
110
|
"""Get arguments and keyword-arguments from function calling string.
|
110
111
|
|
111
|
-
:param expr: An expr string value.
|
112
|
+
:param expr: (str) An expr string value.
|
112
113
|
|
113
114
|
:rtype: tuple[str, list[Constant], dict[str, Constant]]
|
114
115
|
"""
|
@@ -154,7 +155,7 @@ def get_args_from_filter(
|
|
154
155
|
and validate it with the filter functions mapping dict.
|
155
156
|
|
156
157
|
:param ft:
|
157
|
-
:param filters:
|
158
|
+
:param filters: A mapping of filter registry.
|
158
159
|
|
159
160
|
:rtype: tuple[str, FilterRegistry, list[Any], dict[Any, Any]]
|
160
161
|
"""
|
@@ -185,7 +186,7 @@ def map_post_filter(
|
|
185
186
|
|
186
187
|
:param value: A string value that want to map with filter function.
|
187
188
|
:param post_filter: A list of post-filter function name.
|
188
|
-
:param filters: A filter registry.
|
189
|
+
:param filters: A mapping of filter registry.
|
189
190
|
|
190
191
|
:rtype: T
|
191
192
|
"""
|
@@ -203,8 +204,8 @@ def map_post_filter(
|
|
203
204
|
except Exception as err:
|
204
205
|
logger.warning(str(err))
|
205
206
|
raise UtilException(
|
206
|
-
f"The post-filter
|
207
|
-
f"
|
207
|
+
f"The post-filter: {func_name!r} does not fit with {value!r} "
|
208
|
+
f"(type: {type(value).__name__})."
|
208
209
|
) from None
|
209
210
|
return value
|
210
211
|
|
@@ -258,10 +259,10 @@ def str2template(
|
|
258
259
|
with the workflow parameter types that is `str`, `int`, `datetime`, and
|
259
260
|
`list`.
|
260
261
|
|
261
|
-
:param value: A string value that want to map with params
|
262
|
-
:param params: A parameter value that getting with matched
|
263
|
-
expression.
|
264
|
-
:param filters:
|
262
|
+
:param value: (str) A string value that want to map with params.
|
263
|
+
:param params: (DictData) A parameter value that getting with matched
|
264
|
+
regular expression.
|
265
|
+
:param filters: A mapping of filter registry.
|
265
266
|
|
266
267
|
:rtype: str
|
267
268
|
"""
|
@@ -281,11 +282,14 @@ def str2template(
|
|
281
282
|
for i in (found.post_filters.strip().removeprefix("|").split("|"))
|
282
283
|
if i != ""
|
283
284
|
]
|
284
|
-
if not hasdot(caller, params):
|
285
|
-
raise UtilException(f"The params does not set caller: {caller!r}.")
|
286
285
|
|
287
286
|
# NOTE: from validate step, it guarantees that caller exists in params.
|
288
|
-
|
287
|
+
try:
|
288
|
+
getter: Any = getdot(caller, params)
|
289
|
+
except ValueError as err:
|
290
|
+
raise UtilException(
|
291
|
+
f"Params does not set caller: {caller!r}."
|
292
|
+
) from err
|
289
293
|
|
290
294
|
# NOTE:
|
291
295
|
# If type of getter caller is not string type, and it does not use to
|
@@ -301,25 +305,33 @@ def str2template(
|
|
301
305
|
|
302
306
|
value: str = value.replace(found.full, getter, 1)
|
303
307
|
|
308
|
+
if value == "None":
|
309
|
+
return None
|
310
|
+
|
304
311
|
return search_env_replace(value)
|
305
312
|
|
306
313
|
|
307
|
-
def param2template(
|
314
|
+
def param2template(
|
315
|
+
value: T,
|
316
|
+
params: DictData,
|
317
|
+
filters: dict[str, FilterRegistry] | None = None,
|
318
|
+
) -> T:
|
308
319
|
"""Pass param to template string that can search by ``RE_CALLER`` regular
|
309
320
|
expression.
|
310
321
|
|
311
322
|
:param value: A value that want to map with params
|
312
323
|
:param params: A parameter value that getting with matched regular
|
313
324
|
expression.
|
325
|
+
:param filters: A filter mapping for mapping with `map_post_filter` func.
|
314
326
|
|
315
327
|
:rtype: T
|
316
328
|
:returns: An any getter value from the params input.
|
317
329
|
"""
|
318
|
-
filters: dict[str, FilterRegistry] = make_filter_registry()
|
330
|
+
filters: dict[str, FilterRegistry] = filters or make_filter_registry()
|
319
331
|
if isinstance(value, dict):
|
320
|
-
return {k: param2template(value[k], params) for k in value}
|
332
|
+
return {k: param2template(value[k], params, filters) for k in value}
|
321
333
|
elif isinstance(value, (list, tuple, set)):
|
322
|
-
return type(value)([param2template(i, params) for i in value])
|
334
|
+
return type(value)([param2template(i, params, filters) for i in value])
|
323
335
|
elif not isinstance(value, str):
|
324
336
|
return value
|
325
337
|
return str2template(value, params, filters=filters)
|
@@ -329,8 +341,9 @@ def param2template(value: T, params: DictData) -> T:
|
|
329
341
|
def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
330
342
|
"""Format datetime object to string with the format.
|
331
343
|
|
332
|
-
:param value: A datetime value that want to format to string
|
333
|
-
|
344
|
+
:param value: (datetime) A datetime value that want to format to string
|
345
|
+
value.
|
346
|
+
:param fmt: (str) A format string pattern that passing to the `dt.strftime`
|
334
347
|
method.
|
335
348
|
|
336
349
|
:rtype: str
|
@@ -340,3 +353,9 @@ def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
|
340
353
|
raise UtilException(
|
341
354
|
"This custom function should pass input value with datetime type."
|
342
355
|
)
|
356
|
+
|
357
|
+
|
358
|
+
@custom_filter("coalesce") # pragma: no cov
|
359
|
+
def coalesce(value: T | None, default: Any) -> T:
|
360
|
+
"""Coalesce with default value if the main value is None."""
|
361
|
+
return default if value is None else value
|