logging-objects-with-schema 0.1.3__tar.gz → 0.1.4__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.1.3
3
+ Version: 0.1.4
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
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "logging-objects-with-schema"
7
- version = "0.1.3"
7
+ version = "0.1.4"
8
8
  description = "Proxy logging wrapper that validates extra fields against a JSON schema."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -95,3 +95,23 @@ strict_equality = true
95
95
  [tool.bandit]
96
96
  skips = [ "B101", "B601" ]
97
97
  exclude_dirs = [ ".venv", "__pycache__", ".git", "htmlcov" ]
98
+
99
+ [tool.coverage.run]
100
+ source = [ "src" ]
101
+ branch = true
102
+
103
+ [tool.coverage.report]
104
+ exclude_lines = [
105
+ "def __repr__",
106
+ "if self\\.debug",
107
+ "raise AssertionError",
108
+ "raise NotImplementedError",
109
+ "if 0:",
110
+ "if __name__ == .__main__.:",
111
+ "@(abc\\.)?abstractmethod"
112
+ ]
113
+ show_missing = true
114
+ skip_covered = false
115
+
116
+ [tool.coverage.html]
117
+ directory = "htmlcov"
@@ -6,9 +6,13 @@ from dataclasses import dataclass
6
6
 
7
7
 
8
8
  @dataclass
9
- class SchemaProblem:
9
+ class _SchemaProblem:
10
10
  """Describes a single problem encountered while loading the schema.
11
11
 
12
+ This class is part of the internal implementation and is not considered
13
+ a public API. Its signature and behaviour may change between releases
14
+ without preserving backward compatibility.
15
+
12
16
  This class is used to report schema validation errors during schema
13
17
  compilation. Schema problems are fatal: if any are detected during
14
18
  logger initialization, the application is terminated after logging
@@ -36,12 +40,16 @@ class SchemaProblem:
36
40
 
37
41
 
38
42
  @dataclass
39
- class DataProblem:
43
+ class _DataProblem:
40
44
  """Describes a single problem encountered while validating log data.
41
45
 
46
+ This class is part of the internal implementation and is not considered
47
+ a public API. Its signature and behaviour may change between releases
48
+ without preserving backward compatibility.
49
+
42
50
  This class is used to report validation errors when applying the compiled
43
51
  schema to user-provided ``extra`` fields during logging. Unlike
44
- :class:`SchemaProblem`, data problems are not fatal: they are collected
52
+ :class:`_SchemaProblem`, data problems are not fatal: they are collected
45
53
  and logged as ERROR messages *after* the main log record has been emitted,
46
54
  ensuring 100% compatibility with standard logger behavior.
47
55
 
@@ -11,8 +11,8 @@ from collections import defaultdict
11
11
  from collections.abc import Mapping, MutableMapping
12
12
  from typing import Any
13
13
 
14
- from .errors import DataProblem
15
- from .schema_loader import CompiledSchema, SchemaLeaf
14
+ from .errors import _DataProblem
15
+ from .schema_loader import _CompiledSchema, _SchemaLeaf
16
16
 
17
17
 
18
18
  def _create_validation_error_json(field: str, error: str, value: Any) -> str:
@@ -45,7 +45,7 @@ def _validate_list_value(
45
45
  value: list,
46
46
  source: str,
47
47
  item_expected_type: type | None,
48
- ) -> DataProblem | None:
48
+ ) -> _DataProblem | None:
49
49
  """Validate that a list value matches the expected item type.
50
50
 
51
51
  Validates that all elements in the list have the exact type declared by
@@ -58,11 +58,11 @@ def _validate_list_value(
58
58
  for list-typed leaves.
59
59
 
60
60
  Returns:
61
- DataProblem if validation fails, None if validation succeeds.
61
+ _DataProblem if validation fails, None if validation succeeds.
62
62
  """
63
63
  if item_expected_type is None:
64
64
  error_msg = "is a list but has no item type configured"
65
- return DataProblem(_create_validation_error_json(source, error_msg, value))
65
+ return _DataProblem(_create_validation_error_json(source, error_msg, value))
66
66
 
67
67
  if len(value) == 0:
68
68
  # Empty lists are always valid
@@ -83,7 +83,7 @@ def _validate_list_value(
83
83
  f"expected all elements to be of type "
84
84
  f"{item_expected_type.__name__}"
85
85
  )
86
- return DataProblem(_create_validation_error_json(source, error_msg, value))
86
+ return _DataProblem(_create_validation_error_json(source, error_msg, value))
87
87
 
88
88
  return None
89
89
 
@@ -124,11 +124,11 @@ def _set_nested_value(
124
124
 
125
125
 
126
126
  def _validate_and_apply_leaf(
127
- leaf: SchemaLeaf,
127
+ leaf: _SchemaLeaf,
128
128
  value: Any,
129
129
  source: str,
130
130
  extra: MutableMapping[str, Any],
131
- problems: list[DataProblem],
131
+ problems: list[_DataProblem],
132
132
  ) -> None:
133
133
  """Validate a value against a schema leaf and apply it if valid.
134
134
 
@@ -152,7 +152,7 @@ def _validate_and_apply_leaf(
152
152
  f"expected {leaf.expected_type.__name__}"
153
153
  )
154
154
  problems.append(
155
- DataProblem(_create_validation_error_json(source, error_msg, value))
155
+ _DataProblem(_create_validation_error_json(source, error_msg, value))
156
156
  )
157
157
  return
158
158
 
@@ -203,27 +203,27 @@ def _strip_empty(node: Any) -> Any:
203
203
 
204
204
 
205
205
  def _apply_schema_internal(
206
- compiled: CompiledSchema,
206
+ compiled: _CompiledSchema,
207
207
  extra_values: Mapping[str, Any],
208
- ) -> tuple[dict[str, Any], list[DataProblem]]:
208
+ ) -> tuple[dict[str, Any], list[_DataProblem]]:
209
209
  """Internal function to build structured ``extra`` from compiled schema.
210
210
 
211
- The function applies a :class:`CompiledSchema` to user-provided ``extra``
211
+ The function applies a :class:`_CompiledSchema` to user-provided ``extra``
212
212
  values and returns a tuple ``(structured_extra, problems)`` where:
213
213
 
214
214
  - ``structured_extra`` is a nested dictionary that follows the schema
215
215
  structure and contains only fields that passed validation;
216
- - ``problems`` is a list of :class:`DataProblem` describing all data
216
+ - ``problems`` is a list of :class:`_DataProblem` describing all data
217
217
  issues observed during processing.
218
218
 
219
219
  Behaviour summary:
220
220
 
221
221
  - If the compiled schema is effectively empty (no valid leaves),
222
222
  all fields from ``extra_values`` are treated as redundant: the returned
223
- payload is empty, and a :class:`DataProblem` is created for each field.
223
+ payload is empty, and a :class:`_DataProblem` is created for each field.
224
224
  - For each ``source`` mentioned in the schema when there are valid leaves:
225
225
  - if the source is missing from ``extra_values``, it is silently skipped;
226
- - if the corresponding value is ``None``, a ``DataProblem`` is recorded
226
+ - if the corresponding value is ``None``, a ``_DataProblem`` is recorded
227
227
  and the value is not written to the payload.
228
228
  - Type checks are strict: the runtime type must exactly match the declared
229
229
  Python type (``type(value) is leaf.expected_type``). This prevents
@@ -233,17 +233,17 @@ def _apply_schema_internal(
233
233
  - all elements must have the exact type declared by the leaf
234
234
  ``item_expected_type`` (for example, list[str], list[int]);
235
235
  - non-primitive elements and elements of a different primitive type are
236
- rejected with a ``DataProblem`` and the list value is not written.
236
+ rejected with a ``_DataProblem`` and the list value is not written.
237
237
  - Redundant fields from ``extra_values`` (not referenced by any leaf
238
238
  ``source``) are always reported as problems: each such field generates
239
- a :class:`DataProblem` indicating that it is not defined in the schema.
239
+ a :class:`_DataProblem` indicating that it is not defined in the schema.
240
240
  - A single ``source`` may be used by multiple leaves. The value is
241
241
  validated independently for each leaf and written only to locations
242
- where the type matches; mismatched locations produce ``DataProblem``
242
+ where the type matches; mismatched locations produce ``_DataProblem``
243
243
  entries, but do not affect successful locations.
244
244
 
245
245
  The function itself does not raise exceptions; it only accumulates
246
- :class:`DataProblem` instances for the caller to handle.
246
+ :class:`_DataProblem` instances for the caller to handle.
247
247
 
248
248
  Performance considerations:
249
249
  Time complexity is O(n + m) where n is the number of leaves in the
@@ -269,10 +269,10 @@ def _apply_schema_internal(
269
269
  change between releases without preserving backward compatibility.
270
270
 
271
271
  Returns:
272
- Tuple of (structured_extra, list[DataProblem]).
272
+ Tuple of (structured_extra, list[_DataProblem]).
273
273
  """
274
274
  extra: dict[str, Any] = {}
275
- problems: list[DataProblem] = []
275
+ problems: list[_DataProblem] = []
276
276
 
277
277
  # Group leaves by source field name. This is necessary because a single source
278
278
  # can be referenced by multiple leaves (allowing the same value to appear in
@@ -280,7 +280,7 @@ def _apply_schema_internal(
280
280
  # all leaves for a given source together, which is more efficient and allows
281
281
  # us to validate the value once per source (e.g., checking for None) rather
282
282
  # than once per leaf.
283
- source_to_leaves: dict[str, list[SchemaLeaf]] = defaultdict(list)
283
+ source_to_leaves: dict[str, list[_SchemaLeaf]] = defaultdict(list)
284
284
  for leaf in compiled.leaves:
285
285
  source_to_leaves[leaf.source].append(leaf)
286
286
 
@@ -304,7 +304,7 @@ def _apply_schema_internal(
304
304
  if value is None:
305
305
  error_msg = "is None"
306
306
  problems.append(
307
- DataProblem(_create_validation_error_json(source, error_msg, None))
307
+ _DataProblem(_create_validation_error_json(source, error_msg, None))
308
308
  )
309
309
  continue
310
310
 
@@ -329,7 +329,7 @@ def _apply_schema_internal(
329
329
  for key in redundant_keys:
330
330
  error_msg = "is not defined in schema"
331
331
  problems.append(
332
- DataProblem(
332
+ _DataProblem(
333
333
  _create_validation_error_json(key, error_msg, extra_values[key])
334
334
  )
335
335
  )
@@ -18,18 +18,22 @@ from dataclasses import dataclass
18
18
  from pathlib import Path
19
19
  from typing import Any
20
20
 
21
- from .errors import SchemaProblem
21
+ from .errors import _SchemaProblem
22
22
 
23
- SCHEMA_FILE_NAME = "logging_objects_with_schema.json"
23
+ _SCHEMA_FILE_NAME = "logging_objects_with_schema.json"
24
24
 
25
25
  # Maximum allowed depth for schema nesting (protection against DoS)
26
26
  MAX_SCHEMA_DEPTH = 100
27
27
 
28
28
 
29
29
  @dataclass
30
- class SchemaLeaf:
30
+ class _SchemaLeaf:
31
31
  """Represents a single leaf in the schema tree.
32
32
 
33
+ This class is part of the internal implementation and is not considered
34
+ a public API. Its signature and behaviour may change between releases
35
+ without preserving backward compatibility.
36
+
33
37
  Attributes:
34
38
  path: Full path of keys from the schema root to this leaf.
35
39
  source: Name of the field in the ``extra`` mapping.
@@ -45,10 +49,15 @@ class SchemaLeaf:
45
49
 
46
50
 
47
51
  @dataclass
48
- class CompiledSchema:
49
- """Internal representation of a compiled schema."""
52
+ class _CompiledSchema:
53
+ """Internal representation of a compiled schema.
54
+
55
+ This class is part of the internal implementation and is not considered
56
+ a public API. Its signature and behaviour may change between releases
57
+ without preserving backward compatibility.
58
+ """
50
59
 
51
- leaves: list[SchemaLeaf]
60
+ leaves: list[_SchemaLeaf]
52
61
 
53
62
  @property
54
63
  def is_empty(self) -> bool:
@@ -58,17 +67,17 @@ class CompiledSchema:
58
67
 
59
68
 
60
69
  def _create_empty_compiled_schema_with_problems(
61
- problems: list[SchemaProblem],
62
- ) -> tuple[CompiledSchema, list[SchemaProblem]]:
63
- """Create an empty CompiledSchema with problems.
70
+ problems: list[_SchemaProblem],
71
+ ) -> tuple[_CompiledSchema, list[_SchemaProblem]]:
72
+ """Create an empty _CompiledSchema with problems.
64
73
 
65
74
  Args:
66
75
  problems: List of schema problems.
67
76
 
68
77
  Returns:
69
- Tuple of (empty CompiledSchema, problems list).
78
+ Tuple of (empty _CompiledSchema, problems list).
70
79
  """
71
- return (CompiledSchema(leaves=[]), problems)
80
+ return (_CompiledSchema(leaves=[]), problems)
72
81
 
73
82
 
74
83
  _TYPE_MAP: Mapping[str, type] = {
@@ -101,9 +110,9 @@ _TYPE_MAP: Mapping[str, type] = {
101
110
  # use double-checked locking to avoid race conditions.
102
111
 
103
112
  # Compiled schema cache: Key is absolute schema_path, Value is tuple of
104
- # (CompiledSchema, list[SchemaProblem]). This cache stores both successful
113
+ # (_CompiledSchema, list[_SchemaProblem]). This cache stores both successful
105
114
  # compilations and failures (with problems list).
106
- _SCHEMA_CACHE: dict[Path, tuple[CompiledSchema, list[SchemaProblem]]] = {}
115
+ _SCHEMA_CACHE: dict[Path, tuple[_CompiledSchema, list[_SchemaProblem]]] = {}
107
116
 
108
117
  _cache_lock = threading.RLock()
109
118
 
@@ -134,7 +143,7 @@ def _find_schema_file() -> Path | None:
134
143
  current = start_path
135
144
 
136
145
  while True:
137
- schema_path = current / SCHEMA_FILE_NAME
146
+ schema_path = current / _SCHEMA_FILE_NAME
138
147
  if schema_path.exists():
139
148
  # Use resolve() to get an absolute path and resolve any symbolic links.
140
149
  # This ensures we have a canonical path that can be used as a cache key
@@ -238,7 +247,7 @@ def _cache_and_return_missing_path() -> Path:
238
247
  global _resolved_schema_path, _cached_cwd
239
248
 
240
249
  current_cwd = _get_current_working_directory()
241
- schema_path = (current_cwd / SCHEMA_FILE_NAME).resolve()
250
+ schema_path = (current_cwd / _SCHEMA_FILE_NAME).resolve()
242
251
  _resolved_schema_path = schema_path
243
252
  _cached_cwd = current_cwd # Track CWD since path depends on it
244
253
  return schema_path
@@ -247,7 +256,7 @@ def _cache_and_return_missing_path() -> Path:
247
256
  def _get_schema_path() -> Path:
248
257
  """Resolve the absolute path to the JSON schema file with caching semantics.
249
258
 
250
- The function searches for ``SCHEMA_FILE_NAME`` by walking upward from the
259
+ The function searches for ``_SCHEMA_FILE_NAME`` by walking upward from the
251
260
  current working directory and caches the result:
252
261
 
253
262
  - When the schema file is found, its absolute path is cached as
@@ -265,16 +274,20 @@ def _get_schema_path() -> Path:
265
274
  Absolute path where the schema file is located or expected to be.
266
275
  """
267
276
  with _path_cache_lock:
277
+ # Check cached path for missing file first (CWD-dependent)
278
+ # This must be checked before found file cache, because
279
+ # _check_cached_found_file_path would invalidate the cache if file
280
+ # doesn't exist, even for missing file cache.
281
+ if _cached_cwd is not None:
282
+ cached_path = _check_cached_missing_file_path()
283
+ if cached_path is not None:
284
+ return cached_path
285
+
268
286
  # Check cached path for found file (CWD-independent)
269
287
  cached_path = _check_cached_found_file_path()
270
288
  if cached_path is not None:
271
289
  return cached_path
272
290
 
273
- # Check cached path for missing file (CWD-dependent)
274
- cached_path = _check_cached_missing_file_path()
275
- if cached_path is not None:
276
- return cached_path
277
-
278
291
  # Search for schema file
279
292
  found_path = _find_schema_file()
280
293
  if found_path is not None:
@@ -291,7 +304,7 @@ def _load_raw_schema(schema_path: Path) -> tuple[dict[str, Any], Path]:
291
304
  This function attempts to read and parse the schema file. If any problems
292
305
  occur (file not found, I/O errors, invalid JSON, wrong top-level type),
293
306
  it raises exceptions (FileNotFoundError or ValueError). These exceptions
294
- are then converted to :class:`SchemaProblem` instances by the caller
307
+ are then converted to :class:`_SchemaProblem` instances by the caller
295
308
  (_compile_schema_internal).
296
309
 
297
310
  Args:
@@ -319,7 +332,7 @@ def _load_raw_schema(schema_path: Path) -> tuple[dict[str, Any], Path]:
319
332
  except OSError as exc:
320
333
  # Normalise I/O errors when reading the schema file (e.g., permission
321
334
  # denied, file not found) to ValueError so that _compile_schema_internal()
322
- # can report them as SchemaProblem instances instead of leaking raw
335
+ # can report them as _SchemaProblem instances instead of leaking raw
323
336
  # OSError to callers. Note: System-level OSError (e.g., from os.getcwd())
324
337
  # is not caught here and propagates directly.
325
338
  raise ValueError(
@@ -330,7 +343,7 @@ def _load_raw_schema(schema_path: Path) -> tuple[dict[str, Any], Path]:
330
343
 
331
344
  if not isinstance(data, dict):
332
345
  # Normalise non-object top-level schemas into a ValueError so that the
333
- # caller can report it as a SchemaProblem while keeping type safety.
346
+ # caller can report it as a _SchemaProblem while keeping type safety.
334
347
  raise ValueError("Top-level schema must be a JSON object")
335
348
 
336
349
  return data, schema_path
@@ -392,9 +405,9 @@ def _validate_and_create_leaf(
392
405
  value_dict: dict[str, Any],
393
406
  path: tuple[str, ...],
394
407
  key: str,
395
- problems: list[SchemaProblem],
396
- ) -> SchemaLeaf | None:
397
- """Validate a leaf node and create SchemaLeaf if valid.
408
+ problems: list[_SchemaProblem],
409
+ ) -> _SchemaLeaf | None:
410
+ """Validate a leaf node and create _SchemaLeaf if valid.
398
411
 
399
412
  Args:
400
413
  value_dict: Dictionary containing leaf node data.
@@ -403,7 +416,7 @@ def _validate_and_create_leaf(
403
416
  problems: List to collect validation problems.
404
417
 
405
418
  Returns:
406
- SchemaLeaf if validation passes, None otherwise.
419
+ _SchemaLeaf if validation passes, None otherwise.
407
420
  """
408
421
  leaf_type = value_dict.get("type")
409
422
  leaf_source = value_dict.get("source")
@@ -414,7 +427,7 @@ def _validate_and_create_leaf(
414
427
 
415
428
  if type_invalid:
416
429
  problems.append(
417
- SchemaProblem(
430
+ _SchemaProblem(
418
431
  f"Incomplete leaf at {_format_path(path, key)}: "
419
432
  f"type cannot be None or empty",
420
433
  ),
@@ -422,7 +435,7 @@ def _validate_and_create_leaf(
422
435
 
423
436
  if source_invalid:
424
437
  problems.append(
425
- SchemaProblem(
438
+ _SchemaProblem(
426
439
  f"Incomplete leaf at {_format_path(path, key)}: "
427
440
  f"source cannot be None or empty",
428
441
  ),
@@ -437,7 +450,7 @@ def _validate_and_create_leaf(
437
450
  expected_type = _TYPE_MAP.get(str(leaf_type))
438
451
  if expected_type is None:
439
452
  problems.append(
440
- SchemaProblem(
453
+ _SchemaProblem(
441
454
  f"Unknown type '{leaf_type}' at {_format_path(path, key)}",
442
455
  ),
443
456
  )
@@ -451,7 +464,7 @@ def _validate_and_create_leaf(
451
464
  item_type_invalid = _is_empty_or_none(item_type_name)
452
465
  if item_type_invalid:
453
466
  problems.append(
454
- SchemaProblem(
467
+ _SchemaProblem(
455
468
  f"Incomplete leaf at {_format_path(path, key)}: "
456
469
  f"item_type is required for list type and "
457
470
  f"cannot be None or empty",
@@ -463,7 +476,7 @@ def _validate_and_create_leaf(
463
476
  # Item type must be a primitive (str, int, float, bool), not list
464
477
  if item_expected_type is None or item_expected_type is list:
465
478
  problems.append(
466
- SchemaProblem(
479
+ _SchemaProblem(
467
480
  f"Invalid item_type '{item_type_name}' at "
468
481
  f"{_format_path(path, key)}: only primitive item types "
469
482
  f"('str', 'int', 'float', 'bool') are allowed for lists",
@@ -471,11 +484,11 @@ def _validate_and_create_leaf(
471
484
  )
472
485
  return None
473
486
 
474
- return SchemaLeaf(
487
+ return _SchemaLeaf(
475
488
  path=path + (key,),
476
489
  # Convert to string to ensure type consistency. Even though source should
477
490
  # be a string from JSON, this guards against unexpected types and ensures
478
- # the SchemaLeaf always has a string source.
491
+ # the _SchemaLeaf always has a string source.
479
492
  source=str(leaf_source),
480
493
  expected_type=expected_type,
481
494
  item_expected_type=item_expected_type,
@@ -485,14 +498,14 @@ def _validate_and_create_leaf(
485
498
  def _compile_schema_tree(
486
499
  node: MutableMapping[str, Any],
487
500
  path: tuple[str, ...],
488
- problems: list[SchemaProblem],
489
- ) -> Iterable[SchemaLeaf]:
490
- """Recursively compile a schema node into SchemaLeaf objects.
501
+ problems: list[_SchemaProblem],
502
+ ) -> Iterable[_SchemaLeaf]:
503
+ """Recursively compile a schema node into _SchemaLeaf objects.
491
504
 
492
505
  This function recursively walks the schema tree structure, identifying leaf
493
506
  nodes (those with ``type`` and ``source`` fields) and inner nodes (those
494
507
  without these fields). Leaf nodes are validated and converted to
495
- :class:`SchemaLeaf` objects, while inner nodes are recursively processed.
508
+ :class:`_SchemaLeaf` objects, while inner nodes are recursively processed.
496
509
 
497
510
  Performance considerations:
498
511
  Time complexity is O(n) where n is the total number of nodes in the
@@ -512,7 +525,7 @@ def _compile_schema_tree(
512
525
  problems: List to collect validation problems.
513
526
 
514
527
  Yields:
515
- SchemaLeaf objects found in the tree.
528
+ _SchemaLeaf objects found in the tree.
516
529
  """
517
530
  # Check for excessive nesting depth (DoS protection: prevent deeply nested
518
531
  # schemas that could cause stack overflow or excessive memory usage).
@@ -520,7 +533,7 @@ def _compile_schema_tree(
520
533
  # and to provide clear error messages about the problematic path.
521
534
  if len(path) > MAX_SCHEMA_DEPTH:
522
535
  problems.append(
523
- SchemaProblem(
536
+ _SchemaProblem(
524
537
  f"Schema nesting depth exceeds maximum allowed depth of "
525
538
  f"{MAX_SCHEMA_DEPTH} at path {_format_path(path)}"
526
539
  ),
@@ -530,7 +543,7 @@ def _compile_schema_tree(
530
543
  for key, value in node.items():
531
544
  if not isinstance(value, Mapping):
532
545
  problems.append(
533
- SchemaProblem(
546
+ _SchemaProblem(
534
547
  f"Invalid schema at {_format_path(path, key)}: expected object"
535
548
  ),
536
549
  )
@@ -552,9 +565,13 @@ def _compile_schema_tree(
552
565
 
553
566
 
554
567
  @functools.lru_cache(maxsize=1)
555
- def get_builtin_logrecord_attributes() -> set[str]:
568
+ def _get_builtin_logrecord_attributes() -> set[str]:
556
569
  """Get set of standard LogRecord attribute names.
557
570
 
571
+ This function is part of the internal implementation and is not considered
572
+ a public API. Its signature and behaviour may change between releases
573
+ without preserving backward compatibility.
574
+
558
575
  This function extracts attribute names from LogRecord that represent
559
576
  system fields and should not be used as root keys in the schema or
560
577
  treated as user-provided extra fields.
@@ -573,7 +590,7 @@ def get_builtin_logrecord_attributes() -> set[str]:
573
590
  Examples include: 'name', 'levelno', 'pathname', 'lineno', 'msg', etc.
574
591
 
575
592
  Example:
576
- >>> forbidden = get_builtin_logrecord_attributes()
593
+ >>> forbidden = _get_builtin_logrecord_attributes()
577
594
  >>> "name" in forbidden
578
595
  True
579
596
  >>> "ServicePayload" in forbidden
@@ -608,32 +625,32 @@ def get_builtin_logrecord_attributes() -> set[str]:
608
625
 
609
626
 
610
627
  def _check_root_conflicts(
611
- schema_dict: Mapping[str, Any], problems: list[SchemaProblem]
628
+ schema_dict: Mapping[str, Any], problems: list[_SchemaProblem]
612
629
  ) -> None:
613
630
  """Check schema root keys for conflicts with reserved logging fields."""
614
631
 
615
- forbidden_root_keys = get_builtin_logrecord_attributes()
632
+ forbidden_root_keys = _get_builtin_logrecord_attributes()
616
633
 
617
634
  for key in schema_dict.keys():
618
635
  if key in forbidden_root_keys:
619
636
  problems.append(
620
- SchemaProblem(
637
+ _SchemaProblem(
621
638
  f"Root key '{key}' conflicts with reserved logging fields",
622
639
  ),
623
640
  )
624
641
 
625
642
 
626
- def _compile_schema_internal() -> tuple[CompiledSchema, list[SchemaProblem]]:
627
- """Compile JSON schema into ``CompiledSchema`` and collect all problems.
643
+ def _compile_schema_internal() -> tuple[_CompiledSchema, list[_SchemaProblem]]:
644
+ """Compile JSON schema into ``_CompiledSchema`` and collect all problems.
628
645
 
629
646
  The function loads the raw JSON schema, validates its structure, checks
630
647
  root keys for conflicts with reserved ``logging.LogRecord`` attributes
631
- and compiles all valid leaves into a :class:`CompiledSchema`. All issues
632
- discovered during this process are reported as :class:`SchemaProblem`
648
+ and compiles all valid leaves into a :class:`_CompiledSchema`. All issues
649
+ discovered during this process are reported as :class:`_SchemaProblem`
633
650
  instances.
634
651
 
635
652
  Results are cached process-wide: the cache key is the absolute schema
636
- file path and the value is a tuple ``(CompiledSchema, list[SchemaProblem])``.
653
+ file path and the value is a tuple ``(_CompiledSchema, list[_SchemaProblem])``.
637
654
  Once a schema for a given path has been observed (including the cases when
638
655
  it is missing or invalid), subsequent calls always return the cached result
639
656
  without re-reading or re-compiling the schema. To pick up on-disk changes
@@ -642,7 +659,7 @@ def _compile_schema_internal() -> tuple[CompiledSchema, list[SchemaProblem]]:
642
659
 
643
660
  This function never raises exceptions. It always returns the best-effort
644
661
  compiled schema together with a list of problems detected during processing
645
- (an empty ``CompiledSchema`` when the schema is missing or invalid).
662
+ (an empty ``_CompiledSchema`` when the schema is missing or invalid).
646
663
 
647
664
  Performance considerations:
648
665
  First compilation of a schema involves file I/O, JSON parsing, and tree
@@ -665,13 +682,13 @@ def _compile_schema_internal() -> tuple[CompiledSchema, list[SchemaProblem]]:
665
682
  between releases without preserving backward compatibility.
666
683
 
667
684
  Returns:
668
- Tuple of (CompiledSchema, list[SchemaProblem]).
685
+ Tuple of (_CompiledSchema, list[_SchemaProblem]).
669
686
  """
670
687
  schema_path = _get_schema_path()
671
688
 
672
- # Fast-path: First check (without holding lock) if we have already attempted
673
- # to compile schema for this path. This avoids lock contention in the common
674
- # case when the schema is already cached.
689
+ # Fast-path: First check (with lock for thread-safety) if we have already attempted
690
+ # to compile schema for this path. This provides thread-safe cache access
691
+ # in the common case when the schema is already cached.
675
692
  with _cache_lock:
676
693
  cached = _SCHEMA_CACHE.get(schema_path)
677
694
  if cached is not None:
@@ -684,12 +701,12 @@ def _compile_schema_internal() -> tuple[CompiledSchema, list[SchemaProblem]]:
684
701
  # while holding the lock. If another thread already compiled it, we use that
685
702
  # result instead of storing our own (which might be different if the file changed).
686
703
 
687
- problems: list[SchemaProblem] = []
704
+ problems: list[_SchemaProblem] = []
688
705
 
689
706
  try:
690
707
  raw_schema, _ = _load_raw_schema(schema_path)
691
708
  except (FileNotFoundError, ValueError) as exc:
692
- problems.append(SchemaProblem(str(exc)))
709
+ problems.append(_SchemaProblem(str(exc)))
693
710
  result = _create_empty_compiled_schema_with_problems(problems)
694
711
  # Double-checked locking: Check cache again while holding lock. Another
695
712
  # thread might have compiled the schema (or handled the same error) while
@@ -708,11 +725,11 @@ def _compile_schema_internal() -> tuple[CompiledSchema, list[SchemaProblem]]:
708
725
 
709
726
  # Compile the schema tree into leaves. Each root key becomes a separate
710
727
  # tree that we compile recursively. Problems are collected as we go.
711
- leaves: list[SchemaLeaf] = []
728
+ leaves: list[_SchemaLeaf] = []
712
729
  for key, value in raw_schema.items():
713
730
  if not isinstance(value, Mapping):
714
731
  problems.append(
715
- SchemaProblem(f"Invalid schema at {key}: expected object"),
732
+ _SchemaProblem(f"Invalid schema at {key}: expected object"),
716
733
  )
717
734
  continue
718
735
 
@@ -722,7 +739,7 @@ def _compile_schema_internal() -> tuple[CompiledSchema, list[SchemaProblem]]:
722
739
  for leaf in _compile_schema_tree(dict(value), (key,), problems):
723
740
  leaves.append(leaf)
724
741
 
725
- compiled = CompiledSchema(leaves=leaves)
742
+ compiled = _CompiledSchema(leaves=leaves)
726
743
  result = (compiled, problems)
727
744
 
728
745
  # Double-checked locking: Check cache again while holding lock. Another thread
@@ -17,9 +17,9 @@ import sys
17
17
  from collections.abc import Mapping
18
18
  from typing import Any
19
19
 
20
- from .errors import SchemaProblem
20
+ from .errors import _SchemaProblem
21
21
  from .schema_applier import _apply_schema_internal
22
- from .schema_loader import CompiledSchema, _compile_schema_internal
22
+ from .schema_loader import _compile_schema_internal, _CompiledSchema
23
23
 
24
24
  # Python 3.11+ has improved findCaller() implementation with proper stacklevel support.
25
25
  # For Python < 3.11, we use inspect.stack() as a fallback due to known issues with
@@ -27,7 +27,7 @@ from .schema_loader import CompiledSchema, _compile_schema_internal
27
27
  _USE_FINDCALLER = sys.version_info >= (3, 11)
28
28
 
29
29
 
30
- def _log_schema_problems_and_exit(problems: list[SchemaProblem]) -> None:
30
+ def _log_schema_problems_and_exit(problems: list[_SchemaProblem]) -> None:
31
31
  """Log schema problems to stderr and terminate the application.
32
32
 
33
33
  Uses os._exit(1) instead of sys.exit(1) to ensure immediate termination
@@ -87,22 +87,22 @@ class SchemaLogger(logging.Logger):
87
87
  try:
88
88
  compiled, problems = _compile_schema_internal()
89
89
  except (OSError, ValueError, RuntimeError) as exc:
90
- # Convert system-level exceptions to SchemaProblem so they can be
90
+ # Convert system-level exceptions to _SchemaProblem so they can be
91
91
  # handled the same way as schema validation problems.
92
92
  # - OSError: system-level file system issues (e.g., os.getcwd() failures
93
93
  # when the current working directory is inaccessible or deleted).
94
94
  # Note: OSError that occurs when reading the schema file (e.g., permission
95
- # denied, I/O errors) is converted to SchemaProblem in _load_raw_schema()
95
+ # denied, I/O errors) is converted to _SchemaProblem in _load_raw_schema()
96
96
  # and does not reach this exception handler.
97
97
  # - ValueError: path resolution issues (e.g., invalid path characters,
98
98
  # malformed paths during schema file discovery)
99
99
  # - RuntimeError: threading issues (e.g., lock acquisition problems)
100
100
  # Note: JSON parsing and schema structure validation errors are
101
- # converted to SchemaProblem instances and do not raise ValueError here.
101
+ # converted to _SchemaProblem instances and do not raise ValueError here.
102
102
  # Note: System exceptions (KeyboardInterrupt, SystemExit) are not
103
103
  # caught, which is the correct behavior.
104
- problems = [SchemaProblem(f"Schema compilation failed: {exc}")]
105
- compiled = CompiledSchema(leaves=[])
104
+ problems = [_SchemaProblem(f"Schema compilation failed: {exc}")]
105
+ compiled = _CompiledSchema(leaves=[])
106
106
 
107
107
  if problems:
108
108
  # Schema is invalid; log problems and terminate without creating
@@ -111,7 +111,7 @@ class SchemaLogger(logging.Logger):
111
111
 
112
112
  # Schema is valid; create the logger instance.
113
113
  super().__init__(name, level)
114
- self._schema: CompiledSchema = compiled
114
+ self._schema: _CompiledSchema = compiled
115
115
 
116
116
  def _log(
117
117
  self,
@@ -203,7 +203,7 @@ class SchemaLogger(logging.Logger):
203
203
  stack_info=False, stacklevel=stacklevel + 1
204
204
  )
205
205
  # Format error message as JSON for machine processing.
206
- # Each DataProblem.message is already a JSON string (created by
206
+ # Each _DataProblem.message is already a JSON string (created by
207
207
  # _create_validation_error_json) with structure:
208
208
  # {"field": "...", "error": "...", "value": "..."}
209
209
  # We parse them back to dicts and combine into a single JSON object
@@ -226,7 +226,7 @@ class SchemaLogger(logging.Logger):
226
226
  # create a fallback error object. This should never happen in
227
227
  # normal operation since problem.message is always created via
228
228
  # _create_validation_error_json, but protects against unexpected
229
- # data corruption or manual DataProblem creation. The fallback
229
+ # data corruption or manual _DataProblem creation. The fallback
230
230
  # preserves the same structure (field, error, value) for
231
231
  # consistency.
232
232
  validation_errors.append(