ddeutil-workflow 0.0.63__py3-none-any.whl → 0.0.65__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 -8
- ddeutil/workflow/api/__init__.py +5 -84
- ddeutil/workflow/api/routes/__init__.py +0 -1
- ddeutil/workflow/api/routes/job.py +2 -3
- ddeutil/workflow/api/routes/logs.py +0 -2
- ddeutil/workflow/api/routes/workflows.py +0 -3
- ddeutil/workflow/conf.py +6 -38
- ddeutil/workflow/{exceptions.py → errors.py} +47 -12
- ddeutil/workflow/job.py +249 -118
- ddeutil/workflow/params.py +11 -11
- ddeutil/workflow/result.py +86 -10
- ddeutil/workflow/reusables.py +54 -23
- ddeutil/workflow/stages.py +692 -464
- ddeutil/workflow/utils.py +37 -2
- ddeutil/workflow/workflow.py +163 -664
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/METADATA +17 -67
- ddeutil_workflow-0.0.65.dist-info/RECORD +28 -0
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/WHEEL +1 -1
- ddeutil/workflow/api/routes/schedules.py +0 -141
- ddeutil/workflow/api/utils.py +0 -174
- ddeutil/workflow/scheduler.py +0 -813
- ddeutil_workflow-0.0.63.dist-info/RECORD +0 -31
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/licenses/LICENSE +0 -0
- {ddeutil_workflow-0.0.63.dist-info → ddeutil_workflow-0.0.65.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.65"
|
ddeutil/workflow/__init__.py
CHANGED
@@ -6,8 +6,8 @@
|
|
6
6
|
from .__cron import CronJob, CronRunner
|
7
7
|
from .__types import DictData, DictStr, Matrix, Re, TupleStr
|
8
8
|
from .conf import *
|
9
|
+
from .errors import *
|
9
10
|
from .event import *
|
10
|
-
from .exceptions import *
|
11
11
|
from .job import *
|
12
12
|
from .logs import (
|
13
13
|
Audit,
|
@@ -33,13 +33,6 @@ from .result import (
|
|
33
33
|
Status,
|
34
34
|
)
|
35
35
|
from .reusables import *
|
36
|
-
from .scheduler import (
|
37
|
-
Schedule,
|
38
|
-
ScheduleWorkflow,
|
39
|
-
schedule_control,
|
40
|
-
schedule_runner,
|
41
|
-
schedule_task,
|
42
|
-
)
|
43
36
|
from .stages import *
|
44
37
|
from .utils import *
|
45
38
|
from .workflow import *
|
ddeutil/workflow/api/__init__.py
CHANGED
@@ -7,8 +7,6 @@ from __future__ import annotations
|
|
7
7
|
|
8
8
|
import contextlib
|
9
9
|
from collections.abc import AsyncIterator
|
10
|
-
from datetime import datetime, timedelta
|
11
|
-
from typing import TypedDict
|
12
10
|
|
13
11
|
from dotenv import load_dotenv
|
14
12
|
from fastapi import FastAPI, Request
|
@@ -20,49 +18,18 @@ from fastapi.middleware.gzip import GZipMiddleware
|
|
20
18
|
from fastapi.responses import UJSONResponse
|
21
19
|
|
22
20
|
from ..__about__ import __version__
|
23
|
-
from ..conf import api_config
|
21
|
+
from ..conf import api_config
|
24
22
|
from ..logs import get_logger
|
25
|
-
from
|
26
|
-
from ..workflow import ReleaseQueue, WorkflowTask
|
27
|
-
from .routes import job, log
|
28
|
-
from .utils import repeat_at
|
23
|
+
from .routes import job, log, workflow
|
29
24
|
|
30
25
|
load_dotenv()
|
31
26
|
logger = get_logger("uvicorn.error")
|
32
27
|
|
33
28
|
|
34
|
-
class State(TypedDict):
|
35
|
-
"""TypeDict for State of FastAPI application."""
|
36
|
-
|
37
|
-
scheduler: list[str]
|
38
|
-
workflow_threads: ReleaseThreads
|
39
|
-
workflow_tasks: list[WorkflowTask]
|
40
|
-
workflow_queue: dict[str, ReleaseQueue]
|
41
|
-
|
42
|
-
|
43
29
|
@contextlib.asynccontextmanager
|
44
|
-
async def lifespan(
|
30
|
+
async def lifespan(_: FastAPI) -> AsyncIterator[dict[str, list]]:
|
45
31
|
"""Lifespan function for the FastAPI application."""
|
46
|
-
|
47
|
-
a.state.workflow_threads = {}
|
48
|
-
a.state.workflow_tasks = []
|
49
|
-
a.state.workflow_queue = {}
|
50
|
-
|
51
|
-
yield {
|
52
|
-
# NOTE: Scheduler value should be contained a key of workflow and
|
53
|
-
# list of datetime of queue and running.
|
54
|
-
#
|
55
|
-
# ... {
|
56
|
-
# ... '<workflow-name>': (
|
57
|
-
# ... [<running-datetime>, ...], [<queue-datetime>, ...]
|
58
|
-
# ... )
|
59
|
-
# ... }
|
60
|
-
#
|
61
|
-
"scheduler": a.state.scheduler,
|
62
|
-
"workflow_queue": a.state.workflow_queue,
|
63
|
-
"workflow_threads": a.state.workflow_threads,
|
64
|
-
"workflow_tasks": a.state.workflow_tasks,
|
65
|
-
}
|
32
|
+
yield {}
|
66
33
|
|
67
34
|
|
68
35
|
app = FastAPI(
|
@@ -99,53 +66,7 @@ async def health():
|
|
99
66
|
# NOTE Add the jobs and logs routes by default.
|
100
67
|
app.include_router(job, prefix=api_config.prefix_path)
|
101
68
|
app.include_router(log, prefix=api_config.prefix_path)
|
102
|
-
|
103
|
-
|
104
|
-
# NOTE: Enable the workflows route.
|
105
|
-
if api_config.enable_route_workflow:
|
106
|
-
from .routes import workflow
|
107
|
-
|
108
|
-
app.include_router(workflow, prefix=api_config.prefix_path)
|
109
|
-
|
110
|
-
|
111
|
-
# NOTE: Enable the schedules route.
|
112
|
-
if api_config.enable_route_schedule:
|
113
|
-
from ..logs import get_audit
|
114
|
-
from ..scheduler import schedule_task
|
115
|
-
from .routes import schedule
|
116
|
-
|
117
|
-
app.include_router(schedule, prefix=api_config.prefix_path)
|
118
|
-
|
119
|
-
@schedule.on_event("startup")
|
120
|
-
@repeat_at(cron="* * * * *", delay=2)
|
121
|
-
def scheduler_listener():
|
122
|
-
"""Schedule broker every minute at 02 second."""
|
123
|
-
logger.debug(
|
124
|
-
f"[SCHEDULER]: Start listening schedule from queue "
|
125
|
-
f"{app.state.scheduler}"
|
126
|
-
)
|
127
|
-
if app.state.workflow_tasks:
|
128
|
-
schedule_task(
|
129
|
-
app.state.workflow_tasks,
|
130
|
-
stop=datetime.now(config.tz) + timedelta(minutes=1),
|
131
|
-
queue=app.state.workflow_queue,
|
132
|
-
threads=app.state.workflow_threads,
|
133
|
-
audit=get_audit(),
|
134
|
-
)
|
135
|
-
|
136
|
-
@schedule.on_event("startup")
|
137
|
-
@repeat_at(cron="*/5 * * * *", delay=10)
|
138
|
-
def monitoring():
|
139
|
-
"""Monitoring workflow thread that running in the background."""
|
140
|
-
logger.debug("[MONITOR]: Start monitoring threading.")
|
141
|
-
snapshot_threads: list[str] = list(app.state.workflow_threads.keys())
|
142
|
-
for t_name in snapshot_threads:
|
143
|
-
|
144
|
-
thread_release: ReleaseThread = app.state.workflow_threads[t_name]
|
145
|
-
|
146
|
-
# NOTE: remove the thread that running success.
|
147
|
-
if not thread_release["thread"].is_alive():
|
148
|
-
app.state.workflow_threads.pop(t_name)
|
69
|
+
app.include_router(workflow, prefix=api_config.prefix_path)
|
149
70
|
|
150
71
|
|
151
72
|
@app.exception_handler(RequestValidationError)
|
@@ -12,7 +12,7 @@ from fastapi.responses import UJSONResponse
|
|
12
12
|
from pydantic import BaseModel, Field
|
13
13
|
|
14
14
|
from ...__types import DictData
|
15
|
-
from ...
|
15
|
+
from ...errors import JobError
|
16
16
|
from ...job import Job
|
17
17
|
from ...logs import get_logger
|
18
18
|
from ...result import Result
|
@@ -59,7 +59,7 @@ async def job_execute(
|
|
59
59
|
).context,
|
60
60
|
to=context,
|
61
61
|
)
|
62
|
-
except
|
62
|
+
except JobError as err:
|
63
63
|
rs.trace.error(f"[JOB]: {err.__class__.__name__}: {err}")
|
64
64
|
|
65
65
|
return {
|
@@ -69,7 +69,6 @@ async def job_execute(
|
|
69
69
|
by_alias=True,
|
70
70
|
exclude_none=False,
|
71
71
|
exclude_unset=True,
|
72
|
-
exclude_defaults=True,
|
73
72
|
),
|
74
73
|
"params": params,
|
75
74
|
"context": context,
|
@@ -44,7 +44,6 @@ async def get_traces(
|
|
44
44
|
by_alias=True,
|
45
45
|
exclude_none=True,
|
46
46
|
exclude_unset=True,
|
47
|
-
exclude_defaults=True,
|
48
47
|
)
|
49
48
|
for trace in result.trace.find_traces()
|
50
49
|
],
|
@@ -73,7 +72,6 @@ async def get_trace_with_id(run_id: str):
|
|
73
72
|
by_alias=True,
|
74
73
|
exclude_none=True,
|
75
74
|
exclude_unset=True,
|
76
|
-
exclude_defaults=True,
|
77
75
|
)
|
78
76
|
),
|
79
77
|
}
|
@@ -56,7 +56,6 @@ async def get_workflow_by_name(name: str) -> DictData:
|
|
56
56
|
by_alias=True,
|
57
57
|
exclude_none=False,
|
58
58
|
exclude_unset=True,
|
59
|
-
exclude_defaults=True,
|
60
59
|
)
|
61
60
|
|
62
61
|
|
@@ -99,7 +98,6 @@ async def get_workflow_audits(name: str):
|
|
99
98
|
by_alias=True,
|
100
99
|
exclude_none=False,
|
101
100
|
exclude_unset=True,
|
102
|
-
exclude_defaults=True,
|
103
101
|
)
|
104
102
|
for audit in get_audit().find_audits(name=name)
|
105
103
|
],
|
@@ -133,6 +131,5 @@ async def get_workflow_release_audit(name: str, release: str):
|
|
133
131
|
by_alias=True,
|
134
132
|
exclude_none=False,
|
135
133
|
exclude_unset=True,
|
136
|
-
exclude_defaults=True,
|
137
134
|
),
|
138
135
|
}
|
ddeutil/workflow/conf.py
CHANGED
@@ -6,11 +6,9 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import copy
|
9
|
-
import json
|
10
9
|
import os
|
11
10
|
from abc import ABC, abstractmethod
|
12
11
|
from collections.abc import Iterator
|
13
|
-
from datetime import timedelta
|
14
12
|
from functools import cached_property
|
15
13
|
from inspect import isclass
|
16
14
|
from pathlib import Path
|
@@ -20,7 +18,7 @@ from zoneinfo import ZoneInfo
|
|
20
18
|
from ddeutil.core import str2bool
|
21
19
|
from ddeutil.io import YamlFlResolve, search_env_replace
|
22
20
|
from ddeutil.io.paths import glob_files, is_ignored, read_ignore
|
23
|
-
from pydantic import SecretStr
|
21
|
+
from pydantic import SecretStr, TypeAdapter
|
24
22
|
|
25
23
|
from .__types import DictData
|
26
24
|
|
@@ -143,10 +141,6 @@ class Config: # pragma: no cov
|
|
143
141
|
def log_datetime_format(self) -> str:
|
144
142
|
return env("LOG_DATETIME_FORMAT", "%Y-%m-%d %H:%M:%S")
|
145
143
|
|
146
|
-
@property
|
147
|
-
def stage_raise_error(self) -> bool:
|
148
|
-
return str2bool(env("CORE_STAGE_RAISE_ERROR", "false"))
|
149
|
-
|
150
144
|
@property
|
151
145
|
def stage_default_id(self) -> bool:
|
152
146
|
return str2bool(env("CORE_STAGE_DEFAULT_ID", "false"))
|
@@ -163,28 +157,6 @@ class Config: # pragma: no cov
|
|
163
157
|
def max_queue_complete_hist(self) -> int:
|
164
158
|
return int(env("CORE_MAX_QUEUE_COMPLETE_HIST", "16"))
|
165
159
|
|
166
|
-
# NOTE: App
|
167
|
-
@property
|
168
|
-
def max_schedule_process(self) -> int:
|
169
|
-
return int(env("APP_MAX_PROCESS", "2"))
|
170
|
-
|
171
|
-
@property
|
172
|
-
def max_schedule_per_process(self) -> int:
|
173
|
-
return int(env("APP_MAX_SCHEDULE_PER_PROCESS", "100"))
|
174
|
-
|
175
|
-
@property
|
176
|
-
def stop_boundary_delta(self) -> timedelta:
|
177
|
-
stop_boundary_delta_str: str = env(
|
178
|
-
"APP_STOP_BOUNDARY_DELTA", '{"minutes": 5, "seconds": 20}'
|
179
|
-
)
|
180
|
-
try:
|
181
|
-
return timedelta(**json.loads(stop_boundary_delta_str))
|
182
|
-
except Exception as err:
|
183
|
-
raise ValueError(
|
184
|
-
"Config `WORKFLOW_APP_STOP_BOUNDARY_DELTA` can not parsing to"
|
185
|
-
f"timedelta with {stop_boundary_delta_str}."
|
186
|
-
) from err
|
187
|
-
|
188
160
|
|
189
161
|
class APIConfig:
|
190
162
|
"""API Config object."""
|
@@ -193,14 +165,6 @@ class APIConfig:
|
|
193
165
|
def prefix_path(self) -> str:
|
194
166
|
return env("API_PREFIX_PATH", "/api/v1")
|
195
167
|
|
196
|
-
@property
|
197
|
-
def enable_route_workflow(self) -> bool:
|
198
|
-
return str2bool(env("API_ENABLE_ROUTE_WORKFLOW", "true"))
|
199
|
-
|
200
|
-
@property
|
201
|
-
def enable_route_schedule(self) -> bool:
|
202
|
-
return str2bool(env("API_ENABLE_ROUTE_SCHEDULE", "true"))
|
203
|
-
|
204
168
|
|
205
169
|
class BaseLoad(ABC): # pragma: no cov
|
206
170
|
"""Base Load object is the abstraction object for any Load object that
|
@@ -496,7 +460,7 @@ def pass_env(value: T) -> T: # pragma: no cov
|
|
496
460
|
return None if rs == "null" else rs
|
497
461
|
|
498
462
|
|
499
|
-
class
|
463
|
+
class CallerSecret(SecretStr): # pragma: no cov
|
500
464
|
"""Workflow Secret String model."""
|
501
465
|
|
502
466
|
def get_secret_value(self) -> str:
|
@@ -506,3 +470,7 @@ class WorkflowSecret(SecretStr): # pragma: no cov
|
|
506
470
|
:rtype: str
|
507
471
|
"""
|
508
472
|
return pass_env(super().get_secret_value())
|
473
|
+
|
474
|
+
|
475
|
+
# NOTE: Define the caller secret type for use it directly in the caller func.
|
476
|
+
CallerSecretType = TypeAdapter(CallerSecret)
|
@@ -11,6 +11,8 @@ from __future__ import annotations
|
|
11
11
|
|
12
12
|
from typing import Literal, Optional, TypedDict, Union, overload
|
13
13
|
|
14
|
+
from .__types import DictData, StrOrInt
|
15
|
+
|
14
16
|
|
15
17
|
class ErrorData(TypedDict):
|
16
18
|
"""Error data type dict for typing necessary keys of return of to_dict func
|
@@ -21,7 +23,7 @@ class ErrorData(TypedDict):
|
|
21
23
|
message: str
|
22
24
|
|
23
25
|
|
24
|
-
def to_dict(exception: Exception) -> ErrorData: # pragma: no cov
|
26
|
+
def to_dict(exception: Exception, **kwargs) -> ErrorData: # pragma: no cov
|
25
27
|
"""Create dict data from exception instance.
|
26
28
|
|
27
29
|
:param exception: An exception object.
|
@@ -31,17 +33,27 @@ def to_dict(exception: Exception) -> ErrorData: # pragma: no cov
|
|
31
33
|
return {
|
32
34
|
"name": exception.__class__.__name__,
|
33
35
|
"message": str(exception),
|
36
|
+
**kwargs,
|
34
37
|
}
|
35
38
|
|
36
39
|
|
37
|
-
class
|
40
|
+
class BaseError(Exception):
|
38
41
|
"""Base Workflow exception class will implement the `refs` argument for
|
39
42
|
making an error context to the result context.
|
40
43
|
"""
|
41
44
|
|
42
|
-
def __init__(
|
45
|
+
def __init__(
|
46
|
+
self,
|
47
|
+
message: str,
|
48
|
+
*,
|
49
|
+
refs: Optional[StrOrInt] = None,
|
50
|
+
context: Optional[DictData] = None,
|
51
|
+
params: Optional[DictData] = None,
|
52
|
+
) -> None:
|
43
53
|
super().__init__(message)
|
44
54
|
self.refs: Optional[str] = refs
|
55
|
+
self.context: DictData = context or {}
|
56
|
+
self.params: DictData = params or {}
|
45
57
|
|
46
58
|
@overload
|
47
59
|
def to_dict(
|
@@ -54,7 +66,9 @@ class BaseWorkflowException(Exception):
|
|
54
66
|
) -> ErrorData: ... # pragma: no cov
|
55
67
|
|
56
68
|
def to_dict(
|
57
|
-
self,
|
69
|
+
self,
|
70
|
+
with_refs: bool = False,
|
71
|
+
**kwargs,
|
58
72
|
) -> Union[ErrorData, dict[str, ErrorData]]:
|
59
73
|
"""Return ErrorData data from the current exception object. If with_refs
|
60
74
|
flag was set, it will return mapping of refs and itself data.
|
@@ -64,25 +78,46 @@ class BaseWorkflowException(Exception):
|
|
64
78
|
data: ErrorData = to_dict(self)
|
65
79
|
if with_refs and (self.refs is not None and self.refs != "EMPTY"):
|
66
80
|
return {self.refs: data}
|
67
|
-
return data
|
81
|
+
return data | kwargs
|
82
|
+
|
83
|
+
|
84
|
+
class UtilError(BaseError): ...
|
85
|
+
|
86
|
+
|
87
|
+
class ResultError(UtilError): ...
|
88
|
+
|
89
|
+
|
90
|
+
class StageError(BaseError): ...
|
91
|
+
|
92
|
+
|
93
|
+
class StageRetryError(StageError): ...
|
94
|
+
|
95
|
+
|
96
|
+
class StageCancelError(StageError): ...
|
97
|
+
|
98
|
+
|
99
|
+
class StageSkipError(StageError): ...
|
100
|
+
|
101
|
+
|
102
|
+
class JobError(BaseError): ...
|
68
103
|
|
69
104
|
|
70
|
-
class
|
105
|
+
class JobCancelError(JobError): ...
|
71
106
|
|
72
107
|
|
73
|
-
class
|
108
|
+
class JobSkipError(JobError): ...
|
74
109
|
|
75
110
|
|
76
|
-
class
|
111
|
+
class WorkflowError(BaseError): ...
|
77
112
|
|
78
113
|
|
79
|
-
class
|
114
|
+
class WorkflowCancelError(WorkflowError): ...
|
80
115
|
|
81
116
|
|
82
|
-
class
|
117
|
+
class WorkflowSkipError(WorkflowError): ...
|
83
118
|
|
84
119
|
|
85
|
-
class
|
120
|
+
class WorkflowTimeoutError(WorkflowError): ...
|
86
121
|
|
87
122
|
|
88
|
-
class
|
123
|
+
class ParamError(WorkflowError): ...
|