ddeutil-workflow 0.0.34__py3-none-any.whl → 0.0.36__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 +8 -3
- ddeutil/workflow/api/api.py +58 -14
- ddeutil/workflow/api/repeat.py +21 -11
- ddeutil/workflow/api/routes/__init__.py +9 -0
- ddeutil/workflow/api/routes/job.py +73 -0
- ddeutil/workflow/api/routes/logs.py +64 -0
- ddeutil/workflow/api/{route.py → routes/schedules.py} +3 -131
- ddeutil/workflow/api/routes/workflows.py +137 -0
- ddeutil/workflow/audit.py +9 -6
- ddeutil/workflow/{call.py → caller.py} +4 -4
- ddeutil/workflow/job.py +63 -24
- ddeutil/workflow/logs.py +326 -0
- ddeutil/workflow/params.py +87 -22
- ddeutil/workflow/result.py +17 -141
- ddeutil/workflow/scheduler.py +69 -41
- ddeutil/workflow/stages.py +68 -14
- ddeutil/workflow/utils.py +7 -1
- ddeutil/workflow/workflow.py +2 -16
- {ddeutil_workflow-0.0.34.dist-info → ddeutil_workflow-0.0.36.dist-info}/METADATA +32 -27
- ddeutil_workflow-0.0.36.dist-info/RECORD +31 -0
- {ddeutil_workflow-0.0.34.dist-info → ddeutil_workflow-0.0.36.dist-info}/WHEEL +1 -1
- ddeutil_workflow-0.0.34.dist-info/RECORD +0 -26
- {ddeutil_workflow-0.0.34.dist-info → ddeutil_workflow-0.0.36.dist-info}/LICENSE +0 -0
- {ddeutil_workflow-0.0.34.dist-info → ddeutil_workflow-0.0.36.dist-info}/top_level.txt +0 -0
ddeutil/workflow/__about__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__: str = "0.0.
|
1
|
+
__version__: str = "0.0.36"
|
ddeutil/workflow/__init__.py
CHANGED
@@ -9,7 +9,7 @@ from .audit import (
|
|
9
9
|
Audit,
|
10
10
|
get_audit,
|
11
11
|
)
|
12
|
-
from .
|
12
|
+
from .caller import (
|
13
13
|
ReturnTagFunc,
|
14
14
|
TagFunc,
|
15
15
|
extract_call,
|
@@ -37,8 +37,15 @@ from .exceptions import (
|
|
37
37
|
)
|
38
38
|
from .job import (
|
39
39
|
Job,
|
40
|
+
RunsOn,
|
40
41
|
Strategy,
|
41
42
|
)
|
43
|
+
from .logs import (
|
44
|
+
TraceData,
|
45
|
+
TraceLog,
|
46
|
+
get_dt_tznow,
|
47
|
+
get_trace,
|
48
|
+
)
|
42
49
|
from .params import (
|
43
50
|
ChoiceParam,
|
44
51
|
DatetimeParam,
|
@@ -49,9 +56,7 @@ from .params import (
|
|
49
56
|
from .result import (
|
50
57
|
Result,
|
51
58
|
Status,
|
52
|
-
TraceLog,
|
53
59
|
default_gen_id,
|
54
|
-
get_dt_tznow,
|
55
60
|
)
|
56
61
|
from .scheduler import (
|
57
62
|
Schedule,
|
ddeutil/workflow/api/api.py
CHANGED
@@ -11,7 +11,11 @@ from datetime import datetime, timedelta
|
|
11
11
|
from typing import TypedDict
|
12
12
|
|
13
13
|
from dotenv import load_dotenv
|
14
|
-
from fastapi import FastAPI
|
14
|
+
from fastapi import FastAPI, Request
|
15
|
+
from fastapi import status as st
|
16
|
+
from fastapi.encoders import jsonable_encoder
|
17
|
+
from fastapi.exceptions import RequestValidationError
|
18
|
+
from fastapi.middleware.cors import CORSMiddleware
|
15
19
|
from fastapi.middleware.gzip import GZipMiddleware
|
16
20
|
from fastapi.responses import UJSONResponse
|
17
21
|
|
@@ -20,6 +24,7 @@ from ..conf import config, get_logger
|
|
20
24
|
from ..scheduler import ReleaseThread, ReleaseThreads
|
21
25
|
from ..workflow import ReleaseQueue, WorkflowTask
|
22
26
|
from .repeat import repeat_at
|
27
|
+
from .routes import job, log
|
23
28
|
|
24
29
|
load_dotenv()
|
25
30
|
logger = get_logger("ddeutil.workflow")
|
@@ -60,39 +65,57 @@ async def lifespan(a: FastAPI) -> AsyncIterator[State]:
|
|
60
65
|
|
61
66
|
|
62
67
|
app = FastAPI(
|
63
|
-
titile="Workflow
|
68
|
+
titile="Workflow",
|
64
69
|
description=(
|
65
|
-
"This is workflow FastAPI
|
66
|
-
"execute
|
70
|
+
"This is a workflow FastAPI application that use to manage manual "
|
71
|
+
"execute, logging, and schedule workflow via RestAPI."
|
67
72
|
),
|
68
73
|
version=__version__,
|
69
74
|
lifespan=lifespan,
|
70
75
|
default_response_class=UJSONResponse,
|
71
76
|
)
|
72
77
|
app.add_middleware(GZipMiddleware, minimum_size=1000)
|
78
|
+
origins: list[str] = [
|
79
|
+
"http://localhost",
|
80
|
+
"http://localhost:88",
|
81
|
+
"http://localhost:80",
|
82
|
+
]
|
83
|
+
app.add_middleware(
|
84
|
+
CORSMiddleware,
|
85
|
+
allow_origins=origins,
|
86
|
+
allow_credentials=True,
|
87
|
+
allow_methods=["*"],
|
88
|
+
allow_headers=["*"],
|
89
|
+
)
|
73
90
|
|
74
91
|
|
75
92
|
@app.get("/")
|
76
93
|
async def health():
|
77
|
-
|
94
|
+
"""Index view that not return any template without json status."""
|
95
|
+
return {"message": "Workflow already start up with healthy status."}
|
96
|
+
|
78
97
|
|
98
|
+
# NOTE Add the jobs and logs routes by default.
|
99
|
+
app.include_router(job, prefix=config.prefix_path)
|
100
|
+
app.include_router(log, prefix=config.prefix_path)
|
79
101
|
|
80
|
-
|
102
|
+
|
103
|
+
# NOTE: Enable the workflows route.
|
81
104
|
if config.enable_route_workflow:
|
82
|
-
from .
|
105
|
+
from .routes import workflow
|
83
106
|
|
84
|
-
app.include_router(
|
107
|
+
app.include_router(workflow, prefix=config.prefix_path)
|
85
108
|
|
86
109
|
|
87
|
-
# NOTE: Enable the
|
110
|
+
# NOTE: Enable the schedules route.
|
88
111
|
if config.enable_route_schedule:
|
89
112
|
from ..audit import get_audit
|
90
113
|
from ..scheduler import schedule_task
|
91
|
-
from .
|
114
|
+
from .routes import schedule
|
92
115
|
|
93
|
-
app.include_router(
|
116
|
+
app.include_router(schedule, prefix=config.prefix_path)
|
94
117
|
|
95
|
-
@
|
118
|
+
@schedule.on_event("startup")
|
96
119
|
@repeat_at(cron="* * * * *", delay=2)
|
97
120
|
def scheduler_listener():
|
98
121
|
"""Schedule broker every minute at 02 second."""
|
@@ -106,12 +129,13 @@ if config.enable_route_schedule:
|
|
106
129
|
stop=datetime.now(config.tz) + timedelta(minutes=1),
|
107
130
|
queue=app.state.workflow_queue,
|
108
131
|
threads=app.state.workflow_threads,
|
109
|
-
|
132
|
+
audit=get_audit(),
|
110
133
|
)
|
111
134
|
|
112
|
-
@
|
135
|
+
@schedule.on_event("startup")
|
113
136
|
@repeat_at(cron="*/5 * * * *", delay=10)
|
114
137
|
def monitoring():
|
138
|
+
"""Monitoring workflow thread that running in the background."""
|
115
139
|
logger.debug("[MONITOR]: Start monitoring threading.")
|
116
140
|
snapshot_threads: list[str] = list(app.state.workflow_threads.keys())
|
117
141
|
for t_name in snapshot_threads:
|
@@ -121,3 +145,23 @@ if config.enable_route_schedule:
|
|
121
145
|
# NOTE: remove the thread that running success.
|
122
146
|
if not thread_release["thread"].is_alive():
|
123
147
|
app.state.workflow_threads.pop(t_name)
|
148
|
+
|
149
|
+
|
150
|
+
@app.exception_handler(RequestValidationError)
|
151
|
+
async def validation_exception_handler(
|
152
|
+
request: Request, exc: RequestValidationError
|
153
|
+
):
|
154
|
+
return UJSONResponse(
|
155
|
+
status_code=st.HTTP_422_UNPROCESSABLE_ENTITY,
|
156
|
+
content=jsonable_encoder({"detail": exc.errors(), "body": exc.body}),
|
157
|
+
)
|
158
|
+
|
159
|
+
|
160
|
+
if __name__ == "__main__":
|
161
|
+
import uvicorn
|
162
|
+
|
163
|
+
uvicorn.run(
|
164
|
+
app,
|
165
|
+
host="0.0.0.0",
|
166
|
+
port=80,
|
167
|
+
)
|
ddeutil/workflow/api/repeat.py
CHANGED
@@ -21,17 +21,26 @@ logger = get_logger("ddeutil.workflow")
|
|
21
21
|
def get_cronjob_delta(cron: str) -> float:
|
22
22
|
"""This function returns the time delta between now and the next cron
|
23
23
|
execution time.
|
24
|
+
|
25
|
+
:rtype: float
|
24
26
|
"""
|
25
27
|
now: datetime = datetime.now(tz=config.tz)
|
26
28
|
cron = CronJob(cron)
|
27
29
|
return (cron.schedule(now).next - now).total_seconds()
|
28
30
|
|
29
31
|
|
30
|
-
def cron_valid(cron: str):
|
32
|
+
def cron_valid(cron: str, raise_error: bool = True) -> bool:
|
33
|
+
"""Check this crontab string value is valid with its cron syntax.
|
34
|
+
|
35
|
+
:rtype: bool
|
36
|
+
"""
|
31
37
|
try:
|
32
38
|
CronJob(cron)
|
39
|
+
return True
|
33
40
|
except Exception as err:
|
34
|
-
|
41
|
+
if raise_error:
|
42
|
+
raise ValueError(f"Crontab value does not valid, {cron}") from err
|
43
|
+
return False
|
35
44
|
|
36
45
|
|
37
46
|
async def run_func(
|
@@ -41,6 +50,7 @@ async def run_func(
|
|
41
50
|
raise_exceptions: bool = False,
|
42
51
|
**kwargs,
|
43
52
|
):
|
53
|
+
"""Run function inside the repeat decorator functions."""
|
44
54
|
try:
|
45
55
|
if is_coroutine:
|
46
56
|
await func(*args, **kwargs)
|
@@ -62,11 +72,11 @@ def repeat_at(
|
|
62
72
|
"""This function returns a decorator that makes a function execute
|
63
73
|
periodically as per the cron expression provided.
|
64
74
|
|
65
|
-
:param cron: str
|
66
|
-
|
67
|
-
:param delay:
|
68
|
-
:param raise_exceptions: bool
|
69
|
-
|
75
|
+
:param cron: (str) A Cron-style string for periodic execution, e.g.
|
76
|
+
'0 0 * * *' every midnight
|
77
|
+
:param delay: (float) A delay seconds value.
|
78
|
+
:param raise_exceptions: (bool) A raise exception flag. Whether to raise
|
79
|
+
exceptions or log them if raise was set be false.
|
70
80
|
:param max_repetitions: int (default None)
|
71
81
|
Maximum number of times to repeat the function. If None, repeat
|
72
82
|
indefinitely.
|
@@ -81,12 +91,12 @@ def repeat_at(
|
|
81
91
|
|
82
92
|
@wraps(func)
|
83
93
|
def wrapper(*_args, **_kwargs):
|
84
|
-
|
94
|
+
repetitions: int = 0
|
85
95
|
cron_valid(cron)
|
86
96
|
|
87
97
|
async def loop(*args, **kwargs):
|
88
|
-
nonlocal
|
89
|
-
while max_repetitions is None or
|
98
|
+
nonlocal repetitions
|
99
|
+
while max_repetitions is None or repetitions < max_repetitions:
|
90
100
|
sleep_time = get_cronjob_delta(cron) + delay
|
91
101
|
await asyncio.sleep(sleep_time)
|
92
102
|
await run_func(
|
@@ -96,7 +106,7 @@ def repeat_at(
|
|
96
106
|
raise_exceptions=raise_exceptions,
|
97
107
|
**kwargs,
|
98
108
|
)
|
99
|
-
|
109
|
+
repetitions += 1
|
100
110
|
|
101
111
|
ensure_future(loop(*_args, **_kwargs))
|
102
112
|
|
@@ -0,0 +1,9 @@
|
|
1
|
+
# ------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2022 Korawich Anuttra. All rights reserved.
|
3
|
+
# Licensed under the MIT License. See LICENSE in the project root for
|
4
|
+
# license information.
|
5
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from .job import job_route as job
|
7
|
+
from .logs import log_route as log
|
8
|
+
from .schedules import schedule_route as schedule
|
9
|
+
from .workflows import workflow_route as workflow
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# ------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2022 Korawich Anuttra. All rights reserved.
|
3
|
+
# Licensed under the MIT License. See LICENSE in the project root for
|
4
|
+
# license information.
|
5
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
from typing import Any, Optional
|
9
|
+
|
10
|
+
from fastapi import APIRouter
|
11
|
+
from fastapi.responses import UJSONResponse
|
12
|
+
from pydantic import BaseModel
|
13
|
+
|
14
|
+
from ...__types import DictData
|
15
|
+
from ...conf import get_logger
|
16
|
+
from ...exceptions import JobException
|
17
|
+
from ...job import Job
|
18
|
+
from ...result import Result
|
19
|
+
|
20
|
+
logger = get_logger("ddeutil.workflow")
|
21
|
+
|
22
|
+
|
23
|
+
job_route = APIRouter(
|
24
|
+
prefix="/job",
|
25
|
+
tags=["job"],
|
26
|
+
default_response_class=UJSONResponse,
|
27
|
+
)
|
28
|
+
|
29
|
+
|
30
|
+
class ResultPost(BaseModel):
|
31
|
+
context: DictData
|
32
|
+
run_id: str
|
33
|
+
parent_run_id: Optional[str] = None
|
34
|
+
|
35
|
+
|
36
|
+
@job_route.post(path="/execute/")
|
37
|
+
async def job_execute(
|
38
|
+
result: ResultPost,
|
39
|
+
job: Job,
|
40
|
+
params: dict[str, Any],
|
41
|
+
):
|
42
|
+
"""Execute job via API."""
|
43
|
+
rs: Result = Result(
|
44
|
+
context=result.context,
|
45
|
+
run_id=result.run_id,
|
46
|
+
parent_run_id=result.parent_run_id,
|
47
|
+
)
|
48
|
+
try:
|
49
|
+
job.set_outputs(
|
50
|
+
job.execute(
|
51
|
+
params=params,
|
52
|
+
run_id=rs.run_id,
|
53
|
+
parent_run_id=rs.parent_run_id,
|
54
|
+
).context,
|
55
|
+
to=params,
|
56
|
+
)
|
57
|
+
except JobException as err:
|
58
|
+
rs.trace.error(f"[WORKFLOW]: {err.__class__.__name__}: {err}")
|
59
|
+
|
60
|
+
return {
|
61
|
+
"message": "Start execute job via API.",
|
62
|
+
"result": {
|
63
|
+
"run_id": rs.run_id,
|
64
|
+
"parent_run_id": rs.parent_run_id,
|
65
|
+
},
|
66
|
+
"job": job.model_dump(
|
67
|
+
by_alias=True,
|
68
|
+
exclude_none=True,
|
69
|
+
exclude_unset=True,
|
70
|
+
exclude_defaults=True,
|
71
|
+
),
|
72
|
+
"params": params,
|
73
|
+
}
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# ------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2022 Korawich Anuttra. All rights reserved.
|
3
|
+
# Licensed under the MIT License. See LICENSE in the project root for
|
4
|
+
# license information.
|
5
|
+
# ------------------------------------------------------------------------------
|
6
|
+
"""This route include audit and trace log paths."""
|
7
|
+
from __future__ import annotations
|
8
|
+
|
9
|
+
from fastapi import APIRouter
|
10
|
+
from fastapi.responses import UJSONResponse
|
11
|
+
|
12
|
+
from ...audit import get_audit
|
13
|
+
from ...logs import get_trace_obj
|
14
|
+
|
15
|
+
log_route = APIRouter(
|
16
|
+
prefix="/logs",
|
17
|
+
tags=["logs", "trace", "audit"],
|
18
|
+
default_response_class=UJSONResponse,
|
19
|
+
)
|
20
|
+
|
21
|
+
|
22
|
+
@log_route.get(path="/trace/")
|
23
|
+
async def get_traces():
|
24
|
+
"""Get all trace logs."""
|
25
|
+
return {
|
26
|
+
"message": "Getting trace logs",
|
27
|
+
"traces": list(get_trace_obj().find_logs()),
|
28
|
+
}
|
29
|
+
|
30
|
+
|
31
|
+
@log_route.get(path="/trace/{run_id}")
|
32
|
+
async def get_trace_with_id(run_id: str):
|
33
|
+
"""Get trace log with specific running ID."""
|
34
|
+
return get_trace_obj().find_log_with_id(run_id)
|
35
|
+
|
36
|
+
|
37
|
+
@log_route.get(path="/audit/")
|
38
|
+
async def get_audits():
|
39
|
+
"""Get all audit logs."""
|
40
|
+
return {
|
41
|
+
"message": "Getting audit logs",
|
42
|
+
"audits": list(get_audit().find_audits(name="demo")),
|
43
|
+
}
|
44
|
+
|
45
|
+
|
46
|
+
@log_route.get(path="/audit/{workflow}/")
|
47
|
+
async def get_audit_with_workflow(workflow: str):
|
48
|
+
"""Get all audit logs."""
|
49
|
+
return {
|
50
|
+
"message": f"Getting audit logs with workflow name {workflow}",
|
51
|
+
"audits": list(get_audit().find_audits(name="demo")),
|
52
|
+
}
|
53
|
+
|
54
|
+
|
55
|
+
@log_route.get(path="/audit/{workflow}/{release}")
|
56
|
+
async def get_audit_with_workflow_release(workflow: str, release: str):
|
57
|
+
"""Get all audit logs."""
|
58
|
+
return {
|
59
|
+
"message": (
|
60
|
+
f"Getting audit logs with workflow name {workflow} and release "
|
61
|
+
f"{release}"
|
62
|
+
),
|
63
|
+
"audits": list(get_audit().find_audits(name="demo")),
|
64
|
+
}
|
@@ -6,30 +6,17 @@
|
|
6
6
|
from __future__ import annotations
|
7
7
|
|
8
8
|
import copy
|
9
|
-
from dataclasses import asdict
|
10
9
|
from datetime import datetime, timedelta
|
11
|
-
from typing import Any
|
12
10
|
|
13
11
|
from fastapi import APIRouter, HTTPException, Request
|
14
12
|
from fastapi import status as st
|
15
13
|
from fastapi.responses import UJSONResponse
|
16
|
-
from pydantic import BaseModel
|
17
14
|
|
18
|
-
from
|
19
|
-
from
|
20
|
-
from ..conf import Loader, config, get_logger
|
21
|
-
from ..result import Result
|
22
|
-
from ..scheduler import Schedule
|
23
|
-
from ..workflow import Workflow
|
15
|
+
from ...conf import config, get_logger
|
16
|
+
from ...scheduler import Schedule
|
24
17
|
|
25
18
|
logger = get_logger("ddeutil.workflow")
|
26
19
|
|
27
|
-
workflow_route = APIRouter(
|
28
|
-
prefix="/workflows",
|
29
|
-
tags=["workflows"],
|
30
|
-
default_response_class=UJSONResponse,
|
31
|
-
)
|
32
|
-
|
33
20
|
schedule_route = APIRouter(
|
34
21
|
prefix="/schedules",
|
35
22
|
tags=["schedules"],
|
@@ -37,124 +24,9 @@ schedule_route = APIRouter(
|
|
37
24
|
)
|
38
25
|
|
39
26
|
|
40
|
-
@workflow_route.get(path="/")
|
41
|
-
async def get_workflows() -> DictData:
|
42
|
-
"""Return all workflow workflows that exists in config path."""
|
43
|
-
workflows: DictData = dict(Loader.finds(Workflow))
|
44
|
-
return {
|
45
|
-
"message": f"Getting all workflows: {len(workflows)}",
|
46
|
-
"count": len(workflows),
|
47
|
-
"workflows": workflows,
|
48
|
-
}
|
49
|
-
|
50
|
-
|
51
|
-
@workflow_route.get(path="/{name}")
|
52
|
-
async def get_workflow_by_name(name: str) -> DictData:
|
53
|
-
"""Return model of workflow that passing an input workflow name."""
|
54
|
-
try:
|
55
|
-
workflow: Workflow = Workflow.from_loader(name=name, externals={})
|
56
|
-
except ValueError as err:
|
57
|
-
logger.exception(err)
|
58
|
-
raise HTTPException(
|
59
|
-
status_code=st.HTTP_404_NOT_FOUND,
|
60
|
-
detail=(
|
61
|
-
f"Workflow workflow name: {name!r} does not found in /conf path"
|
62
|
-
),
|
63
|
-
) from None
|
64
|
-
return workflow.model_dump(
|
65
|
-
by_alias=True,
|
66
|
-
exclude_none=True,
|
67
|
-
exclude_unset=True,
|
68
|
-
exclude_defaults=True,
|
69
|
-
)
|
70
|
-
|
71
|
-
|
72
|
-
class ExecutePayload(BaseModel):
|
73
|
-
params: dict[str, Any]
|
74
|
-
|
75
|
-
|
76
|
-
@workflow_route.post(path="/{name}/execute", status_code=st.HTTP_202_ACCEPTED)
|
77
|
-
async def execute_workflow(name: str, payload: ExecutePayload) -> DictData:
|
78
|
-
"""Return model of workflow that passing an input workflow name."""
|
79
|
-
try:
|
80
|
-
workflow: Workflow = Workflow.from_loader(name=name, externals={})
|
81
|
-
except ValueError:
|
82
|
-
raise HTTPException(
|
83
|
-
status_code=st.HTTP_404_NOT_FOUND,
|
84
|
-
detail=(
|
85
|
-
f"Workflow workflow name: {name!r} does not found in /conf path"
|
86
|
-
),
|
87
|
-
) from None
|
88
|
-
|
89
|
-
# NOTE: Start execute manually
|
90
|
-
try:
|
91
|
-
result: Result = workflow.execute(params=payload.params)
|
92
|
-
except Exception as err:
|
93
|
-
raise HTTPException(
|
94
|
-
status_code=st.HTTP_500_INTERNAL_SERVER_ERROR,
|
95
|
-
detail=f"{type(err)}: {err}",
|
96
|
-
) from None
|
97
|
-
|
98
|
-
return asdict(result)
|
99
|
-
|
100
|
-
|
101
|
-
@workflow_route.get(path="/{name}/logs")
|
102
|
-
async def get_workflow_logs(name: str):
|
103
|
-
try:
|
104
|
-
return {
|
105
|
-
"message": f"Getting workflow {name!r} logs",
|
106
|
-
"logs": [
|
107
|
-
log.model_dump(
|
108
|
-
by_alias=True,
|
109
|
-
exclude_none=True,
|
110
|
-
exclude_unset=True,
|
111
|
-
exclude_defaults=True,
|
112
|
-
)
|
113
|
-
for log in get_audit().find_audits(name=name)
|
114
|
-
],
|
115
|
-
}
|
116
|
-
except FileNotFoundError:
|
117
|
-
raise HTTPException(
|
118
|
-
status_code=st.HTTP_404_NOT_FOUND,
|
119
|
-
detail=f"Does not found log for workflow {name!r}",
|
120
|
-
) from None
|
121
|
-
|
122
|
-
|
123
|
-
@workflow_route.get(path="/{name}/logs/{release}")
|
124
|
-
async def get_workflow_release_log(name: str, release: str):
|
125
|
-
try:
|
126
|
-
log: Audit = get_audit().find_audit_with_release(
|
127
|
-
name=name, release=datetime.strptime(release, "%Y%m%d%H%M%S")
|
128
|
-
)
|
129
|
-
except FileNotFoundError:
|
130
|
-
raise HTTPException(
|
131
|
-
status_code=st.HTTP_404_NOT_FOUND,
|
132
|
-
detail=(
|
133
|
-
f"Does not found log for workflow {name!r} "
|
134
|
-
f"with release {release!r}"
|
135
|
-
),
|
136
|
-
) from None
|
137
|
-
return {
|
138
|
-
"message": f"Getting workflow {name!r} log in release {release}",
|
139
|
-
"log": log.model_dump(
|
140
|
-
by_alias=True,
|
141
|
-
exclude_none=True,
|
142
|
-
exclude_unset=True,
|
143
|
-
exclude_defaults=True,
|
144
|
-
),
|
145
|
-
}
|
146
|
-
|
147
|
-
|
148
|
-
@workflow_route.delete(
|
149
|
-
path="/{name}/logs/{release}",
|
150
|
-
status_code=st.HTTP_204_NO_CONTENT,
|
151
|
-
)
|
152
|
-
async def del_workflow_release_log(name: str, release: str):
|
153
|
-
return {"message": f"Deleted workflow {name!r} log in release {release}"}
|
154
|
-
|
155
|
-
|
156
27
|
@schedule_route.get(path="/{name}")
|
157
28
|
async def get_schedules(name: str):
|
29
|
+
"""Get schedule object."""
|
158
30
|
try:
|
159
31
|
schedule: Schedule = Schedule.from_loader(name=name, externals={})
|
160
32
|
except ValueError:
|
@@ -0,0 +1,137 @@
|
|
1
|
+
# ------------------------------------------------------------------------------
|
2
|
+
# Copyright (c) 2022 Korawich Anuttra. All rights reserved.
|
3
|
+
# Licensed under the MIT License. See LICENSE in the project root for
|
4
|
+
# license information.
|
5
|
+
# ------------------------------------------------------------------------------
|
6
|
+
from __future__ import annotations
|
7
|
+
|
8
|
+
from dataclasses import asdict
|
9
|
+
from datetime import datetime
|
10
|
+
from typing import Any
|
11
|
+
|
12
|
+
from fastapi import APIRouter, HTTPException
|
13
|
+
from fastapi import status as st
|
14
|
+
from fastapi.responses import UJSONResponse
|
15
|
+
from pydantic import BaseModel
|
16
|
+
|
17
|
+
from ...__types import DictData
|
18
|
+
from ...audit import Audit, get_audit
|
19
|
+
from ...conf import Loader, get_logger
|
20
|
+
from ...result import Result
|
21
|
+
from ...workflow import Workflow
|
22
|
+
|
23
|
+
logger = get_logger("ddeutil.workflow")
|
24
|
+
|
25
|
+
workflow_route = APIRouter(
|
26
|
+
prefix="/workflows",
|
27
|
+
tags=["workflows"],
|
28
|
+
default_response_class=UJSONResponse,
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
@workflow_route.get(path="/")
|
33
|
+
async def get_workflows() -> DictData:
|
34
|
+
"""Return all workflow workflows that exists in config path."""
|
35
|
+
workflows: DictData = dict(Loader.finds(Workflow))
|
36
|
+
return {
|
37
|
+
"message": f"Getting all workflows: {len(workflows)}",
|
38
|
+
"count": len(workflows),
|
39
|
+
"workflows": workflows,
|
40
|
+
}
|
41
|
+
|
42
|
+
|
43
|
+
@workflow_route.get(path="/{name}")
|
44
|
+
async def get_workflow_by_name(name: str) -> DictData:
|
45
|
+
"""Return model of workflow that passing an input workflow name."""
|
46
|
+
try:
|
47
|
+
workflow: Workflow = Workflow.from_loader(name=name, externals={})
|
48
|
+
except ValueError as err:
|
49
|
+
logger.exception(err)
|
50
|
+
raise HTTPException(
|
51
|
+
status_code=st.HTTP_404_NOT_FOUND,
|
52
|
+
detail=(
|
53
|
+
f"Workflow workflow name: {name!r} does not found in /conf path"
|
54
|
+
),
|
55
|
+
) from None
|
56
|
+
return workflow.model_dump(
|
57
|
+
by_alias=True,
|
58
|
+
exclude_none=True,
|
59
|
+
exclude_unset=True,
|
60
|
+
exclude_defaults=True,
|
61
|
+
)
|
62
|
+
|
63
|
+
|
64
|
+
class ExecutePayload(BaseModel):
|
65
|
+
params: dict[str, Any]
|
66
|
+
|
67
|
+
|
68
|
+
@workflow_route.post(path="/{name}/execute", status_code=st.HTTP_202_ACCEPTED)
|
69
|
+
async def execute_workflow(name: str, payload: ExecutePayload) -> DictData:
|
70
|
+
"""Return model of workflow that passing an input workflow name."""
|
71
|
+
try:
|
72
|
+
workflow: Workflow = Workflow.from_loader(name=name, externals={})
|
73
|
+
except ValueError:
|
74
|
+
raise HTTPException(
|
75
|
+
status_code=st.HTTP_404_NOT_FOUND,
|
76
|
+
detail=(
|
77
|
+
f"Workflow workflow name: {name!r} does not found in /conf path"
|
78
|
+
),
|
79
|
+
) from None
|
80
|
+
|
81
|
+
# NOTE: Start execute manually
|
82
|
+
try:
|
83
|
+
result: Result = workflow.execute(params=payload.params)
|
84
|
+
except Exception as err:
|
85
|
+
raise HTTPException(
|
86
|
+
status_code=st.HTTP_500_INTERNAL_SERVER_ERROR,
|
87
|
+
detail=f"{type(err)}: {err}",
|
88
|
+
) from None
|
89
|
+
|
90
|
+
return asdict(result)
|
91
|
+
|
92
|
+
|
93
|
+
@workflow_route.get(path="/{name}/audits")
|
94
|
+
async def get_workflow_audits(name: str):
|
95
|
+
try:
|
96
|
+
return {
|
97
|
+
"message": f"Getting workflow {name!r} audits",
|
98
|
+
"audits": [
|
99
|
+
audit.model_dump(
|
100
|
+
by_alias=True,
|
101
|
+
exclude_none=True,
|
102
|
+
exclude_unset=True,
|
103
|
+
exclude_defaults=True,
|
104
|
+
)
|
105
|
+
for audit in get_audit().find_audits(name=name)
|
106
|
+
],
|
107
|
+
}
|
108
|
+
except FileNotFoundError:
|
109
|
+
raise HTTPException(
|
110
|
+
status_code=st.HTTP_404_NOT_FOUND,
|
111
|
+
detail=f"Does not found audit for workflow {name!r}",
|
112
|
+
) from None
|
113
|
+
|
114
|
+
|
115
|
+
@workflow_route.get(path="/{name}/audits/{release}")
|
116
|
+
async def get_workflow_release_audit(name: str, release: str):
|
117
|
+
try:
|
118
|
+
audit: Audit = get_audit().find_audit_with_release(
|
119
|
+
name=name, release=datetime.strptime(release, "%Y%m%d%H%M%S")
|
120
|
+
)
|
121
|
+
except FileNotFoundError:
|
122
|
+
raise HTTPException(
|
123
|
+
status_code=st.HTTP_404_NOT_FOUND,
|
124
|
+
detail=(
|
125
|
+
f"Does not found audit for workflow {name!r} "
|
126
|
+
f"with release {release!r}"
|
127
|
+
),
|
128
|
+
) from None
|
129
|
+
return {
|
130
|
+
"message": f"Getting workflow {name!r} audit in release {release}",
|
131
|
+
"audit": audit.model_dump(
|
132
|
+
by_alias=True,
|
133
|
+
exclude_none=True,
|
134
|
+
exclude_unset=True,
|
135
|
+
exclude_defaults=True,
|
136
|
+
),
|
137
|
+
}
|