ddeutil-workflow 0.0.40__py3-none-any.whl → 0.0.42__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,32 +3,36 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
+ # [x] Use dynamic 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
25
- from .conf import config
28
+ from .conf import dynamic
26
29
  from .exceptions import UtilException
27
30
 
28
31
  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 dynamic("regis_filter", f=registers):
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=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
+ extras: Optional[DictData] = 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 extras: (Optional[list[str]]) An Override extras.
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
+ registers: Optional[list[str]] = (
346
+ extras.get("regis_filter") if extras else None
347
+ )
348
+ filters: dict[str, FilterRegistry] = filters or make_filter_registry(
349
+ registers=registers
350
+ )
331
351
  if isinstance(value, dict):
332
- return {k: param2template(value[k], params, filters) for k in value}
352
+ return {
353
+ k: param2template(value[k], params, filters, extras=extras)
354
+ for k in value
355
+ }
333
356
  elif isinstance(value, (list, tuple, set)):
334
- return type(value)([param2template(i, params, filters) for i in value])
357
+ return type(value)(
358
+ [param2template(i, params, filters, extras=extras) for i in value]
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,167 @@ 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] = dynamic(
451
+ "regis_call", f=registries
452
+ ) # pragma: no cov
453
+ regis_calls.extend(["ddeutil.vendors"])
454
+
455
+ for module in regis_calls:
456
+ # NOTE: try to sequential import task functions
457
+ try:
458
+ importer = import_module(f"{module}.{submodule}")
459
+ except ModuleNotFoundError:
460
+ continue
461
+
462
+ for fstr, func in inspect.getmembers(importer, inspect.isfunction):
463
+ # NOTE: check function attribute that already set tag by
464
+ # ``utils.tag`` decorator.
465
+ if not (
466
+ hasattr(func, "tag") and hasattr(func, "name")
467
+ ): # pragma: no cov
468
+ continue
469
+
470
+ # NOTE: Define type of the func value.
471
+ func: TagFunc
472
+
473
+ # NOTE: Create new register name if it not exists
474
+ if func.name not in rs:
475
+ rs[func.name] = {func.tag: lazy(f"{module}.{submodule}.{fstr}")}
476
+ continue
477
+
478
+ if func.tag in rs[func.name]:
479
+ raise ValueError(
480
+ f"The tag {func.tag!r} already exists on "
481
+ f"{module}.{submodule}, you should change this tag name or "
482
+ f"change it func name."
483
+ )
484
+ rs[func.name][func.tag] = lazy(f"{module}.{submodule}.{fstr}")
485
+
486
+ return rs
487
+
488
+
489
+ @dataclass(frozen=True)
490
+ class CallSearchData:
491
+ """Call Search dataclass that use for receive regular expression grouping
492
+ dict from searching call string value.
493
+ """
494
+
495
+ path: str
496
+ func: str
497
+ tag: str
498
+
499
+
500
+ def extract_call(
501
+ call: str,
502
+ registries: Optional[list[str]] = None,
503
+ ) -> Callable[[], TagFunc]:
504
+ """Extract Call function from string value to call partial function that
505
+ does run it at runtime.
506
+
507
+ :param call: (str) A call value that able to match with Task regex.
508
+ :param registries: (Optional[list[str]]) A list of registry.
509
+
510
+ The format of call value should contain 3 regular expression groups
511
+ which match with the below config format:
512
+
513
+ >>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
514
+
515
+ Examples:
516
+ >>> extract_call("tasks/el-postgres-to-delta@polars")
517
+ ...
518
+ >>> extract_call("tasks/return-type-not-valid@raise")
519
+ ...
520
+
521
+ :raise NotImplementedError: When the searching call's function result does
522
+ not exist in the registry.
523
+ :raise NotImplementedError: When the searching call's tag result does not
524
+ exist in the registry with its function key.
525
+
526
+ :rtype: Callable[[], TagFunc]
527
+ """
528
+ if not (found := Re.RE_TASK_FMT.search(call)):
529
+ raise ValueError(
530
+ f"Call {call!r} does not match with the call regex format."
531
+ )
532
+
533
+ call: CallSearchData = CallSearchData(**found.groupdict())
534
+ rgt: dict[str, Registry] = make_registry(
535
+ submodule=f"{call.path}",
536
+ registries=registries,
537
+ )
538
+
539
+ if call.func not in rgt:
540
+ raise NotImplementedError(
541
+ f"`REGISTER-MODULES.{call.path}.registries` not implement "
542
+ f"registry: {call.func!r}."
543
+ )
544
+
545
+ if call.tag not in rgt[call.func]:
546
+ raise NotImplementedError(
547
+ f"tag: {call.tag!r} not found on registry func: "
548
+ f"`REGISTER-MODULES.{call.path}.registries.{call.func}`"
549
+ )
550
+ return rgt[call.func][call.tag]
@@ -3,8 +3,8 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """
7
- The main schedule running is `schedule_runner` function that trigger the
6
+ # [x] Use fix config
7
+ """The main schedule running is `schedule_runner` function that trigger the
8
8
  multiprocess of `schedule_control` function for listing schedules on the
9
9
  config by `Loader.finds(Schedule)`.
10
10
 
@@ -52,10 +52,10 @@ except ImportError: # pragma: no cov
52
52
 
53
53
  from .__cron import CronRunner
54
54
  from .__types import DictData, TupleStr
55
- from .audit import Audit, get_audit
56
55
  from .conf import Loader, SimLoad, config, get_logger
57
56
  from .cron import On
58
57
  from .exceptions import ScheduleException, WorkflowException
58
+ from .logs import Audit, get_audit
59
59
  from .result import Result, Status
60
60
  from .utils import batch, delay
61
61
  from .workflow import Release, ReleaseQueue, Workflow, WorkflowTask
@@ -86,9 +86,9 @@ class ScheduleWorkflow(BaseModel):
86
86
  model.
87
87
 
88
88
  This on field does not equal to the on field of Workflow model, but it
89
- uses same logic to generate running release date with crontab object. It use
90
- for override the on field if the schedule time was change but you do not
91
- want to change on the workflow model.
89
+ uses same logic to generate running release date with crontab object. It
90
+ uses for override the on field if the schedule time was change, but you do
91
+ not want to change on the workflow model.
92
92
  """
93
93
 
94
94
  alias: Optional[str] = Field(
@@ -177,7 +177,7 @@ class ScheduleWorkflow(BaseModel):
177
177
  start_date: datetime,
178
178
  queue: dict[str, ReleaseQueue],
179
179
  *,
180
- externals: DictData | None = None,
180
+ extras: DictData | None = None,
181
181
  ) -> list[WorkflowTask]:
182
182
  """Return the list of WorkflowTask object from the specific input
183
183
  datetime that mapping with the on field.
@@ -187,17 +187,17 @@ class ScheduleWorkflow(BaseModel):
187
187
 
188
188
  :param start_date: A start date that get from the workflow schedule.
189
189
  :param queue: A mapping of name and list of datetime for queue.
190
- :param externals: An external parameters that pass to the Loader object.
190
+ :param extras: An extra parameters that pass to the Loader object.
191
191
 
192
192
  :rtype: list[WorkflowTask]
193
193
  :return: Return the list of WorkflowTask object from the specific
194
194
  input datetime that mapping with the on field.
195
195
  """
196
196
  workflow_tasks: list[WorkflowTask] = []
197
- extras: DictData = externals or {}
197
+ extras: DictData = extras or {}
198
198
 
199
199
  # NOTE: Loading workflow model from the name of workflow.
200
- wf: Workflow = Workflow.from_loader(self.name, externals=extras)
200
+ wf: Workflow = Workflow.from_conf(self.name, extras=extras)
201
201
  wf_queue: ReleaseQueue = queue[self.alias]
202
202
 
203
203
  # IMPORTANT: Create the default 'on' value if it does not pass the `on`
@@ -254,24 +254,24 @@ class Schedule(BaseModel):
254
254
  return dedent(value)
255
255
 
256
256
  @classmethod
257
- def from_loader(
257
+ def from_conf(
258
258
  cls,
259
259
  name: str,
260
- externals: DictData | None = None,
260
+ extras: DictData | None = None,
261
261
  ) -> Self:
262
262
  """Create Schedule instance from the Loader object that only receive
263
263
  an input schedule name. The loader object will use this schedule name to
264
264
  searching configuration data of this schedule model in conf path.
265
265
 
266
266
  :param name: (str) A schedule name that want to pass to Loader object.
267
- :param externals: An external parameters that want to pass to Loader
267
+ :param extras: An extra parameters that want to pass to Loader
268
268
  object.
269
269
 
270
270
  :raise ValueError: If the type does not match with current object.
271
271
 
272
272
  :rtype: Self
273
273
  """
274
- loader: Loader = Loader(name, externals=(externals or {}))
274
+ loader: Loader = Loader(name, externals=(extras or {}))
275
275
 
276
276
  # NOTE: Validate the config type match with current connection model
277
277
  if loader.type != cls.__name__:
@@ -325,16 +325,16 @@ class Schedule(BaseModel):
325
325
  start_date: datetime,
326
326
  queue: dict[str, ReleaseQueue],
327
327
  *,
328
- externals: DictData | None = None,
328
+ extras: DictData | None = None,
329
329
  ) -> list[WorkflowTask]:
330
330
  """Return the list of WorkflowTask object from the specific input
331
331
  datetime that mapping with the on field from workflow schedule model.
332
332
 
333
333
  :param start_date: A start date that get from the workflow schedule.
334
- :param queue: A mapping of name and list of datetime for queue.
335
- :type queue: dict[str, ReleaseQueue]
336
- :param externals: An external parameters that pass to the Loader object.
337
- :type externals: DictData | None
334
+ :param queue: (dict[str, ReleaseQueue]) A mapping of name and list of
335
+ datetime for queue.
336
+ :param extras: (DictData) An extra parameters that pass to the Loader
337
+ object.
338
338
 
339
339
  :rtype: list[WorkflowTask]
340
340
  :return: Return the list of WorkflowTask object from the specific
@@ -348,7 +348,7 @@ class Schedule(BaseModel):
348
348
  queue[workflow.alias] = ReleaseQueue()
349
349
 
350
350
  workflow_tasks.extend(
351
- workflow.tasks(start_date, queue=queue, externals=externals)
351
+ workflow.tasks(start_date, queue=queue, extras=extras)
352
352
  )
353
353
 
354
354
  return workflow_tasks
@@ -357,14 +357,14 @@ class Schedule(BaseModel):
357
357
  self,
358
358
  *,
359
359
  stop: datetime | None = None,
360
- externals: DictData | None = None,
360
+ extras: DictData | None = None,
361
361
  audit: type[Audit] | None = None,
362
362
  parent_run_id: str | None = None,
363
363
  ) -> Result: # pragma: no cov
364
364
  """Pending this schedule tasks with the schedule package.
365
365
 
366
366
  :param stop: A datetime value that use to stop running schedule.
367
- :param externals: An external parameters that pass to Loader.
367
+ :param extras: An extra parameters that pass to Loader.
368
368
  :param audit: An audit class that use on the workflow task release for
369
369
  writing its release audit context.
370
370
  :param parent_run_id: A parent workflow running ID for this release.
@@ -385,9 +385,7 @@ class Schedule(BaseModel):
385
385
  ) + timedelta(minutes=1)
386
386
 
387
387
  scheduler_pending(
388
- tasks=self.tasks(
389
- start_date_waiting, queue=queue, externals=externals
390
- ),
388
+ tasks=self.tasks(start_date_waiting, queue=queue, extras=extras),
391
389
  stop=stop_date,
392
390
  queue=queue,
393
391
  threads=threads,
@@ -709,7 +707,7 @@ def scheduler_pending(
709
707
  def schedule_control(
710
708
  schedules: list[str],
711
709
  stop: datetime | None = None,
712
- externals: DictData | None = None,
710
+ extras: DictData | None = None,
713
711
  *,
714
712
  audit: type[Audit] | None = None,
715
713
  parent_run_id: str | None = None,
@@ -720,7 +718,7 @@ def schedule_control(
720
718
 
721
719
  :param schedules: A list of workflow names that want to schedule running.
722
720
  :param stop: A datetime value that use to stop running schedule.
723
- :param externals: An external parameters that pass to Loader.
721
+ :param extras: An extra parameters that pass to Loader.
724
722
  :param audit: An audit class that use on the workflow task release for
725
723
  writing its release audit context.
726
724
  :param parent_run_id: A parent workflow running ID for this release.
@@ -745,10 +743,10 @@ def schedule_control(
745
743
  tasks: list[WorkflowTask] = []
746
744
  for name in schedules:
747
745
  tasks.extend(
748
- Schedule.from_loader(name, externals=externals).tasks(
746
+ Schedule.from_conf(name, extras=extras).tasks(
749
747
  start_date_waiting,
750
748
  queue=queue,
751
- externals=externals,
749
+ extras=extras,
752
750
  ),
753
751
  )
754
752