logger-36 2025.24__py3-none-any.whl → 2025.25__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.
- logger_36/api/{time.py → chronos.py} +1 -1
- logger_36/catalog/handler/console_rich.py +32 -21
- logger_36/catalog/handler/file.py +1 -2
- logger_36/catalog/handler/generic.py +23 -9
- logger_36/catalog/handler/memory.py +24 -2
- logger_36/catalog/logger/chronos.py +10 -2
- logger_36/catalog/logger/memory.py +1 -1
- logger_36/config/message.py +1 -4
- logger_36/constant/{date_time.py → chronos.py} +4 -0
- logger_36/constant/error.py +2 -0
- logger_36/constant/html.py +2 -0
- logger_36/constant/message.py +7 -7
- logger_36/constant/record.py +2 -0
- logger_36/extension/{record.py → file.py} +11 -21
- logger_36/extension/line.py +1 -1
- logger_36/task/format/memory.py +1 -1
- logger_36/task/format/message.py +7 -5
- logger_36/task/measure/chronos.py +21 -11
- logger_36/task/storage.py +30 -55
- logger_36/type/handler.py +21 -23
- logger_36/type/issue.py +7 -4
- logger_36/type/logger.py +136 -44
- logger_36/version.py +1 -1
- {logger_36-2025.24.dist-info → logger_36-2025.25.dist-info}/METADATA +30 -44
- logger_36-2025.25.dist-info/RECORD +53 -0
- logger_36-2025.24.dist-info/RECORD +0 -53
- /logger_36/{constant/generic.py → extension/sentinel.py} +0 -0
- {logger_36-2025.24.dist-info → logger_36-2025.25.dist-info}/WHEEL +0 -0
- {logger_36-2025.24.dist-info → logger_36-2025.25.dist-info}/top_level.txt +0 -0
@@ -4,7 +4,7 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
|
|
4
4
|
SEE COPYRIGHT NOTICE BELOW
|
5
5
|
"""
|
6
6
|
|
7
|
-
from logger_36.task.measure.chronos import
|
7
|
+
from logger_36.task.measure.chronos import FormattedElapsedTime, TimeStamp # noqa
|
8
8
|
|
9
9
|
"""
|
10
10
|
COPYRIGHT NOTICE
|
@@ -11,6 +11,7 @@ from rich.console import Console as console_t # noqa
|
|
11
11
|
from rich.console import RenderableType as renderable_t # noqa
|
12
12
|
from rich.markup import escape as EscapedVersion # noqa
|
13
13
|
from rich.rule import Rule as rule_t # noqa
|
14
|
+
from rich.style import Style as style_t # noqa
|
14
15
|
from rich.text import Text as text_t # noqa
|
15
16
|
from rich.traceback import install as InstallTracebackHandler # noqa
|
16
17
|
|
@@ -24,8 +25,9 @@ from logger_36.catalog.config.console_rich import (
|
|
24
25
|
RULE_COLOR,
|
25
26
|
WHITE_COLOR,
|
26
27
|
)
|
27
|
-
from logger_36.config.message import ACTUAL_PATTERNS, EXPECTED_PATTERNS
|
28
|
+
from logger_36.config.message import ACTUAL_PATTERNS, EXPECTED_PATTERNS
|
28
29
|
from logger_36.constant.message import CONTEXT_LENGTH
|
30
|
+
from logger_36.constant.record import HAS_ACTUAL_EXPECTED_ATTR
|
29
31
|
from logger_36.type.handler import non_file_handler_t as base_t
|
30
32
|
|
31
33
|
_COMMON_TRACEBACK_ARGUMENTS = ("theme", "width")
|
@@ -62,8 +64,13 @@ class console_rich_handler_t(base_t):
|
|
62
64
|
base_t.__init__(self, name, message_width, EscapedVersion, level, kwargs)
|
63
65
|
|
64
66
|
self.console = None # console_t | None.
|
65
|
-
|
66
|
-
|
67
|
+
if alternating_logs == 0:
|
68
|
+
self.background_style = None
|
69
|
+
elif alternating_logs == 1:
|
70
|
+
self.background_style = ALTERNATIVE_BACKGROUND_FOR_DARK
|
71
|
+
else:
|
72
|
+
self.background_style = ALTERNATIVE_BACKGROUND_FOR_LIGHT
|
73
|
+
self._should_style_background = False
|
67
74
|
|
68
75
|
self.__post_init_local__(should_install_traceback, **kwargs)
|
69
76
|
|
@@ -104,17 +111,24 @@ class console_rich_handler_t(base_t):
|
|
104
111
|
|
105
112
|
def emit(self, record: l.LogRecord, /) -> None:
|
106
113
|
""""""
|
107
|
-
message, is_not_a_rule = self.MessageFromRecord(
|
114
|
+
message, is_not_a_rule, where_location = self.MessageFromRecord(
|
115
|
+
record, rule_color=RULE_COLOR
|
116
|
+
)
|
108
117
|
if is_not_a_rule:
|
118
|
+
if self._should_style_background:
|
119
|
+
background_style = self.background_style
|
120
|
+
else:
|
121
|
+
background_style = None
|
109
122
|
message = HighlightedVersion(
|
110
123
|
self.console,
|
111
124
|
message,
|
125
|
+
getattr(record, HAS_ACTUAL_EXPECTED_ATTR, False),
|
126
|
+
where_location,
|
112
127
|
record.levelno,
|
113
|
-
|
114
|
-
self._log_parity,
|
128
|
+
background_style,
|
115
129
|
)
|
116
130
|
self.console.print(message, crop=False, overflow="ignore")
|
117
|
-
self.
|
131
|
+
self._should_style_background = not self._should_style_background
|
118
132
|
|
119
133
|
def EmitMessage(self, message: str | renderable_t, /) -> None:
|
120
134
|
""""""
|
@@ -124,27 +138,24 @@ class console_rich_handler_t(base_t):
|
|
124
138
|
def HighlightedVersion(
|
125
139
|
_: console_t,
|
126
140
|
message: str,
|
141
|
+
has_actual_expected: bool,
|
142
|
+
where_location: int | None,
|
127
143
|
log_level: int,
|
128
|
-
|
129
|
-
should_tint_background: bool,
|
144
|
+
background_style: style_t | None,
|
130
145
|
/,
|
131
146
|
) -> renderable_t:
|
132
147
|
""""""
|
133
148
|
output = text_t(message, WHITE_COLOR)
|
134
149
|
|
135
150
|
output.stylize(LEVEL_COLOR[log_level], end=CONTEXT_LENGTH)
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
if
|
143
|
-
|
144
|
-
style = ALTERNATIVE_BACKGROUND_FOR_DARK
|
145
|
-
else:
|
146
|
-
style = ALTERNATIVE_BACKGROUND_FOR_LIGHT
|
147
|
-
output.stylize(style)
|
151
|
+
if where_location is not None:
|
152
|
+
output.stylize(GRAY_COLOR, start=where_location)
|
153
|
+
if has_actual_expected:
|
154
|
+
_ = output.highlight_words(ACTUAL_PATTERNS, style=ACTUAL_COLOR)
|
155
|
+
_ = output.highlight_regex(EXPECTED_PATTERNS, style=EXPECTED_COLOR)
|
156
|
+
|
157
|
+
if background_style is not None:
|
158
|
+
output.stylize(background_style)
|
148
159
|
|
149
160
|
return output
|
150
161
|
|
@@ -28,8 +28,7 @@ class file_handler_t(base_t):
|
|
28
28
|
|
29
29
|
def emit(self, record: l.LogRecord, /) -> None:
|
30
30
|
""""""
|
31
|
-
|
32
|
-
self.stream.write(output[0] + "\n")
|
31
|
+
self.stream.write(self.MessageFromRecord(record)[0] + "\n")
|
33
32
|
self.stream.flush()
|
34
33
|
|
35
34
|
def EmitMessage(self, message: str, /) -> None:
|
@@ -8,16 +8,20 @@ import logging as l
|
|
8
8
|
import typing as h
|
9
9
|
|
10
10
|
from logger_36.catalog.config.optional import RICH_IS_AVAILABLE
|
11
|
+
from logger_36.constant.record import HAS_ACTUAL_EXPECTED_ATTR
|
11
12
|
|
12
13
|
if RICH_IS_AVAILABLE:
|
14
|
+
from logger_36.catalog.config.console_rich import (
|
15
|
+
ALTERNATIVE_BACKGROUND_FOR_DARK,
|
16
|
+
ALTERNATIVE_BACKGROUND_FOR_LIGHT,
|
17
|
+
RULE_COLOR,
|
18
|
+
)
|
19
|
+
from logger_36.catalog.handler.console_rich import HighlightedVersion
|
13
20
|
from rich.console import Console as console_t
|
14
21
|
from rich.markup import escape as EscapedForRich
|
15
22
|
from rich.rule import Rule as rule_t
|
16
23
|
from rich.terminal_theme import DEFAULT_TERMINAL_THEME
|
17
24
|
from rich.text import Text as text_t
|
18
|
-
|
19
|
-
from logger_36.catalog.config.console_rich import RULE_COLOR
|
20
|
-
from logger_36.catalog.handler.console_rich import HighlightedVersion
|
21
25
|
else:
|
22
26
|
console_t = EscapedForRich = rule_t = DEFAULT_TERMINAL_THEME = text_t = (
|
23
27
|
RULE_COLOR
|
@@ -54,8 +58,13 @@ class generic_handler_t(base_t):
|
|
54
58
|
self.is_rich = False
|
55
59
|
self.console = None # console_t | None.
|
56
60
|
self.console_options = None # rich.console.ConsoleOptions | None.
|
57
|
-
|
58
|
-
|
61
|
+
if alternating_logs == 0:
|
62
|
+
self.background_style = None
|
63
|
+
elif alternating_logs == 1:
|
64
|
+
self.background_style = ALTERNATIVE_BACKGROUND_FOR_DARK
|
65
|
+
else:
|
66
|
+
self.background_style = ALTERNATIVE_BACKGROUND_FOR_LIGHT
|
67
|
+
self._should_style_background = False
|
59
68
|
|
60
69
|
self.__post_init_local__(supports_html)
|
61
70
|
|
@@ -94,16 +103,21 @@ class generic_handler_t(base_t):
|
|
94
103
|
def emit(self, record: l.LogRecord, /) -> None:
|
95
104
|
""""""
|
96
105
|
if self.is_rich:
|
97
|
-
message, is_not_a_rule = self.MessageFromRecord(
|
106
|
+
message, is_not_a_rule, where_location = self.MessageFromRecord(
|
98
107
|
record, rule_color=RULE_COLOR
|
99
108
|
)
|
100
109
|
if is_not_a_rule:
|
110
|
+
if self._should_style_background:
|
111
|
+
background_style = self.background_style
|
112
|
+
else:
|
113
|
+
background_style = None
|
101
114
|
message = HighlightedVersion(
|
102
115
|
self.console,
|
103
116
|
message,
|
117
|
+
getattr(record, HAS_ACTUAL_EXPECTED_ATTR, False),
|
118
|
+
where_location,
|
104
119
|
record.levelno,
|
105
|
-
|
106
|
-
self._log_parity,
|
120
|
+
background_style,
|
107
121
|
)
|
108
122
|
segments = self.console.render(message, options=self.console_options)
|
109
123
|
|
@@ -132,7 +146,7 @@ class generic_handler_t(base_t):
|
|
132
146
|
message = self.MessageFromRecord(record)[0]
|
133
147
|
|
134
148
|
self.EmitMessage(message)
|
135
|
-
self.
|
149
|
+
self._should_style_background = not self._should_style_background
|
136
150
|
|
137
151
|
|
138
152
|
"""
|
@@ -12,6 +12,10 @@ from logger_36.type.handler import non_file_handler_t as base_t
|
|
12
12
|
|
13
13
|
formats_h = h.Literal["dict", "json", "message", "raw"]
|
14
14
|
|
15
|
+
record_raw_h = dict | str | l.LogRecord
|
16
|
+
record_h = tuple[int, record_raw_h, bool] # level, [...], is_not_a_rule.
|
17
|
+
records_h = list[record_h]
|
18
|
+
|
15
19
|
|
16
20
|
class memory_handler_t(base_t):
|
17
21
|
def __init__(
|
@@ -23,7 +27,7 @@ class memory_handler_t(base_t):
|
|
23
27
|
base_t.__init__(self, name, message_width, None, level)
|
24
28
|
|
25
29
|
self.format_ = format_
|
26
|
-
self.records = []
|
30
|
+
self.records: records_h = []
|
27
31
|
|
28
32
|
@classmethod
|
29
33
|
def New(
|
@@ -52,7 +56,7 @@ class memory_handler_t(base_t):
|
|
52
56
|
record = json.dumps(record.__dict__)
|
53
57
|
is_not_a_rule = True
|
54
58
|
else:
|
55
|
-
record, is_not_a_rule = self.MessageFromRecord(record)
|
59
|
+
record, is_not_a_rule, _ = self.MessageFromRecord(record)
|
56
60
|
|
57
61
|
self.records.append((level, record, is_not_a_rule))
|
58
62
|
|
@@ -67,6 +71,24 @@ class memory_handler_t(base_t):
|
|
67
71
|
)
|
68
72
|
self.emit(record)
|
69
73
|
|
74
|
+
@staticmethod
|
75
|
+
def IsRecord(checked: h.Any, /) -> bool:
|
76
|
+
""""""
|
77
|
+
return (
|
78
|
+
isinstance(checked, tuple)
|
79
|
+
and (checked.__len__() == 3)
|
80
|
+
and isinstance(checked[0], int)
|
81
|
+
and isinstance(checked[1], record_raw_h)
|
82
|
+
and isinstance(checked[2], bool)
|
83
|
+
)
|
84
|
+
|
85
|
+
@classmethod
|
86
|
+
def AreRecords(cls, checked: h.Any, /) -> bool:
|
87
|
+
""""""
|
88
|
+
return isinstance(checked, list | tuple) and (
|
89
|
+
(checked.__len__() == 0) or all(map(cls.IsRecord, checked))
|
90
|
+
)
|
91
|
+
|
70
92
|
|
71
93
|
"""
|
72
94
|
COPYRIGHT NOTICE
|
@@ -4,14 +4,22 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
|
|
4
4
|
SEE COPYRIGHT NOTICE BELOW
|
5
5
|
"""
|
6
6
|
|
7
|
+
from datetime import datetime as date_time_t
|
8
|
+
|
9
|
+
from logger_36.constant.chronos import FORMATTED_START_DATE_TIME
|
10
|
+
from logger_36.constant.record import SHOW_WHEN_ATTR
|
7
11
|
from logger_36.instance.logger import L
|
8
|
-
from logger_36.task.measure.chronos import
|
12
|
+
from logger_36.task.measure.chronos import FormattedElapsedTime
|
9
13
|
from logger_36.type.logger import logger_t
|
10
14
|
|
11
15
|
|
12
16
|
def LogElapsedTime(*, logger: logger_t = L) -> None:
|
13
17
|
""""""
|
14
|
-
logger.info(
|
18
|
+
logger.info(
|
19
|
+
f"Elapsed Time: {FormattedElapsedTime(date_time_t.now())[1:]} "
|
20
|
+
f"(since {FORMATTED_START_DATE_TIME})",
|
21
|
+
extra={SHOW_WHEN_ATTR: False},
|
22
|
+
)
|
15
23
|
|
16
24
|
|
17
25
|
"""
|
logger_36/config/message.py
CHANGED
@@ -6,12 +6,9 @@ SEE COPYRIGHT NOTICE BELOW
|
|
6
6
|
|
7
7
|
from datetime import timedelta as time_delta_t
|
8
8
|
|
9
|
-
LEVEL_OPENING = "("
|
10
|
-
LEVEL_CLOSING = ")"
|
11
|
-
MESSAGE_MARKER = "|"
|
12
9
|
WHERE_SEPARATOR = "@"
|
13
10
|
ELAPSED_TIME_SEPARATOR = "+"
|
14
|
-
FALLBACK_MESSAGE_WIDTH =
|
11
|
+
FALLBACK_MESSAGE_WIDTH = 20
|
15
12
|
|
16
13
|
DATE_FORMAT = "%Y-%m-%d"
|
17
14
|
TIME_FORMAT = "%H:%M:%S"
|
@@ -6,10 +6,14 @@ SEE COPYRIGHT NOTICE BELOW
|
|
6
6
|
|
7
7
|
from datetime import datetime as date_time_t
|
8
8
|
|
9
|
+
from logger_36.config.message import DATE_FORMAT, TIME_FORMAT
|
10
|
+
|
9
11
|
# This module is imported early. Therefore, the current date and time should be close
|
10
12
|
# enough to the real start date and time of the main script.
|
11
13
|
START_DATE_TIME = date_time_t.now()
|
12
14
|
|
15
|
+
FORMATTED_START_DATE_TIME = f"{START_DATE_TIME:{TIME_FORMAT + ' on ' + DATE_FORMAT}}"
|
16
|
+
|
13
17
|
DATE_TIME_ORIGIN = date_time_t.fromtimestamp(1970, None)
|
14
18
|
DATE_ORIGIN = DATE_TIME_ORIGIN.date()
|
15
19
|
|
logger_36/constant/error.py
CHANGED
logger_36/constant/html.py
CHANGED
logger_36/constant/message.py
CHANGED
@@ -9,18 +9,18 @@ import typing as h
|
|
9
9
|
|
10
10
|
from logger_36.config.message import (
|
11
11
|
ELAPSED_TIME_SEPARATOR,
|
12
|
-
LEVEL_CLOSING,
|
13
|
-
LEVEL_OPENING,
|
14
|
-
MESSAGE_MARKER,
|
15
12
|
TIME_FORMAT,
|
13
|
+
WHERE_SEPARATOR,
|
16
14
|
)
|
17
15
|
|
18
16
|
TIME_LENGTH = time.strftime(TIME_FORMAT, time.gmtime(0)).__len__()
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
LINE_INDENT = (CONTEXT_LENGTH + MESSAGE_MARKER.__len__() + 2) * " "
|
17
|
+
CONTEXT_LENGTH = TIME_LENGTH + 2 # 2=<underscore><level name first letter>.
|
18
|
+
CONTEXT_LENGTH_p_1 = CONTEXT_LENGTH + 1
|
19
|
+
LINE_INDENT = (CONTEXT_LENGTH + 1) * " "
|
23
20
|
NEXT_LINE_PROLOGUE = "\n" + LINE_INDENT
|
21
|
+
WHERE_PROLOGUE = NEXT_LINE_PROLOGUE[:-2] + WHERE_SEPARATOR + " "
|
22
|
+
|
23
|
+
TIME_PLACEHOLDER = TIME_LENGTH * ELAPSED_TIME_SEPARATOR
|
24
24
|
|
25
25
|
expected_op_h = h.Literal[":", ": ", "=", "!=", ">=", "<="]
|
26
26
|
EXPECTED_OP: tuple[str, ...] = h.get_args(expected_op_h)
|
logger_36/constant/record.py
CHANGED
@@ -4,6 +4,8 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
|
|
4
4
|
SEE COPYRIGHT NOTICE BELOW
|
5
5
|
"""
|
6
6
|
|
7
|
+
HAS_ACTUAL_EXPECTED_ATTR = "has_actual_expected"
|
8
|
+
SHOW_WHEN_ATTR = "should_show_when"
|
7
9
|
SHOW_WHERE_ATTR = "should_show_where"
|
8
10
|
SHOW_W_RULE_ATTR = "should_show_w_rule"
|
9
11
|
WHEN_OR_ELAPSED_ATTR = "when_or_elapsed"
|
@@ -4,33 +4,23 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
|
|
4
4
|
SEE COPYRIGHT NOTICE BELOW
|
5
5
|
"""
|
6
6
|
|
7
|
-
import
|
8
|
-
import
|
9
|
-
from os import sep as FOLDER_SEPARATOR
|
7
|
+
import os as o
|
8
|
+
import tempfile as tmps
|
10
9
|
from pathlib import Path as path_t
|
11
10
|
|
12
|
-
from logger_36.constant.path import USER_FOLDER
|
13
|
-
from logger_36.constant.record import WHERE_ATTR
|
14
11
|
|
15
|
-
|
16
|
-
|
12
|
+
def NewTemporaryFile(
|
13
|
+
suffix: str, /, *, should_return_accessor: bool = False
|
14
|
+
) -> path_t | tuple[path_t, int]:
|
17
15
|
""""""
|
18
|
-
|
19
|
-
|
20
|
-
if module.is_relative_to(path):
|
21
|
-
module = module.relative_to(path).with_suffix("")
|
22
|
-
module = str(module).replace(FOLDER_SEPARATOR, ".")
|
23
|
-
break
|
24
|
-
else:
|
25
|
-
if module.is_relative_to(USER_FOLDER):
|
26
|
-
module = module.relative_to(USER_FOLDER)
|
27
|
-
|
28
|
-
output = f"{module}:{record.funcName}:{record.lineno}"
|
16
|
+
accessor, path = tmps.mkstemp(suffix=suffix)
|
17
|
+
path = path_t(path)
|
29
18
|
|
30
|
-
if
|
31
|
-
|
19
|
+
if should_return_accessor:
|
20
|
+
return path, accessor
|
32
21
|
|
33
|
-
|
22
|
+
o.close(accessor)
|
23
|
+
return path
|
34
24
|
|
35
25
|
|
36
26
|
"""
|
logger_36/extension/line.py
CHANGED
@@ -12,7 +12,7 @@ def WrappedLines(lines: list[str], message_width: int, /) -> list[str]:
|
|
12
12
|
for line in lines:
|
13
13
|
while line.__len__() > message_width:
|
14
14
|
if all(
|
15
|
-
|
15
|
+
_ != " " for _ in line[(message_width - 1) : (message_width + 1)]
|
16
16
|
):
|
17
17
|
if line[message_width - 2] == " ":
|
18
18
|
piece, line = (
|
logger_36/task/format/memory.py
CHANGED
logger_36/task/format/message.py
CHANGED
@@ -7,8 +7,8 @@ SEE COPYRIGHT NOTICE BELOW
|
|
7
7
|
import difflib as diff
|
8
8
|
import typing as h
|
9
9
|
|
10
|
-
from logger_36.constant.generic import NOT_PASSED
|
11
10
|
from logger_36.constant.message import expected_op_h
|
11
|
+
from logger_36.extension.sentinel import NOT_PASSED
|
12
12
|
|
13
13
|
|
14
14
|
def MessageWithActualExpected(
|
@@ -20,8 +20,10 @@ def MessageWithActualExpected(
|
|
20
20
|
expected_is_choices: bool = False,
|
21
21
|
expected_op: expected_op_h = "=",
|
22
22
|
with_final_dot: bool = True,
|
23
|
-
) -> str:
|
24
|
-
"""
|
23
|
+
) -> tuple[str, bool]:
|
24
|
+
"""
|
25
|
+
Second return: has_actual_expected.
|
26
|
+
"""
|
25
27
|
if actual is NOT_PASSED:
|
26
28
|
if with_final_dot:
|
27
29
|
if message[-1] != ".":
|
@@ -29,7 +31,7 @@ def MessageWithActualExpected(
|
|
29
31
|
elif message[-1] == ".":
|
30
32
|
message = message[:-1]
|
31
33
|
|
32
|
-
return message
|
34
|
+
return message, False
|
33
35
|
|
34
36
|
if message[-1] == ".":
|
35
37
|
message = message[:-1]
|
@@ -44,7 +46,7 @@ def MessageWithActualExpected(
|
|
44
46
|
else:
|
45
47
|
actual = f"{actual}:{type(actual).__name__}"
|
46
48
|
|
47
|
-
return f"{message}: Actual={actual}; {expected}{dot}"
|
49
|
+
return f"{message}: Actual={actual}; {expected}{dot}", True
|
48
50
|
|
49
51
|
|
50
52
|
def _FormattedExpected(
|
@@ -4,32 +4,42 @@ Contributor(s): Eric Debreuve (eric.debreuve@cnrs.fr) since 2023
|
|
4
4
|
SEE COPYRIGHT NOTICE BELOW
|
5
5
|
"""
|
6
6
|
|
7
|
-
import time
|
8
7
|
from datetime import datetime as date_time_t
|
9
8
|
|
10
|
-
from logger_36.
|
9
|
+
from logger_36.config.message import ELAPSED_TIME_SEPARATOR
|
10
|
+
from logger_36.constant.chronos import START_DATE_TIME
|
11
|
+
from logger_36.constant.message import TIME_LENGTH
|
11
12
|
|
12
13
|
|
13
14
|
def TimeStamp(*, precision: str = "microseconds") -> str:
|
14
|
-
"""
|
15
|
+
"""
|
16
|
+
precision: See documentation of date_time_t.isoformat.
|
17
|
+
"""
|
15
18
|
return (
|
16
19
|
date_time_t.now()
|
17
20
|
.isoformat(timespec=precision)
|
18
|
-
.replace(".", "
|
21
|
+
.replace(".", "_")
|
19
22
|
.replace(":", "-")
|
20
23
|
)
|
21
24
|
|
22
25
|
|
23
|
-
def
|
26
|
+
def FormattedElapsedTime(
|
27
|
+
now: date_time_t, /, *, reference: date_time_t = START_DATE_TIME
|
28
|
+
) -> str:
|
24
29
|
""""""
|
25
|
-
|
26
|
-
|
27
|
-
output
|
30
|
+
output = str(now - reference)
|
31
|
+
|
32
|
+
if output.startswith("0:"):
|
33
|
+
output = output[2:]
|
28
34
|
while output.startswith("00:"):
|
29
|
-
output = output
|
35
|
+
output = output[3:]
|
36
|
+
if output[0] == "0":
|
37
|
+
output = output[1:]
|
38
|
+
|
39
|
+
output = ELAPSED_TIME_SEPARATOR + output
|
30
40
|
|
31
|
-
if
|
32
|
-
return output
|
41
|
+
if output.__len__() > TIME_LENGTH:
|
42
|
+
return output[:TIME_LENGTH]
|
33
43
|
return output
|
34
44
|
|
35
45
|
|
logger_36/task/storage.py
CHANGED
@@ -5,12 +5,17 @@ SEE COPYRIGHT NOTICE BELOW
|
|
5
5
|
"""
|
6
6
|
|
7
7
|
import logging as l
|
8
|
-
import os as o
|
9
|
-
import tempfile as tmps
|
10
8
|
from io import IOBase as io_base_t
|
11
9
|
from pathlib import Path as path_t
|
12
10
|
|
13
|
-
from logger_36.constant.
|
11
|
+
from logger_36.constant.error import CANNOT_SAVE_RECORDS
|
12
|
+
from logger_36.constant.html import (
|
13
|
+
BODY_PLACEHOLDER,
|
14
|
+
HTML_SUFFIX,
|
15
|
+
MINIMAL_HTML,
|
16
|
+
TITLE_PLACEHOLDER,
|
17
|
+
)
|
18
|
+
from logger_36.extension.file import NewTemporaryFile
|
14
19
|
from logger_36.instance.logger import L
|
15
20
|
from logger_36.type.logger import logger_t
|
16
21
|
|
@@ -21,60 +26,17 @@ def SaveLOGasHTML(
|
|
21
26
|
"""
|
22
27
|
From first console handler found.
|
23
28
|
"""
|
24
|
-
|
25
|
-
|
26
|
-
if path is None:
|
27
|
-
for handler in logger.handlers:
|
28
|
-
if isinstance(handler, l.FileHandler):
|
29
|
-
path = path_t(handler.baseFilename).with_suffix(".htm")
|
30
|
-
break
|
31
|
-
else:
|
32
|
-
logger.warning(f"{cannot_save}: No file handler to build a filename from.")
|
33
|
-
return
|
34
|
-
|
35
|
-
if path.exists():
|
36
|
-
logger.warning(
|
37
|
-
f'{cannot_save}: Automatically generated path "{path}" already exists.'
|
38
|
-
)
|
39
|
-
return
|
40
|
-
elif isinstance(path, str):
|
41
|
-
path = path_t(path)
|
42
|
-
|
43
|
-
actual_file = isinstance(path, path_t)
|
44
|
-
|
45
|
-
if actual_file and path.exists():
|
46
|
-
existing = path
|
47
|
-
|
48
|
-
accessor, path = tmps.mkstemp(suffix=".htm")
|
49
|
-
o.close(accessor)
|
50
|
-
path = path_t(path)
|
51
|
-
|
29
|
+
records = logger_t.Records(logger)
|
30
|
+
if records is None:
|
52
31
|
logger.warning(
|
53
|
-
f
|
32
|
+
f"{CANNOT_SAVE_RECORDS}: No handlers with recording capability found"
|
54
33
|
)
|
55
|
-
|
56
|
-
for handler in logger.handlers:
|
57
|
-
records = getattr(handler, "records", None)
|
58
|
-
if isinstance(records, list) and (
|
59
|
-
(records.__len__() == 0)
|
60
|
-
or all(
|
61
|
-
isinstance(_, tuple)
|
62
|
-
and isinstance(_[0], int)
|
63
|
-
and isinstance(_[1], dict | str | l.LogRecord)
|
64
|
-
and isinstance(_[2], bool)
|
65
|
-
for _ in records
|
66
|
-
)
|
67
|
-
):
|
68
|
-
break
|
69
|
-
else:
|
70
|
-
logger.warning(f"{cannot_save}: No handlers with recording capability found.")
|
71
34
|
return
|
72
|
-
|
73
35
|
if records.__len__() == 0:
|
74
36
|
return
|
75
37
|
|
76
38
|
if isinstance(records[0][1], str):
|
77
|
-
records = map(
|
39
|
+
records = map(_HighlightedRecord, records)
|
78
40
|
else:
|
79
41
|
records = map(lambda _: str(_[1]), records)
|
80
42
|
body = "\n".join(records)
|
@@ -82,16 +44,29 @@ def SaveLOGasHTML(
|
|
82
44
|
BODY_PLACEHOLDER, body
|
83
45
|
)
|
84
46
|
|
85
|
-
if
|
86
|
-
|
87
|
-
|
47
|
+
if path is None:
|
48
|
+
path = logger_t.StoragePath(logger, HTML_SUFFIX)
|
49
|
+
logger.info(f'Saving LOG as HTML in "{path}"')
|
50
|
+
elif isinstance(path, str):
|
51
|
+
path = path_t(path)
|
52
|
+
if path.exists():
|
53
|
+
existing = path
|
54
|
+
path = NewTemporaryFile(HTML_SUFFIX)
|
55
|
+
logger.warning(
|
56
|
+
f'File "{existing}" already exists: '
|
57
|
+
f'Saving LOG as HTML in "{path}" instead'
|
58
|
+
)
|
88
59
|
else:
|
89
60
|
path.write(html)
|
61
|
+
return
|
62
|
+
|
63
|
+
with open(path, "w") as accessor:
|
64
|
+
accessor.write(html)
|
90
65
|
|
91
66
|
|
92
|
-
def
|
67
|
+
def _HighlightedRecord(record: tuple[int, str, bool], /) -> str:
|
93
68
|
""""""
|
94
|
-
level, message, is_not_a_rule =
|
69
|
+
level, message, is_not_a_rule = record
|
95
70
|
|
96
71
|
if is_not_a_rule:
|
97
72
|
if level == l.DEBUG:
|