ddeutil-workflow 0.0.59__py3-none-any.whl → 0.0.60__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/__types.py +9 -2
- ddeutil/workflow/event.py +15 -12
- ddeutil/workflow/exceptions.py +4 -3
- ddeutil/workflow/job.py +16 -16
- ddeutil/workflow/logs.py +95 -24
- ddeutil/workflow/result.py +1 -1
- ddeutil/workflow/stages.py +37 -26
- ddeutil/workflow/utils.py +5 -4
- ddeutil/workflow/workflow.py +4 -3
- {ddeutil_workflow-0.0.59.dist-info → ddeutil_workflow-0.0.60.dist-info}/METADATA +3 -1
- {ddeutil_workflow-0.0.59.dist-info → ddeutil_workflow-0.0.60.dist-info}/RECORD +16 -16
- {ddeutil_workflow-0.0.59.dist-info → ddeutil_workflow-0.0.60.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.59.dist-info → ddeutil_workflow-0.0.60.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.59.dist-info → ddeutil_workflow-0.0.60.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.59.dist-info → ddeutil_workflow-0.0.60.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.60"
|
ddeutil/workflow/__types.py
CHANGED
@@ -20,6 +20,7 @@ from typing import Any, Optional, TypedDict, Union
|
|
20
20
|
|
21
21
|
from typing_extensions import Self
|
22
22
|
|
23
|
+
StrOrNone = Optional[str]
|
23
24
|
StrOrInt = Union[str, int]
|
24
25
|
TupleStr = tuple[str, ...]
|
25
26
|
DictData = dict[str, Any]
|
@@ -42,7 +43,7 @@ class CallerRe:
|
|
42
43
|
|
43
44
|
full: str
|
44
45
|
caller: str
|
45
|
-
caller_prefix:
|
46
|
+
caller_prefix: StrOrNone
|
46
47
|
caller_last: str
|
47
48
|
post_filters: str
|
48
49
|
|
@@ -50,6 +51,9 @@ class CallerRe:
|
|
50
51
|
def from_regex(cls, match: Match[str]) -> Self:
|
51
52
|
"""Class construct from matching result.
|
52
53
|
|
54
|
+
:param match: A match string object for contract this Caller regex data
|
55
|
+
class.
|
56
|
+
|
53
57
|
:rtype: Self
|
54
58
|
"""
|
55
59
|
return cls(full=match.group(0), **match.groupdict())
|
@@ -121,10 +125,13 @@ class Re:
|
|
121
125
|
)
|
122
126
|
|
123
127
|
@classmethod
|
124
|
-
def finditer_caller(cls, value) -> Iterator[CallerRe]:
|
128
|
+
def finditer_caller(cls, value: str) -> Iterator[CallerRe]:
|
125
129
|
"""Generate CallerRe object that create from matching object that
|
126
130
|
extract with re.finditer function.
|
127
131
|
|
132
|
+
:param value: (str) A string value that want to finditer with the caller
|
133
|
+
regular expression.
|
134
|
+
|
128
135
|
:rtype: Iterator[CallerRe]
|
129
136
|
"""
|
130
137
|
for found in cls.RE_CALLER.finditer(value):
|
ddeutil/workflow/event.py
CHANGED
@@ -3,8 +3,9 @@
|
|
3
3
|
# Licensed under the MIT License. See LICENSE in the project root for
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
|
-
"""Event module
|
7
|
-
`CrontabYear`
|
6
|
+
"""Event module include all event object for trigger the Workflow to release.
|
7
|
+
Now, it has only `Crontab` and `CrontabYear` event models on this module because
|
8
|
+
I think it is the core event for workflow orchestration.
|
8
9
|
"""
|
9
10
|
from __future__ import annotations
|
10
11
|
|
@@ -86,7 +87,7 @@ class Crontab(BaseModel):
|
|
86
87
|
CronJob,
|
87
88
|
Field(
|
88
89
|
description=(
|
89
|
-
"A Cronjob object that use for validate and generate datetime."
|
90
|
+
"A Cronjob object that use for validate and generate datetime."
|
90
91
|
),
|
91
92
|
),
|
92
93
|
]
|
@@ -117,7 +118,6 @@ class Crontab(BaseModel):
|
|
117
118
|
passing["cronjob"] = interval2crontab(
|
118
119
|
**{v: value[v] for v in value if v in ("interval", "day", "time")}
|
119
120
|
)
|
120
|
-
print(passing)
|
121
121
|
return cls(extras=extras | passing.pop("extras", {}), **passing)
|
122
122
|
|
123
123
|
@classmethod
|
@@ -170,9 +170,10 @@ class Crontab(BaseModel):
|
|
170
170
|
|
171
171
|
@model_validator(mode="before")
|
172
172
|
def __prepare_values(cls, data: Any) -> Any:
|
173
|
-
"""Extract tz key from
|
173
|
+
"""Extract a `tz` key from data and change the key name from `tz` to
|
174
|
+
`timezone`.
|
174
175
|
|
175
|
-
:param data: (DictData) A data that want to pass for create
|
176
|
+
:param data: (DictData) A data that want to pass for create a Crontab
|
176
177
|
model.
|
177
178
|
|
178
179
|
:rtype: DictData
|
@@ -198,7 +199,7 @@ class Crontab(BaseModel):
|
|
198
199
|
"cronjob", mode="before", json_schema_input_type=Union[CronJob, str]
|
199
200
|
)
|
200
201
|
def __prepare_cronjob(
|
201
|
-
cls, value: str
|
202
|
+
cls, value: Union[str, CronJob], info: ValidationInfo
|
202
203
|
) -> CronJob:
|
203
204
|
"""Prepare crontab value that able to receive with string type.
|
204
205
|
This step will get options kwargs from extras field and pass to the
|
@@ -234,7 +235,7 @@ class Crontab(BaseModel):
|
|
234
235
|
"""
|
235
236
|
return str(value)
|
236
237
|
|
237
|
-
def generate(self, start: str
|
238
|
+
def generate(self, start: Union[str, datetime]) -> CronRunner:
|
238
239
|
"""Return CronRunner object from an initial datetime.
|
239
240
|
|
240
241
|
:param start: (str | datetime) A string or datetime for generate the
|
@@ -248,7 +249,7 @@ class Crontab(BaseModel):
|
|
248
249
|
raise TypeError("start value should be str or datetime type.")
|
249
250
|
return self.cronjob.schedule(date=start, tz=self.tz)
|
250
251
|
|
251
|
-
def next(self, start: str
|
252
|
+
def next(self, start: Union[str, datetime]) -> CronRunner:
|
252
253
|
"""Return a next datetime from Cron runner object that start with any
|
253
254
|
date that given from input.
|
254
255
|
|
@@ -277,16 +278,18 @@ class CrontabYear(Crontab):
|
|
277
278
|
CronJobYear,
|
278
279
|
Field(
|
279
280
|
description=(
|
280
|
-
"A Cronjob object that use for validate and generate datetime."
|
281
|
+
"A Cronjob object that use for validate and generate datetime."
|
281
282
|
),
|
282
283
|
),
|
283
284
|
]
|
284
285
|
|
285
286
|
@field_validator(
|
286
|
-
"cronjob",
|
287
|
+
"cronjob",
|
288
|
+
mode="before",
|
289
|
+
json_schema_input_type=Union[CronJobYear, str],
|
287
290
|
)
|
288
291
|
def __prepare_cronjob(
|
289
|
-
cls, value:
|
292
|
+
cls, value: Union[CronJobYear, str], info: ValidationInfo
|
290
293
|
) -> CronJobYear:
|
291
294
|
"""Prepare crontab value that able to receive with string type.
|
292
295
|
This step will get options kwargs from extras field and pass to the
|
ddeutil/workflow/exceptions.py
CHANGED
@@ -9,7 +9,7 @@ annotate for handle error only.
|
|
9
9
|
"""
|
10
10
|
from __future__ import annotations
|
11
11
|
|
12
|
-
from typing import Literal, Optional, TypedDict, overload
|
12
|
+
from typing import Literal, Optional, TypedDict, Union, overload
|
13
13
|
|
14
14
|
|
15
15
|
class ErrorData(TypedDict):
|
@@ -55,8 +55,9 @@ class BaseWorkflowException(Exception):
|
|
55
55
|
|
56
56
|
def to_dict(
|
57
57
|
self, with_refs: bool = False
|
58
|
-
) -> ErrorData
|
59
|
-
"""Return ErrorData data from the current exception object.
|
58
|
+
) -> Union[ErrorData, dict[str, ErrorData]]:
|
59
|
+
"""Return ErrorData data from the current exception object. If with_refs
|
60
|
+
flag was set, it will return mapping of refs and itself data.
|
60
61
|
|
61
62
|
:rtype: ErrorData
|
62
63
|
"""
|
ddeutil/workflow/job.py
CHANGED
@@ -39,7 +39,7 @@ from pydantic import BaseModel, Discriminator, Field, SecretStr, Tag
|
|
39
39
|
from pydantic.functional_validators import field_validator, model_validator
|
40
40
|
from typing_extensions import Self
|
41
41
|
|
42
|
-
from .__types import DictData, DictStr, Matrix
|
42
|
+
from .__types import DictData, DictStr, Matrix, StrOrNone
|
43
43
|
from .exceptions import (
|
44
44
|
JobException,
|
45
45
|
StageException,
|
@@ -329,14 +329,14 @@ class Job(BaseModel):
|
|
329
329
|
... }
|
330
330
|
"""
|
331
331
|
|
332
|
-
id:
|
332
|
+
id: StrOrNone = Field(
|
333
333
|
default=None,
|
334
334
|
description=(
|
335
335
|
"A job ID that was set from Workflow model after initialize step. "
|
336
336
|
"If this model create standalone, it will be None."
|
337
337
|
),
|
338
338
|
)
|
339
|
-
desc:
|
339
|
+
desc: StrOrNone = Field(
|
340
340
|
default=None,
|
341
341
|
description="A job description that can be markdown syntax.",
|
342
342
|
)
|
@@ -345,7 +345,7 @@ class Job(BaseModel):
|
|
345
345
|
description="A target node for this job to use for execution.",
|
346
346
|
alias="runs-on",
|
347
347
|
)
|
348
|
-
condition:
|
348
|
+
condition: StrOrNone = Field(
|
349
349
|
default=None,
|
350
350
|
description="A job condition statement to allow job executable.",
|
351
351
|
alias="if",
|
@@ -526,7 +526,7 @@ class Job(BaseModel):
|
|
526
526
|
output: DictData,
|
527
527
|
to: DictData,
|
528
528
|
*,
|
529
|
-
job_id:
|
529
|
+
job_id: StrOrNone = None,
|
530
530
|
) -> DictData:
|
531
531
|
"""Set an outputs from execution result context to the received context
|
532
532
|
with a `to` input parameter. The result context from job strategy
|
@@ -567,7 +567,7 @@ class Job(BaseModel):
|
|
567
567
|
:param output: (DictData) A result data context that want to extract
|
568
568
|
and transfer to the `strategies` key in receive context.
|
569
569
|
:param to: (DictData) A received context data.
|
570
|
-
:param job_id: (
|
570
|
+
:param job_id: (StrOrNone) A job ID if the `id` field does not set.
|
571
571
|
|
572
572
|
:rtype: DictData
|
573
573
|
"""
|
@@ -607,8 +607,8 @@ class Job(BaseModel):
|
|
607
607
|
self,
|
608
608
|
params: DictData,
|
609
609
|
*,
|
610
|
-
run_id:
|
611
|
-
parent_run_id:
|
610
|
+
run_id: StrOrNone = None,
|
611
|
+
parent_run_id: StrOrNone = None,
|
612
612
|
event: Optional[Event] = None,
|
613
613
|
) -> Result:
|
614
614
|
"""Job execution with passing dynamic parameters from the workflow
|
@@ -800,8 +800,8 @@ def local_execute(
|
|
800
800
|
job: Job,
|
801
801
|
params: DictData,
|
802
802
|
*,
|
803
|
-
run_id:
|
804
|
-
parent_run_id:
|
803
|
+
run_id: StrOrNone = None,
|
804
|
+
parent_run_id: StrOrNone = None,
|
805
805
|
event: Optional[Event] = None,
|
806
806
|
) -> Result:
|
807
807
|
"""Local job execution with passing dynamic parameters from the workflow
|
@@ -919,8 +919,8 @@ def self_hosted_execute(
|
|
919
919
|
job: Job,
|
920
920
|
params: DictData,
|
921
921
|
*,
|
922
|
-
run_id:
|
923
|
-
parent_run_id:
|
922
|
+
run_id: StrOrNone = None,
|
923
|
+
parent_run_id: StrOrNone = None,
|
924
924
|
event: Optional[Event] = None,
|
925
925
|
) -> Result: # pragma: no cov
|
926
926
|
"""Self-Hosted job execution with passing dynamic parameters from the
|
@@ -982,8 +982,8 @@ def azure_batch_execute(
|
|
982
982
|
job: Job,
|
983
983
|
params: DictData,
|
984
984
|
*,
|
985
|
-
run_id:
|
986
|
-
parent_run_id:
|
985
|
+
run_id: StrOrNone = None,
|
986
|
+
parent_run_id: StrOrNone = None,
|
987
987
|
event: Optional[Event] = None,
|
988
988
|
) -> Result: # pragma: no cov
|
989
989
|
"""Azure Batch job execution that will run all job's stages on the Azure
|
@@ -1036,8 +1036,8 @@ def docker_execution(
|
|
1036
1036
|
job: Job,
|
1037
1037
|
params: DictData,
|
1038
1038
|
*,
|
1039
|
-
run_id:
|
1040
|
-
parent_run_id:
|
1039
|
+
run_id: StrOrNone = None,
|
1040
|
+
parent_run_id: StrOrNone = None,
|
1041
1041
|
event: Optional[Event] = None,
|
1042
1042
|
): # pragma: no cov
|
1043
1043
|
"""Docker job execution.
|
ddeutil/workflow/logs.py
CHANGED
@@ -4,13 +4,17 @@
|
|
4
4
|
# license information.
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
# [x] Use fix config for `get_logger`, and Model initialize step.
|
7
|
-
"""A Logs module contain Trace
|
7
|
+
"""A Logs module contain Trace and Audit Pydantic models for process log from
|
8
|
+
the core workflow engine. I separate part of log to 2 types:
|
9
|
+
- Trace: A stdout and stderr log
|
10
|
+
- Audit: An audit release log for tracking incremental running workflow.
|
8
11
|
"""
|
9
12
|
from __future__ import annotations
|
10
13
|
|
11
14
|
import json
|
12
15
|
import logging
|
13
16
|
import os
|
17
|
+
import re
|
14
18
|
from abc import ABC, abstractmethod
|
15
19
|
from collections.abc import Iterator
|
16
20
|
from datetime import datetime
|
@@ -18,6 +22,7 @@ from functools import lru_cache
|
|
18
22
|
from inspect import Traceback, currentframe, getframeinfo
|
19
23
|
from pathlib import Path
|
20
24
|
from threading import get_ident
|
25
|
+
from types import FrameType
|
21
26
|
from typing import ClassVar, Literal, Optional, TypeVar, Union
|
22
27
|
|
23
28
|
from pydantic import BaseModel, ConfigDict, Field
|
@@ -28,12 +33,14 @@ from .__types import DictData
|
|
28
33
|
from .conf import config, dynamic
|
29
34
|
from .utils import cut_id, get_dt_now, prepare_newline
|
30
35
|
|
36
|
+
METADATA: str = "metadata.json"
|
37
|
+
|
31
38
|
|
32
39
|
@lru_cache
|
33
40
|
def get_logger(name: str):
|
34
41
|
"""Return logger object with an input module name.
|
35
42
|
|
36
|
-
:param name: A module name that want to log.
|
43
|
+
:param name: (str) A module name that want to log.
|
37
44
|
"""
|
38
45
|
lg = logging.getLogger(name)
|
39
46
|
|
@@ -68,13 +75,55 @@ def get_dt_tznow() -> datetime: # pragma: no cov
|
|
68
75
|
|
69
76
|
|
70
77
|
PREFIX_LOGS: dict[str, dict] = {
|
71
|
-
"CALLER": {
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
"
|
76
|
-
"
|
78
|
+
"CALLER": {
|
79
|
+
"emoji": "📍",
|
80
|
+
"desc": "logs from any usage from custom caller function.",
|
81
|
+
},
|
82
|
+
"STAGE": {"emoji": "⚙️", "desc": "logs from stages module."},
|
83
|
+
"JOB": {"emoji": "⛓️", "desc": "logs from job module."},
|
84
|
+
"WORKFLOW": {"emoji": "🏃", "desc": "logs from workflow module."},
|
85
|
+
"RELEASE": {"emoji": "📅", "desc": "logs from release workflow method."},
|
86
|
+
"POKING": {"emoji": "⏰", "desc": "logs from poke workflow method."},
|
77
87
|
} # pragma: no cov
|
88
|
+
PREFIX_DEFAULT: str = "CALLER"
|
89
|
+
PREFIX_LOGS_REGEX: re.Pattern[str] = re.compile(
|
90
|
+
rf"(^\[(?P<name>{'|'.join(PREFIX_LOGS)})]:\s?)?(?P<message>.*)",
|
91
|
+
re.MULTILINE | re.DOTALL | re.ASCII | re.VERBOSE,
|
92
|
+
) # pragma: no cov
|
93
|
+
|
94
|
+
|
95
|
+
class PrefixMsg(BaseModel):
|
96
|
+
"""Prefix Message model for receive grouping dict from searching prefix data
|
97
|
+
from logging message.
|
98
|
+
"""
|
99
|
+
|
100
|
+
name: Optional[str] = Field(default=None)
|
101
|
+
message: Optional[str] = Field(default=None)
|
102
|
+
|
103
|
+
def prepare(self, extras: Optional[DictData] = None) -> str:
|
104
|
+
"""Prepare message with force add prefix before writing trace log.
|
105
|
+
|
106
|
+
:rtype: str
|
107
|
+
"""
|
108
|
+
name: str = self.name or PREFIX_DEFAULT
|
109
|
+
emoji: str = (
|
110
|
+
f"{PREFIX_LOGS[name]['emoji']} "
|
111
|
+
if (extras or {}).get("log_add_emoji", True)
|
112
|
+
else ""
|
113
|
+
)
|
114
|
+
return f"{emoji}[{name}]: {self.message}"
|
115
|
+
|
116
|
+
|
117
|
+
def extract_msg_prefix(msg: str) -> PrefixMsg:
|
118
|
+
"""Extract message prefix from an input message.
|
119
|
+
|
120
|
+
:param msg: A message that want to extract.
|
121
|
+
|
122
|
+
:rtype: PrefixMsg
|
123
|
+
"""
|
124
|
+
return PrefixMsg.model_validate(
|
125
|
+
obj=PREFIX_LOGS_REGEX.search(msg).groupdict()
|
126
|
+
)
|
78
127
|
|
79
128
|
|
80
129
|
class TraceMeta(BaseModel): # pragma: no cov
|
@@ -91,6 +140,28 @@ class TraceMeta(BaseModel): # pragma: no cov
|
|
91
140
|
filename: str = Field(description="A filename of this log.")
|
92
141
|
lineno: int = Field(description="A line number of this log.")
|
93
142
|
|
143
|
+
@classmethod
|
144
|
+
def dynamic_frame(
|
145
|
+
cls, frame: FrameType, *, extras: Optional[DictData] = None
|
146
|
+
) -> Traceback:
|
147
|
+
"""Dynamic Frame information base on the `logs_trace_frame_layer` config
|
148
|
+
value that was set from the extra parameter.
|
149
|
+
|
150
|
+
:param frame: (FrameType) The current frame that want to dynamic.
|
151
|
+
:param extras: (DictData) An extra parameter that want to get the
|
152
|
+
`logs_trace_frame_layer` config value.
|
153
|
+
"""
|
154
|
+
extras: DictData = extras or {}
|
155
|
+
layer: int = extras.get("logs_trace_frame_layer", 4)
|
156
|
+
for _ in range(layer):
|
157
|
+
_frame: Optional[FrameType] = frame.f_back
|
158
|
+
if _frame is None:
|
159
|
+
raise ValueError(
|
160
|
+
f"Layer value does not valid, the maximum frame is: {_ + 1}"
|
161
|
+
)
|
162
|
+
frame: FrameType = _frame
|
163
|
+
return getframeinfo(frame)
|
164
|
+
|
94
165
|
@classmethod
|
95
166
|
def make(
|
96
167
|
cls,
|
@@ -100,7 +171,8 @@ class TraceMeta(BaseModel): # pragma: no cov
|
|
100
171
|
*,
|
101
172
|
extras: Optional[DictData] = None,
|
102
173
|
) -> Self:
|
103
|
-
"""Make the current
|
174
|
+
"""Make the current metric for contract this TraceMeta model instance
|
175
|
+
that will catch local states like PID, thread identity.
|
104
176
|
|
105
177
|
:param mode: (Literal["stdout", "stderr"]) A metadata mode.
|
106
178
|
:param message: (str) A message.
|
@@ -110,9 +182,8 @@ class TraceMeta(BaseModel): # pragma: no cov
|
|
110
182
|
|
111
183
|
:rtype: Self
|
112
184
|
"""
|
113
|
-
|
114
|
-
|
115
|
-
)
|
185
|
+
frame: FrameType = currentframe()
|
186
|
+
frame_info: Traceback = cls.dynamic_frame(frame, extras=extras)
|
116
187
|
extras: DictData = extras or {}
|
117
188
|
return cls(
|
118
189
|
mode=mode,
|
@@ -157,13 +228,11 @@ class TraceData(BaseModel): # pragma: no cov
|
|
157
228
|
if (file / f"{mode}.txt").exists():
|
158
229
|
data[mode] = (file / f"{mode}.txt").read_text(encoding="utf-8")
|
159
230
|
|
160
|
-
if (file /
|
231
|
+
if (file / METADATA).exists():
|
161
232
|
data["meta"] = [
|
162
233
|
json.loads(line)
|
163
234
|
for line in (
|
164
|
-
(file /
|
165
|
-
.read_text(encoding="utf-8")
|
166
|
-
.splitlines()
|
235
|
+
(file / METADATA).read_text(encoding="utf-8").splitlines()
|
167
236
|
)
|
168
237
|
]
|
169
238
|
|
@@ -263,7 +332,9 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
263
332
|
|
264
333
|
:param message: (str) A message that want to log.
|
265
334
|
"""
|
266
|
-
msg: str = prepare_newline(
|
335
|
+
msg: str = prepare_newline(
|
336
|
+
self.make_message(extract_msg_prefix(message).prepare(self.extras))
|
337
|
+
)
|
267
338
|
|
268
339
|
if mode != "debug" or (
|
269
340
|
mode == "debug" and dynamic("debug", extras=self.extras)
|
@@ -320,7 +391,9 @@ class BaseTrace(BaseModel, ABC): # pragma: no cov
|
|
320
391
|
|
321
392
|
:param message: (str) A message that want to log.
|
322
393
|
"""
|
323
|
-
msg: str = prepare_newline(
|
394
|
+
msg: str = prepare_newline(
|
395
|
+
self.make_message(extract_msg_prefix(message).prepare(self.extras))
|
396
|
+
)
|
324
397
|
|
325
398
|
if mode != "debug" or (
|
326
399
|
mode == "debug" and dynamic("debug", extras=self.extras)
|
@@ -468,7 +541,7 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
468
541
|
|
469
542
|
mode: Literal["stdout", "stderr"] = "stderr" if is_err else "stdout"
|
470
543
|
trace_meta: TraceMeta = TraceMeta.make(
|
471
|
-
mode=mode, level=level, message=message
|
544
|
+
mode=mode, level=level, message=message, extras=self.extras
|
472
545
|
)
|
473
546
|
|
474
547
|
with (self.pointer / f"{mode}.txt").open(
|
@@ -477,9 +550,7 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
477
550
|
fmt: str = dynamic("log_format_file", extras=self.extras)
|
478
551
|
f.write(f"{fmt}\n".format(**trace_meta.model_dump()))
|
479
552
|
|
480
|
-
with (self.pointer /
|
481
|
-
mode="at", encoding="utf-8"
|
482
|
-
) as f:
|
553
|
+
with (self.pointer / METADATA).open(mode="at", encoding="utf-8") as f:
|
483
554
|
f.write(trace_meta.model_dump_json() + "\n")
|
484
555
|
|
485
556
|
async def awriter(
|
@@ -496,7 +567,7 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
496
567
|
|
497
568
|
mode: Literal["stdout", "stderr"] = "stderr" if is_err else "stdout"
|
498
569
|
trace_meta: TraceMeta = TraceMeta.make(
|
499
|
-
mode=mode, level=level, message=message
|
570
|
+
mode=mode, level=level, message=message, extras=self.extras
|
500
571
|
)
|
501
572
|
|
502
573
|
async with aiofiles.open(
|
@@ -506,7 +577,7 @@ class FileTrace(BaseTrace): # pragma: no cov
|
|
506
577
|
await f.write(f"{fmt}\n".format(**trace_meta.model_dump()))
|
507
578
|
|
508
579
|
async with aiofiles.open(
|
509
|
-
self.pointer /
|
580
|
+
self.pointer / METADATA, mode="at", encoding="utf-8"
|
510
581
|
) as f:
|
511
582
|
await f.write(trace_meta.model_dump_json() + "\n")
|
512
583
|
|
ddeutil/workflow/result.py
CHANGED
ddeutil/workflow/stages.py
CHANGED
@@ -58,7 +58,7 @@ from pydantic import BaseModel, Field
|
|
58
58
|
from pydantic.functional_validators import model_validator
|
59
59
|
from typing_extensions import Self
|
60
60
|
|
61
|
-
from .__types import DictData, DictStr, StrOrInt, TupleStr
|
61
|
+
from .__types import DictData, DictStr, StrOrInt, StrOrNone, TupleStr
|
62
62
|
from .conf import dynamic
|
63
63
|
from .exceptions import StageException, to_dict
|
64
64
|
from .result import CANCEL, FAILED, SUCCESS, WAIT, Result, Status
|
@@ -87,7 +87,7 @@ class BaseStage(BaseModel, ABC):
|
|
87
87
|
default_factory=dict,
|
88
88
|
description="An extra parameter that override core config values.",
|
89
89
|
)
|
90
|
-
id:
|
90
|
+
id: StrOrNone = Field(
|
91
91
|
default=None,
|
92
92
|
description=(
|
93
93
|
"A stage ID that use to keep execution output or getting by job "
|
@@ -97,7 +97,7 @@ class BaseStage(BaseModel, ABC):
|
|
97
97
|
name: str = Field(
|
98
98
|
description="A stage name that want to logging when start execution.",
|
99
99
|
)
|
100
|
-
condition:
|
100
|
+
condition: StrOrNone = Field(
|
101
101
|
default=None,
|
102
102
|
description=(
|
103
103
|
"A stage condition statement to allow stage executable. This field "
|
@@ -162,8 +162,8 @@ class BaseStage(BaseModel, ABC):
|
|
162
162
|
self,
|
163
163
|
params: DictData,
|
164
164
|
*,
|
165
|
-
run_id:
|
166
|
-
parent_run_id:
|
165
|
+
run_id: StrOrNone = None,
|
166
|
+
parent_run_id: StrOrNone = None,
|
167
167
|
result: Optional[Result] = None,
|
168
168
|
event: Optional[Event] = None,
|
169
169
|
raise_error: Optional[bool] = None,
|
@@ -411,8 +411,8 @@ class BaseAsyncStage(BaseStage):
|
|
411
411
|
self,
|
412
412
|
params: DictData,
|
413
413
|
*,
|
414
|
-
run_id:
|
415
|
-
parent_run_id:
|
414
|
+
run_id: StrOrNone = None,
|
415
|
+
parent_run_id: StrOrNone = None,
|
416
416
|
result: Optional[Result] = None,
|
417
417
|
event: Optional[Event] = None,
|
418
418
|
raise_error: Optional[bool] = None,
|
@@ -469,7 +469,7 @@ class EmptyStage(BaseAsyncStage):
|
|
469
469
|
... }
|
470
470
|
"""
|
471
471
|
|
472
|
-
echo:
|
472
|
+
echo: StrOrNone = Field(
|
473
473
|
default=None,
|
474
474
|
description="A message that want to show on the stdout.",
|
475
475
|
)
|
@@ -598,14 +598,14 @@ class BashStage(BaseAsyncStage):
|
|
598
598
|
)
|
599
599
|
|
600
600
|
@contextlib.asynccontextmanager
|
601
|
-
async def
|
602
|
-
self, bash: str, env: DictStr, run_id:
|
601
|
+
async def async_create_sh_file(
|
602
|
+
self, bash: str, env: DictStr, run_id: StrOrNone = None
|
603
603
|
) -> AsyncIterator[TupleStr]:
|
604
604
|
"""Async create and write `.sh` file with the `aiofiles` package.
|
605
605
|
|
606
606
|
:param bash: (str) A bash statement.
|
607
607
|
:param env: (DictStr) An environment variable that set before run bash.
|
608
|
-
:param run_id: (
|
608
|
+
:param run_id: (StrOrNone) A running stage ID that use for writing sh
|
609
609
|
file instead generate by UUID4.
|
610
610
|
|
611
611
|
:rtype: AsyncIterator[TupleStr]
|
@@ -635,14 +635,14 @@ class BashStage(BaseAsyncStage):
|
|
635
635
|
|
636
636
|
@contextlib.contextmanager
|
637
637
|
def create_sh_file(
|
638
|
-
self, bash: str, env: DictStr, run_id:
|
638
|
+
self, bash: str, env: DictStr, run_id: StrOrNone = None
|
639
639
|
) -> Iterator[TupleStr]:
|
640
640
|
"""Create and write the `.sh` file before giving this file name to
|
641
641
|
context. After that, it will auto delete this file automatic.
|
642
642
|
|
643
643
|
:param bash: (str) A bash statement.
|
644
644
|
:param env: (DictStr) An environment variable that set before run bash.
|
645
|
-
:param run_id: (
|
645
|
+
:param run_id: (StrOrNone) A running stage ID that use for writing sh
|
646
646
|
file instead generate by UUID4.
|
647
647
|
|
648
648
|
:rtype: Iterator[TupleStr]
|
@@ -752,7 +752,7 @@ class BashStage(BaseAsyncStage):
|
|
752
752
|
dedent(self.bash.strip("\n")), params, extras=self.extras
|
753
753
|
)
|
754
754
|
|
755
|
-
async with self.
|
755
|
+
async with self.async_create_sh_file(
|
756
756
|
bash=bash,
|
757
757
|
env=param2template(self.env, params, extras=self.extras),
|
758
758
|
run_id=result.run_id,
|
@@ -1294,11 +1294,12 @@ class TriggerStage(BaseStage):
|
|
1294
1294
|
extras=self.extras | {"stage_raise_error": True},
|
1295
1295
|
).execute(
|
1296
1296
|
params=param2template(self.params, params, extras=self.extras),
|
1297
|
-
|
1297
|
+
run_id=None,
|
1298
|
+
parent_run_id=result.parent_run_id,
|
1298
1299
|
event=event,
|
1299
1300
|
)
|
1300
1301
|
if rs.status == FAILED:
|
1301
|
-
err_msg:
|
1302
|
+
err_msg: StrOrNone = (
|
1302
1303
|
f" with:\n{msg}"
|
1303
1304
|
if (msg := rs.context.get("errors", {}).get("message"))
|
1304
1305
|
else "."
|
@@ -1826,7 +1827,10 @@ class UntilStage(BaseNestedStage):
|
|
1826
1827
|
... "stages": [
|
1827
1828
|
... {
|
1828
1829
|
... "name": "Start increase item value.",
|
1829
|
-
... "run":
|
1830
|
+
... "run": (
|
1831
|
+
... "item = ${{ item }}\\n"
|
1832
|
+
... "item += 1\\n"
|
1833
|
+
... )
|
1830
1834
|
... },
|
1831
1835
|
... ],
|
1832
1836
|
... }
|
@@ -2215,9 +2219,7 @@ class CaseStage(BaseNestedStage):
|
|
2215
2219
|
extras=self.extras,
|
2216
2220
|
)
|
2217
2221
|
|
2218
|
-
_case:
|
2219
|
-
self.case, params, extras=self.extras
|
2220
|
-
)
|
2222
|
+
_case: StrOrNone = param2template(self.case, params, extras=self.extras)
|
2221
2223
|
|
2222
2224
|
result.trace.info(f"[STAGE]: Execute Case-Stage: {_case!r}.")
|
2223
2225
|
_else: Optional[Match] = None
|
@@ -2396,8 +2398,14 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
2396
2398
|
|
2397
2399
|
:rtype: Result
|
2398
2400
|
"""
|
2399
|
-
|
2400
|
-
|
2401
|
+
try:
|
2402
|
+
from docker import DockerClient
|
2403
|
+
from docker.errors import ContainerError
|
2404
|
+
except ImportError:
|
2405
|
+
raise ImportError(
|
2406
|
+
"Docker stage need the docker package, you should install it "
|
2407
|
+
"by `pip install docker` first."
|
2408
|
+
) from None
|
2401
2409
|
|
2402
2410
|
client = DockerClient(
|
2403
2411
|
base_url="unix://var/run/docker.sock", version="auto"
|
@@ -2459,7 +2467,7 @@ class DockerStage(BaseStage): # pragma: no cov
|
|
2459
2467
|
exit_status,
|
2460
2468
|
None,
|
2461
2469
|
f"{self.image}:{self.tag}",
|
2462
|
-
out,
|
2470
|
+
out.decode("utf-8"),
|
2463
2471
|
)
|
2464
2472
|
output_file: Path = Path(f".docker.{result.run_id}.logs/outputs.json")
|
2465
2473
|
if not output_file.exists():
|
@@ -2518,7 +2526,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
2518
2526
|
py: str,
|
2519
2527
|
values: DictData,
|
2520
2528
|
deps: list[str],
|
2521
|
-
run_id:
|
2529
|
+
run_id: StrOrNone = None,
|
2522
2530
|
) -> Iterator[str]:
|
2523
2531
|
"""Create the .py file with an input Python string statement.
|
2524
2532
|
|
@@ -2526,7 +2534,7 @@ class VirtualPyStage(PyStage): # pragma: no cov
|
|
2526
2534
|
:param values: A variable that want to set before running this
|
2527
2535
|
:param deps: An additional Python dependencies that want install before
|
2528
2536
|
run this python stage.
|
2529
|
-
:param run_id: (
|
2537
|
+
:param run_id: (StrOrNone) A running ID of this stage execution.
|
2530
2538
|
"""
|
2531
2539
|
run_id: str = run_id or uuid.uuid4()
|
2532
2540
|
f_name: str = f"{run_id}.py"
|
@@ -2644,5 +2652,8 @@ Stage = Annotated[
|
|
2644
2652
|
RaiseStage,
|
2645
2653
|
EmptyStage,
|
2646
2654
|
],
|
2647
|
-
Field(
|
2655
|
+
Field(
|
2656
|
+
union_mode="smart",
|
2657
|
+
description="A stage models that already implemented on this package.",
|
2658
|
+
),
|
2648
2659
|
] # pragma: no cov
|
ddeutil/workflow/utils.py
CHANGED
@@ -24,7 +24,7 @@ from .__types import DictData, Matrix
|
|
24
24
|
|
25
25
|
T = TypeVar("T")
|
26
26
|
UTC: Final[ZoneInfo] = ZoneInfo("UTC")
|
27
|
-
|
27
|
+
MARK_NEWLINE: Final[str] = "||"
|
28
28
|
|
29
29
|
|
30
30
|
def prepare_newline(msg: str) -> str:
|
@@ -34,11 +34,12 @@ def prepare_newline(msg: str) -> str:
|
|
34
34
|
|
35
35
|
:rtype: str
|
36
36
|
"""
|
37
|
-
|
38
|
-
|
37
|
+
# NOTE: Remove ending with "\n" and replace "\n" with the "||" value.
|
38
|
+
msg: str = msg.strip("\n").replace("\n", MARK_NEWLINE)
|
39
|
+
if MARK_NEWLINE not in msg:
|
39
40
|
return msg
|
40
41
|
|
41
|
-
msg_lines: list[str] = msg.split(
|
42
|
+
msg_lines: list[str] = msg.split(MARK_NEWLINE)
|
42
43
|
msg_last: str = msg_lines[-1]
|
43
44
|
msg_body: str = (
|
44
45
|
"\n" + "\n".join(f" ... | \t{s}" for s in msg_lines[1:-1])
|
ddeutil/workflow/workflow.py
CHANGED
@@ -563,11 +563,12 @@ class Workflow(BaseModel):
|
|
563
563
|
adding jobs key to this parameter.
|
564
564
|
"""
|
565
565
|
# VALIDATE: Incoming params should have keys that set on this workflow.
|
566
|
-
|
566
|
+
check_key: list[str] = [
|
567
567
|
f"{k!r}"
|
568
568
|
for k in self.params
|
569
569
|
if (k not in params and self.params[k].required)
|
570
|
-
]
|
570
|
+
]
|
571
|
+
if check_key:
|
571
572
|
raise WorkflowException(
|
572
573
|
f"Required Param on this workflow setting does not set: "
|
573
574
|
f"{', '.join(check_key)}."
|
@@ -670,7 +671,7 @@ class Workflow(BaseModel):
|
|
670
671
|
rs: Result = self.execute(
|
671
672
|
params=values,
|
672
673
|
result=result,
|
673
|
-
parent_run_id=result.
|
674
|
+
parent_run_id=result.run_id,
|
674
675
|
timeout=timeout,
|
675
676
|
)
|
676
677
|
result.trace.info(
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ddeutil-workflow
|
3
|
-
Version: 0.0.
|
3
|
+
Version: 0.0.60
|
4
4
|
Summary: Lightweight workflow orchestration
|
5
5
|
Author-email: ddeutils <korawich.anu@gmail.com>
|
6
6
|
License: MIT
|
@@ -44,6 +44,8 @@ Requires-Dist: aiofiles; extra == "async"
|
|
44
44
|
Requires-Dist: aiohttp; extra == "async"
|
45
45
|
Provides-Extra: docker
|
46
46
|
Requires-Dist: docker==7.1.0; extra == "docker"
|
47
|
+
Provides-Extra: self-hosted
|
48
|
+
Requires-Dist: requests==2.32.3; extra == "self-hosted"
|
47
49
|
Dynamic: license-file
|
48
50
|
|
49
51
|
# Workflow Orchestration
|
@@ -1,20 +1,20 @@
|
|
1
|
-
ddeutil/workflow/__about__.py,sha256=
|
1
|
+
ddeutil/workflow/__about__.py,sha256=sQSmxiDbXlnTI1qDQGcyxr1EGvwITzIX0PKi2dOg4LU,28
|
2
2
|
ddeutil/workflow/__cron.py,sha256=5DHQKejG-76L_oREW78RcwMzeyKddJxSMmBzYyMAeeY,28536
|
3
3
|
ddeutil/workflow/__init__.py,sha256=NXEhjzKFdIGa-jtIq9HXChLCjSXNPd8VJ8ltggxbBO8,1371
|
4
4
|
ddeutil/workflow/__main__.py,sha256=x-sYedl4T8p6054aySk-EQX6vhytvPR0HvaBNYxMzp0,364
|
5
|
-
ddeutil/workflow/__types.py,sha256=
|
5
|
+
ddeutil/workflow/__types.py,sha256=uNfoRbVmNK5O37UUMVnqcmoghD9oMS1q9fXC0APnjSI,4584
|
6
6
|
ddeutil/workflow/conf.py,sha256=NLvjZ8bpDsn4e0MG3m1vgMdAwtmii5hP1D0STKQyZeo,14907
|
7
|
-
ddeutil/workflow/event.py,sha256=
|
8
|
-
ddeutil/workflow/exceptions.py,sha256=
|
9
|
-
ddeutil/workflow/job.py,sha256=
|
10
|
-
ddeutil/workflow/logs.py,sha256=
|
7
|
+
ddeutil/workflow/event.py,sha256=iAvd7TfAJaMndDhxbi1xNLzl4wNlgLqe1nIseaIm5-Y,10533
|
8
|
+
ddeutil/workflow/exceptions.py,sha256=TKHBIlfquz3yEb8_kg6UXpxVLKxstt3QA9a1XYsLPJk,2455
|
9
|
+
ddeutil/workflow/job.py,sha256=Php1b3n6c-jddel8PTSa61kAW22QBTetzoLVR4XXM4E,35240
|
10
|
+
ddeutil/workflow/logs.py,sha256=81wl83dbYDcMctGmWiptmFaoZoXFO2TS0E4sxOILOQk,31321
|
11
11
|
ddeutil/workflow/params.py,sha256=tBjKe1_e0TlUrSrlMahDuAdNNBlGBAKMmMMQ9eV-YSs,11616
|
12
|
-
ddeutil/workflow/result.py,sha256=
|
12
|
+
ddeutil/workflow/result.py,sha256=4M9VCcveI8Yz6ZrnI-67SZlry-Z8G7e0hziy1k-pklk,5906
|
13
13
|
ddeutil/workflow/reusables.py,sha256=mw_Fi763B5am0EmntcjLBF7MDEhKqud2BYHcYyno5Ec,17663
|
14
14
|
ddeutil/workflow/scheduler.py,sha256=OsEyj2zscQ-3bDMk2z7UtKlCWLlgoGjaRFt17o1B1ew,27263
|
15
|
-
ddeutil/workflow/stages.py,sha256=
|
16
|
-
ddeutil/workflow/utils.py,sha256=
|
17
|
-
ddeutil/workflow/workflow.py,sha256=
|
15
|
+
ddeutil/workflow/stages.py,sha256=N_DkEUGiwpglovtXx-Wg3zX_03eGBT650zRsZV7knKk,92640
|
16
|
+
ddeutil/workflow/utils.py,sha256=ADJTt3kiF44qntsRnOUdCFihlB2WWbRE-Tojp5EOYbk,8898
|
17
|
+
ddeutil/workflow/workflow.py,sha256=BFnaB_7mrYZ3KV07AV16xR9khsoSt9i3QLyEtrLNAqs,44877
|
18
18
|
ddeutil/workflow/api/__init__.py,sha256=kY30dL8HPY8tY_GBmm7y_3OdoXzB1-EA2a96PLU0AQw,5278
|
19
19
|
ddeutil/workflow/api/logs.py,sha256=NMTnOnsBrDB5129329xF2myLdrb-z9k1MQrmrP7qXJw,1818
|
20
20
|
ddeutil/workflow/api/utils.py,sha256=uTtUFVLpiYYahXvCVx8sueRQ03K2Xw1id_gW3IMmX1U,5295
|
@@ -23,9 +23,9 @@ ddeutil/workflow/api/routes/job.py,sha256=8X5VLDJH6PumyNIY6JGRNBsf2gWN0eG9DzxRPS
|
|
23
23
|
ddeutil/workflow/api/routes/logs.py,sha256=U6vOni3wd-ZTOwd3yVdSOpgyRmNdcgfngU5KlLM3Cww,5383
|
24
24
|
ddeutil/workflow/api/routes/schedules.py,sha256=14RnaJKEGMSJtncI1H_QQVZNBe_jDS40PPRO6qFc3i0,4805
|
25
25
|
ddeutil/workflow/api/routes/workflows.py,sha256=GJu5PiXEylswrXylEImpncySjeU9chrvrtjhiMCw2RQ,4529
|
26
|
-
ddeutil_workflow-0.0.
|
27
|
-
ddeutil_workflow-0.0.
|
28
|
-
ddeutil_workflow-0.0.
|
29
|
-
ddeutil_workflow-0.0.
|
30
|
-
ddeutil_workflow-0.0.
|
31
|
-
ddeutil_workflow-0.0.
|
26
|
+
ddeutil_workflow-0.0.60.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
|
27
|
+
ddeutil_workflow-0.0.60.dist-info/METADATA,sha256=VixljHKK-7rmiv5UC_65sp2DAgb3kHjms1H7vvF0DyY,19427
|
28
|
+
ddeutil_workflow-0.0.60.dist-info/WHEEL,sha256=0CuiUZ_p9E4cD6NyLD6UG80LBXYyiSYZOKDm5lp32xk,91
|
29
|
+
ddeutil_workflow-0.0.60.dist-info/entry_points.txt,sha256=qDTpPSauL0ciO6T4iSVt8bJeYrVEkkoEEw_RlGx6Kgk,63
|
30
|
+
ddeutil_workflow-0.0.60.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
|
31
|
+
ddeutil_workflow-0.0.60.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|