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.
Files changed (53) hide show
  1. simplibs/logger/SimpleLogger.py +347 -0
  2. simplibs/logger/__init__.py +87 -0
  3. simplibs/logger/annotations/BackupCount.py +24 -0
  4. simplibs/logger/annotations/LogStream.py +29 -0
  5. simplibs/logger/annotations/MaxBytes.py +24 -0
  6. simplibs/logger/annotations/__init__.py +20 -0
  7. simplibs/logger/annotations/_helpers/__init__.py +19 -0
  8. simplibs/logger/annotations/_helpers/is_backup_count.py +25 -0
  9. simplibs/logger/annotations/_helpers/is_max_bytes.py +26 -0
  10. simplibs/logger/annotations/_helpers/is_writable_stream.py +30 -0
  11. simplibs/logger/definitions/DateFormat.py +51 -0
  12. simplibs/logger/definitions/LoggerLevel.py +73 -0
  13. simplibs/logger/definitions/LoggerName.py +58 -0
  14. simplibs/logger/definitions/MessageFormat.py +59 -0
  15. simplibs/logger/definitions/OutputFormat.py +38 -0
  16. simplibs/logger/definitions/__init__.py +31 -0
  17. simplibs/logger/definitions/constants/LEVEL_NAMES.py +36 -0
  18. simplibs/logger/definitions/constants/OUTPUT_FORMATS.py +25 -0
  19. simplibs/logger/definitions/constants/SUCCESS_LEVEL.py +23 -0
  20. simplibs/logger/definitions/constants/__init__.py +19 -0
  21. simplibs/logger/formatting/ColoredFormatter.py +97 -0
  22. simplibs/logger/formatting/JsonFormatter.py +159 -0
  23. simplibs/logger/formatting/__init__.py +21 -0
  24. simplibs/logger/formatting/_helpers/__init__.py +24 -0
  25. simplibs/logger/formatting/_helpers/collect_extra_fields.py +43 -0
  26. simplibs/logger/formatting/_helpers/constants/JSON_KEYS.py +22 -0
  27. simplibs/logger/formatting/_helpers/constants/LEVEL_STYLES.py +38 -0
  28. simplibs/logger/formatting/_helpers/constants/RECORD_ATTRIBUTES.py +24 -0
  29. simplibs/logger/formatting/_helpers/constants/__init__.py +16 -0
  30. simplibs/logger/formatting/_helpers/encode_unknown_value.py +69 -0
  31. simplibs/logger/formatting/_helpers/format_json_time.py +40 -0
  32. simplibs/logger/formatting/_helpers/parse_levelname_field.py +56 -0
  33. simplibs/logger/formatting/_helpers/pick_level_style.py +34 -0
  34. simplibs/logger/formatting/_helpers/replace_non_finite.py +48 -0
  35. simplibs/logger/formatting/_helpers/style_levelname.py +47 -0
  36. simplibs/logger/formatting/build_formatter.py +89 -0
  37. simplibs/logger/handlers/__init__.py +18 -0
  38. simplibs/logger/handlers/_validations/__init__.py +12 -0
  39. simplibs/logger/handlers/_validations/raise_log_file_open_error.py +47 -0
  40. simplibs/logger/handlers/build_console_handler.py +83 -0
  41. simplibs/logger/handlers/build_file_handler.py +119 -0
  42. simplibs/logger/levels/__init__.py +18 -0
  43. simplibs/logger/levels/register_success_level.py +33 -0
  44. simplibs/logger/levels/resolve_level.py +43 -0
  45. simplibs/logger/lifecycle/__init__.py +19 -0
  46. simplibs/logger/lifecycle/close_logger.py +45 -0
  47. simplibs/logger/lifecycle/create_logger.py +172 -0
  48. simplibs/logger/py.typed +1 -0
  49. simplibs_logger-0.1.0.dist-info/METADATA +508 -0
  50. simplibs_logger-0.1.0.dist-info/RECORD +53 -0
  51. simplibs_logger-0.1.0.dist-info/WHEEL +5 -0
  52. simplibs_logger-0.1.0.dist-info/licenses/LICENSE +21 -0
  53. 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
+ """