ddeutil-workflow 0.0.77__py3-none-any.whl → 0.0.79__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 +1 -5
- ddeutil/workflow/api/routes/job.py +2 -2
- ddeutil/workflow/audits.py +554 -112
- ddeutil/workflow/cli.py +25 -3
- ddeutil/workflow/conf.py +16 -28
- ddeutil/workflow/errors.py +13 -15
- ddeutil/workflow/event.py +37 -41
- ddeutil/workflow/job.py +161 -92
- ddeutil/workflow/params.py +172 -58
- ddeutil/workflow/plugins/__init__.py +0 -0
- ddeutil/workflow/plugins/providers/__init__.py +0 -0
- ddeutil/workflow/plugins/providers/aws.py +908 -0
- ddeutil/workflow/plugins/providers/az.py +1003 -0
- ddeutil/workflow/plugins/providers/container.py +703 -0
- ddeutil/workflow/plugins/providers/gcs.py +826 -0
- ddeutil/workflow/result.py +35 -37
- ddeutil/workflow/reusables.py +153 -96
- ddeutil/workflow/stages.py +84 -60
- ddeutil/workflow/traces.py +1660 -521
- ddeutil/workflow/utils.py +111 -69
- ddeutil/workflow/workflow.py +74 -47
- {ddeutil_workflow-0.0.77.dist-info → ddeutil_workflow-0.0.79.dist-info}/METADATA +52 -20
- ddeutil_workflow-0.0.79.dist-info/RECORD +36 -0
- ddeutil_workflow-0.0.77.dist-info/RECORD +0 -30
- {ddeutil_workflow-0.0.77.dist-info → ddeutil_workflow-0.0.79.dist-info}/WHEEL +0 -0
- {ddeutil_workflow-0.0.77.dist-info → ddeutil_workflow-0.0.79.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.77.dist-info → ddeutil_workflow-0.0.79.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.77.dist-info → ddeutil_workflow-0.0.79.dist-info}/top_level.txt +0 -0
ddeutil/workflow/cli.py
CHANGED
@@ -57,7 +57,7 @@ def init() -> None:
|
|
57
57
|
wf-example:
|
58
58
|
type: Workflow
|
59
59
|
desc: |
|
60
|
-
An example workflow template.
|
60
|
+
An example workflow template that provide the demo of workflow.
|
61
61
|
params:
|
62
62
|
name:
|
63
63
|
type: str
|
@@ -65,10 +65,18 @@ def init() -> None:
|
|
65
65
|
jobs:
|
66
66
|
first-job:
|
67
67
|
stages:
|
68
|
+
|
69
|
+
- name: "Hello Stage"
|
70
|
+
echo: "Start say hi to the console"
|
71
|
+
|
68
72
|
- name: "Call tasks"
|
69
73
|
uses: tasks/say-hello-func@example
|
70
74
|
with:
|
71
75
|
name: ${{ params.name }}
|
76
|
+
second-job:
|
77
|
+
|
78
|
+
- name: "Hello Env"
|
79
|
+
echo: "Start say hi with ${ WORKFLOW_DEMO_HELLO }"
|
72
80
|
"""
|
73
81
|
).lstrip("\n")
|
74
82
|
)
|
@@ -94,8 +102,22 @@ def init() -> None:
|
|
94
102
|
|
95
103
|
init_path = task_path / "__init__.py"
|
96
104
|
init_path.write_text("from .example import hello_world_task\n")
|
105
|
+
|
106
|
+
dotenv_file = Path(".env")
|
107
|
+
mode: str = "a" if dotenv_file.exists() else "w"
|
108
|
+
with dotenv_file.open(mode=mode) as f:
|
109
|
+
f.write("\n# Workflow env vars\n")
|
110
|
+
f.write(
|
111
|
+
"WORKFLOW_DEMO_HELLO=foo\n"
|
112
|
+
"WORKFLOW_CORE_DEBUG_MODE=true\n"
|
113
|
+
"WORKFLOW_LOG_TIMEZONE=Asia/Bangkok\n"
|
114
|
+
"WORKFLOW_LOG_TRACE_ENABLE_WRITE=false\n"
|
115
|
+
"WORKFLOW_LOG_AUDIT_ENABLE_WRITE=true\n"
|
116
|
+
)
|
117
|
+
|
118
|
+
typer.echo("Starter command:")
|
97
119
|
typer.echo(
|
98
|
-
"
|
120
|
+
"> `source .env && workflow-cli workflows execute --name=wf-example`"
|
99
121
|
)
|
100
122
|
|
101
123
|
|
@@ -232,7 +254,7 @@ def workflow_json_schema(
|
|
232
254
|
json_schema = TypeAdapter(template).json_schema(by_alias=True)
|
233
255
|
template_schema: dict[str, str] = {
|
234
256
|
"$schema": "http://json-schema.org/draft-07/schema#",
|
235
|
-
"title": "Workflow Configuration Schema",
|
257
|
+
"title": "Workflow Configuration JSON Schema",
|
236
258
|
"version": __version__,
|
237
259
|
}
|
238
260
|
with open(output, mode="w", encoding="utf-8") as f:
|
ddeutil/workflow/conf.py
CHANGED
@@ -40,12 +40,12 @@ Note:
|
|
40
40
|
${VAR_NAME} syntax and provide extensive validation capabilities.
|
41
41
|
"""
|
42
42
|
import copy
|
43
|
+
import json
|
43
44
|
import os
|
44
45
|
from collections.abc import Iterator
|
45
46
|
from functools import cached_property
|
46
47
|
from pathlib import Path
|
47
|
-
from typing import Final, Optional, TypeVar, Union
|
48
|
-
from urllib.parse import ParseResult, urlparse
|
48
|
+
from typing import Any, Final, Optional, TypeVar, Union
|
49
49
|
from zoneinfo import ZoneInfo
|
50
50
|
|
51
51
|
from ddeutil.core import str2bool
|
@@ -122,8 +122,8 @@ class Config: # pragma: no cov
|
|
122
122
|
return [r.strip() for r in regis_filter_str.split(",")]
|
123
123
|
|
124
124
|
@property
|
125
|
-
def
|
126
|
-
return
|
125
|
+
def trace_handlers(self) -> list[dict[str, Any]]:
|
126
|
+
return json.loads(env("LOG_TRACE_HANDLERS", '[{"type": "console"}]'))
|
127
127
|
|
128
128
|
@property
|
129
129
|
def debug(self) -> bool:
|
@@ -155,22 +155,8 @@ class Config: # pragma: no cov
|
|
155
155
|
)
|
156
156
|
|
157
157
|
@property
|
158
|
-
def
|
159
|
-
return env(
|
160
|
-
"LOG_FORMAT_FILE",
|
161
|
-
(
|
162
|
-
"{datetime} ({process:5d}, {thread:5d}) ({cut_id}) "
|
163
|
-
"{message:120s} ({filename}:{lineno})"
|
164
|
-
),
|
165
|
-
)
|
166
|
-
|
167
|
-
@property
|
168
|
-
def enable_write_log(self) -> bool:
|
169
|
-
return str2bool(env("LOG_TRACE_ENABLE_WRITE", "false"))
|
170
|
-
|
171
|
-
@property
|
172
|
-
def audit_url(self) -> ParseResult:
|
173
|
-
return urlparse(env("LOG_AUDIT_URL", "file:./audits"))
|
158
|
+
def audit_url(self) -> str:
|
159
|
+
return env("LOG_AUDIT_URL", "file:./audits")
|
174
160
|
|
175
161
|
@property
|
176
162
|
def enable_write_audit(self) -> bool:
|
@@ -307,8 +293,6 @@ class YamlParser:
|
|
307
293
|
all_data.append((file_stat.st_mtime, data))
|
308
294
|
elif (t := data.get("type")) and t == obj_type:
|
309
295
|
all_data.append((file_stat.st_mtime, data))
|
310
|
-
else:
|
311
|
-
continue
|
312
296
|
|
313
297
|
return {} if not all_data else max(all_data, key=lambda x: x[0])[1]
|
314
298
|
|
@@ -322,7 +306,7 @@ class YamlParser:
|
|
322
306
|
excluded: Optional[list[str]] = None,
|
323
307
|
extras: Optional[DictData] = None,
|
324
308
|
ignore_filename: Optional[str] = None,
|
325
|
-
tags: Optional[list[str]] = None,
|
309
|
+
tags: Optional[list[Union[str, int]]] = None,
|
326
310
|
) -> Iterator[tuple[str, DictData]]:
|
327
311
|
"""Find all data that match with object type in config path. This class
|
328
312
|
method can use include and exclude list of identity name for filter and
|
@@ -373,13 +357,15 @@ class YamlParser:
|
|
373
357
|
|
374
358
|
if (
|
375
359
|
tags
|
376
|
-
and (ts := data
|
377
|
-
and
|
378
|
-
|
379
|
-
): # pragma: no cov
|
360
|
+
and isinstance((ts := data.get("tags", [])), list)
|
361
|
+
and any(t not in ts for t in tags)
|
362
|
+
):
|
380
363
|
continue
|
381
364
|
|
382
365
|
if (t := data.get("type")) and t == obj_type:
|
366
|
+
file_stat: os.stat_result = file.lstat()
|
367
|
+
data["created_at"] = file_stat.st_ctime
|
368
|
+
data["updated_at"] = file_stat.st_mtime
|
383
369
|
marking: tuple[float, DictData] = (
|
384
370
|
file.lstat().st_mtime,
|
385
371
|
data,
|
@@ -464,7 +450,9 @@ def dynamic(
|
|
464
450
|
conf: Optional[T] = getattr(config, key, None) if f is None else f
|
465
451
|
if extra is None:
|
466
452
|
return conf
|
467
|
-
|
453
|
+
# NOTE: Fix type checking for boolean value and int type like
|
454
|
+
# `isinstance(False, int)` which return True.
|
455
|
+
if type(extra) is not type(conf):
|
468
456
|
raise TypeError(
|
469
457
|
f"Type of config {key!r} from extras: {extra!r} does not valid "
|
470
458
|
f"as config {type(conf)}."
|
ddeutil/workflow/errors.py
CHANGED
@@ -56,13 +56,13 @@ def to_dict(exception: Exception, **kwargs) -> ErrorData: # pragma: no cov
|
|
56
56
|
ErrorData: Dictionary containing exception name and message
|
57
57
|
|
58
58
|
Example:
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
59
|
+
>>> try:
|
60
|
+
>>> raise ValueError("Something went wrong")
|
61
|
+
>>> except Exception as e:
|
62
|
+
>>> error_data = to_dict(e, context="workflow_execution")
|
63
|
+
>>> # Returns: {
|
64
|
+
>>> # "name": "ValueError", "message": "Something went wrong", "context": "workflow_execution"
|
65
|
+
>>> # }
|
66
66
|
"""
|
67
67
|
return {
|
68
68
|
"name": exception.__class__.__name__,
|
@@ -85,14 +85,12 @@ class BaseError(Exception):
|
|
85
85
|
params: Parameter data that was being processed when error occurred
|
86
86
|
|
87
87
|
Example:
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
print(f"Error in {e.refs}: {error_dict}")
|
95
|
-
```
|
88
|
+
>>> try:
|
89
|
+
>>> # NOTE: Some workflow operation
|
90
|
+
>>> pass
|
91
|
+
>>> except BaseError as e:
|
92
|
+
>>> error_dict = e.to_dict(with_refs=True)
|
93
|
+
>>> print(f\"Error in {e.refs}: {error_dict}\")
|
96
94
|
"""
|
97
95
|
|
98
96
|
def __init__(
|
ddeutil/workflow/event.py
CHANGED
@@ -19,6 +19,9 @@ Classes:
|
|
19
19
|
Crontab: Main cron-based event scheduler.
|
20
20
|
CrontabYear: Enhanced cron scheduler with year constraints.
|
21
21
|
ReleaseEvent: Release-based event triggers.
|
22
|
+
FileEvent: File system monitoring triggers.
|
23
|
+
WebhookEvent: API/webhook-based triggers.
|
24
|
+
DatabaseEvent: Database change monitoring triggers.
|
22
25
|
SensorEvent: Sensor-based event monitoring.
|
23
26
|
|
24
27
|
Example:
|
@@ -39,7 +42,6 @@ from __future__ import annotations
|
|
39
42
|
from dataclasses import fields
|
40
43
|
from datetime import datetime
|
41
44
|
from typing import Annotated, Any, Literal, Optional, Union
|
42
|
-
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
43
45
|
|
44
46
|
from pydantic import BaseModel, ConfigDict, Field, ValidationInfo
|
45
47
|
from pydantic.functional_serializers import field_serializer
|
@@ -107,7 +109,7 @@ class BaseCrontab(BaseModel):
|
|
107
109
|
)
|
108
110
|
tz: TimeZoneName = Field(
|
109
111
|
default="UTC",
|
110
|
-
description="A timezone string value.",
|
112
|
+
description="A timezone string value that will pass to ZoneInfo.",
|
111
113
|
alias="timezone",
|
112
114
|
)
|
113
115
|
|
@@ -125,38 +127,25 @@ class BaseCrontab(BaseModel):
|
|
125
127
|
data["timezone"] = tz
|
126
128
|
return data
|
127
129
|
|
128
|
-
@field_validator("tz")
|
129
|
-
def __validate_tz(cls, value: str) -> str:
|
130
|
-
"""Validate timezone value.
|
131
|
-
|
132
|
-
Args:
|
133
|
-
value: Timezone string to validate.
|
134
|
-
|
135
|
-
Returns:
|
136
|
-
Validated timezone string.
|
137
|
-
|
138
|
-
Raises:
|
139
|
-
ValueError: If timezone is invalid.
|
140
|
-
"""
|
141
|
-
try:
|
142
|
-
_ = ZoneInfo(value)
|
143
|
-
return value
|
144
|
-
except ZoneInfoNotFoundError as e:
|
145
|
-
raise ValueError(f"Invalid timezone: {value}") from e
|
146
|
-
|
147
130
|
|
148
131
|
class CrontabValue(BaseCrontab):
|
149
132
|
"""Crontab model using interval-based specification.
|
150
133
|
|
151
134
|
Attributes:
|
152
|
-
interval:
|
153
|
-
|
135
|
+
interval: (Interval)
|
136
|
+
A scheduling interval string ('daily', 'weekly', 'monthly').
|
137
|
+
day: (str, default None)
|
138
|
+
Day specification for weekly/monthly schedules.
|
154
139
|
time: Time of day in 'HH:MM' format.
|
155
140
|
"""
|
156
141
|
|
157
|
-
interval: Interval
|
142
|
+
interval: Interval = Field(description="A scheduling interval string.")
|
158
143
|
day: Optional[str] = Field(default=None)
|
159
|
-
time: str = Field(
|
144
|
+
time: str = Field(
|
145
|
+
default="00:00",
|
146
|
+
pattern=r"\d{2}:\d{2}",
|
147
|
+
description="A time of day that pass with format 'HH:MM'.",
|
148
|
+
)
|
160
149
|
|
161
150
|
@property
|
162
151
|
def cronjob(self) -> CronJob:
|
@@ -182,10 +171,13 @@ class CrontabValue(BaseCrontab):
|
|
182
171
|
TypeError: If start parameter is neither string nor datetime.
|
183
172
|
"""
|
184
173
|
if isinstance(start, str):
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
174
|
+
return self.cronjob.schedule(
|
175
|
+
date=datetime.fromisoformat(start), tz=self.tz
|
176
|
+
)
|
177
|
+
|
178
|
+
if isinstance(start, datetime):
|
179
|
+
return self.cronjob.schedule(date=start, tz=self.tz)
|
180
|
+
raise TypeError("start value should be str or datetime type.")
|
189
181
|
|
190
182
|
def next(self, start: Union[str, datetime]) -> CronRunner:
|
191
183
|
"""Get next scheduled datetime after given start time.
|
@@ -222,11 +214,6 @@ class Crontab(BaseCrontab):
|
|
222
214
|
"A Cronjob object that use for validate and generate datetime."
|
223
215
|
),
|
224
216
|
)
|
225
|
-
tz: TimeZoneName = Field(
|
226
|
-
default="UTC",
|
227
|
-
description="A timezone string value.",
|
228
|
-
alias="timezone",
|
229
|
-
)
|
230
217
|
|
231
218
|
@model_validator(mode="before")
|
232
219
|
def __prepare_values(cls, data: Any) -> Any:
|
@@ -328,11 +315,9 @@ class CrontabYear(Crontab):
|
|
328
315
|
cronjob: CronJobYear instance for year-aware schedule validation and generation.
|
329
316
|
"""
|
330
317
|
|
331
|
-
cronjob: CronJobYear = (
|
332
|
-
|
333
|
-
|
334
|
-
"A Cronjob object that use for validate and generate datetime."
|
335
|
-
),
|
318
|
+
cronjob: CronJobYear = Field(
|
319
|
+
description=(
|
320
|
+
"A Cronjob object that use for validate and generate datetime."
|
336
321
|
),
|
337
322
|
)
|
338
323
|
|
@@ -376,13 +361,24 @@ Cron = Annotated[
|
|
376
361
|
],
|
377
362
|
Field(
|
378
363
|
union_mode="smart",
|
379
|
-
description=
|
364
|
+
description=(
|
365
|
+
"Event model type supporting year-based, standard, and "
|
366
|
+
"interval-based cron scheduling."
|
367
|
+
),
|
380
368
|
),
|
381
369
|
] # pragma: no cov
|
382
370
|
|
383
371
|
|
384
372
|
class Event(BaseModel):
|
385
|
-
"""Event model.
|
373
|
+
"""Event model with comprehensive trigger support.
|
374
|
+
|
375
|
+
Supports multiple types of event triggers including cron scheduling,
|
376
|
+
file monitoring, webhooks, database changes, sensor-based triggers,
|
377
|
+
polling-based triggers, message queue events, stream processing events,
|
378
|
+
batch processing events, data quality events, API rate limiting events,
|
379
|
+
data lineage events, ML pipeline events, data catalog events,
|
380
|
+
infrastructure events, compliance events, and business events.
|
381
|
+
"""
|
386
382
|
|
387
383
|
schedule: list[Cron] = Field(
|
388
384
|
default_factory=list,
|