ddeutil-workflow 0.0.80__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 +64 -44
- ddeutil/workflow/errors.py +12 -2
- ddeutil/workflow/job.py +70 -16
- ddeutil/workflow/result.py +33 -11
- ddeutil/workflow/reusables.py +16 -17
- ddeutil/workflow/stages.py +172 -134
- ddeutil/workflow/traces.py +64 -24
- ddeutil/workflow/utils.py +7 -15
- ddeutil/workflow/workflow.py +73 -84
- {ddeutil_workflow-0.0.80.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 -274
- ddeutil_workflow-0.0.80.dist-info/RECORD +0 -36
- {ddeutil_workflow-0.0.80.dist-info → ddeutil_workflow-0.0.82.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.80.dist-info → ddeutil_workflow-0.0.82.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.80.dist-info → ddeutil_workflow-0.0.82.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.80.dist-info → ddeutil_workflow-0.0.82.dist-info}/top_level.txt +0 -0
ddeutil/workflow/conf.py
CHANGED
@@ -22,19 +22,6 @@ Functions:
|
|
22
22
|
pass_env: Process environment variable substitution
|
23
23
|
api_config: Get API-specific configuration settings
|
24
24
|
|
25
|
-
Example:
|
26
|
-
```python
|
27
|
-
from ddeutil.workflow.conf import Config, YamlParser
|
28
|
-
|
29
|
-
# Load workflow configuration
|
30
|
-
parser = YamlParser("my-workflow")
|
31
|
-
workflow_config = parser.data
|
32
|
-
|
33
|
-
# Access dynamic configuration
|
34
|
-
from ddeutil.workflow.conf import dynamic
|
35
|
-
log_level = dynamic("log_level", default="INFO")
|
36
|
-
```
|
37
|
-
|
38
25
|
Note:
|
39
26
|
Configuration files support environment variable substitution using
|
40
27
|
${VAR_NAME} syntax and provide extensive validation capabilities.
|
@@ -155,7 +142,7 @@ class Config: # pragma: no cov
|
|
155
142
|
)
|
156
143
|
|
157
144
|
@property
|
158
|
-
def audit_conf(self) -> str:
|
145
|
+
def audit_conf(self) -> dict[str, Any]:
|
159
146
|
return json.loads(
|
160
147
|
env("LOG_AUDIT_URL", '{"type": "file", "path": "./audits"}')
|
161
148
|
)
|
@@ -189,17 +176,6 @@ class YamlParser:
|
|
189
176
|
"""Base Load object that use to search config data by given some identity
|
190
177
|
value like name of `Workflow` or `Crontab` templates.
|
191
178
|
|
192
|
-
:param name: (str) A name of key of config data that read with YAML
|
193
|
-
Environment object.
|
194
|
-
:param path: (Path) A config path object.
|
195
|
-
:param externals: (DictData) An external config data that want to add to
|
196
|
-
loaded config data.
|
197
|
-
:param extras: (DictDdata) An extra parameters that use to override core
|
198
|
-
config values.
|
199
|
-
|
200
|
-
:raise ValueError: If the data does not find on the config path with the
|
201
|
-
name parameter.
|
202
|
-
|
203
179
|
Noted:
|
204
180
|
The config data should have `type` key for modeling validation that
|
205
181
|
make this loader know what is config should to do pass to.
|
@@ -222,6 +198,23 @@ class YamlParser:
|
|
222
198
|
extras: Optional[DictData] = None,
|
223
199
|
obj: Optional[Union[object, str]] = None,
|
224
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
|
+
"""
|
225
218
|
self.path: Path = Path(dynamic("conf_path", f=path, extras=extras))
|
226
219
|
self.externals: DictData = externals or {}
|
227
220
|
self.extras: DictData = extras or {}
|
@@ -255,17 +248,19 @@ class YamlParser:
|
|
255
248
|
"""Find data with specific key and return the latest modify date data if
|
256
249
|
this key exists multiple files.
|
257
250
|
|
258
|
-
:
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
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.
|
267
261
|
|
268
|
-
:
|
262
|
+
Returns:
|
263
|
+
DictData: A config data that was found on the searching paths.
|
269
264
|
"""
|
270
265
|
path: Path = dynamic("conf_path", f=path, extras=extras)
|
271
266
|
if not paths:
|
@@ -288,9 +283,12 @@ class YamlParser:
|
|
288
283
|
continue
|
289
284
|
|
290
285
|
if data := cls.filter_yaml(file, name=name):
|
286
|
+
|
287
|
+
# NOTE: Start adding file metadata.
|
291
288
|
file_stat: os.stat_result = file.lstat()
|
292
289
|
data["created_at"] = file_stat.st_ctime
|
293
290
|
data["updated_at"] = file_stat.st_mtime
|
291
|
+
|
294
292
|
if not obj_type:
|
295
293
|
all_data.append((file_stat.st_mtime, data))
|
296
294
|
elif (t := data.get("type")) and t == obj_type:
|
@@ -324,11 +322,12 @@ class YamlParser:
|
|
324
322
|
extras: (DictData) An extra parameter that use to override core
|
325
323
|
config values.
|
326
324
|
ignore_filename: (str) An ignore filename. Default is
|
327
|
-
|
328
|
-
tags
|
329
|
-
A list of tag that want to filter.
|
325
|
+
``.confignore`` filename.
|
326
|
+
tags (list[str]): A list of tag that want to filter.
|
330
327
|
|
331
|
-
:
|
328
|
+
Returns:
|
329
|
+
Iterator[tuple[str, DictData]]: An iterator of config data that was
|
330
|
+
found on the searching paths.
|
332
331
|
"""
|
333
332
|
excluded: list[str] = excluded or []
|
334
333
|
tags: list[str] = tags or []
|
@@ -364,7 +363,12 @@ class YamlParser:
|
|
364
363
|
):
|
365
364
|
continue
|
366
365
|
|
367
|
-
if (
|
366
|
+
if (
|
367
|
+
# isinstance(data, dict) and
|
368
|
+
(t := data.get("type"))
|
369
|
+
and t == obj_type
|
370
|
+
):
|
371
|
+
# NOTE: Start adding file metadata.
|
368
372
|
file_stat: os.stat_result = file.lstat()
|
369
373
|
data["created_at"] = file_stat.st_ctime
|
370
374
|
data["updated_at"] = file_stat.st_mtime
|
@@ -372,6 +376,7 @@ class YamlParser:
|
|
372
376
|
file.lstat().st_mtime,
|
373
377
|
data,
|
374
378
|
)
|
379
|
+
|
375
380
|
if key in all_data:
|
376
381
|
all_data[key].append(marking)
|
377
382
|
else:
|
@@ -405,15 +410,30 @@ class YamlParser:
|
|
405
410
|
def filter_yaml(cls, file: Path, name: Optional[str] = None) -> DictData:
|
406
411
|
"""Read a YAML file context from an input file path and specific name.
|
407
412
|
|
408
|
-
:
|
409
|
-
|
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"}}
|
410
419
|
|
411
|
-
:
|
420
|
+
Args:
|
421
|
+
file (Path): A file path that want to extract YAML context.
|
422
|
+
name (str): A key name that search on a YAML context.
|
423
|
+
|
424
|
+
Returns:
|
425
|
+
DictData: A data that read from this file if it is YAML format.
|
412
426
|
"""
|
413
427
|
if any(file.suffix.endswith(s) for s in (".yml", ".yaml")):
|
414
428
|
values: DictData = YamlFlResolve(file).read()
|
415
429
|
if values is not None:
|
416
|
-
|
430
|
+
if name:
|
431
|
+
if "name" in values and values.get("name") == name:
|
432
|
+
return values
|
433
|
+
return (
|
434
|
+
values[name] | {"name": name} if name in values else {}
|
435
|
+
)
|
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
@@ -135,11 +135,12 @@ class BaseError(Exception):
|
|
135
135
|
|
136
136
|
Example:
|
137
137
|
>>> error = BaseError("Something failed", refs="stage-1")
|
138
|
-
|
138
|
+
|
139
|
+
Simple format
|
139
140
|
>>> error.to_dict()
|
140
141
|
>>> # Returns: {"name": "BaseError", "message": "Something failed"}
|
141
142
|
|
142
|
-
|
143
|
+
With reference mapping
|
143
144
|
>>> error.to_dict(with_refs=True)
|
144
145
|
>>> # Returns: {"stage-1": {"name": "BaseError", "message": "Something failed"}}
|
145
146
|
```
|
@@ -165,6 +166,15 @@ class StageCancelError(StageError): ...
|
|
165
166
|
class StageSkipError(StageError): ...
|
166
167
|
|
167
168
|
|
169
|
+
class StageNestedError(StageError): ...
|
170
|
+
|
171
|
+
|
172
|
+
class StageNestedCancelError(StageNestedError): ...
|
173
|
+
|
174
|
+
|
175
|
+
class StageNestedSkipError(StageNestedError): ...
|
176
|
+
|
177
|
+
|
168
178
|
class JobError(BaseError): ...
|
169
179
|
|
170
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]
|
ddeutil/workflow/reusables.py
CHANGED
@@ -25,22 +25,20 @@ Functions:
|
|
25
25
|
create_model_from_caller: Generate Pydantic models from function signatures
|
26
26
|
|
27
27
|
Example:
|
28
|
-
|
29
|
-
from ddeutil.workflow.reusables import tag
|
30
|
-
|
31
|
-
@tag("data-processing", alias="process-csv")
|
32
|
-
def process_csv_file(input_path: str, output_path: str) -> dict:
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
#
|
37
|
-
#
|
38
|
-
#
|
39
|
-
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
# output_path: "/data/output.csv"
|
43
|
-
```
|
28
|
+
|
29
|
+
>>> from ddeutil.workflow.reusables import tag
|
30
|
+
>>>
|
31
|
+
>>> @tag("data-processing", alias="process-csv")
|
32
|
+
>>> def process_csv_file(input_path: str, output_path: str) -> dict:
|
33
|
+
>>> return {"status": "completed", "rows_processed": 1000}
|
34
|
+
|
35
|
+
>>> # Use in workflow YAML:
|
36
|
+
>>> # stages:
|
37
|
+
>>> # - name: "Process data"
|
38
|
+
>>> # uses: "data-processing/process-csv@latest"
|
39
|
+
>>> # args:
|
40
|
+
>>> # input_path: "/data/input.csv"
|
41
|
+
>>> # output_path: "/data/output.csv"
|
44
42
|
|
45
43
|
Note:
|
46
44
|
The registry system supports versioning and aliasing for better function
|
@@ -64,6 +62,7 @@ from typing import (
|
|
64
62
|
Protocol,
|
65
63
|
TypeVar,
|
66
64
|
Union,
|
65
|
+
cast,
|
67
66
|
get_type_hints,
|
68
67
|
)
|
69
68
|
|
@@ -201,7 +200,7 @@ def get_args_const(
|
|
201
200
|
f"Post-filter: {expr} does not valid because it raise syntax error."
|
202
201
|
) from None
|
203
202
|
|
204
|
-
body: list[Expr] = mod.body
|
203
|
+
body: list[Expr] = cast(list[Expr], mod.body)
|
205
204
|
if len(body) > 1:
|
206
205
|
raise UtilError(
|
207
206
|
"Post-filter function should be only one calling per workflow."
|