ddeutil-workflow 0.0.10__py3-none-any.whl → 0.0.11__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/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 .pipeline import Pipeline
15
- from .repeat import repeat_every
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
- @workflow.get(
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 pipeline workflows that exists in config path."""
36
- pipelines: DictData = Loader.finds(Pipeline)
42
+ """Return all workflow workflows that exists in config path."""
43
+ workflows: DictData = Loader.finds(Workflow)
37
44
  return {
38
- "message": f"getting all pipelines: {pipelines}",
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 pipeline that passing an input pipeline name."""
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
- pipeline: Pipeline = Pipeline.from_loader(name=name, externals={})
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 pipeline name: {name!r} does not found in /conf path"
83
+ f"Workflow workflow name: {name!r} does not found in /conf path"
56
84
  ),
57
85
  ) from None
58
- return pipeline.model_dump(
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 pipeline {name} logs"}
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 pipeline {name} log in release {release}"}
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"getting pipeline {name} log in release {release}"}
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
- @schedule.on_event("startup")
85
- @repeat_every(seconds=60)
86
- def schedule_broker_up():
87
- logger.info("Start listening schedule from queue ...")
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
- @schedule.get("/", response_class=UJSONResponse)
91
- async def get_jobs(request: Request):
92
- return {}
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
+ )