ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.35__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 -10
- ddeutil/workflow/api/api.py +13 -8
- ddeutil/workflow/api/routes/__init__.py +8 -0
- ddeutil/workflow/api/routes/logs.py +36 -0
- ddeutil/workflow/api/{route.py → routes/schedules.py} +2 -131
- ddeutil/workflow/api/routes/workflows.py +137 -0
- ddeutil/workflow/audit.py +28 -37
- ddeutil/workflow/{hook.py → caller.py} +27 -27
- ddeutil/workflow/conf.py +47 -12
- ddeutil/workflow/job.py +149 -138
- ddeutil/workflow/logs.py +214 -0
- ddeutil/workflow/params.py +40 -12
- ddeutil/workflow/result.py +40 -61
- ddeutil/workflow/scheduler.py +185 -163
- ddeutil/workflow/{stage.py → stages.py} +105 -42
- ddeutil/workflow/utils.py +20 -2
- ddeutil/workflow/workflow.py +142 -117
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.35.dist-info}/METADATA +36 -32
- ddeutil_workflow-0.0.35.dist-info/RECORD +30 -0
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.35.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.33.dist-info/RECORD +0 -26
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.35.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.33.dist-info → ddeutil_workflow-0.0.35.dist-info}/top_level.txt +0 -0
ddeutil/workflow/logs.py
ADDED
@@ -0,0 +1,214 @@
|
|
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
|
+
"""This is the Logs module. This module provide TraceLog dataclasses.
|
7
|
+
"""
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
import os
|
11
|
+
from abc import ABC, abstractmethod
|
12
|
+
from collections.abc import Iterator
|
13
|
+
from datetime import datetime
|
14
|
+
from inspect import Traceback, currentframe, getframeinfo
|
15
|
+
from pathlib import Path
|
16
|
+
from threading import get_ident
|
17
|
+
from typing import Optional, Union
|
18
|
+
|
19
|
+
from pydantic.dataclasses import dataclass
|
20
|
+
|
21
|
+
from .__types import TupleStr
|
22
|
+
from .conf import config, get_logger
|
23
|
+
from .utils import cut_id, get_dt_now
|
24
|
+
|
25
|
+
logger = get_logger("ddeutil.workflow")
|
26
|
+
|
27
|
+
__all__: TupleStr = (
|
28
|
+
"FileTraceLog",
|
29
|
+
"TraceLog",
|
30
|
+
"get_dt_tznow",
|
31
|
+
"get_trace",
|
32
|
+
)
|
33
|
+
|
34
|
+
|
35
|
+
def get_dt_tznow() -> datetime:
|
36
|
+
"""Return the current datetime object that passing the config timezone.
|
37
|
+
|
38
|
+
:rtype: datetime
|
39
|
+
"""
|
40
|
+
return get_dt_now(tz=config.tz)
|
41
|
+
|
42
|
+
|
43
|
+
@dataclass(frozen=True)
|
44
|
+
class BaseTraceLog(ABC): # pragma: no cov
|
45
|
+
"""Base Trace Log dataclass object."""
|
46
|
+
|
47
|
+
run_id: str
|
48
|
+
parent_run_id: Optional[str] = None
|
49
|
+
|
50
|
+
@abstractmethod
|
51
|
+
def writer(self, message: str, is_err: bool = False) -> None:
|
52
|
+
raise NotImplementedError(
|
53
|
+
"Create writer logic for this trace object before using."
|
54
|
+
)
|
55
|
+
|
56
|
+
@abstractmethod
|
57
|
+
def make_message(self, message: str) -> str:
|
58
|
+
raise NotImplementedError(
|
59
|
+
"Adjust make message method for this trace object before using."
|
60
|
+
)
|
61
|
+
|
62
|
+
def debug(self, message: str):
|
63
|
+
msg: str = self.make_message(message)
|
64
|
+
|
65
|
+
# NOTE: Write file if debug mode.
|
66
|
+
if config.debug:
|
67
|
+
self.writer(msg)
|
68
|
+
|
69
|
+
logger.debug(msg, stacklevel=2)
|
70
|
+
|
71
|
+
def info(self, message: str):
|
72
|
+
msg: str = self.make_message(message)
|
73
|
+
self.writer(msg)
|
74
|
+
logger.info(msg, stacklevel=2)
|
75
|
+
|
76
|
+
def warning(self, message: str):
|
77
|
+
msg: str = self.make_message(message)
|
78
|
+
self.writer(msg)
|
79
|
+
logger.warning(msg, stacklevel=2)
|
80
|
+
|
81
|
+
def error(self, message: str):
|
82
|
+
msg: str = self.make_message(message)
|
83
|
+
self.writer(msg, is_err=True)
|
84
|
+
logger.error(msg, stacklevel=2)
|
85
|
+
|
86
|
+
|
87
|
+
class FileTraceLog(BaseTraceLog): # pragma: no cov
|
88
|
+
"""Trace Log object that write file to the local storage."""
|
89
|
+
|
90
|
+
@classmethod
|
91
|
+
def find_logs(cls) -> Iterator[dict[str, str]]: # pragma: no cov
|
92
|
+
for file in config.log_path.glob("./run_id=*"):
|
93
|
+
data: dict[str, str] = {}
|
94
|
+
|
95
|
+
if (file / "stdout.txt").exists():
|
96
|
+
data["stdout"] = (file / "stdout.txt").read_text(
|
97
|
+
encoding="utf-8"
|
98
|
+
)
|
99
|
+
|
100
|
+
if (file / "stderr.txt").exists():
|
101
|
+
data["stdout"] = (file / "stdout.txt").read_text(
|
102
|
+
encoding="utf-8"
|
103
|
+
)
|
104
|
+
|
105
|
+
yield data
|
106
|
+
|
107
|
+
@classmethod
|
108
|
+
def find_log_with_id(cls, run_id: str) -> dict[str, str]:
|
109
|
+
file: Path = config.log_path / f"run_id={run_id}"
|
110
|
+
data: dict[str, str] = {}
|
111
|
+
|
112
|
+
if (file / "stdout.txt").exists():
|
113
|
+
data["stdout"] = (file / "stdout.txt").read_text(encoding="utf-8")
|
114
|
+
|
115
|
+
if (file / "stderr.txt").exists():
|
116
|
+
data["stdout"] = (file / "stdout.txt").read_text(encoding="utf-8")
|
117
|
+
|
118
|
+
return data
|
119
|
+
|
120
|
+
@property
|
121
|
+
def log_file(self) -> Path:
|
122
|
+
log_file: Path = (
|
123
|
+
config.log_path / f"run_id={self.parent_run_id or self.run_id}"
|
124
|
+
)
|
125
|
+
if not log_file.exists():
|
126
|
+
log_file.mkdir(parents=True)
|
127
|
+
return log_file
|
128
|
+
|
129
|
+
@property
|
130
|
+
def cut_id(self) -> str:
|
131
|
+
"""Combine cutting ID of parent running ID if it set."""
|
132
|
+
cut_run_id: str = cut_id(self.run_id)
|
133
|
+
if not self.parent_run_id:
|
134
|
+
return f"{cut_run_id} -> {' ' * 6}"
|
135
|
+
|
136
|
+
cut_parent_run_id: str = cut_id(self.parent_run_id)
|
137
|
+
return f"{cut_parent_run_id} -> {cut_run_id}"
|
138
|
+
|
139
|
+
def make_message(self, message: str) -> str:
|
140
|
+
return f"({self.cut_id}) {message}"
|
141
|
+
|
142
|
+
def writer(self, message: str, is_err: bool = False) -> None:
|
143
|
+
"""The path of logging data will store by format:
|
144
|
+
|
145
|
+
... ./logs/run_id=<run-id>/stdout.txt
|
146
|
+
... ./logs/run_id=<run-id>/stderr.txt
|
147
|
+
|
148
|
+
:param message:
|
149
|
+
:param is_err:
|
150
|
+
"""
|
151
|
+
if not config.enable_write_log:
|
152
|
+
return
|
153
|
+
|
154
|
+
frame_info: Traceback = getframeinfo(currentframe().f_back.f_back)
|
155
|
+
filename: str = frame_info.filename.split(os.path.sep)[-1]
|
156
|
+
lineno: int = frame_info.lineno
|
157
|
+
|
158
|
+
# NOTE: set process and thread IDs.
|
159
|
+
process: int = os.getpid()
|
160
|
+
thread: int = get_ident()
|
161
|
+
|
162
|
+
write_file: str = "stderr.txt" if is_err else "stdout.txt"
|
163
|
+
with (self.log_file / write_file).open(
|
164
|
+
mode="at", encoding="utf-8"
|
165
|
+
) as f:
|
166
|
+
msg_fmt: str = f"{config.log_format_file}\n"
|
167
|
+
print(msg_fmt)
|
168
|
+
f.write(
|
169
|
+
msg_fmt.format(
|
170
|
+
**{
|
171
|
+
"datetime": get_dt_tznow().strftime(
|
172
|
+
config.log_datetime_format
|
173
|
+
),
|
174
|
+
"process": process,
|
175
|
+
"thread": thread,
|
176
|
+
"message": message,
|
177
|
+
"filename": filename,
|
178
|
+
"lineno": lineno,
|
179
|
+
}
|
180
|
+
)
|
181
|
+
)
|
182
|
+
|
183
|
+
|
184
|
+
class SQLiteTraceLog(BaseTraceLog): # pragma: no cov
|
185
|
+
|
186
|
+
@classmethod
|
187
|
+
def find_logs(cls) -> Iterator[dict[str, str]]: ...
|
188
|
+
|
189
|
+
@classmethod
|
190
|
+
def find_log_with_id(cls, run_id: str) -> dict[str, str]: ...
|
191
|
+
|
192
|
+
def make_message(self, message: str) -> str: ...
|
193
|
+
|
194
|
+
def writer(self, message: str, is_err: bool = False) -> None: ...
|
195
|
+
|
196
|
+
|
197
|
+
TraceLog = Union[
|
198
|
+
FileTraceLog,
|
199
|
+
SQLiteTraceLog,
|
200
|
+
]
|
201
|
+
|
202
|
+
|
203
|
+
def get_trace(
|
204
|
+
run_id: str, parent_run_id: str | None = None
|
205
|
+
) -> TraceLog: # pragma: no cov
|
206
|
+
if config.log_path.is_file():
|
207
|
+
return SQLiteTraceLog(run_id, parent_run_id=parent_run_id)
|
208
|
+
return FileTraceLog(run_id, parent_run_id=parent_run_id)
|
209
|
+
|
210
|
+
|
211
|
+
def get_trace_obj() -> type[TraceLog]: # pragma: no cov
|
212
|
+
if config.log_path.is_file():
|
213
|
+
return SQLiteTraceLog
|
214
|
+
return FileTraceLog
|
ddeutil/workflow/params.py
CHANGED
@@ -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
|
-
"""Param
|
7
|
-
Workflow and Schedule objects.
|
6
|
+
"""This module include all Param Models that use for parsing incoming parameters
|
7
|
+
that pass to the Workflow and Schedule objects.
|
8
8
|
"""
|
9
9
|
from __future__ import annotations
|
10
10
|
|
@@ -12,7 +12,7 @@ import decimal
|
|
12
12
|
import logging
|
13
13
|
from abc import ABC, abstractmethod
|
14
14
|
from datetime import date, datetime
|
15
|
-
from typing import Any, Literal, Optional, Union
|
15
|
+
from typing import Annotated, Any, Literal, Optional, Union
|
16
16
|
|
17
17
|
from pydantic import BaseModel, Field
|
18
18
|
|
@@ -32,8 +32,9 @@ __all__: TupleStr = (
|
|
32
32
|
|
33
33
|
|
34
34
|
class BaseParam(BaseModel, ABC):
|
35
|
-
"""Base Parameter that use to make any Params
|
36
|
-
with the type field that made from literal string.
|
35
|
+
"""Base Parameter that use to make any Params Models. The parameter type
|
36
|
+
will dynamic with the setup type field that made from literal string.
|
37
|
+
"""
|
37
38
|
|
38
39
|
desc: Optional[str] = Field(
|
39
40
|
default=None, description="A description of parameter providing."
|
@@ -169,9 +170,11 @@ class ChoiceParam(BaseParam):
|
|
169
170
|
"""Choice parameter."""
|
170
171
|
|
171
172
|
type: Literal["choice"] = "choice"
|
172
|
-
options: list[str] = Field(
|
173
|
+
options: Union[list[str], list[int]] = Field(
|
174
|
+
description="A list of choice parameters that able be str or int.",
|
175
|
+
)
|
173
176
|
|
174
|
-
def receive(self, value: str | None = None) -> str:
|
177
|
+
def receive(self, value: Union[str, int] | None = None) -> Union[str, int]:
|
175
178
|
"""Receive value that match with options.
|
176
179
|
|
177
180
|
:param value: A value that want to select from the options field.
|
@@ -188,9 +191,34 @@ class ChoiceParam(BaseParam):
|
|
188
191
|
return value
|
189
192
|
|
190
193
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
194
|
+
# TODO: Not implement this parameter yet
|
195
|
+
class MappingParam(DefaultParam): # pragma: no cov
|
196
|
+
|
197
|
+
type: Literal["map"] = "map"
|
198
|
+
default: dict[Any, Any] = Field(default_factory=dict)
|
199
|
+
|
200
|
+
def receive(self, value: Optional[dict[Any, Any]] = None) -> dict[Any, Any]:
|
201
|
+
if value is None:
|
202
|
+
return self.default
|
203
|
+
|
204
|
+
|
205
|
+
# TODO: Not implement this parameter yet
|
206
|
+
class ArrayParam(DefaultParam): # pragma: no cov
|
207
|
+
|
208
|
+
type: Literal["array"] = "array"
|
209
|
+
default: list[Any] = Field(default_factory=list)
|
210
|
+
|
211
|
+
def receive(self, value: Optional[list[Any]] = None) -> list[Any]:
|
212
|
+
if value is None:
|
213
|
+
return self.default
|
214
|
+
|
215
|
+
|
216
|
+
Param = Annotated[
|
217
|
+
Union[
|
218
|
+
ChoiceParam,
|
219
|
+
DatetimeParam,
|
220
|
+
IntParam,
|
221
|
+
StrParam,
|
222
|
+
],
|
223
|
+
Field(discriminator="type"),
|
196
224
|
]
|
ddeutil/workflow/result.py
CHANGED
@@ -3,6 +3,9 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
+
"""This is the Result module. It is the data context transfer objects that use
|
7
|
+
by all object in this package. This module provide Result dataclass.
|
8
|
+
"""
|
6
9
|
from __future__ import annotations
|
7
10
|
|
8
11
|
from dataclasses import field
|
@@ -13,17 +16,20 @@ from typing import Optional
|
|
13
16
|
|
14
17
|
from pydantic import ConfigDict
|
15
18
|
from pydantic.dataclasses import dataclass
|
19
|
+
from pydantic.functional_validators import model_validator
|
16
20
|
from typing_extensions import Self
|
17
21
|
|
18
22
|
from .__types import DictData, TupleStr
|
19
|
-
from .conf import
|
20
|
-
from .
|
23
|
+
from .conf import get_logger
|
24
|
+
from .logs import TraceLog, get_dt_tznow, get_trace
|
25
|
+
from .utils import gen_id
|
21
26
|
|
22
|
-
logger = get_logger("ddeutil.workflow
|
27
|
+
logger = get_logger("ddeutil.workflow")
|
23
28
|
|
24
29
|
__all__: TupleStr = (
|
25
30
|
"Result",
|
26
31
|
"Status",
|
32
|
+
"default_gen_id",
|
27
33
|
)
|
28
34
|
|
29
35
|
|
@@ -36,14 +42,6 @@ def default_gen_id() -> str:
|
|
36
42
|
return gen_id("manual", unique=True)
|
37
43
|
|
38
44
|
|
39
|
-
def get_dt_tznow() -> datetime:
|
40
|
-
"""Return the current datetime object that passing the config timezone.
|
41
|
-
|
42
|
-
:rtype: datetime
|
43
|
-
"""
|
44
|
-
return get_dt_now(tz=config.tz)
|
45
|
-
|
46
|
-
|
47
45
|
class Status(IntEnum):
|
48
46
|
"""Status Int Enum object."""
|
49
47
|
|
@@ -52,27 +50,6 @@ class Status(IntEnum):
|
|
52
50
|
WAIT: int = 2
|
53
51
|
|
54
52
|
|
55
|
-
class TraceLog: # pragma: no cov
|
56
|
-
"""Trace Log object."""
|
57
|
-
|
58
|
-
__slots__: TupleStr = ("run_id",)
|
59
|
-
|
60
|
-
def __init__(self, run_id: str):
|
61
|
-
self.run_id: str = run_id
|
62
|
-
|
63
|
-
def debug(self, message: str):
|
64
|
-
logger.debug(f"({cut_id(self.run_id)}) {message}")
|
65
|
-
|
66
|
-
def info(self, message: str):
|
67
|
-
logger.info(f"({cut_id(self.run_id)}) {message}")
|
68
|
-
|
69
|
-
def warning(self, message: str):
|
70
|
-
logger.warning(f"({cut_id(self.run_id)}) {message}")
|
71
|
-
|
72
|
-
def error(self, message: str):
|
73
|
-
logger.error(f"({cut_id(self.run_id)}) {message}")
|
74
|
-
|
75
|
-
|
76
53
|
@dataclass(
|
77
54
|
config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True)
|
78
55
|
)
|
@@ -88,19 +65,37 @@ class Result:
|
|
88
65
|
status: Status = field(default=Status.WAIT)
|
89
66
|
context: DictData = field(default_factory=dict)
|
90
67
|
run_id: Optional[str] = field(default_factory=default_gen_id)
|
91
|
-
|
92
|
-
# NOTE: Ignore this field to compare another result model with __eq__.
|
93
68
|
parent_run_id: Optional[str] = field(default=None, compare=False)
|
94
69
|
event: Event = field(default_factory=Event, compare=False)
|
95
70
|
ts: datetime = field(default_factory=get_dt_tznow, compare=False)
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
:
|
71
|
+
trace: Optional[TraceLog] = field(default=None)
|
72
|
+
|
73
|
+
@classmethod
|
74
|
+
def construct_with_rs_or_id(
|
75
|
+
cls,
|
76
|
+
result: Result | None = None,
|
77
|
+
run_id: str | None = None,
|
78
|
+
parent_run_id: str | None = None,
|
79
|
+
id_logic: str | None = None,
|
80
|
+
) -> Self: # pragma: no cov
|
81
|
+
"""Create the Result object or set parent running id if passing Result
|
82
|
+
object.
|
102
83
|
"""
|
103
|
-
|
84
|
+
if result is None:
|
85
|
+
result: Result = cls(
|
86
|
+
run_id=(run_id or gen_id(id_logic or "", unique=True)),
|
87
|
+
parent_run_id=parent_run_id,
|
88
|
+
)
|
89
|
+
elif parent_run_id:
|
90
|
+
result.set_parent_run_id(parent_run_id)
|
91
|
+
return result
|
92
|
+
|
93
|
+
@model_validator(mode="after")
|
94
|
+
def __prepare_trace(self) -> Self:
|
95
|
+
"""Prepare trace field that want to pass after its initialize step."""
|
96
|
+
if self.trace is None: # pragma: no cove
|
97
|
+
self.trace: TraceLog = get_trace(self.run_id, self.parent_run_id)
|
98
|
+
|
104
99
|
return self
|
105
100
|
|
106
101
|
def set_parent_run_id(self, running_id: str) -> Self:
|
@@ -110,6 +105,7 @@ class Result:
|
|
110
105
|
:rtype: Self
|
111
106
|
"""
|
112
107
|
self.parent_run_id: str = running_id
|
108
|
+
self.trace: TraceLog = get_trace(self.run_id, running_id)
|
113
109
|
return self
|
114
110
|
|
115
111
|
def catch(
|
@@ -130,26 +126,9 @@ class Result:
|
|
130
126
|
self.__dict__["context"].update(context or {})
|
131
127
|
return self
|
132
128
|
|
133
|
-
def
|
134
|
-
"""
|
135
|
-
|
136
|
-
:rtype: Self
|
137
|
-
"""
|
138
|
-
self.__dict__["status"] = result.status
|
139
|
-
self.__dict__["context"].update(result.context)
|
140
|
-
|
141
|
-
# NOTE: Update running ID from an incoming result.
|
142
|
-
self.parent_run_id = result.parent_run_id
|
143
|
-
self.run_id = result.run_id
|
144
|
-
return self
|
145
|
-
|
146
|
-
@property
|
147
|
-
def trace(self) -> TraceLog:
|
148
|
-
"""Return TraceLog object that passing its running ID.
|
129
|
+
def alive_time(self) -> float: # pragma: no cov
|
130
|
+
"""Return total seconds that this object use since it was created.
|
149
131
|
|
150
|
-
:rtype:
|
132
|
+
:rtype: float
|
151
133
|
"""
|
152
|
-
return TraceLog(self.run_id)
|
153
|
-
|
154
|
-
def alive_time(self) -> float: # pragma: no cov
|
155
134
|
return (get_dt_tznow() - self.ts).total_seconds()
|