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.
@@ -0,0 +1,214 @@
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 is the Logs module. This module provide TraceLog dataclasses.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from abc import ABC, abstractmethod
12
+ from collections.abc import Iterator
13
+ from datetime import datetime
14
+ from inspect import Traceback, currentframe, getframeinfo
15
+ from pathlib import Path
16
+ from threading import get_ident
17
+ from typing import Optional, Union
18
+
19
+ from pydantic.dataclasses import dataclass
20
+
21
+ from .__types import TupleStr
22
+ from .conf import config, get_logger
23
+ from .utils import cut_id, get_dt_now
24
+
25
+ logger = get_logger("ddeutil.workflow")
26
+
27
+ __all__: TupleStr = (
28
+ "FileTraceLog",
29
+ "TraceLog",
30
+ "get_dt_tznow",
31
+ "get_trace",
32
+ )
33
+
34
+
35
+ def get_dt_tznow() -> datetime:
36
+ """Return the current datetime object that passing the config timezone.
37
+
38
+ :rtype: datetime
39
+ """
40
+ return get_dt_now(tz=config.tz)
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class BaseTraceLog(ABC): # pragma: no cov
45
+ """Base Trace Log dataclass object."""
46
+
47
+ run_id: str
48
+ parent_run_id: Optional[str] = None
49
+
50
+ @abstractmethod
51
+ def writer(self, message: str, is_err: bool = False) -> None:
52
+ raise NotImplementedError(
53
+ "Create writer logic for this trace object before using."
54
+ )
55
+
56
+ @abstractmethod
57
+ def make_message(self, message: str) -> str:
58
+ raise NotImplementedError(
59
+ "Adjust make message method for this trace object before using."
60
+ )
61
+
62
+ def debug(self, message: str):
63
+ msg: str = self.make_message(message)
64
+
65
+ # NOTE: Write file if debug mode.
66
+ if config.debug:
67
+ self.writer(msg)
68
+
69
+ logger.debug(msg, stacklevel=2)
70
+
71
+ def info(self, message: str):
72
+ msg: str = self.make_message(message)
73
+ self.writer(msg)
74
+ logger.info(msg, stacklevel=2)
75
+
76
+ def warning(self, message: str):
77
+ msg: str = self.make_message(message)
78
+ self.writer(msg)
79
+ logger.warning(msg, stacklevel=2)
80
+
81
+ def error(self, message: str):
82
+ msg: str = self.make_message(message)
83
+ self.writer(msg, is_err=True)
84
+ logger.error(msg, stacklevel=2)
85
+
86
+
87
+ class FileTraceLog(BaseTraceLog): # pragma: no cov
88
+ """Trace Log object that write file to the local storage."""
89
+
90
+ @classmethod
91
+ def find_logs(cls) -> Iterator[dict[str, str]]: # pragma: no cov
92
+ for file in config.log_path.glob("./run_id=*"):
93
+ data: dict[str, str] = {}
94
+
95
+ if (file / "stdout.txt").exists():
96
+ data["stdout"] = (file / "stdout.txt").read_text(
97
+ encoding="utf-8"
98
+ )
99
+
100
+ if (file / "stderr.txt").exists():
101
+ data["stdout"] = (file / "stdout.txt").read_text(
102
+ encoding="utf-8"
103
+ )
104
+
105
+ yield data
106
+
107
+ @classmethod
108
+ def find_log_with_id(cls, run_id: str) -> dict[str, str]:
109
+ file: Path = config.log_path / f"run_id={run_id}"
110
+ data: dict[str, str] = {}
111
+
112
+ if (file / "stdout.txt").exists():
113
+ data["stdout"] = (file / "stdout.txt").read_text(encoding="utf-8")
114
+
115
+ if (file / "stderr.txt").exists():
116
+ data["stdout"] = (file / "stdout.txt").read_text(encoding="utf-8")
117
+
118
+ return data
119
+
120
+ @property
121
+ def log_file(self) -> Path:
122
+ log_file: Path = (
123
+ config.log_path / f"run_id={self.parent_run_id or self.run_id}"
124
+ )
125
+ if not log_file.exists():
126
+ log_file.mkdir(parents=True)
127
+ return log_file
128
+
129
+ @property
130
+ def cut_id(self) -> str:
131
+ """Combine cutting ID of parent running ID if it set."""
132
+ cut_run_id: str = cut_id(self.run_id)
133
+ if not self.parent_run_id:
134
+ return f"{cut_run_id} -> {' ' * 6}"
135
+
136
+ cut_parent_run_id: str = cut_id(self.parent_run_id)
137
+ return f"{cut_parent_run_id} -> {cut_run_id}"
138
+
139
+ def make_message(self, message: str) -> str:
140
+ return f"({self.cut_id}) {message}"
141
+
142
+ def writer(self, message: str, is_err: bool = False) -> None:
143
+ """The path of logging data will store by format:
144
+
145
+ ... ./logs/run_id=<run-id>/stdout.txt
146
+ ... ./logs/run_id=<run-id>/stderr.txt
147
+
148
+ :param message:
149
+ :param is_err:
150
+ """
151
+ if not config.enable_write_log:
152
+ return
153
+
154
+ frame_info: Traceback = getframeinfo(currentframe().f_back.f_back)
155
+ filename: str = frame_info.filename.split(os.path.sep)[-1]
156
+ lineno: int = frame_info.lineno
157
+
158
+ # NOTE: set process and thread IDs.
159
+ process: int = os.getpid()
160
+ thread: int = get_ident()
161
+
162
+ write_file: str = "stderr.txt" if is_err else "stdout.txt"
163
+ with (self.log_file / write_file).open(
164
+ mode="at", encoding="utf-8"
165
+ ) as f:
166
+ msg_fmt: str = f"{config.log_format_file}\n"
167
+ print(msg_fmt)
168
+ f.write(
169
+ msg_fmt.format(
170
+ **{
171
+ "datetime": get_dt_tznow().strftime(
172
+ config.log_datetime_format
173
+ ),
174
+ "process": process,
175
+ "thread": thread,
176
+ "message": message,
177
+ "filename": filename,
178
+ "lineno": lineno,
179
+ }
180
+ )
181
+ )
182
+
183
+
184
+ class SQLiteTraceLog(BaseTraceLog): # pragma: no cov
185
+
186
+ @classmethod
187
+ def find_logs(cls) -> Iterator[dict[str, str]]: ...
188
+
189
+ @classmethod
190
+ def find_log_with_id(cls, run_id: str) -> dict[str, str]: ...
191
+
192
+ def make_message(self, message: str) -> str: ...
193
+
194
+ def writer(self, message: str, is_err: bool = False) -> None: ...
195
+
196
+
197
+ TraceLog = Union[
198
+ FileTraceLog,
199
+ SQLiteTraceLog,
200
+ ]
201
+
202
+
203
+ def get_trace(
204
+ run_id: str, parent_run_id: str | None = None
205
+ ) -> TraceLog: # pragma: no cov
206
+ if config.log_path.is_file():
207
+ return SQLiteTraceLog(run_id, parent_run_id=parent_run_id)
208
+ return FileTraceLog(run_id, parent_run_id=parent_run_id)
209
+
210
+
211
+ def get_trace_obj() -> type[TraceLog]: # pragma: no cov
212
+ if config.log_path.is_file():
213
+ return SQLiteTraceLog
214
+ return FileTraceLog
@@ -3,8 +3,8 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
- """Param Model that use for parsing incoming parameters that pass to the
7
- Workflow and Schedule objects.
6
+ """This module include all Param Models that use for parsing incoming parameters
7
+ that pass to the Workflow and Schedule objects.
8
8
  """
9
9
  from __future__ import annotations
10
10
 
@@ -12,7 +12,7 @@ import decimal
12
12
  import logging
13
13
  from abc import ABC, abstractmethod
14
14
  from datetime import date, datetime
15
- from typing import Any, Literal, Optional, Union
15
+ from typing import Annotated, Any, Literal, Optional, Union
16
16
 
17
17
  from pydantic import BaseModel, Field
18
18
 
@@ -32,8 +32,9 @@ __all__: TupleStr = (
32
32
 
33
33
 
34
34
  class BaseParam(BaseModel, ABC):
35
- """Base Parameter that use to make any Params Model. The type will dynamic
36
- with the type field that made from literal string."""
35
+ """Base Parameter that use to make any Params Models. The parameter type
36
+ will dynamic with the setup type field that made from literal string.
37
+ """
37
38
 
38
39
  desc: Optional[str] = Field(
39
40
  default=None, description="A description of parameter providing."
@@ -169,9 +170,11 @@ class ChoiceParam(BaseParam):
169
170
  """Choice parameter."""
170
171
 
171
172
  type: Literal["choice"] = "choice"
172
- options: list[str] = Field(description="A list of choice parameters.")
173
+ options: Union[list[str], list[int]] = Field(
174
+ description="A list of choice parameters that able be str or int.",
175
+ )
173
176
 
174
- def receive(self, value: str | None = None) -> str:
177
+ def receive(self, value: Union[str, int] | None = None) -> Union[str, int]:
175
178
  """Receive value that match with options.
176
179
 
177
180
  :param value: A value that want to select from the options field.
@@ -188,9 +191,34 @@ class ChoiceParam(BaseParam):
188
191
  return value
189
192
 
190
193
 
191
- Param = Union[
192
- ChoiceParam,
193
- DatetimeParam,
194
- IntParam,
195
- StrParam,
194
+ # TODO: Not implement this parameter yet
195
+ class MappingParam(DefaultParam): # pragma: no cov
196
+
197
+ type: Literal["map"] = "map"
198
+ default: dict[Any, Any] = Field(default_factory=dict)
199
+
200
+ def receive(self, value: Optional[dict[Any, Any]] = None) -> dict[Any, Any]:
201
+ if value is None:
202
+ return self.default
203
+
204
+
205
+ # TODO: Not implement this parameter yet
206
+ class ArrayParam(DefaultParam): # pragma: no cov
207
+
208
+ type: Literal["array"] = "array"
209
+ default: list[Any] = Field(default_factory=list)
210
+
211
+ def receive(self, value: Optional[list[Any]] = None) -> list[Any]:
212
+ if value is None:
213
+ return self.default
214
+
215
+
216
+ Param = Annotated[
217
+ Union[
218
+ ChoiceParam,
219
+ DatetimeParam,
220
+ IntParam,
221
+ StrParam,
222
+ ],
223
+ Field(discriminator="type"),
196
224
  ]
@@ -3,6 +3,9 @@
3
3
  # Licensed under the MIT License. See LICENSE in the project root for
4
4
  # license information.
5
5
  # ------------------------------------------------------------------------------
6
+ """This is the Result module. It is the data context transfer objects that use
7
+ by all object in this package. This module provide Result dataclass.
8
+ """
6
9
  from __future__ import annotations
7
10
 
8
11
  from dataclasses import field
@@ -13,17 +16,20 @@ from typing import Optional
13
16
 
14
17
  from pydantic import ConfigDict
15
18
  from pydantic.dataclasses import dataclass
19
+ from pydantic.functional_validators import model_validator
16
20
  from typing_extensions import Self
17
21
 
18
22
  from .__types import DictData, TupleStr
19
- from .conf import config, get_logger
20
- from .utils import cut_id, gen_id, get_dt_now
23
+ from .conf import get_logger
24
+ from .logs import TraceLog, get_dt_tznow, get_trace
25
+ from .utils import gen_id
21
26
 
22
- logger = get_logger("ddeutil.workflow.audit")
27
+ logger = get_logger("ddeutil.workflow")
23
28
 
24
29
  __all__: TupleStr = (
25
30
  "Result",
26
31
  "Status",
32
+ "default_gen_id",
27
33
  )
28
34
 
29
35
 
@@ -36,14 +42,6 @@ def default_gen_id() -> str:
36
42
  return gen_id("manual", unique=True)
37
43
 
38
44
 
39
- def get_dt_tznow() -> datetime:
40
- """Return the current datetime object that passing the config timezone.
41
-
42
- :rtype: datetime
43
- """
44
- return get_dt_now(tz=config.tz)
45
-
46
-
47
45
  class Status(IntEnum):
48
46
  """Status Int Enum object."""
49
47
 
@@ -52,27 +50,6 @@ class Status(IntEnum):
52
50
  WAIT: int = 2
53
51
 
54
52
 
55
- class TraceLog: # pragma: no cov
56
- """Trace Log object."""
57
-
58
- __slots__: TupleStr = ("run_id",)
59
-
60
- def __init__(self, run_id: str):
61
- self.run_id: str = run_id
62
-
63
- def debug(self, message: str):
64
- logger.debug(f"({cut_id(self.run_id)}) {message}")
65
-
66
- def info(self, message: str):
67
- logger.info(f"({cut_id(self.run_id)}) {message}")
68
-
69
- def warning(self, message: str):
70
- logger.warning(f"({cut_id(self.run_id)}) {message}")
71
-
72
- def error(self, message: str):
73
- logger.error(f"({cut_id(self.run_id)}) {message}")
74
-
75
-
76
53
  @dataclass(
77
54
  config=ConfigDict(arbitrary_types_allowed=True, use_enum_values=True)
78
55
  )
@@ -88,19 +65,37 @@ class Result:
88
65
  status: Status = field(default=Status.WAIT)
89
66
  context: DictData = field(default_factory=dict)
90
67
  run_id: Optional[str] = field(default_factory=default_gen_id)
91
-
92
- # NOTE: Ignore this field to compare another result model with __eq__.
93
68
  parent_run_id: Optional[str] = field(default=None, compare=False)
94
69
  event: Event = field(default_factory=Event, compare=False)
95
70
  ts: datetime = field(default_factory=get_dt_tznow, compare=False)
96
-
97
- def set_run_id(self, running_id: str) -> Self:
98
- """Set a running ID.
99
-
100
- :param running_id: A running ID that want to update on this model.
101
- :rtype: Self
71
+ trace: Optional[TraceLog] = field(default=None)
72
+
73
+ @classmethod
74
+ def construct_with_rs_or_id(
75
+ cls,
76
+ result: Result | None = None,
77
+ run_id: str | None = None,
78
+ parent_run_id: str | None = None,
79
+ id_logic: str | None = None,
80
+ ) -> Self: # pragma: no cov
81
+ """Create the Result object or set parent running id if passing Result
82
+ object.
102
83
  """
103
- self.run_id: str = running_id
84
+ if result is None:
85
+ result: Result = cls(
86
+ run_id=(run_id or gen_id(id_logic or "", unique=True)),
87
+ parent_run_id=parent_run_id,
88
+ )
89
+ elif parent_run_id:
90
+ result.set_parent_run_id(parent_run_id)
91
+ return result
92
+
93
+ @model_validator(mode="after")
94
+ def __prepare_trace(self) -> Self:
95
+ """Prepare trace field that want to pass after its initialize step."""
96
+ if self.trace is None: # pragma: no cove
97
+ self.trace: TraceLog = get_trace(self.run_id, self.parent_run_id)
98
+
104
99
  return self
105
100
 
106
101
  def set_parent_run_id(self, running_id: str) -> Self:
@@ -110,6 +105,7 @@ class Result:
110
105
  :rtype: Self
111
106
  """
112
107
  self.parent_run_id: str = running_id
108
+ self.trace: TraceLog = get_trace(self.run_id, running_id)
113
109
  return self
114
110
 
115
111
  def catch(
@@ -130,26 +126,9 @@ class Result:
130
126
  self.__dict__["context"].update(context or {})
131
127
  return self
132
128
 
133
- def receive(self, result: Result) -> Self:
134
- """Receive context from another result object.
135
-
136
- :rtype: Self
137
- """
138
- self.__dict__["status"] = result.status
139
- self.__dict__["context"].update(result.context)
140
-
141
- # NOTE: Update running ID from an incoming result.
142
- self.parent_run_id = result.parent_run_id
143
- self.run_id = result.run_id
144
- return self
145
-
146
- @property
147
- def trace(self) -> TraceLog:
148
- """Return TraceLog object that passing its running ID.
129
+ def alive_time(self) -> float: # pragma: no cov
130
+ """Return total seconds that this object use since it was created.
149
131
 
150
- :rtype: TraceLog
132
+ :rtype: float
151
133
  """
152
- return TraceLog(self.run_id)
153
-
154
- def alive_time(self) -> float: # pragma: no cov
155
134
  return (get_dt_tznow() - self.ts).total_seconds()