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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logging-objects-with-schema
3
- Version: 0.4.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.10
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.4.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"
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",
@@ -5,8 +5,6 @@ logging package that validates and filters user-provided extra fields
5
5
  according to an application-defined JSON schema.
6
6
  """
7
7
 
8
- from __future__ import annotations
9
-
10
8
  from .schema_logger import SchemaLogger
11
9
 
12
10
  __all__ = [
@@ -1,8 +1,8 @@
1
1
  """Custom exception types used by logging_objects_with_schema."""
2
2
 
3
- from __future__ import annotations
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
- message: JSON string containing structured error information. The message
64
- is always a valid JSON object with the following structure:
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
- message: str
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 _create_validation_error_json(field: str, error: str, value: Any) -> str:
19
- """Create JSON string for a single validation error.
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() before JSON serialization. This ensures:
22
- - Any value type can be safely serialized (even non-JSON-serializable types)
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
- JSON string with field, error, and value (all via repr() for safety).
26
+ Dict with field, error, and value (all via repr() for safety).
34
27
  """
35
- return json.dumps(
36
- {
37
- "field": repr(field),
38
- "error": repr(error),
39
- "value": repr(value),
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(_create_validation_error_json(source, error_msg, value))
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
- # Collect unique type names of items that don't match the expected type.
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(_create_validation_error_json(source, error_msg, value))
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
- # If the key doesn't exist or exists but is not a dict, create a new dict.
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
- # Use strict type checking (type() is) instead of isinstance() to
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(_create_validation_error_json(source, error_msg, value))
131
+ _DataProblem(_create_validation_error_dict(source, error_msg, value))
156
132
  )
157
133
  return
158
134
 
159
- # For lists, validate that all elements strictly match the declared
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
- cleaned = {k: _strip_empty(v) for k, v in node.items()}
201
- return {k: v for k, v in cleaned.items() if v != {} and v is not None}
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
- # Group leaves by source field name. This is necessary because a single source
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(_create_validation_error_json(source, error_msg, None))
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 used_sources
327
- else (key for key in extra_values.keys() if key not in used_sources)
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
- _create_validation_error_json(key, error_msg, extra_values[key])
278
+ _create_validation_error_dict(key, error_msg, extra_values[key])
334
279
  )
335
280
  )
336
281