ddeutil-workflow 0.0.10__py3-none-any.whl → 0.0.12__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 +3 -2
- ddeutil/workflow/api.py +84 -16
- ddeutil/workflow/cli.py +14 -14
- ddeutil/workflow/exceptions.py +6 -6
- ddeutil/workflow/job.py +572 -0
- ddeutil/workflow/log.py +10 -10
- ddeutil/workflow/repeat.py +4 -2
- ddeutil/workflow/route.py +165 -36
- ddeutil/workflow/scheduler.py +733 -110
- ddeutil/workflow/stage.py +12 -12
- ddeutil/workflow/utils.py +4 -4
- {ddeutil_workflow-0.0.10.dist-info → ddeutil_workflow-0.0.12.dist-info}/METADATA +66 -70
- ddeutil_workflow-0.0.12.dist-info/RECORD +21 -0
- {ddeutil_workflow-0.0.10.dist-info → ddeutil_workflow-0.0.12.dist-info}/WHEEL +1 -1
- ddeutil/workflow/pipeline.py +0 -1186
- ddeutil_workflow-0.0.10.dist-info/RECORD +0 -21
- {ddeutil_workflow-0.0.10.dist-info → ddeutil_workflow-0.0.12.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.10.dist-info → ddeutil_workflow-0.0.12.dist-info}/entry_points.txt +0 -0
- {ddeutil_workflow-0.0.10.dist-info → ddeutil_workflow-0.0.12.dist-info}/top_level.txt +0 -0
ddeutil/workflow/route.py
CHANGED
@@ -5,57 +5,89 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
|
+
import copy
|
9
|
+
import os
|
10
|
+
from datetime import datetime, timedelta
|
11
|
+
from typing import Any
|
12
|
+
from zoneinfo import ZoneInfo
|
13
|
+
|
8
14
|
from fastapi import APIRouter, HTTPException, Request
|
9
15
|
from fastapi import status as st
|
10
16
|
from fastapi.responses import UJSONResponse
|
17
|
+
from pydantic import BaseModel
|
11
18
|
|
19
|
+
from . import Workflow
|
12
20
|
from .__types import DictData
|
13
21
|
from .log import get_logger
|
14
|
-
from .
|
15
|
-
from .
|
16
|
-
from .utils import Loader
|
22
|
+
from .scheduler import Schedule
|
23
|
+
from .utils import Loader, Result
|
17
24
|
|
18
25
|
logger = get_logger("ddeutil.workflow")
|
19
26
|
workflow = APIRouter(
|
20
|
-
prefix="/workflow",
|
27
|
+
prefix="/api/workflow",
|
21
28
|
tags=["workflow"],
|
29
|
+
default_response_class=UJSONResponse,
|
22
30
|
)
|
23
31
|
schedule = APIRouter(
|
24
|
-
prefix="/schedule",
|
32
|
+
prefix="/api/schedule",
|
25
33
|
tags=["schedule"],
|
34
|
+
default_response_class=UJSONResponse,
|
26
35
|
)
|
27
36
|
|
37
|
+
ListDate = list[datetime]
|
28
38
|
|
29
|
-
|
30
|
-
|
31
|
-
response_class=UJSONResponse,
|
32
|
-
status_code=st.HTTP_200_OK,
|
33
|
-
)
|
39
|
+
|
40
|
+
@workflow.get("/")
|
34
41
|
async def get_workflows():
|
35
|
-
"""Return all
|
36
|
-
|
42
|
+
"""Return all workflow workflows that exists in config path."""
|
43
|
+
workflows: DictData = Loader.finds(Workflow)
|
37
44
|
return {
|
38
|
-
"message": f"getting all
|
45
|
+
"message": f"getting all workflows: {workflows}",
|
39
46
|
}
|
40
47
|
|
41
48
|
|
42
|
-
@workflow.get(
|
43
|
-
"/{name}",
|
44
|
-
response_class=UJSONResponse,
|
45
|
-
status_code=st.HTTP_200_OK,
|
46
|
-
)
|
49
|
+
@workflow.get("/{name}")
|
47
50
|
async def get_workflow(name: str) -> DictData:
|
48
|
-
"""Return model of
|
51
|
+
"""Return model of workflow that passing an input workflow name."""
|
52
|
+
try:
|
53
|
+
wf: Workflow = Workflow.from_loader(name=name, externals={})
|
54
|
+
except ValueError as err:
|
55
|
+
logger.exception(err)
|
56
|
+
raise HTTPException(
|
57
|
+
status_code=st.HTTP_404_NOT_FOUND,
|
58
|
+
detail=(
|
59
|
+
f"Workflow workflow name: {name!r} does not found in /conf path"
|
60
|
+
),
|
61
|
+
) from None
|
62
|
+
return wf.model_dump(
|
63
|
+
by_alias=True,
|
64
|
+
exclude_none=True,
|
65
|
+
exclude_unset=True,
|
66
|
+
exclude_defaults=True,
|
67
|
+
)
|
68
|
+
|
69
|
+
|
70
|
+
class ExecutePayload(BaseModel):
|
71
|
+
params: dict[str, Any]
|
72
|
+
|
73
|
+
|
74
|
+
@workflow.post("/{name}/execute", status_code=st.HTTP_202_ACCEPTED)
|
75
|
+
async def execute_workflow(name: str, payload: ExecutePayload) -> DictData:
|
76
|
+
"""Return model of workflow that passing an input workflow name."""
|
49
77
|
try:
|
50
|
-
|
78
|
+
wf: Workflow = Workflow.from_loader(name=name, externals={})
|
51
79
|
except ValueError:
|
52
80
|
raise HTTPException(
|
53
81
|
status_code=st.HTTP_404_NOT_FOUND,
|
54
82
|
detail=(
|
55
|
-
f"Workflow
|
83
|
+
f"Workflow workflow name: {name!r} does not found in /conf path"
|
56
84
|
),
|
57
85
|
) from None
|
58
|
-
|
86
|
+
|
87
|
+
# NOTE: Start execute manually
|
88
|
+
rs: Result = wf.execute(params=payload.params)
|
89
|
+
|
90
|
+
return rs.model_dump(
|
59
91
|
by_alias=True,
|
60
92
|
exclude_none=True,
|
61
93
|
exclude_unset=True,
|
@@ -65,28 +97,125 @@ async def get_workflow(name: str) -> DictData:
|
|
65
97
|
|
66
98
|
@workflow.get("/{name}/logs")
|
67
99
|
async def get_workflow_logs(name: str):
|
68
|
-
return {"message": f"getting
|
100
|
+
return {"message": f"getting workflow {name!r} logs"}
|
69
101
|
|
70
102
|
|
71
103
|
@workflow.get("/{name}/logs/{release}")
|
72
104
|
async def get_workflow_release_log(name: str, release: str):
|
73
|
-
return {"message": f"getting
|
105
|
+
return {"message": f"getting workflow {name!r} log in release {release}"}
|
74
106
|
|
75
107
|
|
76
|
-
@workflow.delete(
|
77
|
-
"/{name}/logs/{release}",
|
78
|
-
status_code=st.HTTP_204_NO_CONTENT,
|
79
|
-
)
|
108
|
+
@workflow.delete("/{name}/logs/{release}", status_code=st.HTTP_204_NO_CONTENT)
|
80
109
|
async def del_workflow_release_log(name: str, release: str):
|
81
|
-
return {"message": f"
|
110
|
+
return {"message": f"deleted workflow {name!r} log in release {release}"}
|
111
|
+
|
112
|
+
|
113
|
+
@schedule.get("/{name}")
|
114
|
+
async def get_schedule(name: str):
|
115
|
+
try:
|
116
|
+
sch: Schedule = Schedule.from_loader(name=name, externals={})
|
117
|
+
except ValueError:
|
118
|
+
raise HTTPException(
|
119
|
+
status_code=st.HTTP_404_NOT_FOUND,
|
120
|
+
detail=f"Schedule name: {name!r} does not found in /conf path",
|
121
|
+
) from None
|
122
|
+
return sch.model_dump(
|
123
|
+
by_alias=True,
|
124
|
+
exclude_none=True,
|
125
|
+
exclude_unset=True,
|
126
|
+
exclude_defaults=True,
|
127
|
+
)
|
128
|
+
|
129
|
+
|
130
|
+
@schedule.get("/deploy")
|
131
|
+
async def get_deploy_schedulers(request: Request):
|
132
|
+
snapshot = copy.deepcopy(request.state.scheduler)
|
133
|
+
return {"schedule": snapshot}
|
134
|
+
|
135
|
+
|
136
|
+
@schedule.get("/deploy/{name}")
|
137
|
+
async def get_deploy_scheduler(request: Request, name: str):
|
138
|
+
if name in request.state.scheduler:
|
139
|
+
sch = Schedule.from_loader(name)
|
140
|
+
getter: list[dict[str, dict[str, list[datetime]]]] = []
|
141
|
+
for wf in sch.workflows:
|
142
|
+
getter.append(
|
143
|
+
{
|
144
|
+
wf.name: {
|
145
|
+
"queue": copy.deepcopy(
|
146
|
+
request.state.workflow_queue[wf.name]
|
147
|
+
),
|
148
|
+
"running": copy.deepcopy(
|
149
|
+
request.state.workflow_running[wf.name]
|
150
|
+
),
|
151
|
+
}
|
152
|
+
}
|
153
|
+
)
|
154
|
+
return {
|
155
|
+
"message": f"getting {name!r} to schedule listener.",
|
156
|
+
"scheduler": getter,
|
157
|
+
}
|
158
|
+
raise HTTPException(
|
159
|
+
status_code=st.HTTP_404_NOT_FOUND,
|
160
|
+
detail=f"Does not found {name!r} in schedule listener",
|
161
|
+
)
|
162
|
+
|
163
|
+
|
164
|
+
@schedule.post("/deploy/{name}")
|
165
|
+
async def add_deploy_scheduler(request: Request, name: str):
|
166
|
+
"""Adding schedule name to application state store."""
|
167
|
+
if name in request.state.scheduler:
|
168
|
+
raise HTTPException(
|
169
|
+
status_code=st.HTTP_302_FOUND,
|
170
|
+
detail="This schedule already exists in scheduler list.",
|
171
|
+
)
|
82
172
|
|
173
|
+
request.state.scheduler.append(name)
|
83
174
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
175
|
+
tz: ZoneInfo = ZoneInfo(os.getenv("WORKFLOW_CORE_TIMEZONE", "UTC"))
|
176
|
+
start_date: datetime = datetime.now(tz=tz)
|
177
|
+
start_date_waiting: datetime = (start_date + timedelta(minutes=1)).replace(
|
178
|
+
second=0, microsecond=0
|
179
|
+
)
|
88
180
|
|
181
|
+
# NOTE: Create pair of workflow and on from schedule model.
|
182
|
+
try:
|
183
|
+
sch = Schedule.from_loader(name)
|
184
|
+
except ValueError as e:
|
185
|
+
request.state.scheduler.remove(name)
|
186
|
+
logger.exception(e)
|
187
|
+
raise HTTPException(
|
188
|
+
status_code=st.HTTP_404_NOT_FOUND,
|
189
|
+
detail=str(e),
|
190
|
+
) from None
|
191
|
+
request.state.workflow_tasks.extend(
|
192
|
+
sch.tasks(
|
193
|
+
start_date_waiting,
|
194
|
+
queue=request.state.workflow_queue,
|
195
|
+
running=request.state.workflow_running,
|
196
|
+
),
|
197
|
+
)
|
198
|
+
return {"message": f"adding {name!r} to schedule listener."}
|
89
199
|
|
90
|
-
|
91
|
-
|
92
|
-
|
200
|
+
|
201
|
+
@schedule.delete("/deploy/{name}")
|
202
|
+
async def del_deploy_scheduler(request: Request, name: str):
|
203
|
+
if name in request.state.scheduler:
|
204
|
+
request.state.scheduler.remove(name)
|
205
|
+
sche = Schedule.from_loader(name)
|
206
|
+
for workflow_task in sche.tasks(datetime.now(), {}, {}):
|
207
|
+
request.state.workflow_tasks.remove(workflow_task)
|
208
|
+
|
209
|
+
for wf in sche.workflows:
|
210
|
+
if wf in request.state.workflow_queue:
|
211
|
+
request.state.workflow_queue.pop(wf, {})
|
212
|
+
|
213
|
+
if wf in request.state.workflow_running:
|
214
|
+
request.state.workflow_running.pop(wf, {})
|
215
|
+
|
216
|
+
return {"message": f"deleted {name!r} to schedule listener."}
|
217
|
+
|
218
|
+
raise HTTPException(
|
219
|
+
status_code=st.HTTP_404_NOT_FOUND,
|
220
|
+
detail=f"Does not found {name!r} in schedule listener",
|
221
|
+
)
|