ddeutil-workflow 0.0.33__py3-none-any.whl → 0.0.35__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.
@@ -1 +1 @@
1
- __version__: str = "0.0.33"
1
+ __version__: str = "0.0.35"
@@ -9,6 +9,13 @@ from .audit import (
9
9
  Audit,
10
10
  get_audit,
11
11
  )
12
+ from .caller import (
13
+ ReturnTagFunc,
14
+ TagFunc,
15
+ extract_call,
16
+ make_registry,
17
+ tag,
18
+ )
12
19
  from .conf import (
13
20
  Config,
14
21
  Loader,
@@ -28,17 +35,15 @@ from .exceptions import (
28
35
  UtilException,
29
36
  WorkflowException,
30
37
  )
31
- from .hook import (
32
- ReturnTagFunc,
33
- TagFunc,
34
- extract_hook,
35
- make_registry,
36
- tag,
37
- )
38
38
  from .job import (
39
39
  Job,
40
40
  Strategy,
41
41
  )
42
+ from .logs import (
43
+ TraceLog,
44
+ get_dt_tznow,
45
+ get_trace,
46
+ )
42
47
  from .params import (
43
48
  ChoiceParam,
44
49
  DatetimeParam,
@@ -46,7 +51,11 @@ from .params import (
46
51
  Param,
47
52
  StrParam,
48
53
  )
49
- from .result import Result
54
+ from .result import (
55
+ Result,
56
+ Status,
57
+ default_gen_id,
58
+ )
50
59
  from .scheduler import (
51
60
  Schedule,
52
61
  ScheduleWorkflow,
@@ -54,10 +63,10 @@ from .scheduler import (
54
63
  schedule_runner,
55
64
  schedule_task,
56
65
  )
57
- from .stage import (
66
+ from .stages import (
58
67
  BashStage,
68
+ CallStage,
59
69
  EmptyStage,
60
- HookStage,
61
70
  PyStage,
62
71
  Stage,
63
72
  TriggerStage,
@@ -20,6 +20,7 @@ from ..conf import config, get_logger
20
20
  from ..scheduler import ReleaseThread, ReleaseThreads
21
21
  from ..workflow import ReleaseQueue, WorkflowTask
22
22
  from .repeat import repeat_at
23
+ from .routes import log
23
24
 
24
25
  load_dotenv()
25
26
  logger = get_logger("ddeutil.workflow")
@@ -77,22 +78,26 @@ async def health():
77
78
  return {"message": "Workflow API already start up"}
78
79
 
79
80
 
80
- # NOTE: Enable the workflow route.
81
+ # NOTE Add the logs route by default.
82
+ app.include_router(log, prefix=config.prefix_path)
83
+
84
+
85
+ # NOTE: Enable the workflows route.
81
86
  if config.enable_route_workflow:
82
- from .route import workflow_route
87
+ from .routes import workflow
83
88
 
84
- app.include_router(workflow_route, prefix=config.prefix_path)
89
+ app.include_router(workflow, prefix=config.prefix_path)
85
90
 
86
91
 
87
- # NOTE: Enable the schedule route.
92
+ # NOTE: Enable the schedules route.
88
93
  if config.enable_route_schedule:
89
94
  from ..audit import get_audit
90
95
  from ..scheduler import schedule_task
91
- from .route import schedule_route
96
+ from .routes import schedule
92
97
 
93
- app.include_router(schedule_route, prefix=config.prefix_path)
98
+ app.include_router(schedule, prefix=config.prefix_path)
94
99
 
95
- @schedule_route.on_event("startup")
100
+ @schedule.on_event("startup")
96
101
  @repeat_at(cron="* * * * *", delay=2)
97
102
  def scheduler_listener():
98
103
  """Schedule broker every minute at 02 second."""
@@ -109,7 +114,7 @@ if config.enable_route_schedule:
109
114
  log=get_audit(),
110
115
  )
111
116
 
112
- @schedule_route.on_event("startup")
117
+ @schedule.on_event("startup")
113
118
  @repeat_at(cron="*/5 * * * *", delay=10)
114
119
  def monitoring():
115
120
  logger.debug("[MONITOR]: Start monitoring threading.")
@@ -0,0 +1,8 @@
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 .logs import log_route as log
7
+ from .schedules import schedule_route as schedule
8
+ from .workflows import workflow_route as workflow
@@ -0,0 +1,36 @@
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 fastapi import APIRouter
9
+ from fastapi.responses import UJSONResponse
10
+
11
+ from ...conf import get_logger
12
+ from ...logs import get_trace_obj
13
+
14
+ logger = get_logger("ddeutil.workflow")
15
+
16
+
17
+ # NOTE: Start create the schedule routes.
18
+ #
19
+ log_route = APIRouter(
20
+ prefix="/logs",
21
+ tags=["logs"],
22
+ default_response_class=UJSONResponse,
23
+ )
24
+
25
+
26
+ @log_route.get(path="/")
27
+ async def get_logs():
28
+ return {
29
+ "message": "Getting logs",
30
+ "audits": list(get_trace_obj().find_logs()),
31
+ }
32
+
33
+
34
+ @log_route.get(path="/{run_id}")
35
+ async def get_log_with_run_id(run_id: str):
36
+ return get_trace_obj().find_log_with_id(run_id)
@@ -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 ..__types import DictData
19
- from ..audit import Audit, get_audit
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,122 +24,6 @@ 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_logs(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_log_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):
158
29
  try:
@@ -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
+ }
ddeutil/workflow/audit.py CHANGED
@@ -12,16 +12,15 @@ from abc import ABC, abstractmethod
12
12
  from collections.abc import Iterator
13
13
  from datetime import datetime
14
14
  from pathlib import Path
15
- from typing import Any, ClassVar, Optional, Union
15
+ from typing import ClassVar, Optional, Union
16
16
 
17
17
  from pydantic import BaseModel, Field
18
18
  from pydantic.functional_validators import model_validator
19
19
  from typing_extensions import Self
20
20
 
21
21
  from .__types import DictData, TupleStr
22
- from .conf import config, get_logger
23
-
24
- logger = get_logger("ddeutil.workflow")
22
+ from .conf import config
23
+ from .logs import TraceLog, get_trace
25
24
 
26
25
  __all__: TupleStr = (
27
26
  "get_audit",
@@ -55,7 +54,7 @@ class BaseAudit(BaseModel, ABC):
55
54
 
56
55
  :rtype: Self
57
56
  """
58
- if config.enable_write_log:
57
+ if config.enable_write_audit:
59
58
  self.do_before()
60
59
  return self
61
60
 
@@ -83,8 +82,8 @@ class FileAudit(BaseAudit):
83
82
  self.pointer().mkdir(parents=True, exist_ok=True)
84
83
 
85
84
  @classmethod
86
- def find_logs(cls, name: str) -> Iterator[Self]:
87
- """Generate the logging data that found from logs path with specific a
85
+ def find_audits(cls, name: str) -> Iterator[Self]:
86
+ """Generate the audit data that found from logs path with specific a
88
87
  workflow name.
89
88
 
90
89
  :param name: A workflow name that want to search release logging data.
@@ -100,12 +99,12 @@ class FileAudit(BaseAudit):
100
99
  yield cls.model_validate(obj=json.load(f))
101
100
 
102
101
  @classmethod
103
- def find_log_with_release(
102
+ def find_audit_with_release(
104
103
  cls,
105
104
  name: str,
106
105
  release: datetime | None = None,
107
106
  ) -> Self:
108
- """Return the logging data that found from logs path with specific
107
+ """Return the audit data that found from logs path with specific
109
108
  workflow name and release values. If a release does not pass to an input
110
109
  argument, it will return the latest release from the current log path.
111
110
 
@@ -147,7 +146,7 @@ class FileAudit(BaseAudit):
147
146
  :return: Return False if the release log was not pointed or created.
148
147
  """
149
148
  # NOTE: Return False if enable writing log flag does not set.
150
- if not config.enable_write_log:
149
+ if not config.enable_write_audit:
151
150
  return False
152
151
 
153
152
  # NOTE: create pointer path that use the same logic of pointer method.
@@ -175,14 +174,11 @@ class FileAudit(BaseAudit):
175
174
 
176
175
  :rtype: Self
177
176
  """
178
- from .utils import cut_id
177
+ trace: TraceLog = get_trace(self.run_id, self.parent_run_id)
179
178
 
180
179
  # NOTE: Check environ variable was set for real writing.
181
- if not config.enable_write_log:
182
- logger.debug(
183
- f"({cut_id(self.run_id)}) [LOG]: Skip writing log cause "
184
- f"config was set"
185
- )
180
+ if not config.enable_write_audit:
181
+ trace.debug("[LOG]: Skip writing log cause config was set")
186
182
  return self
187
183
 
188
184
  log_file: Path = self.pointer() / f"{self.run_id}.log"
@@ -200,34 +196,29 @@ class FileAudit(BaseAudit):
200
196
  class SQLiteAudit(BaseAudit): # pragma: no cov
201
197
  """SQLite Audit Pydantic Model."""
202
198
 
203
- @staticmethod
204
- def meta() -> dict[str, Any]:
205
- return {
206
- "table": "workflow_log",
207
- "ddl": """
208
- workflow str,
209
- release int,
210
- type str,
211
- context json,
212
- parent_run_id int,
213
- run_id int,
214
- update datetime
215
- primary key ( run_id )
216
- """,
217
- }
199
+ table_name: ClassVar[str] = "workflow_log"
200
+ schemas: ClassVar[
201
+ str
202
+ ] = """
203
+ workflow str,
204
+ release int,
205
+ type str,
206
+ context json,
207
+ parent_run_id int,
208
+ run_id int,
209
+ update datetime
210
+ primary key ( run_id )
211
+ """
218
212
 
219
213
  def save(self, excluded: list[str] | None) -> SQLiteAudit:
220
214
  """Save logging data that receive a context data from a workflow
221
215
  execution result.
222
216
  """
223
- from .utils import cut_id
217
+ trace: TraceLog = get_trace(self.run_id, self.parent_run_id)
224
218
 
225
219
  # NOTE: Check environ variable was set for real writing.
226
- if not config.enable_write_log:
227
- logger.debug(
228
- f"({cut_id(self.run_id)}) [LOG]: Skip writing log cause "
229
- f"config was set"
230
- )
220
+ if not config.enable_write_audit:
221
+ trace.debug("[LOG]: Skip writing log cause config was set")
231
222
  return self
232
223
 
233
224
  raise NotImplementedError("SQLiteAudit does not implement yet.")