xtr-logging-contracts 1.0.0__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.
- xtr_logging_contracts-1.0.0/LICENSE +21 -0
- xtr_logging_contracts-1.0.0/PKG-INFO +179 -0
- xtr_logging_contracts-1.0.0/README.md +158 -0
- xtr_logging_contracts-1.0.0/pyproject.toml +140 -0
- xtr_logging_contracts-1.0.0/pyproject.toml.orig +139 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/__init__.py +55 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/abstract_logger.py +69 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/context.py +23 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/exception/__init__.py +17 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/exception/invalid_level_error.py +26 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/exception/logging_error.py +14 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/level.py +108 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/logger_aware.py +36 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/logger_aware_interface.py +23 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/logger_interface.py +75 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/null_logger.py +32 -0
- xtr_logging_contracts-1.0.0/src/xtr_logging_contracts/py.typed +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 xterr
|
|
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.
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: xtr-logging-contracts
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: The logging contract: one interface, eight severities, and a logger that discards everything.
|
|
5
|
+
Keywords: logging,contracts,interface,protocol,structured-logging
|
|
6
|
+
Author: Razvan Ceana
|
|
7
|
+
Author-email: Razvan Ceana <razvan@ceana.ro>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Topic :: System :: Logging
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Requires-Dist: typing-extensions>=4.4
|
|
19
|
+
Requires-Python: >=3.11
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
<div align="center">
|
|
23
|
+
|
|
24
|
+
# xtr-logging-contracts
|
|
25
|
+
|
|
26
|
+
**The logging contract, and nothing else â so a library that logs installs nothing else.**
|
|
27
|
+
|
|
28
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
29
|
+
<img alt="core dependencies: 1" src="https://img.shields.io/badge/core%20deps-1-3FB950">
|
|
30
|
+
<img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
|
|
31
|
+
<img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
|
|
32
|
+
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Why?
|
|
38
|
+
|
|
39
|
+
A library that logs should not decide where records go. It takes a `LoggerInterface`, defaults to
|
|
40
|
+
a `NullLogger`, and leaves handlers, formatters and configuration to the application that wires
|
|
41
|
+
it.
|
|
42
|
+
|
|
43
|
+
Which means a library needs the *contract*, not an implementation â and should not pay for one.
|
|
44
|
+
Depending on [xtr-logging](https://github.com/xterr/python-xtr-logging) to annotate one parameter
|
|
45
|
+
drags in a JSON codec, a clock and seventeen handlers the library never touches. This package is
|
|
46
|
+
that dependency, reduced to what the seam is actually made of:
|
|
47
|
+
|
|
48
|
+
- ð§Đ **`LoggerInterface`** â eight severities plus `log()`, with a context mapping.
|
|
49
|
+
- ðģïļ **`NullLogger`** â what makes logging optional for your callers.
|
|
50
|
+
- ðŠ **`Level`** â the eight RFC 5424 severities, comparable as integers.
|
|
51
|
+
- ðïļ **`Context`** â the mapping every method takes, and the key an exception goes under.
|
|
52
|
+
- ðŠķ **One dependency** â `typing-extensions`, for `@override` on 3.11.
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from xtr_logging_contracts import LoggerInterface, NullLogger
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class Checkout:
|
|
59
|
+
def __init__(self, logger: LoggerInterface | None = None) -> None:
|
|
60
|
+
self._logger = logger or NullLogger()
|
|
61
|
+
|
|
62
|
+
def pay(self, order: Order) -> None:
|
|
63
|
+
self._logger.error("payment {order} failed", {"order": order.id})
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Install
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
uv add xtr-logging-contracts
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Requires Python 3.11+.
|
|
73
|
+
|
|
74
|
+
## Who installs what
|
|
75
|
+
|
|
76
|
+
| | Depends on |
|
|
77
|
+
| --- | --- |
|
|
78
|
+
| **A library that logs** | `xtr-logging-contracts` at runtime, `xtr-logging` as a dev dependency â its tests build real loggers and assert on a `TestHandler`. |
|
|
79
|
+
| **An application** | `xtr-logging`, which implements this contract and wires channels, handlers and processors from configuration. |
|
|
80
|
+
|
|
81
|
+
`xtr-logging` **re-exports** every symbol here rather than redefining it, so
|
|
82
|
+
`xtr_logging.LoggerInterface is xtr_logging_contracts.LoggerInterface`. That identity is what lets
|
|
83
|
+
a container register a logger under the interface and a library, which never imported
|
|
84
|
+
`xtr-logging`, receive it.
|
|
85
|
+
|
|
86
|
+
## The interface
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
class LoggerInterface(Protocol):
|
|
90
|
+
def emergency(self, message: str, /, context: Context | None = None) -> None: ...
|
|
91
|
+
def alert(self, message: str, /, context: Context | None = None) -> None: ...
|
|
92
|
+
def critical(self, message: str, /, context: Context | None = None) -> None: ...
|
|
93
|
+
def error(self, message: str, /, context: Context | None = None) -> None: ...
|
|
94
|
+
def warning(self, message: str, /, context: Context | None = None) -> None: ...
|
|
95
|
+
def notice(self, message: str, /, context: Context | None = None) -> None: ...
|
|
96
|
+
def info(self, message: str, /, context: Context | None = None) -> None: ...
|
|
97
|
+
def debug(self, message: str, /, context: Context | None = None) -> None: ...
|
|
98
|
+
def log(self, level: LevelLike, message: str, /, context: Context | None = None) -> None: ...
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The rules:
|
|
102
|
+
|
|
103
|
+
- `context` is a mapping of anything. Formatters describe what they cannot serialise; a value
|
|
104
|
+
never makes logging fail.
|
|
105
|
+
- An exception to report goes under `context["exception"]`.
|
|
106
|
+
- `{key}` placeholders in the message are filled from context downstream, not by the logger, so a
|
|
107
|
+
handler can still see the template and the values apart.
|
|
108
|
+
|
|
109
|
+
`AbstractLogger` implements the eight severity methods on top of `log()`, so an implementation
|
|
110
|
+
writes one method. `LoggerAware` gives a class a `logger` that is a `NullLogger` until
|
|
111
|
+
`set_logger()` is called.
|
|
112
|
+
|
|
113
|
+
> ruff's `PLE1205` assumes every `logger.info(...)` is the standard library's and flags the
|
|
114
|
+
> context mapping as a stray format argument. Ignore it in projects using this interface.
|
|
115
|
+
|
|
116
|
+
### Levels
|
|
117
|
+
|
|
118
|
+
| Level | Value | RFC 5424 | | Level | Value | RFC 5424 |
|
|
119
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
120
|
+
| `DEBUG` | 100 | 7 | | `ERROR` | 400 | 3 |
|
|
121
|
+
| `INFO` | 200 | 6 | | `CRITICAL` | 500 | 2 |
|
|
122
|
+
| `NOTICE` | 250 | 5 | | `ALERT` | 550 | 1 |
|
|
123
|
+
| `WARNING` | 300 | 4 | | `EMERGENCY` | 600 | 0 |
|
|
124
|
+
|
|
125
|
+
Anywhere a level is accepted, `Level.parse` reads it: a `Level`, its value, an RFC 5424 severity,
|
|
126
|
+
or a name in any case (`"error"`). Anything else raises `InvalidLevelError`.
|
|
127
|
+
|
|
128
|
+
### Context
|
|
129
|
+
|
|
130
|
+
`Context` is `Mapping[str, object]` â the second argument to every method above. Values are
|
|
131
|
+
`object` because a caller may log anything; whatever writes the record normalises what it cannot
|
|
132
|
+
serialise rather than refusing it. An exception to report goes under `EXCEPTION_KEY`:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from xtr_logging_contracts import EXCEPTION_KEY
|
|
136
|
+
|
|
137
|
+
logger.error("payment failed", {"order": order.id, EXCEPTION_KEY: error})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## What is not here
|
|
141
|
+
|
|
142
|
+
Everything that *acts* on what was logged: `LogRecord`, `Logger`, `LoggerFactory`,
|
|
143
|
+
`LoggingConfig`, the seventeen handlers, the processors, the formatters, ambient
|
|
144
|
+
`bound_context()`, and the standard-library bridge. All of that is
|
|
145
|
+
[xtr-logging](https://github.com/xterr/python-xtr-logging).
|
|
146
|
+
|
|
147
|
+
`LogRecord` in particular belongs there, not here: it appears in no signature above. A library
|
|
148
|
+
that logs never builds one or sees one â it is made inside the logger and consumed by handlers
|
|
149
|
+
and processors, which are implementation.
|
|
150
|
+
|
|
151
|
+
`HandlerInterface`, `ProcessorInterface` and `FormatterInterface` are deliberately absent for the
|
|
152
|
+
same reason, plus one more: nothing outside `xtr-logging` implements them yet, and a contract
|
|
153
|
+
package earns its stability by staying small. They move here when a third-party handler needs
|
|
154
|
+
them â and `LogRecord` would move with them.
|
|
155
|
+
|
|
156
|
+
## Errors
|
|
157
|
+
|
|
158
|
+
| Error | Raised when |
|
|
159
|
+
| --- | --- |
|
|
160
|
+
| `LoggingError` | Never directly â the base every logging error derives from, `xtr-logging`'s included |
|
|
161
|
+
| `InvalidLevelError` | A value names no level (also a `ValueError`) |
|
|
162
|
+
|
|
163
|
+
## Development
|
|
164
|
+
|
|
165
|
+
Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
|
|
166
|
+
`packages/xtr-logging-contracts`; run the commands below from there. The `python-xtr-logging-contracts` repository is a
|
|
167
|
+
read-only copy, so send issues and pull requests to the monorepo.
|
|
168
|
+
|
|
169
|
+
```sh
|
|
170
|
+
uv sync
|
|
171
|
+
uv run ruff check . && uv run ruff format --check .
|
|
172
|
+
uv run basedpyright
|
|
173
|
+
uv run ty check
|
|
174
|
+
uv run pytest
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## License
|
|
178
|
+
|
|
179
|
+
MIT â see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# xtr-logging-contracts
|
|
4
|
+
|
|
5
|
+
**The logging contract, and nothing else â so a library that logs installs nothing else.**
|
|
6
|
+
|
|
7
|
+
<img alt="python 3.11+" src="https://img.shields.io/badge/python-%E2%89%A5%203.11-3776AB?logo=python&logoColor=white">
|
|
8
|
+
<img alt="core dependencies: 1" src="https://img.shields.io/badge/core%20deps-1-3FB950">
|
|
9
|
+
<img alt="typed" src="https://img.shields.io/badge/typed-ty%20%2B%20basedpyright-1f6feb">
|
|
10
|
+
<img alt="license MIT" src="https://img.shields.io/badge/license-MIT-blue">
|
|
11
|
+
|
|
12
|
+
</div>
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Why?
|
|
17
|
+
|
|
18
|
+
A library that logs should not decide where records go. It takes a `LoggerInterface`, defaults to
|
|
19
|
+
a `NullLogger`, and leaves handlers, formatters and configuration to the application that wires
|
|
20
|
+
it.
|
|
21
|
+
|
|
22
|
+
Which means a library needs the *contract*, not an implementation â and should not pay for one.
|
|
23
|
+
Depending on [xtr-logging](https://github.com/xterr/python-xtr-logging) to annotate one parameter
|
|
24
|
+
drags in a JSON codec, a clock and seventeen handlers the library never touches. This package is
|
|
25
|
+
that dependency, reduced to what the seam is actually made of:
|
|
26
|
+
|
|
27
|
+
- ð§Đ **`LoggerInterface`** â eight severities plus `log()`, with a context mapping.
|
|
28
|
+
- ðģïļ **`NullLogger`** â what makes logging optional for your callers.
|
|
29
|
+
- ðŠ **`Level`** â the eight RFC 5424 severities, comparable as integers.
|
|
30
|
+
- ðïļ **`Context`** â the mapping every method takes, and the key an exception goes under.
|
|
31
|
+
- ðŠķ **One dependency** â `typing-extensions`, for `@override` on 3.11.
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from xtr_logging_contracts import LoggerInterface, NullLogger
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class Checkout:
|
|
38
|
+
def __init__(self, logger: LoggerInterface | None = None) -> None:
|
|
39
|
+
self._logger = logger or NullLogger()
|
|
40
|
+
|
|
41
|
+
def pay(self, order: Order) -> None:
|
|
42
|
+
self._logger.error("payment {order} failed", {"order": order.id})
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
uv add xtr-logging-contracts
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Requires Python 3.11+.
|
|
52
|
+
|
|
53
|
+
## Who installs what
|
|
54
|
+
|
|
55
|
+
| | Depends on |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| **A library that logs** | `xtr-logging-contracts` at runtime, `xtr-logging` as a dev dependency â its tests build real loggers and assert on a `TestHandler`. |
|
|
58
|
+
| **An application** | `xtr-logging`, which implements this contract and wires channels, handlers and processors from configuration. |
|
|
59
|
+
|
|
60
|
+
`xtr-logging` **re-exports** every symbol here rather than redefining it, so
|
|
61
|
+
`xtr_logging.LoggerInterface is xtr_logging_contracts.LoggerInterface`. That identity is what lets
|
|
62
|
+
a container register a logger under the interface and a library, which never imported
|
|
63
|
+
`xtr-logging`, receive it.
|
|
64
|
+
|
|
65
|
+
## The interface
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
class LoggerInterface(Protocol):
|
|
69
|
+
def emergency(self, message: str, /, context: Context | None = None) -> None: ...
|
|
70
|
+
def alert(self, message: str, /, context: Context | None = None) -> None: ...
|
|
71
|
+
def critical(self, message: str, /, context: Context | None = None) -> None: ...
|
|
72
|
+
def error(self, message: str, /, context: Context | None = None) -> None: ...
|
|
73
|
+
def warning(self, message: str, /, context: Context | None = None) -> None: ...
|
|
74
|
+
def notice(self, message: str, /, context: Context | None = None) -> None: ...
|
|
75
|
+
def info(self, message: str, /, context: Context | None = None) -> None: ...
|
|
76
|
+
def debug(self, message: str, /, context: Context | None = None) -> None: ...
|
|
77
|
+
def log(self, level: LevelLike, message: str, /, context: Context | None = None) -> None: ...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The rules:
|
|
81
|
+
|
|
82
|
+
- `context` is a mapping of anything. Formatters describe what they cannot serialise; a value
|
|
83
|
+
never makes logging fail.
|
|
84
|
+
- An exception to report goes under `context["exception"]`.
|
|
85
|
+
- `{key}` placeholders in the message are filled from context downstream, not by the logger, so a
|
|
86
|
+
handler can still see the template and the values apart.
|
|
87
|
+
|
|
88
|
+
`AbstractLogger` implements the eight severity methods on top of `log()`, so an implementation
|
|
89
|
+
writes one method. `LoggerAware` gives a class a `logger` that is a `NullLogger` until
|
|
90
|
+
`set_logger()` is called.
|
|
91
|
+
|
|
92
|
+
> ruff's `PLE1205` assumes every `logger.info(...)` is the standard library's and flags the
|
|
93
|
+
> context mapping as a stray format argument. Ignore it in projects using this interface.
|
|
94
|
+
|
|
95
|
+
### Levels
|
|
96
|
+
|
|
97
|
+
| Level | Value | RFC 5424 | | Level | Value | RFC 5424 |
|
|
98
|
+
| --- | --- | --- | --- | --- | --- | --- |
|
|
99
|
+
| `DEBUG` | 100 | 7 | | `ERROR` | 400 | 3 |
|
|
100
|
+
| `INFO` | 200 | 6 | | `CRITICAL` | 500 | 2 |
|
|
101
|
+
| `NOTICE` | 250 | 5 | | `ALERT` | 550 | 1 |
|
|
102
|
+
| `WARNING` | 300 | 4 | | `EMERGENCY` | 600 | 0 |
|
|
103
|
+
|
|
104
|
+
Anywhere a level is accepted, `Level.parse` reads it: a `Level`, its value, an RFC 5424 severity,
|
|
105
|
+
or a name in any case (`"error"`). Anything else raises `InvalidLevelError`.
|
|
106
|
+
|
|
107
|
+
### Context
|
|
108
|
+
|
|
109
|
+
`Context` is `Mapping[str, object]` â the second argument to every method above. Values are
|
|
110
|
+
`object` because a caller may log anything; whatever writes the record normalises what it cannot
|
|
111
|
+
serialise rather than refusing it. An exception to report goes under `EXCEPTION_KEY`:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from xtr_logging_contracts import EXCEPTION_KEY
|
|
115
|
+
|
|
116
|
+
logger.error("payment failed", {"order": order.id, EXCEPTION_KEY: error})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## What is not here
|
|
120
|
+
|
|
121
|
+
Everything that *acts* on what was logged: `LogRecord`, `Logger`, `LoggerFactory`,
|
|
122
|
+
`LoggingConfig`, the seventeen handlers, the processors, the formatters, ambient
|
|
123
|
+
`bound_context()`, and the standard-library bridge. All of that is
|
|
124
|
+
[xtr-logging](https://github.com/xterr/python-xtr-logging).
|
|
125
|
+
|
|
126
|
+
`LogRecord` in particular belongs there, not here: it appears in no signature above. A library
|
|
127
|
+
that logs never builds one or sees one â it is made inside the logger and consumed by handlers
|
|
128
|
+
and processors, which are implementation.
|
|
129
|
+
|
|
130
|
+
`HandlerInterface`, `ProcessorInterface` and `FormatterInterface` are deliberately absent for the
|
|
131
|
+
same reason, plus one more: nothing outside `xtr-logging` implements them yet, and a contract
|
|
132
|
+
package earns its stability by staying small. They move here when a third-party handler needs
|
|
133
|
+
them â and `LogRecord` would move with them.
|
|
134
|
+
|
|
135
|
+
## Errors
|
|
136
|
+
|
|
137
|
+
| Error | Raised when |
|
|
138
|
+
| --- | --- |
|
|
139
|
+
| `LoggingError` | Never directly â the base every logging error derives from, `xtr-logging`'s included |
|
|
140
|
+
| `InvalidLevelError` | A value names no level (also a `ValueError`) |
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
Developed in the [python-xtr](https://github.com/xterr/python-xtr) monorepo, under
|
|
145
|
+
`packages/xtr-logging-contracts`; run the commands below from there. The `python-xtr-logging-contracts` repository is a
|
|
146
|
+
read-only copy, so send issues and pull requests to the monorepo.
|
|
147
|
+
|
|
148
|
+
```sh
|
|
149
|
+
uv sync
|
|
150
|
+
uv run ruff check . && uv run ruff format --check .
|
|
151
|
+
uv run basedpyright
|
|
152
|
+
uv run ty check
|
|
153
|
+
uv run pytest
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## License
|
|
157
|
+
|
|
158
|
+
MIT â see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xtr-logging-contracts"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "The logging contract: one interface, eight severities, and a logger that discards everything."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.11"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"logging",
|
|
11
|
+
"contracts",
|
|
12
|
+
"interface",
|
|
13
|
+
"protocol",
|
|
14
|
+
"structured-logging",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"Programming Language :: Python :: 3.14",
|
|
23
|
+
"Topic :: System :: Logging",
|
|
24
|
+
"Typing :: Typed",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["typing-extensions>=4.4"]
|
|
27
|
+
|
|
28
|
+
[[project.authors]]
|
|
29
|
+
name = "Razvan Ceana"
|
|
30
|
+
email = "razvan@ceana.ro"
|
|
31
|
+
|
|
32
|
+
[dependency-groups]
|
|
33
|
+
dev = [
|
|
34
|
+
"basedpyright>=1.21",
|
|
35
|
+
"ruff>=0.8",
|
|
36
|
+
"pytest>=8",
|
|
37
|
+
"pytest-cov>=5",
|
|
38
|
+
"ty>=0.0.83",
|
|
39
|
+
]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
43
|
+
build-backend = "uv_build"
|
|
44
|
+
|
|
45
|
+
[tool.basedpyright]
|
|
46
|
+
typeCheckingMode = "all"
|
|
47
|
+
pythonVersion = "3.11"
|
|
48
|
+
pythonPlatform = "All"
|
|
49
|
+
include = [
|
|
50
|
+
"src",
|
|
51
|
+
"tests",
|
|
52
|
+
]
|
|
53
|
+
exclude = [
|
|
54
|
+
"**/__pycache__",
|
|
55
|
+
"**/.venv",
|
|
56
|
+
"**/build",
|
|
57
|
+
"**/dist",
|
|
58
|
+
".tmp",
|
|
59
|
+
]
|
|
60
|
+
reportUnusedCallResult = "warning"
|
|
61
|
+
reportUnnecessaryTypeIgnoreComment = "error"
|
|
62
|
+
reportUnusedVariable = "error"
|
|
63
|
+
reportMissingParameterType = "error"
|
|
64
|
+
reportPrivateUsage = "error"
|
|
65
|
+
|
|
66
|
+
[tool.ruff]
|
|
67
|
+
target-version = "py311"
|
|
68
|
+
line-length = 100
|
|
69
|
+
src = [
|
|
70
|
+
"src",
|
|
71
|
+
"tests",
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
[tool.ruff.lint]
|
|
75
|
+
select = ["ALL"]
|
|
76
|
+
ignore = [
|
|
77
|
+
"COM812",
|
|
78
|
+
"ISC001",
|
|
79
|
+
"D203",
|
|
80
|
+
"D213",
|
|
81
|
+
"CPY001",
|
|
82
|
+
"FBT001",
|
|
83
|
+
"FBT002",
|
|
84
|
+
"TD002",
|
|
85
|
+
"TD003",
|
|
86
|
+
"FIX002",
|
|
87
|
+
"TRY003",
|
|
88
|
+
"EM101",
|
|
89
|
+
"EM102",
|
|
90
|
+
"PLE1205",
|
|
91
|
+
"PLE1206",
|
|
92
|
+
]
|
|
93
|
+
fixable = ["ALL"]
|
|
94
|
+
unfixable = []
|
|
95
|
+
|
|
96
|
+
[tool.ruff.lint.per-file-ignores]
|
|
97
|
+
"tests/**/*.py" = [
|
|
98
|
+
"S101",
|
|
99
|
+
"ARG",
|
|
100
|
+
"PLR2004",
|
|
101
|
+
"SLF001",
|
|
102
|
+
"D",
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
[tool.ruff.lint.pydocstyle]
|
|
106
|
+
convention = "google"
|
|
107
|
+
|
|
108
|
+
[tool.ruff.format]
|
|
109
|
+
quote-style = "double"
|
|
110
|
+
indent-style = "space"
|
|
111
|
+
docstring-code-format = true
|
|
112
|
+
docstring-code-line-length = "dynamic"
|
|
113
|
+
|
|
114
|
+
[tool.ty.src]
|
|
115
|
+
include = [
|
|
116
|
+
"src",
|
|
117
|
+
"tests",
|
|
118
|
+
]
|
|
119
|
+
|
|
120
|
+
[tool.pytest.ini_options]
|
|
121
|
+
minversion = "8.0"
|
|
122
|
+
testpaths = ["tests"]
|
|
123
|
+
addopts = [
|
|
124
|
+
"-ra",
|
|
125
|
+
"--strict-config",
|
|
126
|
+
"--strict-markers",
|
|
127
|
+
]
|
|
128
|
+
filterwarnings = ["error"]
|
|
129
|
+
|
|
130
|
+
[tool.coverage.run]
|
|
131
|
+
source = ["src"]
|
|
132
|
+
branch = true
|
|
133
|
+
|
|
134
|
+
[tool.coverage.report]
|
|
135
|
+
exclude_lines = [
|
|
136
|
+
"pragma: no cover",
|
|
137
|
+
"if TYPE_CHECKING:",
|
|
138
|
+
"raise NotImplementedError",
|
|
139
|
+
'^\s*\.\.\.$',
|
|
140
|
+
]
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "xtr-logging-contracts"
|
|
3
|
+
version = "1.0.0"
|
|
4
|
+
description = "The logging contract: one interface, eight severities, and a logger that discards everything."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "Razvan Ceana", email = "razvan@ceana.ro" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
keywords = ["logging", "contracts", "interface", "protocol", "structured-logging"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 3 - Alpha",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
"Programming Language :: Python :: 3.11",
|
|
17
|
+
"Programming Language :: Python :: 3.12",
|
|
18
|
+
"Programming Language :: Python :: 3.13",
|
|
19
|
+
"Programming Language :: Python :: 3.14",
|
|
20
|
+
"Topic :: System :: Logging",
|
|
21
|
+
"Typing :: Typed",
|
|
22
|
+
]
|
|
23
|
+
# One dependency, for `@override` on 3.11, where typing has none. A library
|
|
24
|
+
# that only logs should install nothing else.
|
|
25
|
+
dependencies = [
|
|
26
|
+
"typing-extensions>=4.4",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = [
|
|
31
|
+
"basedpyright>=1.21",
|
|
32
|
+
"ruff>=0.8",
|
|
33
|
+
"pytest>=8",
|
|
34
|
+
"pytest-cov>=5",
|
|
35
|
+
"ty>=0.0.83",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[build-system]
|
|
39
|
+
requires = ["uv_build>=0.9.18,<0.10.0"]
|
|
40
|
+
build-backend = "uv_build"
|
|
41
|
+
|
|
42
|
+
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
43
|
+
# basedpyright â typeCheckingMode = "all" sets every report flag to error
|
|
44
|
+
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
45
|
+
[tool.basedpyright]
|
|
46
|
+
typeCheckingMode = "all"
|
|
47
|
+
pythonVersion = "3.11"
|
|
48
|
+
pythonPlatform = "All"
|
|
49
|
+
include = ["src", "tests"]
|
|
50
|
+
exclude = ["**/__pycache__", "**/.venv", "**/build", "**/dist", ".tmp"]
|
|
51
|
+
|
|
52
|
+
reportUnusedCallResult = "warning"
|
|
53
|
+
reportUnnecessaryTypeIgnoreComment = "error"
|
|
54
|
+
reportUnusedVariable = "error"
|
|
55
|
+
reportMissingParameterType = "error"
|
|
56
|
+
reportPrivateUsage = "error"
|
|
57
|
+
|
|
58
|
+
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
59
|
+
# ruff â select = ["ALL"], minimal justified ignores
|
|
60
|
+
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
target-version = "py311"
|
|
63
|
+
line-length = 100
|
|
64
|
+
src = ["src", "tests"]
|
|
65
|
+
|
|
66
|
+
[tool.ruff.lint]
|
|
67
|
+
select = ["ALL"]
|
|
68
|
+
ignore = [
|
|
69
|
+
# Formatter conflicts (ruff documents these)
|
|
70
|
+
"COM812",
|
|
71
|
+
"ISC001",
|
|
72
|
+
# Mutually-exclusive docstring conventions; keep the modern one
|
|
73
|
+
"D203",
|
|
74
|
+
"D213",
|
|
75
|
+
# Project-specific noise
|
|
76
|
+
"CPY001",
|
|
77
|
+
"FBT001",
|
|
78
|
+
"FBT002",
|
|
79
|
+
"TD002",
|
|
80
|
+
"TD003",
|
|
81
|
+
"FIX002",
|
|
82
|
+
# Exceptions compose their own messages from typed fields; the string at a
|
|
83
|
+
# raise site is a data field. See xtr_logging_contracts/exception/.
|
|
84
|
+
"TRY003",
|
|
85
|
+
"EM101",
|
|
86
|
+
"EM102",
|
|
87
|
+
# A context mapping after the message is this interface's signature, not a stray
|
|
88
|
+
# %-format argument: these rules assume every `logger.info` is stdlib's.
|
|
89
|
+
"PLE1205",
|
|
90
|
+
"PLE1206",
|
|
91
|
+
]
|
|
92
|
+
fixable = ["ALL"]
|
|
93
|
+
unfixable = []
|
|
94
|
+
|
|
95
|
+
[tool.ruff.lint.per-file-ignores]
|
|
96
|
+
"tests/**/*.py" = [
|
|
97
|
+
"S101", # assert is the point of pytest
|
|
98
|
+
"ARG", # fixtures look unused
|
|
99
|
+
"PLR2004", # magic numbers in test data
|
|
100
|
+
"SLF001", # tests reach into privates
|
|
101
|
+
"D", # test names are the docs
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
[tool.ruff.lint.pydocstyle]
|
|
105
|
+
convention = "google"
|
|
106
|
+
|
|
107
|
+
[tool.ruff.format]
|
|
108
|
+
quote-style = "double"
|
|
109
|
+
indent-style = "space"
|
|
110
|
+
docstring-code-format = true
|
|
111
|
+
docstring-code-line-length = "dynamic"
|
|
112
|
+
|
|
113
|
+
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
114
|
+
# ty â a second opinion on the source
|
|
115
|
+
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
116
|
+
[tool.ty.src]
|
|
117
|
+
include = ["src", "tests"]
|
|
118
|
+
|
|
119
|
+
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
120
|
+
# pytest
|
|
121
|
+
# âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ
|
|
122
|
+
[tool.pytest.ini_options]
|
|
123
|
+
minversion = "8.0"
|
|
124
|
+
testpaths = ["tests"]
|
|
125
|
+
addopts = ["-ra", "--strict-config", "--strict-markers"]
|
|
126
|
+
filterwarnings = ["error"]
|
|
127
|
+
|
|
128
|
+
[tool.coverage.run]
|
|
129
|
+
source = ["src"]
|
|
130
|
+
branch = true
|
|
131
|
+
|
|
132
|
+
[tool.coverage.report]
|
|
133
|
+
exclude_lines = [
|
|
134
|
+
"pragma: no cover",
|
|
135
|
+
"if TYPE_CHECKING:",
|
|
136
|
+
"raise NotImplementedError",
|
|
137
|
+
# A Protocol body states a contract; there is nothing there to run.
|
|
138
|
+
"^\\s*\\.\\.\\.$",
|
|
139
|
+
]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"""The logging contract: what code that logs depends on, and nothing more.
|
|
2
|
+
|
|
3
|
+
A library that logs should not decide where records go. It takes a
|
|
4
|
+
:class:`LoggerInterface`, defaults to a :class:`NullLogger`, and leaves
|
|
5
|
+
handlers, formatters and configuration to the application that wires it â so
|
|
6
|
+
depending on this package costs a library nothing but the contract.
|
|
7
|
+
|
|
8
|
+
What is here is only what appears in that contract: the interface itself, the
|
|
9
|
+
:class:`Context` and :class:`Level` its methods accept, and the two ways of
|
|
10
|
+
satisfying it â :class:`AbstractLogger` to implement it in one method, and
|
|
11
|
+
:class:`NullLogger` to implement it in none. Everything that *acts* on what was
|
|
12
|
+
logged â records, handlers, processors, formatters, configuration â is
|
|
13
|
+
``xtr-logging``, which implements this contract and re-exports it, so the two
|
|
14
|
+
are never two different objects.
|
|
15
|
+
|
|
16
|
+
from xtr_logging_contracts import LoggerInterface, NullLogger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Checkout:
|
|
20
|
+
def __init__(self, logger: LoggerInterface | None = None) -> None:
|
|
21
|
+
self._logger = logger or NullLogger()
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
25
|
+
|
|
26
|
+
from .abstract_logger import AbstractLogger
|
|
27
|
+
from .context import EXCEPTION_KEY, Context
|
|
28
|
+
from .exception import InvalidLevelError, LoggingError
|
|
29
|
+
from .level import Level, LevelLike
|
|
30
|
+
from .logger_aware import LoggerAware
|
|
31
|
+
from .logger_aware_interface import LoggerAwareInterface
|
|
32
|
+
from .logger_interface import LoggerInterface
|
|
33
|
+
from .null_logger import NullLogger
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
__version__ = version("xtr-logging-contracts")
|
|
37
|
+
except PackageNotFoundError: # pragma: no cover
|
|
38
|
+
# Running from a source tree or a vendored copy, with no installed
|
|
39
|
+
# metadata to read. Having no version is better than refusing to import.
|
|
40
|
+
__version__ = "0+unknown"
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"EXCEPTION_KEY",
|
|
44
|
+
"AbstractLogger",
|
|
45
|
+
"Context",
|
|
46
|
+
"InvalidLevelError",
|
|
47
|
+
"Level",
|
|
48
|
+
"LevelLike",
|
|
49
|
+
"LoggerAware",
|
|
50
|
+
"LoggerAwareInterface",
|
|
51
|
+
"LoggerInterface",
|
|
52
|
+
"LoggingError",
|
|
53
|
+
"NullLogger",
|
|
54
|
+
"__version__",
|
|
55
|
+
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""A logger whose eight severity methods all funnel into ``log``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from typing_extensions import override
|
|
9
|
+
|
|
10
|
+
from .level import Level
|
|
11
|
+
from .logger_interface import LoggerInterface
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .context import Context
|
|
15
|
+
from .level import LevelLike
|
|
16
|
+
|
|
17
|
+
__all__ = ["AbstractLogger"]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AbstractLogger(LoggerInterface, ABC):
|
|
21
|
+
"""Implements every severity method in terms of :meth:`log`.
|
|
22
|
+
|
|
23
|
+
Subclass it and write ``log`` alone.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
@abstractmethod
|
|
27
|
+
@override
|
|
28
|
+
def log(self, level: LevelLike, message: str, /, context: Context | None = None) -> None:
|
|
29
|
+
"""Log ``message`` at ``level``."""
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
def emergency(self, message: str, /, context: Context | None = None) -> None:
|
|
33
|
+
"""Log at :attr:`Level.EMERGENCY`."""
|
|
34
|
+
self.log(Level.EMERGENCY, message, context)
|
|
35
|
+
|
|
36
|
+
@override
|
|
37
|
+
def alert(self, message: str, /, context: Context | None = None) -> None:
|
|
38
|
+
"""Log at :attr:`Level.ALERT`."""
|
|
39
|
+
self.log(Level.ALERT, message, context)
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
def critical(self, message: str, /, context: Context | None = None) -> None:
|
|
43
|
+
"""Log at :attr:`Level.CRITICAL`."""
|
|
44
|
+
self.log(Level.CRITICAL, message, context)
|
|
45
|
+
|
|
46
|
+
@override
|
|
47
|
+
def error(self, message: str, /, context: Context | None = None) -> None:
|
|
48
|
+
"""Log at :attr:`Level.ERROR`."""
|
|
49
|
+
self.log(Level.ERROR, message, context)
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
def warning(self, message: str, /, context: Context | None = None) -> None:
|
|
53
|
+
"""Log at :attr:`Level.WARNING`."""
|
|
54
|
+
self.log(Level.WARNING, message, context)
|
|
55
|
+
|
|
56
|
+
@override
|
|
57
|
+
def notice(self, message: str, /, context: Context | None = None) -> None:
|
|
58
|
+
"""Log at :attr:`Level.NOTICE`."""
|
|
59
|
+
self.log(Level.NOTICE, message, context)
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
def info(self, message: str, /, context: Context | None = None) -> None:
|
|
63
|
+
"""Log at :attr:`Level.INFO`."""
|
|
64
|
+
self.log(Level.INFO, message, context)
|
|
65
|
+
|
|
66
|
+
@override
|
|
67
|
+
def debug(self, message: str, /, context: Context | None = None) -> None:
|
|
68
|
+
"""Log at :attr:`Level.DEBUG`."""
|
|
69
|
+
self.log(Level.DEBUG, message, context)
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""The structured data a caller attaches to what it logs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from typing import Final, TypeAlias
|
|
7
|
+
|
|
8
|
+
__all__ = ["EXCEPTION_KEY", "Context"]
|
|
9
|
+
|
|
10
|
+
Context: TypeAlias = Mapping[str, object]
|
|
11
|
+
"""Structured data attached to what is logged.
|
|
12
|
+
|
|
13
|
+
Values are ``object`` because a caller may log anything; an implementation
|
|
14
|
+
normalises what it cannot serialise rather than refusing it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
EXCEPTION_KEY: Final = "exception"
|
|
18
|
+
"""The context key reserved for an exception to report.
|
|
19
|
+
|
|
20
|
+
Part of the contract rather than of any implementation: a library puts the
|
|
21
|
+
exception it is reporting here, and whatever writes the record knows to look
|
|
22
|
+
for it.
|
|
23
|
+
"""
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""The errors the contract itself can raise.
|
|
2
|
+
|
|
3
|
+
Only two live here, because only two can be raised by the contract rather than
|
|
4
|
+
by an implementation: :class:`LoggingError`, the root every logging error
|
|
5
|
+
derives from â including every one ``xtr-logging`` adds â and
|
|
6
|
+
:class:`InvalidLevelError`, which :meth:`Level.parse` raises. Catching
|
|
7
|
+
:class:`LoggingError` therefore catches anything logging can go wrong with,
|
|
8
|
+
whichever package raised it.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from .invalid_level_error import InvalidLevelError
|
|
12
|
+
from .logging_error import LoggingError
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"InvalidLevelError",
|
|
16
|
+
"LoggingError",
|
|
17
|
+
]
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""A value names no log level."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from .logging_error import LoggingError
|
|
6
|
+
|
|
7
|
+
__all__ = ["InvalidLevelError"]
|
|
8
|
+
|
|
9
|
+
_ACCEPTED = "a Level, a level name, a value from 100 to 600, or an RFC 5424 severity from 0 to 7"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class InvalidLevelError(LoggingError, ValueError):
|
|
13
|
+
"""A value names no log level.
|
|
14
|
+
|
|
15
|
+
Also a :class:`ValueError`, which is what lets a configuration parser
|
|
16
|
+
report the field that held it.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
value: int | str
|
|
20
|
+
|
|
21
|
+
def __init__(self, value: int | str) -> None:
|
|
22
|
+
"""Record the value that could not be read as a level."""
|
|
23
|
+
self.value = value
|
|
24
|
+
super().__init__(
|
|
25
|
+
f"{value!r} is not a log level; expected {_ACCEPTED}",
|
|
26
|
+
)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""The root every error in this library derives from."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
__all__ = ["LoggingError"]
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LoggingError(Exception):
|
|
9
|
+
"""Base class for every error raised by this library.
|
|
10
|
+
|
|
11
|
+
Catch this to handle anything logging can go wrong with; catch a subclass
|
|
12
|
+
to handle one cause. Every subclass carries the data a caller needs as
|
|
13
|
+
typed attributes and composes its own message from them.
|
|
14
|
+
"""
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"""The eight RFC 5424 severities, valued 100 to 600.
|
|
2
|
+
|
|
3
|
+
The values leave room between them, so a level added later still sorts where
|
|
4
|
+
it belongs, and they compare as integers: ``Level.ERROR > Level.WARNING``.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from enum import IntEnum
|
|
10
|
+
from typing import TYPE_CHECKING, Final, TypeAlias
|
|
11
|
+
|
|
12
|
+
from .exception.invalid_level_error import InvalidLevelError
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Mapping
|
|
16
|
+
|
|
17
|
+
__all__ = ["Level", "LevelLike"]
|
|
18
|
+
|
|
19
|
+
_LOWEST_RFC5424: Final = 0
|
|
20
|
+
_HIGHEST_RFC5424: Final = 7
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Level(IntEnum):
|
|
24
|
+
"""A log level, ordered from least to most severe."""
|
|
25
|
+
|
|
26
|
+
DEBUG = 100
|
|
27
|
+
INFO = 200
|
|
28
|
+
NOTICE = 250
|
|
29
|
+
WARNING = 300
|
|
30
|
+
ERROR = 400
|
|
31
|
+
CRITICAL = 500
|
|
32
|
+
ALERT = 550
|
|
33
|
+
EMERGENCY = 600
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def parse(cls, value: LevelLike) -> Level:
|
|
37
|
+
"""Read any accepted spelling of a level.
|
|
38
|
+
|
|
39
|
+
Accepts a :class:`Level`, its value (``400``), an RFC 5424 severity
|
|
40
|
+
(``3``), or a name in any case (``"error"``, ``"ERROR"``).
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
InvalidLevelError: If ``value`` names no level.
|
|
44
|
+
"""
|
|
45
|
+
match value:
|
|
46
|
+
case Level():
|
|
47
|
+
return value
|
|
48
|
+
case bool():
|
|
49
|
+
raise InvalidLevelError(value)
|
|
50
|
+
case int():
|
|
51
|
+
if _LOWEST_RFC5424 <= value <= _HIGHEST_RFC5424:
|
|
52
|
+
return _BY_RFC5424[value]
|
|
53
|
+
if value in _BY_VALUE:
|
|
54
|
+
return _BY_VALUE[value]
|
|
55
|
+
raise InvalidLevelError(value)
|
|
56
|
+
case str():
|
|
57
|
+
return cls.from_name(value)
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def from_name(cls, name: str) -> Level:
|
|
61
|
+
"""Return the level called ``name``, in any case.
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
InvalidLevelError: If no level has that name.
|
|
65
|
+
"""
|
|
66
|
+
found = cls.__members__.get(name.upper())
|
|
67
|
+
if found is None:
|
|
68
|
+
raise InvalidLevelError(name)
|
|
69
|
+
return found
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def lower_name(self) -> str:
|
|
73
|
+
"""The name of this level in lower case, as configuration writes it."""
|
|
74
|
+
return self.name.lower()
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def rfc5424(self) -> int:
|
|
78
|
+
"""The RFC 5424 severity: 0 for EMERGENCY through 7 for DEBUG."""
|
|
79
|
+
return _TO_RFC5424[self]
|
|
80
|
+
|
|
81
|
+
def includes(self, other: Level) -> bool:
|
|
82
|
+
"""Whether a threshold of this level lets ``other`` through."""
|
|
83
|
+
return self <= other
|
|
84
|
+
|
|
85
|
+
def is_higher_than(self, other: Level) -> bool:
|
|
86
|
+
"""Whether this level is strictly more severe than ``other``."""
|
|
87
|
+
return self > other
|
|
88
|
+
|
|
89
|
+
def is_lower_than(self, other: Level) -> bool:
|
|
90
|
+
"""Whether this level is strictly less severe than ``other``."""
|
|
91
|
+
return self < other
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
LevelLike: TypeAlias = Level | int | str
|
|
95
|
+
"""Anything :meth:`Level.parse` accepts."""
|
|
96
|
+
|
|
97
|
+
_TO_RFC5424: Final[Mapping[Level, int]] = {
|
|
98
|
+
Level.EMERGENCY: 0,
|
|
99
|
+
Level.ALERT: 1,
|
|
100
|
+
Level.CRITICAL: 2,
|
|
101
|
+
Level.ERROR: 3,
|
|
102
|
+
Level.WARNING: 4,
|
|
103
|
+
Level.NOTICE: 5,
|
|
104
|
+
Level.INFO: 6,
|
|
105
|
+
Level.DEBUG: 7,
|
|
106
|
+
}
|
|
107
|
+
_BY_RFC5424: Final[Mapping[int, Level]] = {rfc: level for level, rfc in _TO_RFC5424.items()}
|
|
108
|
+
_BY_VALUE: Final[Mapping[int, Level]] = {level.value: level for level in Level}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""A mixin giving a class a logger that is never missing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
6
|
+
|
|
7
|
+
from typing_extensions import override
|
|
8
|
+
|
|
9
|
+
from .logger_aware_interface import LoggerAwareInterface
|
|
10
|
+
from .null_logger import NullLogger
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .logger_interface import LoggerInterface
|
|
14
|
+
|
|
15
|
+
__all__ = ["LoggerAware"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoggerAware(LoggerAwareInterface):
|
|
19
|
+
"""Holds a logger, defaulting to a :class:`NullLogger`.
|
|
20
|
+
|
|
21
|
+
Code in the class logs through :attr:`logger` without checking whether one
|
|
22
|
+
was ever set.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
_default: ClassVar[LoggerInterface] = NullLogger()
|
|
26
|
+
_logger: LoggerInterface | None = None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def logger(self) -> LoggerInterface:
|
|
30
|
+
"""The logger set last, or one that discards everything."""
|
|
31
|
+
return self._logger if self._logger is not None else self._default
|
|
32
|
+
|
|
33
|
+
@override
|
|
34
|
+
def set_logger(self, logger: LoggerInterface, /) -> None:
|
|
35
|
+
"""Log through ``logger`` from now on."""
|
|
36
|
+
self._logger = logger
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Something that can be handed a logger after it is built."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .logger_interface import LoggerInterface
|
|
9
|
+
|
|
10
|
+
__all__ = ["LoggerAwareInterface"]
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@runtime_checkable
|
|
14
|
+
class LoggerAwareInterface(Protocol):
|
|
15
|
+
"""Accepts a logger through a setter.
|
|
16
|
+
|
|
17
|
+
Prefer a constructor argument. This exists for objects a framework builds
|
|
18
|
+
for you, where the constructor is not yours to extend.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
def set_logger(self, logger: LoggerInterface, /) -> None:
|
|
22
|
+
"""Log through ``logger`` from now on."""
|
|
23
|
+
...
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""The logging surface code writes through."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, Protocol, runtime_checkable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from .context import Context
|
|
9
|
+
from .level import LevelLike
|
|
10
|
+
|
|
11
|
+
__all__ = ["LoggerInterface"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@runtime_checkable
|
|
15
|
+
class LoggerInterface(Protocol):
|
|
16
|
+
"""Writes messages at one of eight severities, with structured context.
|
|
17
|
+
|
|
18
|
+
The only type code that logs should depend on. Whatever implements it â
|
|
19
|
+
a channel of ``xtr-logging``'s ``Logger``, a
|
|
20
|
+
:class:`~xtr_logging_contracts.null_logger.NullLogger`, an adapter over a
|
|
21
|
+
standard library logger â can be swapped without touching a call site.
|
|
22
|
+
|
|
23
|
+
The rules:
|
|
24
|
+
|
|
25
|
+
- ``message`` may hold ``{key}`` placeholders naming entries of
|
|
26
|
+
``context``. The logger does not substitute them; something later in the
|
|
27
|
+
pipeline does â ``xtr-logging``'s ``PlaceholderProcessor`` â so a handler
|
|
28
|
+
may still see the template and the values apart.
|
|
29
|
+
- An exception to report goes under ``context["exception"]``.
|
|
30
|
+
- Context is data, never code: an implementation must not fail because of
|
|
31
|
+
what it holds.
|
|
32
|
+
|
|
33
|
+
``message`` is positional-only, so an implementation may name it freely;
|
|
34
|
+
``context`` may also be passed by keyword.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def emergency(self, message: str, /, context: Context | None = None) -> None:
|
|
38
|
+
"""The system is unusable."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
def alert(self, message: str, /, context: Context | None = None) -> None:
|
|
42
|
+
"""Action must be taken immediately â the whole site is down, say."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
def critical(self, message: str, /, context: Context | None = None) -> None:
|
|
46
|
+
"""A critical condition, such as an unavailable component."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
def error(self, message: str, /, context: Context | None = None) -> None:
|
|
50
|
+
"""A runtime error that needs no immediate action but must be seen."""
|
|
51
|
+
...
|
|
52
|
+
|
|
53
|
+
def warning(self, message: str, /, context: Context | None = None) -> None:
|
|
54
|
+
"""Something exceptional that is not an error, like a deprecated call."""
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
def notice(self, message: str, /, context: Context | None = None) -> None:
|
|
58
|
+
"""A normal but significant event."""
|
|
59
|
+
...
|
|
60
|
+
|
|
61
|
+
def info(self, message: str, /, context: Context | None = None) -> None:
|
|
62
|
+
"""An interesting event, such as a user logging in."""
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
def debug(self, message: str, /, context: Context | None = None) -> None:
|
|
66
|
+
"""Detailed information for debugging."""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
def log(self, level: LevelLike, message: str, /, context: Context | None = None) -> None:
|
|
70
|
+
"""Log at ``level``, given in any form :meth:`Level.parse` accepts.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
InvalidLevelError: If ``level`` names no level.
|
|
74
|
+
"""
|
|
75
|
+
...
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""A logger that discards everything."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING, final
|
|
6
|
+
|
|
7
|
+
from typing_extensions import override
|
|
8
|
+
|
|
9
|
+
from .abstract_logger import AbstractLogger
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .context import Context
|
|
13
|
+
from .level import LevelLike
|
|
14
|
+
|
|
15
|
+
__all__ = ["NullLogger"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@final
|
|
19
|
+
class NullLogger(AbstractLogger):
|
|
20
|
+
"""Discards everything it is given.
|
|
21
|
+
|
|
22
|
+
The default for a library that accepts a logger: logging stays optional
|
|
23
|
+
for its callers, and the library's code never has to ask whether it has
|
|
24
|
+
one.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
__slots__ = ()
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
def log(self, level: LevelLike, message: str, /, context: Context | None = None) -> None:
|
|
31
|
+
"""Do nothing â not even check ``level``, which costs a call site nothing."""
|
|
32
|
+
del level, message, context
|
|
File without changes
|