ddeutil-workflow 0.0.35__py3-none-any.whl → 0.0.36__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 +2 -0
- ddeutil/workflow/api/api.py +47 -8
- ddeutil/workflow/api/repeat.py +21 -11
- ddeutil/workflow/api/routes/__init__.py +1 -0
- ddeutil/workflow/api/routes/job.py +73 -0
- ddeutil/workflow/api/routes/logs.py +41 -13
- ddeutil/workflow/api/routes/schedules.py +1 -0
- ddeutil/workflow/audit.py +6 -3
- ddeutil/workflow/job.py +6 -16
- ddeutil/workflow/logs.py +164 -52
- ddeutil/workflow/params.py +52 -15
- ddeutil/workflow/result.py +3 -5
- ddeutil/workflow/scheduler.py +31 -10
- ddeutil/workflow/stages.py +61 -4
- ddeutil/workflow/utils.py +7 -1
- ddeutil/workflow/workflow.py +1 -15
- {ddeutil_workflow-0.0.35.dist-info → ddeutil_workflow-0.0.36.dist-info}/METADATA +11 -9
- ddeutil_workflow-0.0.36.dist-info/RECORD +31 -0
- {ddeutil_workflow-0.0.35.dist-info → ddeutil_workflow-0.0.36.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.35.dist-info/RECORD +0 -30
- {ddeutil_workflow-0.0.35.dist-info → ddeutil_workflow-0.0.36.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.35.dist-info → ddeutil_workflow-0.0.36.dist-info}/top_level.txt +0 -0
ddeutil/workflow/logs.py
CHANGED
@@ -3,10 +3,11 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""
|
6
|
+
"""A Logs module contain a TraceLog dataclass.
|
7
7
|
"""
|
8
8
|
from __future__ import annotations
|
9
9
|
|
10
|
+
import json
|
10
11
|
import os
|
11
12
|
from abc import ABC, abstractmethod
|
12
13
|
from collections.abc import Iterator
|
@@ -14,11 +15,13 @@ from datetime import datetime
|
|
14
15
|
from inspect import Traceback, currentframe, getframeinfo
|
15
16
|
from pathlib import Path
|
16
17
|
from threading import get_ident
|
17
|
-
from typing import Optional, Union
|
18
|
+
from typing import ClassVar, Literal, Optional, Union
|
18
19
|
|
20
|
+
from pydantic import BaseModel, Field
|
19
21
|
from pydantic.dataclasses import dataclass
|
22
|
+
from typing_extensions import Self
|
20
23
|
|
21
|
-
from .__types import TupleStr
|
24
|
+
from .__types import DictStr, TupleStr
|
22
25
|
from .conf import config, get_logger
|
23
26
|
from .utils import cut_id, get_dt_now
|
24
27
|
|
@@ -26,13 +29,17 @@ logger = get_logger("ddeutil.workflow")
|
|
26
29
|
|
27
30
|
__all__: TupleStr = (
|
28
31
|
"FileTraceLog",
|
32
|
+
"SQLiteTraceLog",
|
33
|
+
"TraceData",
|
34
|
+
"TraceMeda",
|
29
35
|
"TraceLog",
|
30
36
|
"get_dt_tznow",
|
31
37
|
"get_trace",
|
38
|
+
"get_trace_obj",
|
32
39
|
)
|
33
40
|
|
34
41
|
|
35
|
-
def get_dt_tznow() -> datetime:
|
42
|
+
def get_dt_tznow() -> datetime: # pragma: no cov
|
36
43
|
"""Return the current datetime object that passing the config timezone.
|
37
44
|
|
38
45
|
:rtype: datetime
|
@@ -49,76 +56,154 @@ class BaseTraceLog(ABC): # pragma: no cov
|
|
49
56
|
|
50
57
|
@abstractmethod
|
51
58
|
def writer(self, message: str, is_err: bool = False) -> None:
|
59
|
+
"""Write a trace message after making to target pointer object. The
|
60
|
+
target can be anything be inherited this class and overwrite this method
|
61
|
+
such as file, console, or database.
|
62
|
+
|
63
|
+
:param message: A message after making.
|
64
|
+
:param is_err: A flag for writing with an error trace or not.
|
65
|
+
"""
|
52
66
|
raise NotImplementedError(
|
53
67
|
"Create writer logic for this trace object before using."
|
54
68
|
)
|
55
69
|
|
56
70
|
@abstractmethod
|
57
71
|
def make_message(self, message: str) -> str:
|
72
|
+
"""Prepare and Make a message before write and log processes.
|
73
|
+
|
74
|
+
:param message: A message that want to prepare and make before.
|
75
|
+
|
76
|
+
:rtype: str
|
77
|
+
"""
|
58
78
|
raise NotImplementedError(
|
59
79
|
"Adjust make message method for this trace object before using."
|
60
80
|
)
|
61
81
|
|
62
82
|
def debug(self, message: str):
|
83
|
+
"""Write trace log with append mode and logging this message with the
|
84
|
+
DEBUG level.
|
85
|
+
|
86
|
+
:param message: (str) A message that want to log.
|
87
|
+
"""
|
63
88
|
msg: str = self.make_message(message)
|
64
89
|
|
65
|
-
# NOTE: Write file if debug mode.
|
90
|
+
# NOTE: Write file if debug mode was enabled.
|
66
91
|
if config.debug:
|
67
92
|
self.writer(msg)
|
68
93
|
|
69
94
|
logger.debug(msg, stacklevel=2)
|
70
95
|
|
71
|
-
def info(self, message: str):
|
96
|
+
def info(self, message: str) -> None:
|
97
|
+
"""Write trace log with append mode and logging this message with the
|
98
|
+
INFO level.
|
99
|
+
|
100
|
+
:param message: (str) A message that want to log.
|
101
|
+
"""
|
72
102
|
msg: str = self.make_message(message)
|
73
103
|
self.writer(msg)
|
74
104
|
logger.info(msg, stacklevel=2)
|
75
105
|
|
76
|
-
def warning(self, message: str):
|
106
|
+
def warning(self, message: str) -> None:
|
107
|
+
"""Write trace log with append mode and logging this message with the
|
108
|
+
WARNING level.
|
109
|
+
|
110
|
+
:param message: (str) A message that want to log.
|
111
|
+
"""
|
77
112
|
msg: str = self.make_message(message)
|
78
113
|
self.writer(msg)
|
79
114
|
logger.warning(msg, stacklevel=2)
|
80
115
|
|
81
|
-
def error(self, message: str):
|
116
|
+
def error(self, message: str) -> None:
|
117
|
+
"""Write trace log with append mode and logging this message with the
|
118
|
+
ERROR level.
|
119
|
+
|
120
|
+
:param message: (str) A message that want to log.
|
121
|
+
"""
|
82
122
|
msg: str = self.make_message(message)
|
83
123
|
self.writer(msg, is_err=True)
|
84
124
|
logger.error(msg, stacklevel=2)
|
85
125
|
|
126
|
+
def exception(self, message: str) -> None:
|
127
|
+
"""Write trace log with append mode and logging this message with the
|
128
|
+
EXCEPTION level.
|
86
129
|
|
87
|
-
|
88
|
-
|
130
|
+
:param message: (str) A message that want to log.
|
131
|
+
"""
|
132
|
+
msg: str = self.make_message(message)
|
133
|
+
self.writer(msg, is_err=True)
|
134
|
+
logger.exception(msg, stacklevel=2)
|
135
|
+
|
136
|
+
|
137
|
+
class TraceMeda(BaseModel): # pragma: no cov
|
138
|
+
mode: Literal["stdout", "stderr"]
|
139
|
+
datetime: str
|
140
|
+
process: int
|
141
|
+
thread: int
|
142
|
+
message: str
|
143
|
+
filename: str
|
144
|
+
lineno: int
|
145
|
+
|
146
|
+
|
147
|
+
class TraceData(BaseModel): # pragma: no cov
|
148
|
+
stdout: str = Field(description="A standard output trace data.")
|
149
|
+
stderr: str = Field(description="A standard error trace data.")
|
150
|
+
meta: list[TraceMeda] = Field(
|
151
|
+
default_factory=list,
|
152
|
+
description=(
|
153
|
+
"A metadata mapping of this output and error before making it to "
|
154
|
+
"standard value."
|
155
|
+
),
|
156
|
+
)
|
89
157
|
|
90
158
|
@classmethod
|
91
|
-
def
|
92
|
-
|
93
|
-
data: dict[str, str] = {}
|
159
|
+
def from_path(cls, file: Path) -> Self:
|
160
|
+
data: DictStr = {"stdout": "", "stderr": "", "meta": []}
|
94
161
|
|
95
|
-
|
96
|
-
|
97
|
-
encoding="utf-8"
|
98
|
-
)
|
162
|
+
if (file / "stdout.txt").exists():
|
163
|
+
data["stdout"] = (file / "stdout.txt").read_text(encoding="utf-8")
|
99
164
|
|
100
|
-
|
101
|
-
|
102
|
-
|
165
|
+
if (file / "stderr.txt").exists():
|
166
|
+
data["stderr"] = (file / "stderr.txt").read_text(encoding="utf-8")
|
167
|
+
|
168
|
+
if (file / "metadata.json").exists():
|
169
|
+
data["meta"] = [
|
170
|
+
json.loads(line)
|
171
|
+
for line in (
|
172
|
+
(file / "metadata.json")
|
173
|
+
.read_text(encoding="utf-8")
|
174
|
+
.splitlines()
|
103
175
|
)
|
176
|
+
]
|
104
177
|
|
105
|
-
|
178
|
+
return cls.model_validate(data)
|
106
179
|
|
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
180
|
|
112
|
-
|
113
|
-
|
181
|
+
class FileTraceLog(BaseTraceLog): # pragma: no cov
|
182
|
+
"""Trace Log object that write file to the local storage."""
|
114
183
|
|
115
|
-
|
116
|
-
|
184
|
+
@classmethod
|
185
|
+
def find_logs(cls) -> Iterator[TraceData]: # pragma: no cov
|
186
|
+
for file in sorted(
|
187
|
+
config.log_path.glob("./run_id=*"),
|
188
|
+
key=lambda f: f.lstat().st_mtime,
|
189
|
+
):
|
190
|
+
yield TraceData.from_path(file)
|
117
191
|
|
118
|
-
|
192
|
+
@classmethod
|
193
|
+
def find_log_with_id(
|
194
|
+
cls, run_id: str, force_raise: bool = True
|
195
|
+
) -> TraceData:
|
196
|
+
file: Path = config.log_path / f"run_id={run_id}"
|
197
|
+
if file.exists():
|
198
|
+
return TraceData.from_path(file)
|
199
|
+
elif force_raise:
|
200
|
+
raise FileNotFoundError(
|
201
|
+
f"Trace log on path 'run_id={run_id}' does not found."
|
202
|
+
)
|
203
|
+
return {}
|
119
204
|
|
120
205
|
@property
|
121
|
-
def
|
206
|
+
def pointer(self) -> Path:
|
122
207
|
log_file: Path = (
|
123
208
|
config.log_path / f"run_id={self.parent_run_id or self.run_id}"
|
124
209
|
)
|
@@ -128,7 +213,10 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
|
|
128
213
|
|
129
214
|
@property
|
130
215
|
def cut_id(self) -> str:
|
131
|
-
"""Combine cutting ID of parent running ID if it set.
|
216
|
+
"""Combine cutting ID of parent running ID if it set.
|
217
|
+
|
218
|
+
:rtype: str
|
219
|
+
"""
|
132
220
|
cut_run_id: str = cut_id(self.run_id)
|
133
221
|
if not self.parent_run_id:
|
134
222
|
return f"{cut_run_id} -> {' ' * 6}"
|
@@ -137,16 +225,26 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
|
|
137
225
|
return f"{cut_parent_run_id} -> {cut_run_id}"
|
138
226
|
|
139
227
|
def make_message(self, message: str) -> str:
|
228
|
+
"""Prepare and Make a message before write and log processes.
|
229
|
+
|
230
|
+
:param message: A message that want to prepare and make before.
|
231
|
+
|
232
|
+
:rtype: str
|
233
|
+
"""
|
140
234
|
return f"({self.cut_id}) {message}"
|
141
235
|
|
142
236
|
def writer(self, message: str, is_err: bool = False) -> None:
|
143
|
-
"""
|
237
|
+
""" "Write a trace message after making to target file and write metadata
|
238
|
+
in the same path of standard files.
|
239
|
+
|
240
|
+
The path of logging data will store by format:
|
144
241
|
|
242
|
+
... ./logs/run_id=<run-id>/metadata.json
|
145
243
|
... ./logs/run_id=<run-id>/stdout.txt
|
146
244
|
... ./logs/run_id=<run-id>/stderr.txt
|
147
245
|
|
148
|
-
:param message:
|
149
|
-
:param is_err:
|
246
|
+
:param message: A message after making.
|
247
|
+
:param is_err: A flag for writing with an error trace or not.
|
150
248
|
"""
|
151
249
|
if not config.enable_write_log:
|
152
250
|
return
|
@@ -160,34 +258,47 @@ class FileTraceLog(BaseTraceLog): # pragma: no cov
|
|
160
258
|
thread: int = get_ident()
|
161
259
|
|
162
260
|
write_file: str = "stderr.txt" if is_err else "stdout.txt"
|
163
|
-
|
261
|
+
write_data: dict[str, Union[str, int]] = {
|
262
|
+
"datetime": get_dt_tznow().strftime(config.log_datetime_format),
|
263
|
+
"process": process,
|
264
|
+
"thread": thread,
|
265
|
+
"message": message,
|
266
|
+
"filename": filename,
|
267
|
+
"lineno": lineno,
|
268
|
+
}
|
269
|
+
|
270
|
+
with (self.pointer / write_file).open(mode="at", encoding="utf-8") as f:
|
271
|
+
msg_fmt: str = f"{config.log_format_file}\n"
|
272
|
+
f.write(msg_fmt.format(**write_data))
|
273
|
+
|
274
|
+
with (self.pointer / "metadata.json").open(
|
164
275
|
mode="at", encoding="utf-8"
|
165
276
|
) as f:
|
166
|
-
msg_fmt: str = f"{config.log_format_file}\n"
|
167
|
-
print(msg_fmt)
|
168
277
|
f.write(
|
169
|
-
|
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
|
-
)
|
278
|
+
json.dumps({"mode": write_file.split(".")[0]} | write_data)
|
279
|
+
+ "\n"
|
181
280
|
)
|
182
281
|
|
183
282
|
|
184
283
|
class SQLiteTraceLog(BaseTraceLog): # pragma: no cov
|
284
|
+
"""Trace Log object that write trace log to the SQLite database file."""
|
285
|
+
|
286
|
+
table_name: ClassVar[str] = "audits"
|
287
|
+
schemas: ClassVar[
|
288
|
+
str
|
289
|
+
] = """
|
290
|
+
run_id int,
|
291
|
+
stdout str,
|
292
|
+
stderr str,
|
293
|
+
update datetime
|
294
|
+
primary key ( run_id )
|
295
|
+
"""
|
185
296
|
|
186
297
|
@classmethod
|
187
|
-
def find_logs(cls) -> Iterator[
|
298
|
+
def find_logs(cls) -> Iterator[DictStr]: ...
|
188
299
|
|
189
300
|
@classmethod
|
190
|
-
def find_log_with_id(cls, run_id: str) ->
|
301
|
+
def find_log_with_id(cls, run_id: str) -> DictStr: ...
|
191
302
|
|
192
303
|
def make_message(self, message: str) -> str: ...
|
193
304
|
|
@@ -203,6 +314,7 @@ TraceLog = Union[
|
|
203
314
|
def get_trace(
|
204
315
|
run_id: str, parent_run_id: str | None = None
|
205
316
|
) -> TraceLog: # pragma: no cov
|
317
|
+
"""Get dynamic TraceLog object from the setting config."""
|
206
318
|
if config.log_path.is_file():
|
207
319
|
return SQLiteTraceLog(run_id, parent_run_id=parent_run_id)
|
208
320
|
return FileTraceLog(run_id, parent_run_id=parent_run_id)
|
ddeutil/workflow/params.py
CHANGED
@@ -3,33 +3,37 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""This module include all Param Models that use for parsing
|
7
|
-
that
|
6
|
+
"""This module include all Param Pydantic Models that use for parsing an
|
7
|
+
incoming parameters that was passed to the Workflow and Schedule objects before
|
8
|
+
execution or release methods.
|
9
|
+
|
10
|
+
The Param model allow you to handle validation and preparation steps before
|
11
|
+
passing an input value to target execution method.
|
8
12
|
"""
|
9
13
|
from __future__ import annotations
|
10
14
|
|
11
15
|
import decimal
|
12
|
-
import logging
|
13
16
|
from abc import ABC, abstractmethod
|
14
17
|
from datetime import date, datetime
|
15
|
-
from typing import Annotated, Any, Literal, Optional, Union
|
18
|
+
from typing import Annotated, Any, Literal, Optional, TypeVar, Union
|
16
19
|
|
17
20
|
from pydantic import BaseModel, Field
|
18
21
|
|
19
22
|
from .__types import TupleStr
|
20
23
|
from .exceptions import ParamValueException
|
21
|
-
from .utils import get_dt_now
|
22
|
-
|
23
|
-
logger = logging.getLogger("ddeutil.workflow")
|
24
|
+
from .utils import get_d_now, get_dt_now
|
24
25
|
|
25
26
|
__all__: TupleStr = (
|
26
27
|
"ChoiceParam",
|
27
28
|
"DatetimeParam",
|
29
|
+
"DateParam",
|
28
30
|
"IntParam",
|
29
31
|
"Param",
|
30
32
|
"StrParam",
|
31
33
|
)
|
32
34
|
|
35
|
+
T = TypeVar("T")
|
36
|
+
|
33
37
|
|
34
38
|
class BaseParam(BaseModel, ABC):
|
35
39
|
"""Base Parameter that use to make any Params Models. The parameter type
|
@@ -46,7 +50,7 @@ class BaseParam(BaseModel, ABC):
|
|
46
50
|
type: str = Field(description="A type of parameter.")
|
47
51
|
|
48
52
|
@abstractmethod
|
49
|
-
def receive(self, value: Optional[
|
53
|
+
def receive(self, value: Optional[T] = None) -> T:
|
50
54
|
raise NotImplementedError(
|
51
55
|
"Receive value and validate typing before return valid value."
|
52
56
|
)
|
@@ -73,17 +77,42 @@ class DefaultParam(BaseParam):
|
|
73
77
|
)
|
74
78
|
|
75
79
|
|
76
|
-
# TODO: Not implement this parameter yet
|
77
80
|
class DateParam(DefaultParam): # pragma: no cov
|
78
|
-
"""Date parameter."""
|
81
|
+
"""Date parameter model."""
|
79
82
|
|
80
83
|
type: Literal["date"] = "date"
|
84
|
+
default: date = Field(default_factory=get_d_now)
|
85
|
+
|
86
|
+
def receive(self, value: Optional[str | datetime | date] = None) -> date:
|
87
|
+
"""Receive value that match with date. If an input value pass with
|
88
|
+
None, it will use default value instead.
|
81
89
|
|
82
|
-
|
90
|
+
:param value: A value that want to validate with date parameter type.
|
91
|
+
|
92
|
+
:rtype: date
|
93
|
+
"""
|
94
|
+
if value is None:
|
95
|
+
return self.default
|
96
|
+
|
97
|
+
if isinstance(value, datetime):
|
98
|
+
return value.date()
|
99
|
+
elif isinstance(value, date):
|
100
|
+
return value
|
101
|
+
elif not isinstance(value, str):
|
102
|
+
raise ParamValueException(
|
103
|
+
f"Value that want to convert to date does not support for "
|
104
|
+
f"type: {type(value)}"
|
105
|
+
)
|
106
|
+
try:
|
107
|
+
return date.fromisoformat(value)
|
108
|
+
except ValueError:
|
109
|
+
raise ParamValueException(
|
110
|
+
f"Invalid the ISO format string for date: {value!r}"
|
111
|
+
) from None
|
83
112
|
|
84
113
|
|
85
114
|
class DatetimeParam(DefaultParam):
|
86
|
-
"""Datetime parameter."""
|
115
|
+
"""Datetime parameter model."""
|
87
116
|
|
88
117
|
type: Literal["datetime"] = "datetime"
|
89
118
|
default: datetime = Field(default_factory=get_dt_now)
|
@@ -94,6 +123,7 @@ class DatetimeParam(DefaultParam):
|
|
94
123
|
|
95
124
|
:param value: A value that want to validate with datetime parameter
|
96
125
|
type.
|
126
|
+
|
97
127
|
:rtype: datetime
|
98
128
|
"""
|
99
129
|
if value is None:
|
@@ -112,7 +142,7 @@ class DatetimeParam(DefaultParam):
|
|
112
142
|
return datetime.fromisoformat(value)
|
113
143
|
except ValueError:
|
114
144
|
raise ParamValueException(
|
115
|
-
f"Invalid the ISO format string: {value!r}"
|
145
|
+
f"Invalid the ISO format string for datetime: {value!r}"
|
116
146
|
) from None
|
117
147
|
|
118
148
|
|
@@ -192,7 +222,7 @@ class ChoiceParam(BaseParam):
|
|
192
222
|
|
193
223
|
|
194
224
|
# TODO: Not implement this parameter yet
|
195
|
-
class
|
225
|
+
class MapParam(DefaultParam): # pragma: no cov
|
196
226
|
|
197
227
|
type: Literal["map"] = "map"
|
198
228
|
default: dict[Any, Any] = Field(default_factory=dict)
|
@@ -208,15 +238,22 @@ class ArrayParam(DefaultParam): # pragma: no cov
|
|
208
238
|
type: Literal["array"] = "array"
|
209
239
|
default: list[Any] = Field(default_factory=list)
|
210
240
|
|
211
|
-
def receive(self, value: Optional[list[
|
241
|
+
def receive(self, value: Optional[list[T]] = None) -> list[T]:
|
212
242
|
if value is None:
|
213
243
|
return self.default
|
244
|
+
if not isinstance(value, list):
|
245
|
+
raise ParamValueException(
|
246
|
+
f"Value that want to convert to array does not support for "
|
247
|
+
f"type: {type(value)}"
|
248
|
+
)
|
249
|
+
return value
|
214
250
|
|
215
251
|
|
216
252
|
Param = Annotated[
|
217
253
|
Union[
|
218
254
|
ChoiceParam,
|
219
255
|
DatetimeParam,
|
256
|
+
DateParam,
|
220
257
|
IntParam,
|
221
258
|
StrParam,
|
222
259
|
],
|
ddeutil/workflow/result.py
CHANGED
@@ -20,12 +20,9 @@ from pydantic.functional_validators import model_validator
|
|
20
20
|
from typing_extensions import Self
|
21
21
|
|
22
22
|
from .__types import DictData, TupleStr
|
23
|
-
from .conf import get_logger
|
24
23
|
from .logs import TraceLog, get_dt_tznow, get_trace
|
25
24
|
from .utils import gen_id
|
26
25
|
|
27
|
-
logger = get_logger("ddeutil.workflow")
|
28
|
-
|
29
26
|
__all__: TupleStr = (
|
30
27
|
"Result",
|
31
28
|
"Status",
|
@@ -66,9 +63,10 @@ class Result:
|
|
66
63
|
context: DictData = field(default_factory=dict)
|
67
64
|
run_id: Optional[str] = field(default_factory=default_gen_id)
|
68
65
|
parent_run_id: Optional[str] = field(default=None, compare=False)
|
69
|
-
event: Event = field(default_factory=Event, compare=False)
|
70
66
|
ts: datetime = field(default_factory=get_dt_tznow, compare=False)
|
71
|
-
|
67
|
+
|
68
|
+
event: Event = field(default_factory=Event, compare=False, repr=False)
|
69
|
+
trace: Optional[TraceLog] = field(default=None, compare=False, repr=False)
|
72
70
|
|
73
71
|
@classmethod
|
74
72
|
def construct_with_rs_or_id(
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -359,12 +359,16 @@ ReturnResultOrCancel = Callable[P, ResultOrCancel]
|
|
359
359
|
DecoratorCancelJob = Callable[[ReturnResultOrCancel], ReturnResultOrCancel]
|
360
360
|
|
361
361
|
|
362
|
-
def catch_exceptions(
|
362
|
+
def catch_exceptions(
|
363
|
+
cancel_on_failure: bool = False,
|
364
|
+
parent_run_id: str | None = None,
|
365
|
+
) -> DecoratorCancelJob:
|
363
366
|
"""Catch exception error from scheduler job that running with schedule
|
364
367
|
package and return CancelJob if this function raise an error.
|
365
368
|
|
366
369
|
:param cancel_on_failure: A flag that allow to return the CancelJob or not
|
367
370
|
it will raise.
|
371
|
+
:param parent_run_id:
|
368
372
|
|
369
373
|
:rtype: DecoratorCancelJob
|
370
374
|
"""
|
@@ -375,10 +379,17 @@ def catch_exceptions(cancel_on_failure: bool = False) -> DecoratorCancelJob:
|
|
375
379
|
|
376
380
|
@wraps(func)
|
377
381
|
def wrapper(*args: P.args, **kwargs: P.kwargs) -> ResultOrCancel:
|
382
|
+
|
378
383
|
try:
|
379
384
|
return func(*args, **kwargs)
|
385
|
+
|
380
386
|
except Exception as err:
|
381
|
-
|
387
|
+
if parent_run_id:
|
388
|
+
(
|
389
|
+
Result(parent_run_id=parent_run_id).trace.exception(
|
390
|
+
str(err)
|
391
|
+
)
|
392
|
+
)
|
382
393
|
if cancel_on_failure:
|
383
394
|
return CancelJob
|
384
395
|
raise err
|
@@ -399,13 +410,13 @@ class ReleaseThread(TypedDict):
|
|
399
410
|
ReleaseThreads = dict[str, ReleaseThread]
|
400
411
|
|
401
412
|
|
402
|
-
@catch_exceptions(cancel_on_failure=True)
|
403
413
|
def schedule_task(
|
404
414
|
tasks: list[WorkflowTask],
|
405
415
|
stop: datetime,
|
406
416
|
queue: dict[str, ReleaseQueue],
|
407
417
|
threads: ReleaseThreads,
|
408
418
|
audit: type[Audit],
|
419
|
+
*,
|
409
420
|
parent_run_id: str | None = None,
|
410
421
|
) -> ResultOrCancel:
|
411
422
|
"""Schedule task function that generate thread of workflow task release
|
@@ -491,8 +502,14 @@ def schedule_task(
|
|
491
502
|
# job.
|
492
503
|
thread_name: str = f"{task.alias}|{release.date:%Y%m%d%H%M}"
|
493
504
|
thread: Thread = Thread(
|
494
|
-
target=catch_exceptions(
|
495
|
-
|
505
|
+
target=catch_exceptions(
|
506
|
+
cancel_on_failure=True,
|
507
|
+
)(task.release),
|
508
|
+
kwargs={
|
509
|
+
"release": release,
|
510
|
+
"queue": q,
|
511
|
+
"audit": audit,
|
512
|
+
},
|
496
513
|
name=thread_name,
|
497
514
|
daemon=True,
|
498
515
|
)
|
@@ -508,8 +525,8 @@ def schedule_task(
|
|
508
525
|
delay()
|
509
526
|
|
510
527
|
result.trace.debug(
|
511
|
-
f"[SCHEDULE]: End schedule task
|
512
|
-
f"{'=' *
|
528
|
+
f"[SCHEDULE]: End schedule task that run since "
|
529
|
+
f"{current_date:%Y-%m-%d %H:%M:%S} {'=' * 30}"
|
513
530
|
)
|
514
531
|
return result.catch(
|
515
532
|
status=Status.SUCCESS, context={"task_date": current_date}
|
@@ -575,7 +592,10 @@ def scheduler_pending(
|
|
575
592
|
scheduler.every(1)
|
576
593
|
.minutes.at(":02")
|
577
594
|
.do(
|
578
|
-
|
595
|
+
catch_exceptions(
|
596
|
+
cancel_on_failure=True,
|
597
|
+
parent_run_id=result.parent_run_id,
|
598
|
+
)(schedule_task),
|
579
599
|
tasks=tasks,
|
580
600
|
stop=stop,
|
581
601
|
queue=queue,
|
@@ -594,6 +614,7 @@ def scheduler_pending(
|
|
594
614
|
.do(
|
595
615
|
monitor,
|
596
616
|
threads=threads,
|
617
|
+
parent_run_id=result.parent_run_id,
|
597
618
|
)
|
598
619
|
.tag("monitor")
|
599
620
|
)
|
@@ -617,7 +638,7 @@ def scheduler_pending(
|
|
617
638
|
"running in background."
|
618
639
|
)
|
619
640
|
delay(10)
|
620
|
-
monitor(threads)
|
641
|
+
monitor(threads, parent_run_id=result.parent_run_id)
|
621
642
|
|
622
643
|
break
|
623
644
|
|
@@ -752,7 +773,7 @@ def schedule_runner(
|
|
752
773
|
|
753
774
|
# NOTE: Raise error when it has any error from schedule_control.
|
754
775
|
if err := future.exception():
|
755
|
-
|
776
|
+
result.trace.error(str(err))
|
756
777
|
raise WorkflowException(str(err)) from err
|
757
778
|
|
758
779
|
rs: Result = future.result(timeout=1)
|