logging-objects-with-schema 0.4.0__tar.gz → 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.
- {logging_objects_with_schema-0.4.0 → logging_objects_with_schema-1.0.0}/PKG-INFO +11 -5
- {logging_objects_with_schema-0.4.0 → logging_objects_with_schema-1.0.0}/README.md +8 -1
- {logging_objects_with_schema-0.4.0 → logging_objects_with_schema-1.0.0}/pyproject.toml +3 -3
- {logging_objects_with_schema-0.4.0 → logging_objects_with_schema-1.0.0}/src/logging_objects_with_schema/__init__.py +0 -2
- {logging_objects_with_schema-0.4.0 → logging_objects_with_schema-1.0.0}/src/logging_objects_with_schema/errors.py +9 -12
- {logging_objects_with_schema-0.4.0 → logging_objects_with_schema-1.0.0}/src/logging_objects_with_schema/schema_applier.py +28 -83
- {logging_objects_with_schema-0.4.0 → logging_objects_with_schema-1.0.0}/src/logging_objects_with_schema/schema_loader.py +52 -168
- logging_objects_with_schema-1.0.0/src/logging_objects_with_schema/schema_logger.py +191 -0
- logging_objects_with_schema-0.4.0/src/logging_objects_with_schema/schema_logger.py +0 -296
- {logging_objects_with_schema-0.4.0 → logging_objects_with_schema-1.0.0}/LICENSE +0 -0
- {logging_objects_with_schema-0.4.0 → logging_objects_with_schema-1.0.0}/src/logging_objects_with_schema/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logging-objects-with-schema
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.0
|
|
4
4
|
Summary: Proxy logging wrapper that validates extra fields against a JSON schema.
|
|
5
5
|
Keywords: logging,schema,validation,structured-logging,structured-logs
|
|
6
6
|
Author: Dmitrii Safronov
|
|
@@ -8,7 +8,6 @@ Author-email: Dmitrii Safronov <dhameoelin+logging-objects-with-schema@gmail.com
|
|
|
8
8
|
License-Expression: Apache-2.0
|
|
9
9
|
License-File: LICENSE
|
|
10
10
|
Classifier: Programming Language :: Python :: 3
|
|
11
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
12
11
|
Classifier: Programming Language :: Python :: 3.11
|
|
13
12
|
Classifier: Programming Language :: Python :: 3.12
|
|
14
13
|
Classifier: Programming Language :: Python :: 3.13
|
|
@@ -17,9 +16,9 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
17
16
|
Classifier: Intended Audience :: Developers
|
|
18
17
|
Classifier: Topic :: System :: Logging
|
|
19
18
|
Classifier: Typing :: Typed
|
|
20
|
-
Requires-Python: >=3.
|
|
21
|
-
Project-URL: Issues, https://github.com/disafronov/python-logging-objects-with-schema/issues
|
|
19
|
+
Requires-Python: >=3.11
|
|
22
20
|
Project-URL: Source, https://github.com/disafronov/python-logging-objects-with-schema
|
|
21
|
+
Project-URL: Issues, https://github.com/disafronov/python-logging-objects-with-schema/issues
|
|
23
22
|
Description-Content-Type: text/markdown
|
|
24
23
|
|
|
25
24
|
# python-logging-objects-with-schema
|
|
@@ -52,6 +51,9 @@ systems) can rely on a consistent log structure.
|
|
|
52
51
|
problem.
|
|
53
52
|
- Any mismatch between runtime values and the declared types (wrong types,
|
|
54
53
|
`None` values, disallowed list elements) is also treated as a data error.
|
|
54
|
+
Type checks use `type()`, not `isinstance()`: `bool` is **not** accepted
|
|
55
|
+
where `int` is declared, and `int` is **not** accepted where `float` is
|
|
56
|
+
declared. There are no implicit type conversions.
|
|
55
57
|
- All validation problems are aggregated and logged as a single ERROR message
|
|
56
58
|
**after** the log record has been emitted, ensuring 100% compatibility with
|
|
57
59
|
standard logger behavior (no exceptions are raised).
|
|
@@ -169,7 +171,9 @@ directory until it finds the file or reaches the filesystem root.
|
|
|
169
171
|
**Important**: If there are any problems with the schema (missing file, broken
|
|
170
172
|
JSON, invalid structure, etc.), the application is terminated after logging
|
|
171
173
|
schema problems to stderr. Schema validation happens when the first logger
|
|
172
|
-
instance is created.
|
|
174
|
+
instance is created. Termination uses `os._exit(1)` — bypassing `atexit`
|
|
175
|
+
handlers and `finally` blocks — to prevent any cleanup code from running
|
|
176
|
+
against a broken logger that was never fully registered.
|
|
173
177
|
|
|
174
178
|
**Note**: The schema is compiled once per process and cached. Schema changes
|
|
175
179
|
require an application restart to take effect. The library is thread-safe.
|
|
@@ -365,3 +369,5 @@ All keys are merged together - they are not replaced, only supplemented.
|
|
|
365
369
|
keys are used, maintaining 100% backward compatibility
|
|
366
370
|
- `None` and empty `set()` are semantically equivalent for `forbidden_keys` -
|
|
367
371
|
both mean "no additional forbidden keys" and produce the same result
|
|
372
|
+
|
|
373
|
+
<!--placeholder -->
|
|
@@ -28,6 +28,9 @@ systems) can rely on a consistent log structure.
|
|
|
28
28
|
problem.
|
|
29
29
|
- Any mismatch between runtime values and the declared types (wrong types,
|
|
30
30
|
`None` values, disallowed list elements) is also treated as a data error.
|
|
31
|
+
Type checks use `type()`, not `isinstance()`: `bool` is **not** accepted
|
|
32
|
+
where `int` is declared, and `int` is **not** accepted where `float` is
|
|
33
|
+
declared. There are no implicit type conversions.
|
|
31
34
|
- All validation problems are aggregated and logged as a single ERROR message
|
|
32
35
|
**after** the log record has been emitted, ensuring 100% compatibility with
|
|
33
36
|
standard logger behavior (no exceptions are raised).
|
|
@@ -145,7 +148,9 @@ directory until it finds the file or reaches the filesystem root.
|
|
|
145
148
|
**Important**: If there are any problems with the schema (missing file, broken
|
|
146
149
|
JSON, invalid structure, etc.), the application is terminated after logging
|
|
147
150
|
schema problems to stderr. Schema validation happens when the first logger
|
|
148
|
-
instance is created.
|
|
151
|
+
instance is created. Termination uses `os._exit(1)` — bypassing `atexit`
|
|
152
|
+
handlers and `finally` blocks — to prevent any cleanup code from running
|
|
153
|
+
against a broken logger that was never fully registered.
|
|
149
154
|
|
|
150
155
|
**Note**: The schema is compiled once per process and cached. Schema changes
|
|
151
156
|
require an application restart to take effect. The library is thread-safe.
|
|
@@ -341,3 +346,5 @@ All keys are merged together - they are not replaced, only supplemented.
|
|
|
341
346
|
keys are used, maintaining 100% backward compatibility
|
|
342
347
|
- `None` and empty `set()` are semantically equivalent for `forbidden_keys` -
|
|
343
348
|
both mean "no additional forbidden keys" and produce the same result
|
|
349
|
+
|
|
350
|
+
<!--placeholder -->
|
|
@@ -4,10 +4,10 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "logging-objects-with-schema"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "1.0.0"
|
|
8
8
|
description = "Proxy logging wrapper that validates extra fields against a JSON schema."
|
|
9
9
|
readme = "README.md"
|
|
10
|
-
requires-python = ">=3.
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
11
|
dependencies = [ ]
|
|
12
12
|
license = "Apache-2.0"
|
|
13
13
|
license-files = [ "LICENSE" ]
|
|
@@ -20,7 +20,6 @@ keywords = [
|
|
|
20
20
|
]
|
|
21
21
|
classifiers = [
|
|
22
22
|
"Programming Language :: Python :: 3",
|
|
23
|
-
"Programming Language :: Python :: 3.10",
|
|
24
23
|
"Programming Language :: Python :: 3.11",
|
|
25
24
|
"Programming Language :: Python :: 3.12",
|
|
26
25
|
"Programming Language :: Python :: 3.13",
|
|
@@ -102,6 +101,7 @@ source = [ "src" ]
|
|
|
102
101
|
branch = true
|
|
103
102
|
|
|
104
103
|
[tool.coverage.report]
|
|
104
|
+
fail_under = 100
|
|
105
105
|
exclude_lines = [
|
|
106
106
|
"def __repr__",
|
|
107
107
|
"if self\\.debug",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Custom exception types used by logging_objects_with_schema."""
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import json
|
|
5
4
|
from dataclasses import dataclass
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
@dataclass
|
|
@@ -60,15 +60,12 @@ class _DataProblem:
|
|
|
60
60
|
- Redundant fields (fields not defined in the schema)
|
|
61
61
|
|
|
62
62
|
Attributes:
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
``{"field": "...", "error": "...", "value": "..."}``
|
|
66
|
-
All values are serialized via ``repr()`` for safety and consistency.
|
|
67
|
-
Examples:
|
|
68
|
-
- ``{"field": "'user_id'", "error": "'has type str, expected int'", "value": "'abc-123'"}``
|
|
69
|
-
- ``{"field": "'request_id'", "error": "'is None'", "value": "None"}``
|
|
70
|
-
- ``{"field": "'tags'", "error": "'is a list but contains elements with types dict; expected all elements to be of type str'", "value": "[{'key': 'color'}]"}`` # noqa: E501
|
|
71
|
-
- ``{"field": "'unknown_field'", "error": "'is not defined in schema'", "value": "'some_value'"}``
|
|
63
|
+
data: Dict containing structured error information with keys
|
|
64
|
+
``field``, ``error``, and ``value`` (all via ``repr()``).
|
|
72
65
|
"""
|
|
73
66
|
|
|
74
|
-
|
|
67
|
+
data: dict[str, Any]
|
|
68
|
+
|
|
69
|
+
@property
|
|
70
|
+
def message(self) -> str:
|
|
71
|
+
return json.dumps(self.data)
|
|
@@ -4,10 +4,6 @@ This module provides the core function for applying a compiled schema
|
|
|
4
4
|
to user-provided extra fields, used by SchemaLogger.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
from collections import defaultdict
|
|
11
7
|
from collections.abc import Mapping, MutableMapping
|
|
12
8
|
from typing import Any
|
|
13
9
|
|
|
@@ -15,14 +11,11 @@ from .errors import _DataProblem
|
|
|
15
11
|
from .schema_loader import _CompiledSchema, _SchemaLeaf
|
|
16
12
|
|
|
17
13
|
|
|
18
|
-
def
|
|
19
|
-
"""Create
|
|
14
|
+
def _create_validation_error_dict(field: str, error: str, value: Any) -> dict[str, Any]:
|
|
15
|
+
"""Create a dict for a single validation error.
|
|
20
16
|
|
|
21
|
-
All values are wrapped in repr()
|
|
22
|
-
|
|
23
|
-
- The error message always contains a valid Python representation of the value
|
|
24
|
-
- Security: prevents issues with special characters or control sequences
|
|
25
|
-
- Consistency: all error messages have the same format regardless of value type
|
|
17
|
+
All values are wrapped in repr() to ensure any type is representable
|
|
18
|
+
and special characters cannot cause issues downstream.
|
|
26
19
|
|
|
27
20
|
Args:
|
|
28
21
|
field: Field name that caused the validation error.
|
|
@@ -30,15 +23,13 @@ def _create_validation_error_json(field: str, error: str, value: Any) -> str:
|
|
|
30
23
|
value: Invalid value that caused the error.
|
|
31
24
|
|
|
32
25
|
Returns:
|
|
33
|
-
|
|
26
|
+
Dict with field, error, and value (all via repr() for safety).
|
|
34
27
|
"""
|
|
35
|
-
return
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
)
|
|
28
|
+
return {
|
|
29
|
+
"field": repr(field),
|
|
30
|
+
"error": repr(error),
|
|
31
|
+
"value": repr(value),
|
|
32
|
+
}
|
|
42
33
|
|
|
43
34
|
|
|
44
35
|
def _validate_list_value(
|
|
@@ -62,16 +53,12 @@ def _validate_list_value(
|
|
|
62
53
|
"""
|
|
63
54
|
if item_expected_type is None:
|
|
64
55
|
error_msg = "is a list but has no item type configured"
|
|
65
|
-
return _DataProblem(
|
|
56
|
+
return _DataProblem(_create_validation_error_dict(source, error_msg, value))
|
|
66
57
|
|
|
67
58
|
if len(value) == 0:
|
|
68
|
-
# Empty lists are always valid
|
|
69
59
|
return None
|
|
70
60
|
|
|
71
|
-
#
|
|
72
|
-
# We use a set comprehension to get unique type names (not the types themselves)
|
|
73
|
-
# for the error message. This gives a clear, readable error message showing
|
|
74
|
-
# which types were found (e.g., "int, str") vs what was expected.
|
|
61
|
+
# __name__ gives "int, str" in error messages instead of "<class 'int'>".
|
|
75
62
|
invalid_item_types = {
|
|
76
63
|
type(item).__name__ for item in value if type(item) is not item_expected_type
|
|
77
64
|
}
|
|
@@ -83,7 +70,7 @@ def _validate_list_value(
|
|
|
83
70
|
f"expected all elements to be of type "
|
|
84
71
|
f"{item_expected_type.__name__}"
|
|
85
72
|
)
|
|
86
|
-
return _DataProblem(
|
|
73
|
+
return _DataProblem(_create_validation_error_dict(source, error_msg, value))
|
|
87
74
|
|
|
88
75
|
return None
|
|
89
76
|
|
|
@@ -104,22 +91,14 @@ def _set_nested_value(
|
|
|
104
91
|
value: The value to set at the target location.
|
|
105
92
|
"""
|
|
106
93
|
current = target
|
|
107
|
-
# Navigate through intermediate dictionaries, creating them as needed.
|
|
108
|
-
# We iterate through all keys except the last one (path[:-1]) to build
|
|
109
|
-
# the nested structure.
|
|
110
94
|
for key in path[:-1]:
|
|
111
95
|
child = current.get(key)
|
|
112
|
-
#
|
|
113
|
-
# This overwrites any non-dict value that might have been there (which
|
|
114
|
-
# shouldn't happen in normal operation, but we handle it defensively).
|
|
115
|
-
# We use isinstance() instead of checking for None because we need to
|
|
116
|
-
# ensure the value is actually a dict, not just that the key exists.
|
|
96
|
+
# isinstance catches both missing keys and non-dict values at the same key.
|
|
117
97
|
if not isinstance(child, dict):
|
|
118
98
|
child = {}
|
|
119
99
|
current[key] = child
|
|
120
100
|
current = child
|
|
121
101
|
|
|
122
|
-
# Set the final value at the last key in the path
|
|
123
102
|
current[path[-1]] = value
|
|
124
103
|
|
|
125
104
|
|
|
@@ -142,24 +121,18 @@ def _validate_and_apply_leaf(
|
|
|
142
121
|
extra: The target dictionary to write the value to if validation passes.
|
|
143
122
|
problems: List to append validation problems to.
|
|
144
123
|
"""
|
|
145
|
-
#
|
|
146
|
-
# prevent bool values from passing validation for int types (since
|
|
147
|
-
# bool is a subclass of int). This ensures that the actual
|
|
148
|
-
# runtime type matches the schema type exactly.
|
|
124
|
+
# type() is, not isinstance(): bool subclasses int and must not pass int validation.
|
|
149
125
|
if type(value) is not leaf.expected_type:
|
|
150
126
|
error_msg = (
|
|
151
127
|
f"has type {type(value).__name__}, "
|
|
152
128
|
f"expected {leaf.expected_type.__name__}"
|
|
153
129
|
)
|
|
154
130
|
problems.append(
|
|
155
|
-
_DataProblem(
|
|
131
|
+
_DataProblem(_create_validation_error_dict(source, error_msg, value))
|
|
156
132
|
)
|
|
157
133
|
return
|
|
158
134
|
|
|
159
|
-
#
|
|
160
|
-
# item_expected_type (homogeneous primitive list).
|
|
161
|
-
# Note: isinstance() check is needed for type narrowing (mypy), even though
|
|
162
|
-
# type(value) is list is already guaranteed by the check above.
|
|
135
|
+
# isinstance() for mypy narrowing; type(value) is list is already guaranteed above.
|
|
163
136
|
if leaf.expected_type is list and isinstance(value, list):
|
|
164
137
|
list_problem = _validate_list_value(value, source, leaf.item_expected_type)
|
|
165
138
|
if list_problem is not None:
|
|
@@ -197,8 +170,12 @@ def _strip_empty(node: Any) -> Any:
|
|
|
197
170
|
The cleaned structure with empty dicts and None values removed.
|
|
198
171
|
"""
|
|
199
172
|
if isinstance(node, dict):
|
|
200
|
-
|
|
201
|
-
|
|
173
|
+
result: dict[str, Any] = {}
|
|
174
|
+
for k, v in node.items():
|
|
175
|
+
v = _strip_empty(v)
|
|
176
|
+
if v != {} and v is not None:
|
|
177
|
+
result[k] = v
|
|
178
|
+
return result
|
|
202
179
|
return node
|
|
203
180
|
|
|
204
181
|
|
|
@@ -274,63 +251,31 @@ def _apply_schema_internal(
|
|
|
274
251
|
extra: dict[str, Any] = {}
|
|
275
252
|
problems: list[_DataProblem] = []
|
|
276
253
|
|
|
277
|
-
|
|
278
|
-
# can be referenced by multiple leaves (allowing the same value to appear in
|
|
279
|
-
# different locations in the output structure). Grouping allows us to process
|
|
280
|
-
# all leaves for a given source together, which is more efficient and allows
|
|
281
|
-
# us to validate the value once per source (e.g., checking for None) rather
|
|
282
|
-
# than once per leaf.
|
|
283
|
-
source_to_leaves: dict[str, list[_SchemaLeaf]] = defaultdict(list)
|
|
284
|
-
for leaf in compiled.leaves:
|
|
285
|
-
source_to_leaves[leaf.source].append(leaf)
|
|
286
|
-
|
|
287
|
-
used_sources = set(source_to_leaves.keys())
|
|
288
|
-
|
|
289
|
-
# Process each source that appears in the schema. If a source is missing from
|
|
290
|
-
# extra_values, we silently skip it (this is normal - not all sources need to
|
|
291
|
-
# be present in every log call). We only validate and apply sources that are
|
|
292
|
-
# actually provided.
|
|
293
|
-
for source, leaves in source_to_leaves.items():
|
|
254
|
+
for source, leaves in compiled.source_to_leaves.items():
|
|
294
255
|
if source not in extra_values:
|
|
295
|
-
# Source not provided - this is normal, not an error. Skip it.
|
|
296
256
|
continue
|
|
297
257
|
|
|
298
258
|
value = extra_values[source]
|
|
299
259
|
|
|
300
|
-
# Check for None values explicitly. None is never allowed for any type,
|
|
301
|
-
# so we check it once per source (not once per leaf) before attempting
|
|
302
|
-
# type-specific validation. This avoids redundant checks when a source
|
|
303
|
-
# is used by multiple leaves.
|
|
304
260
|
if value is None:
|
|
305
|
-
error_msg = "is None"
|
|
306
261
|
problems.append(
|
|
307
|
-
_DataProblem(
|
|
262
|
+
_DataProblem(_create_validation_error_dict(source, "is None", None))
|
|
308
263
|
)
|
|
309
264
|
continue
|
|
310
265
|
|
|
311
|
-
# Validate the value against each leaf that references this source.
|
|
312
|
-
# Each leaf validates independently, so a value might pass validation
|
|
313
|
-
# for some leaves (where type matches) but fail for others (where type
|
|
314
|
-
# doesn't match). The value is written only to locations where validation
|
|
315
|
-
# succeeds.
|
|
316
266
|
for leaf in leaves:
|
|
317
267
|
_validate_and_apply_leaf(leaf, value, source, extra, problems)
|
|
318
268
|
|
|
319
|
-
# Report redundant fields: any keys in extra_values that are not referenced
|
|
320
|
-
# by any schema leaf. These are fields that the user provided but which are
|
|
321
|
-
# not defined in the schema, so they cannot be included in the log output.
|
|
322
|
-
# Optimization: if schema is empty (no used_sources), all fields are redundant,
|
|
323
|
-
# so we can skip the membership check for each key.
|
|
324
269
|
redundant_keys = (
|
|
325
270
|
extra_values.keys()
|
|
326
|
-
if not
|
|
327
|
-
else (key for key in extra_values.keys() if key not in
|
|
271
|
+
if not compiled.known_sources
|
|
272
|
+
else (key for key in extra_values.keys() if key not in compiled.known_sources)
|
|
328
273
|
)
|
|
329
274
|
for key in redundant_keys:
|
|
330
275
|
error_msg = "is not defined in schema"
|
|
331
276
|
problems.append(
|
|
332
277
|
_DataProblem(
|
|
333
|
-
|
|
278
|
+
_create_validation_error_dict(key, error_msg, extra_values[key])
|
|
334
279
|
)
|
|
335
280
|
)
|
|
336
281
|
|