logging-objects-with-schema 0.2.0__tar.gz → 0.3.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.2.0 → logging_objects_with_schema-0.3.0}/PKG-INFO +21 -4
- {logging_objects_with_schema-0.2.0 → logging_objects_with_schema-0.3.0}/README.md +19 -3
- {logging_objects_with_schema-0.2.0 → logging_objects_with_schema-0.3.0}/pyproject.toml +4 -4
- logging_objects_with_schema-0.3.0/src/logging_objects_with_schema/py.typed +0 -0
- {logging_objects_with_schema-0.2.0 → logging_objects_with_schema-0.3.0}/src/logging_objects_with_schema/schema_applier.py +1 -1
- {logging_objects_with_schema-0.2.0 → logging_objects_with_schema-0.3.0}/src/logging_objects_with_schema/schema_loader.py +132 -10
- {logging_objects_with_schema-0.2.0 → logging_objects_with_schema-0.3.0}/LICENSE +0 -0
- {logging_objects_with_schema-0.2.0 → logging_objects_with_schema-0.3.0}/src/logging_objects_with_schema/__init__.py +0 -0
- {logging_objects_with_schema-0.2.0 → logging_objects_with_schema-0.3.0}/src/logging_objects_with_schema/errors.py +0 -0
- {logging_objects_with_schema-0.2.0 → logging_objects_with_schema-0.3.0}/src/logging_objects_with_schema/schema_logger.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logging-objects-with-schema
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -16,6 +16,7 @@ Classifier: Programming Language :: Python :: 3.14
|
|
|
16
16
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
17
|
Classifier: Intended Audience :: Developers
|
|
18
18
|
Classifier: Topic :: System :: Logging
|
|
19
|
+
Classifier: Typing :: Typed
|
|
19
20
|
Requires-Dist: pytest>=7.0.0 ; extra == 'dev'
|
|
20
21
|
Requires-Dist: pytest-cov>=4.0.0 ; extra == 'dev'
|
|
21
22
|
Requires-Dist: black>=23.0.0 ; extra == 'dev'
|
|
@@ -213,11 +214,27 @@ An example of a valid empty schema (no leaves, no problems):
|
|
|
213
214
|
|
|
214
215
|
**Schema structure:**
|
|
215
216
|
|
|
216
|
-
- **Inner nodes**: Objects
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
- **Inner nodes**: Objects that contain child nodes (any fields that are objects).
|
|
218
|
+
Inner nodes cannot have `type` or `source` as strings (they can only have
|
|
219
|
+
these fields as objects, which are child nodes). Inner nodes must have at
|
|
220
|
+
least one child node.
|
|
221
|
+
- **Leaf nodes**: Objects with `type` and `source` fields as strings (properties).
|
|
222
|
+
A valid leaf must have both `type` and `source` present and non-empty. Leaf
|
|
223
|
+
nodes cannot have child nodes (any fields that are objects).
|
|
224
|
+
- **Node validation rules**:
|
|
225
|
+
- A node cannot be both a leaf and an inner node (cannot have both properties
|
|
226
|
+
and children simultaneously).
|
|
227
|
+
- A node cannot be empty (must be either a leaf with properties or an inner
|
|
228
|
+
node with children).
|
|
229
|
+
- Fields `type`, `source`, and `item_type` can be used as child node names
|
|
230
|
+
in inner nodes (as objects), but then they are treated as children, not
|
|
231
|
+
as leaf properties.
|
|
219
232
|
- **`type`**: One of `"str"`, `"int"`, `"float"`, `"bool"`, or `"list"`.
|
|
233
|
+
Must be a string for leaf nodes.
|
|
220
234
|
- **`source`**: The name of the field in `extra` from which the value is taken.
|
|
235
|
+
Must be a string for leaf nodes.
|
|
236
|
+
- **`item_type`**: Optional field for list-typed leaves. Must be a string if
|
|
237
|
+
present. See "List-typed fields" section below.
|
|
221
238
|
- **Root key restrictions**: Root keys cannot conflict with standard `logging`
|
|
222
239
|
module fields (e.g., `name`, `levelno`, `pathname`). Such conflicts cause
|
|
223
240
|
schema validation to fail.
|
|
@@ -180,11 +180,27 @@ An example of a valid empty schema (no leaves, no problems):
|
|
|
180
180
|
|
|
181
181
|
**Schema structure:**
|
|
182
182
|
|
|
183
|
-
- **Inner nodes**: Objects
|
|
184
|
-
|
|
185
|
-
|
|
183
|
+
- **Inner nodes**: Objects that contain child nodes (any fields that are objects).
|
|
184
|
+
Inner nodes cannot have `type` or `source` as strings (they can only have
|
|
185
|
+
these fields as objects, which are child nodes). Inner nodes must have at
|
|
186
|
+
least one child node.
|
|
187
|
+
- **Leaf nodes**: Objects with `type` and `source` fields as strings (properties).
|
|
188
|
+
A valid leaf must have both `type` and `source` present and non-empty. Leaf
|
|
189
|
+
nodes cannot have child nodes (any fields that are objects).
|
|
190
|
+
- **Node validation rules**:
|
|
191
|
+
- A node cannot be both a leaf and an inner node (cannot have both properties
|
|
192
|
+
and children simultaneously).
|
|
193
|
+
- A node cannot be empty (must be either a leaf with properties or an inner
|
|
194
|
+
node with children).
|
|
195
|
+
- Fields `type`, `source`, and `item_type` can be used as child node names
|
|
196
|
+
in inner nodes (as objects), but then they are treated as children, not
|
|
197
|
+
as leaf properties.
|
|
186
198
|
- **`type`**: One of `"str"`, `"int"`, `"float"`, `"bool"`, or `"list"`.
|
|
199
|
+
Must be a string for leaf nodes.
|
|
187
200
|
- **`source`**: The name of the field in `extra` from which the value is taken.
|
|
201
|
+
Must be a string for leaf nodes.
|
|
202
|
+
- **`item_type`**: Optional field for list-typed leaves. Must be a string if
|
|
203
|
+
present. See "List-typed fields" section below.
|
|
188
204
|
- **Root key restrictions**: Root keys cannot conflict with standard `logging`
|
|
189
205
|
module fields (e.g., `name`, `levelno`, `pathname`). Such conflicts cause
|
|
190
206
|
schema validation to fail.
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "logging-objects-with-schema"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
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"
|
|
@@ -27,7 +27,8 @@ classifiers = [
|
|
|
27
27
|
"Programming Language :: Python :: 3.14",
|
|
28
28
|
"Programming Language :: Python :: 3 :: Only",
|
|
29
29
|
"Intended Audience :: Developers",
|
|
30
|
-
"Topic :: System :: Logging"
|
|
30
|
+
"Topic :: System :: Logging",
|
|
31
|
+
"Typing :: Typed"
|
|
31
32
|
]
|
|
32
33
|
|
|
33
34
|
[[project.authors]]
|
|
@@ -93,8 +94,7 @@ warn_unreachable = true
|
|
|
93
94
|
strict_equality = true
|
|
94
95
|
|
|
95
96
|
[tool.bandit]
|
|
96
|
-
|
|
97
|
-
exclude_dirs = [ ".venv", "__pycache__", ".git", "htmlcov" ]
|
|
97
|
+
exclude_dirs = [ ".venv", "__pycache__", ".git", "htmlcov", "tests" ]
|
|
98
98
|
|
|
99
99
|
[tool.coverage.run]
|
|
100
100
|
source = [ "src" ]
|
|
File without changes
|
|
@@ -16,7 +16,7 @@ import threading
|
|
|
16
16
|
from collections.abc import Iterable, Mapping, MutableMapping
|
|
17
17
|
from dataclasses import dataclass
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from typing import Any
|
|
19
|
+
from typing import Any, Literal
|
|
20
20
|
|
|
21
21
|
from .errors import _SchemaProblem
|
|
22
22
|
|
|
@@ -383,11 +383,99 @@ def _is_empty_or_none(value: Any) -> bool:
|
|
|
383
383
|
return value is None or (isinstance(value, str) and value.strip() == "")
|
|
384
384
|
|
|
385
385
|
|
|
386
|
+
def _determine_node_type_and_validate(
|
|
387
|
+
value_dict: dict[str, Any],
|
|
388
|
+
path: tuple[str, ...],
|
|
389
|
+
key: str,
|
|
390
|
+
problems: list[_SchemaProblem],
|
|
391
|
+
) -> tuple[Literal["leaf", "inner"] | None, bool]:
|
|
392
|
+
"""Determine node type (leaf/inner) and validate node structure.
|
|
393
|
+
|
|
394
|
+
A node can be either:
|
|
395
|
+
- A leaf node: has 'type' and 'source' as strings (properties), no children
|
|
396
|
+
- An inner node: has children (any fields that are objects), no leaf properties
|
|
397
|
+
|
|
398
|
+
A node cannot have both properties and children, and cannot be empty.
|
|
399
|
+
|
|
400
|
+
Args:
|
|
401
|
+
value_dict: Dictionary containing node data.
|
|
402
|
+
path: Current path in the schema tree.
|
|
403
|
+
key: Current key being processed.
|
|
404
|
+
problems: List to collect validation problems. Validation errors are
|
|
405
|
+
automatically appended to this list when an invalid node is detected.
|
|
406
|
+
|
|
407
|
+
Returns:
|
|
408
|
+
Tuple of (node_type, is_valid) where:
|
|
409
|
+
- node_type: "leaf" for valid leaf nodes, "inner" for valid inner nodes,
|
|
410
|
+
or None if the node is invalid
|
|
411
|
+
- is_valid: True if node is valid, False if there are validation errors
|
|
412
|
+
|
|
413
|
+
When is_valid is False:
|
|
414
|
+
- node_type is always None
|
|
415
|
+
- A validation problem has been added to the problems list
|
|
416
|
+
- The caller should skip processing this node (e.g., use continue)
|
|
417
|
+
"""
|
|
418
|
+
type_value = value_dict.get("type")
|
|
419
|
+
source_value = value_dict.get("source")
|
|
420
|
+
item_type_value = value_dict.get("item_type")
|
|
421
|
+
|
|
422
|
+
# Check if node has leaf properties
|
|
423
|
+
# Leaf properties are: type (required, string), source (required, string),
|
|
424
|
+
# item_type (optional, string). If type/source/item_type are objects,
|
|
425
|
+
# they are children, not properties
|
|
426
|
+
has_leaf_properties = (
|
|
427
|
+
isinstance(type_value, str)
|
|
428
|
+
or isinstance(source_value, str)
|
|
429
|
+
or isinstance(item_type_value, str)
|
|
430
|
+
)
|
|
431
|
+
|
|
432
|
+
# Check if node has children (any field that is an object/Mapping)
|
|
433
|
+
# Children can have ANY names, including type, source, item_type - this is
|
|
434
|
+
# valid for inner nodes. If type, source, or item_type are objects,
|
|
435
|
+
# they count as children
|
|
436
|
+
has_children = any(
|
|
437
|
+
isinstance(field_value, Mapping) for field_value in value_dict.values()
|
|
438
|
+
)
|
|
439
|
+
|
|
440
|
+
# Validate node structure
|
|
441
|
+
if has_leaf_properties and has_children:
|
|
442
|
+
# Node cannot have both properties and children
|
|
443
|
+
problems.append(
|
|
444
|
+
_SchemaProblem(
|
|
445
|
+
f"Invalid node at {_format_path(path, key)}: "
|
|
446
|
+
f"node cannot have both properties (type/source as strings) "
|
|
447
|
+
f"and children (object fields)"
|
|
448
|
+
),
|
|
449
|
+
)
|
|
450
|
+
return (None, False)
|
|
451
|
+
|
|
452
|
+
if not has_leaf_properties and not has_children:
|
|
453
|
+
# Node must be either a leaf or have children
|
|
454
|
+
problems.append(
|
|
455
|
+
_SchemaProblem(
|
|
456
|
+
f"Invalid node at {_format_path(path, key)}: "
|
|
457
|
+
f"node must be either a leaf (with type/source as strings) "
|
|
458
|
+
f"or have children (object fields)"
|
|
459
|
+
),
|
|
460
|
+
)
|
|
461
|
+
return (None, False)
|
|
462
|
+
|
|
463
|
+
# Node is valid - determine type
|
|
464
|
+
if has_leaf_properties:
|
|
465
|
+
return ("leaf", True)
|
|
466
|
+
else: # has_children
|
|
467
|
+
return ("inner", True)
|
|
468
|
+
|
|
469
|
+
|
|
386
470
|
def _is_leaf_node(value_dict: dict[str, Any]) -> bool:
|
|
387
471
|
"""Check if a schema node is a leaf node.
|
|
388
472
|
|
|
389
|
-
A leaf node is identified by having
|
|
390
|
-
|
|
473
|
+
A leaf node is identified by having at least one of 'type' or 'source' fields
|
|
474
|
+
that is a string. If 'type' or 'source' are themselves objects (not strings),
|
|
475
|
+
they are child nodes, not leaf properties.
|
|
476
|
+
|
|
477
|
+
Inner nodes have either no 'type'/'source' fields, or have these fields as
|
|
478
|
+
objects (child nodes) rather than strings.
|
|
391
479
|
|
|
392
480
|
We use `.get()` with `is not None` check instead of `in` operator because:
|
|
393
481
|
- A field might be present but have a None value (which indicates an error)
|
|
@@ -400,7 +488,19 @@ def _is_leaf_node(value_dict: dict[str, Any]) -> bool:
|
|
|
400
488
|
Returns:
|
|
401
489
|
True if the node is a leaf, False if it's an inner node.
|
|
402
490
|
"""
|
|
403
|
-
|
|
491
|
+
type_value = value_dict.get("type")
|
|
492
|
+
source_value = value_dict.get("source")
|
|
493
|
+
|
|
494
|
+
# If either field is an object (Mapping), this is an inner node, not a leaf
|
|
495
|
+
if isinstance(type_value, Mapping) or isinstance(source_value, Mapping):
|
|
496
|
+
return False
|
|
497
|
+
|
|
498
|
+
# Check if at least one field is present and is a string
|
|
499
|
+
if isinstance(type_value, str) or isinstance(source_value, str):
|
|
500
|
+
return True
|
|
501
|
+
|
|
502
|
+
# Neither field is present as a string - this is an inner node
|
|
503
|
+
return False
|
|
404
504
|
|
|
405
505
|
|
|
406
506
|
def _validate_and_create_leaf(
|
|
@@ -424,8 +524,10 @@ def _validate_and_create_leaf(
|
|
|
424
524
|
leaf_source = value_dict.get("source")
|
|
425
525
|
|
|
426
526
|
# This is supposed to be a leaf - validate required fields first.
|
|
427
|
-
|
|
428
|
-
|
|
527
|
+
# Type must be a string (not None, not empty, and not other types like bool/int)
|
|
528
|
+
type_invalid = _is_empty_or_none(leaf_type) or not isinstance(leaf_type, str)
|
|
529
|
+
# Source must be a string (not None, not empty, and not other types like bool/int)
|
|
530
|
+
source_invalid = _is_empty_or_none(leaf_source) or not isinstance(leaf_source, str)
|
|
429
531
|
|
|
430
532
|
if type_invalid:
|
|
431
533
|
problems.append(
|
|
@@ -446,6 +548,9 @@ def _validate_and_create_leaf(
|
|
|
446
548
|
if type_invalid or source_invalid:
|
|
447
549
|
return None
|
|
448
550
|
|
|
551
|
+
# Note: Check for children is done in _determine_node_type_and_validate()
|
|
552
|
+
# before this function is called, so we don't need to check here.
|
|
553
|
+
|
|
449
554
|
# Convert to string before lookup to handle cases where the JSON parser
|
|
450
555
|
# might return non-string types (though this shouldn't happen with valid JSON).
|
|
451
556
|
# This ensures type safety and consistent behavior.
|
|
@@ -463,7 +568,11 @@ def _validate_and_create_leaf(
|
|
|
463
568
|
# to ensure element homogeneity (e.g. list[str], list[int]).
|
|
464
569
|
if expected_type is list:
|
|
465
570
|
item_type_name = value_dict.get("item_type")
|
|
466
|
-
|
|
571
|
+
# Item type must be a string (not None, not empty, and not other types
|
|
572
|
+
# like bool/int)
|
|
573
|
+
item_type_invalid = _is_empty_or_none(item_type_name) or not isinstance(
|
|
574
|
+
item_type_name, str
|
|
575
|
+
)
|
|
467
576
|
if item_type_invalid:
|
|
468
577
|
problems.append(
|
|
469
578
|
_SchemaProblem(
|
|
@@ -556,12 +665,25 @@ def _compile_schema_tree(
|
|
|
556
665
|
# might be a read-only Mapping (e.g., from JSON parsing).
|
|
557
666
|
value_dict = dict(value)
|
|
558
667
|
|
|
559
|
-
|
|
668
|
+
# Determine node type and validate structure
|
|
669
|
+
# (checks for mixed nodes, empty nodes, etc.)
|
|
670
|
+
node_type, is_valid = _determine_node_type_and_validate(
|
|
671
|
+
value_dict, path, key, problems
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
if not is_valid:
|
|
675
|
+
# Node has validation errors, skip processing but continue with other nodes
|
|
676
|
+
continue
|
|
677
|
+
|
|
678
|
+
if node_type == "leaf":
|
|
679
|
+
# Process as leaf node
|
|
560
680
|
leaf = _validate_and_create_leaf(value_dict, path, key, problems)
|
|
561
681
|
if leaf is not None:
|
|
562
682
|
yield leaf
|
|
563
|
-
else:
|
|
564
|
-
#
|
|
683
|
+
else: # node_type == "inner"
|
|
684
|
+
# Process as inner node - recurse into children
|
|
685
|
+
# Note: node_type can only be "leaf" or "inner" when is_valid is True.
|
|
686
|
+
# If is_valid is False, we already continue above.
|
|
565
687
|
for child_leaf in _compile_schema_tree(value_dict, path + (key,), problems):
|
|
566
688
|
yield child_leaf
|
|
567
689
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|