ddeutil-workflow 0.0.32__py3-none-any.whl → 0.0.34__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.32"
1
+ __version__: str = "0.0.34"
@@ -5,13 +5,22 @@
5
5
  # ------------------------------------------------------------------------------
6
6
  from .__cron import CronJob, CronRunner
7
7
  from .__types import Re
8
+ from .audit import (
9
+ Audit,
10
+ get_audit,
11
+ )
12
+ from .call import (
13
+ ReturnTagFunc,
14
+ TagFunc,
15
+ extract_call,
16
+ make_registry,
17
+ tag,
18
+ )
8
19
  from .conf import (
9
20
  Config,
10
21
  Loader,
11
- Log,
12
22
  config,
13
23
  env,
14
- get_log,
15
24
  get_logger,
16
25
  )
17
26
  from .cron import (
@@ -26,13 +35,6 @@ from .exceptions import (
26
35
  UtilException,
27
36
  WorkflowException,
28
37
  )
29
- from .hook import (
30
- ReturnTagFunc,
31
- TagFunc,
32
- extract_hook,
33
- make_registry,
34
- tag,
35
- )
36
38
  from .job import (
37
39
  Job,
38
40
  Strategy,
@@ -44,7 +46,13 @@ from .params import (
44
46
  Param,
45
47
  StrParam,
46
48
  )
47
- from .result import Result
49
+ from .result import (
50
+ Result,
51
+ Status,
52
+ TraceLog,
53
+ default_gen_id,
54
+ get_dt_tznow,
55
+ )
48
56
  from .scheduler import (
49
57
  Schedule,
50
58
  ScheduleWorkflow,
@@ -52,10 +60,10 @@ from .scheduler import (
52
60
  schedule_runner,
53
61
  schedule_task,
54
62
  )
55
- from .stage import (
63
+ from .stages import (
56
64
  BashStage,
65
+ CallStage,
57
66
  EmptyStage,
58
- HookStage,
59
67
  PyStage,
60
68
  Stage,
61
69
  TriggerStage,
@@ -86,7 +86,7 @@ if config.enable_route_workflow:
86
86
 
87
87
  # NOTE: Enable the schedule route.
88
88
  if config.enable_route_schedule:
89
- from ..conf import get_log
89
+ from ..audit import get_audit
90
90
  from ..scheduler import schedule_task
91
91
  from .route import schedule_route
92
92
 
@@ -106,7 +106,7 @@ if config.enable_route_schedule:
106
106
  stop=datetime.now(config.tz) + timedelta(minutes=1),
107
107
  queue=app.state.workflow_queue,
108
108
  threads=app.state.workflow_threads,
109
- log=get_log(),
109
+ log=get_audit(),
110
110
  )
111
111
 
112
112
  @schedule_route.on_event("startup")
@@ -16,7 +16,8 @@ from fastapi.responses import UJSONResponse
16
16
  from pydantic import BaseModel
17
17
 
18
18
  from ..__types import DictData
19
- from ..conf import FileLog, Loader, config, get_logger
19
+ from ..audit import Audit, get_audit
20
+ from ..conf import Loader, config, get_logger
20
21
  from ..result import Result
21
22
  from ..scheduler import Schedule
22
23
  from ..workflow import Workflow
@@ -109,7 +110,7 @@ async def get_workflow_logs(name: str):
109
110
  exclude_unset=True,
110
111
  exclude_defaults=True,
111
112
  )
112
- for log in FileLog.find_logs(name=name)
113
+ for log in get_audit().find_audits(name=name)
113
114
  ],
114
115
  }
115
116
  except FileNotFoundError:
@@ -122,7 +123,7 @@ async def get_workflow_logs(name: str):
122
123
  @workflow_route.get(path="/{name}/logs/{release}")
123
124
  async def get_workflow_release_log(name: str, release: str):
124
125
  try:
125
- log: FileLog = FileLog.find_log_with_release(
126
+ log: Audit = get_audit().find_audit_with_release(
126
127
  name=name, release=datetime.strptime(release, "%Y%m%d%H%M%S")
127
128
  )
128
129
  except FileNotFoundError:
@@ -0,0 +1,252 @@
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
+ """Audit Log module."""
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import os
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import Iterator
13
+ from datetime import datetime
14
+ from pathlib import Path
15
+ from typing import ClassVar, Optional, Union
16
+
17
+ from pydantic import BaseModel, Field
18
+ from pydantic.functional_validators import model_validator
19
+ from typing_extensions import Self
20
+
21
+ from .__types import DictData, TupleStr
22
+ from .conf import config
23
+ from .result import TraceLog
24
+
25
+ __all__: TupleStr = (
26
+ "get_audit",
27
+ "FileAudit",
28
+ "SQLiteAudit",
29
+ "Audit",
30
+ )
31
+
32
+
33
+ class BaseAudit(BaseModel, ABC):
34
+ """Base Audit Pydantic Model with abstraction class property that implement
35
+ only model fields. This model should to use with inherit to logging
36
+ subclass like file, sqlite, etc.
37
+ """
38
+
39
+ name: str = Field(description="A workflow name.")
40
+ release: datetime = Field(description="A release datetime.")
41
+ type: str = Field(description="A running type before logging.")
42
+ context: DictData = Field(
43
+ default_factory=dict,
44
+ description="A context that receive from a workflow execution result.",
45
+ )
46
+ parent_run_id: Optional[str] = Field(default=None)
47
+ run_id: str
48
+ update: datetime = Field(default_factory=datetime.now)
49
+ execution_time: float = Field(default=0)
50
+
51
+ @model_validator(mode="after")
52
+ def __model_action(self) -> Self:
53
+ """Do before the Audit action with WORKFLOW_AUDIT_ENABLE_WRITE env variable.
54
+
55
+ :rtype: Self
56
+ """
57
+ if config.enable_write_audit:
58
+ self.do_before()
59
+ return self
60
+
61
+ def do_before(self) -> None: # pragma: no cov
62
+ """To something before end up of initial log model."""
63
+
64
+ @abstractmethod
65
+ def save(self, excluded: list[str] | None) -> None: # pragma: no cov
66
+ """Save this model logging to target logging store."""
67
+ raise NotImplementedError("Audit should implement ``save`` method.")
68
+
69
+
70
+ class FileAudit(BaseAudit):
71
+ """File Audit Pydantic Model that use to saving log data from result of
72
+ workflow execution. It inherits from BaseAudit model that implement the
73
+ ``self.save`` method for file.
74
+ """
75
+
76
+ filename_fmt: ClassVar[str] = (
77
+ "workflow={name}/release={release:%Y%m%d%H%M%S}"
78
+ )
79
+
80
+ def do_before(self) -> None:
81
+ """Create directory of release before saving log file."""
82
+ self.pointer().mkdir(parents=True, exist_ok=True)
83
+
84
+ @classmethod
85
+ def find_audits(cls, name: str) -> Iterator[Self]:
86
+ """Generate the audit data that found from logs path with specific a
87
+ workflow name.
88
+
89
+ :param name: A workflow name that want to search release logging data.
90
+
91
+ :rtype: Iterator[Self]
92
+ """
93
+ pointer: Path = config.audit_path / f"workflow={name}"
94
+ if not pointer.exists():
95
+ raise FileNotFoundError(f"Pointer: {pointer.absolute()}.")
96
+
97
+ for file in pointer.glob("./release=*/*.log"):
98
+ with file.open(mode="r", encoding="utf-8") as f:
99
+ yield cls.model_validate(obj=json.load(f))
100
+
101
+ @classmethod
102
+ def find_audit_with_release(
103
+ cls,
104
+ name: str,
105
+ release: datetime | None = None,
106
+ ) -> Self:
107
+ """Return the audit data that found from logs path with specific
108
+ workflow name and release values. If a release does not pass to an input
109
+ argument, it will return the latest release from the current log path.
110
+
111
+ :param name: A workflow name that want to search log.
112
+ :param release: A release datetime that want to search log.
113
+
114
+ :raise FileNotFoundError:
115
+ :raise NotImplementedError:
116
+
117
+ :rtype: Self
118
+ """
119
+ if release is None:
120
+ raise NotImplementedError("Find latest log does not implement yet.")
121
+
122
+ pointer: Path = (
123
+ config.audit_path
124
+ / f"workflow={name}/release={release:%Y%m%d%H%M%S}"
125
+ )
126
+ if not pointer.exists():
127
+ raise FileNotFoundError(
128
+ f"Pointer: ./logs/workflow={name}/"
129
+ f"release={release:%Y%m%d%H%M%S} does not found."
130
+ )
131
+
132
+ with max(pointer.glob("./*.log"), key=os.path.getctime).open(
133
+ mode="r", encoding="utf-8"
134
+ ) as f:
135
+ return cls.model_validate(obj=json.load(f))
136
+
137
+ @classmethod
138
+ def is_pointed(cls, name: str, release: datetime) -> bool:
139
+ """Check the release log already pointed or created at the destination
140
+ log path.
141
+
142
+ :param name: A workflow name.
143
+ :param release: A release datetime.
144
+
145
+ :rtype: bool
146
+ :return: Return False if the release log was not pointed or created.
147
+ """
148
+ # NOTE: Return False if enable writing log flag does not set.
149
+ if not config.enable_write_audit:
150
+ return False
151
+
152
+ # NOTE: create pointer path that use the same logic of pointer method.
153
+ pointer: Path = config.audit_path / cls.filename_fmt.format(
154
+ name=name, release=release
155
+ )
156
+
157
+ return pointer.exists()
158
+
159
+ def pointer(self) -> Path:
160
+ """Return release directory path that was generated from model data.
161
+
162
+ :rtype: Path
163
+ """
164
+ return config.audit_path / self.filename_fmt.format(
165
+ name=self.name, release=self.release
166
+ )
167
+
168
+ def save(self, excluded: list[str] | None) -> Self:
169
+ """Save logging data that receive a context data from a workflow
170
+ execution result.
171
+
172
+ :param excluded: An excluded list of key name that want to pass in the
173
+ model_dump method.
174
+
175
+ :rtype: Self
176
+ """
177
+ trace: TraceLog = TraceLog(self.run_id, self.parent_run_id)
178
+
179
+ # NOTE: Check environ variable was set for real writing.
180
+ if not config.enable_write_audit:
181
+ trace.debug("[LOG]: Skip writing log cause config was set")
182
+ return self
183
+
184
+ log_file: Path = self.pointer() / f"{self.run_id}.log"
185
+ log_file.write_text(
186
+ json.dumps(
187
+ self.model_dump(exclude=excluded),
188
+ default=str,
189
+ indent=2,
190
+ ),
191
+ encoding="utf-8",
192
+ )
193
+ return self
194
+
195
+
196
+ class SQLiteAudit(BaseAudit): # pragma: no cov
197
+ """SQLite Audit Pydantic Model."""
198
+
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
+ """
212
+
213
+ def save(self, excluded: list[str] | None) -> SQLiteAudit:
214
+ """Save logging data that receive a context data from a workflow
215
+ execution result.
216
+ """
217
+ trace: TraceLog = TraceLog(self.run_id, self.parent_run_id)
218
+
219
+ # NOTE: Check environ variable was set for real writing.
220
+ if not config.enable_write_audit:
221
+ trace.debug("[LOG]: Skip writing log cause config was set")
222
+ return self
223
+
224
+ raise NotImplementedError("SQLiteAudit does not implement yet.")
225
+
226
+
227
+ class RemoteFileAudit(FileAudit): # pragma: no cov
228
+ """Remote File Audit Pydantic Model."""
229
+
230
+ def save(self, excluded: list[str] | None) -> RemoteFileAudit: ...
231
+
232
+
233
+ class RedisAudit(BaseAudit): # pragma: no cov
234
+ """Redis Audit Pydantic Model."""
235
+
236
+ def save(self, excluded: list[str] | None) -> RedisAudit: ...
237
+
238
+
239
+ Audit = Union[
240
+ FileAudit,
241
+ SQLiteAudit,
242
+ ]
243
+
244
+
245
+ def get_audit() -> type[Audit]: # pragma: no cov
246
+ """Get an audit class that dynamic base on the config audit path value.
247
+
248
+ :rtype: type[Audit]
249
+ """
250
+ if config.audit_path.is_file():
251
+ return SQLiteAudit
252
+ return FileAudit
@@ -60,7 +60,7 @@ def tag(
60
60
 
61
61
  @wraps(func)
62
62
  def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
63
- # NOTE: Able to do anything before calling hook function.
63
+ # NOTE: Able to do anything before calling call function.
64
64
  return func(*args, **kwargs)
65
65
 
66
66
  return wrapped
@@ -79,9 +79,9 @@ def make_registry(submodule: str) -> dict[str, Registry]:
79
79
  :rtype: dict[str, Registry]
80
80
  """
81
81
  rs: dict[str, Registry] = {}
82
- regis_hooks: list[str] = config.regis_hook
83
- regis_hooks.extend(["ddeutil.vendors"])
84
- for module in regis_hooks:
82
+ regis_calls: list[str] = config.regis_call
83
+ regis_calls.extend(["ddeutil.vendors"])
84
+ for module in regis_calls:
85
85
  # NOTE: try to sequential import task functions
86
86
  try:
87
87
  importer = import_module(f"{module}.{submodule}")
@@ -114,9 +114,9 @@ def make_registry(submodule: str) -> dict[str, Registry]:
114
114
 
115
115
 
116
116
  @dataclass(frozen=True)
117
- class HookSearchData:
118
- """Hook Search dataclass that use for receive regular expression grouping
119
- dict from searching hook string value.
117
+ class CallSearchData:
118
+ """Call Search dataclass that use for receive regular expression grouping
119
+ dict from searching call string value.
120
120
  """
121
121
 
122
122
  path: str
@@ -124,49 +124,49 @@ class HookSearchData:
124
124
  tag: str
125
125
 
126
126
 
127
- def extract_hook(hook: str) -> Callable[[], TagFunc]:
128
- """Extract Hook function from string value to hook partial function that
127
+ def extract_call(call: str) -> Callable[[], TagFunc]:
128
+ """Extract Call function from string value to call partial function that
129
129
  does run it at runtime.
130
130
 
131
- :raise NotImplementedError: When the searching hook's function result does
131
+ :raise NotImplementedError: When the searching call's function result does
132
132
  not exist in the registry.
133
- :raise NotImplementedError: When the searching hook's tag result does not
133
+ :raise NotImplementedError: When the searching call's tag result does not
134
134
  exist in the registry with its function key.
135
135
 
136
- :param hook: A hook value that able to match with Task regex.
136
+ :param call: A call value that able to match with Task regex.
137
137
 
138
- The format of hook value should contain 3 regular expression groups
138
+ The format of call value should contain 3 regular expression groups
139
139
  which match with the below config format:
140
140
 
141
141
  >>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
142
142
 
143
143
  Examples:
144
- >>> extract_hook("tasks/el-postgres-to-delta@polars")
144
+ >>> extract_call("tasks/el-postgres-to-delta@polars")
145
145
  ...
146
- >>> extract_hook("tasks/return-type-not-valid@raise")
146
+ >>> extract_call("tasks/return-type-not-valid@raise")
147
147
  ...
148
148
 
149
149
  :rtype: Callable[[], TagFunc]
150
150
  """
151
- if not (found := Re.RE_TASK_FMT.search(hook)):
151
+ if not (found := Re.RE_TASK_FMT.search(call)):
152
152
  raise ValueError(
153
- f"Hook {hook!r} does not match with hook format regex."
153
+ f"Call {call!r} does not match with call format regex."
154
154
  )
155
155
 
156
- # NOTE: Pass the searching hook string to `path`, `func`, and `tag`.
157
- hook: HookSearchData = HookSearchData(**found.groupdict())
156
+ # NOTE: Pass the searching call string to `path`, `func`, and `tag`.
157
+ call: CallSearchData = CallSearchData(**found.groupdict())
158
158
 
159
159
  # NOTE: Registry object should implement on this package only.
160
- rgt: dict[str, Registry] = make_registry(f"{hook.path}")
161
- if hook.func not in rgt:
160
+ rgt: dict[str, Registry] = make_registry(f"{call.path}")
161
+ if call.func not in rgt:
162
162
  raise NotImplementedError(
163
- f"``REGISTER-MODULES.{hook.path}.registries`` does not "
164
- f"implement registry: {hook.func!r}."
163
+ f"``REGISTER-MODULES.{call.path}.registries`` does not "
164
+ f"implement registry: {call.func!r}."
165
165
  )
166
166
 
167
- if hook.tag not in rgt[hook.func]:
167
+ if call.tag not in rgt[call.func]:
168
168
  raise NotImplementedError(
169
- f"tag: {hook.tag!r} does not found on registry func: "
170
- f"``REGISTER-MODULES.{hook.path}.registries.{hook.func}``"
169
+ f"tag: {call.tag!r} does not found on registry func: "
170
+ f"``REGISTER-MODULES.{call.path}.registries.{call.func}``"
171
171
  )
172
- return rgt[hook.func][hook.tag]
172
+ return rgt[call.func][call.tag]