kitty-logger 0.2.0.dev3__tar.gz → 0.2.0.dev5__tar.gz
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.
- kitty_logger-0.2.0.dev5/LICENSE +9 -0
- {kitty_logger-0.2.0.dev3/src/kitty_logger.egg-info → kitty_logger-0.2.0.dev5}/PKG-INFO +1 -1
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/pyproject.toml +1 -1
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger/__init__.py +11 -2
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger/_client.py +36 -16
- kitty_logger-0.2.0.dev5/src/kitty_logger/_formatters.py +211 -0
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger/_server.py +29 -4
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger/_server_main.py +12 -2
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger/_setup.py +42 -17
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5/src/kitty_logger.egg-info}/PKG-INFO +1 -1
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/tests/test_kitty_logger.py +57 -3
- kitty_logger-0.2.0.dev3/LICENSE +0 -21
- kitty_logger-0.2.0.dev3/src/kitty_logger/_formatters.py +0 -82
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/README.md +0 -0
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/setup.cfg +0 -0
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger/_env.py +0 -0
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger.egg-info/SOURCES.txt +0 -0
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger.egg-info/dependency_links.txt +0 -0
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger.egg-info/requires.txt +0 -0
- {kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kitty
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "kitty_logger"
|
|
7
|
-
version = "0.2.0.
|
|
7
|
+
version = "0.2.0.dev5"
|
|
8
8
|
description = "Cross-process logging via a dedicated log server process and SocketHandler."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.12"
|
|
@@ -30,7 +30,16 @@
|
|
|
30
30
|
"""
|
|
31
31
|
|
|
32
32
|
from ._client import getLogger
|
|
33
|
+
from ._formatters import PRESETS, FormatPreset, FormatSpec, get_preset
|
|
33
34
|
from ._setup import setup_logging, shutdown_logging
|
|
34
35
|
|
|
35
|
-
__all__ = [
|
|
36
|
-
|
|
36
|
+
__all__ = [
|
|
37
|
+
"setup_logging",
|
|
38
|
+
"shutdown_logging",
|
|
39
|
+
"getLogger",
|
|
40
|
+
"FormatPreset",
|
|
41
|
+
"FormatSpec",
|
|
42
|
+
"PRESETS",
|
|
43
|
+
"get_preset",
|
|
44
|
+
]
|
|
45
|
+
__version__ = "0.2.0.dev5"
|
|
@@ -32,16 +32,18 @@ KITTY_NAMESPACE = "kitty"
|
|
|
32
32
|
_lock = threading.Lock()
|
|
33
33
|
|
|
34
34
|
|
|
35
|
-
def _read_endpoint() -> tuple[str, int]:
|
|
36
|
-
"""从环境变量读出 ``(host, port)
|
|
35
|
+
def _read_endpoint() -> tuple[str, int] | None:
|
|
36
|
+
"""从环境变量读出 ``(host, port)``;未设置则返回 ``None``。
|
|
37
|
+
|
|
38
|
+
返回 ``None`` 表示当前进程树尚未调用 :func:`setup_logging`。这是
|
|
39
|
+
合法状态——用户可能在模块顶层先 ``log = getLogger(__name__)``、
|
|
40
|
+
后续才调 ``setup_logging``,期间发出的日志按"无 handler"处理,
|
|
41
|
+
不强制报错。
|
|
42
|
+
"""
|
|
37
43
|
host = os.environ.get(ENV_HOST)
|
|
38
44
|
port = os.environ.get(ENV_PORT)
|
|
39
45
|
if not host or not port:
|
|
40
|
-
|
|
41
|
-
"kitty_logger 尚未在当前进程树中初始化。"
|
|
42
|
-
+ "请先在主进程调用 kitty_logger.setup_logging(),"
|
|
43
|
-
+ "再创建(spawn)子进程或调用 getLogger()。"
|
|
44
|
-
)
|
|
46
|
+
return None
|
|
45
47
|
return host, int(port)
|
|
46
48
|
|
|
47
49
|
|
|
@@ -58,10 +60,17 @@ def _read_level() -> int:
|
|
|
58
60
|
|
|
59
61
|
|
|
60
62
|
def _ensure_kitty_handler() -> logging.Logger:
|
|
61
|
-
"""
|
|
63
|
+
"""尽力保证本进程内 ``kitty`` logger 已挂上指向日志服务的 SocketHandler。
|
|
64
|
+
|
|
65
|
+
幂等且容忍未初始化:
|
|
62
66
|
|
|
63
|
-
|
|
64
|
-
|
|
67
|
+
- 已经挂过 SocketHandler:直接返回。
|
|
68
|
+
- ENV 中没有端点(尚未 ``setup_logging``):返回未挂 handler 的
|
|
69
|
+
``kitty`` logger。此时 ``kitty`` 仍保持默认 ``propagate=True``,
|
|
70
|
+
用户在 ``setup_logging`` 之前发出的日志会冒到 root(沿用标准
|
|
71
|
+
库行为),等到 ``setup_logging`` 真正调用时再来一次本函数完成
|
|
72
|
+
挂载与 ``propagate=False`` 的切换。
|
|
73
|
+
- ENV 存在:装 SocketHandler、按需设置 level、关闭 propagate。
|
|
65
74
|
|
|
66
75
|
.. note::
|
|
67
76
|
:class:`logging.handlers.SocketHandler` 直接 pickle ``LogRecord``
|
|
@@ -76,7 +85,14 @@ def _ensure_kitty_handler() -> logging.Logger:
|
|
|
76
85
|
if isinstance(h, SocketHandler):
|
|
77
86
|
return kitty
|
|
78
87
|
|
|
79
|
-
|
|
88
|
+
endpoint = _read_endpoint()
|
|
89
|
+
if endpoint is None:
|
|
90
|
+
# 尚未 setup_logging:保持 ``kitty`` 处于"无 handler、向 root
|
|
91
|
+
# 冒泡"的标准状态,让用户即使在 setup 之前调 getLogger 也不
|
|
92
|
+
# 会炸。后续调 setup_logging 时会再次进入本函数完成真正挂载。
|
|
93
|
+
return kitty
|
|
94
|
+
|
|
95
|
+
host, port = endpoint
|
|
80
96
|
level = _read_level()
|
|
81
97
|
|
|
82
98
|
kitty.addHandler(SocketHandler(host, port))
|
|
@@ -95,17 +111,21 @@ def _ensure_kitty_handler() -> logging.Logger:
|
|
|
95
111
|
|
|
96
112
|
|
|
97
113
|
def getLogger(name: str | None = None) -> logging.Logger:
|
|
98
|
-
"""返回挂在 ``kitty`` 命名空间下的 logger
|
|
114
|
+
"""返回挂在 ``kitty`` 命名空间下的 logger。
|
|
115
|
+
|
|
116
|
+
可在 :func:`kitty_logger.setup_logging` 之前调用——例如在模块顶层
|
|
117
|
+
写 ``log = getLogger(__name__)``。此时拿到的 logger 还没有连上日志
|
|
118
|
+
服务,等 ``setup_logging`` 实际执行后会自动生效(通过 ``kitty``
|
|
119
|
+
祖先 logger 上的 SocketHandler)。
|
|
99
120
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
121
|
+
本库只支持 ``spawn`` 启动方式;spawn 出来的子进程模块级状态天然为
|
|
122
|
+
空,第一次调用 ``getLogger`` 时会按需完成 SocketHandler 挂载(依赖
|
|
123
|
+
父进程通过 ENV 传入的端点)。
|
|
103
124
|
|
|
104
125
|
:param name: 业务名;``None`` 表示直接返回 ``kitty`` 本身。``"foo"``
|
|
105
126
|
会被转换为 ``logging.getLogger("kitty.foo")``——日志记录里
|
|
106
127
|
``%(name)s`` 字段会带 ``kitty.`` 前缀,这是有意保留的标记,方便
|
|
107
128
|
和第三方库日志区分。
|
|
108
|
-
:raises RuntimeError: 当前进程树尚未调用 ``setup_logging``。
|
|
109
129
|
"""
|
|
110
130
|
_ensure_kitty_handler()
|
|
111
131
|
full = KITTY_NAMESPACE if not name else f"{KITTY_NAMESPACE}.{name}"
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""日志格式化器:控制台版(按字段着色 ANSI)与文件版(无颜色)。
|
|
2
|
+
|
|
3
|
+
控制台格式化器把不同字段染成不同颜色,便于在终端里快速分辨;
|
|
4
|
+
文件格式化器保持纯文本,避免日志文件被颜色码污染。两者都通过
|
|
5
|
+
:class:`_MillisecondTimeFormatter` 共享毫秒精度的时间戳实现,
|
|
6
|
+
可通过 ``show_msec=False`` 切换为秒级精度。
|
|
7
|
+
|
|
8
|
+
模块同时提供四档预置格式 :data:`PRESETS`:``minimal`` / ``standard``
|
|
9
|
+
/ ``verbose`` / ``debug``,每档封装好 ``console_fmt`` / ``file_fmt``
|
|
10
|
+
/ ``datefmt`` / ``show_msec``,由 :func:`kitty_logger.setup_logging`
|
|
11
|
+
通过 ``preset=`` 参数选用。
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
import time
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from typing import Literal, override
|
|
18
|
+
|
|
19
|
+
# 256 色调色板,在深色终端下不刺眼。
|
|
20
|
+
_LEVEL_COLORS: dict[str, str] = {
|
|
21
|
+
"DEBUG": "\033[38;5;32m",
|
|
22
|
+
"INFO": "\033[38;5;70m",
|
|
23
|
+
"WARNING": "\033[38;5;186m",
|
|
24
|
+
"ERROR": "\033[38;5;206m",
|
|
25
|
+
"CRITICAL": "\033[1;38;5;196m",
|
|
26
|
+
}
|
|
27
|
+
_PROC_COLOR = "\033[38;5;141m"
|
|
28
|
+
_THREAD_COLOR = "\033[38;5;177m"
|
|
29
|
+
_NAME_COLOR = "\033[38;5;36m"
|
|
30
|
+
_FILE_COLOR = "\033[38;5;45m"
|
|
31
|
+
_FUNC_COLOR = "\033[38;5;208m"
|
|
32
|
+
_RESET = "\033[0m"
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# minimal:仅时间 + 级别 + 消息,时间戳到秒,适合脚本类小工具或交互式调试。
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
_MINIMAL_CONSOLE_FMT = "%(asctime)s %(color)s%(levelname)s%(reset)s %(message)s"
|
|
38
|
+
_MINIMAL_FILE_FMT = "%(asctime)s %(levelname)s %(message)s"
|
|
39
|
+
|
|
40
|
+
# ---------------------------------------------------------------------------
|
|
41
|
+
# standard:时间 + 级别 + logger 名 + 消息,毫秒精度,足够定位大部分问题。
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
_STANDARD_CONSOLE_FMT = (
|
|
44
|
+
"%(asctime)s | "
|
|
45
|
+
"%(color)s%(levelname)-8s%(reset)s | "
|
|
46
|
+
"%(name_color)s%(name)s%(reset)s | "
|
|
47
|
+
"%(message)s"
|
|
48
|
+
)
|
|
49
|
+
_STANDARD_FILE_FMT = "%(asctime)s | %(levelname)-8s | %(name)s | %(message)s"
|
|
50
|
+
|
|
51
|
+
# ---------------------------------------------------------------------------
|
|
52
|
+
# verbose:进程信息 + 文件位置 + 函数名,毫秒精度。本库的历史默认值。
|
|
53
|
+
# ---------------------------------------------------------------------------
|
|
54
|
+
DEFAULT_CONSOLE_FMT = (
|
|
55
|
+
"%(asctime)s | "
|
|
56
|
+
"%(proc_color)s%(processName)s(%(process)d)%(reset)s | "
|
|
57
|
+
"%(name_color)s%(name)s%(reset)s | "
|
|
58
|
+
"%(file_color)s%(filename)s:%(lineno)d%(reset)s | "
|
|
59
|
+
"%(func_color)s%(funcName)s%(reset)s | "
|
|
60
|
+
"%(color)s%(levelname)s%(reset)s | "
|
|
61
|
+
"%(message)s"
|
|
62
|
+
)
|
|
63
|
+
DEFAULT_FILE_FMT = (
|
|
64
|
+
"%(asctime)s | %(processName)s(%(process)d) | %(name)s | "
|
|
65
|
+
"%(filename)s:%(lineno)d | %(funcName)s | %(levelname)s | %(message)s"
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
# debug:在 verbose 基础上加入线程名,便于排查多线程时序问题。
|
|
70
|
+
# ---------------------------------------------------------------------------
|
|
71
|
+
_DEBUG_CONSOLE_FMT = (
|
|
72
|
+
"%(asctime)s | "
|
|
73
|
+
"%(proc_color)s%(processName)s(%(process)d)%(reset)s | "
|
|
74
|
+
"%(thread_color)s%(threadName)s%(reset)s | "
|
|
75
|
+
"%(name_color)s%(name)s%(reset)s | "
|
|
76
|
+
"%(file_color)s%(filename)s:%(lineno)d%(reset)s | "
|
|
77
|
+
"%(func_color)s%(funcName)s%(reset)s | "
|
|
78
|
+
"%(color)s%(levelname)s%(reset)s | "
|
|
79
|
+
"%(message)s"
|
|
80
|
+
)
|
|
81
|
+
_DEBUG_FILE_FMT = (
|
|
82
|
+
"%(asctime)s | %(processName)s(%(process)d) | %(threadName)s | %(name)s | "
|
|
83
|
+
"%(filename)s:%(lineno)d | %(funcName)s | %(levelname)s | %(message)s"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
FormatPreset = Literal["minimal", "standard", "verbose", "debug"]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class FormatSpec:
|
|
92
|
+
"""单档预置格式的完整规格。
|
|
93
|
+
|
|
94
|
+
:param console_fmt: 控制台 handler 的 format 字符串(含 ANSI 占位符)。
|
|
95
|
+
:param file_fmt: 文件 handler 的 format 字符串(无 ANSI)。
|
|
96
|
+
:param datefmt: ``strftime`` 格式串。
|
|
97
|
+
:param show_msec: 是否在时间戳尾部追加 ``.毫秒``。
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
console_fmt: str
|
|
101
|
+
file_fmt: str
|
|
102
|
+
datefmt: str
|
|
103
|
+
show_msec: bool
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
PRESETS: dict[FormatPreset, FormatSpec] = {
|
|
107
|
+
"minimal": FormatSpec(
|
|
108
|
+
console_fmt=_MINIMAL_CONSOLE_FMT,
|
|
109
|
+
file_fmt=_MINIMAL_FILE_FMT,
|
|
110
|
+
datefmt="%H:%M:%S",
|
|
111
|
+
show_msec=False,
|
|
112
|
+
),
|
|
113
|
+
"standard": FormatSpec(
|
|
114
|
+
console_fmt=_STANDARD_CONSOLE_FMT,
|
|
115
|
+
file_fmt=_STANDARD_FILE_FMT,
|
|
116
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
117
|
+
show_msec=True,
|
|
118
|
+
),
|
|
119
|
+
"verbose": FormatSpec(
|
|
120
|
+
console_fmt=DEFAULT_CONSOLE_FMT,
|
|
121
|
+
file_fmt=DEFAULT_FILE_FMT,
|
|
122
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
123
|
+
show_msec=True,
|
|
124
|
+
),
|
|
125
|
+
"debug": FormatSpec(
|
|
126
|
+
console_fmt=_DEBUG_CONSOLE_FMT,
|
|
127
|
+
file_fmt=_DEBUG_FILE_FMT,
|
|
128
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
129
|
+
show_msec=True,
|
|
130
|
+
),
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_preset(name: FormatPreset) -> FormatSpec:
|
|
135
|
+
"""返回 ``name`` 档预置格式规格;未知名抛 :class:`ValueError`。"""
|
|
136
|
+
try:
|
|
137
|
+
return PRESETS[name]
|
|
138
|
+
except KeyError:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"未知的 format preset: {name!r},可选: {list(PRESETS)}"
|
|
141
|
+
) from None
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class _MillisecondTimeFormatter(logging.Formatter):
|
|
145
|
+
"""带可选毫秒精度的时间格式化基类。
|
|
146
|
+
|
|
147
|
+
标准库 :meth:`logging.Formatter.formatTime` 在传入 ``datefmt`` 时
|
|
148
|
+
不会自动拼接毫秒,这里覆写以按 ``show_msec`` 决定是否追加 ``.毫秒``。
|
|
149
|
+
"""
|
|
150
|
+
|
|
151
|
+
def __init__(
|
|
152
|
+
self,
|
|
153
|
+
fmt: str | None = None,
|
|
154
|
+
datefmt: str | None = None,
|
|
155
|
+
*,
|
|
156
|
+
show_msec: bool = True,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""
|
|
159
|
+
:param fmt: 透传给 :class:`logging.Formatter` 的 format 字符串。
|
|
160
|
+
:param datefmt: 透传给 :class:`logging.Formatter` 的 ``strftime`` 格式。
|
|
161
|
+
:param show_msec: 是否在时间戳尾部追加 ``.毫秒``。
|
|
162
|
+
``minimal`` 档为秒级精度,因此传入 ``False``;其余档位均为 ``True``。
|
|
163
|
+
"""
|
|
164
|
+
super().__init__(fmt=fmt, datefmt=datefmt)
|
|
165
|
+
self._show_msec = show_msec
|
|
166
|
+
|
|
167
|
+
@override
|
|
168
|
+
def formatTime(
|
|
169
|
+
self,
|
|
170
|
+
record: logging.LogRecord,
|
|
171
|
+
datefmt: str | None = None,
|
|
172
|
+
) -> str:
|
|
173
|
+
"""格式化时间戳;按 ``self._show_msec`` 决定是否追加 ``.毫秒``。"""
|
|
174
|
+
ct = self.converter(record.created)
|
|
175
|
+
s = time.strftime(datefmt or "%Y-%m-%d %H:%M:%S", ct)
|
|
176
|
+
if self._show_msec:
|
|
177
|
+
return f"{s}.{int(record.msecs):03d}"
|
|
178
|
+
return s
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ColorFormatter(_MillisecondTimeFormatter):
|
|
182
|
+
"""控制台格式化器:按字段分别添加 ANSI 颜色。
|
|
183
|
+
|
|
184
|
+
实现思路是把颜色码作为属性挂到 ``record`` 上,再在格式串里通过
|
|
185
|
+
``%(xxx_color)s ... %(reset)s`` 占位符把颜色串入对应字段,从而让
|
|
186
|
+
进程名、线程名、logger 名、文件位置、函数名、级别各自拥有独立的配色。
|
|
187
|
+
未在格式串中引用的占位符会被忽略,因此各 preset 共用同一份属性注入。
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
@override
|
|
191
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
192
|
+
"""把各字段对应的 ANSI 颜色码作为属性挂到 ``record`` 上再走父类格式化。
|
|
193
|
+
|
|
194
|
+
注入的属性涵盖 level / 进程 / 线程 / logger 名 / 文件位置 / 函数名
|
|
195
|
+
各自的颜色,以及统一的 ``reset``。具体格式串引用其中哪几个由 preset
|
|
196
|
+
决定(例如 ``debug`` 档会用到 ``thread_color``,``minimal`` 档则只
|
|
197
|
+
用到 ``color`` / ``reset``);未被引用的属性由标准库
|
|
198
|
+
:class:`logging.Formatter` 自动忽略。
|
|
199
|
+
"""
|
|
200
|
+
record.color = _LEVEL_COLORS.get(record.levelname, "") # type: ignore[attr-defined]
|
|
201
|
+
record.proc_color = _PROC_COLOR # type: ignore[attr-defined]
|
|
202
|
+
record.thread_color = _THREAD_COLOR # type: ignore[attr-defined]
|
|
203
|
+
record.name_color = _NAME_COLOR # type: ignore[attr-defined]
|
|
204
|
+
record.file_color = _FILE_COLOR # type: ignore[attr-defined]
|
|
205
|
+
record.func_color = _FUNC_COLOR # type: ignore[attr-defined]
|
|
206
|
+
record.reset = _RESET # type: ignore[attr-defined]
|
|
207
|
+
return super().format(record)
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class FileFormatter(_MillisecondTimeFormatter):
|
|
211
|
+
"""文件格式化器:与控制台版字段一致但不带任何 ANSI 颜色码。"""
|
|
@@ -131,9 +131,26 @@ def _build_handlers(
|
|
|
131
131
|
stream: bool,
|
|
132
132
|
console_fmt: str,
|
|
133
133
|
file_fmt: str,
|
|
134
|
-
|
|
134
|
+
console_datefmt: str | None,
|
|
135
|
+
file_datefmt: str | None,
|
|
136
|
+
console_show_msec: bool,
|
|
137
|
+
file_show_msec: bool,
|
|
135
138
|
) -> list[logging.Handler]:
|
|
136
|
-
"""根据用户传入的参数组合出落地用的 handler 列表。
|
|
139
|
+
"""根据用户传入的参数组合出落地用的 handler 列表。
|
|
140
|
+
|
|
141
|
+
控制台与文件两侧的 ``datefmt`` / ``show_msec`` 各自独立——上层允许
|
|
142
|
+
分别选择不同的 preset,因此服务端不再共用同一份时间设置。
|
|
143
|
+
|
|
144
|
+
:param log_file: 日志文件路径;``None`` 表示不挂文件 handler。
|
|
145
|
+
:param stream: 是否同时挂上指向 ``sys.stderr`` 的 :class:`StreamHandler`。
|
|
146
|
+
:param console_fmt: 控制台 handler 的 format 字符串(含 ANSI 占位符)。
|
|
147
|
+
:param file_fmt: 文件 handler 的 format 字符串(无 ANSI)。
|
|
148
|
+
:param console_datefmt: 控制台时间戳的 ``strftime`` 格式;``None`` 走默认值。
|
|
149
|
+
:param file_datefmt: 文件时间戳的 ``strftime`` 格式;``None`` 走默认值。
|
|
150
|
+
:param console_show_msec: 控制台时间戳是否在尾部追加 ``.毫秒``。
|
|
151
|
+
:param file_show_msec: 文件时间戳是否在尾部追加 ``.毫秒``。
|
|
152
|
+
:return: 顺序为 ``[file?, stream?]`` 的 handler 列表,调用方按需挂载。
|
|
153
|
+
"""
|
|
137
154
|
handlers: list[logging.Handler] = []
|
|
138
155
|
if log_file:
|
|
139
156
|
# 按天轮转,每天 0 点切一份,最多保留 365 天历史。
|
|
@@ -144,10 +161,18 @@ def _build_handlers(
|
|
|
144
161
|
backupCount=365,
|
|
145
162
|
encoding="utf-8",
|
|
146
163
|
)
|
|
147
|
-
fh.setFormatter(
|
|
164
|
+
fh.setFormatter(
|
|
165
|
+
FileFormatter(fmt=file_fmt, datefmt=file_datefmt, show_msec=file_show_msec)
|
|
166
|
+
)
|
|
148
167
|
handlers.append(fh)
|
|
149
168
|
if stream:
|
|
150
169
|
sh = logging.StreamHandler(stream=sys.stderr)
|
|
151
|
-
sh.setFormatter(
|
|
170
|
+
sh.setFormatter(
|
|
171
|
+
ColorFormatter(
|
|
172
|
+
fmt=console_fmt,
|
|
173
|
+
datefmt=console_datefmt,
|
|
174
|
+
show_msec=console_show_msec,
|
|
175
|
+
)
|
|
176
|
+
)
|
|
152
177
|
handlers.append(sh)
|
|
153
178
|
return handlers
|
|
@@ -38,7 +38,10 @@ def _parse_args() -> argparse.Namespace:
|
|
|
38
38
|
ap.add_argument("--stream", action="store_true")
|
|
39
39
|
ap.add_argument("--console-fmt", required=True)
|
|
40
40
|
ap.add_argument("--file-fmt", required=True)
|
|
41
|
-
ap.add_argument("--datefmt", default=None)
|
|
41
|
+
ap.add_argument("--console-datefmt", default=None)
|
|
42
|
+
ap.add_argument("--file-datefmt", default=None)
|
|
43
|
+
ap.add_argument("--console-show-msec", action="store_true")
|
|
44
|
+
ap.add_argument("--file-show-msec", action="store_true")
|
|
42
45
|
return ap.parse_args()
|
|
43
46
|
|
|
44
47
|
|
|
@@ -86,7 +89,14 @@ def main() -> None:
|
|
|
86
89
|
for h in list(root.handlers):
|
|
87
90
|
root.removeHandler(h)
|
|
88
91
|
for h in _build_handlers(
|
|
89
|
-
args.log_file,
|
|
92
|
+
args.log_file,
|
|
93
|
+
args.stream,
|
|
94
|
+
args.console_fmt,
|
|
95
|
+
args.file_fmt,
|
|
96
|
+
args.console_datefmt,
|
|
97
|
+
args.file_datefmt,
|
|
98
|
+
args.console_show_msec,
|
|
99
|
+
args.file_show_msec,
|
|
90
100
|
):
|
|
91
101
|
root.addHandler(h)
|
|
92
102
|
root.setLevel(args.level)
|
|
@@ -8,10 +8,11 @@ import os
|
|
|
8
8
|
import socket
|
|
9
9
|
import subprocess
|
|
10
10
|
import sys
|
|
11
|
+
from dataclasses import replace
|
|
11
12
|
from typing import IO, Literal, TypedDict, TypeGuard, cast
|
|
12
13
|
|
|
13
14
|
from ._env import ENV_HOST, ENV_LEVEL, ENV_PORT
|
|
14
|
-
from ._formatters import
|
|
15
|
+
from ._formatters import FormatPreset, FormatSpec, get_preset
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
class _NotRunningState(TypedDict):
|
|
@@ -128,9 +129,8 @@ def _build_server_argv(
|
|
|
128
129
|
level: int,
|
|
129
130
|
log_file: str | None,
|
|
130
131
|
stream: bool,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
datefmt: str | None,
|
|
132
|
+
console_spec: FormatSpec,
|
|
133
|
+
file_spec: FormatSpec,
|
|
134
134
|
) -> list[str]:
|
|
135
135
|
"""把 ``setup_logging`` 的参数翻译成服务子进程的命令行参数。"""
|
|
136
136
|
argv = [
|
|
@@ -144,16 +144,22 @@ def _build_server_argv(
|
|
|
144
144
|
"--level",
|
|
145
145
|
str(level),
|
|
146
146
|
"--console-fmt",
|
|
147
|
-
console_fmt,
|
|
147
|
+
console_spec.console_fmt,
|
|
148
148
|
"--file-fmt",
|
|
149
|
-
file_fmt,
|
|
149
|
+
file_spec.file_fmt,
|
|
150
|
+
"--console-datefmt",
|
|
151
|
+
console_spec.datefmt,
|
|
152
|
+
"--file-datefmt",
|
|
153
|
+
file_spec.datefmt,
|
|
150
154
|
]
|
|
151
155
|
if log_file is not None:
|
|
152
156
|
argv += ["--log-file", log_file]
|
|
153
157
|
if stream:
|
|
154
158
|
argv += ["--stream"]
|
|
155
|
-
if
|
|
156
|
-
argv += ["--
|
|
159
|
+
if console_spec.show_msec:
|
|
160
|
+
argv += ["--console-show-msec"]
|
|
161
|
+
if file_spec.show_msec:
|
|
162
|
+
argv += ["--file-show-msec"]
|
|
157
163
|
return argv
|
|
158
164
|
|
|
159
165
|
|
|
@@ -163,8 +169,11 @@ def setup_logging(
|
|
|
163
169
|
host: str = "127.0.0.1",
|
|
164
170
|
port: int = 0,
|
|
165
171
|
stream: bool = True,
|
|
166
|
-
|
|
167
|
-
|
|
172
|
+
preset: FormatPreset = "verbose",
|
|
173
|
+
console_preset: FormatPreset | None = None,
|
|
174
|
+
file_preset: FormatPreset | None = None,
|
|
175
|
+
console_fmt: str | None = None,
|
|
176
|
+
file_fmt: str | None = None,
|
|
168
177
|
datefmt: str | None = None,
|
|
169
178
|
) -> tuple[str, int]:
|
|
170
179
|
"""在主进程里启动跨进程日志服务,返回服务真实绑定到的 ``(host, port)``。
|
|
@@ -176,7 +185,9 @@ def setup_logging(
|
|
|
176
185
|
通过 :func:`kitty_logger.getLogger` 连上来。
|
|
177
186
|
|
|
178
187
|
控制台输出使用带 ANSI 颜色的 :class:`ColorFormatter`,文件输出使用
|
|
179
|
-
无颜色的 :class:`FileFormatter
|
|
188
|
+
无颜色的 :class:`FileFormatter`。两侧的格式由四档预置之一决定:
|
|
189
|
+
``minimal`` / ``standard`` / ``verbose`` / ``debug``,每档同时封装
|
|
190
|
+
format 字符串、``datefmt`` 与是否显示毫秒。
|
|
180
191
|
|
|
181
192
|
幂等:重复调用直接返回已记录的 ``(host, port)``,不会重复启动子进程。
|
|
182
193
|
|
|
@@ -187,9 +198,12 @@ def setup_logging(
|
|
|
187
198
|
绑定非 loopback 地址会直接 :class:`ValueError`。
|
|
188
199
|
:param port: 监听端口;``0`` 让 OS 自动选择空闲端口。
|
|
189
200
|
:param stream: 是否同时输出到 ``stderr``。
|
|
190
|
-
:param
|
|
191
|
-
:param
|
|
192
|
-
:param
|
|
201
|
+
:param preset: 同时作用于控制台与文件两侧的预置档位。
|
|
202
|
+
:param console_preset: 仅覆盖控制台侧的档位;不传则沿用 ``preset``。
|
|
203
|
+
:param file_preset: 仅覆盖文件侧的档位;不传则沿用 ``preset``。
|
|
204
|
+
:param console_fmt: 进一步覆盖控制台 handler 的 format 字符串。
|
|
205
|
+
:param file_fmt: 进一步覆盖文件 handler 的 format 字符串。
|
|
206
|
+
:param datefmt: 进一步覆盖时间戳的 ``strftime`` 格式(同时作用于两侧)。
|
|
193
207
|
:return: 服务真实绑定到的 ``(host, port)``。
|
|
194
208
|
|
|
195
209
|
.. note::
|
|
@@ -229,15 +243,26 @@ def setup_logging(
|
|
|
229
243
|
|
|
230
244
|
_check_loopback_only(host)
|
|
231
245
|
|
|
246
|
+
# 解析每侧的 preset:单侧 preset 优先于全局 preset;具体的 fmt/datefmt
|
|
247
|
+
# 参数则在 preset 选定后再做字段级覆盖。
|
|
248
|
+
console_spec = get_preset(console_preset or preset)
|
|
249
|
+
file_spec = get_preset(file_preset or preset)
|
|
250
|
+
if console_fmt is not None:
|
|
251
|
+
console_spec = replace(console_spec, console_fmt=console_fmt)
|
|
252
|
+
if file_fmt is not None:
|
|
253
|
+
file_spec = replace(file_spec, file_fmt=file_fmt)
|
|
254
|
+
if datefmt is not None:
|
|
255
|
+
console_spec = replace(console_spec, datefmt=datefmt)
|
|
256
|
+
file_spec = replace(file_spec, datefmt=datefmt)
|
|
257
|
+
|
|
232
258
|
argv = _build_server_argv(
|
|
233
259
|
host=host,
|
|
234
260
|
port=port,
|
|
235
261
|
level=level,
|
|
236
262
|
log_file=log_file,
|
|
237
263
|
stream=stream,
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
datefmt=datefmt,
|
|
264
|
+
console_spec=console_spec,
|
|
265
|
+
file_spec=file_spec,
|
|
241
266
|
)
|
|
242
267
|
# 把本包所在目录拼进子进程的 ``PYTHONPATH``,保证子进程的解释器能
|
|
243
268
|
# ``import`` 到 ``kitty_logger._server_main``。已经 ``pip install`` 装到
|
|
@@ -111,12 +111,25 @@ def test_shutdown_logging_idempotent(tmp_path: Path):
|
|
|
111
111
|
kitty_logger.shutdown_logging()
|
|
112
112
|
|
|
113
113
|
|
|
114
|
-
def
|
|
114
|
+
def test_get_logger_before_setup_works(tmp_path: Path):
|
|
115
|
+
"""允许在 ``setup_logging`` 之前 ``getLogger``——典型用法是模块顶层
|
|
116
|
+
``log = getLogger(__name__)``。setup 之后日志应能正常落到服务端。"""
|
|
115
117
|
# 必须没有环境变量。
|
|
116
118
|
for k in ("KITTY_LOGGER_HOST", "KITTY_LOGGER_PORT"):
|
|
117
119
|
os.environ.pop(k, None)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
|
|
121
|
+
# 还没 setup 就 getLogger:不应抛错,拿到的 logger 暂无 SocketHandler。
|
|
122
|
+
log = kitty_logger.getLogger("early")
|
|
123
|
+
assert not any(
|
|
124
|
+
isinstance(h, logging.handlers.SocketHandler)
|
|
125
|
+
for h in logging.getLogger("kitty").handlers
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# setup 之后再用同一个 logger 写日志:应当落到文件。
|
|
129
|
+
log_path = tmp_path / "app.log"
|
|
130
|
+
kitty_logger.setup_logging(log_file=str(log_path), stream=False)
|
|
131
|
+
log.info("late-binding-ok")
|
|
132
|
+
_wait_for_lines(log_path, lambda t: "late-binding-ok" in t)
|
|
120
133
|
|
|
121
134
|
|
|
122
135
|
def test_non_loopback_raises(tmp_path: Path):
|
|
@@ -333,3 +346,44 @@ def test_setup_logging_in_child_is_noop(tmp_path: Path):
|
|
|
333
346
|
lambda t: f"host={host} port={port}" in t and "kitty.child.0" in t,
|
|
334
347
|
)
|
|
335
348
|
assert "SpawnProcess" in text
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def test_preset_minimal_writes_short_format(tmp_path: Path):
|
|
352
|
+
"""``minimal`` 档:时间戳到秒、字段只剩 level + message。"""
|
|
353
|
+
log_path = tmp_path / "app.log"
|
|
354
|
+
kitty_logger.setup_logging(log_file=str(log_path), stream=False, preset="minimal")
|
|
355
|
+
kitty_logger.getLogger("m").info("hi-minimal")
|
|
356
|
+
|
|
357
|
+
text = _wait_for_lines(log_path, lambda t: "hi-minimal" in t)
|
|
358
|
+
line = next(line for line in text.splitlines() if "hi-minimal" in line)
|
|
359
|
+
# minimal 不带毫秒(无 ``.\d{3}``)也不带管道分隔符。
|
|
360
|
+
assert "|" not in line
|
|
361
|
+
assert "INFO hi-minimal" in line
|
|
362
|
+
# 形如 ``HH:MM:SS INFO hi-minimal``,前 8 个字符是时间戳。
|
|
363
|
+
assert line[2] == ":" and line[5] == ":"
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def test_preset_debug_includes_thread_name(tmp_path: Path):
|
|
367
|
+
"""``debug`` 档:在 verbose 字段基础上额外带 ``threadName``。"""
|
|
368
|
+
log_path = tmp_path / "app.log"
|
|
369
|
+
kitty_logger.setup_logging(log_file=str(log_path), stream=False, preset="debug")
|
|
370
|
+
kitty_logger.getLogger("d").info("hi-debug")
|
|
371
|
+
|
|
372
|
+
_wait_for_lines(log_path, lambda t: "hi-debug" in t and "MainThread" in t)
|
|
373
|
+
|
|
374
|
+
|
|
375
|
+
def test_independent_console_and_file_presets(tmp_path: Path):
|
|
376
|
+
"""``console_preset`` / ``file_preset`` 可以分别选档。"""
|
|
377
|
+
log_path = tmp_path / "app.log"
|
|
378
|
+
# 控制台 minimal、文件 debug:只校验文件这侧拿到 debug 字段。
|
|
379
|
+
kitty_logger.setup_logging(
|
|
380
|
+
log_file=str(log_path),
|
|
381
|
+
stream=False,
|
|
382
|
+
console_preset="minimal",
|
|
383
|
+
file_preset="debug",
|
|
384
|
+
)
|
|
385
|
+
kitty_logger.getLogger("ind").info("hi-ind")
|
|
386
|
+
|
|
387
|
+
text = _wait_for_lines(log_path, lambda t: "hi-ind" in t and "MainThread" in t)
|
|
388
|
+
# 文件侧是 debug 档,必含进程名与线程名。
|
|
389
|
+
assert "MainProcess" in text
|
kitty_logger-0.2.0.dev3/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Kitty
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
"""日志格式化器:控制台版(按字段着色 ANSI)与文件版(无颜色)。
|
|
2
|
-
|
|
3
|
-
控制台格式化器把不同字段染成不同颜色,便于在终端里快速分辨;
|
|
4
|
-
文件格式化器保持纯文本,避免日志文件被颜色码污染。两者都通过
|
|
5
|
-
:class:`_MillisecondTimeFormatter` 共享毫秒精度的时间戳实现。
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
import logging
|
|
9
|
-
import time
|
|
10
|
-
from typing import override
|
|
11
|
-
|
|
12
|
-
# 256 色调色板,在深色终端下不刺眼。
|
|
13
|
-
_LEVEL_COLORS: dict[str, str] = {
|
|
14
|
-
"DEBUG": "\033[38;5;32m",
|
|
15
|
-
"INFO": "\033[38;5;70m",
|
|
16
|
-
"WARNING": "\033[38;5;186m",
|
|
17
|
-
"ERROR": "\033[38;5;206m",
|
|
18
|
-
"CRITICAL": "\033[1;38;5;196m",
|
|
19
|
-
}
|
|
20
|
-
_PROC_COLOR = "\033[38;5;141m"
|
|
21
|
-
_NAME_COLOR = "\033[38;5;36m"
|
|
22
|
-
_FILE_COLOR = "\033[38;5;45m"
|
|
23
|
-
_FUNC_COLOR = "\033[38;5;208m"
|
|
24
|
-
_RESET = "\033[0m"
|
|
25
|
-
|
|
26
|
-
# 控制台格式:每个字段独立着色,进程信息排在最前以契合 kitty_logger 的跨进程定位。
|
|
27
|
-
DEFAULT_CONSOLE_FMT = (
|
|
28
|
-
"%(asctime)s | "
|
|
29
|
-
"%(proc_color)s%(processName)s(%(process)d)%(reset)s | "
|
|
30
|
-
"%(name_color)s%(name)s%(reset)s | "
|
|
31
|
-
"%(file_color)s%(filename)s:%(lineno)d%(reset)s | "
|
|
32
|
-
"%(func_color)s%(funcName)s%(reset)s | "
|
|
33
|
-
"%(color)s%(levelname)s%(reset)s | "
|
|
34
|
-
"%(message)s"
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
# 文件格式:字段一致,但不带任何 ANSI 颜色码。
|
|
38
|
-
DEFAULT_FILE_FMT = (
|
|
39
|
-
"%(asctime)s | %(processName)s(%(process)d) | %(name)s | "
|
|
40
|
-
"%(filename)s:%(lineno)d | %(funcName)s | %(levelname)s | %(message)s"
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
class _MillisecondTimeFormatter(logging.Formatter):
|
|
45
|
-
"""带毫秒精度的时间格式化基类。
|
|
46
|
-
|
|
47
|
-
标准库 :meth:`logging.Formatter.formatTime` 在传入 ``datefmt`` 时
|
|
48
|
-
不会自动拼接毫秒,这里覆写以始终带上 ``.毫秒`` 后缀。
|
|
49
|
-
"""
|
|
50
|
-
|
|
51
|
-
@override
|
|
52
|
-
def formatTime(
|
|
53
|
-
self,
|
|
54
|
-
record: logging.LogRecord,
|
|
55
|
-
datefmt: str | None = None,
|
|
56
|
-
) -> str:
|
|
57
|
-
ct = self.converter(record.created)
|
|
58
|
-
s = time.strftime(datefmt or "%Y-%m-%d %H:%M:%S", ct)
|
|
59
|
-
return f"{s}.{int(record.msecs):03d}"
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
class ColorFormatter(_MillisecondTimeFormatter):
|
|
63
|
-
"""控制台格式化器:按字段分别添加 ANSI 颜色。
|
|
64
|
-
|
|
65
|
-
实现思路是把颜色码作为属性挂到 ``record`` 上,再在格式串里通过
|
|
66
|
-
``%(xxx_color)s ... %(reset)s`` 占位符把颜色串入对应字段,从而让
|
|
67
|
-
进程名、logger 名、文件位置、函数名、级别各自拥有独立的配色。
|
|
68
|
-
"""
|
|
69
|
-
|
|
70
|
-
@override
|
|
71
|
-
def format(self, record: logging.LogRecord) -> str:
|
|
72
|
-
record.color = _LEVEL_COLORS.get(record.levelname, "") # type: ignore[attr-defined]
|
|
73
|
-
record.proc_color = _PROC_COLOR # type: ignore[attr-defined]
|
|
74
|
-
record.name_color = _NAME_COLOR # type: ignore[attr-defined]
|
|
75
|
-
record.file_color = _FILE_COLOR # type: ignore[attr-defined]
|
|
76
|
-
record.func_color = _FUNC_COLOR # type: ignore[attr-defined]
|
|
77
|
-
record.reset = _RESET # type: ignore[attr-defined]
|
|
78
|
-
return super().format(record)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
class FileFormatter(_MillisecondTimeFormatter):
|
|
82
|
-
"""文件格式化器:与控制台版字段一致但不带任何 ANSI 颜色码。"""
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{kitty_logger-0.2.0.dev3 → kitty_logger-0.2.0.dev5}/src/kitty_logger.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|