ddeutil-workflow 0.0.26.post1__py3-none-any.whl → 0.0.28__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 +19 -14
- ddeutil/workflow/api/api.py +1 -53
- ddeutil/workflow/conf.py +44 -23
- ddeutil/workflow/cron.py +7 -7
- ddeutil/workflow/exceptions.py +1 -4
- ddeutil/workflow/hook.py +168 -0
- ddeutil/workflow/job.py +18 -17
- ddeutil/workflow/params.py +3 -3
- ddeutil/workflow/result.py +3 -3
- ddeutil/workflow/scheduler.py +9 -9
- ddeutil/workflow/stage.py +87 -170
- ddeutil/workflow/templates.py +336 -0
- ddeutil/workflow/utils.py +23 -404
- ddeutil/workflow/workflow.py +22 -23
- ddeutil_workflow-0.0.28.dist-info/METADATA +284 -0
- ddeutil_workflow-0.0.28.dist-info/RECORD +25 -0
- {ddeutil_workflow-0.0.26.post1.dist-info → ddeutil_workflow-0.0.28.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.26.post1.dist-info/METADATA +0 -230
- ddeutil_workflow-0.0.26.post1.dist-info/RECORD +0 -23
- {ddeutil_workflow-0.0.26.post1.dist-info → ddeutil_workflow-0.0.28.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.26.post1.dist-info → ddeutil_workflow-0.0.28.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.28"
|
ddeutil/workflow/__init__.py
CHANGED
@@ -8,6 +8,7 @@ from .conf import (
|
|
8
8
|
Config,
|
9
9
|
Loader,
|
10
10
|
Log,
|
11
|
+
config,
|
11
12
|
env,
|
12
13
|
get_log,
|
13
14
|
get_logger,
|
@@ -24,6 +25,13 @@ from .exceptions import (
|
|
24
25
|
UtilException,
|
25
26
|
WorkflowException,
|
26
27
|
)
|
28
|
+
from .hook import (
|
29
|
+
ReturnTagFunc,
|
30
|
+
TagFunc,
|
31
|
+
extract_hook,
|
32
|
+
make_registry,
|
33
|
+
tag,
|
34
|
+
)
|
27
35
|
from .job import (
|
28
36
|
Job,
|
29
37
|
Strategy,
|
@@ -48,33 +56,30 @@ from .stage import (
|
|
48
56
|
PyStage,
|
49
57
|
Stage,
|
50
58
|
TriggerStage,
|
51
|
-
extract_hook,
|
52
59
|
)
|
53
|
-
from .
|
60
|
+
from .templates import (
|
54
61
|
FILTERS,
|
55
62
|
FilterFunc,
|
56
63
|
FilterRegistry,
|
57
|
-
|
58
|
-
|
64
|
+
custom_filter,
|
65
|
+
get_args_const,
|
66
|
+
has_template,
|
67
|
+
make_filter_registry,
|
68
|
+
map_post_filter,
|
69
|
+
not_in_template,
|
70
|
+
param2template,
|
71
|
+
str2template,
|
72
|
+
)
|
73
|
+
from .utils import (
|
59
74
|
batch,
|
60
75
|
cross_product,
|
61
|
-
custom_filter,
|
62
76
|
dash2underscore,
|
63
77
|
delay,
|
64
78
|
filter_func,
|
65
79
|
gen_id,
|
66
|
-
get_args_const,
|
67
80
|
get_diff_sec,
|
68
81
|
get_dt_now,
|
69
|
-
has_template,
|
70
82
|
make_exec,
|
71
|
-
make_filter_registry,
|
72
|
-
make_registry,
|
73
|
-
map_post_filter,
|
74
|
-
not_in_template,
|
75
|
-
param2template,
|
76
|
-
str2template,
|
77
|
-
tag,
|
78
83
|
)
|
79
84
|
from .workflow import (
|
80
85
|
Workflow,
|
ddeutil/workflow/api/api.py
CHANGED
@@ -5,25 +5,21 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
-
import asyncio
|
9
8
|
import contextlib
|
10
|
-
import uuid
|
11
9
|
from collections.abc import AsyncIterator
|
12
10
|
from datetime import datetime, timedelta
|
13
|
-
from queue import Empty, Queue
|
14
11
|
from typing import TypedDict
|
15
12
|
|
16
13
|
from dotenv import load_dotenv
|
17
14
|
from fastapi import FastAPI
|
18
15
|
from fastapi.middleware.gzip import GZipMiddleware
|
19
16
|
from fastapi.responses import UJSONResponse
|
20
|
-
from pydantic import BaseModel
|
21
17
|
|
22
18
|
from ..__about__ import __version__
|
23
19
|
from ..conf import config, get_logger
|
24
20
|
from ..scheduler import ReleaseThread, ReleaseThreads
|
25
21
|
from ..workflow import WorkflowQueue, WorkflowTask
|
26
|
-
from .repeat import repeat_at
|
22
|
+
from .repeat import repeat_at
|
27
23
|
|
28
24
|
load_dotenv()
|
29
25
|
logger = get_logger("ddeutil.workflow")
|
@@ -32,11 +28,6 @@ logger = get_logger("ddeutil.workflow")
|
|
32
28
|
class State(TypedDict):
|
33
29
|
"""TypeDict for State of FastAPI application."""
|
34
30
|
|
35
|
-
# NOTE: For upper queue route.
|
36
|
-
upper_queue: Queue
|
37
|
-
upper_result: dict[str, str]
|
38
|
-
|
39
|
-
# NOTE: For schedule listener.
|
40
31
|
scheduler: list[str]
|
41
32
|
workflow_threads: ReleaseThreads
|
42
33
|
workflow_tasks: list[WorkflowTask]
|
@@ -46,15 +37,11 @@ class State(TypedDict):
|
|
46
37
|
@contextlib.asynccontextmanager
|
47
38
|
async def lifespan(a: FastAPI) -> AsyncIterator[State]:
|
48
39
|
"""Lifespan function for the FastAPI application."""
|
49
|
-
a.state.upper_queue = Queue()
|
50
|
-
a.state.upper_result = {}
|
51
40
|
a.state.scheduler = []
|
52
41
|
a.state.workflow_threads = {}
|
53
42
|
a.state.workflow_tasks = []
|
54
43
|
a.state.workflow_queue = {}
|
55
44
|
|
56
|
-
await asyncio.create_task(broker_upper_messages())
|
57
|
-
|
58
45
|
yield {
|
59
46
|
"upper_queue": a.state.upper_queue,
|
60
47
|
"upper_result": a.state.upper_result,
|
@@ -87,50 +74,11 @@ app = FastAPI(
|
|
87
74
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
88
75
|
|
89
76
|
|
90
|
-
@repeat_every(seconds=10)
|
91
|
-
async def broker_upper_messages():
|
92
|
-
"""Broker for receive message from the `/upper` path and change it to upper
|
93
|
-
case. This broker use interval running in background every 10 seconds.
|
94
|
-
"""
|
95
|
-
for _ in range(10):
|
96
|
-
try:
|
97
|
-
obj = app.state.upper_queue.get_nowait()
|
98
|
-
app.state.upper_result[obj["request_id"]] = obj["text"].upper()
|
99
|
-
logger.info(f"Upper message: {app.state.upper_result}")
|
100
|
-
except Empty:
|
101
|
-
pass
|
102
|
-
await asyncio.sleep(0.0001)
|
103
|
-
|
104
|
-
|
105
|
-
class Payload(BaseModel):
|
106
|
-
text: str
|
107
|
-
|
108
|
-
|
109
|
-
async def get_result(request_id: str) -> dict[str, str]:
|
110
|
-
"""Get data from output dict that global."""
|
111
|
-
while True:
|
112
|
-
if request_id in app.state.upper_result:
|
113
|
-
result: str = app.state.upper_result[request_id]
|
114
|
-
del app.state.upper_result[request_id]
|
115
|
-
return {"message": result}
|
116
|
-
await asyncio.sleep(0.0025)
|
117
|
-
|
118
|
-
|
119
77
|
@app.get("/")
|
120
78
|
async def health():
|
121
79
|
return {"message": "Workflow API already start up"}
|
122
80
|
|
123
81
|
|
124
|
-
@app.post(f"{config.prefix_path}/upper")
|
125
|
-
async def message_upper(payload: Payload):
|
126
|
-
"""Convert message from any case to the upper case."""
|
127
|
-
request_id: str = str(uuid.uuid4())
|
128
|
-
app.state.upper_queue.put(
|
129
|
-
{"text": payload.text, "request_id": request_id},
|
130
|
-
)
|
131
|
-
return await get_result(request_id)
|
132
|
-
|
133
|
-
|
134
82
|
# NOTE: Enable the workflow route.
|
135
83
|
if config.enable_route_workflow:
|
136
84
|
from .route import workflow_route
|
ddeutil/workflow/conf.py
CHANGED
@@ -24,9 +24,11 @@ from typing_extensions import Self
|
|
24
24
|
|
25
25
|
from .__types import DictData, TupleStr
|
26
26
|
|
27
|
+
PREFIX: str = "WORKFLOW"
|
28
|
+
|
27
29
|
|
28
30
|
def env(var: str, default: str | None = None) -> str | None: # pragma: no cov
|
29
|
-
return os.getenv(f"
|
31
|
+
return os.getenv(f"{PREFIX}_{var.upper().replace(' ', '_')}", default)
|
30
32
|
|
31
33
|
|
32
34
|
def glob_files(path: Path) -> Iterator[Path]: # pragma: no cov
|
@@ -80,14 +82,18 @@ def get_logger(name: str):
|
|
80
82
|
|
81
83
|
|
82
84
|
class Config: # pragma: no cov
|
83
|
-
"""Config object for keeping
|
85
|
+
"""Config object for keeping core configurations on the current session
|
84
86
|
without changing when if the application still running.
|
87
|
+
|
88
|
+
The config value can change when you call that config property again.
|
85
89
|
"""
|
86
90
|
|
91
|
+
__slots__ = ()
|
92
|
+
|
87
93
|
# NOTE: Core
|
88
94
|
@property
|
89
95
|
def root_path(self) -> Path:
|
90
|
-
return Path(env("
|
96
|
+
return Path(env("CORE_ROOT_PATH", "."))
|
91
97
|
|
92
98
|
@property
|
93
99
|
def conf_path(self) -> Path:
|
@@ -95,7 +101,7 @@ class Config: # pragma: no cov
|
|
95
101
|
|
96
102
|
:rtype: Path
|
97
103
|
"""
|
98
|
-
return self.root_path / env("
|
104
|
+
return self.root_path / env("CORE_CONF_PATH", "conf")
|
99
105
|
|
100
106
|
@property
|
101
107
|
def tz(self) -> ZoneInfo:
|
@@ -108,15 +114,13 @@ class Config: # pragma: no cov
|
|
108
114
|
# NOTE: Register
|
109
115
|
@property
|
110
116
|
def regis_hook(self) -> list[str]:
|
111
|
-
regis_hook_str: str = env(
|
112
|
-
"CORE_REGISTRY", "src,src.ddeutil.workflow,tests,tests.utils"
|
113
|
-
)
|
117
|
+
regis_hook_str: str = env("CORE_REGISTRY", "src")
|
114
118
|
return [r.strip() for r in regis_hook_str.split(",")]
|
115
119
|
|
116
120
|
@property
|
117
121
|
def regis_filter(self) -> list[str]:
|
118
122
|
regis_filter_str: str = env(
|
119
|
-
"CORE_REGISTRY_FILTER", "ddeutil.workflow.
|
123
|
+
"CORE_REGISTRY_FILTER", "ddeutil.workflow.templates"
|
120
124
|
)
|
121
125
|
return [r.strip() for r in regis_filter_str.split(",")]
|
122
126
|
|
@@ -312,6 +316,10 @@ class SimLoad:
|
|
312
316
|
)
|
313
317
|
|
314
318
|
|
319
|
+
config = Config()
|
320
|
+
logger = get_logger("ddeutil.workflow")
|
321
|
+
|
322
|
+
|
315
323
|
class Loader(SimLoad):
|
316
324
|
"""Loader Object that get the config `yaml` file from current path.
|
317
325
|
|
@@ -337,21 +345,17 @@ class Loader(SimLoad):
|
|
337
345
|
:rtype: Iterator[tuple[str, DictData]]
|
338
346
|
"""
|
339
347
|
return super().finds(
|
340
|
-
obj=obj, conf=
|
348
|
+
obj=obj, conf=config, included=included, excluded=excluded
|
341
349
|
)
|
342
350
|
|
343
351
|
def __init__(self, name: str, externals: DictData) -> None:
|
344
|
-
super().__init__(name, conf=
|
345
|
-
|
346
|
-
|
347
|
-
config = Config()
|
348
|
-
logger = get_logger("ddeutil.workflow")
|
352
|
+
super().__init__(name, conf=config, externals=externals)
|
349
353
|
|
350
354
|
|
351
355
|
class BaseLog(BaseModel, ABC):
|
352
356
|
"""Base Log Pydantic Model with abstraction class property that implement
|
353
357
|
only model fields. This model should to use with inherit to logging
|
354
|
-
|
358
|
+
subclass like file, sqlite, etc.
|
355
359
|
"""
|
356
360
|
|
357
361
|
name: str = Field(description="A workflow name.")
|
@@ -359,9 +363,7 @@ class BaseLog(BaseModel, ABC):
|
|
359
363
|
type: str = Field(description="A running type before logging.")
|
360
364
|
context: DictData = Field(
|
361
365
|
default_factory=dict,
|
362
|
-
description=
|
363
|
-
"A context data that receive from a workflow execution result.",
|
364
|
-
),
|
366
|
+
description="A context that receive from a workflow execution result.",
|
365
367
|
)
|
366
368
|
parent_run_id: Optional[str] = Field(default=None)
|
367
369
|
run_id: str
|
@@ -388,7 +390,7 @@ class BaseLog(BaseModel, ABC):
|
|
388
390
|
|
389
391
|
class FileLog(BaseLog):
|
390
392
|
"""File Log Pydantic Model that use to saving log data from result of
|
391
|
-
workflow execution. It
|
393
|
+
workflow execution. It inherits from BaseLog model that implement the
|
392
394
|
``self.save`` method for file.
|
393
395
|
"""
|
394
396
|
|
@@ -427,8 +429,8 @@ class FileLog(BaseLog):
|
|
427
429
|
workflow name and release values. If a release does not pass to an input
|
428
430
|
argument, it will return the latest release from the current log path.
|
429
431
|
|
430
|
-
:param name:
|
431
|
-
:param release:
|
432
|
+
:param name: A workflow name that want to search log.
|
433
|
+
:param release: A release datetime that want to search log.
|
432
434
|
|
433
435
|
:raise FileNotFoundError:
|
434
436
|
:raise NotImplementedError:
|
@@ -492,8 +494,14 @@ class FileLog(BaseLog):
|
|
492
494
|
|
493
495
|
:rtype: Self
|
494
496
|
"""
|
497
|
+
from .utils import cut_id
|
498
|
+
|
495
499
|
# NOTE: Check environ variable was set for real writing.
|
496
500
|
if not config.enable_write_log:
|
501
|
+
logger.debug(
|
502
|
+
f"({cut_id(self.run_id)}) [LOG]: Skip writing log cause "
|
503
|
+
f"config was set"
|
504
|
+
)
|
497
505
|
return self
|
498
506
|
|
499
507
|
log_file: Path = self.pointer() / f"{self.run_id}.log"
|
@@ -522,7 +530,20 @@ class SQLiteLog(BaseLog): # pragma: no cov
|
|
522
530
|
primary key ( run_id )
|
523
531
|
"""
|
524
532
|
|
525
|
-
def save(self, excluded: list[str] | None) ->
|
533
|
+
def save(self, excluded: list[str] | None) -> SQLiteLog:
|
534
|
+
"""Save logging data that receive a context data from a workflow
|
535
|
+
execution result.
|
536
|
+
"""
|
537
|
+
from .utils import cut_id
|
538
|
+
|
539
|
+
# NOTE: Check environ variable was set for real writing.
|
540
|
+
if not config.enable_write_log:
|
541
|
+
logger.debug(
|
542
|
+
f"({cut_id(self.run_id)}) [LOG]: Skip writing log cause "
|
543
|
+
f"config was set"
|
544
|
+
)
|
545
|
+
return self
|
546
|
+
|
526
547
|
raise NotImplementedError("SQLiteLog does not implement yet.")
|
527
548
|
|
528
549
|
|
@@ -532,7 +553,7 @@ Log = Union[
|
|
532
553
|
]
|
533
554
|
|
534
555
|
|
535
|
-
def get_log() -> Log: # pragma: no cov
|
556
|
+
def get_log() -> type[Log]: # pragma: no cov
|
536
557
|
if config.log_path.is_file():
|
537
558
|
return SQLiteLog
|
538
559
|
return FileLog
|
ddeutil/workflow/cron.py
CHANGED
@@ -32,10 +32,10 @@ def interval2crontab(
|
|
32
32
|
) -> str:
|
33
33
|
"""Return the crontab string that was generated from specific values.
|
34
34
|
|
35
|
-
:param interval:
|
35
|
+
:param interval: An interval value that is one of 'daily', 'weekly', or
|
36
36
|
'monthly'.
|
37
37
|
:param day: A day value that will be day of week. The default value is
|
38
|
-
monday if it
|
38
|
+
monday if it is weekly interval.
|
39
39
|
:param time: A time value that passing with format '%H:%M'.
|
40
40
|
|
41
41
|
Examples:
|
@@ -50,9 +50,9 @@ def interval2crontab(
|
|
50
50
|
"""
|
51
51
|
d: str = "*"
|
52
52
|
if interval == "weekly":
|
53
|
-
d = WEEKDAYS[(day or "monday")[:3].title()]
|
53
|
+
d = str(WEEKDAYS[(day or "monday")[:3].title()])
|
54
54
|
elif interval == "monthly" and day:
|
55
|
-
d = WEEKDAYS[day[:3].title()]
|
55
|
+
d = str(WEEKDAYS[day[:3].title()])
|
56
56
|
|
57
57
|
h, m = tuple(
|
58
58
|
i.lstrip("0") if i != "00" else "0" for i in time.split(":", maxsplit=1)
|
@@ -95,7 +95,7 @@ class On(BaseModel):
|
|
95
95
|
|
96
96
|
:param value: A mapping value that will generate crontab before create
|
97
97
|
schedule model.
|
98
|
-
:param externals:
|
98
|
+
:param externals: An extras external parameter that will keep in extras.
|
99
99
|
"""
|
100
100
|
passing: DictStr = {}
|
101
101
|
if "timezone" in value:
|
@@ -114,8 +114,8 @@ class On(BaseModel):
|
|
114
114
|
"""Constructor from the name of config that will use loader object for
|
115
115
|
getting the data.
|
116
116
|
|
117
|
-
:param name: A name of config that will
|
118
|
-
:param externals:
|
117
|
+
:param name: A name of config that will get from loader.
|
118
|
+
:param externals: An extras external parameter that will keep in extras.
|
119
119
|
"""
|
120
120
|
externals: DictData = externals or {}
|
121
121
|
loader: Loader = Loader(name, externals=externals)
|
ddeutil/workflow/exceptions.py
CHANGED
@@ -4,7 +4,7 @@
|
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
"""Exception objects for this package do not do anything because I want to
|
7
|
-
create the lightweight workflow package. So, this module do just
|
7
|
+
create the lightweight workflow package. So, this module do just an exception
|
8
8
|
annotate for handle error only.
|
9
9
|
"""
|
10
10
|
from __future__ import annotations
|
@@ -29,6 +29,3 @@ class WorkflowFailException(WorkflowException): ...
|
|
29
29
|
|
30
30
|
|
31
31
|
class ParamValueException(WorkflowException): ...
|
32
|
-
|
33
|
-
|
34
|
-
class CliException(BaseWorkflowException): ...
|
ddeutil/workflow/hook.py
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
# ------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2022 Korawich Anuttra. All rights reserved.
|
3
|
+
# Licensed under the MIT License. See LICENSE in the project root for
|
4
|
+
# license information.
|
5
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
import inspect
|
9
|
+
import logging
|
10
|
+
from dataclasses import dataclass
|
11
|
+
from functools import wraps
|
12
|
+
from importlib import import_module
|
13
|
+
from typing import Any, Callable, Protocol, TypeVar
|
14
|
+
|
15
|
+
try:
|
16
|
+
from typing import ParamSpec
|
17
|
+
except ImportError:
|
18
|
+
from typing_extensions import ParamSpec
|
19
|
+
|
20
|
+
from ddeutil.core import lazy
|
21
|
+
|
22
|
+
from .__types import Re
|
23
|
+
from .conf import config
|
24
|
+
|
25
|
+
T = TypeVar("T")
|
26
|
+
P = ParamSpec("P")
|
27
|
+
|
28
|
+
logger = logging.getLogger("ddeutil.workflow")
|
29
|
+
|
30
|
+
|
31
|
+
class TagFunc(Protocol):
|
32
|
+
"""Tag Function Protocol"""
|
33
|
+
|
34
|
+
name: str
|
35
|
+
tag: str
|
36
|
+
|
37
|
+
def __call__(self, *args, **kwargs): ... # pragma: no cov
|
38
|
+
|
39
|
+
|
40
|
+
ReturnTagFunc = Callable[P, TagFunc]
|
41
|
+
DecoratorTagFunc = Callable[[Callable[[...], Any]], ReturnTagFunc]
|
42
|
+
|
43
|
+
|
44
|
+
def tag(
|
45
|
+
name: str, alias: str | None = None
|
46
|
+
) -> DecoratorTagFunc: # pragma: no cov
|
47
|
+
"""Tag decorator function that set function attributes, ``tag`` and ``name``
|
48
|
+
for making registries variable.
|
49
|
+
|
50
|
+
:param: name: A tag name for make different use-case of a function.
|
51
|
+
:param: alias: A alias function name that keeping in registries. If this
|
52
|
+
value does not supply, it will use original function name from __name__.
|
53
|
+
:rtype: Callable[P, TagFunc]
|
54
|
+
"""
|
55
|
+
|
56
|
+
def func_internal(func: Callable[[...], Any]) -> ReturnTagFunc:
|
57
|
+
func.tag = name
|
58
|
+
func.name = alias or func.__name__.replace("_", "-")
|
59
|
+
|
60
|
+
@wraps(func)
|
61
|
+
def wrapped(*args, **kwargs):
|
62
|
+
# NOTE: Able to do anything before calling hook function.
|
63
|
+
return func(*args, **kwargs)
|
64
|
+
|
65
|
+
return wrapped
|
66
|
+
|
67
|
+
return func_internal
|
68
|
+
|
69
|
+
|
70
|
+
Registry = dict[str, Callable[[], TagFunc]]
|
71
|
+
|
72
|
+
|
73
|
+
def make_registry(submodule: str) -> dict[str, Registry]:
|
74
|
+
"""Return registries of all functions that able to called with task.
|
75
|
+
|
76
|
+
:param submodule: A module prefix that want to import registry.
|
77
|
+
:rtype: dict[str, Registry]
|
78
|
+
"""
|
79
|
+
rs: dict[str, Registry] = {}
|
80
|
+
for module in config.regis_hook:
|
81
|
+
# NOTE: try to sequential import task functions
|
82
|
+
try:
|
83
|
+
importer = import_module(f"{module}.{submodule}")
|
84
|
+
except ModuleNotFoundError:
|
85
|
+
continue
|
86
|
+
|
87
|
+
for fstr, func in inspect.getmembers(importer, inspect.isfunction):
|
88
|
+
# NOTE: check function attribute that already set tag by
|
89
|
+
# ``utils.tag`` decorator.
|
90
|
+
if not (hasattr(func, "tag") and hasattr(func, "name")):
|
91
|
+
continue
|
92
|
+
|
93
|
+
# NOTE: Define type of the func value.
|
94
|
+
func: TagFunc
|
95
|
+
|
96
|
+
# NOTE: Create new register name if it not exists
|
97
|
+
if func.name not in rs:
|
98
|
+
rs[func.name] = {func.tag: lazy(f"{module}.{submodule}.{fstr}")}
|
99
|
+
continue
|
100
|
+
|
101
|
+
if func.tag in rs[func.name]:
|
102
|
+
raise ValueError(
|
103
|
+
f"The tag {func.tag!r} already exists on "
|
104
|
+
f"{module}.{submodule}, you should change this tag name or "
|
105
|
+
f"change it func name."
|
106
|
+
)
|
107
|
+
rs[func.name][func.tag] = lazy(f"{module}.{submodule}.{fstr}")
|
108
|
+
|
109
|
+
return rs
|
110
|
+
|
111
|
+
|
112
|
+
@dataclass(frozen=True)
|
113
|
+
class HookSearchData:
|
114
|
+
"""Hook Search dataclass that use for receive regular expression grouping
|
115
|
+
dict from searching hook string value.
|
116
|
+
"""
|
117
|
+
|
118
|
+
path: str
|
119
|
+
func: str
|
120
|
+
tag: str
|
121
|
+
|
122
|
+
|
123
|
+
def extract_hook(hook: str) -> Callable[[], TagFunc]:
|
124
|
+
"""Extract Hook function from string value to hook partial function that
|
125
|
+
does run it at runtime.
|
126
|
+
|
127
|
+
:raise NotImplementedError: When the searching hook's function result does
|
128
|
+
not exist in the registry.
|
129
|
+
:raise NotImplementedError: When the searching hook's tag result does not
|
130
|
+
exist in the registry with its function key.
|
131
|
+
|
132
|
+
:param hook: A hook value that able to match with Task regex.
|
133
|
+
|
134
|
+
The format of hook value should contain 3 regular expression groups
|
135
|
+
which match with the below config format:
|
136
|
+
|
137
|
+
>>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
|
138
|
+
|
139
|
+
Examples:
|
140
|
+
>>> extract_hook("tasks/el-postgres-to-delta@polars")
|
141
|
+
...
|
142
|
+
>>> extract_hook("tasks/return-type-not-valid@raise")
|
143
|
+
...
|
144
|
+
|
145
|
+
:rtype: Callable[[], TagFunc]
|
146
|
+
"""
|
147
|
+
if not (found := Re.RE_TASK_FMT.search(hook)):
|
148
|
+
raise ValueError(
|
149
|
+
f"Hook {hook!r} does not match with hook format regex."
|
150
|
+
)
|
151
|
+
|
152
|
+
# NOTE: Pass the searching hook string to `path`, `func`, and `tag`.
|
153
|
+
hook: HookSearchData = HookSearchData(**found.groupdict())
|
154
|
+
|
155
|
+
# NOTE: Registry object should implement on this package only.
|
156
|
+
rgt: dict[str, Registry] = make_registry(f"{hook.path}")
|
157
|
+
if hook.func not in rgt:
|
158
|
+
raise NotImplementedError(
|
159
|
+
f"``REGISTER-MODULES.{hook.path}.registries`` does not "
|
160
|
+
f"implement registry: {hook.func!r}."
|
161
|
+
)
|
162
|
+
|
163
|
+
if hook.tag not in rgt[hook.func]:
|
164
|
+
raise NotImplementedError(
|
165
|
+
f"tag: {hook.tag!r} does not found on registry func: "
|
166
|
+
f"``REGISTER-MODULES.{hook.path}.registries.{hook.func}``"
|
167
|
+
)
|
168
|
+
return rgt[hook.func][hook.tag]
|