simplibs-logger 0.1.0__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.
- simplibs/logger/SimpleLogger.py +347 -0
- simplibs/logger/__init__.py +87 -0
- simplibs/logger/annotations/BackupCount.py +24 -0
- simplibs/logger/annotations/LogStream.py +29 -0
- simplibs/logger/annotations/MaxBytes.py +24 -0
- simplibs/logger/annotations/__init__.py +20 -0
- simplibs/logger/annotations/_helpers/__init__.py +19 -0
- simplibs/logger/annotations/_helpers/is_backup_count.py +25 -0
- simplibs/logger/annotations/_helpers/is_max_bytes.py +26 -0
- simplibs/logger/annotations/_helpers/is_writable_stream.py +30 -0
- simplibs/logger/definitions/DateFormat.py +51 -0
- simplibs/logger/definitions/LoggerLevel.py +73 -0
- simplibs/logger/definitions/LoggerName.py +58 -0
- simplibs/logger/definitions/MessageFormat.py +59 -0
- simplibs/logger/definitions/OutputFormat.py +38 -0
- simplibs/logger/definitions/__init__.py +31 -0
- simplibs/logger/definitions/constants/LEVEL_NAMES.py +36 -0
- simplibs/logger/definitions/constants/OUTPUT_FORMATS.py +25 -0
- simplibs/logger/definitions/constants/SUCCESS_LEVEL.py +23 -0
- simplibs/logger/definitions/constants/__init__.py +19 -0
- simplibs/logger/formatting/ColoredFormatter.py +97 -0
- simplibs/logger/formatting/JsonFormatter.py +159 -0
- simplibs/logger/formatting/__init__.py +21 -0
- simplibs/logger/formatting/_helpers/__init__.py +24 -0
- simplibs/logger/formatting/_helpers/collect_extra_fields.py +43 -0
- simplibs/logger/formatting/_helpers/constants/JSON_KEYS.py +22 -0
- simplibs/logger/formatting/_helpers/constants/LEVEL_STYLES.py +38 -0
- simplibs/logger/formatting/_helpers/constants/RECORD_ATTRIBUTES.py +24 -0
- simplibs/logger/formatting/_helpers/constants/__init__.py +16 -0
- simplibs/logger/formatting/_helpers/encode_unknown_value.py +69 -0
- simplibs/logger/formatting/_helpers/format_json_time.py +40 -0
- simplibs/logger/formatting/_helpers/parse_levelname_field.py +56 -0
- simplibs/logger/formatting/_helpers/pick_level_style.py +34 -0
- simplibs/logger/formatting/_helpers/replace_non_finite.py +48 -0
- simplibs/logger/formatting/_helpers/style_levelname.py +47 -0
- simplibs/logger/formatting/build_formatter.py +89 -0
- simplibs/logger/handlers/__init__.py +18 -0
- simplibs/logger/handlers/_validations/__init__.py +12 -0
- simplibs/logger/handlers/_validations/raise_log_file_open_error.py +47 -0
- simplibs/logger/handlers/build_console_handler.py +83 -0
- simplibs/logger/handlers/build_file_handler.py +119 -0
- simplibs/logger/levels/__init__.py +18 -0
- simplibs/logger/levels/register_success_level.py +33 -0
- simplibs/logger/levels/resolve_level.py +43 -0
- simplibs/logger/lifecycle/__init__.py +19 -0
- simplibs/logger/lifecycle/close_logger.py +45 -0
- simplibs/logger/lifecycle/create_logger.py +172 -0
- simplibs/logger/py.typed +1 -0
- simplibs_logger-0.1.0.dist-info/METADATA +508 -0
- simplibs_logger-0.1.0.dist-info/RECORD +53 -0
- simplibs_logger-0.1.0.dist-info/WHEEL +5 -0
- simplibs_logger-0.1.0.dist-info/licenses/LICENSE +21 -0
- simplibs_logger-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from collections.abc import Callable
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from types import TracebackType
|
|
5
|
+
from typing import Any
|
|
6
|
+
from simplibs.file import EncodingName
|
|
7
|
+
from simplibs.validate import validate_call
|
|
8
|
+
# Inners
|
|
9
|
+
from .annotations import BackupCount, LogStream, MaxBytes
|
|
10
|
+
from .definitions import SUCCESS_LEVEL
|
|
11
|
+
from .levels import resolve_level
|
|
12
|
+
from .lifecycle import close_logger, create_logger
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class SimpleLogger:
|
|
16
|
+
"""A ready-to-use logger: a configured stdlib `logging.Logger` behind a small calling interface.
|
|
17
|
+
|
|
18
|
+
Example:
|
|
19
|
+
logger = SimpleLogger(name="my_app", file_path="logs/app.log")
|
|
20
|
+
logger.log_info("Application started")
|
|
21
|
+
logger("Disk almost full", level="warning")
|
|
22
|
+
logger.log_exception("Import failed")
|
|
23
|
+
logger.close()
|
|
24
|
+
|
|
25
|
+
It is also a context manager (`with SimpleLogger(...) as logger:`) that
|
|
26
|
+
closes its handlers on exit.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
__slots__ = ("_logger", "_enabled", "_default_level")
|
|
30
|
+
|
|
31
|
+
# ----------------------------------------------------------------------
|
|
32
|
+
# Constructor initialization
|
|
33
|
+
# ----------------------------------------------------------------------
|
|
34
|
+
@validate_call
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
*,
|
|
38
|
+
enabled: bool = True,
|
|
39
|
+
name: str | None = None,
|
|
40
|
+
level: int | str | None = None,
|
|
41
|
+
default_level: int | str | None = None,
|
|
42
|
+
file_path: str | Path | None = None,
|
|
43
|
+
encoding: EncodingName = "utf-8",
|
|
44
|
+
console: bool = True,
|
|
45
|
+
colored: bool = True,
|
|
46
|
+
console_level: int | str | None = None,
|
|
47
|
+
file_level: int | str | None = None,
|
|
48
|
+
stream: LogStream = None,
|
|
49
|
+
message_format: str | None = None,
|
|
50
|
+
date_format: str | None = None,
|
|
51
|
+
utc: bool = False,
|
|
52
|
+
max_bytes: MaxBytes = None,
|
|
53
|
+
backup_count: BackupCount = 3,
|
|
54
|
+
console_output: str | None = None,
|
|
55
|
+
file_output: str | None = None,
|
|
56
|
+
json_default: Callable[[Any], Any] | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
"""Create the logger and its handlers.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
enabled: If False, nothing is written until `enabled` is set to True.
|
|
62
|
+
name: The logger's name. `None` for `"simplibs"`.
|
|
63
|
+
level: The level of the logger, as a number or a name. `None` for INFO.
|
|
64
|
+
default_level: The level used by `log()` when no `level` is given.
|
|
65
|
+
`None` takes the value of `level` at creation.
|
|
66
|
+
file_path: If given, a file handler writes here.
|
|
67
|
+
encoding: The text encoding of the log file.
|
|
68
|
+
console: If True (default), a console handler is attached.
|
|
69
|
+
colored: If True (default), the console colors the level name.
|
|
70
|
+
console_level: An extra level filter of the console handler.
|
|
71
|
+
file_level: An extra level filter of the file handler.
|
|
72
|
+
stream: Where the console writes. `None` for standard error.
|
|
73
|
+
message_format: The log message format of every text handler.
|
|
74
|
+
date_format: The strftime format of the timestamp of every text handler.
|
|
75
|
+
utc: If True, timestamps are in UTC.
|
|
76
|
+
max_bytes: Rotate the log file at this size. `None` never rotates.
|
|
77
|
+
backup_count: How many rotated files to keep.
|
|
78
|
+
console_output: `"text"` (default) or `"json"` for the console.
|
|
79
|
+
file_output: `"text"` (default) or `"json"` for the file.
|
|
80
|
+
json_default: For JSON output, turns a value JSON cannot hold
|
|
81
|
+
into one it can. `None` for the built-in fallback.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
# 1. Resolve the levels first, so a wrong one fails before any handler exists
|
|
85
|
+
resolved_level = resolve_level(level)
|
|
86
|
+
resolved_default = resolved_level if default_level is None else resolve_level(default_level)
|
|
87
|
+
|
|
88
|
+
# 2. Build the underlying stdlib logger
|
|
89
|
+
self._logger = create_logger(
|
|
90
|
+
name=name,
|
|
91
|
+
level=resolved_level,
|
|
92
|
+
file_path=file_path,
|
|
93
|
+
encoding=encoding,
|
|
94
|
+
console=console,
|
|
95
|
+
colored=colored,
|
|
96
|
+
console_level=console_level,
|
|
97
|
+
file_level=file_level,
|
|
98
|
+
stream=stream,
|
|
99
|
+
message_format=message_format,
|
|
100
|
+
date_format=date_format,
|
|
101
|
+
utc=utc,
|
|
102
|
+
max_bytes=max_bytes,
|
|
103
|
+
backup_count=backup_count,
|
|
104
|
+
console_output=console_output,
|
|
105
|
+
file_output=file_output,
|
|
106
|
+
json_default=json_default,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# 3. Store the switch and the level used by a bare `log(...)`
|
|
110
|
+
self._enabled = enabled
|
|
111
|
+
self._default_level = resolved_default
|
|
112
|
+
|
|
113
|
+
# ----------------------------------------------------------------------
|
|
114
|
+
# State
|
|
115
|
+
# ----------------------------------------------------------------------
|
|
116
|
+
@property
|
|
117
|
+
def logger(self) -> logging.Logger:
|
|
118
|
+
"""The underlying stdlib `logging.Logger`, for direct access."""
|
|
119
|
+
return self._logger
|
|
120
|
+
|
|
121
|
+
@property
|
|
122
|
+
def name(self) -> str:
|
|
123
|
+
"""The name of the logger."""
|
|
124
|
+
return self._logger.name
|
|
125
|
+
|
|
126
|
+
@property
|
|
127
|
+
def level(self) -> int:
|
|
128
|
+
"""The level of the logger as a number."""
|
|
129
|
+
return self._logger.level
|
|
130
|
+
|
|
131
|
+
@property
|
|
132
|
+
def enabled(self) -> bool:
|
|
133
|
+
"""The switch: False means nothing is written, whatever the level."""
|
|
134
|
+
return self._enabled
|
|
135
|
+
|
|
136
|
+
@enabled.setter
|
|
137
|
+
@validate_call
|
|
138
|
+
def enabled(self, value: bool) -> None:
|
|
139
|
+
self._enabled = value
|
|
140
|
+
|
|
141
|
+
def is_enabled_for(
|
|
142
|
+
self,
|
|
143
|
+
level: int | str | None = None,
|
|
144
|
+
) -> bool:
|
|
145
|
+
"""Tell whether a record of this level would be written now.
|
|
146
|
+
|
|
147
|
+
Args:
|
|
148
|
+
level: The level to ask about. `None` for `default_level`.
|
|
149
|
+
|
|
150
|
+
Returns:
|
|
151
|
+
True if the logger is enabled and its own level lets the level through.
|
|
152
|
+
"""
|
|
153
|
+
|
|
154
|
+
# 1. The switch comes first
|
|
155
|
+
if not self._enabled:
|
|
156
|
+
return False
|
|
157
|
+
|
|
158
|
+
# 2. Then the level of the logger
|
|
159
|
+
resolved = self._default_level if level is None else resolve_level(level)
|
|
160
|
+
return self._logger.isEnabledFor(resolved)
|
|
161
|
+
|
|
162
|
+
def set_level(
|
|
163
|
+
self,
|
|
164
|
+
level: int | str,
|
|
165
|
+
) -> None:
|
|
166
|
+
"""Change the level of the logger.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
level: A positive number or a level name.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
# 1. Handlers follow the logger unless they were given a level of their own
|
|
173
|
+
self._logger.setLevel(resolve_level(level))
|
|
174
|
+
|
|
175
|
+
# ----------------------------------------------------------------------
|
|
176
|
+
# Logging - main
|
|
177
|
+
# ----------------------------------------------------------------------
|
|
178
|
+
def log(
|
|
179
|
+
self,
|
|
180
|
+
message: object,
|
|
181
|
+
*args: Any,
|
|
182
|
+
level: int | str | None = None,
|
|
183
|
+
**kwargs: Any,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Write a message at `level`, or at `default_level` when none is given.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
message: The message; may contain `%` placeholders filled from `args`.
|
|
189
|
+
*args: Values for the placeholders, as in `logging`.
|
|
190
|
+
level: A number or a name that replaces `default_level` for this call.
|
|
191
|
+
**kwargs: Forwarded to `logging.Logger.log` (`exc_info`, `extra`, ...).
|
|
192
|
+
"""
|
|
193
|
+
|
|
194
|
+
# 1. Only an explicit level is validated; the default was validated at creation
|
|
195
|
+
target_level = self._default_level if level is None else resolve_level(level)
|
|
196
|
+
|
|
197
|
+
# 2. Hand over to the one place that writes
|
|
198
|
+
self._emit(target_level, message, args, kwargs)
|
|
199
|
+
|
|
200
|
+
# `logger("text")` is `logger.log("text")`; the same function keeps the call depth equal
|
|
201
|
+
__call__ = log
|
|
202
|
+
|
|
203
|
+
# ----------------------------------------------------------------------
|
|
204
|
+
# Logging - levels
|
|
205
|
+
# ----------------------------------------------------------------------
|
|
206
|
+
def log_debug(self, message: object, *args: Any, **kwargs: Any) -> None:
|
|
207
|
+
"""Log a message with DEBUG severity."""
|
|
208
|
+
self._emit(logging.DEBUG, message, args, kwargs)
|
|
209
|
+
|
|
210
|
+
def log_info(self, message: object, *args: Any, **kwargs: Any) -> None:
|
|
211
|
+
"""Log a message with INFO severity."""
|
|
212
|
+
self._emit(logging.INFO, message, args, kwargs)
|
|
213
|
+
|
|
214
|
+
def log_success(self, message: object, *args: Any, **kwargs: Any) -> None:
|
|
215
|
+
"""Log a message with SUCCESS severity (25, between INFO and WARNING)."""
|
|
216
|
+
self._emit(SUCCESS_LEVEL, message, args, kwargs)
|
|
217
|
+
|
|
218
|
+
def log_warning(self, message: object, *args: Any, **kwargs: Any) -> None:
|
|
219
|
+
"""Log a message with WARNING severity."""
|
|
220
|
+
self._emit(logging.WARNING, message, args, kwargs)
|
|
221
|
+
|
|
222
|
+
def log_error(self, message: object, *args: Any, **kwargs: Any) -> None:
|
|
223
|
+
"""Log a message with ERROR severity."""
|
|
224
|
+
self._emit(logging.ERROR, message, args, kwargs)
|
|
225
|
+
|
|
226
|
+
def log_critical(self, message: object, *args: Any, **kwargs: Any) -> None:
|
|
227
|
+
"""Log a message with CRITICAL severity."""
|
|
228
|
+
self._emit(logging.CRITICAL, message, args, kwargs)
|
|
229
|
+
|
|
230
|
+
def log_exception(self, message: object, *args: Any, **kwargs: Any) -> None:
|
|
231
|
+
"""Log a message with ERROR severity and the current exception's traceback.
|
|
232
|
+
|
|
233
|
+
Meant for an `except` block. Pass `exc_info=<exception>` to log one
|
|
234
|
+
that is not being handled.
|
|
235
|
+
"""
|
|
236
|
+
kwargs.setdefault("exc_info", True)
|
|
237
|
+
self._emit(logging.ERROR, message, args, kwargs)
|
|
238
|
+
|
|
239
|
+
# ----------------------------------------------------------------------
|
|
240
|
+
# Lifecycle
|
|
241
|
+
# ----------------------------------------------------------------------
|
|
242
|
+
def close(self) -> None:
|
|
243
|
+
"""Close every handler (the log file included) and stop writing.
|
|
244
|
+
|
|
245
|
+
The logger can be created again under the same name.
|
|
246
|
+
"""
|
|
247
|
+
|
|
248
|
+
# 1. Nothing is written after the handlers are gone
|
|
249
|
+
self._enabled = False
|
|
250
|
+
|
|
251
|
+
# 2. Release the files
|
|
252
|
+
close_logger(self._logger)
|
|
253
|
+
|
|
254
|
+
def __enter__(self) -> "SimpleLogger":
|
|
255
|
+
return self
|
|
256
|
+
|
|
257
|
+
def __exit__(
|
|
258
|
+
self,
|
|
259
|
+
exc_type: type[BaseException] | None,
|
|
260
|
+
exc_value: BaseException | None,
|
|
261
|
+
traceback: TracebackType | None,
|
|
262
|
+
) -> None:
|
|
263
|
+
|
|
264
|
+
# 1. Close on the way out; an exception in the block is not suppressed
|
|
265
|
+
self.close()
|
|
266
|
+
|
|
267
|
+
# ----------------------------------------------------------------------
|
|
268
|
+
# Internal
|
|
269
|
+
# ----------------------------------------------------------------------
|
|
270
|
+
def _emit(
|
|
271
|
+
self,
|
|
272
|
+
level: int,
|
|
273
|
+
message: object,
|
|
274
|
+
args: tuple[Any, ...],
|
|
275
|
+
kwargs: dict[str, Any],
|
|
276
|
+
) -> None:
|
|
277
|
+
"""Write one record. The only place that talks to the stdlib logger."""
|
|
278
|
+
|
|
279
|
+
# 1. Skip entirely when switched off
|
|
280
|
+
if not self._enabled:
|
|
281
|
+
return
|
|
282
|
+
|
|
283
|
+
# 2. Point file, line and function at the caller of the public method:
|
|
284
|
+
# caller -> public method -> _emit -> Logger.log, so two frames are ours
|
|
285
|
+
kwargs["stacklevel"] = kwargs.get("stacklevel", 1) + 2
|
|
286
|
+
|
|
287
|
+
# 3. Delegate to the stdlib logger
|
|
288
|
+
self._logger.log(level, message, *args, **kwargs)
|
|
289
|
+
|
|
290
|
+
def __repr__(self) -> str:
|
|
291
|
+
return f"SimpleLogger(name={self.name!r}, level={self.level}, enabled={self._enabled})"
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
_DESIGN_NOTES = """
|
|
295
|
+
# SimpleLogger — A Logger With a Short Way to Use It
|
|
296
|
+
|
|
297
|
+
## Purpose
|
|
298
|
+
A stdlib logger already configured (console, file, colors, format) and a
|
|
299
|
+
small interface: `log_info("text %s", value)`, `logger("text")`.
|
|
300
|
+
|
|
301
|
+
## Changes Against the Old Version
|
|
302
|
+
* **`level` is keyword-only.** In `log("user %s", name)`, `name` is a
|
|
303
|
+
placeholder value, as in `logging`, never the level.
|
|
304
|
+
* **File, line and function are the caller's.** Every public method calls
|
|
305
|
+
`_emit` directly, and `_emit` adds exactly two frames, so `log`, `log_info`
|
|
306
|
+
and `__call__` all report the caller and never `SimpleLogger.py`. A
|
|
307
|
+
`stacklevel` given by the caller still counts from their own call.
|
|
308
|
+
* **`enabled` and `is_enabled_for(level)` are two things.** `enabled` is the
|
|
309
|
+
switch and reads back what was assigned; "would a record of this level be
|
|
310
|
+
written?" has a level in it, so it is a method.
|
|
311
|
+
* **JSON output** (`console_output`, `file_output`, `json_default`): a
|
|
312
|
+
readable console and a JSON file, or JSON on both. Extra fields
|
|
313
|
+
(`logger.log_info("x", extra={"request_id": 7})`) become keys of the object.
|
|
314
|
+
* **`log_exception`, `log_success`** (the level of the `SUCCESS` style
|
|
315
|
+
preset), **`close()`**, **`with`**, **`set_level`**, `name`, `level`.
|
|
316
|
+
* **`default_level` starts at `level` when not given.** With `level="warning"`
|
|
317
|
+
a bare `logger("text")` would otherwise be silently below the logger's
|
|
318
|
+
level.
|
|
319
|
+
|
|
320
|
+
## The Hot Path
|
|
321
|
+
`log_*` check one bool and call the stdlib; no validation, because their
|
|
322
|
+
levels are constants. Only an explicit `level=` is validated. `__call__ =
|
|
323
|
+
log` is the same function, so it costs nothing extra and keeps the depth
|
|
324
|
+
that `stacklevel` counts.
|
|
325
|
+
|
|
326
|
+
## `close()`
|
|
327
|
+
Switches `enabled` off and closes the handlers. Otherwise a late `log_error`
|
|
328
|
+
would reach Python's "last resort" handler and appear on standard error.
|
|
329
|
+
|
|
330
|
+
## `default_level` Is Fixed at Creation
|
|
331
|
+
`set_level` changes the logger, not the level of a bare `log(...)`. A verbosity
|
|
332
|
+
switch such as `set_level("error")` should quiet bare calls too, not promote
|
|
333
|
+
them to ERROR. Create the logger again to change `default_level`.
|
|
334
|
+
|
|
335
|
+
## Same Name, Same Logger
|
|
336
|
+
A `SimpleLogger` wraps the logger of the standard registry under its name,
|
|
337
|
+
so a second `SimpleLogger(name="x")` reconfigures the first. Give different
|
|
338
|
+
loggers different names. The default `"simplibs"` is the parent of every
|
|
339
|
+
`simplibs.*` logger and receives what other simplibs libraries log through
|
|
340
|
+
plain `logging`.
|
|
341
|
+
|
|
342
|
+
## `propagate` Is Not a Parameter
|
|
343
|
+
A `SimpleLogger` owns its handlers, so its records stop there
|
|
344
|
+
(`propagate=False`) and nothing is written twice. `create_logger` has the
|
|
345
|
+
parameter for the rare caller who wants the handlers of a parent as well;
|
|
346
|
+
through a `SimpleLogger` that is `logger.logger.propagate = True`.
|
|
347
|
+
"""
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from .annotations import BackupCount, LogStream, MaxBytes
|
|
2
|
+
from .definitions import (
|
|
3
|
+
DateFormat,
|
|
4
|
+
LEVEL_NAMES,
|
|
5
|
+
LoggerLevel,
|
|
6
|
+
LoggerName,
|
|
7
|
+
MessageFormat,
|
|
8
|
+
OUTPUT_FORMATS,
|
|
9
|
+
OutputFormat,
|
|
10
|
+
SUCCESS_LEVEL,
|
|
11
|
+
)
|
|
12
|
+
from .levels import register_success_level, resolve_level
|
|
13
|
+
from .formatting import ColoredFormatter, JsonFormatter, build_formatter
|
|
14
|
+
from .handlers import build_console_handler, build_file_handler
|
|
15
|
+
from .lifecycle import close_logger, create_logger
|
|
16
|
+
from .SimpleLogger import SimpleLogger
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
# core — the logger itself
|
|
20
|
+
"SimpleLogger",
|
|
21
|
+
"create_logger",
|
|
22
|
+
"close_logger",
|
|
23
|
+
# handlers — where records go
|
|
24
|
+
"build_console_handler",
|
|
25
|
+
"build_file_handler",
|
|
26
|
+
# formatting — how a record looks
|
|
27
|
+
"build_formatter",
|
|
28
|
+
"ColoredFormatter",
|
|
29
|
+
"JsonFormatter",
|
|
30
|
+
# levels
|
|
31
|
+
"resolve_level",
|
|
32
|
+
"register_success_level",
|
|
33
|
+
"SUCCESS_LEVEL",
|
|
34
|
+
"LEVEL_NAMES",
|
|
35
|
+
# validated atoms
|
|
36
|
+
"LoggerLevel",
|
|
37
|
+
"LoggerName",
|
|
38
|
+
"DateFormat",
|
|
39
|
+
"MessageFormat",
|
|
40
|
+
"OutputFormat",
|
|
41
|
+
"OUTPUT_FORMATS",
|
|
42
|
+
# annotations
|
|
43
|
+
"BackupCount",
|
|
44
|
+
"LogStream",
|
|
45
|
+
"MaxBytes",
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
_DESIGN_NOTES = """
|
|
50
|
+
# simplibs-logger — Package Root
|
|
51
|
+
|
|
52
|
+
## Purpose
|
|
53
|
+
A ready-to-use logger with a console, an optional rotating file, colored
|
|
54
|
+
level names, optional JSON output and a short interface:
|
|
55
|
+
|
|
56
|
+
from simplibs.logger import SimpleLogger
|
|
57
|
+
|
|
58
|
+
logger = SimpleLogger(file_path="logs/app.log")
|
|
59
|
+
logger.log_info("Application started")
|
|
60
|
+
|
|
61
|
+
## What the Root Exports
|
|
62
|
+
Everything a caller needs, from one import line: the logger (`SimpleLogger`,
|
|
63
|
+
`create_logger`, `close_logger`), the parts it is built from
|
|
64
|
+
(`build_console_handler`, `build_file_handler`, `build_formatter`,
|
|
65
|
+
`ColoredFormatter`, `JsonFormatter`), the level helpers, the validated atoms and the
|
|
66
|
+
annotations for the caller's own signatures.
|
|
67
|
+
|
|
68
|
+
## What the Root Deliberately Does NOT Export
|
|
69
|
+
Internals: the `_helpers/` and `_validations/` of each sub-package.
|
|
70
|
+
|
|
71
|
+
## Layers That Only Look Left
|
|
72
|
+
`definitions` and `annotations` <- `levels` <- `formatting` <- `handlers` <-
|
|
73
|
+
`lifecycle` <- `SimpleLogger`. Each layer imports only from those before it.
|
|
74
|
+
|
|
75
|
+
## One Error Family
|
|
76
|
+
The library has no exception root of its own. Atoms raise the cards of
|
|
77
|
+
`simplibs-object`, signatures raise those of `simplibs-validate`, and the one
|
|
78
|
+
state of its own (a log file that cannot be opened) is an `AccessError` named
|
|
79
|
+
`LOGGER_FILE_OPEN_FAILED`.
|
|
80
|
+
|
|
81
|
+
## Dependencies
|
|
82
|
+
`simplibs-exception`, `simplibs-object`, `simplibs-validate`,
|
|
83
|
+
`simplibs-path`, `simplibs-style`, `simplibs-file` (`EncodingName`).
|
|
84
|
+
|
|
85
|
+
## `__all__`
|
|
86
|
+
Defined here only; the sub-packages export by plain imports.
|
|
87
|
+
"""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
# Inners
|
|
3
|
+
from ._helpers import is_backup_count
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
BackupCount = Annotated[int, is_backup_count]
|
|
7
|
+
"""How many rotated log files to keep: an integer of at least one. Zero, negatives and `bool` are rejected."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_DESIGN_NOTES = """
|
|
11
|
+
# BackupCount — Rotation Needs at Least One Backup
|
|
12
|
+
|
|
13
|
+
## Purpose
|
|
14
|
+
The type of `backup_count`. It only has an effect together with `max_bytes`.
|
|
15
|
+
A rejected value is refused by `@validate_call`, with the card of `simplibs-validate`.
|
|
16
|
+
|
|
17
|
+
## Why `Annotated`
|
|
18
|
+
`Annotated[int, is_backup_count]` is a type an editor and a type checker
|
|
19
|
+
understand: they see a plain `int` and offer its methods and hints, while
|
|
20
|
+
`@validate_call` reads the predicate from the metadata and checks the value.
|
|
21
|
+
`validated_type(int, is_backup_count)` builds the same thing, but as a function
|
|
22
|
+
call — the result is a value, not a type, so editors cannot follow it. It is
|
|
23
|
+
meant for building many such types in bulk, not for a hand-written definition.
|
|
24
|
+
"""
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
# Inners
|
|
3
|
+
from ._helpers import is_writable_stream
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
LogStream = Annotated[object | None, is_writable_stream]
|
|
7
|
+
"""A stream the console handler writes to: any object with `write`; `None` for standard error."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_DESIGN_NOTES = """
|
|
11
|
+
# LogStream — Where the Console Handler Writes
|
|
12
|
+
|
|
13
|
+
## Purpose
|
|
14
|
+
The type of `stream`. `None` is part of the type itself.
|
|
15
|
+
|
|
16
|
+
## Colors Follow the Style Switch, Not the Stream
|
|
17
|
+
`simplibs-style` decides by its own switch (and by standard output when it
|
|
18
|
+
starts from the environment), not by what this stream is. A program that
|
|
19
|
+
writes logs into a file-like object can pass `colored=False`, or switch
|
|
20
|
+
styles off with `set_styles_enabled(False)`.
|
|
21
|
+
|
|
22
|
+
## Why `Annotated`
|
|
23
|
+
`Annotated[object | None, is_writable_stream]` is a type an editor and a type checker
|
|
24
|
+
understand: they see the underlying type and offer its methods and hints, while
|
|
25
|
+
`@validate_call` reads the predicate from the metadata and checks the value.
|
|
26
|
+
`validated_type` builds the same thing as a function call — the result is a value,
|
|
27
|
+
not a type, so editors cannot follow it. It is meant for building many such types
|
|
28
|
+
in bulk, not for a hand-written definition.
|
|
29
|
+
"""
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
# Inners
|
|
3
|
+
from ._helpers import is_max_bytes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
MaxBytes = Annotated[int | None, is_max_bytes]
|
|
7
|
+
"""The size at which a log file is rotated, in bytes; `None` for a file that is never rotated."""
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
_DESIGN_NOTES = """
|
|
11
|
+
# MaxBytes — A Size, or No Rotation
|
|
12
|
+
|
|
13
|
+
## Purpose
|
|
14
|
+
The type of `max_bytes`. `None` is part of the type itself, so a signature
|
|
15
|
+
needs no separate `| None` next to a validated type.
|
|
16
|
+
|
|
17
|
+
## Why `Annotated`
|
|
18
|
+
`Annotated[int | None, is_max_bytes]` is a type an editor and a type checker
|
|
19
|
+
understand: they see the underlying type and offer its methods and hints, while
|
|
20
|
+
`@validate_call` reads the predicate from the metadata and checks the value.
|
|
21
|
+
`validated_type` builds the same thing as a function call — the result is a value,
|
|
22
|
+
not a type, so editors cannot follow it. It is meant for building many such types
|
|
23
|
+
in bulk, not for a hand-written definition.
|
|
24
|
+
"""
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
from .BackupCount import BackupCount
|
|
2
|
+
from .LogStream import LogStream
|
|
3
|
+
from .MaxBytes import MaxBytes
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_DESIGN_NOTES = """
|
|
7
|
+
# annotations — Shared Types of simplibs-logger
|
|
8
|
+
|
|
9
|
+
Plain imports, no `__all__` (the package root decides what is public).
|
|
10
|
+
`PathLike` and `EncodingName` are not redefined here: they come from
|
|
11
|
+
`simplibs.path` and `simplibs.file`.
|
|
12
|
+
|
|
13
|
+
## Internal Components Registry
|
|
14
|
+
|
|
15
|
+
| Component | Type | Description |
|
|
16
|
+
| :------------ | :------------- | :-------------------------------------------------------- |
|
|
17
|
+
| `BackupCount` | Annotated Type | How many rotated files to keep; at least one. |
|
|
18
|
+
| `LogStream` | Annotated Type | A stream with `write`, or `None` for standard error. |
|
|
19
|
+
| `MaxBytes` | Annotated Type | A size limit in bytes, or `None` for no rotation. |
|
|
20
|
+
"""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from .is_backup_count import is_backup_count
|
|
2
|
+
from .is_max_bytes import is_max_bytes
|
|
3
|
+
from .is_writable_stream import is_writable_stream
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
_DESIGN_NOTES = """
|
|
7
|
+
# Logger Annotation Helpers Sub-Package
|
|
8
|
+
|
|
9
|
+
## Purpose
|
|
10
|
+
The rules behind the validated types of the logger.
|
|
11
|
+
|
|
12
|
+
## Internal Components Registry
|
|
13
|
+
|
|
14
|
+
| Component | Type | Description |
|
|
15
|
+
| :------------------- | :------- | :------------------------------------------------------------ |
|
|
16
|
+
| `is_backup_count` | Function | An `int` of at least one. |
|
|
17
|
+
| `is_max_bytes` | Function | `None`, or an `int` greater than zero. |
|
|
18
|
+
| `is_writable_stream` | Function | `None`, or an object with a callable `write`. |
|
|
19
|
+
"""
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
def is_backup_count(value: int) -> bool:
|
|
2
|
+
"""Check whether a value is a usable number of rotated log files to keep.
|
|
3
|
+
|
|
4
|
+
Args:
|
|
5
|
+
value: The candidate.
|
|
6
|
+
|
|
7
|
+
Returns:
|
|
8
|
+
True for an `int` of at least one; False for zero, negatives and `bool`.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
# 1. A real integer (a bool is an int, but never a count)
|
|
12
|
+
if not isinstance(value, int) or isinstance(value, bool):
|
|
13
|
+
return False
|
|
14
|
+
|
|
15
|
+
# 2. At least one backup, or the file would never rotate
|
|
16
|
+
return value >= 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
_DESIGN_NOTES = """
|
|
20
|
+
# is_backup_count — Why Zero Is Refused
|
|
21
|
+
|
|
22
|
+
The rule behind `BackupCount`. In the standard `RotatingFileHandler`
|
|
23
|
+
`backupCount=0` switches rotation off, whatever the size limit says. The
|
|
24
|
+
log would grow without bound while the caller believes it is capped.
|
|
25
|
+
"""
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
def is_max_bytes(value: int | None) -> bool:
|
|
2
|
+
"""Check whether a value is a usable size limit of a log file.
|
|
3
|
+
|
|
4
|
+
Args:
|
|
5
|
+
value: The candidate.
|
|
6
|
+
|
|
7
|
+
Returns:
|
|
8
|
+
True for `None` (no limit) or an `int` greater than zero; False for
|
|
9
|
+
zero, negatives and `bool`.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
# 1. None means "never rotate"
|
|
13
|
+
if value is None:
|
|
14
|
+
return True
|
|
15
|
+
|
|
16
|
+
# 2. Otherwise a real, positive integer (a bool is an int, but never a size)
|
|
17
|
+
return isinstance(value, int) and not isinstance(value, bool) and value > 0
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_DESIGN_NOTES = """
|
|
21
|
+
# is_max_bytes — "No Limit, or a Positive Size"
|
|
22
|
+
|
|
23
|
+
The rule behind `MaxBytes`. A limit of zero would be read by the standard
|
|
24
|
+
library as "never rotate", the opposite of what a caller who wrote a limit
|
|
25
|
+
meant, so it is refused at the signature instead.
|
|
26
|
+
"""
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def is_writable_stream(value: Any) -> bool:
|
|
5
|
+
"""Check whether a value can serve as the stream of a console handler.
|
|
6
|
+
|
|
7
|
+
Args:
|
|
8
|
+
value: The candidate.
|
|
9
|
+
|
|
10
|
+
Returns:
|
|
11
|
+
True for `None` (the default, standard error) or any object with a
|
|
12
|
+
callable `write`; False otherwise.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
# 1. None means "use standard error"
|
|
16
|
+
if value is None:
|
|
17
|
+
return True
|
|
18
|
+
|
|
19
|
+
# 2. Duck typing: wrapped streams (such as colorama's) are not `TextIOBase` subclasses
|
|
20
|
+
return callable(getattr(value, "write", None))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
_DESIGN_NOTES = """
|
|
24
|
+
# is_writable_stream — Anything That Can Write
|
|
25
|
+
|
|
26
|
+
The rule behind `LogStream`. `isinstance(stream, io.TextIOBase)` would refuse
|
|
27
|
+
the stream wrappers that Windows programs often put on `sys.stderr`. What the
|
|
28
|
+
handler needs is `write`, so that is what is checked, at the signature rather
|
|
29
|
+
than in the middle of the first emit.
|
|
30
|
+
"""
|