ddeutil-workflow 0.0.40__py3-none-any.whl → 0.0.41__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/audit.py DELETED
@@ -1,257 +0,0 @@
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 .logs import TraceLog, get_dt_tznow, get_trace
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(
47
- default=None, description="A parent running ID."
48
- )
49
- run_id: str = Field(description="A running ID")
50
- update: datetime = Field(default_factory=get_dt_tznow)
51
- execution_time: float = Field(default=0, description="An execution time.")
52
-
53
- @model_validator(mode="after")
54
- def __model_action(self) -> Self:
55
- """Do before the Audit action with WORKFLOW_AUDIT_ENABLE_WRITE env variable.
56
-
57
- :rtype: Self
58
- """
59
- if config.enable_write_audit:
60
- self.do_before()
61
- return self
62
-
63
- def do_before(self) -> None: # pragma: no cov
64
- """To something before end up of initial log model."""
65
-
66
- @abstractmethod
67
- def save(self, excluded: list[str] | None) -> None: # pragma: no cov
68
- """Save this model logging to target logging store."""
69
- raise NotImplementedError("Audit should implement ``save`` method.")
70
-
71
-
72
- class FileAudit(BaseAudit):
73
- """File Audit Pydantic Model that use to saving log data from result of
74
- workflow execution. It inherits from BaseAudit model that implement the
75
- ``self.save`` method for file.
76
- """
77
-
78
- filename_fmt: ClassVar[str] = (
79
- "workflow={name}/release={release:%Y%m%d%H%M%S}"
80
- )
81
-
82
- def do_before(self) -> None:
83
- """Create directory of release before saving log file."""
84
- self.pointer().mkdir(parents=True, exist_ok=True)
85
-
86
- @classmethod
87
- def find_audits(cls, name: str) -> Iterator[Self]:
88
- """Generate the audit data that found from logs path with specific a
89
- workflow name.
90
-
91
- :param name: A workflow name that want to search release logging data.
92
-
93
- :rtype: Iterator[Self]
94
- """
95
- pointer: Path = config.audit_path / f"workflow={name}"
96
- if not pointer.exists():
97
- raise FileNotFoundError(f"Pointer: {pointer.absolute()}.")
98
-
99
- for file in pointer.glob("./release=*/*.log"):
100
- with file.open(mode="r", encoding="utf-8") as f:
101
- yield cls.model_validate(obj=json.load(f))
102
-
103
- @classmethod
104
- def find_audit_with_release(
105
- cls,
106
- name: str,
107
- release: datetime | None = None,
108
- ) -> Self:
109
- """Return the audit data that found from logs path with specific
110
- workflow name and release values. If a release does not pass to an input
111
- argument, it will return the latest release from the current log path.
112
-
113
- :param name: A workflow name that want to search log.
114
- :param release: A release datetime that want to search log.
115
-
116
- :raise FileNotFoundError:
117
- :raise NotImplementedError: If an input release does not pass to this
118
- method. Because this method does not implement latest log.
119
-
120
- :rtype: Self
121
- """
122
- if release is None:
123
- raise NotImplementedError("Find latest log does not implement yet.")
124
-
125
- pointer: Path = (
126
- config.audit_path
127
- / f"workflow={name}/release={release:%Y%m%d%H%M%S}"
128
- )
129
- if not pointer.exists():
130
- raise FileNotFoundError(
131
- f"Pointer: ./logs/workflow={name}/"
132
- f"release={release:%Y%m%d%H%M%S} does not found."
133
- )
134
-
135
- with max(pointer.glob("./*.log"), key=os.path.getctime).open(
136
- mode="r", encoding="utf-8"
137
- ) as f:
138
- return cls.model_validate(obj=json.load(f))
139
-
140
- @classmethod
141
- def is_pointed(cls, name: str, release: datetime) -> bool:
142
- """Check the release log already pointed or created at the destination
143
- log path.
144
-
145
- :param name: A workflow name.
146
- :param release: A release datetime.
147
-
148
- :rtype: bool
149
- :return: Return False if the release log was not pointed or created.
150
- """
151
- # NOTE: Return False if enable writing log flag does not set.
152
- if not config.enable_write_audit:
153
- return False
154
-
155
- # NOTE: create pointer path that use the same logic of pointer method.
156
- pointer: Path = config.audit_path / cls.filename_fmt.format(
157
- name=name, release=release
158
- )
159
-
160
- return pointer.exists()
161
-
162
- def pointer(self) -> Path:
163
- """Return release directory path that was generated from model data.
164
-
165
- :rtype: Path
166
- """
167
- return config.audit_path / self.filename_fmt.format(
168
- name=self.name, release=self.release
169
- )
170
-
171
- def save(self, excluded: list[str] | None) -> Self:
172
- """Save logging data that receive a context data from a workflow
173
- execution result.
174
-
175
- :param excluded: An excluded list of key name that want to pass in the
176
- model_dump method.
177
-
178
- :rtype: Self
179
- """
180
- trace: TraceLog = get_trace(self.run_id, self.parent_run_id)
181
-
182
- # NOTE: Check environ variable was set for real writing.
183
- if not config.enable_write_audit:
184
- trace.debug("[LOG]: Skip writing log cause config was set")
185
- return self
186
-
187
- log_file: Path = (
188
- self.pointer() / f"{self.parent_run_id or self.run_id}.log"
189
- )
190
- log_file.write_text(
191
- json.dumps(
192
- self.model_dump(exclude=excluded),
193
- default=str,
194
- indent=2,
195
- ),
196
- encoding="utf-8",
197
- )
198
- return self
199
-
200
-
201
- class SQLiteAudit(BaseAudit): # pragma: no cov
202
- """SQLite Audit Pydantic Model."""
203
-
204
- table_name: ClassVar[str] = "audits"
205
- schemas: ClassVar[
206
- str
207
- ] = """
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
-
218
- def save(self, excluded: list[str] | None) -> SQLiteAudit:
219
- """Save logging data that receive a context data from a workflow
220
- execution result.
221
- """
222
- trace: TraceLog = get_trace(self.run_id, self.parent_run_id)
223
-
224
- # NOTE: Check environ variable was set for real writing.
225
- if not config.enable_write_audit:
226
- trace.debug("[LOG]: Skip writing log cause config was set")
227
- return self
228
-
229
- raise NotImplementedError("SQLiteAudit does not implement yet.")
230
-
231
-
232
- class RemoteFileAudit(FileAudit): # pragma: no cov
233
- """Remote File Audit Pydantic Model."""
234
-
235
- def save(self, excluded: list[str] | None) -> RemoteFileAudit: ...
236
-
237
-
238
- class RedisAudit(BaseAudit): # pragma: no cov
239
- """Redis Audit Pydantic Model."""
240
-
241
- def save(self, excluded: list[str] | None) -> RedisAudit: ...
242
-
243
-
244
- Audit = Union[
245
- FileAudit,
246
- SQLiteAudit,
247
- ]
248
-
249
-
250
- def get_audit() -> type[Audit]: # pragma: no cov
251
- """Get an audit class that dynamic base on the config audit path value.
252
-
253
- :rtype: type[Audit]
254
- """
255
- if config.audit_path.is_file():
256
- return SQLiteAudit
257
- return FileAudit
@@ -1,179 +0,0 @@
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
- import inspect
9
- import logging
10
- from dataclasses import dataclass
11
- from functools import wraps
12
- from importlib import import_module
13
- from typing import Any, Callable, Protocol, TypeVar
14
-
15
- try:
16
- from typing import ParamSpec
17
- except ImportError:
18
- from typing_extensions import ParamSpec
19
-
20
- from ddeutil.core import lazy
21
-
22
- from .__types import Re
23
- from .conf import config
24
-
25
- T = TypeVar("T")
26
- P = ParamSpec("P")
27
-
28
- logger = logging.getLogger("ddeutil.workflow")
29
- logging.getLogger("asyncio").setLevel(logging.INFO)
30
-
31
-
32
- class TagFunc(Protocol):
33
- """Tag Function Protocol"""
34
-
35
- name: str
36
- tag: str
37
-
38
- def __call__(self, *args, **kwargs): ... # pragma: no cov
39
-
40
-
41
- ReturnTagFunc = Callable[P, TagFunc]
42
- DecoratorTagFunc = Callable[[Callable[[...], Any]], ReturnTagFunc]
43
-
44
-
45
- def tag(
46
- name: str, alias: str | None = None
47
- ) -> DecoratorTagFunc: # pragma: no cov
48
- """Tag decorator function that set function attributes, ``tag`` and ``name``
49
- for making registries variable.
50
-
51
- :param: name: (str) A tag name for make different use-case of a function.
52
- :param: alias: (str) A alias function name that keeping in registries.
53
- If this value does not supply, it will use original function name
54
- from `__name__` argument.
55
-
56
- :rtype: Callable[P, TagFunc]
57
- """
58
-
59
- def func_internal(func: Callable[[...], Any]) -> ReturnTagFunc:
60
- func.tag = name
61
- func.name = alias or func.__name__.replace("_", "-")
62
-
63
- @wraps(func)
64
- def wrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
65
- return func(*args, **kwargs)
66
-
67
- @wraps(func)
68
- async def awrapped(*args: P.args, **kwargs: P.kwargs) -> TagFunc:
69
- return await func(*args, **kwargs)
70
-
71
- return awrapped if inspect.iscoroutinefunction(func) else wrapped
72
-
73
- return func_internal
74
-
75
-
76
- Registry = dict[str, Callable[[], TagFunc]]
77
-
78
-
79
- def make_registry(submodule: str) -> dict[str, Registry]:
80
- """Return registries of all functions that able to called with task.
81
-
82
- :param submodule: (str) A module prefix that want to import registry.
83
-
84
- :rtype: dict[str, Registry]
85
- """
86
- rs: dict[str, Registry] = {}
87
- regis_calls: list[str] = config.regis_call
88
- regis_calls.extend(["ddeutil.vendors"])
89
- for module in regis_calls:
90
- # NOTE: try to sequential import task functions
91
- try:
92
- importer = import_module(f"{module}.{submodule}")
93
- except ModuleNotFoundError:
94
- continue
95
-
96
- for fstr, func in inspect.getmembers(importer, inspect.isfunction):
97
- # NOTE: check function attribute that already set tag by
98
- # ``utils.tag`` decorator.
99
- if not (
100
- hasattr(func, "tag") and hasattr(func, "name")
101
- ): # pragma: no cov
102
- continue
103
-
104
- # NOTE: Define type of the func value.
105
- func: TagFunc
106
-
107
- # NOTE: Create new register name if it not exists
108
- if func.name not in rs:
109
- rs[func.name] = {func.tag: lazy(f"{module}.{submodule}.{fstr}")}
110
- continue
111
-
112
- if func.tag in rs[func.name]:
113
- raise ValueError(
114
- f"The tag {func.tag!r} already exists on "
115
- f"{module}.{submodule}, you should change this tag name or "
116
- f"change it func name."
117
- )
118
- rs[func.name][func.tag] = lazy(f"{module}.{submodule}.{fstr}")
119
-
120
- return rs
121
-
122
-
123
- @dataclass(frozen=True)
124
- class CallSearchData:
125
- """Call Search dataclass that use for receive regular expression grouping
126
- dict from searching call string value.
127
- """
128
-
129
- path: str
130
- func: str
131
- tag: str
132
-
133
-
134
- def extract_call(call: str) -> Callable[[], TagFunc]:
135
- """Extract Call function from string value to call partial function that
136
- does run it at runtime.
137
-
138
- :param call: (str) A call value that able to match with Task regex.
139
-
140
- The format of call value should contain 3 regular expression groups
141
- which match with the below config format:
142
-
143
- >>> "^(?P<path>[^/@]+)/(?P<func>[^@]+)@(?P<tag>.+)$"
144
-
145
- Examples:
146
- >>> extract_call("tasks/el-postgres-to-delta@polars")
147
- ...
148
- >>> extract_call("tasks/return-type-not-valid@raise")
149
- ...
150
-
151
- :raise NotImplementedError: When the searching call's function result does
152
- not exist in the registry.
153
- :raise NotImplementedError: When the searching call's tag result does not
154
- exist in the registry with its function key.
155
-
156
- :rtype: Callable[[], TagFunc]
157
- """
158
- if not (found := Re.RE_TASK_FMT.search(call)):
159
- raise ValueError(
160
- f"Call {call!r} does not match with the call regex format."
161
- )
162
-
163
- # NOTE: Pass the searching call string to `path`, `func`, and `tag`.
164
- call: CallSearchData = CallSearchData(**found.groupdict())
165
-
166
- # NOTE: Registry object should implement on this package only.
167
- rgt: dict[str, Registry] = make_registry(f"{call.path}")
168
- if call.func not in rgt:
169
- raise NotImplementedError(
170
- f"`REGISTER-MODULES.{call.path}.registries` does not "
171
- f"implement registry: {call.func!r}."
172
- )
173
-
174
- if call.tag not in rgt[call.func]:
175
- raise NotImplementedError(
176
- f"tag: {call.tag!r} does not found on registry func: "
177
- f"`REGISTER-MODULES.{call.path}.registries.{call.func}`"
178
- )
179
- return rgt[call.func][call.tag]
@@ -1,33 +0,0 @@
1
- ddeutil/workflow/__about__.py,sha256=4_L4aAteV6ecIgxXQuBmMHF6LoFXtYgCVakxe2GlHjk,28
2
- ddeutil/workflow/__cron.py,sha256=h8rLeIUAAEB2SdZ4Jhch7LU1Yl3bbJ-iNNJ3tQ0eYVM,28095
3
- ddeutil/workflow/__init__.py,sha256=hIM2Ha-7F3YF3aLsu4IY3N-UvD_EE1ai6DO6WL2mILg,1908
4
- ddeutil/workflow/__types.py,sha256=8jBdbfb3aZSetjz0mvNrpGHwwxJff7mK8_4v41cLqlc,4316
5
- ddeutil/workflow/audit.py,sha256=BpzLI6ZKXi6xQO8C2sAHxSi8RAfF31dMPdjn3DbYlw8,8364
6
- ddeutil/workflow/caller.py,sha256=M7_nan1DbRUAHEIQoQc1qIX4AHgI5fKFNx0KDbzhDMk,5736
7
- ddeutil/workflow/conf.py,sha256=USYzITPwBS5Q_pl3DTTdFK8OxeL9GQu4GUXqi8nKpJo,11819
8
- ddeutil/workflow/context.py,sha256=vsk4JQL7t3KsnKPfshw3O7YrPFo2h4rnnNd3B-G9Kj4,1700
9
- ddeutil/workflow/cron.py,sha256=j8EeoHst70toRfnD_frix41vrI-eLYVJkZ9yeJtpfnI,8871
10
- ddeutil/workflow/exceptions.py,sha256=fO37f9p7lOjIJgVOpKE_1X44yJTwBepyukZV9a7NNm4,1241
11
- ddeutil/workflow/job.py,sha256=LCQf_3gyTmnj6aAQ0ksz4lJxU10-F5MfVFHczkWt_VE,28669
12
- ddeutil/workflow/logs.py,sha256=mAKQjNri-oourd3dBLLG3Fqoo4QG27U9IO2h6IqCOU0,10196
13
- ddeutil/workflow/params.py,sha256=qw9XJyjh2ocf9pf6h_XiYHLOvQN4R5TMqPElmItKnRM,8019
14
- ddeutil/workflow/result.py,sha256=cDkItrhpzZfMS1Oj8IZX8O-KBD4KZYDi43XJZvvC3Gc,4318
15
- ddeutil/workflow/scheduler.py,sha256=daNdcTp8dQN0gXuK3cg7K7idspOcqKNmfOLTWirNEUM,27652
16
- ddeutil/workflow/stages.py,sha256=ONBR7GjgLEbA21wNioBrhOucwqj6M1v2ht5JhipoWSg,36994
17
- ddeutil/workflow/templates.py,sha256=yA2xgrSXcxfBxNT2hc6v06HkVY_0RKsc1UwdJRip9EE,11554
18
- ddeutil/workflow/utils.py,sha256=Fz5y-LK_JfikDfvMKcFaxad_VvCnr7UC2C9KFCbzPNA,7105
19
- ddeutil/workflow/workflow.py,sha256=8ForojeLboLL6dHmV5-NjtU9o4mIL7NtY2F2NFxdqZY,49400
20
- ddeutil/workflow/api/__init__.py,sha256=F53NMBWtb9IKaDWkPU5KvybGGfKAcbehgn6TLBwHuuM,21
21
- ddeutil/workflow/api/api.py,sha256=gGQtqkzyJNaJIfka_w2M1lrCS3Ep46re2Dznsk9RxYQ,5191
22
- ddeutil/workflow/api/log.py,sha256=NMTnOnsBrDB5129329xF2myLdrb-z9k1MQrmrP7qXJw,1818
23
- ddeutil/workflow/api/repeat.py,sha256=cycd1-91j-4v6uY1SkrZHd9l95e-YgVC4UCSNNFuGJ8,5277
24
- ddeutil/workflow/api/routes/__init__.py,sha256=qoGtOMyVgQ5nTUc8J8wH27A8isaxl3IFCX8qoyibeCY,484
25
- ddeutil/workflow/api/routes/job.py,sha256=YVta083i8vU8-o4WdKFwDpfdC9vN1dZ6goZSmNlQXHA,1954
26
- ddeutil/workflow/api/routes/logs.py,sha256=uEQ6k5PwRg-k0eSiSFArXfyeDq5xzepxILrRnwgAe1I,5373
27
- ddeutil/workflow/api/routes/schedules.py,sha256=uWYDOwlV8w56hKQmfkQFwdZ6t2gZSJeCdBIzMmJenAQ,4824
28
- ddeutil/workflow/api/routes/workflows.py,sha256=KVywA7vD9b4QrfmWBdSFF5chj34yJe1zNCzl6iBMeGI,4538
29
- ddeutil_workflow-0.0.40.dist-info/licenses/LICENSE,sha256=nGFZ1QEhhhWeMHf9n99_fdt4vQaXS29xWKxt-OcLywk,1085
30
- ddeutil_workflow-0.0.40.dist-info/METADATA,sha256=AdIiZTWsPhfcVba7x2OrWw5CN3y0OQB-dRdBb9RQh2o,19501
31
- ddeutil_workflow-0.0.40.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91
32
- ddeutil_workflow-0.0.40.dist-info/top_level.txt,sha256=m9M6XeSWDwt_yMsmH6gcOjHZVK5O0-vgtNBuncHjzW4,8
33
- ddeutil_workflow-0.0.40.dist-info/RECORD,,