ddeutil-workflow 0.0.12__py3-none-any.whl → 0.0.14__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/stage.py CHANGED
@@ -12,14 +12,16 @@ can tracking logs.
12
12
  handle stage error on this stage model. I think stage model should have a lot of
13
13
  usecase and it does not worry when I want to create a new one.
14
14
 
15
- Execution --> Ok --> Result with 0
16
- --> Error --> Raise StageException
15
+ Execution --> Ok --> Result with 0
16
+ --> Error --> Raise StageException
17
+
18
+ On the context I/O that pass to stage object at execute process. The execute
19
+ method receive `{"params": {...}}` for mapping to template.
17
20
  """
18
21
  from __future__ import annotations
19
22
 
20
23
  import contextlib
21
24
  import inspect
22
- import os
23
25
  import subprocess
24
26
  import sys
25
27
  import uuid
@@ -38,12 +40,12 @@ try:
38
40
  except ImportError:
39
41
  from typing_extensions import ParamSpec
40
42
 
41
- from ddeutil.core import str2bool
42
43
  from pydantic import BaseModel, Field
43
44
  from pydantic.functional_validators import model_validator
44
45
  from typing_extensions import Self
45
46
 
46
47
  from .__types import DictData, DictStr, Re, TupleStr
48
+ from .conf import config
47
49
  from .exceptions import StageException
48
50
  from .log import get_logger
49
51
  from .utils import (
@@ -62,12 +64,15 @@ logger = get_logger("ddeutil.workflow")
62
64
 
63
65
 
64
66
  __all__: TupleStr = (
65
- "Stage",
67
+ "BaseStage",
66
68
  "EmptyStage",
67
69
  "BashStage",
68
70
  "PyStage",
69
71
  "HookStage",
70
72
  "TriggerStage",
73
+ "Stage",
74
+ "HookSearchData",
75
+ "extract_hook",
71
76
  "handler_result",
72
77
  )
73
78
 
@@ -76,8 +81,26 @@ def handler_result(message: str | None = None) -> Callable[P, Result]:
76
81
  """Decorator function for handler result from the stage execution. This
77
82
  function should to use with execution method only.
78
83
 
84
+ This stage exception handler still use ok-error concept but it allow
85
+ you force catching an output result with error message by specific
86
+ environment variable,`WORKFLOW_CORE_STAGE_RAISE_ERROR`.
87
+
88
+ Execution --> Ok --> Result with 0
89
+ --> Error --> Raise StageException
90
+ --> Result with 1 (if env var was set)
91
+
92
+ On the last step, it will set the running ID on a return result object
93
+ from current stage ID before release the final result.
94
+
79
95
  :param message: A message that want to add at prefix of exception statement.
96
+ :rtype: Callable[P, Result]
80
97
  """
98
+ # NOTE: The prefix message string that want to add on the first exception
99
+ # message dialog.
100
+ #
101
+ # ... ValueError: {message}
102
+ # ... raise value error from the stage execution process.
103
+ #
81
104
  message: str = message or ""
82
105
 
83
106
  def decorator(func: Callable[P, Result]) -> Callable[P, Result]:
@@ -92,9 +115,7 @@ def handler_result(message: str | None = None) -> Callable[P, Result]:
92
115
  logger.error(
93
116
  f"({self.run_id}) [STAGE]: {err.__class__.__name__}: {err}"
94
117
  )
95
- if str2bool(
96
- os.getenv("WORKFLOW_CORE_STAGE_RAISE_ERROR", "true")
97
- ):
118
+ if config.stage_raise_error:
98
119
  # NOTE: If error that raise from stage execution course by
99
120
  # itself, it will return that error with previous
100
121
  # dependency.
@@ -106,13 +127,16 @@ def handler_result(message: str | None = None) -> Callable[P, Result]:
106
127
  f"{self.__class__.__name__}: {message}\n\t"
107
128
  f"{err.__class__.__name__}: {err}"
108
129
  ) from None
109
- rs: Result = Result(
130
+
131
+ # NOTE: Catching exception error object to result with
132
+ # error_message and error keys.
133
+ return Result(
110
134
  status=1,
111
135
  context={
136
+ "error": err,
112
137
  "error_message": f"{err.__class__.__name__}: {err}",
113
138
  },
114
- )
115
- return rs.set_run_id(self.run_id)
139
+ ).set_run_id(self.run_id)
116
140
 
117
141
  return wrapped
118
142
 
@@ -148,10 +172,12 @@ class BaseStage(BaseModel, ABC):
148
172
  )
149
173
 
150
174
  @model_validator(mode="after")
151
- def __prepare_running_id(self):
175
+ def __prepare_running_id(self) -> Self:
152
176
  """Prepare stage running ID that use default value of field and this
153
177
  method will validate name and id fields should not contain any template
154
178
  parameter (exclude matrix template).
179
+
180
+ :rtype: Self
155
181
  """
156
182
  if self.run_id is None:
157
183
  self.run_id = gen_id(self.name + (self.id or ""), unique=True)
@@ -185,16 +211,28 @@ class BaseStage(BaseModel, ABC):
185
211
  raise NotImplementedError("Stage should implement ``execute`` method.")
186
212
 
187
213
  def set_outputs(self, output: DictData, to: DictData) -> DictData:
188
- """Set an outputs from execution process to an input params.
214
+ """Set an outputs from execution process to the receive context. The
215
+ result from execution will pass to value of ``outputs`` key.
216
+
217
+ For example of setting output method, If you receive execute output
218
+ and want to set on the `to` like;
219
+
220
+ ... (i) output: {'foo': bar}
221
+ ... (ii) to: {}
222
+
223
+ The result of the `to` variable will be;
224
+
225
+ ... (iii) to: {
226
+ 'stages': {
227
+ '<stage-id>': {'outputs': {'foo': 'bar'}}
228
+ }
229
+ }
189
230
 
190
231
  :param output: A output data that want to extract to an output key.
191
232
  :param to: A context data that want to add output result.
192
233
  :rtype: DictData
193
234
  """
194
- if not (
195
- self.id
196
- or str2bool(os.getenv("WORKFLOW_CORE_STAGE_DEFAULT_ID", "false"))
197
- ):
235
+ if not (self.id or config.stage_default_id):
198
236
  logger.debug(
199
237
  f"({self.run_id}) [STAGE]: Output does not set because this "
200
238
  f"stage does not set ID or default stage ID config flag not be "
@@ -206,28 +244,29 @@ class BaseStage(BaseModel, ABC):
206
244
  if "stages" not in to:
207
245
  to["stages"] = {}
208
246
 
209
- if self.id:
210
- _id: str = param2template(self.id, params=to)
211
- else:
212
- _id: str = gen_id(param2template(self.name, params=to))
213
-
214
- # NOTE: Set the output to that stage generated ID.
215
- logger.debug(
216
- f"({self.run_id}) [STAGE]: Set output complete with stage ID: {_id}"
247
+ # NOTE: If the stage ID did not set, it will use its name instead.
248
+ _id: str = (
249
+ param2template(self.id, params=to)
250
+ if self.id
251
+ else gen_id(param2template(self.name, params=to))
217
252
  )
253
+
254
+ # NOTE: Set the output to that stage generated ID with ``outputs`` key.
255
+ logger.debug(f"({self.run_id}) [STAGE]: Set outputs on: {_id}")
218
256
  to["stages"][_id] = {"outputs": output}
219
257
  return to
220
258
 
221
259
  def is_skipped(self, params: DictData | None = None) -> bool:
222
- """Return true if condition of this stage do not correct.
260
+ """Return true if condition of this stage do not correct. This process
261
+ use build-in eval function to execute the if-condition.
223
262
 
224
263
  :param params: A parameters that want to pass to condition template.
225
264
  :rtype: bool
226
265
  """
227
- params: DictData = params or {}
228
266
  if self.condition is None:
229
267
  return False
230
268
 
269
+ params: DictData = {} if params is None else params
231
270
  _g: DictData = globals() | params
232
271
  try:
233
272
  rs: bool = eval(param2template(self.condition, params), _g, {})
@@ -257,10 +296,15 @@ class EmptyStage(BaseStage):
257
296
 
258
297
  def execute(self, params: DictData) -> Result:
259
298
  """Execution method for the Empty stage that do only logging out to
260
- stdout.
299
+ stdout. This method does not use the `handler_result` decorator because
300
+ it does not get any error from logging function.
301
+
302
+ The result context should be empty and do not process anything
303
+ without calling logging function.
261
304
 
262
305
  :param params: A context data that want to add output result. But this
263
306
  stage does not pass any output.
307
+ :rtype: Result
264
308
  """
265
309
  logger.info(
266
310
  f"({self.run_id}) [STAGE]: Empty-Execute: {self.name!r}: "
@@ -302,6 +346,10 @@ class BashStage(BaseStage):
302
346
  """Return context of prepared bash statement that want to execute. This
303
347
  step will write the `.sh` file before giving this file name to context.
304
348
  After that, it will auto delete this file automatic.
349
+
350
+ :param bash: A bash statement that want to execute.
351
+ :param env: An environment variable that use on this bash statement.
352
+ :rtype: Iterator[TupleStr]
305
353
  """
306
354
  f_name: str = f"{uuid.uuid4()}.sh"
307
355
  f_shebang: str = "bash" if sys.platform.startswith("win") else "sh"
@@ -348,6 +396,7 @@ class BashStage(BaseStage):
348
396
  text=True,
349
397
  )
350
398
  if rs.returncode > 0:
399
+ # NOTE: Prepare stderr message that returning from subprocess.
351
400
  err: str = (
352
401
  rs.stderr.encode("utf-8").decode("utf-16")
353
402
  if "\\x00" in rs.stderr
@@ -368,8 +417,11 @@ class BashStage(BaseStage):
368
417
 
369
418
 
370
419
  class PyStage(BaseStage):
371
- """Python executor stage that running the Python statement that receive
372
- globals nad additional variables.
420
+ """Python executor stage that running the Python statement with receiving
421
+ globals and additional variables.
422
+
423
+ This stage allow you to use any Python object that exists on the globals
424
+ such as import your installed package.
373
425
 
374
426
  Data Validate:
375
427
  >>> stage = {
@@ -392,7 +444,8 @@ class PyStage(BaseStage):
392
444
  )
393
445
 
394
446
  def set_outputs(self, output: DictData, to: DictData) -> DictData:
395
- """Set an outputs from the Python execution process to an input params.
447
+ """Override set an outputs method for the Python execution process that
448
+ extract output from all the locals values.
396
449
 
397
450
  :param output: A output data that want to extract to an output key.
398
451
  :param to: A context data that want to add output result.
@@ -432,12 +485,13 @@ class PyStage(BaseStage):
432
485
  exec(run, _globals, _locals)
433
486
 
434
487
  return Result(
435
- status=0, context={"locals": _locals, "globals": _globals}
488
+ status=0,
489
+ context={"locals": _locals, "globals": _globals},
436
490
  )
437
491
 
438
492
 
439
- @dataclass
440
- class HookSearch:
493
+ @dataclass(frozen=True)
494
+ class HookSearchData:
441
495
  """Hook Search dataclass that use for receive regular expression grouping
442
496
  dict from searching hook string value.
443
497
  """
@@ -460,7 +514,7 @@ def extract_hook(hook: str) -> Callable[[], TagFunc]:
460
514
  )
461
515
 
462
516
  # NOTE: Pass the searching hook string to `path`, `func`, and `tag`.
463
- hook: HookSearch = HookSearch(**found.groupdict())
517
+ hook: HookSearchData = HookSearchData(**found.groupdict())
464
518
 
465
519
  # NOTE: Registry object should implement on this package only.
466
520
  rgt: dict[str, Registry] = make_registry(f"{hook.path}")
@@ -551,7 +605,9 @@ class HookStage(BaseStage):
551
605
 
552
606
 
553
607
  class TriggerStage(BaseStage):
554
- """Trigger Workflow execution stage that execute another workflow object.
608
+ """Trigger Workflow execution stage that execute another workflow. This this
609
+ the core stage that allow you to create the reusable workflow object or
610
+ dynamic parameters workflow for common usecase.
555
611
 
556
612
  Data Validate:
557
613
  >>> stage = {
@@ -564,7 +620,11 @@ class TriggerStage(BaseStage):
564
620
  ... }
565
621
  """
566
622
 
567
- trigger: str = Field(description="A trigger workflow name.")
623
+ trigger: str = Field(
624
+ description=(
625
+ "A trigger workflow name that should already exist on the config."
626
+ ),
627
+ )
568
628
  params: DictData = Field(
569
629
  default_factory=dict,
570
630
  description="A parameter that want to pass to workflow execution.",
@@ -572,11 +632,13 @@ class TriggerStage(BaseStage):
572
632
 
573
633
  @handler_result("Raise from TriggerStage")
574
634
  def execute(self, params: DictData) -> Result:
575
- """Trigger workflow execution.
635
+ """Trigger another workflow execution. It will waiting the trigger
636
+ workflow running complete before catching its result.
576
637
 
577
638
  :param params: A parameter data that want to use in this execution.
578
639
  :rtype: Result
579
640
  """
641
+ # NOTE: Lazy import this workflow object.
580
642
  from . import Workflow
581
643
 
582
644
  # NOTE: Loading workflow object from trigger name.
@@ -591,7 +653,11 @@ class TriggerStage(BaseStage):
591
653
  return wf.execute(params=param2template(self.params, params))
592
654
 
593
655
 
594
- # NOTE: Order of parsing stage data
656
+ # NOTE:
657
+ # An order of parsing stage model on the Job model with ``stages`` field.
658
+ # From the current build-in stages, they do not have stage that have the same
659
+ # fields that be cause of parsing on the Job's stages key.
660
+ #
595
661
  Stage = Union[
596
662
  PyStage,
597
663
  BashStage,
ddeutil/workflow/utils.py CHANGED
@@ -13,6 +13,7 @@ import time
13
13
  from abc import ABC, abstractmethod
14
14
  from ast import Call, Constant, Expr, Module, Name, parse
15
15
  from collections.abc import Iterator
16
+ from dataclasses import field
16
17
  from datetime import date, datetime
17
18
  from functools import cached_property, wraps
18
19
  from hashlib import md5
@@ -32,19 +33,22 @@ except ImportError:
32
33
  from ddeutil.core import getdot, hasdot, hash_str, import_string, lazy, str2bool
33
34
  from ddeutil.io import PathData, PathSearch, YamlFlResolve, search_env_replace
34
35
  from ddeutil.io.models.lineage import dt_now
35
- from pydantic import BaseModel, ConfigDict, Field, PrivateAttr
36
+ from pydantic import BaseModel, ConfigDict, Field
37
+ from pydantic.dataclasses import dataclass
36
38
  from pydantic.functional_serializers import field_serializer
37
39
  from pydantic.functional_validators import model_validator
38
40
  from typing_extensions import Self
39
41
 
40
42
  from .__types import DictData, Matrix, Re
43
+ from .conf import config
41
44
  from .exceptions import ParamValueException, UtilException
42
45
 
43
- logger = logging.getLogger("ddeutil.workflow")
44
46
  P = ParamSpec("P")
45
47
  AnyModel = TypeVar("AnyModel", bound=BaseModel)
46
48
  AnyModelType = type[AnyModel]
47
49
 
50
+ logger = logging.getLogger("ddeutil.workflow")
51
+
48
52
 
49
53
  def get_diff_sec(dt: datetime, tz: ZoneInfo | None = None) -> int:
50
54
  """Return second value that come from diff of an input datetime and the
@@ -110,7 +114,7 @@ class ConfParams(BaseModel):
110
114
  )
111
115
 
112
116
 
113
- def config() -> ConfParams:
117
+ def load_config() -> ConfParams:
114
118
  """Load Config data from ``workflows-conf.yaml`` file.
115
119
 
116
120
  Configuration Docs:
@@ -158,7 +162,7 @@ class SimLoad:
158
162
  :param externals: An external parameters
159
163
 
160
164
  Noted:
161
- ---
165
+
162
166
  The config data should have ``type`` key for modeling validation that
163
167
  make this loader know what is config should to do pass to.
164
168
 
@@ -248,11 +252,11 @@ class Loader(SimLoad):
248
252
  ) -> DictData:
249
253
  """Override the find class method from the Simple Loader object."""
250
254
  return super().finds(
251
- obj=obj, params=config(), include=include, exclude=exclude
255
+ obj=obj, params=load_config(), include=include, exclude=exclude
252
256
  )
253
257
 
254
258
  def __init__(self, name: str, externals: DictData) -> None:
255
- super().__init__(name, config(), externals)
259
+ super().__init__(name, load_config(), externals)
256
260
 
257
261
 
258
262
  def gen_id(
@@ -275,15 +279,14 @@ def gen_id(
275
279
  if not isinstance(value, str):
276
280
  value: str = str(value)
277
281
 
278
- tz: ZoneInfo = ZoneInfo(os.getenv("WORKFLOW_CORE_TIMEZONE", "UTC"))
279
282
  if str2bool(os.getenv("WORKFLOW_CORE_PIPELINE_ID_SIMPLE", "true")):
280
283
  return hash_str(f"{(value if sensitive else value.lower())}", n=10) + (
281
- f"{datetime.now(tz=tz):%Y%m%d%H%M%S%f}" if unique else ""
284
+ f"{datetime.now(tz=config.tz):%Y%m%d%H%M%S%f}" if unique else ""
282
285
  )
283
286
  return md5(
284
287
  (
285
288
  f"{(value if sensitive else value.lower())}"
286
- + (f"{datetime.now(tz=tz):%Y%m%d%H%M%S%f}" if unique else "")
289
+ + (f"{datetime.now(tz=config.tz):%Y%m%d%H%M%S%f}" if unique else "")
287
290
  ).encode()
288
291
  ).hexdigest()
289
292
 
@@ -317,13 +320,14 @@ class TagFunc(Protocol):
317
320
  def __call__(self, *args, **kwargs): ...
318
321
 
319
322
 
320
- def tag(name: str, alias: str | None = None):
323
+ def tag(name: str, alias: str | None = None) -> Callable[P, TagFunc]:
321
324
  """Tag decorator function that set function attributes, ``tag`` and ``name``
322
325
  for making registries variable.
323
326
 
324
- :param: name: A tag value for make different use-case of a function.
327
+ :param: name: A tag name for make different use-case of a function.
325
328
  :param: alias: A alias function name that keeping in registries. If this
326
329
  value does not supply, it will use original function name from __name__.
330
+ :rtype: Callable[P, TagFunc]
327
331
  """
328
332
 
329
333
  def func_internal(func: Callable[[...], Any]) -> TagFunc:
@@ -350,7 +354,7 @@ def make_registry(submodule: str) -> dict[str, Registry]:
350
354
  :rtype: dict[str, Registry]
351
355
  """
352
356
  rs: dict[str, Registry] = {}
353
- for module in config().engine.registry:
357
+ for module in load_config().engine.registry:
354
358
  # NOTE: try to sequential import task functions
355
359
  try:
356
360
  importer = import_module(f"{module}.{submodule}")
@@ -515,29 +519,50 @@ Param = Union[
515
519
  ]
516
520
 
517
521
 
518
- class Result(BaseModel):
519
- """Result Pydantic Model for passing parameter and receiving output from
520
- the workflow execution.
522
+ @dataclass
523
+ class Result:
524
+ """Result Pydantic Model for passing and receiving data context from any
525
+ module execution process like stage execution, job execution, or workflow
526
+ execution.
527
+
528
+ For comparison property, this result will use ``status``, ``context``,
529
+ and ``_run_id`` fields to comparing with other result instance.
521
530
  """
522
531
 
523
- status: int = Field(default=2)
524
- context: DictData = Field(default_factory=dict)
532
+ status: int = field(default=2)
533
+ context: DictData = field(default_factory=dict)
534
+ start_at: datetime = field(default_factory=dt_now, compare=False)
535
+ end_at: Optional[datetime] = field(default=None, compare=False)
525
536
 
526
537
  # NOTE: Ignore this field to compare another result model with __eq__.
527
- _parent_run_id: Optional[str] = PrivateAttr(default=None)
528
- _run_id: Optional[str] = PrivateAttr(default=None)
538
+ _run_id: Optional[str] = field(default=None)
539
+ _parent_run_id: Optional[str] = field(default=None, compare=False)
529
540
 
530
541
  @model_validator(mode="after")
531
- def __prepare_run_id(self):
532
- if self._run_id is None:
533
- self._run_id = gen_id("manual", unique=True)
542
+ def __prepare_run_id(self) -> Self:
543
+ """Prepare running ID which use default ID if it initialize at the first
544
+ time
545
+
546
+ :rtype: Self
547
+ """
548
+ self._run_id = gen_id("manual", unique=True)
534
549
  return self
535
550
 
536
551
  def set_run_id(self, running_id: str) -> Self:
552
+ """Set a running ID.
553
+
554
+ :param running_id: A running ID that want to update on this model.
555
+ :rtype: Self
556
+ """
537
557
  self._run_id = running_id
538
558
  return self
539
559
 
540
560
  def set_parent_run_id(self, running_id: str) -> Self:
561
+ """Set a parent running ID.
562
+
563
+ :param running_id: A running ID that want to update on this model.
564
+ :rtype: Self
565
+ """
541
566
  self._parent_run_id = running_id
542
567
  return self
543
568
 
@@ -549,33 +574,55 @@ class Result(BaseModel):
549
574
  def run_id(self):
550
575
  return self._run_id
551
576
 
552
- def receive(self, result: Result) -> Result:
577
+ def catch(self, status: int, context: DictData) -> Self:
578
+ """Catch the status and context to current data."""
579
+ self.__dict__["status"] = status
580
+ self.__dict__["context"].update(context)
581
+ return self
582
+
583
+ def receive(self, result: Result) -> Self:
584
+ """Receive context from another result object.
585
+
586
+ :rtype: Self
587
+ """
553
588
  self.__dict__["status"] = result.status
554
589
  self.__dict__["context"].update(result.context)
590
+
591
+ # NOTE: Update running ID from an incoming result.
555
592
  self._parent_run_id = result.parent_run_id
556
593
  self._run_id = result.run_id
557
594
  return self
558
595
 
559
- def receive_jobs(self, result: Result) -> Result:
596
+ def receive_jobs(self, result: Result) -> Self:
597
+ """Receive context from another result object that use on the workflow
598
+ execution which create a ``jobs`` keys on the context if it do not
599
+ exist.
600
+
601
+ :rtype: Self
602
+ """
560
603
  self.__dict__["status"] = result.status
561
604
 
562
605
  # NOTE: Check the context has jobs key.
563
606
  if "jobs" not in self.__dict__["context"]:
564
607
  self.__dict__["context"]["jobs"] = {}
565
-
566
608
  self.__dict__["context"]["jobs"].update(result.context)
609
+
610
+ # NOTE: Update running ID from an incoming result.
567
611
  self._parent_run_id = result.parent_run_id
568
612
  self._run_id = result.run_id
569
613
  return self
570
614
 
571
615
 
572
- def make_exec(path: str | Path):
573
- """Change mode of file to be executable file."""
616
+ def make_exec(path: str | Path) -> None: # pragma: no cov
617
+ """Change mode of file to be executable file.
618
+
619
+ :param path: A file path that want to make executable permission.
620
+ """
574
621
  f: Path = Path(path) if isinstance(path, str) else path
575
622
  f.chmod(f.stat().st_mode | stat.S_IEXEC)
576
623
 
577
624
 
578
- FILTERS: dict[str, callable] = {
625
+ FILTERS: dict[str, callable] = { # pragma: no cov
579
626
  "abs": abs,
580
627
  "str": str,
581
628
  "int": int,
@@ -590,17 +637,18 @@ class FilterFunc(Protocol):
590
637
 
591
638
  name: str
592
639
 
593
- def __call__(self, *args, **kwargs): ...
640
+ def __call__(self, *args, **kwargs): ... # pragma: no cov
594
641
 
595
642
 
596
- def custom_filter(name: str) -> Callable[P, TagFunc]:
643
+ def custom_filter(name: str) -> Callable[P, FilterFunc]:
597
644
  """Custom filter decorator function that set function attributes, ``filter``
598
645
  for making filter registries variable.
599
646
 
600
647
  :param: name: A filter name for make different use-case of a function.
648
+ :rtype: Callable[P, FilterFunc]
601
649
  """
602
650
 
603
- def func_internal(func: Callable[[...], Any]) -> TagFunc:
651
+ def func_internal(func: Callable[[...], Any]) -> FilterFunc:
604
652
  func.filter = name
605
653
 
606
654
  @wraps(func)
@@ -622,7 +670,7 @@ def make_filter_registry() -> dict[str, FilterRegistry]:
622
670
  :rtype: dict[str, Registry]
623
671
  """
624
672
  rs: dict[str, Registry] = {}
625
- for module in config().engine.registry_filter:
673
+ for module in load_config().engine.registry_filter:
626
674
  # NOTE: try to sequential import task functions
627
675
  try:
628
676
  importer = import_module(module)
@@ -644,7 +692,10 @@ def make_filter_registry() -> dict[str, FilterRegistry]:
644
692
  def get_args_const(
645
693
  expr: str,
646
694
  ) -> tuple[str, list[Constant], dict[str, Constant]]:
647
- """Get arguments and keyword-arguments from function calling string."""
695
+ """Get arguments and keyword-arguments from function calling string.
696
+
697
+ :rtype: tuple[str, list[Constant], dict[str, Constant]]
698
+ """
648
699
  try:
649
700
  mod: Module = parse(expr)
650
701
  except SyntaxError:
@@ -678,6 +729,7 @@ def get_args_const(
678
729
 
679
730
  @custom_filter("fmt")
680
731
  def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
732
+ """Format datetime object to string with the format."""
681
733
  if isinstance(value, datetime):
682
734
  return value.strftime(fmt)
683
735
  raise UtilException(
@@ -699,8 +751,8 @@ def map_post_filter(
699
751
  """
700
752
  for _filter in post_filter:
701
753
  func_name, _args, _kwargs = get_args_const(_filter)
702
- args = [arg.value for arg in _args]
703
- kwargs = {k: v.value for k, v in _kwargs.items()}
754
+ args: list = [arg.value for arg in _args]
755
+ kwargs: dict = {k: v.value for k, v in _kwargs.items()}
704
756
 
705
757
  if func_name not in filters:
706
758
  raise UtilException(
@@ -845,8 +897,12 @@ def param2template(
845
897
 
846
898
 
847
899
  def filter_func(value: Any) -> Any:
848
- """Filter own created function out of any value with replace it to its
849
- function name. If it is built-in function, it does not have any changing.
900
+ """Filter out an own created function of any value of mapping context by
901
+ replacing it to its function name. If it is built-in function, it does not
902
+ have any changing.
903
+
904
+ :param value: A value context data that want to filter out function value.
905
+ :type: The same type of an input ``value``.
850
906
  """
851
907
  if isinstance(value, dict):
852
908
  return {k: filter_func(value[k]) for k in value}
@@ -869,14 +925,20 @@ def dash2underscore(
869
925
  *,
870
926
  fixed: str | None = None,
871
927
  ) -> DictData:
872
- """Change key name that has dash to underscore."""
928
+ """Change key name that has dash to underscore.
929
+
930
+ :rtype: DictData
931
+ """
873
932
  if key in values:
874
933
  values[(fixed or key.replace("-", "_"))] = values.pop(key)
875
934
  return values
876
935
 
877
936
 
878
937
  def cross_product(matrix: Matrix) -> Iterator[DictData]:
879
- """Iterator of products value from matrix."""
938
+ """Iterator of products value from matrix.
939
+
940
+ :rtype: Iterator[DictData]
941
+ """
880
942
  yield from (
881
943
  {_k: _v for e in mapped for _k, _v in e.items()}
882
944
  for mapped in product(
@@ -897,7 +959,7 @@ def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
897
959
  """
898
960
  if n < 1:
899
961
  raise ValueError("n must be at least one")
900
- it = iter(iterable)
962
+ it: Iterator[Any] = iter(iterable)
901
963
  while True:
902
964
  chunk_it = islice(it, n)
903
965
  try:
@@ -905,3 +967,7 @@ def batch(iterable: Iterator[Any], n: int) -> Iterator[Any]:
905
967
  except StopIteration:
906
968
  return
907
969
  yield chain((first_el,), chunk_it)
970
+
971
+
972
+ def queue2str(queue: list[datetime]) -> Iterator[str]: # pragma: no cov
973
+ return (f"{q:%Y-%m-%d %H:%M:%S}" for q in queue)