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/audit.py
CHANGED
@@ -20,7 +20,7 @@ from typing_extensions import Self
|
|
20
20
|
|
21
21
|
from .__types import DictData, TupleStr
|
22
22
|
from .conf import config
|
23
|
-
from .
|
23
|
+
from .logs import TraceLog, get_trace
|
24
24
|
|
25
25
|
__all__: TupleStr = (
|
26
26
|
"get_audit",
|
@@ -112,7 +112,8 @@ class FileAudit(BaseAudit):
|
|
112
112
|
:param release: A release datetime that want to search log.
|
113
113
|
|
114
114
|
:raise FileNotFoundError:
|
115
|
-
:raise NotImplementedError:
|
115
|
+
:raise NotImplementedError: If an input release does not pass to this
|
116
|
+
method. Because this method does not implement latest log.
|
116
117
|
|
117
118
|
:rtype: Self
|
118
119
|
"""
|
@@ -174,14 +175,16 @@ class FileAudit(BaseAudit):
|
|
174
175
|
|
175
176
|
:rtype: Self
|
176
177
|
"""
|
177
|
-
trace: TraceLog =
|
178
|
+
trace: TraceLog = get_trace(self.run_id, self.parent_run_id)
|
178
179
|
|
179
180
|
# NOTE: Check environ variable was set for real writing.
|
180
181
|
if not config.enable_write_audit:
|
181
182
|
trace.debug("[LOG]: Skip writing log cause config was set")
|
182
183
|
return self
|
183
184
|
|
184
|
-
log_file: Path =
|
185
|
+
log_file: Path = (
|
186
|
+
self.pointer() / f"{self.parent_run_id or self.run_id}.log"
|
187
|
+
)
|
185
188
|
log_file.write_text(
|
186
189
|
json.dumps(
|
187
190
|
self.model_dump(exclude=excluded),
|
@@ -196,7 +199,7 @@ class FileAudit(BaseAudit):
|
|
196
199
|
class SQLiteAudit(BaseAudit): # pragma: no cov
|
197
200
|
"""SQLite Audit Pydantic Model."""
|
198
201
|
|
199
|
-
table_name: ClassVar[str] = "
|
202
|
+
table_name: ClassVar[str] = "audits"
|
200
203
|
schemas: ClassVar[
|
201
204
|
str
|
202
205
|
] = """
|
@@ -214,7 +217,7 @@ class SQLiteAudit(BaseAudit): # pragma: no cov
|
|
214
217
|
"""Save logging data that receive a context data from a workflow
|
215
218
|
execution result.
|
216
219
|
"""
|
217
|
-
trace: TraceLog =
|
220
|
+
trace: TraceLog = get_trace(self.run_id, self.parent_run_id)
|
218
221
|
|
219
222
|
# NOTE: Check environ variable was set for real writing.
|
220
223
|
if not config.enable_write_audit:
|
@@ -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 call function.
|
63
|
+
# NOTE: Able to do anything before calling the call function.
|
64
64
|
return func(*args, **kwargs)
|
65
65
|
|
66
66
|
return wrapped
|
@@ -150,7 +150,7 @@ def extract_call(call: str) -> Callable[[], TagFunc]:
|
|
150
150
|
"""
|
151
151
|
if not (found := Re.RE_TASK_FMT.search(call)):
|
152
152
|
raise ValueError(
|
153
|
-
f"Call {call!r} does not match with call format
|
153
|
+
f"Call {call!r} does not match with the call regex format."
|
154
154
|
)
|
155
155
|
|
156
156
|
# NOTE: Pass the searching call string to `path`, `func`, and `tag`.
|
@@ -160,13 +160,13 @@ def extract_call(call: str) -> Callable[[], TagFunc]:
|
|
160
160
|
rgt: dict[str, Registry] = make_registry(f"{call.path}")
|
161
161
|
if call.func not in rgt:
|
162
162
|
raise NotImplementedError(
|
163
|
-
f"
|
163
|
+
f"`REGISTER-MODULES.{call.path}.registries` does not "
|
164
164
|
f"implement registry: {call.func!r}."
|
165
165
|
)
|
166
166
|
|
167
167
|
if call.tag not in rgt[call.func]:
|
168
168
|
raise NotImplementedError(
|
169
169
|
f"tag: {call.tag!r} does not found on registry func: "
|
170
|
-
f"
|
170
|
+
f"`REGISTER-MODULES.{call.path}.registries.{call.func}`"
|
171
171
|
)
|
172
172
|
return rgt[call.func][call.tag]
|
ddeutil/workflow/job.py
CHANGED
@@ -5,7 +5,7 @@
|
|
5
5
|
# ------------------------------------------------------------------------------
|
6
6
|
"""Job Model that use for keeping stages and node that running its stages.
|
7
7
|
The job handle the lineage of stages and location of execution of stages that
|
8
|
-
mean the job model able to define
|
8
|
+
mean the job model able to define `runs-on` key that allow you to run this
|
9
9
|
job.
|
10
10
|
|
11
11
|
This module include Strategy Model that use on the job strategy field.
|
@@ -24,15 +24,15 @@ from enum import Enum
|
|
24
24
|
from functools import lru_cache
|
25
25
|
from textwrap import dedent
|
26
26
|
from threading import Event
|
27
|
-
from typing import Any, Optional, Union
|
27
|
+
from typing import Annotated, Any, Literal, Optional, Union
|
28
28
|
|
29
29
|
from ddeutil.core import freeze_args
|
30
|
-
from pydantic import BaseModel, Field
|
30
|
+
from pydantic import BaseModel, ConfigDict, Field
|
31
31
|
from pydantic.functional_validators import field_validator, model_validator
|
32
32
|
from typing_extensions import Self
|
33
33
|
|
34
34
|
from .__types import DictData, DictStr, Matrix, TupleStr
|
35
|
-
from .conf import config
|
35
|
+
from .conf import config
|
36
36
|
from .exceptions import (
|
37
37
|
JobException,
|
38
38
|
StageException,
|
@@ -48,7 +48,6 @@ from .utils import (
|
|
48
48
|
gen_id,
|
49
49
|
)
|
50
50
|
|
51
|
-
logger = get_logger("ddeutil.workflow")
|
52
51
|
MatrixFilter = list[dict[str, Union[str, int]]]
|
53
52
|
|
54
53
|
|
@@ -56,6 +55,10 @@ __all__: TupleStr = (
|
|
56
55
|
"Strategy",
|
57
56
|
"Job",
|
58
57
|
"TriggerRules",
|
58
|
+
"RunsOn",
|
59
|
+
"RunsOnLocal",
|
60
|
+
"RunsOnSelfHosted",
|
61
|
+
"RunsOnK8s",
|
59
62
|
"make",
|
60
63
|
)
|
61
64
|
|
@@ -216,13 +219,52 @@ class TriggerRules(str, Enum):
|
|
216
219
|
none_skipped: str = "none_skipped"
|
217
220
|
|
218
221
|
|
219
|
-
class
|
222
|
+
class RunsOnType(str, Enum):
|
220
223
|
"""Runs-On enum object."""
|
221
224
|
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
225
|
+
LOCAL: str = "local"
|
226
|
+
SELF_HOSTED: str = "self_hosted"
|
227
|
+
K8S: str = "k8s"
|
228
|
+
|
229
|
+
|
230
|
+
class BaseRunsOn(BaseModel): # pragma: no cov
|
231
|
+
model_config = ConfigDict(use_enum_values=True)
|
232
|
+
|
233
|
+
type: Literal[RunsOnType.LOCAL]
|
234
|
+
args: DictData = Field(
|
235
|
+
default_factory=dict,
|
236
|
+
alias="with",
|
237
|
+
)
|
238
|
+
|
239
|
+
|
240
|
+
class RunsOnLocal(BaseRunsOn): # pragma: no cov
|
241
|
+
"""Runs-on local."""
|
242
|
+
|
243
|
+
type: Literal[RunsOnType.LOCAL] = Field(default=RunsOnType.LOCAL)
|
244
|
+
|
245
|
+
|
246
|
+
class RunsOnSelfHosted(BaseRunsOn): # pragma: no cov
|
247
|
+
"""Runs-on self-hosted."""
|
248
|
+
|
249
|
+
type: Literal[RunsOnType.SELF_HOSTED] = Field(
|
250
|
+
default=RunsOnType.SELF_HOSTED
|
251
|
+
)
|
252
|
+
|
253
|
+
|
254
|
+
class RunsOnK8s(BaseRunsOn): # pragma: no cov
|
255
|
+
"""Runs-on Kubernetes."""
|
256
|
+
|
257
|
+
type: Literal[RunsOnType.K8S] = Field(default=RunsOnType.K8S)
|
258
|
+
|
259
|
+
|
260
|
+
RunsOn = Annotated[
|
261
|
+
Union[
|
262
|
+
RunsOnLocal,
|
263
|
+
RunsOnSelfHosted,
|
264
|
+
RunsOnK8s,
|
265
|
+
],
|
266
|
+
Field(discriminator="type"),
|
267
|
+
]
|
226
268
|
|
227
269
|
|
228
270
|
class Job(BaseModel):
|
@@ -234,7 +276,7 @@ class Job(BaseModel):
|
|
234
276
|
|
235
277
|
Data Validate:
|
236
278
|
>>> job = {
|
237
|
-
... "runs-on":
|
279
|
+
... "runs-on": {"type": "local"},
|
238
280
|
... "strategy": {
|
239
281
|
... "max-parallel": 1,
|
240
282
|
... "matrix": {
|
@@ -263,9 +305,9 @@ class Job(BaseModel):
|
|
263
305
|
default=None,
|
264
306
|
description="A job description that can be string of markdown content.",
|
265
307
|
)
|
266
|
-
runs_on:
|
267
|
-
|
268
|
-
description="A target
|
308
|
+
runs_on: RunsOn = Field(
|
309
|
+
default_factory=RunsOnLocal,
|
310
|
+
description="A target node for this job to use for execution.",
|
269
311
|
serialization_alias="runs-on",
|
270
312
|
)
|
271
313
|
stages: list[Stage] = Field(
|
@@ -359,7 +401,7 @@ class Job(BaseModel):
|
|
359
401
|
|
360
402
|
def set_outputs(self, output: DictData, to: DictData) -> DictData:
|
361
403
|
"""Set an outputs from execution process to the received context. The
|
362
|
-
result from execution will pass to value of
|
404
|
+
result from execution will pass to value of `strategies` key.
|
363
405
|
|
364
406
|
For example of setting output method, If you receive execute output
|
365
407
|
and want to set on the `to` like;
|
@@ -424,14 +466,14 @@ class Job(BaseModel):
|
|
424
466
|
workflow execution to strategy matrix.
|
425
467
|
|
426
468
|
This execution is the minimum level of execution of this job model.
|
427
|
-
It different with
|
469
|
+
It different with `self.execute` because this method run only one
|
428
470
|
strategy and return with context of this strategy data.
|
429
471
|
|
430
472
|
The result of this execution will return result with strategy ID
|
431
473
|
that generated from the `gen_id` function with an input strategy value.
|
432
474
|
|
433
|
-
:raise JobException: If it has any error from
|
434
|
-
|
475
|
+
:raise JobException: If it has any error from `StageException` or
|
476
|
+
`UtilException`.
|
435
477
|
|
436
478
|
:param strategy: A strategy metrix value that use on this execution.
|
437
479
|
This value will pass to the `matrix` key for templating.
|
@@ -510,7 +552,7 @@ class Job(BaseModel):
|
|
510
552
|
#
|
511
553
|
# ... params |= stage.execute(params=params)
|
512
554
|
#
|
513
|
-
# This step will add the stage result to
|
555
|
+
# This step will add the stage result to `stages` key in
|
514
556
|
# that stage id. It will have structure like;
|
515
557
|
#
|
516
558
|
# {
|
@@ -581,7 +623,7 @@ class Job(BaseModel):
|
|
581
623
|
) -> Result:
|
582
624
|
"""Job execution with passing dynamic parameters from the workflow
|
583
625
|
execution. It will generate matrix values at the first step and run
|
584
|
-
multithread on this metrics to the
|
626
|
+
multithread on this metrics to the `stages` field of this job.
|
585
627
|
|
586
628
|
:param params: An input parameters that use on job execution.
|
587
629
|
:param run_id: A job running ID for this execution.
|
@@ -591,15 +633,12 @@ class Job(BaseModel):
|
|
591
633
|
|
592
634
|
:rtype: Result
|
593
635
|
"""
|
594
|
-
|
595
|
-
# NOTE: I use this condition because this method allow passing empty
|
596
|
-
# params and I do not want to create new dict object.
|
597
636
|
if result is None: # pragma: no cov
|
598
637
|
result: Result = Result(
|
599
638
|
run_id=(run_id or gen_id(self.id or "", unique=True)),
|
600
639
|
parent_run_id=parent_run_id,
|
601
640
|
)
|
602
|
-
elif parent_run_id:
|
641
|
+
elif parent_run_id: # pragma: no cov
|
603
642
|
result.set_parent_run_id(parent_run_id)
|
604
643
|
|
605
644
|
# NOTE: Normal Job execution without parallel strategy matrix. It uses
|
ddeutil/workflow/logs.py
ADDED
@@ -0,0 +1,326 @@
|
|
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
|
+
"""A Logs module contain a TraceLog dataclass.
|
7
|
+
"""
|
8
|
+
from __future__ import annotations
|
9
|
+
|
10
|
+
import json
|
11
|
+
import os
|
12
|
+
from abc import ABC, abstractmethod
|
13
|
+
from collections.abc import Iterator
|
14
|
+
from datetime import datetime
|
15
|
+
from inspect import Traceback, currentframe, getframeinfo
|
16
|
+
from pathlib import Path
|
17
|
+
from threading import get_ident
|
18
|
+
from typing import ClassVar, Literal, Optional, Union
|
19
|
+
|
20
|
+
from pydantic import BaseModel, Field
|
21
|
+
from pydantic.dataclasses import dataclass
|
22
|
+
from typing_extensions import Self
|
23
|
+
|
24
|
+
from .__types import DictStr, TupleStr
|
25
|
+
from .conf import config, get_logger
|
26
|
+
from .utils import cut_id, get_dt_now
|
27
|
+
|
28
|
+
logger = get_logger("ddeutil.workflow")
|
29
|
+
|
30
|
+
__all__: TupleStr = (
|
31
|
+
"FileTraceLog",
|
32
|
+
"SQLiteTraceLog",
|
33
|
+
"TraceData",
|
34
|
+
"TraceMeda",
|
35
|
+
"TraceLog",
|
36
|
+
"get_dt_tznow",
|
37
|
+
"get_trace",
|
38
|
+
"get_trace_obj",
|
39
|
+
)
|
40
|
+
|
41
|
+
|
42
|
+
def get_dt_tznow() -> datetime: # pragma: no cov
|
43
|
+
"""Return the current datetime object that passing the config timezone.
|
44
|
+
|
45
|
+
:rtype: datetime
|
46
|
+
"""
|
47
|
+
return get_dt_now(tz=config.tz)
|
48
|
+
|
49
|
+
|
50
|
+
@dataclass(frozen=True)
|
51
|
+
class BaseTraceLog(ABC): # pragma: no cov
|
52
|
+
"""Base Trace Log dataclass object."""
|
53
|
+
|
54
|
+
run_id: str
|
55
|
+
parent_run_id: Optional[str] = None
|
56
|
+
|
57
|
+
@abstractmethod
|
58
|
+
def writer(self, message: str, is_err: bool = False) -> None:
|
59
|
+
"""Write a trace message after making to target pointer object. The
|
60
|
+
target can be anything be inherited this class and overwrite this method
|
61
|
+
such as file, console, or database.
|
62
|
+
|
63
|
+
:param message: A message after making.
|
64
|
+
:param is_err: A flag for writing with an error trace or not.
|
65
|
+
"""
|
66
|
+
raise NotImplementedError(
|
67
|
+
"Create writer logic for this trace object before using."
|
68
|
+
)
|
69
|
+
|
70
|
+
@abstractmethod
|
71
|
+
def make_message(self, message: str) -> str:
|
72
|
+
"""Prepare and Make a message before write and log processes.
|
73
|
+
|
74
|
+
:param message: A message that want to prepare and make before.
|
75
|
+
|
76
|
+
:rtype: str
|
77
|
+
"""
|
78
|
+
raise NotImplementedError(
|
79
|
+
"Adjust make message method for this trace object before using."
|
80
|
+
)
|
81
|
+
|
82
|
+
def debug(self, message: str):
|
83
|
+
"""Write trace log with append mode and logging this message with the
|
84
|
+
DEBUG level.
|
85
|
+
|
86
|
+
:param message: (str) A message that want to log.
|
87
|
+
"""
|
88
|
+
msg: str = self.make_message(message)
|
89
|
+
|
90
|
+
# NOTE: Write file if debug mode was enabled.
|
91
|
+
if config.debug:
|
92
|
+
self.writer(msg)
|
93
|
+
|
94
|
+
logger.debug(msg, stacklevel=2)
|
95
|
+
|
96
|
+
def info(self, message: str) -> None:
|
97
|
+
"""Write trace log with append mode and logging this message with the
|
98
|
+
INFO level.
|
99
|
+
|
100
|
+
:param message: (str) A message that want to log.
|
101
|
+
"""
|
102
|
+
msg: str = self.make_message(message)
|
103
|
+
self.writer(msg)
|
104
|
+
logger.info(msg, stacklevel=2)
|
105
|
+
|
106
|
+
def warning(self, message: str) -> None:
|
107
|
+
"""Write trace log with append mode and logging this message with the
|
108
|
+
WARNING level.
|
109
|
+
|
110
|
+
:param message: (str) A message that want to log.
|
111
|
+
"""
|
112
|
+
msg: str = self.make_message(message)
|
113
|
+
self.writer(msg)
|
114
|
+
logger.warning(msg, stacklevel=2)
|
115
|
+
|
116
|
+
def error(self, message: str) -> None:
|
117
|
+
"""Write trace log with append mode and logging this message with the
|
118
|
+
ERROR level.
|
119
|
+
|
120
|
+
:param message: (str) A message that want to log.
|
121
|
+
"""
|
122
|
+
msg: str = self.make_message(message)
|
123
|
+
self.writer(msg, is_err=True)
|
124
|
+
logger.error(msg, stacklevel=2)
|
125
|
+
|
126
|
+
def exception(self, message: str) -> None:
|
127
|
+
"""Write trace log with append mode and logging this message with the
|
128
|
+
EXCEPTION level.
|
129
|
+
|
130
|
+
:param message: (str) A message that want to log.
|
131
|
+
"""
|
132
|
+
msg: str = self.make_message(message)
|
133
|
+
self.writer(msg, is_err=True)
|
134
|
+
logger.exception(msg, stacklevel=2)
|
135
|
+
|
136
|
+
|
137
|
+
class TraceMeda(BaseModel): # pragma: no cov
|
138
|
+
mode: Literal["stdout", "stderr"]
|
139
|
+
datetime: str
|
140
|
+
process: int
|
141
|
+
thread: int
|
142
|
+
message: str
|
143
|
+
filename: str
|
144
|
+
lineno: int
|
145
|
+
|
146
|
+
|
147
|
+
class TraceData(BaseModel): # pragma: no cov
|
148
|
+
stdout: str = Field(description="A standard output trace data.")
|
149
|
+
stderr: str = Field(description="A standard error trace data.")
|
150
|
+
meta: list[TraceMeda] = Field(
|
151
|
+
default_factory=list,
|
152
|
+
description=(
|
153
|
+
"A metadata mapping of this output and error before making it to "
|
154
|
+
"standard value."
|
155
|
+
),
|
156
|
+
)
|
157
|
+
|
158
|
+
@classmethod
|
159
|
+
def from_path(cls, file: Path) -> Self:
|
160
|
+
data: DictStr = {"stdout": "", "stderr": "", "meta": []}
|
161
|
+
|
162
|
+
if (file / "stdout.txt").exists():
|
163
|
+
data["stdout"] = (file / "stdout.txt").read_text(encoding="utf-8")
|
164
|
+
|
165
|
+
if (file / "stderr.txt").exists():
|
166
|
+
data["stderr"] = (file / "stderr.txt").read_text(encoding="utf-8")
|
167
|
+
|
168
|
+
if (file / "metadata.json").exists():
|
169
|
+
data["meta"] = [
|
170
|
+
json.loads(line)
|
171
|
+
for line in (
|
172
|
+
(file / "metadata.json")
|
173
|
+
.read_text(encoding="utf-8")
|
174
|
+
.splitlines()
|
175
|
+
)
|
176
|
+
]
|
177
|
+
|
178
|
+
return cls.model_validate(data)
|
179
|
+
|
180
|
+
|
181
|
+
class FileTraceLog(BaseTraceLog): # pragma: no cov
|
182
|
+
"""Trace Log object that write file to the local storage."""
|
183
|
+
|
184
|
+
@classmethod
|
185
|
+
def find_logs(cls) -> Iterator[TraceData]: # pragma: no cov
|
186
|
+
for file in sorted(
|
187
|
+
config.log_path.glob("./run_id=*"),
|
188
|
+
key=lambda f: f.lstat().st_mtime,
|
189
|
+
):
|
190
|
+
yield TraceData.from_path(file)
|
191
|
+
|
192
|
+
@classmethod
|
193
|
+
def find_log_with_id(
|
194
|
+
cls, run_id: str, force_raise: bool = True
|
195
|
+
) -> TraceData:
|
196
|
+
file: Path = config.log_path / f"run_id={run_id}"
|
197
|
+
if file.exists():
|
198
|
+
return TraceData.from_path(file)
|
199
|
+
elif force_raise:
|
200
|
+
raise FileNotFoundError(
|
201
|
+
f"Trace log on path 'run_id={run_id}' does not found."
|
202
|
+
)
|
203
|
+
return {}
|
204
|
+
|
205
|
+
@property
|
206
|
+
def pointer(self) -> Path:
|
207
|
+
log_file: Path = (
|
208
|
+
config.log_path / f"run_id={self.parent_run_id or self.run_id}"
|
209
|
+
)
|
210
|
+
if not log_file.exists():
|
211
|
+
log_file.mkdir(parents=True)
|
212
|
+
return log_file
|
213
|
+
|
214
|
+
@property
|
215
|
+
def cut_id(self) -> str:
|
216
|
+
"""Combine cutting ID of parent running ID if it set.
|
217
|
+
|
218
|
+
:rtype: str
|
219
|
+
"""
|
220
|
+
cut_run_id: str = cut_id(self.run_id)
|
221
|
+
if not self.parent_run_id:
|
222
|
+
return f"{cut_run_id} -> {' ' * 6}"
|
223
|
+
|
224
|
+
cut_parent_run_id: str = cut_id(self.parent_run_id)
|
225
|
+
return f"{cut_parent_run_id} -> {cut_run_id}"
|
226
|
+
|
227
|
+
def make_message(self, message: str) -> str:
|
228
|
+
"""Prepare and Make a message before write and log processes.
|
229
|
+
|
230
|
+
:param message: A message that want to prepare and make before.
|
231
|
+
|
232
|
+
:rtype: str
|
233
|
+
"""
|
234
|
+
return f"({self.cut_id}) {message}"
|
235
|
+
|
236
|
+
def writer(self, message: str, is_err: bool = False) -> None:
|
237
|
+
""" "Write a trace message after making to target file and write metadata
|
238
|
+
in the same path of standard files.
|
239
|
+
|
240
|
+
The path of logging data will store by format:
|
241
|
+
|
242
|
+
... ./logs/run_id=<run-id>/metadata.json
|
243
|
+
... ./logs/run_id=<run-id>/stdout.txt
|
244
|
+
... ./logs/run_id=<run-id>/stderr.txt
|
245
|
+
|
246
|
+
:param message: A message after making.
|
247
|
+
:param is_err: A flag for writing with an error trace or not.
|
248
|
+
"""
|
249
|
+
if not config.enable_write_log:
|
250
|
+
return
|
251
|
+
|
252
|
+
frame_info: Traceback = getframeinfo(currentframe().f_back.f_back)
|
253
|
+
filename: str = frame_info.filename.split(os.path.sep)[-1]
|
254
|
+
lineno: int = frame_info.lineno
|
255
|
+
|
256
|
+
# NOTE: set process and thread IDs.
|
257
|
+
process: int = os.getpid()
|
258
|
+
thread: int = get_ident()
|
259
|
+
|
260
|
+
write_file: str = "stderr.txt" if is_err else "stdout.txt"
|
261
|
+
write_data: dict[str, Union[str, int]] = {
|
262
|
+
"datetime": get_dt_tznow().strftime(config.log_datetime_format),
|
263
|
+
"process": process,
|
264
|
+
"thread": thread,
|
265
|
+
"message": message,
|
266
|
+
"filename": filename,
|
267
|
+
"lineno": lineno,
|
268
|
+
}
|
269
|
+
|
270
|
+
with (self.pointer / write_file).open(mode="at", encoding="utf-8") as f:
|
271
|
+
msg_fmt: str = f"{config.log_format_file}\n"
|
272
|
+
f.write(msg_fmt.format(**write_data))
|
273
|
+
|
274
|
+
with (self.pointer / "metadata.json").open(
|
275
|
+
mode="at", encoding="utf-8"
|
276
|
+
) as f:
|
277
|
+
f.write(
|
278
|
+
json.dumps({"mode": write_file.split(".")[0]} | write_data)
|
279
|
+
+ "\n"
|
280
|
+
)
|
281
|
+
|
282
|
+
|
283
|
+
class SQLiteTraceLog(BaseTraceLog): # pragma: no cov
|
284
|
+
"""Trace Log object that write trace log to the SQLite database file."""
|
285
|
+
|
286
|
+
table_name: ClassVar[str] = "audits"
|
287
|
+
schemas: ClassVar[
|
288
|
+
str
|
289
|
+
] = """
|
290
|
+
run_id int,
|
291
|
+
stdout str,
|
292
|
+
stderr str,
|
293
|
+
update datetime
|
294
|
+
primary key ( run_id )
|
295
|
+
"""
|
296
|
+
|
297
|
+
@classmethod
|
298
|
+
def find_logs(cls) -> Iterator[DictStr]: ...
|
299
|
+
|
300
|
+
@classmethod
|
301
|
+
def find_log_with_id(cls, run_id: str) -> DictStr: ...
|
302
|
+
|
303
|
+
def make_message(self, message: str) -> str: ...
|
304
|
+
|
305
|
+
def writer(self, message: str, is_err: bool = False) -> None: ...
|
306
|
+
|
307
|
+
|
308
|
+
TraceLog = Union[
|
309
|
+
FileTraceLog,
|
310
|
+
SQLiteTraceLog,
|
311
|
+
]
|
312
|
+
|
313
|
+
|
314
|
+
def get_trace(
|
315
|
+
run_id: str, parent_run_id: str | None = None
|
316
|
+
) -> TraceLog: # pragma: no cov
|
317
|
+
"""Get dynamic TraceLog object from the setting config."""
|
318
|
+
if config.log_path.is_file():
|
319
|
+
return SQLiteTraceLog(run_id, parent_run_id=parent_run_id)
|
320
|
+
return FileTraceLog(run_id, parent_run_id=parent_run_id)
|
321
|
+
|
322
|
+
|
323
|
+
def get_trace_obj() -> type[TraceLog]: # pragma: no cov
|
324
|
+
if config.log_path.is_file():
|
325
|
+
return SQLiteTraceLog
|
326
|
+
return FileTraceLog
|