ft-logger 2026.1__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.
- ft_logger/__init__.py +24 -0
- ft_logger/api/chronos.py +8 -0
- ft_logger/api/handler.py +7 -0
- ft_logger/api/logger.py +7 -0
- ft_logger/api/memory.py +12 -0
- ft_logger/api/message.py +8 -0
- ft_logger/catalog/config/optional.py +33 -0
- ft_logger/catalog/config/theme_on_dark.py +23 -0
- ft_logger/catalog/config/theme_on_light.py +23 -0
- ft_logger/catalog/handler/color_rich.py +159 -0
- ft_logger/catalog/handler/file.py +60 -0
- ft_logger/catalog/handler/generic.py +125 -0
- ft_logger/catalog/handler/http_local.py +194 -0
- ft_logger/catalog/handler/memory.py +65 -0
- ft_logger/catalog/handler/monochrome.py +51 -0
- ft_logger/catalog/logger/chronos.py +69 -0
- ft_logger/catalog/logger/gpu.py +45 -0
- ft_logger/catalog/logger/memory.py +120 -0
- ft_logger/catalog/logger/system.py +39 -0
- ft_logger/config/memory.py +8 -0
- ft_logger/config/message.py +25 -0
- ft_logger/config/rule.py +8 -0
- ft_logger/config/system.py +24 -0
- ft_logger/constant/chronos.py +18 -0
- ft_logger/constant/color.py +289 -0
- ft_logger/constant/error.py +26 -0
- ft_logger/constant/html.py +51 -0
- ft_logger/constant/logger.py +14 -0
- ft_logger/constant/memory.py +7 -0
- ft_logger/constant/message.py +28 -0
- ft_logger/constant/record.py +13 -0
- ft_logger/constant/rule.py +10 -0
- ft_logger/constant/system.py +16 -0
- ft_logger/constant/theme.py +7 -0
- ft_logger/constant/thread.py +9 -0
- ft_logger/extension/object.py +10 -0
- ft_logger/hint/handle.py +12 -0
- ft_logger/hint/memory.py +9 -0
- ft_logger/hint/message.py +9 -0
- ft_logger/hint/record.py +42 -0
- ft_logger/hint/storage.py +9 -0
- ft_logger/hint/theme.py +18 -0
- ft_logger/hint/value.py +9 -0
- ft_logger/instance/logger.py +13 -0
- ft_logger/task/conversion/color.py +70 -0
- ft_logger/task/conversion/record.py +110 -0
- ft_logger/task/format/chronos.py +36 -0
- ft_logger/task/format/memory.py +78 -0
- ft_logger/task/format/message.py +75 -0
- ft_logger/task/interception.py +134 -0
- ft_logger/task/measure/chronos.py +20 -0
- ft_logger/task/measure/memory.py +37 -0
- ft_logger/type/logger.py +1497 -0
- ft_logger/type/record.py +43 -0
- ft_logger/type/rule.py +47 -0
- ft_logger/type/server.py +67 -0
- ft_logger/type/theme.py +192 -0
- ft_logger/version.py +1 -0
- ft_logger-2026.1.dist-info/METADATA +124 -0
- ft_logger-2026.1.dist-info/RECORD +62 -0
- ft_logger-2026.1.dist-info/WHEEL +5 -0
- ft_logger-2026.1.dist-info/top_level.txt +1 -0
ft_logger/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from beartype.claw import beartype_this_package # noqa
|
|
9
|
+
except ModuleNotFoundError:
|
|
10
|
+
pass
|
|
11
|
+
else:
|
|
12
|
+
import site
|
|
13
|
+
from pathlib import Path as path_t
|
|
14
|
+
|
|
15
|
+
folder = path_t(__file__).parent
|
|
16
|
+
paths = site.getsitepackages() + [site.getusersitepackages()]
|
|
17
|
+
for path in paths:
|
|
18
|
+
if folder.is_relative_to(path):
|
|
19
|
+
break
|
|
20
|
+
else:
|
|
21
|
+
beartype_this_package()
|
|
22
|
+
|
|
23
|
+
from .version import __version__ # noqa
|
|
24
|
+
from .instance.logger import L # noqa
|
ft_logger/api/chronos.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..task.format.chronos import FormattedElapsedTime # noqa
|
|
8
|
+
from ..task.measure.chronos import TimeStamp # noqa
|
ft_logger/api/handler.py
ADDED
ft_logger/api/logger.py
ADDED
ft_logger/api/memory.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..catalog.config.optional import MEMORY_MEASURE_IS_AVAILABLE # noqa
|
|
8
|
+
from ..task.format.memory import FormattedUsage as FormattedMemoryUsage # noqa
|
|
9
|
+
from ..task.format.memory import ( # noqa
|
|
10
|
+
FormattedUsageWithAutoUnit as FormattedMemoryUsageWithAutoUnit,
|
|
11
|
+
)
|
|
12
|
+
from ..task.measure.memory import CurrentUsage as CurrentMemoryUsage # noqa
|
ft_logger/api/message.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from ..constant.message import LINE_INDENT # noqa
|
|
8
|
+
from ..task.format.message import MessageWithActualExpected # noqa
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
import psutil # noqa
|
|
9
|
+
except ModuleNotFoundError:
|
|
10
|
+
MEMORY_MEASURE_IS_AVAILABLE = False
|
|
11
|
+
from ...constant.error import MISSING_MEMORY_MEASURE_MESSAGE
|
|
12
|
+
else:
|
|
13
|
+
MEMORY_MEASURE_IS_AVAILABLE = True
|
|
14
|
+
MISSING_MEMORY_MEASURE_MESSAGE = None
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import rich # noqa
|
|
18
|
+
except ModuleNotFoundError:
|
|
19
|
+
RICH_IS_AVAILABLE = False
|
|
20
|
+
from ...constant.error import MISSING_RICH_MESSAGE
|
|
21
|
+
else:
|
|
22
|
+
RICH_IS_AVAILABLE = True
|
|
23
|
+
MISSING_RICH_MESSAGE = None
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
import tensorflow # noqa
|
|
27
|
+
import tensorrt # noqa
|
|
28
|
+
except ModuleNotFoundError:
|
|
29
|
+
GPU_LOGGING_IS_AVAILABLE = False
|
|
30
|
+
from ...constant.error import MISSING_GPU_LOGGING_MESSAGE
|
|
31
|
+
else:
|
|
32
|
+
GPU_LOGGING_IS_AVAILABLE = True
|
|
33
|
+
MISSING_GPU_LOGGING_MESSAGE = None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging as l
|
|
8
|
+
|
|
9
|
+
TEXT_COLOR = "grey85"
|
|
10
|
+
WHERE_COLOR = "grey58"
|
|
11
|
+
|
|
12
|
+
LEVEL_COLOR = {
|
|
13
|
+
l.DEBUG: "orchid",
|
|
14
|
+
l.INFO: WHERE_COLOR,
|
|
15
|
+
l.WARNING: "yellow1",
|
|
16
|
+
l.ERROR: "dark_orange",
|
|
17
|
+
l.CRITICAL: "bright_red",
|
|
18
|
+
}
|
|
19
|
+
ACTUAL_COLOR = LEVEL_COLOR[l.CRITICAL]
|
|
20
|
+
EXPECTED_COLOR = "green3"
|
|
21
|
+
RULE_COLOR = "sky_blue3"
|
|
22
|
+
|
|
23
|
+
ALTERNATIVE_BACKGROUND = (15, 15, 15)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging as l
|
|
8
|
+
|
|
9
|
+
TEXT_COLOR = "grey27"
|
|
10
|
+
WHERE_COLOR = "grey46"
|
|
11
|
+
|
|
12
|
+
LEVEL_COLOR = {
|
|
13
|
+
l.DEBUG: "orchid",
|
|
14
|
+
l.INFO: WHERE_COLOR,
|
|
15
|
+
l.WARNING: "light_goldenrod3",
|
|
16
|
+
l.ERROR: "dark_orange",
|
|
17
|
+
l.CRITICAL: "bright_red",
|
|
18
|
+
}
|
|
19
|
+
ACTUAL_COLOR = LEVEL_COLOR[l.CRITICAL]
|
|
20
|
+
EXPECTED_COLOR = "green3"
|
|
21
|
+
RULE_COLOR = "dodger_blue1"
|
|
22
|
+
|
|
23
|
+
ALTERNATIVE_BACKGROUND = (230, 230, 230)
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging as l
|
|
8
|
+
import sys as s
|
|
9
|
+
import typing as h
|
|
10
|
+
|
|
11
|
+
from ...config.message import ACTUAL_PATTERNS, EXPECTED_PATTERN, FALLBACK_PAGE_WIDTH
|
|
12
|
+
from ...constant.color import COLORS_TERMINAL_256
|
|
13
|
+
from ...constant.message import CONTEXT_LENGTH_p_1
|
|
14
|
+
from ...extension.object import DefaultNameFor
|
|
15
|
+
from ...task.conversion.record import MessageFromRecord
|
|
16
|
+
from ...type.theme import theme_t
|
|
17
|
+
from ..config.optional import MISSING_RICH_MESSAGE, RICH_IS_AVAILABLE
|
|
18
|
+
|
|
19
|
+
if RICH_IS_AVAILABLE:
|
|
20
|
+
from rich.color import Color as color_t # noqa
|
|
21
|
+
from rich.console import Console as console_t # noqa
|
|
22
|
+
from rich.markup import escape as EscapedVersion # noqa
|
|
23
|
+
from rich.rule import Rule as rule_t # noqa
|
|
24
|
+
from rich.style import Style as style_t # noqa
|
|
25
|
+
from rich.text import Text as text_t # noqa
|
|
26
|
+
from rich.traceback import install as InstallTracebackHandler # noqa
|
|
27
|
+
|
|
28
|
+
_COMMON_TRACEBACK_ARGUMENTS = ("theme", "width")
|
|
29
|
+
_EXCLUSIVE_TRACEBACK_ARGUMENTS = (
|
|
30
|
+
"extra_lines",
|
|
31
|
+
"indent_guides",
|
|
32
|
+
"locals_hide_dunder",
|
|
33
|
+
"locals_hide_sunder",
|
|
34
|
+
"locals_max_length",
|
|
35
|
+
"locals_max_string",
|
|
36
|
+
"max_framesshow_locals",
|
|
37
|
+
"suppress",
|
|
38
|
+
"word_wrap",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
base_t = l.Handler
|
|
42
|
+
|
|
43
|
+
class color_rich_handler_t(base_t):
|
|
44
|
+
def __init__(self, *_, **kwargs) -> None:
|
|
45
|
+
""""""
|
|
46
|
+
base_t.__init__(self)
|
|
47
|
+
|
|
48
|
+
self.name: str = kwargs.pop("name", DefaultNameFor(self))
|
|
49
|
+
self.page_width: int = kwargs.pop("page_width", -1)
|
|
50
|
+
self.wrapping_is_disabled = True
|
|
51
|
+
self.theme: str | theme_t | None = kwargs.pop("theme", None)
|
|
52
|
+
self.console: console_t | None = None
|
|
53
|
+
self.should_install_traceback: bool = kwargs.pop(
|
|
54
|
+
"should_install_traceback", False
|
|
55
|
+
)
|
|
56
|
+
self.DoEmit: h.Callable | None = None
|
|
57
|
+
self.FlushStream: h.Callable | None = None
|
|
58
|
+
|
|
59
|
+
self.__post_init__(kwargs.pop("level", l.INFO), **kwargs)
|
|
60
|
+
|
|
61
|
+
def __post_init__(self, level: int, /, **kwargs) -> None:
|
|
62
|
+
""""""
|
|
63
|
+
self.setLevel(level)
|
|
64
|
+
|
|
65
|
+
if 0 < self.page_width < FALLBACK_PAGE_WIDTH:
|
|
66
|
+
# Prevent message width from being "unrealistically" small.
|
|
67
|
+
self.page_width = FALLBACK_PAGE_WIDTH
|
|
68
|
+
self.wrapping_is_disabled = self.page_width <= 0
|
|
69
|
+
|
|
70
|
+
if self.theme is None:
|
|
71
|
+
self.theme = theme_t.NewDefault("rich", "dark")
|
|
72
|
+
elif isinstance(self.theme, str):
|
|
73
|
+
self.theme = theme_t.NewDefaultFromStr(self.theme)
|
|
74
|
+
MakeThemeRich(self.theme)
|
|
75
|
+
|
|
76
|
+
traceback_kwargs = {}
|
|
77
|
+
if self.should_install_traceback:
|
|
78
|
+
for key in kwargs:
|
|
79
|
+
if key in _COMMON_TRACEBACK_ARGUMENTS:
|
|
80
|
+
traceback_kwargs[key] = kwargs[key]
|
|
81
|
+
elif key in _EXCLUSIVE_TRACEBACK_ARGUMENTS:
|
|
82
|
+
traceback_kwargs[key] = kwargs.pop(key)
|
|
83
|
+
|
|
84
|
+
self.console = console_t(
|
|
85
|
+
highlight=False, force_terminal=True, file=s.__stdout__, **kwargs
|
|
86
|
+
)
|
|
87
|
+
self.DoEmit = self.console.print
|
|
88
|
+
self.FlushStream = self.console.file.flush
|
|
89
|
+
|
|
90
|
+
if self.should_install_traceback:
|
|
91
|
+
traceback_kwargs["console"] = self.console
|
|
92
|
+
InstallTracebackHandler(**traceback_kwargs)
|
|
93
|
+
|
|
94
|
+
def emit(self, record: l.LogRecord, /) -> None:
|
|
95
|
+
""""""
|
|
96
|
+
message = MessageFromRecord(
|
|
97
|
+
record,
|
|
98
|
+
self.page_width,
|
|
99
|
+
self.wrapping_is_disabled,
|
|
100
|
+
self.theme,
|
|
101
|
+
self.Rule,
|
|
102
|
+
)
|
|
103
|
+
self.DoEmit(message, crop=False, overflow="ignore")
|
|
104
|
+
|
|
105
|
+
def flush(self) -> None:
|
|
106
|
+
""""""
|
|
107
|
+
self.FlushStream()
|
|
108
|
+
|
|
109
|
+
def Rule(
|
|
110
|
+
self, /, *, text: str | None = None, color: str | None = None
|
|
111
|
+
) -> str | rule_t:
|
|
112
|
+
""""""
|
|
113
|
+
if color is None:
|
|
114
|
+
color = self.theme.rule
|
|
115
|
+
if text in (None, "None"):
|
|
116
|
+
return rule_t(style=color)
|
|
117
|
+
return rule_t(title=text_t(text, style=f"bold {color}"), style=color)
|
|
118
|
+
|
|
119
|
+
def MakeThemeRich(theme: theme_t, /) -> None:
|
|
120
|
+
""""""
|
|
121
|
+
theme.SetColorizedMessageFunction(_ColorizedMessage)
|
|
122
|
+
if theme.should_alternate_background:
|
|
123
|
+
# Done here to avoid adding the Rich dependency to the theme_t implementation.
|
|
124
|
+
color = color_t.from_rgb(*COLORS_TERMINAL_256[theme.background_alt][2])
|
|
125
|
+
theme.background_alt = style_t(bgcolor=color)
|
|
126
|
+
|
|
127
|
+
def _ColorizedMessage(
|
|
128
|
+
theme: theme_t,
|
|
129
|
+
log_level: int,
|
|
130
|
+
when_or_elapsed_and_level: str,
|
|
131
|
+
message: str,
|
|
132
|
+
where_and_whatnot: str,
|
|
133
|
+
has_actual_expected: bool,
|
|
134
|
+
/,
|
|
135
|
+
) -> text_t:
|
|
136
|
+
""""""
|
|
137
|
+
output = text_t(
|
|
138
|
+
f"{when_or_elapsed_and_level}{message}{where_and_whatnot}", theme.text
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
output.stylize(theme.level[log_level], end=CONTEXT_LENGTH_p_1)
|
|
142
|
+
if has_actual_expected:
|
|
143
|
+
_ = output.highlight_words(ACTUAL_PATTERNS, style=theme.actual)
|
|
144
|
+
_ = output.highlight_regex(EXPECTED_PATTERN, style=theme.expected)
|
|
145
|
+
if where_and_whatnot.__len__() > 0:
|
|
146
|
+
output.stylize(theme.where, start=CONTEXT_LENGTH_p_1 + message.__len__())
|
|
147
|
+
|
|
148
|
+
if theme.should_alternate_background:
|
|
149
|
+
if theme.should_set_background:
|
|
150
|
+
output.stylize(theme.background_alt)
|
|
151
|
+
theme.should_set_background = False
|
|
152
|
+
else:
|
|
153
|
+
theme.should_set_background = True
|
|
154
|
+
|
|
155
|
+
return output
|
|
156
|
+
else:
|
|
157
|
+
from .monochrome import monochrome_handler_t as color_rich_handler_t # noqa
|
|
158
|
+
|
|
159
|
+
s.stderr.write(MISSING_RICH_MESSAGE + "\n")
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging as l
|
|
8
|
+
from pathlib import Path as path_t
|
|
9
|
+
|
|
10
|
+
from ...config.message import FALLBACK_PAGE_WIDTH
|
|
11
|
+
from ...extension.object import DefaultNameFor
|
|
12
|
+
from ...task.conversion.record import MessageFromRecord
|
|
13
|
+
from ...type.rule import NewRule
|
|
14
|
+
|
|
15
|
+
base_t = l.FileHandler
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class file_handler_t(base_t):
|
|
19
|
+
def __init__(self, *_, **kwargs) -> None:
|
|
20
|
+
""""""
|
|
21
|
+
path: str | path_t | None = kwargs.pop("path", None)
|
|
22
|
+
assert path is not None
|
|
23
|
+
|
|
24
|
+
path = path_t(path)
|
|
25
|
+
assert not path.exists()
|
|
26
|
+
|
|
27
|
+
parent = path.parent
|
|
28
|
+
if parent.exists():
|
|
29
|
+
assert parent.is_dir()
|
|
30
|
+
else:
|
|
31
|
+
parent.mkdir(mode=0o700, parents=True)
|
|
32
|
+
|
|
33
|
+
path.touch(mode=0o600, exist_ok=False) # Only to check writability.
|
|
34
|
+
|
|
35
|
+
base_t.__init__(self, path)
|
|
36
|
+
|
|
37
|
+
self.name: str = kwargs.pop("name", DefaultNameFor(self))
|
|
38
|
+
self.page_width: int = kwargs.pop("page_width", -1)
|
|
39
|
+
self.wrapping_is_disabled = True
|
|
40
|
+
self.DoEmit = self.stream.write
|
|
41
|
+
|
|
42
|
+
self.__post_init__(kwargs.pop("level", l.INFO))
|
|
43
|
+
|
|
44
|
+
def __post_init__(self, level: int, /) -> None:
|
|
45
|
+
""""""
|
|
46
|
+
self.setLevel(level)
|
|
47
|
+
|
|
48
|
+
if 0 < self.page_width < FALLBACK_PAGE_WIDTH:
|
|
49
|
+
# Prevent message width from being "unrealistically" small.
|
|
50
|
+
self.page_width = FALLBACK_PAGE_WIDTH
|
|
51
|
+
self.wrapping_is_disabled = self.page_width <= 0
|
|
52
|
+
|
|
53
|
+
def emit(self, record: l.LogRecord, /) -> None:
|
|
54
|
+
""""""
|
|
55
|
+
self.DoEmit(
|
|
56
|
+
MessageFromRecord(
|
|
57
|
+
record, self.page_width, self.wrapping_is_disabled, None, NewRule
|
|
58
|
+
)
|
|
59
|
+
+ "\n"
|
|
60
|
+
)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SEE COPYRIGHT, LICENCE, and DOCUMENTATION NOTICES: files
|
|
3
|
+
README-COPYRIGHT-utf8.txt, README-LICENCE-utf8.txt, and README-DOCUMENTATION-utf8.txt
|
|
4
|
+
at project source root.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import logging as l
|
|
8
|
+
import sys as s
|
|
9
|
+
import typing as h
|
|
10
|
+
|
|
11
|
+
from ...catalog.config.optional import RICH_IS_AVAILABLE
|
|
12
|
+
|
|
13
|
+
if RICH_IS_AVAILABLE:
|
|
14
|
+
from rich.console import Console as console_t
|
|
15
|
+
from rich.rule import Rule as rule_t
|
|
16
|
+
from rich.terminal_theme import DEFAULT_TERMINAL_THEME
|
|
17
|
+
from rich.text import Text as text_t
|
|
18
|
+
|
|
19
|
+
from .color_rich import MakeThemeRich
|
|
20
|
+
else:
|
|
21
|
+
console_t = rule_t = DEFAULT_TERMINAL_THEME = text_t = None
|
|
22
|
+
|
|
23
|
+
from ...config.message import FALLBACK_PAGE_WIDTH
|
|
24
|
+
from ...extension.object import DefaultNameFor
|
|
25
|
+
from ...task.conversion.record import MessageFromRecord
|
|
26
|
+
from ...type.rule import NewRule
|
|
27
|
+
from ...type.theme import theme_t
|
|
28
|
+
|
|
29
|
+
base_t = l.Handler
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class generic_handler_t(base_t):
|
|
33
|
+
"""
|
|
34
|
+
EmitMessage (h.Callable): Some callables may induce infinite loops if the logger
|
|
35
|
+
intercepts "print" calls, e.g. print, s.stdout.write, s.stderr.write.
|
|
36
|
+
"""
|
|
37
|
+
def __init__(self, *_, **kwargs) -> None:
|
|
38
|
+
""""""
|
|
39
|
+
base_t.__init__(self)
|
|
40
|
+
|
|
41
|
+
self.name: str = kwargs.pop("name", DefaultNameFor(self))
|
|
42
|
+
self.page_width: int = kwargs.pop("page_width", -1)
|
|
43
|
+
self.wrapping_is_disabled = True
|
|
44
|
+
self.supports_html = False
|
|
45
|
+
self.theme: str | theme_t | None = kwargs.pop("theme", None)
|
|
46
|
+
self.console = None # console_t | None.
|
|
47
|
+
self.console_options = None # rich.console.ConsoleOptions | None.
|
|
48
|
+
self.DoEmit: h.Callable | None = kwargs.pop("EmitMessage", None)
|
|
49
|
+
|
|
50
|
+
self.__post_init__(
|
|
51
|
+
kwargs.pop("level", l.INFO), kwargs.pop("supports_html", False)
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
def __post_init__(self, level: int, supports_html: bool, /) -> None:
|
|
55
|
+
""""""
|
|
56
|
+
# print, s.stdout.write, s.stderr.write: Basic prevention against infinite loop.
|
|
57
|
+
# See class documentation.
|
|
58
|
+
assert self.DoEmit not in (None, print, s.stdout.write, s.stderr.write)
|
|
59
|
+
|
|
60
|
+
self.setLevel(level)
|
|
61
|
+
|
|
62
|
+
if 0 < self.page_width < FALLBACK_PAGE_WIDTH:
|
|
63
|
+
# Prevent message width from being "unrealistically" small.
|
|
64
|
+
self.page_width = FALLBACK_PAGE_WIDTH
|
|
65
|
+
self.wrapping_is_disabled = self.page_width <= 0
|
|
66
|
+
|
|
67
|
+
if supports_html and (console_t is not None):
|
|
68
|
+
self.supports_html = True
|
|
69
|
+
self.console = console_t(highlight=False, force_terminal=True)
|
|
70
|
+
self.console_options = self.console.options.update(
|
|
71
|
+
overflow="ignore", no_wrap=True
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if self.theme is None:
|
|
75
|
+
self.theme = theme_t.NewDefault("rich", "light")
|
|
76
|
+
elif isinstance(self.theme, str):
|
|
77
|
+
self.theme = theme_t.NewDefaultFromStr(self.theme)
|
|
78
|
+
MakeThemeRich(self.theme)
|
|
79
|
+
|
|
80
|
+
def Rule(
|
|
81
|
+
self, /, *, text: str | None = None, color: str | None = None
|
|
82
|
+
) -> str | rule_t:
|
|
83
|
+
""""""
|
|
84
|
+
if (color is None) and (self.theme is not None):
|
|
85
|
+
color = self.theme.rule
|
|
86
|
+
|
|
87
|
+
if self.supports_html:
|
|
88
|
+
if text in (None, "None"):
|
|
89
|
+
return rule_t(style=color)
|
|
90
|
+
return rule_t(title=text_t(text, style=f"bold {color}"), style=color)
|
|
91
|
+
|
|
92
|
+
return NewRule(text=text, color=color)
|
|
93
|
+
|
|
94
|
+
def emit(self, record: l.LogRecord, /) -> None:
|
|
95
|
+
""""""
|
|
96
|
+
message = MessageFromRecord(
|
|
97
|
+
record, self.page_width, self.wrapping_is_disabled, self.theme, self.Rule
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
if self.supports_html:
|
|
101
|
+
segments = self.console.render(message, options=self.console_options)
|
|
102
|
+
|
|
103
|
+
# Inspired from the code of: rich.console.export_html.
|
|
104
|
+
html_segments = []
|
|
105
|
+
for text, style, _ in segments:
|
|
106
|
+
if text == "\n":
|
|
107
|
+
html_segments.append("\n")
|
|
108
|
+
else:
|
|
109
|
+
if style is not None:
|
|
110
|
+
style = style.get_html_style(DEFAULT_TERMINAL_THEME)
|
|
111
|
+
if (style is not None) and (style.__len__() > 0):
|
|
112
|
+
text = f'<span style="{style};">{text}</span>'
|
|
113
|
+
html_segments.append(text)
|
|
114
|
+
if html_segments[-1] == "\n":
|
|
115
|
+
html_segments = html_segments[:-1]
|
|
116
|
+
|
|
117
|
+
# /!\ For some reason, the widget splits the message into lines, place each
|
|
118
|
+
# line inside a pre tag, and set margin-bottom of the first and list lines
|
|
119
|
+
# to 12px. This can be seen by printing self.contents.toHtml(). To avoid the
|
|
120
|
+
# unwanted extra margins, margin-bottom is set to 0 below.
|
|
121
|
+
message = (
|
|
122
|
+
"<pre style='margin-bottom:0px'>" + "".join(html_segments) + "</pre>"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
self.DoEmit(message)
|