ddeutil-workflow 0.0.81__py3-none-any.whl → 0.0.82__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 +2 -1
- ddeutil/workflow/__init__.py +19 -6
- ddeutil/workflow/__main__.py +280 -1
- ddeutil/workflow/api/routes/job.py +2 -2
- ddeutil/workflow/api/routes/logs.py +8 -61
- ddeutil/workflow/audits.py +46 -17
- ddeutil/workflow/conf.py +45 -25
- ddeutil/workflow/errors.py +9 -0
- ddeutil/workflow/job.py +70 -16
- ddeutil/workflow/result.py +33 -11
- ddeutil/workflow/stages.py +172 -134
- ddeutil/workflow/traces.py +64 -24
- ddeutil/workflow/utils.py +7 -4
- ddeutil/workflow/workflow.py +66 -75
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.82.dist-info}/METADATA +1 -1
- ddeutil_workflow-0.0.82.dist-info/RECORD +35 -0
- ddeutil/workflow/cli.py +0 -284
- ddeutil_workflow-0.0.81.dist-info/RECORD +0 -36
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.82.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.82.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.82.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.81.dist-info → ddeutil_workflow-0.0.82.dist-info}/top_level.txt +0 -0
ddeutil/workflow/conf.py
CHANGED
@@ -176,17 +176,6 @@ class YamlParser:
|
|
176
176
|
"""Base Load object that use to search config data by given some identity
|
177
177
|
value like name of `Workflow` or `Crontab` templates.
|
178
178
|
|
179
|
-
:param name: (str) A name of key of config data that read with YAML
|
180
|
-
Environment object.
|
181
|
-
:param path: (Path) A config path object.
|
182
|
-
:param externals: (DictData) An external config data that want to add to
|
183
|
-
loaded config data.
|
184
|
-
:param extras: (DictDdata) An extra parameters that use to override core
|
185
|
-
config values.
|
186
|
-
|
187
|
-
:raise ValueError: If the data does not find on the config path with the
|
188
|
-
name parameter.
|
189
|
-
|
190
179
|
Noted:
|
191
180
|
The config data should have `type` key for modeling validation that
|
192
181
|
make this loader know what is config should to do pass to.
|
@@ -209,6 +198,23 @@ class YamlParser:
|
|
209
198
|
extras: Optional[DictData] = None,
|
210
199
|
obj: Optional[Union[object, str]] = None,
|
211
200
|
) -> None:
|
201
|
+
"""Main constructure function.
|
202
|
+
|
203
|
+
Args:
|
204
|
+
name (str): A name of key of config data that read with YAML
|
205
|
+
Environment object.
|
206
|
+
path (Path): A config path object.
|
207
|
+
externals (DictData): An external config data that want to add to
|
208
|
+
loaded config data.
|
209
|
+
extras (DictDdata): An extra parameters that use to override core
|
210
|
+
config values.
|
211
|
+
obj (object | str): An object that want to validate from the `type`
|
212
|
+
key before keeping the config data.
|
213
|
+
|
214
|
+
Raises:
|
215
|
+
ValueError: If the data does not find on the config path with the
|
216
|
+
name parameter.
|
217
|
+
"""
|
212
218
|
self.path: Path = Path(dynamic("conf_path", f=path, extras=extras))
|
213
219
|
self.externals: DictData = externals or {}
|
214
220
|
self.extras: DictData = extras or {}
|
@@ -242,17 +248,19 @@ class YamlParser:
|
|
242
248
|
"""Find data with specific key and return the latest modify date data if
|
243
249
|
this key exists multiple files.
|
244
250
|
|
245
|
-
:
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
251
|
+
Args:
|
252
|
+
name (str): A name of data that want to find.
|
253
|
+
path (Path): A config path object.
|
254
|
+
paths (list[Path]): A list of config path object.
|
255
|
+
obj (object | str): An object that want to validate matching
|
256
|
+
before return.
|
257
|
+
extras (DictData): An extra parameter that use to override core
|
258
|
+
config values.
|
259
|
+
ignore_filename (str): An ignore filename. Default is
|
260
|
+
``.confignore`` filename.
|
254
261
|
|
255
|
-
:
|
262
|
+
Returns:
|
263
|
+
DictData: A config data that was found on the searching paths.
|
256
264
|
"""
|
257
265
|
path: Path = dynamic("conf_path", f=path, extras=extras)
|
258
266
|
if not paths:
|
@@ -317,7 +325,9 @@ class YamlParser:
|
|
317
325
|
``.confignore`` filename.
|
318
326
|
tags (list[str]): A list of tag that want to filter.
|
319
327
|
|
320
|
-
:
|
328
|
+
Returns:
|
329
|
+
Iterator[tuple[str, DictData]]: An iterator of config data that was
|
330
|
+
found on the searching paths.
|
321
331
|
"""
|
322
332
|
excluded: list[str] = excluded or []
|
323
333
|
tags: list[str] = tags or []
|
@@ -353,8 +363,11 @@ class YamlParser:
|
|
353
363
|
):
|
354
364
|
continue
|
355
365
|
|
356
|
-
if (
|
357
|
-
|
366
|
+
if (
|
367
|
+
# isinstance(data, dict) and
|
368
|
+
(t := data.get("type"))
|
369
|
+
and t == obj_type
|
370
|
+
):
|
358
371
|
# NOTE: Start adding file metadata.
|
359
372
|
file_stat: os.stat_result = file.lstat()
|
360
373
|
data["created_at"] = file_stat.st_ctime
|
@@ -397,6 +410,13 @@ class YamlParser:
|
|
397
410
|
def filter_yaml(cls, file: Path, name: Optional[str] = None) -> DictData:
|
398
411
|
"""Read a YAML file context from an input file path and specific name.
|
399
412
|
|
413
|
+
Notes:
|
414
|
+
The data that will return from reading context will map with config
|
415
|
+
name if an input searching name does not pass to this function.
|
416
|
+
|
417
|
+
input: {"name": "foo", "type": "Some"}
|
418
|
+
output: {"foo": {"name": "foo", "type": "Some"}}
|
419
|
+
|
400
420
|
Args:
|
401
421
|
file (Path): A file path that want to extract YAML context.
|
402
422
|
name (str): A key name that search on a YAML context.
|
@@ -413,7 +433,7 @@ class YamlParser:
|
|
413
433
|
return (
|
414
434
|
values[name] | {"name": name} if name in values else {}
|
415
435
|
)
|
416
|
-
return values
|
436
|
+
return {values["name"]: values} if "name" in values else values
|
417
437
|
return {}
|
418
438
|
|
419
439
|
@cached_property
|
ddeutil/workflow/errors.py
CHANGED
@@ -166,6 +166,15 @@ class StageCancelError(StageError): ...
|
|
166
166
|
class StageSkipError(StageError): ...
|
167
167
|
|
168
168
|
|
169
|
+
class StageNestedError(StageError): ...
|
170
|
+
|
171
|
+
|
172
|
+
class StageNestedCancelError(StageNestedError): ...
|
173
|
+
|
174
|
+
|
175
|
+
class StageNestedSkipError(StageNestedError): ...
|
176
|
+
|
177
|
+
|
169
178
|
class JobError(BaseError): ...
|
170
179
|
|
171
180
|
|
ddeutil/workflow/job.py
CHANGED
@@ -48,7 +48,7 @@ from enum import Enum
|
|
48
48
|
from functools import lru_cache
|
49
49
|
from textwrap import dedent
|
50
50
|
from threading import Event
|
51
|
-
from typing import Annotated, Any, Optional, Union
|
51
|
+
from typing import Annotated, Any, Literal, Optional, Union
|
52
52
|
|
53
53
|
from ddeutil.core import freeze_args
|
54
54
|
from pydantic import BaseModel, Discriminator, Field, SecretStr, Tag
|
@@ -72,7 +72,7 @@ from .result import (
|
|
72
72
|
)
|
73
73
|
from .reusables import has_template, param2template
|
74
74
|
from .stages import Stage
|
75
|
-
from .traces import
|
75
|
+
from .traces import Trace, get_trace
|
76
76
|
from .utils import cross_product, filter_func, gen_id
|
77
77
|
|
78
78
|
MatrixFilter = list[dict[str, Union[str, int]]]
|
@@ -187,10 +187,8 @@ class Strategy(BaseModel):
|
|
187
187
|
),
|
188
188
|
alias="fail-fast",
|
189
189
|
)
|
190
|
-
max_parallel: int = Field(
|
190
|
+
max_parallel: Union[int, str] = Field(
|
191
191
|
default=1,
|
192
|
-
gt=0,
|
193
|
-
lt=10,
|
194
192
|
description=(
|
195
193
|
"The maximum number of executor thread pool that want to run "
|
196
194
|
"parallel. This value should gather than 0 and less than 10."
|
@@ -427,9 +425,9 @@ class OnGCPBatch(BaseRunsOn): # pragma: no cov
|
|
427
425
|
args: GCPBatchArgs = Field(alias="with")
|
428
426
|
|
429
427
|
|
430
|
-
def get_discriminator_runs_on(
|
428
|
+
def get_discriminator_runs_on(data: dict[str, Any]) -> RunsOn:
|
431
429
|
"""Get discriminator of the RunsOn models."""
|
432
|
-
t: str =
|
430
|
+
t: str = data.get("type")
|
433
431
|
return RunsOn(t) if t else LOCAL
|
434
432
|
|
435
433
|
|
@@ -538,13 +536,28 @@ class Job(BaseModel):
|
|
538
536
|
description="An extra override config values.",
|
539
537
|
)
|
540
538
|
|
539
|
+
@field_validator(
|
540
|
+
"runs_on",
|
541
|
+
mode="before",
|
542
|
+
json_schema_input_type=Union[RunsOnModel, Literal["local"]],
|
543
|
+
)
|
544
|
+
def __prepare_runs_on(cls, data: Any) -> Any:
|
545
|
+
"""Prepare runs on value that was passed with string type."""
|
546
|
+
if isinstance(data, str):
|
547
|
+
if data != "local":
|
548
|
+
raise ValueError(
|
549
|
+
"runs-on that pass with str type should be `local` only"
|
550
|
+
)
|
551
|
+
return {"type": data}
|
552
|
+
return data
|
553
|
+
|
541
554
|
@field_validator("desc", mode="after")
|
542
|
-
def ___prepare_desc__(cls,
|
555
|
+
def ___prepare_desc__(cls, data: str) -> str:
|
543
556
|
"""Prepare description string that was created on a template.
|
544
557
|
|
545
558
|
:rtype: str
|
546
559
|
"""
|
547
|
-
return dedent(
|
560
|
+
return dedent(data.lstrip("\n"))
|
548
561
|
|
549
562
|
@field_validator("stages", mode="after")
|
550
563
|
def __validate_stage_id__(cls, value: list[Stage]) -> list[Stage]:
|
@@ -879,7 +892,7 @@ class Job(BaseModel):
|
|
879
892
|
ts: float = time.monotonic()
|
880
893
|
parent_run_id: str = run_id
|
881
894
|
run_id: str = gen_id((self.id or "EMPTY"), unique=True)
|
882
|
-
trace:
|
895
|
+
trace: Trace = get_trace(
|
883
896
|
run_id, parent_run_id=parent_run_id, extras=self.extras
|
884
897
|
)
|
885
898
|
trace.info(
|
@@ -1016,7 +1029,7 @@ def local_execute_strategy(
|
|
1016
1029
|
|
1017
1030
|
:rtype: tuple[Status, DictData]
|
1018
1031
|
"""
|
1019
|
-
trace:
|
1032
|
+
trace: Trace = get_trace(
|
1020
1033
|
run_id, parent_run_id=parent_run_id, extras=job.extras
|
1021
1034
|
)
|
1022
1035
|
if strategy:
|
@@ -1152,7 +1165,7 @@ def local_execute(
|
|
1152
1165
|
ts: float = time.monotonic()
|
1153
1166
|
parent_run_id: StrOrNone = run_id
|
1154
1167
|
run_id: str = gen_id((job.id or "EMPTY"), unique=True)
|
1155
|
-
trace:
|
1168
|
+
trace: Trace = get_trace(
|
1156
1169
|
run_id, parent_run_id=parent_run_id, extras=job.extras
|
1157
1170
|
)
|
1158
1171
|
context: DictData = {"status": WAIT}
|
@@ -1174,11 +1187,52 @@ def local_execute(
|
|
1174
1187
|
|
1175
1188
|
event: Event = event or Event()
|
1176
1189
|
ls: str = "Fail-Fast" if job.strategy.fail_fast else "All-Completed"
|
1177
|
-
workers: int = job.strategy.max_parallel
|
1190
|
+
workers: Union[int, str] = job.strategy.max_parallel
|
1191
|
+
if isinstance(workers, str):
|
1192
|
+
try:
|
1193
|
+
workers: int = int(
|
1194
|
+
param2template(workers, params=params, extras=job.extras)
|
1195
|
+
)
|
1196
|
+
except Exception as err:
|
1197
|
+
trace.exception(
|
1198
|
+
"[JOB]: Got the error on call param2template to "
|
1199
|
+
f"max-parallel value: {workers}"
|
1200
|
+
)
|
1201
|
+
return Result(
|
1202
|
+
run_id=run_id,
|
1203
|
+
parent_run_id=parent_run_id,
|
1204
|
+
status=FAILED,
|
1205
|
+
context=catch(
|
1206
|
+
context,
|
1207
|
+
status=FAILED,
|
1208
|
+
updated={"errors": to_dict(err)},
|
1209
|
+
),
|
1210
|
+
info={"execution_time": time.monotonic() - ts},
|
1211
|
+
extras=job.extras,
|
1212
|
+
)
|
1213
|
+
if workers >= 10:
|
1214
|
+
err_msg: str = (
|
1215
|
+
f"The max-parallel value should not more than 10, the current value "
|
1216
|
+
f"was set: {workers}."
|
1217
|
+
)
|
1218
|
+
trace.error(f"[JOB]: {err_msg}")
|
1219
|
+
return Result(
|
1220
|
+
run_id=run_id,
|
1221
|
+
parent_run_id=parent_run_id,
|
1222
|
+
status=FAILED,
|
1223
|
+
context=catch(
|
1224
|
+
context,
|
1225
|
+
status=FAILED,
|
1226
|
+
updated={"errors": JobError(err_msg).to_dict()},
|
1227
|
+
),
|
1228
|
+
info={"execution_time": time.monotonic() - ts},
|
1229
|
+
extras=job.extras,
|
1230
|
+
)
|
1231
|
+
|
1178
1232
|
strategies: list[DictStr] = job.strategy.make()
|
1179
1233
|
len_strategy: int = len(strategies)
|
1180
1234
|
trace.info(
|
1181
|
-
f"[JOB]:
|
1235
|
+
f"[JOB]: Mode {ls}: {job.id!r} with {workers} "
|
1182
1236
|
f"worker{'s' if workers > 1 else ''}."
|
1183
1237
|
)
|
1184
1238
|
|
@@ -1295,7 +1349,7 @@ def self_hosted_execute(
|
|
1295
1349
|
"""
|
1296
1350
|
parent_run_id: StrOrNone = run_id
|
1297
1351
|
run_id: str = gen_id((job.id or "EMPTY"), unique=True)
|
1298
|
-
trace:
|
1352
|
+
trace: Trace = get_trace(
|
1299
1353
|
run_id, parent_run_id=parent_run_id, extras=job.extras
|
1300
1354
|
)
|
1301
1355
|
context: DictData = {"status": WAIT}
|
@@ -1378,7 +1432,7 @@ def docker_execution(
|
|
1378
1432
|
"""
|
1379
1433
|
parent_run_id: StrOrNone = run_id
|
1380
1434
|
run_id: str = gen_id((job.id or "EMPTY"), unique=True)
|
1381
|
-
trace:
|
1435
|
+
trace: Trace = get_trace(
|
1382
1436
|
run_id, parent_run_id=parent_run_id, extras=job.extras
|
1383
1437
|
)
|
1384
1438
|
context: DictData = {"status": WAIT}
|
ddeutil/workflow/result.py
CHANGED
@@ -21,12 +21,12 @@ from __future__ import annotations
|
|
21
21
|
|
22
22
|
from dataclasses import field
|
23
23
|
from enum import Enum
|
24
|
-
from typing import Optional, Union
|
24
|
+
from typing import Optional, TypedDict, Union
|
25
25
|
|
26
26
|
from pydantic import ConfigDict
|
27
27
|
from pydantic.dataclasses import dataclass
|
28
28
|
from pydantic.functional_validators import model_validator
|
29
|
-
from typing_extensions import Self
|
29
|
+
from typing_extensions import NotRequired, Self
|
30
30
|
|
31
31
|
from . import (
|
32
32
|
JobCancelError,
|
@@ -34,13 +34,16 @@ from . import (
|
|
34
34
|
JobSkipError,
|
35
35
|
StageCancelError,
|
36
36
|
StageError,
|
37
|
+
StageNestedCancelError,
|
38
|
+
StageNestedError,
|
39
|
+
StageNestedSkipError,
|
37
40
|
StageSkipError,
|
38
41
|
WorkflowCancelError,
|
39
42
|
WorkflowError,
|
40
43
|
)
|
41
44
|
from .__types import DictData
|
42
|
-
from .audits import
|
43
|
-
from .errors import ResultError
|
45
|
+
from .audits import Trace, get_trace
|
46
|
+
from .errors import ErrorData, ResultError
|
44
47
|
from .utils import default_gen_id
|
45
48
|
|
46
49
|
|
@@ -140,6 +143,9 @@ def get_status_from_error(
|
|
140
143
|
StageError,
|
141
144
|
StageCancelError,
|
142
145
|
StageSkipError,
|
146
|
+
StageNestedCancelError,
|
147
|
+
StageNestedError,
|
148
|
+
StageNestedSkipError,
|
143
149
|
JobError,
|
144
150
|
JobCancelError,
|
145
151
|
JobSkipError,
|
@@ -157,10 +163,16 @@ def get_status_from_error(
|
|
157
163
|
Returns:
|
158
164
|
Status: The status from the specific exception class.
|
159
165
|
"""
|
160
|
-
if isinstance(error, (StageSkipError, JobSkipError)):
|
166
|
+
if isinstance(error, (StageNestedSkipError, StageSkipError, JobSkipError)):
|
161
167
|
return SKIP
|
162
168
|
elif isinstance(
|
163
|
-
error,
|
169
|
+
error,
|
170
|
+
(
|
171
|
+
StageNestedCancelError,
|
172
|
+
StageCancelError,
|
173
|
+
JobCancelError,
|
174
|
+
WorkflowCancelError,
|
175
|
+
),
|
164
176
|
):
|
165
177
|
return CANCEL
|
166
178
|
return FAILED
|
@@ -188,9 +200,7 @@ class Result:
|
|
188
200
|
info: DictData = field(default_factory=dict)
|
189
201
|
run_id: str = field(default_factory=default_gen_id)
|
190
202
|
parent_run_id: Optional[str] = field(default=None)
|
191
|
-
trace: Optional[
|
192
|
-
default=None, compare=False, repr=False
|
193
|
-
)
|
203
|
+
trace: Optional[Trace] = field(default=None, compare=False, repr=False)
|
194
204
|
|
195
205
|
@model_validator(mode="after")
|
196
206
|
def __prepare_trace(self) -> Self:
|
@@ -199,7 +209,7 @@ class Result:
|
|
199
209
|
:rtype: Self
|
200
210
|
"""
|
201
211
|
if self.trace is None: # pragma: no cov
|
202
|
-
self.trace:
|
212
|
+
self.trace: Trace = get_trace(
|
203
213
|
self.run_id,
|
204
214
|
parent_run_id=self.parent_run_id,
|
205
215
|
extras=self.extras,
|
@@ -208,7 +218,7 @@ class Result:
|
|
208
218
|
return self
|
209
219
|
|
210
220
|
@classmethod
|
211
|
-
def from_trace(cls, trace:
|
221
|
+
def from_trace(cls, trace: Trace):
|
212
222
|
"""Construct the result model from trace for clean code objective."""
|
213
223
|
return cls(
|
214
224
|
run_id=trace.run_id,
|
@@ -274,6 +284,9 @@ def catch(
|
|
274
284
|
context: A context data that want to be the current context.
|
275
285
|
status: A status enum object.
|
276
286
|
updated: A updated data that will update to the current context.
|
287
|
+
|
288
|
+
Returns:
|
289
|
+
DictData: A catch context data.
|
277
290
|
"""
|
278
291
|
context.update(updated or {})
|
279
292
|
context["status"] = Status(status) if isinstance(status, int) else status
|
@@ -291,3 +304,12 @@ def catch(
|
|
291
304
|
else:
|
292
305
|
raise ResultError(f"The key {k!r} does not exists on context data.")
|
293
306
|
return context
|
307
|
+
|
308
|
+
|
309
|
+
class Context(TypedDict):
|
310
|
+
"""Context dict typed."""
|
311
|
+
|
312
|
+
status: Status
|
313
|
+
context: NotRequired[DictData]
|
314
|
+
errors: NotRequired[Union[list[ErrorData], ErrorData]]
|
315
|
+
info: NotRequired[DictData]
|