ddeutil-workflow 0.0.56__py3-none-any.whl → 0.0.58__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/__cron.py +26 -12
- ddeutil/workflow/__types.py +1 -0
- ddeutil/workflow/conf.py +21 -9
- ddeutil/workflow/event.py +11 -10
- ddeutil/workflow/exceptions.py +33 -12
- ddeutil/workflow/job.py +89 -58
- ddeutil/workflow/logs.py +59 -37
- ddeutil/workflow/params.py +4 -0
- ddeutil/workflow/result.py +9 -4
- ddeutil/workflow/scheduler.py +15 -9
- ddeutil/workflow/stages.py +441 -171
- ddeutil/workflow/utils.py +37 -6
- ddeutil/workflow/workflow.py +218 -243
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/METADATA +41 -35
- ddeutil_workflow-0.0.58.dist-info/RECORD +31 -0
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.56.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.56.dist-info → ddeutil_workflow-0.0.58.dist-info}/top_level.txt +0 -0
ddeutil/workflow/logs.py
CHANGED
@@ -29,7 +29,7 @@ from typing_extensions import Self
|
|
29
29
|
|
30
30
|
from .__types import DictData, DictStr
|
31
31
|
from .conf import config, dynamic
|
32
|
-
from .utils import cut_id, get_dt_now
|
32
|
+
from .utils import cut_id, get_dt_now, prepare_newline
|
33
33
|
|
34
34
|
|
35
35
|
@lru_cache
|
@@ -70,16 +70,28 @@ def get_dt_tznow() -> datetime: # pragma: no cov
|
|
70
70
|
return get_dt_now(tz=config.tz)
|
71
71
|
|
72
72
|
|
73
|
+
PREFIX_LOGS: dict[str, dict] = {
|
74
|
+
"CALLER": {"emoji": ""},
|
75
|
+
"STAGE": {"emoji": ""},
|
76
|
+
"JOB": {"emoji": ""},
|
77
|
+
"WORKFLOW": {"emoji": "🏃"},
|
78
|
+
"RELEASE": {"emoji": ""},
|
79
|
+
"POKE": {"emoji": ""},
|
80
|
+
} # pragma: no cov
|
81
|
+
|
82
|
+
|
73
83
|
class TraceMeta(BaseModel): # pragma: no cov
|
74
|
-
"""Trace
|
84
|
+
"""Trace Metadata model for making the current metadata of this CPU, Memory
|
85
|
+
process, and thread data.
|
86
|
+
"""
|
75
87
|
|
76
|
-
mode: Literal["stdout", "stderr"]
|
77
|
-
datetime: str
|
78
|
-
process: int
|
79
|
-
thread: int
|
80
|
-
message: str
|
81
|
-
filename: str
|
82
|
-
lineno: int
|
88
|
+
mode: Literal["stdout", "stderr"] = Field(description="A meta mode.")
|
89
|
+
datetime: str = Field(description="A datetime in string format.")
|
90
|
+
process: int = Field(description="A process ID.")
|
91
|
+
thread: int = Field(description="A thread ID.")
|
92
|
+
message: str = Field(description="A message log.")
|
93
|
+
filename: str = Field(description="A filename of this log.")
|
94
|
+
lineno: int = Field(description="A line number of this log.")
|
83
95
|
|
84
96
|
@classmethod
|
85
97
|
def make(
|
@@ -91,6 +103,11 @@ class TraceMeta(BaseModel): # pragma: no cov
|
|
91
103
|
) -> Self:
|
92
104
|
"""Make the current TraceMeta instance that catching local state.
|
93
105
|
|
106
|
+
:param mode: A metadata mode.
|
107
|
+
:param message: A message.
|
108
|
+
:param extras: (DictData) An extra parameter that want to override core
|
109
|
+
config values.
|
110
|
+
|
94
111
|
:rtype: Self
|
95
112
|
"""
|
96
113
|
frame_info: Traceback = getframeinfo(
|
@@ -135,11 +152,9 @@ class TraceData(BaseModel): # pragma: no cov
|
|
135
152
|
"""
|
136
153
|
data: DictStr = {"stdout": "", "stderr": "", "meta": []}
|
137
154
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
if (file / "stderr.txt").exists():
|
142
|
-
data["stderr"] = (file / "stderr.txt").read_text(encoding="utf-8")
|
155
|
+
for mode in ("stdout", "stderr"):
|
156
|
+
if (file / f"{mode}.txt").exists():
|
157
|
+
data[mode] = (file / f"{mode}.txt").read_text(encoding="utf-8")
|
143
158
|
|
144
159
|
if (file / "metadata.json").exists():
|
145
160
|
data["meta"] = [
|
@@ -232,7 +247,7 @@ class BaseTrace(ABC): # pragma: no cov
|
|
232
247
|
|
233
248
|
:param message: (str) A message that want to log.
|
234
249
|
"""
|
235
|
-
msg: str = self.make_message(message)
|
250
|
+
msg: str = prepare_newline(self.make_message(message))
|
236
251
|
|
237
252
|
if mode != "debug" or (
|
238
253
|
mode == "debug" and dynamic("debug", extras=self.extras)
|
@@ -281,16 +296,30 @@ class BaseTrace(ABC): # pragma: no cov
|
|
281
296
|
"""
|
282
297
|
self.__logging(message, mode="exception", is_err=True)
|
283
298
|
|
299
|
+
async def __alogging(
|
300
|
+
self, message: str, mode: str, *, is_err: bool = False
|
301
|
+
) -> None:
|
302
|
+
"""Write trace log with append mode and logging this message with any
|
303
|
+
logging level.
|
304
|
+
|
305
|
+
:param message: (str) A message that want to log.
|
306
|
+
"""
|
307
|
+
msg: str = prepare_newline(self.make_message(message))
|
308
|
+
|
309
|
+
if mode != "debug" or (
|
310
|
+
mode == "debug" and dynamic("debug", extras=self.extras)
|
311
|
+
):
|
312
|
+
await self.awriter(msg, is_err=is_err)
|
313
|
+
|
314
|
+
getattr(logger, mode)(msg, stacklevel=3)
|
315
|
+
|
284
316
|
async def adebug(self, message: str) -> None: # pragma: no cov
|
285
317
|
"""Async write trace log with append mode and logging this message with
|
286
318
|
the DEBUG level.
|
287
319
|
|
288
320
|
:param message: (str) A message that want to log.
|
289
321
|
"""
|
290
|
-
|
291
|
-
if dynamic("debug", extras=self.extras):
|
292
|
-
await self.awriter(msg)
|
293
|
-
logger.info(msg, stacklevel=2)
|
322
|
+
await self.__alogging(message, mode="debug")
|
294
323
|
|
295
324
|
async def ainfo(self, message: str) -> None: # pragma: no cov
|
296
325
|
"""Async write trace log with append mode and logging this message with
|
@@ -298,9 +327,7 @@ class BaseTrace(ABC): # pragma: no cov
|
|
298
327
|
|
299
328
|
:param message: (str) A message that want to log.
|
300
329
|
"""
|
301
|
-
|
302
|
-
await self.awriter(msg)
|
303
|
-
logger.info(msg, stacklevel=2)
|
330
|
+
await self.__alogging(message, mode="info")
|
304
331
|
|
305
332
|
async def awarning(self, message: str) -> None: # pragma: no cov
|
306
333
|
"""Async write trace log with append mode and logging this message with
|
@@ -308,9 +335,7 @@ class BaseTrace(ABC): # pragma: no cov
|
|
308
335
|
|
309
336
|
:param message: (str) A message that want to log.
|
310
337
|
"""
|
311
|
-
|
312
|
-
await self.awriter(msg)
|
313
|
-
logger.warning(msg, stacklevel=2)
|
338
|
+
await self.__alogging(message, mode="warning")
|
314
339
|
|
315
340
|
async def aerror(self, message: str) -> None: # pragma: no cov
|
316
341
|
"""Async write trace log with append mode and logging this message with
|
@@ -318,9 +343,7 @@ class BaseTrace(ABC): # pragma: no cov
|
|
318
343
|
|
319
344
|
:param message: (str) A message that want to log.
|
320
345
|
"""
|
321
|
-
|
322
|
-
await self.awriter(msg, is_err=True)
|
323
|
-
logger.error(msg, stacklevel=2)
|
346
|
+
await self.__alogging(message, mode="error", is_err=True)
|
324
347
|
|
325
348
|
async def aexception(self, message: str) -> None: # pragma: no cov
|
326
349
|
"""Async write trace log with append mode and logging this message with
|
@@ -328,9 +351,7 @@ class BaseTrace(ABC): # pragma: no cov
|
|
328
351
|
|
329
352
|
:param message: (str) A message that want to log.
|
330
353
|
"""
|
331
|
-
|
332
|
-
await self.awriter(msg, is_err=True)
|
333
|
-
logger.exception(msg, stacklevel=2)
|
354
|
+
await self.__alogging(message, mode="exception", is_err=True)
|
334
355
|
|
335
356
|
|
336
357
|
class FileTrace(BaseTrace): # pragma: no cov
|
@@ -344,7 +365,7 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
344
365
|
) -> Iterator[TraceData]: # pragma: no cov
|
345
366
|
"""Find trace logs.
|
346
367
|
|
347
|
-
:param path: (Path)
|
368
|
+
:param path: (Path) A trace path that want to find.
|
348
369
|
:param extras: An extra parameter that want to override core config.
|
349
370
|
"""
|
350
371
|
for file in sorted(
|
@@ -357,16 +378,16 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
357
378
|
def find_trace_with_id(
|
358
379
|
cls,
|
359
380
|
run_id: str,
|
360
|
-
force_raise: bool = True,
|
361
381
|
*,
|
382
|
+
force_raise: bool = True,
|
362
383
|
path: Path | None = None,
|
363
384
|
extras: Optional[DictData] = None,
|
364
385
|
) -> TraceData:
|
365
386
|
"""Find trace log with an input specific run ID.
|
366
387
|
|
367
388
|
:param run_id: A running ID of trace log.
|
368
|
-
:param force_raise:
|
369
|
-
:param path:
|
389
|
+
:param force_raise: (bool)
|
390
|
+
:param path: (Path)
|
370
391
|
:param extras: An extra parameter that want to override core config.
|
371
392
|
"""
|
372
393
|
base_path: Path = path or dynamic("trace_path", extras=extras)
|
@@ -445,6 +466,7 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
445
466
|
async def awriter(
|
446
467
|
self, message: str, is_err: bool = False
|
447
468
|
) -> None: # pragma: no cov
|
469
|
+
"""Write with async mode."""
|
448
470
|
if not dynamic("enable_write_log", extras=self.extras):
|
449
471
|
return
|
450
472
|
|
@@ -744,7 +766,7 @@ class FileAudit(BaseAudit):
|
|
744
766
|
|
745
767
|
# NOTE: Check environ variable was set for real writing.
|
746
768
|
if not dynamic("enable_write_audit", extras=self.extras):
|
747
|
-
trace.debug("[
|
769
|
+
trace.debug("[AUDIT]: Skip writing log cause config was set")
|
748
770
|
return self
|
749
771
|
|
750
772
|
log_file: Path = (
|
@@ -813,7 +835,7 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
813
835
|
|
814
836
|
# NOTE: Check environ variable was set for real writing.
|
815
837
|
if not dynamic("enable_write_audit", extras=self.extras):
|
816
|
-
trace.debug("[
|
838
|
+
trace.debug("[AUDIT]: Skip writing log cause config was set")
|
817
839
|
return self
|
818
840
|
|
819
841
|
raise NotImplementedError("SQLiteAudit does not implement yet.")
|
ddeutil/workflow/params.py
CHANGED
@@ -190,10 +190,13 @@ class IntParam(DefaultParam):
|
|
190
190
|
|
191
191
|
|
192
192
|
class FloatParam(DefaultParam): # pragma: no cov
|
193
|
+
"""Float parameter."""
|
194
|
+
|
193
195
|
type: Literal["float"] = "float"
|
194
196
|
precision: int = 6
|
195
197
|
|
196
198
|
def rounding(self, value: float) -> float:
|
199
|
+
"""Rounding float value with the specific precision field."""
|
197
200
|
round_str: str = f"{{0:.{self.precision}f}}"
|
198
201
|
return float(round_str.format(round(value, self.precision)))
|
199
202
|
|
@@ -224,6 +227,7 @@ class DecimalParam(DefaultParam): # pragma: no cov
|
|
224
227
|
precision: int = 6
|
225
228
|
|
226
229
|
def rounding(self, value: Decimal) -> Decimal:
|
230
|
+
"""Rounding float value with the specific precision field."""
|
227
231
|
return value.quantize(Decimal(10) ** -self.precision)
|
228
232
|
|
229
233
|
def receive(self, value: float | Decimal | None = None) -> Decimal:
|
ddeutil/workflow/result.py
CHANGED
@@ -37,6 +37,14 @@ class Status(IntEnum):
|
|
37
37
|
SKIP: int = 3
|
38
38
|
CANCEL: int = 4
|
39
39
|
|
40
|
+
@property
|
41
|
+
def emoji(self) -> str:
|
42
|
+
"""Return the emoji value of this status.
|
43
|
+
|
44
|
+
:rtype: str
|
45
|
+
"""
|
46
|
+
return {0: "✅", 1: "❌", 2: "🟡", 3: "⏩", 4: "🚫"}[self.value]
|
47
|
+
|
40
48
|
|
41
49
|
SUCCESS = Status.SUCCESS
|
42
50
|
FAILED = Status.FAILED
|
@@ -46,10 +54,7 @@ CANCEL = Status.CANCEL
|
|
46
54
|
|
47
55
|
|
48
56
|
@dataclass(
|
49
|
-
config=ConfigDict(
|
50
|
-
arbitrary_types_allowed=True,
|
51
|
-
use_enum_values=True,
|
52
|
-
),
|
57
|
+
config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True),
|
53
58
|
)
|
54
59
|
class Result:
|
55
60
|
"""Result Pydantic Model for passing and receiving data context from any
|
ddeutil/workflow/scheduler.py
CHANGED
@@ -3,7 +3,6 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
# [x] Use dynamic config
|
7
6
|
"""The main schedule running is `schedule_runner` function that trigger the
|
8
7
|
multiprocess of `schedule_control` function for listing schedules on the
|
9
8
|
config by `Loader.finds(Schedule)`.
|
@@ -17,6 +16,11 @@ functions; `workflow_task`, and `workflow_monitor`.
|
|
17
16
|
The `schedule_task` will run `task.release` method in threading object
|
18
17
|
for multithreading strategy. This `release` method will run only one crontab
|
19
18
|
value with the on field.
|
19
|
+
|
20
|
+
Steps:
|
21
|
+
- Extract all schedule config on the conf path.
|
22
|
+
- Slice schedules to multiprocess
|
23
|
+
- Start running task.
|
20
24
|
"""
|
21
25
|
from __future__ import annotations
|
22
26
|
|
@@ -53,7 +57,7 @@ except ImportError: # pragma: no cov
|
|
53
57
|
from .__cron import CronRunner
|
54
58
|
from .__types import DictData, TupleStr
|
55
59
|
from .conf import FileLoad, Loader, dynamic
|
56
|
-
from .event import
|
60
|
+
from .event import Crontab
|
57
61
|
from .exceptions import ScheduleException, WorkflowException
|
58
62
|
from .logs import Audit, get_audit
|
59
63
|
from .result import SUCCESS, Result
|
@@ -99,9 +103,9 @@ class ScheduleWorkflow(BaseModel):
|
|
99
103
|
description="An alias name of workflow that use for schedule model.",
|
100
104
|
)
|
101
105
|
name: str = Field(description="A workflow name.")
|
102
|
-
on: list[
|
106
|
+
on: list[Crontab] = Field(
|
103
107
|
default_factory=list,
|
104
|
-
description="An override the list of
|
108
|
+
description="An override the list of Crontab object values.",
|
105
109
|
)
|
106
110
|
values: DictData = Field(
|
107
111
|
default_factory=dict,
|
@@ -154,15 +158,17 @@ class ScheduleWorkflow(BaseModel):
|
|
154
158
|
return data
|
155
159
|
|
156
160
|
@field_validator("on", mode="after")
|
157
|
-
def __on_no_dup__(
|
161
|
+
def __on_no_dup__(
|
162
|
+
cls, value: list[Crontab], info: ValidationInfo
|
163
|
+
) -> list[Crontab]:
|
158
164
|
"""Validate the on fields should not contain duplicate values and if it
|
159
165
|
contains every minute value, it should have only one on value.
|
160
166
|
|
161
|
-
:param value: (list[
|
167
|
+
:param value: (list[Crontab]) A list of `Crontab` object.
|
162
168
|
:param info: (ValidationInfo) An validation info object for getting an
|
163
169
|
extra parameter.
|
164
170
|
|
165
|
-
:rtype: list[
|
171
|
+
:rtype: list[Crontab]
|
166
172
|
"""
|
167
173
|
set_ons: set[str] = {str(on.cronjob) for on in value}
|
168
174
|
if len(set_ons) != len(value):
|
@@ -205,7 +211,7 @@ class ScheduleWorkflow(BaseModel):
|
|
205
211
|
|
206
212
|
# IMPORTANT: Create the default 'on' value if it does not pass the `on`
|
207
213
|
# field to the Schedule object.
|
208
|
-
ons: list[
|
214
|
+
ons: list[Crontab] = self.on or wf.on.copy()
|
209
215
|
workflow_tasks: list[WorkflowTask] = []
|
210
216
|
for on in ons:
|
211
217
|
|
@@ -699,7 +705,7 @@ def schedule_control(
|
|
699
705
|
:rtype: Result
|
700
706
|
"""
|
701
707
|
audit: type[Audit] = audit or get_audit(extras=extras)
|
702
|
-
result: Result = Result
|
708
|
+
result: Result = Result.construct_with_rs_or_id(parent_run_id=parent_run_id)
|
703
709
|
|
704
710
|
# NOTE: Create the start and stop datetime.
|
705
711
|
start_date: datetime = datetime.now(tz=dynamic("tz", extras=extras))
|