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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: logging-objects-with-schema
3
- Version: 0.2.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 without `type` and `source` fields (used for nesting).
217
- - **Leaf nodes**: Objects with both `type` and `source` fields. A valid leaf
218
- must have both fields present and non-empty.
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 without `type` and `source` fields (used for nesting).
184
- - **Leaf nodes**: Objects with both `type` and `source` fields. A valid leaf
185
- must have both fields present and non-empty.
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.2.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
- skips = [ "B101", "B601" ]
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" ]
@@ -42,7 +42,7 @@ def _create_validation_error_json(field: str, error: str, value: Any) -> str:
42
42
 
43
43
 
44
44
  def _validate_list_value(
45
- value: list,
45
+ value: list[Any],
46
46
  source: str,
47
47
  item_expected_type: type | None,
48
48
  ) -> _DataProblem | None:
@@ -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 either 'type' or 'source' field.
390
- Inner nodes have neither of these fields.
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
- return value_dict.get("type") is not None or value_dict.get("source") is not None
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
- type_invalid = _is_empty_or_none(leaf_type)
428
- source_invalid = _is_empty_or_none(leaf_source)
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
- item_type_invalid = _is_empty_or_none(item_type_name)
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
- if _is_leaf_node(value_dict):
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
- # This is an inner node; recurse into children.
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