ddeutil-workflow 0.0.39__py3-none-any.whl → 0.0.41__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.
@@ -3,22 +3,25 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
+ # [x] Use config
7
+ """Reusables module that keep any templating functions."""
6
8
  from __future__ import annotations
7
9
 
8
10
  import inspect
9
11
  import logging
10
12
  from ast import Call, Constant, Expr, Module, Name, parse
13
+ from dataclasses import dataclass
11
14
  from datetime import datetime
12
15
  from functools import wraps
13
16
  from importlib import import_module
14
- from typing import Any, Callable, Protocol, TypeVar, Union
17
+ from typing import Any, Callable, Optional, Protocol, TypeVar, Union
15
18
 
16
19
  try:
17
20
  from typing import ParamSpec
18
21
  except ImportError:
19
22
  from typing_extensions import ParamSpec
20
23
 
21
- from ddeutil.core import getdot, import_string
24
+ from ddeutil.core import getdot, import_string, lazy
22
25
  from ddeutil.io import search_env_replace
23
26
 
24
27
  from .__types import DictData, Re
@@ -29,6 +32,7 @@ T = TypeVar("T")
29
32
  P = ParamSpec("P")
30
33
 
31
34
  logger = logging.getLogger("ddeutil.workflow")
35
+ logging.getLogger("asyncio").setLevel(logging.INFO)
32
36
 
33
37
 
34
38
  FILTERS: dict[str, callable] = { # pragma: no cov
@@ -77,13 +81,17 @@ def custom_filter(name: str) -> Callable[P, FilterFunc]:
77
81
  return func_internal
78
82
 
79
83
 
80
- def make_filter_registry() -> dict[str, FilterRegistry]:
84
+ def make_filter_registry(
85
+ registers: Optional[list[str]] = None,
86
+ ) -> dict[str, FilterRegistry]:
81
87
  """Return registries of all functions that able to called with task.
82
88
 
89
+ :param registers: (Optional[list[str]]) Override list of register.
90
+
83
91
  :rtype: dict[str, FilterRegistry]
84
92
  """
85
93
  rs: dict[str, FilterRegistry] = {}
86
- for module in config.regis_filter:
94
+ for module in registers or config.regis_filter:
87
95
  # NOTE: try to sequential import task functions
88
96
  try:
89
97
  importer = import_module(module)
@@ -251,6 +259,7 @@ def str2template(
251
259
  params: DictData,
252
260
  *,
253
261
  filters: dict[str, FilterRegistry] | None = None,
262
+ registers: Optional[list[str]] = None,
254
263
  ) -> str:
255
264
  """(Sub-function) Pass param to template string that can search by
256
265
  ``RE_CALLER`` regular expression.
@@ -263,10 +272,13 @@ def str2template(
263
272
  :param params: (DictData) A parameter value that getting with matched
264
273
  regular expression.
265
274
  :param filters: A mapping of filter registry.
275
+ :param registers: (Optional[list[str]]) Override list of register.
266
276
 
267
277
  :rtype: str
268
278
  """
269
- filters: dict[str, FilterRegistry] = filters or make_filter_registry()
279
+ filters: dict[str, FilterRegistry] = filters or make_filter_registry(
280
+ registers
281
+ )
270
282
 
271
283
  # NOTE: remove space before and after this string value.
272
284
  value: str = value.strip()
@@ -315,6 +327,8 @@ def param2template(
315
327
  value: T,
316
328
  params: DictData,
317
329
  filters: dict[str, FilterRegistry] | None = None,
330
+ *,
331
+ registers: Optional[list[str]] = None,
318
332
  ) -> T:
319
333
  """Pass param to template string that can search by ``RE_CALLER`` regular
320
334
  expression.
@@ -323,18 +337,29 @@ def param2template(
323
337
  :param params: A parameter value that getting with matched regular
324
338
  expression.
325
339
  :param filters: A filter mapping for mapping with `map_post_filter` func.
340
+ :param registers: (Optional[list[str]]) Override list of register.
326
341
 
327
342
  :rtype: T
328
343
  :returns: An any getter value from the params input.
329
344
  """
330
- filters: dict[str, FilterRegistry] = filters or make_filter_registry()
345
+ filters: dict[str, FilterRegistry] = filters or make_filter_registry(
346
+ registers
347
+ )
331
348
  if isinstance(value, dict):
332
- return {k: param2template(value[k], params, filters) for k in value}
349
+ return {
350
+ k: param2template(value[k], params, filters, registers=registers)
351
+ for k in value
352
+ }
333
353
  elif isinstance(value, (list, tuple, set)):
334
- return type(value)([param2template(i, params, filters) for i in value])
354
+ return type(value)(
355
+ [
356
+ param2template(i, params, filters, registers=registers)
357
+ for i in value
358
+ ]
359
+ )
335
360
  elif not isinstance(value, str):
336
361
  return value
337
- return str2template(value, params, filters=filters)
362
+ return str2template(value, params, filters=filters, registers=registers)
338
363
 
339
364
 
340
365
  @custom_filter("fmt") # pragma: no cov
@@ -359,3 +384,164 @@ def datetime_format(value: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
359
384
  def coalesce(value: T | None, default: Any) -> T:
360
385
  """Coalesce with default value if the main value is None."""
361
386
  return default if value is None else value
387
+
388
+
389
+ class TagFunc(Protocol):
390
+ """Tag Function Protocol"""
391
+
392
+ name: str
393
+ tag: str
394
+
395
+ def __call__(self, *args, **kwargs): ... # pragma: no cov
396
+
397
+
398
+ ReturnTagFunc = Callable[P, TagFunc]
399
+ DecoratorTagFunc = Callable[[Callable[[...], Any]], ReturnTagFunc]
400
+
401
+
402
+ def tag(
403
+ name: str, alias: str | None = None
404
+ ) -> DecoratorTagFunc: # pragma: no cov
405
+ """Tag decorator function that set function attributes, ``tag`` and ``name``
406
+ for making registries variable.
407
+
408
+ :param: name: (str) A tag name for make different use-case of a function.
409
+ :param: alias: (str) A alias function name that keeping in registries.
410
+ If this value does not supply, it will use original function name
411
+ from `__name__` argument.
412
+
413
+ :rtype: Callable[P, TagFunc]
414
+ """
415
+
416
+ def func_internal(func: Callable[[...], Any]) -> ReturnTagFunc:
417
+ func.tag = name
418
+ func.name = alias or func.__name__.replace("_", "-")
419
+
420
+ @wraps(func)
421
+ def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
422
+ """Wrapped function."""
423
+ return func(*args, **kwargs)
424
+
425
+ @wraps(func)
426
+ async def async_wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
427
+ """Wrapped async function."""
428
+ return await func(*args, **kwargs)
429
+
430
+ return async_wrapped if inspect.iscoroutinefunction(func) else wrapped
431
+
432
+ return func_internal
433
+
434
+
435
+ Registry = dict[str, Callable[[], TagFunc]]
436
+
437
+
438
+ def make_registry(
439
+ submodule: str,
440
+ registries: Optional[list[str]] = None,
441
+ ) -> dict[str, Registry]:
442
+ """Return registries of all functions that able to called with task.
443
+
444
+ :param submodule: (str) A module prefix that want to import registry.
445
+ :param registries: (Optional[list[str]]) A list of registry.
446
+
447
+ :rtype: dict[str, Registry]
448
+ """
449
+ rs: dict[str, Registry] = {}
450
+ regis_calls: list[str] = registries or config.regis_call # pragma: no cov
451
+ regis_calls.extend(["ddeutil.vendors"])
452
+ for module in regis_calls:
453
+ # NOTE: try to sequential import task functions
454
+ try:
455
+ importer = import_module(f"{module}.{submodule}")
456
+ except ModuleNotFoundError:
457
+ continue
458
+
459
+ for fstr, func in inspect.getmembers(importer, inspect.isfunction):
460
+ # NOTE: check function attribute that already set tag by
461
+ # ``utils.tag`` decorator.
462
+ if not (
463
+ hasattr(func, "tag") and hasattr(func, "name")
464
+ ): # pragma: no cov
465
+ continue
466
+
467
+ # NOTE: Define type of the func value.
468
+ func: TagFunc
469
+
470
+ # NOTE: Create new register name if it not exists
471
+ if func.name not in rs:
472
+ rs[func.name] = {func.tag: lazy(f"{module}.{submodule}.{fstr}")}
473
+ continue
474
+
475
+ if func.tag in rs[func.name]:
476
+ raise ValueError(
477
+ f"The tag {func.tag!r} already exists on "
478
+ f"{module}.{submodule}, you should change this tag name or "
479
+ f"change it func name."
480
+ )
481
+ rs[func.name][func.tag] = lazy(f"{module}.{submodule}.{fstr}")
482
+
483
+ return rs
484
+
485
+
486
+ @dataclass(frozen=True)
487
+ class CallSearchData:
488
+ """Call Search dataclass that use for receive regular expression grouping
489
+ dict from searching call string value.
490
+ """
491
+
492
+ path: str
493
+ func: str
494
+ tag: str
495
+
496
+
497
+ def extract_call(
498
+ call: str,
499
+ registries: Optional[list[str]] = None,
500
+ ) -> Callable[[], TagFunc]:
501
+ """Extract Call function from string value to call partial function that
502
+ does run it at runtime.
503
+
504
+ :param call: (str) A call value that able to match with Task regex.
505
+ :param registries: (Optional[list[str]]) A list of registry.
506
+
507
+ The format of call value should contain 3 regular expression groups
508
+ which match with the below config format:
509
+
510
+ >>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
511
+
512
+ Examples:
513
+ >>> extract_call("tasks/el-postgres-to-delta@polars")
514
+ ...
515
+ >>> extract_call("tasks/return-type-not-valid@raise")
516
+ ...
517
+
518
+ :raise NotImplementedError: When the searching call's function result does
519
+ not exist in the registry.
520
+ :raise NotImplementedError: When the searching call's tag result does not
521
+ exist in the registry with its function key.
522
+
523
+ :rtype: Callable[[], TagFunc]
524
+ """
525
+ if not (found := Re.RE_TASK_FMT.search(call)):
526
+ raise ValueError(
527
+ f"Call {call!r} does not match with the call regex format."
528
+ )
529
+
530
+ call: CallSearchData = CallSearchData(**found.groupdict())
531
+ rgt: dict[str, Registry] = make_registry(
532
+ submodule=f"{call.path}",
533
+ registries=registries,
534
+ )
535
+
536
+ if call.func not in rgt:
537
+ raise NotImplementedError(
538
+ f"`REGISTER-MODULES.{call.path}.registries` not implement "
539
+ f"registry: {call.func!r}."
540
+ )
541
+
542
+ if call.tag not in rgt[call.func]:
543
+ raise NotImplementedError(
544
+ f"tag: {call.tag!r} not found on registry func: "
545
+ f"`REGISTER-MODULES.{call.path}.registries.{call.func}`"
546
+ )
547
+ return rgt[call.func][call.tag]
@@ -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
@@ -51,10 +52,10 @@ except ImportError: # pragma: no cov
51
52
 
52
53
  from .__cron import CronRunner
53
54
  from .__types import DictData, TupleStr
54
- from .audit import Audit, get_audit
55
- from .conf import Loader, config, get_logger
55
+ from .conf import Loader, SimLoad, config, get_logger
56
56
  from .cron import On
57
57
  from .exceptions import ScheduleException, WorkflowException
58
+ from .logs import Audit, get_audit
58
59
  from .result import Result, Status
59
60
  from .utils import batch, delay
60
61
  from .workflow import Release, ReleaseQueue, Workflow, WorkflowTask
@@ -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,