ddeutil-workflow 0.0.13__py3-none-any.whl → 0.0.15__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 +4 -1
- ddeutil/workflow/__types.py +59 -10
- ddeutil/workflow/api.py +2 -2
- ddeutil/workflow/conf.py +45 -0
- ddeutil/workflow/cron.py +19 -12
- ddeutil/workflow/job.py +191 -153
- ddeutil/workflow/log.py +28 -14
- ddeutil/workflow/scheduler.py +255 -119
- ddeutil/workflow/stage.py +77 -35
- ddeutil/workflow/utils.py +129 -51
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/METADATA +6 -4
- ddeutil_workflow-0.0.15.dist-info/RECORD +22 -0
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.13.dist-info/RECORD +0 -21
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.13.dist-info → ddeutil_workflow-0.0.15.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.15"
|
ddeutil/workflow/__init__.py
CHANGED
@@ -12,7 +12,10 @@ from .exceptions import (
|
|
12
12
|
)
|
13
13
|
from .job import Job, Strategy
|
14
14
|
from .on import On, interval2crontab
|
15
|
-
from .scheduler import
|
15
|
+
from .scheduler import (
|
16
|
+
Schedule,
|
17
|
+
Workflow,
|
18
|
+
)
|
16
19
|
from .stage import Stage, handler_result
|
17
20
|
from .utils import (
|
18
21
|
Param,
|
ddeutil/workflow/__types.py
CHANGED
@@ -6,14 +6,19 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import re
|
9
|
+
from collections.abc import Iterator
|
10
|
+
from dataclasses import dataclass
|
9
11
|
from re import (
|
10
12
|
IGNORECASE,
|
11
13
|
MULTILINE,
|
12
14
|
UNICODE,
|
13
15
|
VERBOSE,
|
16
|
+
Match,
|
14
17
|
Pattern,
|
15
18
|
)
|
16
|
-
from typing import Any, Union
|
19
|
+
from typing import Any, Optional, Union
|
20
|
+
|
21
|
+
from typing_extensions import Self
|
17
22
|
|
18
23
|
TupleStr = tuple[str, ...]
|
19
24
|
DictData = dict[str, Any]
|
@@ -23,23 +28,57 @@ MatrixInclude = list[dict[str, Union[str, int]]]
|
|
23
28
|
MatrixExclude = list[dict[str, Union[str, int]]]
|
24
29
|
|
25
30
|
|
31
|
+
@dataclass(frozen=True)
|
32
|
+
class CallerRe:
|
33
|
+
"""Caller dataclass that catching result from the matching regex with the
|
34
|
+
Re.RE_CALLER value.
|
35
|
+
"""
|
36
|
+
|
37
|
+
full: str
|
38
|
+
caller: str
|
39
|
+
caller_prefix: Optional[str]
|
40
|
+
caller_last: str
|
41
|
+
post_filters: str
|
42
|
+
|
43
|
+
@classmethod
|
44
|
+
def from_regex(cls, match: Match[str]) -> Self:
|
45
|
+
"""Class construct from matching result.
|
46
|
+
|
47
|
+
:rtype: Self
|
48
|
+
"""
|
49
|
+
return cls(full=match.group(0), **match.groupdict())
|
50
|
+
|
51
|
+
|
26
52
|
class Re:
|
27
|
-
"""Regular expression config."""
|
53
|
+
"""Regular expression config for this package."""
|
28
54
|
|
29
|
-
# NOTE:
|
30
|
-
#
|
55
|
+
# NOTE:
|
56
|
+
# Regular expression:
|
57
|
+
# - Version 1:
|
58
|
+
# \${{\s*(?P<caller>[a-zA-Z0-9_.\s'\"\[\]\(\)\-\{}]+?)\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
|
59
|
+
# - Version 2 (2024-09-30):
|
60
|
+
# \${{\s*(?P<caller>(?P<caller_prefix>(?:[a-zA-Z_-]+\.)*)(?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+))\s*(?P<post_filters>(?:\|\s*(?:[a-zA-Z0-9_]{3,}[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]+)\s*)*)}}
|
61
|
+
#
|
62
|
+
# Examples:
|
63
|
+
# - ${{ params.data_dt }}
|
64
|
+
# - ${{ params.source.table }}
|
65
|
+
#
|
31
66
|
__re_caller: str = r"""
|
32
67
|
\$
|
33
68
|
{{
|
34
69
|
\s*
|
35
70
|
(?P<caller>
|
36
|
-
[a-zA-
|
37
|
-
|
71
|
+
(?P<caller_prefix>(?:[a-zA-Z_-]+\.)*)
|
72
|
+
(?P<caller_last>[a-zA-Z0-9_\-.'\"(\)[\]{}]+)
|
73
|
+
)
|
74
|
+
\s*
|
38
75
|
(?P<post_filters>
|
39
76
|
(?:
|
40
77
|
\|\s*
|
41
|
-
(?:
|
42
|
-
|
78
|
+
(?:
|
79
|
+
[a-zA-Z0-9_]{3,}
|
80
|
+
[a-zA-Z0-9_.,-\\%\s'\"[\]()\{}]*
|
81
|
+
)\s*
|
43
82
|
)*
|
44
83
|
)
|
45
84
|
}}
|
@@ -48,8 +87,13 @@ class Re:
|
|
48
87
|
__re_caller, MULTILINE | IGNORECASE | UNICODE | VERBOSE
|
49
88
|
)
|
50
89
|
|
51
|
-
# NOTE:
|
52
|
-
#
|
90
|
+
# NOTE:
|
91
|
+
# Regular expression:
|
92
|
+
# - Version 1:
|
93
|
+
# ^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$
|
94
|
+
#
|
95
|
+
# Examples:
|
96
|
+
# - tasks/function@dummy
|
53
97
|
__re_task_fmt: str = r"""
|
54
98
|
^
|
55
99
|
(?P<path>[^/@]+)
|
@@ -62,3 +106,8 @@ class Re:
|
|
62
106
|
RE_TASK_FMT: Pattern = re.compile(
|
63
107
|
__re_task_fmt, MULTILINE | IGNORECASE | UNICODE | VERBOSE
|
64
108
|
)
|
109
|
+
|
110
|
+
@classmethod
|
111
|
+
def finditer_caller(cls, value) -> Iterator[CallerRe]:
|
112
|
+
for found in cls.RE_CALLER.finditer(value):
|
113
|
+
yield CallerRe.from_regex(found)
|
ddeutil/workflow/api.py
CHANGED
@@ -25,7 +25,7 @@ from pydantic import BaseModel
|
|
25
25
|
from .__about__ import __version__
|
26
26
|
from .log import get_logger
|
27
27
|
from .repeat import repeat_at, repeat_every
|
28
|
-
from .scheduler import
|
28
|
+
from .scheduler import WorkflowTaskData
|
29
29
|
|
30
30
|
load_dotenv()
|
31
31
|
logger = get_logger("ddeutil.workflow")
|
@@ -36,7 +36,7 @@ class State(TypedDict):
|
|
36
36
|
upper_result: dict[str, str]
|
37
37
|
scheduler: list[str]
|
38
38
|
workflow_threads: dict[str, Thread]
|
39
|
-
workflow_tasks: list[
|
39
|
+
workflow_tasks: list[WorkflowTaskData]
|
40
40
|
workflow_queue: dict[str, list[datetime]]
|
41
41
|
workflow_running: dict[str, list[datetime]]
|
42
42
|
|
ddeutil/workflow/conf.py
ADDED
@@ -0,0 +1,45 @@
|
|
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 os
|
9
|
+
from zoneinfo import ZoneInfo
|
10
|
+
|
11
|
+
from ddeutil.core import str2bool
|
12
|
+
from dotenv import load_dotenv
|
13
|
+
|
14
|
+
load_dotenv()
|
15
|
+
env = os.getenv
|
16
|
+
|
17
|
+
|
18
|
+
class Config:
|
19
|
+
"""Config object for keeping application configuration on current session
|
20
|
+
without changing when if the application still running.
|
21
|
+
"""
|
22
|
+
|
23
|
+
# NOTE: Core
|
24
|
+
tz: ZoneInfo = ZoneInfo(env("WORKFLOW_CORE_TIMEZONE", "UTC"))
|
25
|
+
|
26
|
+
# NOTE: Stage
|
27
|
+
stage_raise_error: bool = str2bool(
|
28
|
+
env("WORKFLOW_CORE_STAGE_RAISE_ERROR", "true")
|
29
|
+
)
|
30
|
+
stage_default_id: bool = str2bool(
|
31
|
+
env("WORKFLOW_CORE_STAGE_DEFAULT_ID", "false")
|
32
|
+
)
|
33
|
+
|
34
|
+
# NOTE: Workflow
|
35
|
+
max_job_parallel: int = int(env("WORKFLOW_CORE_MAX_JOB_PARALLEL", "2"))
|
36
|
+
|
37
|
+
def __init__(self):
|
38
|
+
if self.max_job_parallel < 0:
|
39
|
+
raise ValueError(
|
40
|
+
f"MAX_JOB_PARALLEL should more than 0 but got "
|
41
|
+
f"{self.max_job_parallel}."
|
42
|
+
)
|
43
|
+
|
44
|
+
|
45
|
+
config = Config()
|
ddeutil/workflow/cron.py
CHANGED
@@ -14,7 +14,7 @@ from typing import ClassVar, Optional, Union
|
|
14
14
|
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
15
15
|
|
16
16
|
from ddeutil.core import (
|
17
|
-
|
17
|
+
checker,
|
18
18
|
isinstance_check,
|
19
19
|
must_split,
|
20
20
|
)
|
@@ -38,16 +38,21 @@ class CronYearLimit(Exception): ...
|
|
38
38
|
|
39
39
|
|
40
40
|
def str2cron(value: str) -> str:
|
41
|
-
"""Convert Special String to Crontab.
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
41
|
+
"""Convert Special String with the @ prefix to Crontab value.
|
42
|
+
|
43
|
+
:param value: A string value that want to convert to cron value.
|
44
|
+
:rtype: str
|
45
|
+
|
46
|
+
Table:
|
47
|
+
|
48
|
+
@reboot Run once, at system startup
|
49
|
+
@yearly Run once every year, "0 0 1 1 *"
|
50
|
+
@annually (same as @yearly)
|
51
|
+
@monthly Run once every month, "0 0 1 * *"
|
52
|
+
@weekly Run once every week, "0 0 * * 0"
|
53
|
+
@daily Run once each day, "0 0 * * *"
|
54
|
+
@midnight (same as @daily)
|
55
|
+
@hourly Run once an hour, "0 * * * *"
|
51
56
|
"""
|
52
57
|
mapping_spacial_str = {
|
53
58
|
"@reboot": "",
|
@@ -321,7 +326,9 @@ class CronPart:
|
|
321
326
|
self._parse_range(value_range)
|
322
327
|
)
|
323
328
|
|
324
|
-
if (
|
329
|
+
if (
|
330
|
+
value_step and not checker.is_int(value_step)
|
331
|
+
) or value_step == "":
|
325
332
|
raise ValueError(
|
326
333
|
f"Invalid interval step value {value_step!r} for "
|
327
334
|
f"{self.unit.name!r}"
|