ddeutil-workflow 0.0.27__py3-none-any.whl → 0.0.29__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 +1 -0
- ddeutil/workflow/__types.py +11 -9
- ddeutil/workflow/api/api.py +1 -53
- ddeutil/workflow/conf.py +51 -19
- ddeutil/workflow/cron.py +7 -7
- ddeutil/workflow/exceptions.py +1 -1
- ddeutil/workflow/hook.py +21 -4
- ddeutil/workflow/job.py +17 -16
- ddeutil/workflow/params.py +3 -3
- ddeutil/workflow/result.py +3 -3
- ddeutil/workflow/scheduler.py +9 -9
- ddeutil/workflow/stage.py +84 -115
- ddeutil/workflow/templates.py +11 -12
- ddeutil/workflow/utils.py +20 -13
- ddeutil/workflow/workflow.py +24 -21
- ddeutil_workflow-0.0.29.dist-info/METADATA +292 -0
- ddeutil_workflow-0.0.29.dist-info/RECORD +25 -0
- {ddeutil_workflow-0.0.27.dist-info → ddeutil_workflow-0.0.29.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.27.dist-info/METADATA +0 -230
- ddeutil_workflow-0.0.27.dist-info/RECORD +0 -25
- {ddeutil_workflow-0.0.27.dist-info → ddeutil_workflow-0.0.29.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.27.dist-info → ddeutil_workflow-0.0.29.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.29"
|
ddeutil/workflow/__init__.py
CHANGED
ddeutil/workflow/__types.py
CHANGED
@@ -27,6 +27,8 @@ Matrix = dict[str, Union[list[str], list[int]]]
|
|
27
27
|
|
28
28
|
|
29
29
|
class Context(TypedDict):
|
30
|
+
"""TypeDict support the Context."""
|
31
|
+
|
30
32
|
params: dict[str, Any]
|
31
33
|
jobs: dict[str, Any]
|
32
34
|
|
@@ -71,14 +73,14 @@ class Re:
|
|
71
73
|
# - ${{ params.source?.schema }}
|
72
74
|
#
|
73
75
|
__re_caller: str = r"""
|
74
|
-
\$
|
75
|
-
{{
|
76
|
-
\s*
|
76
|
+
\$ # start with $
|
77
|
+
{{ # value open with {{
|
78
|
+
\s* # whitespace or not
|
77
79
|
(?P<caller>
|
78
80
|
(?P<caller_prefix>(?:[a-zA-Z_-]+\??\.)*)
|
79
81
|
(?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+\??)
|
80
82
|
)
|
81
|
-
\s*
|
83
|
+
\s* # whitespace or not
|
82
84
|
(?P<post_filters>
|
83
85
|
(?:
|
84
86
|
\|\s*
|
@@ -88,7 +90,7 @@ class Re:
|
|
88
90
|
)\s*
|
89
91
|
)*
|
90
92
|
)
|
91
|
-
}}
|
93
|
+
}} # value close with }}
|
92
94
|
"""
|
93
95
|
RE_CALLER: Pattern = re.compile(
|
94
96
|
__re_caller, MULTILINE | IGNORECASE | UNICODE | VERBOSE
|
@@ -103,13 +105,13 @@ class Re:
|
|
103
105
|
# - tasks/function@dummy
|
104
106
|
#
|
105
107
|
__re_task_fmt: str = r"""
|
106
|
-
^
|
108
|
+
^ # start task format
|
107
109
|
(?P<path>[^/@]+)
|
108
|
-
/
|
110
|
+
/ # start get function with /
|
109
111
|
(?P<func>[^@]+)
|
110
|
-
@
|
112
|
+
@ # start tag with @
|
111
113
|
(?P<tag>.+)
|
112
|
-
$
|
114
|
+
$ # end task format
|
113
115
|
"""
|
114
116
|
RE_TASK_FMT: Pattern = re.compile(
|
115
117
|
__re_task_fmt, MULTILINE | IGNORECASE | UNICODE | VERBOSE
|
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
@@ -13,7 +13,7 @@ from collections.abc import Iterator
|
|
13
13
|
from datetime import datetime, timedelta
|
14
14
|
from functools import cached_property, lru_cache
|
15
15
|
from pathlib import Path
|
16
|
-
from typing import ClassVar, Optional, Union
|
16
|
+
from typing import ClassVar, Optional, TypeVar, Union
|
17
17
|
from zoneinfo import ZoneInfo
|
18
18
|
|
19
19
|
from ddeutil.core import str2bool
|
@@ -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
|
@@ -37,6 +39,7 @@ __all__: TupleStr = (
|
|
37
39
|
"env",
|
38
40
|
"get_logger",
|
39
41
|
"get_log",
|
42
|
+
"C",
|
40
43
|
"Config",
|
41
44
|
"SimLoad",
|
42
45
|
"Loader",
|
@@ -79,15 +82,43 @@ def get_logger(name: str):
|
|
79
82
|
return lg
|
80
83
|
|
81
84
|
|
82
|
-
class
|
83
|
-
"""
|
85
|
+
class BaseConfig: # pragma: no cov
|
86
|
+
"""BaseConfig object inheritable."""
|
87
|
+
|
88
|
+
__slots__ = ()
|
89
|
+
|
90
|
+
@property
|
91
|
+
def root_path(self) -> Path:
|
92
|
+
"""Root path or the project path.
|
93
|
+
|
94
|
+
:rtype: Path
|
95
|
+
"""
|
96
|
+
return Path(os.getenv("ROOT_PATH", "."))
|
97
|
+
|
98
|
+
@property
|
99
|
+
def conf_path(self) -> Path:
|
100
|
+
"""Config path that use root_path class argument for this construction.
|
101
|
+
|
102
|
+
:rtype: Path
|
103
|
+
"""
|
104
|
+
return self.root_path / os.getenv("CONF_PATH", "conf")
|
105
|
+
|
106
|
+
|
107
|
+
class Config(BaseConfig): # pragma: no cov
|
108
|
+
"""Config object for keeping core configurations on the current session
|
84
109
|
without changing when if the application still running.
|
110
|
+
|
111
|
+
The config value can change when you call that config property again.
|
85
112
|
"""
|
86
113
|
|
87
114
|
# NOTE: Core
|
88
115
|
@property
|
89
116
|
def root_path(self) -> Path:
|
90
|
-
|
117
|
+
"""Root path or the project path.
|
118
|
+
|
119
|
+
:rtype: Path
|
120
|
+
"""
|
121
|
+
return Path(env("CORE_ROOT_PATH", "."))
|
91
122
|
|
92
123
|
@property
|
93
124
|
def conf_path(self) -> Path:
|
@@ -95,7 +126,7 @@ class Config: # pragma: no cov
|
|
95
126
|
|
96
127
|
:rtype: Path
|
97
128
|
"""
|
98
|
-
return self.root_path / env("
|
129
|
+
return self.root_path / env("CORE_CONF_PATH", "conf")
|
99
130
|
|
100
131
|
@property
|
101
132
|
def tz(self) -> ZoneInfo:
|
@@ -108,7 +139,7 @@ class Config: # pragma: no cov
|
|
108
139
|
# NOTE: Register
|
109
140
|
@property
|
110
141
|
def regis_hook(self) -> list[str]:
|
111
|
-
regis_hook_str: str = env("CORE_REGISTRY", "
|
142
|
+
regis_hook_str: str = env("CORE_REGISTRY", ".")
|
112
143
|
return [r.strip() for r in regis_hook_str.split(",")]
|
113
144
|
|
114
145
|
@property
|
@@ -214,6 +245,9 @@ class Config: # pragma: no cov
|
|
214
245
|
return str2bool(env("API_ENABLE_ROUTE_SCHEDULE", "true"))
|
215
246
|
|
216
247
|
|
248
|
+
C = TypeVar("C", bound=BaseConfig)
|
249
|
+
|
250
|
+
|
217
251
|
class SimLoad:
|
218
252
|
"""Simple Load Object that will search config data by given some identity
|
219
253
|
value like name of workflow or on.
|
@@ -237,7 +271,7 @@ class SimLoad:
|
|
237
271
|
def __init__(
|
238
272
|
self,
|
239
273
|
name: str,
|
240
|
-
conf:
|
274
|
+
conf: C,
|
241
275
|
externals: DictData | None = None,
|
242
276
|
) -> None:
|
243
277
|
self.data: DictData = {}
|
@@ -250,7 +284,7 @@ class SimLoad:
|
|
250
284
|
if not self.data:
|
251
285
|
raise ValueError(f"Config {name!r} does not found on conf path")
|
252
286
|
|
253
|
-
self.conf:
|
287
|
+
self.conf: C = conf
|
254
288
|
self.externals: DictData = externals or {}
|
255
289
|
self.data.update(self.externals)
|
256
290
|
|
@@ -258,7 +292,7 @@ class SimLoad:
|
|
258
292
|
def finds(
|
259
293
|
cls,
|
260
294
|
obj: object,
|
261
|
-
conf:
|
295
|
+
conf: C,
|
262
296
|
*,
|
263
297
|
included: list[str] | None = None,
|
264
298
|
excluded: list[str] | None = None,
|
@@ -267,7 +301,7 @@ class SimLoad:
|
|
267
301
|
method can use include and exclude list of identity name for filter and
|
268
302
|
adds-on.
|
269
303
|
|
270
|
-
:param obj:
|
304
|
+
:param obj: An object that want to validate matching before return.
|
271
305
|
:param conf: A config object.
|
272
306
|
:param included:
|
273
307
|
:param excluded:
|
@@ -332,7 +366,7 @@ class Loader(SimLoad):
|
|
332
366
|
) -> Iterator[tuple[str, DictData]]:
|
333
367
|
"""Override the find class method from the Simple Loader object.
|
334
368
|
|
335
|
-
:param obj:
|
369
|
+
:param obj: An object that want to validate matching before return.
|
336
370
|
:param included:
|
337
371
|
:param excluded:
|
338
372
|
|
@@ -349,7 +383,7 @@ class Loader(SimLoad):
|
|
349
383
|
class BaseLog(BaseModel, ABC):
|
350
384
|
"""Base Log Pydantic Model with abstraction class property that implement
|
351
385
|
only model fields. This model should to use with inherit to logging
|
352
|
-
|
386
|
+
subclass like file, sqlite, etc.
|
353
387
|
"""
|
354
388
|
|
355
389
|
name: str = Field(description="A workflow name.")
|
@@ -357,9 +391,7 @@ class BaseLog(BaseModel, ABC):
|
|
357
391
|
type: str = Field(description="A running type before logging.")
|
358
392
|
context: DictData = Field(
|
359
393
|
default_factory=dict,
|
360
|
-
description=
|
361
|
-
"A context data that receive from a workflow execution result.",
|
362
|
-
),
|
394
|
+
description="A context that receive from a workflow execution result.",
|
363
395
|
)
|
364
396
|
parent_run_id: Optional[str] = Field(default=None)
|
365
397
|
run_id: str
|
@@ -386,7 +418,7 @@ class BaseLog(BaseModel, ABC):
|
|
386
418
|
|
387
419
|
class FileLog(BaseLog):
|
388
420
|
"""File Log Pydantic Model that use to saving log data from result of
|
389
|
-
workflow execution. It
|
421
|
+
workflow execution. It inherits from BaseLog model that implement the
|
390
422
|
``self.save`` method for file.
|
391
423
|
"""
|
392
424
|
|
@@ -526,7 +558,7 @@ class SQLiteLog(BaseLog): # pragma: no cov
|
|
526
558
|
primary key ( run_id )
|
527
559
|
"""
|
528
560
|
|
529
|
-
def save(self, excluded: list[str] | None) ->
|
561
|
+
def save(self, excluded: list[str] | None) -> SQLiteLog:
|
530
562
|
"""Save logging data that receive a context data from a workflow
|
531
563
|
execution result.
|
532
564
|
"""
|
@@ -549,7 +581,7 @@ Log = Union[
|
|
549
581
|
]
|
550
582
|
|
551
583
|
|
552
|
-
def get_log() -> Log: # pragma: no cov
|
584
|
+
def get_log() -> type[Log]: # pragma: no cov
|
553
585
|
if config.log_path.is_file():
|
554
586
|
return SQLiteLog
|
555
587
|
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
|
ddeutil/workflow/hook.py
CHANGED
@@ -50,6 +50,7 @@ def tag(
|
|
50
50
|
:param: name: A tag name for make different use-case of a function.
|
51
51
|
:param: alias: A alias function name that keeping in registries. If this
|
52
52
|
value does not supply, it will use original function name from __name__.
|
53
|
+
|
53
54
|
:rtype: Callable[P, TagFunc]
|
54
55
|
"""
|
55
56
|
|
@@ -58,7 +59,7 @@ def tag(
|
|
58
59
|
func.name = alias or func.__name__.replace("_", "-")
|
59
60
|
|
60
61
|
@wraps(func)
|
61
|
-
def wrapped(*args, **kwargs):
|
62
|
+
def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
|
62
63
|
# NOTE: Able to do anything before calling hook function.
|
63
64
|
return func(*args, **kwargs)
|
64
65
|
|
@@ -74,10 +75,11 @@ def make_registry(submodule: str) -> dict[str, Registry]:
|
|
74
75
|
"""Return registries of all functions that able to called with task.
|
75
76
|
|
76
77
|
:param submodule: A module prefix that want to import registry.
|
78
|
+
|
77
79
|
:rtype: dict[str, Registry]
|
78
80
|
"""
|
79
81
|
rs: dict[str, Registry] = {}
|
80
|
-
for module in config.regis_hook:
|
82
|
+
for module in config.regis_hook | ["ddeutil.vendors"]:
|
81
83
|
# NOTE: try to sequential import task functions
|
82
84
|
try:
|
83
85
|
importer = import_module(f"{module}.{submodule}")
|
@@ -87,9 +89,12 @@ def make_registry(submodule: str) -> dict[str, Registry]:
|
|
87
89
|
for fstr, func in inspect.getmembers(importer, inspect.isfunction):
|
88
90
|
# NOTE: check function attribute that already set tag by
|
89
91
|
# ``utils.tag`` decorator.
|
90
|
-
if not hasattr(func, "tag"):
|
92
|
+
if not (hasattr(func, "tag") and hasattr(func, "name")):
|
91
93
|
continue
|
92
94
|
|
95
|
+
# NOTE: Define type of the func value.
|
96
|
+
func: TagFunc
|
97
|
+
|
93
98
|
# NOTE: Create new register name if it not exists
|
94
99
|
if func.name not in rs:
|
95
100
|
rs[func.name] = {func.tag: lazy(f"{module}.{submodule}.{fstr}")}
|
@@ -124,9 +129,21 @@ def extract_hook(hook: str) -> Callable[[], TagFunc]:
|
|
124
129
|
:raise NotImplementedError: When the searching hook's function result does
|
125
130
|
not exist in the registry.
|
126
131
|
:raise NotImplementedError: When the searching hook's tag result does not
|
127
|
-
|
132
|
+
exist in the registry with its function key.
|
128
133
|
|
129
134
|
:param hook: A hook value that able to match with Task regex.
|
135
|
+
|
136
|
+
The format of hook value should contain 3 regular expression groups
|
137
|
+
which match with the below config format:
|
138
|
+
|
139
|
+
>>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
|
140
|
+
|
141
|
+
Examples:
|
142
|
+
>>> extract_hook("tasks/el-postgres-to-delta@polars")
|
143
|
+
...
|
144
|
+
>>> extract_hook("tasks/return-type-not-valid@raise")
|
145
|
+
...
|
146
|
+
|
130
147
|
:rtype: Callable[[], TagFunc]
|
131
148
|
"""
|
132
149
|
if not (found := Re.RE_TASK_FMT.search(hook)):
|
ddeutil/workflow/job.py
CHANGED
@@ -83,7 +83,7 @@ def make(
|
|
83
83
|
if len(matrix) == 0:
|
84
84
|
return [{}]
|
85
85
|
|
86
|
-
# NOTE: Remove matrix that exists on the
|
86
|
+
# NOTE: Remove matrix that exists on the excluded.
|
87
87
|
final: list[DictStr] = []
|
88
88
|
for r in cross_product(matrix=matrix):
|
89
89
|
if any(
|
@@ -101,7 +101,7 @@ def make(
|
|
101
101
|
add: list[DictStr] = []
|
102
102
|
for inc in include:
|
103
103
|
# VALIDATE:
|
104
|
-
# Validate any key in include list should be a subset of
|
104
|
+
# Validate any key in include list should be a subset of someone
|
105
105
|
# in matrix.
|
106
106
|
if all(not (set(inc.keys()) <= set(m.keys())) for m in final):
|
107
107
|
raise ValueError(
|
@@ -128,9 +128,9 @@ class Strategy(BaseModel):
|
|
128
128
|
special job with combination of matrix data.
|
129
129
|
|
130
130
|
This model does not be the part of job only because you can use it to
|
131
|
-
any model object. The
|
132
|
-
comming from combination logic with any matrix values for running it
|
133
|
-
parallelism.
|
131
|
+
any model object. The objective of this model is generating metrix result
|
132
|
+
that comming from combination logic with any matrix values for running it
|
133
|
+
with parallelism.
|
134
134
|
|
135
135
|
[1, 2, 3] x [a, b] --> [1a], [1b], [2a], [2b], [3a], [3b]
|
136
136
|
|
@@ -180,7 +180,7 @@ class Strategy(BaseModel):
|
|
180
180
|
"""Rename key that use dash to underscore because Python does not
|
181
181
|
support this character exist in any variable name.
|
182
182
|
|
183
|
-
:param values: A parsing values to
|
183
|
+
:param values: A parsing values to these models
|
184
184
|
:rtype: DictData
|
185
185
|
"""
|
186
186
|
dash2underscore("max-parallel", values)
|
@@ -226,7 +226,7 @@ class Job(BaseModel):
|
|
226
226
|
"""Job Pydantic model object (short descripte: a group of stages).
|
227
227
|
|
228
228
|
This job model allow you to use for-loop that call matrix strategy. If
|
229
|
-
you pass matrix mapping and it able to generate, you will see it running
|
229
|
+
you pass matrix mapping, and it is able to generate, you will see it running
|
230
230
|
with loop of matrix values.
|
231
231
|
|
232
232
|
Data Validate:
|
@@ -355,7 +355,7 @@ class Job(BaseModel):
|
|
355
355
|
return all(need in jobs for need in self.needs)
|
356
356
|
|
357
357
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
358
|
-
"""Set an outputs from execution process to the
|
358
|
+
"""Set an outputs from execution process to the received context. The
|
359
359
|
result from execution will pass to value of ``strategies`` key.
|
360
360
|
|
361
361
|
For example of setting output method, If you receive execute output
|
@@ -420,7 +420,7 @@ class Job(BaseModel):
|
|
420
420
|
strategy and return with context of this strategy data.
|
421
421
|
|
422
422
|
The result of this execution will return result with strategy ID
|
423
|
-
that generated from the `gen_id` function with
|
423
|
+
that generated from the `gen_id` function with an input strategy value.
|
424
424
|
|
425
425
|
:raise JobException: If it has any error from ``StageException`` or
|
426
426
|
``UtilException``.
|
@@ -429,7 +429,7 @@ class Job(BaseModel):
|
|
429
429
|
This value will pass to the `matrix` key for templating.
|
430
430
|
:param params: A dynamic parameters that will deepcopy to the context.
|
431
431
|
:param run_id: A job running ID for this strategy execution.
|
432
|
-
:param event: An
|
432
|
+
:param event: An event manager that pass to the PoolThreadExecutor.
|
433
433
|
|
434
434
|
:rtype: Result
|
435
435
|
"""
|
@@ -496,7 +496,7 @@ class Job(BaseModel):
|
|
496
496
|
# PARAGRAPH:
|
497
497
|
#
|
498
498
|
# I do not use below syntax because `params` dict be the
|
499
|
-
# reference memory pointer and it was changed when I action
|
499
|
+
# reference memory pointer, and it was changed when I action
|
500
500
|
# anything like update or re-construct this.
|
501
501
|
#
|
502
502
|
# ... params |= stage.execute(params=params)
|
@@ -513,7 +513,9 @@ class Job(BaseModel):
|
|
513
513
|
#
|
514
514
|
try:
|
515
515
|
stage.set_outputs(
|
516
|
-
stage.
|
516
|
+
stage.handler_execute(
|
517
|
+
params=context, run_id=run_id
|
518
|
+
).context,
|
517
519
|
to=context,
|
518
520
|
)
|
519
521
|
except (StageException, UtilException) as err:
|
@@ -566,7 +568,7 @@ class Job(BaseModel):
|
|
566
568
|
run_id: str = run_id or gen_id(self.id or "", unique=True)
|
567
569
|
context: DictData = {}
|
568
570
|
|
569
|
-
# NOTE: Normal Job execution without parallel strategy matrix. It
|
571
|
+
# NOTE: Normal Job execution without parallel strategy matrix. It uses
|
570
572
|
# for-loop to control strategy execution sequentially.
|
571
573
|
if (not self.strategy.is_set()) or self.strategy.max_parallel == 1:
|
572
574
|
for strategy in self.strategy.make():
|
@@ -585,8 +587,7 @@ class Job(BaseModel):
|
|
585
587
|
event: Event = Event()
|
586
588
|
|
587
589
|
# IMPORTANT: Start running strategy execution by multithreading because
|
588
|
-
# it will
|
589
|
-
# execution.
|
590
|
+
# it will run by strategy values without waiting previous execution.
|
590
591
|
with ThreadPoolExecutor(
|
591
592
|
max_workers=self.strategy.max_parallel,
|
592
593
|
thread_name_prefix="job_strategy_exec_",
|
@@ -618,7 +619,7 @@ class Job(BaseModel):
|
|
618
619
|
timeout: int = 1800,
|
619
620
|
) -> Result:
|
620
621
|
"""Job parallel pool futures catching with fail-fast mode. That will
|
621
|
-
stop and set event on all not done futures if it
|
622
|
+
stop and set event on all not done futures if it receives the first
|
622
623
|
exception from all running futures.
|
623
624
|
|
624
625
|
:param event: An event manager instance that able to set stopper on the
|
ddeutil/workflow/params.py
CHANGED
@@ -75,7 +75,7 @@ class DatetimeParam(DefaultParam):
|
|
75
75
|
default: datetime = Field(default_factory=get_dt_now)
|
76
76
|
|
77
77
|
def receive(self, value: str | datetime | date | None = None) -> datetime:
|
78
|
-
"""Receive value that match with datetime. If
|
78
|
+
"""Receive value that match with datetime. If an input value pass with
|
79
79
|
None, it will use default value instead.
|
80
80
|
|
81
81
|
:param value: A value that want to validate with datetime parameter
|
@@ -98,7 +98,7 @@ class DatetimeParam(DefaultParam):
|
|
98
98
|
return datetime.fromisoformat(value)
|
99
99
|
except ValueError:
|
100
100
|
raise ParamValueException(
|
101
|
-
f"Invalid
|
101
|
+
f"Invalid the ISO format string: {value!r}"
|
102
102
|
) from None
|
103
103
|
|
104
104
|
|
@@ -158,7 +158,7 @@ class ChoiceParam(BaseParam):
|
|
158
158
|
:rtype: str
|
159
159
|
"""
|
160
160
|
# NOTE:
|
161
|
-
# Return the first value in options if does not pass any input value
|
161
|
+
# Return the first value in options if it does not pass any input value
|
162
162
|
if value is None:
|
163
163
|
return self.options[0]
|
164
164
|
if value not in self.options:
|
ddeutil/workflow/result.py
CHANGED
@@ -37,8 +37,8 @@ class Result:
|
|
37
37
|
|
38
38
|
@model_validator(mode="after")
|
39
39
|
def __prepare_run_id(self) -> Self:
|
40
|
-
"""Prepare running ID which use default ID if it
|
41
|
-
time
|
40
|
+
"""Prepare running ID which use default ID if it initializes at the
|
41
|
+
first time.
|
42
42
|
|
43
43
|
:rtype: Self
|
44
44
|
"""
|
@@ -84,7 +84,7 @@ class Result:
|
|
84
84
|
|
85
85
|
def receive_jobs(self, result: Result) -> Self:
|
86
86
|
"""Receive context from another result object that use on the workflow
|
87
|
-
execution which create a ``jobs`` keys on the context if it
|
87
|
+
execution which create a ``jobs`` keys on the context if it does not
|
88
88
|
exist.
|
89
89
|
|
90
90
|
:rtype: Self
|