ddeutil-workflow 0.0.82__tar.gz → 0.0.83__tar.gz
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-0.0.82 → ddeutil_workflow-0.0.83}/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__about__.py +1 -1
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__cron.py +1 -1
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__init__.py +3 -2
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__types.py +10 -1
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/audits.py +64 -41
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/errors.py +3 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/event.py +34 -11
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/job.py +5 -15
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/result.py +41 -12
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/stages.py +504 -292
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/traces.py +9 -5
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/utils.py +34 -20
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/workflow.py +32 -50
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/PKG-INFO +1 -1
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_audits.py +18 -15
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_job.py +7 -1
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_job_exec.py +1 -1
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_result.py +1 -1
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_workflow.py +15 -4
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_workflow_exec.py +24 -5
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_workflow_release.py +17 -17
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/LICENSE +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/README.md +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/pyproject.toml +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/setup.cfg +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/__main__.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/__init__.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/log_conf.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/__init__.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/job.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/logs.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/api/routes/workflows.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/conf.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/params.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/__init__.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/__init__.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/aws.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/az.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/container.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/plugins/providers/gcs.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil/workflow/reusables.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/SOURCES.txt +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/dependency_links.txt +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/requires.txt +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/src/ddeutil_workflow.egg-info/top_level.txt +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test__cron.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test__regex.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_cli.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_conf.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_errors.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_event.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_job_exec_strategy.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_params.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_reusables_call_tag.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_reusables_func_model.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_reusables_template.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_reusables_template_filter.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_strategy.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_traces.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_utils.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_workflow_exec_job.py +0 -0
- {ddeutil_workflow-0.0.82 → ddeutil_workflow-0.0.83}/tests/test_workflow_rerun.py +0 -0
@@ -1,2 +1,2 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.83"
|
2
2
|
__python_version__: str = "3.9"
|
@@ -715,7 +715,7 @@ class CronJob:
|
|
715
715
|
self,
|
716
716
|
date: Optional[datetime] = None,
|
717
717
|
*,
|
718
|
-
tz: Optional[str] = None,
|
718
|
+
tz: Optional[Union[str, ZoneInfo]] = None,
|
719
719
|
) -> CronRunner:
|
720
720
|
"""Returns CronRunner instance that be datetime runner with this
|
721
721
|
cronjob. It can use `next`, `prev`, or `reset` methods to generate
|
@@ -50,12 +50,12 @@ Note:
|
|
50
50
|
from .__cron import CronRunner
|
51
51
|
from .__types import DictData, DictStr, Matrix, Re, TupleStr
|
52
52
|
from .audits import (
|
53
|
-
|
53
|
+
DRYRUN,
|
54
54
|
FORCE,
|
55
55
|
NORMAL,
|
56
56
|
RERUN,
|
57
57
|
Audit,
|
58
|
-
|
58
|
+
LocalFileAudit,
|
59
59
|
get_audit,
|
60
60
|
)
|
61
61
|
from .conf import (
|
@@ -71,6 +71,7 @@ from .conf import (
|
|
71
71
|
)
|
72
72
|
from .errors import (
|
73
73
|
BaseError,
|
74
|
+
EventError,
|
74
75
|
JobCancelError,
|
75
76
|
JobError,
|
76
77
|
JobSkipError,
|
@@ -16,17 +16,26 @@ from re import (
|
|
16
16
|
Match,
|
17
17
|
Pattern,
|
18
18
|
)
|
19
|
-
from typing import Any, Optional, TypedDict, Union
|
19
|
+
from typing import Any, Optional, TypedDict, Union, cast
|
20
20
|
|
21
21
|
from typing_extensions import Self
|
22
22
|
|
23
23
|
StrOrNone = Optional[str]
|
24
24
|
StrOrInt = Union[str, int]
|
25
25
|
TupleStr = tuple[str, ...]
|
26
|
+
ListStr = list[str]
|
27
|
+
ListInt = list[int]
|
26
28
|
DictData = dict[str, Any]
|
29
|
+
DictRange = dict[int, Any]
|
27
30
|
DictStr = dict[str, str]
|
28
31
|
Matrix = dict[str, Union[list[str], list[int]]]
|
29
32
|
|
33
|
+
|
34
|
+
def cast_dict(value: TypedDict[...]) -> DictData:
|
35
|
+
"""Cast any TypedDict object to DictData type."""
|
36
|
+
return cast(DictData, value)
|
37
|
+
|
38
|
+
|
30
39
|
# Pre-compile regex patterns for better performance
|
31
40
|
_RE_CALLER_PATTERN = r"""
|
32
41
|
\$ # start with $
|
@@ -74,31 +74,38 @@ class ReleaseType(str, Enum):
|
|
74
74
|
Attributes:
|
75
75
|
NORMAL: Standard workflow release execution
|
76
76
|
RERUN: Re-execution of previously failed workflow
|
77
|
-
|
77
|
+
DRYRUN: Dry-execution workflow
|
78
78
|
FORCE: Forced execution bypassing normal conditions
|
79
79
|
"""
|
80
80
|
|
81
81
|
NORMAL = "normal"
|
82
82
|
RERUN = "rerun"
|
83
|
-
EVENT = "event"
|
84
83
|
FORCE = "force"
|
84
|
+
DRYRUN = "dryrun"
|
85
85
|
|
86
86
|
|
87
87
|
NORMAL = ReleaseType.NORMAL
|
88
88
|
RERUN = ReleaseType.RERUN
|
89
|
-
|
89
|
+
DRYRUN = ReleaseType.DRYRUN
|
90
90
|
FORCE = ReleaseType.FORCE
|
91
91
|
|
92
92
|
|
93
93
|
class AuditData(BaseModel):
|
94
|
-
"""Audit Data model
|
94
|
+
"""Audit Data model that use to be the core data for any Audit model manage
|
95
|
+
logging at the target pointer system or service like file-system, sqlite
|
96
|
+
database, etc.
|
97
|
+
"""
|
95
98
|
|
96
99
|
model_config = ConfigDict(use_enum_values=True)
|
97
100
|
|
98
101
|
name: str = Field(description="A workflow name.")
|
99
102
|
release: datetime = Field(description="A release datetime.")
|
100
103
|
type: ReleaseType = Field(
|
101
|
-
default=NORMAL,
|
104
|
+
default=NORMAL,
|
105
|
+
description=(
|
106
|
+
"An execution type that should be value in ('normal', 'rerun', "
|
107
|
+
"'force', 'dryrun')."
|
108
|
+
),
|
102
109
|
)
|
103
110
|
context: DictData = Field(
|
104
111
|
default_factory=dict,
|
@@ -121,18 +128,17 @@ class BaseAudit(BaseModel, ABC):
|
|
121
128
|
for logging subclasses like file, sqlite, etc.
|
122
129
|
"""
|
123
130
|
|
124
|
-
type:
|
131
|
+
type: Literal["base"] = "base"
|
132
|
+
logging_name: str = "ddeutil.workflow"
|
125
133
|
extras: DictData = Field(
|
126
134
|
default_factory=dict,
|
127
135
|
description="An extras parameter that want to override core config",
|
128
136
|
)
|
129
137
|
|
130
138
|
@field_validator("extras", mode="before")
|
131
|
-
def
|
139
|
+
def __prepare_extras(cls, v: Any) -> Any:
|
132
140
|
"""Validate extras field to ensure it's a dictionary."""
|
133
|
-
if v is None
|
134
|
-
return {}
|
135
|
-
return v
|
141
|
+
return {} if v is None else v
|
136
142
|
|
137
143
|
@model_validator(mode="after")
|
138
144
|
def __model_action(self) -> Self:
|
@@ -148,7 +154,7 @@ class BaseAudit(BaseModel, ABC):
|
|
148
154
|
self.do_before()
|
149
155
|
|
150
156
|
# NOTE: Start setting log config in this line with cache.
|
151
|
-
set_logging(
|
157
|
+
set_logging(self.logging_name)
|
152
158
|
return self
|
153
159
|
|
154
160
|
@abstractmethod
|
@@ -248,7 +254,7 @@ class BaseAudit(BaseModel, ABC):
|
|
248
254
|
raise NotImplementedError("Audit should implement `save` method.")
|
249
255
|
|
250
256
|
|
251
|
-
class
|
257
|
+
class LocalFileAudit(BaseAudit):
|
252
258
|
"""File Audit Pydantic Model for saving log data from workflow execution.
|
253
259
|
|
254
260
|
This class inherits from BaseAudit and implements file-based storage
|
@@ -256,19 +262,25 @@ class FileAudit(BaseAudit):
|
|
256
262
|
in a structured directory hierarchy.
|
257
263
|
|
258
264
|
Attributes:
|
259
|
-
|
265
|
+
file_fmt: Class variable defining the filename format for audit log.
|
266
|
+
file_release_fmt: Class variable defining the filename format for audit
|
267
|
+
release log.
|
260
268
|
"""
|
261
269
|
|
262
|
-
|
263
|
-
|
264
|
-
)
|
270
|
+
file_fmt: ClassVar[str] = "workflow={name}"
|
271
|
+
file_release_fmt: ClassVar[str] = "release={release:%Y%m%d%H%M%S}"
|
265
272
|
|
266
273
|
type: Literal["file"] = "file"
|
267
|
-
path:
|
268
|
-
default="./audits",
|
274
|
+
path: Path = Field(
|
275
|
+
default=Path("./audits"),
|
269
276
|
description="A file path that use to manage audit logs.",
|
270
277
|
)
|
271
278
|
|
279
|
+
@field_validator("path", mode="before", json_schema_input_type=str)
|
280
|
+
def __prepare_path(cls, data: Any) -> Any:
|
281
|
+
"""Prepare path that passing with string to Path instance."""
|
282
|
+
return Path(data) if isinstance(data, str) else data
|
283
|
+
|
272
284
|
def do_before(self) -> None:
|
273
285
|
"""Create directory of release before saving log file.
|
274
286
|
|
@@ -278,7 +290,10 @@ class FileAudit(BaseAudit):
|
|
278
290
|
Path(self.path).mkdir(parents=True, exist_ok=True)
|
279
291
|
|
280
292
|
def find_audits(
|
281
|
-
self,
|
293
|
+
self,
|
294
|
+
name: str,
|
295
|
+
*,
|
296
|
+
extras: Optional[DictData] = None,
|
282
297
|
) -> Iterator[AuditData]:
|
283
298
|
"""Generate audit data found from logs path for a specific workflow name.
|
284
299
|
|
@@ -292,7 +307,7 @@ class FileAudit(BaseAudit):
|
|
292
307
|
Raises:
|
293
308
|
FileNotFoundError: If the workflow directory does not exist.
|
294
309
|
"""
|
295
|
-
pointer: Path =
|
310
|
+
pointer: Path = self.path / self.file_fmt.format(name=name)
|
296
311
|
if not pointer.exists():
|
297
312
|
raise FileNotFoundError(f"Pointer: {pointer.absolute()}.")
|
298
313
|
|
@@ -325,7 +340,7 @@ class FileAudit(BaseAudit):
|
|
325
340
|
ValueError: If no releases found when release is None.
|
326
341
|
"""
|
327
342
|
if release is None:
|
328
|
-
pointer: Path =
|
343
|
+
pointer: Path = self.path / self.file_fmt.format(name=name)
|
329
344
|
if not pointer.exists():
|
330
345
|
raise FileNotFoundError(f"Pointer: {pointer.absolute()}.")
|
331
346
|
|
@@ -382,8 +397,10 @@ class FileAudit(BaseAudit):
|
|
382
397
|
Returns:
|
383
398
|
Path: The directory path for the current workflow and release.
|
384
399
|
"""
|
385
|
-
return
|
386
|
-
|
400
|
+
return (
|
401
|
+
self.path
|
402
|
+
/ self.file_fmt.format(**data.model_dump(by_alias=True))
|
403
|
+
/ self.file_release_fmt.format(**data.model_dump(by_alias=True))
|
387
404
|
)
|
388
405
|
|
389
406
|
def save(self, data: Any, excluded: Optional[list[str]] = None) -> Self:
|
@@ -459,7 +476,7 @@ class FileAudit(BaseAudit):
|
|
459
476
|
return cleaned_count
|
460
477
|
|
461
478
|
|
462
|
-
class
|
479
|
+
class LocalSQLiteAudit(BaseAudit): # pragma: no cov
|
463
480
|
"""SQLite Audit model for database-based audit storage.
|
464
481
|
|
465
482
|
This class inherits from BaseAudit and implements SQLite database storage
|
@@ -467,11 +484,11 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
467
484
|
|
468
485
|
Attributes:
|
469
486
|
table_name: Class variable defining the database table name.
|
470
|
-
|
487
|
+
ddl: Class variable defining the database schema.
|
471
488
|
"""
|
472
489
|
|
473
490
|
table_name: ClassVar[str] = "audits"
|
474
|
-
|
491
|
+
ddl: ClassVar[
|
475
492
|
str
|
476
493
|
] = """
|
477
494
|
CREATE TABLE IF NOT EXISTS audits (
|
@@ -489,22 +506,21 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
489
506
|
"""
|
490
507
|
|
491
508
|
type: Literal["sqlite"] = "sqlite"
|
492
|
-
path:
|
509
|
+
path: Path = Field(
|
510
|
+
default=Path("./audits.db"),
|
511
|
+
description="A SQLite filepath.",
|
512
|
+
)
|
493
513
|
|
494
|
-
def
|
514
|
+
def do_before(self) -> None:
|
495
515
|
"""Ensure the audit table exists in the database."""
|
496
|
-
|
497
|
-
if audit_url is None or not audit_url.path:
|
516
|
+
if self.path.is_dir():
|
498
517
|
raise ValueError(
|
499
|
-
"SQLite
|
518
|
+
"SQLite path must specify a database file path not dir."
|
500
519
|
)
|
501
520
|
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
with sqlite3.connect(db_path) as conn:
|
507
|
-
conn.execute(self.schemas)
|
521
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
522
|
+
with sqlite3.connect(self.path) as conn:
|
523
|
+
conn.execute(self.ddl)
|
508
524
|
conn.commit()
|
509
525
|
|
510
526
|
def is_pointed(
|
@@ -771,24 +787,31 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
771
787
|
return cursor.rowcount
|
772
788
|
|
773
789
|
|
790
|
+
class PostgresAudit(BaseAudit, ABC): ... # pragma: no cov
|
791
|
+
|
792
|
+
|
774
793
|
Audit = Annotated[
|
775
794
|
Union[
|
776
|
-
|
777
|
-
|
795
|
+
LocalFileAudit,
|
796
|
+
LocalSQLiteAudit,
|
778
797
|
],
|
779
798
|
Field(discriminator="type"),
|
780
799
|
]
|
781
800
|
|
782
801
|
|
783
|
-
def get_audit(
|
802
|
+
def get_audit(
|
803
|
+
audit_conf: Optional[DictData] = None,
|
804
|
+
extras: Optional[DictData] = None,
|
805
|
+
) -> Audit: # pragma: no cov
|
784
806
|
"""Get an audit model dynamically based on the config audit path value.
|
785
807
|
|
786
808
|
Args:
|
809
|
+
audit_conf (DictData):
|
787
810
|
extras: Optional extra parameters to override the core config.
|
788
811
|
|
789
812
|
Returns:
|
790
813
|
Audit: The appropriate audit model class based on configuration.
|
791
814
|
"""
|
792
|
-
audit_conf = dynamic("audit_conf", extras=extras)
|
815
|
+
audit_conf = dynamic("audit_conf", f=audit_conf, extras=extras)
|
793
816
|
model = TypeAdapter(Audit).validate_python(audit_conf | {"extras": extras})
|
794
817
|
return model
|
@@ -16,13 +16,9 @@ Attributes:
|
|
16
16
|
Interval: Type alias for scheduling intervals ('daily', 'weekly', 'monthly')
|
17
17
|
|
18
18
|
Classes:
|
19
|
+
CrontabValue:
|
19
20
|
Crontab: Main cron-based event scheduler.
|
20
21
|
CrontabYear: Enhanced cron scheduler with year constraints.
|
21
|
-
ReleaseEvent: Release-based event triggers.
|
22
|
-
FileEvent: File system monitoring triggers.
|
23
|
-
WebhookEvent: API/webhook-based triggers.
|
24
|
-
DatabaseEvent: Database change monitoring triggers.
|
25
|
-
SensorEvent: Sensor-based event monitoring.
|
26
22
|
|
27
23
|
Example:
|
28
24
|
>>> from ddeutil.workflow.event import Crontab
|
@@ -50,6 +46,8 @@ from pydantic_extra_types.timezone_name import TimeZoneName
|
|
50
46
|
|
51
47
|
from .__cron import WEEKDAYS, CronJob, CronJobYear, CronRunner, Options
|
52
48
|
from .__types import DictData
|
49
|
+
from .errors import EventError
|
50
|
+
from .utils import UTC, replace_sec
|
53
51
|
|
54
52
|
Interval = Literal["daily", "weekly", "monthly"]
|
55
53
|
|
@@ -393,19 +391,16 @@ class Event(BaseModel):
|
|
393
391
|
)
|
394
392
|
|
395
393
|
@field_validator("schedule", mode="after")
|
396
|
-
def
|
397
|
-
cls,
|
398
|
-
value: list[Crontab],
|
399
|
-
) -> list[Crontab]:
|
394
|
+
def __prepare_schedule__(cls, value: list[Crontab]) -> list[Crontab]:
|
400
395
|
"""Validate the on fields should not contain duplicate values and if it
|
401
396
|
contains the every minute value more than one value, it will remove to
|
402
397
|
only one value.
|
403
398
|
|
404
399
|
Args:
|
405
|
-
value: A list of on object.
|
400
|
+
value (list[Crontab]): A list of on object.
|
406
401
|
|
407
402
|
Returns:
|
408
|
-
list[
|
403
|
+
list[Crontab]: The validated list of Crontab objects.
|
409
404
|
|
410
405
|
Raises:
|
411
406
|
ValueError: If it has some duplicate value.
|
@@ -434,3 +429,31 @@ class Event(BaseModel):
|
|
434
429
|
"The number of the on should not more than 10 crontabs."
|
435
430
|
)
|
436
431
|
return value
|
432
|
+
|
433
|
+
def validate_dt(self, dt: datetime) -> datetime:
|
434
|
+
"""Validate the release datetime that should was replaced second and
|
435
|
+
millisecond to 0 and replaced timezone to None before checking it match
|
436
|
+
with the set `on` field.
|
437
|
+
|
438
|
+
Args:
|
439
|
+
dt (datetime): A datetime object that want to validate.
|
440
|
+
|
441
|
+
Returns:
|
442
|
+
datetime: The validated release datetime.
|
443
|
+
"""
|
444
|
+
if dt.tzinfo is None:
|
445
|
+
dt = dt.replace(tzinfo=UTC)
|
446
|
+
|
447
|
+
release: datetime = replace_sec(dt.astimezone(UTC))
|
448
|
+
|
449
|
+
# NOTE: Return itself if schedule event does not set.
|
450
|
+
if not self.schedule:
|
451
|
+
return release
|
452
|
+
|
453
|
+
for on in self.schedule:
|
454
|
+
if release == on.cronjob.schedule(release, tz=UTC).next:
|
455
|
+
return release
|
456
|
+
raise EventError(
|
457
|
+
f"This datetime, {datetime}, does not support for this event "
|
458
|
+
f"schedule."
|
459
|
+
)
|
@@ -73,7 +73,7 @@ from .result import (
|
|
73
73
|
from .reusables import has_template, param2template
|
74
74
|
from .stages import Stage
|
75
75
|
from .traces import Trace, get_trace
|
76
|
-
from .utils import cross_product, filter_func, gen_id
|
76
|
+
from .utils import cross_product, extract_id, filter_func, gen_id
|
77
77
|
|
78
78
|
MatrixFilter = list[dict[str, Union[str, int]]]
|
79
79
|
|
@@ -890,8 +890,9 @@ class Job(BaseModel):
|
|
890
890
|
Result: Return Result object that create from execution context.
|
891
891
|
"""
|
892
892
|
ts: float = time.monotonic()
|
893
|
-
parent_run_id
|
894
|
-
|
893
|
+
parent_run_id, run_id = extract_id(
|
894
|
+
(self.id or "EMPTY"), run_id=run_id, extras=self.extras
|
895
|
+
)
|
895
896
|
trace: Trace = get_trace(
|
896
897
|
run_id, parent_run_id=parent_run_id, extras=self.extras
|
897
898
|
)
|
@@ -1272,7 +1273,6 @@ def local_execute(
|
|
1272
1273
|
|
1273
1274
|
errors: DictData = {}
|
1274
1275
|
statuses: list[Status] = [WAIT] * len_strategy
|
1275
|
-
fail_fast: bool = False
|
1276
1276
|
|
1277
1277
|
if not job.strategy.fail_fast:
|
1278
1278
|
done: Iterator[Future] = as_completed(futures)
|
@@ -1297,7 +1297,6 @@ def local_execute(
|
|
1297
1297
|
)
|
1298
1298
|
trace.debug(f"[JOB]: ... Job was set Fail-Fast{nd}")
|
1299
1299
|
done: Iterator[Future] = as_completed(futures)
|
1300
|
-
fail_fast: bool = True
|
1301
1300
|
|
1302
1301
|
for i, future in enumerate(done, start=0):
|
1303
1302
|
try:
|
@@ -1312,19 +1311,10 @@ def local_execute(
|
|
1312
1311
|
pass
|
1313
1312
|
|
1314
1313
|
status: Status = validate_statuses(statuses)
|
1315
|
-
|
1316
|
-
# NOTE: Prepare status because it does not cancel from parent event but
|
1317
|
-
# cancel from failed item execution.
|
1318
|
-
if fail_fast and status == CANCEL:
|
1319
|
-
status = FAILED
|
1320
|
-
|
1321
|
-
return Result(
|
1322
|
-
run_id=run_id,
|
1323
|
-
parent_run_id=parent_run_id,
|
1314
|
+
return Result.from_trace(trace).catch(
|
1324
1315
|
status=status,
|
1325
1316
|
context=catch(context, status=status, updated=errors),
|
1326
1317
|
info={"execution_time": time.monotonic() - ts},
|
1327
|
-
extras=job.extras,
|
1328
1318
|
)
|
1329
1319
|
|
1330
1320
|
|
@@ -8,20 +8,12 @@
|
|
8
8
|
This module provides the core result and status management functionality for
|
9
9
|
workflow execution tracking. It includes the Status enumeration for execution
|
10
10
|
states and the Result dataclass for context transfer between workflow components.
|
11
|
-
|
12
|
-
Classes:
|
13
|
-
Status: Enumeration for execution status tracking
|
14
|
-
Result: Dataclass for execution context and result management
|
15
|
-
|
16
|
-
Functions:
|
17
|
-
validate_statuses: Determine final status from multiple status values
|
18
|
-
get_status_from_error: Convert exception types to appropriate status
|
19
11
|
"""
|
20
12
|
from __future__ import annotations
|
21
13
|
|
22
14
|
from dataclasses import field
|
23
15
|
from enum import Enum
|
24
|
-
from typing import Optional, TypedDict, Union
|
16
|
+
from typing import Any, Optional, TypedDict, Union
|
25
17
|
|
26
18
|
from pydantic import ConfigDict
|
27
19
|
from pydantic.dataclasses import dataclass
|
@@ -126,10 +118,10 @@ def validate_statuses(statuses: list[Status]) -> Status:
|
|
126
118
|
>>> validate_statuses([SUCCESS, SUCCESS, SUCCESS])
|
127
119
|
>>> # Returns: SUCCESS
|
128
120
|
"""
|
129
|
-
if any(s ==
|
130
|
-
return CANCEL
|
131
|
-
elif any(s == FAILED for s in statuses):
|
121
|
+
if any(s == FAILED for s in statuses):
|
132
122
|
return FAILED
|
123
|
+
elif any(s == CANCEL for s in statuses):
|
124
|
+
return CANCEL
|
133
125
|
elif any(s == WAIT for s in statuses):
|
134
126
|
return WAIT
|
135
127
|
for status in (SUCCESS, SKIP):
|
@@ -313,3 +305,40 @@ class Context(TypedDict):
|
|
313
305
|
context: NotRequired[DictData]
|
314
306
|
errors: NotRequired[Union[list[ErrorData], ErrorData]]
|
315
307
|
info: NotRequired[DictData]
|
308
|
+
|
309
|
+
|
310
|
+
class Layer(str, Enum):
|
311
|
+
WORKFLOW = "workflow"
|
312
|
+
JOB = "job"
|
313
|
+
STRATEGY = "strategy"
|
314
|
+
STAGE = "stage"
|
315
|
+
|
316
|
+
|
317
|
+
def get_context_by_layer(
|
318
|
+
context: DictData,
|
319
|
+
key: str,
|
320
|
+
layer: Layer,
|
321
|
+
context_key: str,
|
322
|
+
*,
|
323
|
+
default: Optional[Any] = None,
|
324
|
+
) -> Any: # pragma: no cov
|
325
|
+
if layer == Layer.WORKFLOW:
|
326
|
+
return context.get("jobs", {}).get(key, {}).get(context_key, default)
|
327
|
+
elif layer == Layer.JOB:
|
328
|
+
return context.get("stages", {}).get(key, {}).get(context_key, default)
|
329
|
+
elif layer == Layer.STRATEGY:
|
330
|
+
return (
|
331
|
+
context.get("strategies", {}).get(key, {}).get(context_key, default)
|
332
|
+
)
|
333
|
+
return context.get(key, {}).get(context_key, default)
|
334
|
+
|
335
|
+
|
336
|
+
def get_status(
|
337
|
+
context: DictData,
|
338
|
+
key: str,
|
339
|
+
layer: Layer,
|
340
|
+
) -> Status: # pragma: no cov
|
341
|
+
"""Get status from context by a specific key and context layer."""
|
342
|
+
return get_context_by_layer(
|
343
|
+
context, key, layer, context_key="status", default=WAIT
|
344
|
+
)
|