logging-objects-with-schema 0.1.0__tar.gz → 0.1.1__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.1.0 → logging_objects_with_schema-0.1.1}/PKG-INFO +23 -1
- {logging_objects_with_schema-0.1.0 → logging_objects_with_schema-0.1.1}/README.md +22 -0
- logging_objects_with_schema-0.1.1/pyproject.toml +97 -0
- {logging_objects_with_schema-0.1.0 → logging_objects_with_schema-0.1.1}/src/logging_objects_with_schema/errors.py +4 -5
- logging_objects_with_schema-0.1.1/src/logging_objects_with_schema/schema_applier.py +254 -0
- {logging_objects_with_schema-0.1.0 → logging_objects_with_schema-0.1.1}/src/logging_objects_with_schema/schema_loader.py +292 -142
- {logging_objects_with_schema-0.1.0 → logging_objects_with_schema-0.1.1}/src/logging_objects_with_schema/schema_logger.py +24 -3
- logging_objects_with_schema-0.1.0/pyproject.toml +0 -96
- logging_objects_with_schema-0.1.0/src/logging_objects_with_schema/schema_applier.py +0 -184
- {logging_objects_with_schema-0.1.0 → logging_objects_with_schema-0.1.1}/LICENSE +0 -0
- {logging_objects_with_schema-0.1.0 → logging_objects_with_schema-0.1.1}/src/logging_objects_with_schema/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: logging-objects-with-schema
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
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
|
|
@@ -177,6 +177,13 @@ except SchemaValidationError as e:
|
|
|
177
177
|
print(f" - {problem.message}")
|
|
178
178
|
# Decide whether to abort or continue with default logger
|
|
179
179
|
raise
|
|
180
|
+
except (OSError, ValueError, RuntimeError) as e:
|
|
181
|
+
# System-level errors (e.g., inaccessible working directory when os.getcwd()
|
|
182
|
+
# fails, lock failures, path resolution issues) are raised directly, not
|
|
183
|
+
# wrapped in SchemaValidationError. Note: OSError when reading the schema
|
|
184
|
+
# file is converted to SchemaValidationError and handled above.
|
|
185
|
+
print(f"System error during logger initialization: {e}")
|
|
186
|
+
raise
|
|
180
187
|
|
|
181
188
|
# When logging, handle DataValidationError
|
|
182
189
|
try:
|
|
@@ -356,6 +363,21 @@ consistent type expectations when reusing a `source` field.
|
|
|
356
363
|
The application decides whether `SchemaValidationError` is a fatal error that
|
|
357
364
|
should abort startup, or whether it should be logged and ignored.
|
|
358
365
|
|
|
366
|
+
**Note**: In rare cases, system-level errors may be raised directly instead of
|
|
367
|
+
being wrapped in `SchemaValidationError`. Specifically:
|
|
368
|
+
|
|
369
|
+
- `OSError` when the current working directory is inaccessible or deleted (e.g.,
|
|
370
|
+
when `os.getcwd()` fails) is propagated as-is to indicate environmental issues.
|
|
371
|
+
However, `OSError` that occurs when reading the schema file (e.g., permission
|
|
372
|
+
denied, I/O errors) is converted to `SchemaValidationError` and reported as a
|
|
373
|
+
schema problem.
|
|
374
|
+
- `ValueError` when path resolution fails due to invalid characters or malformed
|
|
375
|
+
paths during schema file discovery is propagated as-is.
|
|
376
|
+
- `RuntimeError` when thread lock acquisition fails is propagated as-is.
|
|
377
|
+
|
|
378
|
+
Applications should handle these exceptions separately if they need to
|
|
379
|
+
distinguish between schema validation failures and system-level errors.
|
|
380
|
+
|
|
359
381
|
## Schema caching and thread safety
|
|
360
382
|
|
|
361
383
|
- The library caches compiled schemas to avoid recompiling the same schema file
|
|
@@ -144,6 +144,13 @@ except SchemaValidationError as e:
|
|
|
144
144
|
print(f" - {problem.message}")
|
|
145
145
|
# Decide whether to abort or continue with default logger
|
|
146
146
|
raise
|
|
147
|
+
except (OSError, ValueError, RuntimeError) as e:
|
|
148
|
+
# System-level errors (e.g., inaccessible working directory when os.getcwd()
|
|
149
|
+
# fails, lock failures, path resolution issues) are raised directly, not
|
|
150
|
+
# wrapped in SchemaValidationError. Note: OSError when reading the schema
|
|
151
|
+
# file is converted to SchemaValidationError and handled above.
|
|
152
|
+
print(f"System error during logger initialization: {e}")
|
|
153
|
+
raise
|
|
147
154
|
|
|
148
155
|
# When logging, handle DataValidationError
|
|
149
156
|
try:
|
|
@@ -323,6 +330,21 @@ consistent type expectations when reusing a `source` field.
|
|
|
323
330
|
The application decides whether `SchemaValidationError` is a fatal error that
|
|
324
331
|
should abort startup, or whether it should be logged and ignored.
|
|
325
332
|
|
|
333
|
+
**Note**: In rare cases, system-level errors may be raised directly instead of
|
|
334
|
+
being wrapped in `SchemaValidationError`. Specifically:
|
|
335
|
+
|
|
336
|
+
- `OSError` when the current working directory is inaccessible or deleted (e.g.,
|
|
337
|
+
when `os.getcwd()` fails) is propagated as-is to indicate environmental issues.
|
|
338
|
+
However, `OSError` that occurs when reading the schema file (e.g., permission
|
|
339
|
+
denied, I/O errors) is converted to `SchemaValidationError` and reported as a
|
|
340
|
+
schema problem.
|
|
341
|
+
- `ValueError` when path resolution fails due to invalid characters or malformed
|
|
342
|
+
paths during schema file discovery is propagated as-is.
|
|
343
|
+
- `RuntimeError` when thread lock acquisition fails is propagated as-is.
|
|
344
|
+
|
|
345
|
+
Applications should handle these exceptions separately if they need to
|
|
346
|
+
distinguish between schema validation failures and system-level errors.
|
|
347
|
+
|
|
326
348
|
## Schema caching and thread safety
|
|
327
349
|
|
|
328
350
|
- The library caches compiled schemas to avoid recompiling the same schema file
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = [ "uv_build >= 0.9.11, <0.10.0" ]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "logging-objects-with-schema"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Proxy logging wrapper that validates extra fields against a JSON schema."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dependencies = [ ]
|
|
12
|
+
license = "Apache-2.0"
|
|
13
|
+
license-files = [ "LICENSE" ]
|
|
14
|
+
keywords = [
|
|
15
|
+
"logging",
|
|
16
|
+
"schema",
|
|
17
|
+
"validation",
|
|
18
|
+
"structured-logging",
|
|
19
|
+
"structured-logs"
|
|
20
|
+
]
|
|
21
|
+
classifiers = [
|
|
22
|
+
"Programming Language :: Python :: 3",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Programming Language :: Python :: 3.14",
|
|
28
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
29
|
+
"Intended Audience :: Developers",
|
|
30
|
+
"Topic :: System :: Logging"
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[[project.authors]]
|
|
34
|
+
name = "Dmitrii Safronov"
|
|
35
|
+
email = "dhameoelin+logging-objects-with-schema@gmail.com"
|
|
36
|
+
|
|
37
|
+
[project.optional-dependencies]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=7.0.0",
|
|
40
|
+
"pytest-cov>=4.0.0",
|
|
41
|
+
"black>=23.0.0",
|
|
42
|
+
"isort>=5.12.0",
|
|
43
|
+
"flake8>=6.0.0",
|
|
44
|
+
"flake8-pyproject>=1.2.3",
|
|
45
|
+
"mypy>=1.18.1",
|
|
46
|
+
"bandit>=1.8.6",
|
|
47
|
+
"pre-commit>=3.5.0"
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[project.urls]
|
|
51
|
+
Source = "https://github.com/disafronov/python-logging-objects-with-schema"
|
|
52
|
+
Issues = "https://github.com/disafronov/python-logging-objects-with-schema/issues"
|
|
53
|
+
|
|
54
|
+
[tool.isort]
|
|
55
|
+
profile = "black"
|
|
56
|
+
line_length = 88
|
|
57
|
+
multi_line_output = 3
|
|
58
|
+
|
|
59
|
+
[tool.black]
|
|
60
|
+
line-length = 88
|
|
61
|
+
include = "\\.pyi?$"
|
|
62
|
+
extend-exclude = """
|
|
63
|
+
/(
|
|
64
|
+
\\.eggs
|
|
65
|
+
| \\.git
|
|
66
|
+
| \\.hg
|
|
67
|
+
| \\.mypy_cache
|
|
68
|
+
| \\.tox
|
|
69
|
+
| \\.venv
|
|
70
|
+
| build
|
|
71
|
+
| dist
|
|
72
|
+
)/
|
|
73
|
+
"""
|
|
74
|
+
|
|
75
|
+
[tool.flake8]
|
|
76
|
+
max-line-length = 88
|
|
77
|
+
extend-ignore = [ "E203", "W503" ]
|
|
78
|
+
exclude = [ ".venv", "__pycache__", ".git", "htmlcov" ]
|
|
79
|
+
|
|
80
|
+
[tool.mypy]
|
|
81
|
+
exclude = "tests/"
|
|
82
|
+
warn_return_any = true
|
|
83
|
+
warn_unused_configs = true
|
|
84
|
+
disallow_untyped_defs = true
|
|
85
|
+
disallow_incomplete_defs = true
|
|
86
|
+
check_untyped_defs = true
|
|
87
|
+
disallow_untyped_decorators = true
|
|
88
|
+
no_implicit_optional = true
|
|
89
|
+
warn_redundant_casts = true
|
|
90
|
+
warn_unused_ignores = true
|
|
91
|
+
warn_no_return = true
|
|
92
|
+
warn_unreachable = true
|
|
93
|
+
strict_equality = true
|
|
94
|
+
|
|
95
|
+
[tool.bandit]
|
|
96
|
+
skips = [ "B101", "B601" ]
|
|
97
|
+
exclude_dirs = [ ".venv", "__pycache__", ".git", "htmlcov" ]
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
from dataclasses import dataclass
|
|
6
|
-
from typing import List
|
|
7
6
|
|
|
8
7
|
|
|
9
8
|
@dataclass
|
|
@@ -40,10 +39,10 @@ class SchemaValidationError(Exception):
|
|
|
40
39
|
"""
|
|
41
40
|
|
|
42
41
|
def __init__(
|
|
43
|
-
self, message: str, problems:
|
|
42
|
+
self, message: str, problems: list[SchemaProblem] | None = None
|
|
44
43
|
) -> None:
|
|
45
44
|
super().__init__(message)
|
|
46
|
-
self.problems:
|
|
45
|
+
self.problems: list[SchemaProblem] = problems or []
|
|
47
46
|
|
|
48
47
|
|
|
49
48
|
@dataclass
|
|
@@ -80,6 +79,6 @@ class DataValidationError(Exception):
|
|
|
80
79
|
... # Note: valid fields were already logged before this exception
|
|
81
80
|
"""
|
|
82
81
|
|
|
83
|
-
def __init__(self, message: str, problems:
|
|
82
|
+
def __init__(self, message: str, problems: list[DataProblem] | None = None) -> None:
|
|
84
83
|
super().__init__(message)
|
|
85
|
-
self.problems:
|
|
84
|
+
self.problems: list[DataProblem] = problems or []
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"""Internal schema application logic for logging_objects_with_schema.
|
|
2
|
+
|
|
3
|
+
This module provides the core function for applying a compiled schema
|
|
4
|
+
to user-provided extra fields, used by SchemaLogger.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from typing import Any, Mapping, MutableMapping
|
|
11
|
+
|
|
12
|
+
from .errors import DataProblem
|
|
13
|
+
from .schema_loader import CompiledSchema, SchemaLeaf
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _validate_list_value(
|
|
17
|
+
value: list,
|
|
18
|
+
source: str,
|
|
19
|
+
item_expected_type: type | None,
|
|
20
|
+
) -> DataProblem | None:
|
|
21
|
+
"""Validate that a list value matches the expected item type.
|
|
22
|
+
|
|
23
|
+
Validates that all elements in the list have the exact type declared by
|
|
24
|
+
``item_expected_type``. Empty lists are always considered valid.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
value: The list value to validate.
|
|
28
|
+
source: The source field name (for error messages).
|
|
29
|
+
item_expected_type: Expected type for list elements. Must not be None
|
|
30
|
+
for list-typed leaves.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
DataProblem if validation fails, None if validation succeeds.
|
|
34
|
+
"""
|
|
35
|
+
if item_expected_type is None:
|
|
36
|
+
return DataProblem(
|
|
37
|
+
f"Field '{source}' is declared as list in schema but "
|
|
38
|
+
f"has no item type configured",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
if len(value) == 0:
|
|
42
|
+
# Empty lists are always valid
|
|
43
|
+
return None
|
|
44
|
+
|
|
45
|
+
invalid_item_types = {
|
|
46
|
+
type(item).__name__ for item in value if type(item) is not item_expected_type
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if invalid_item_types:
|
|
50
|
+
return DataProblem(
|
|
51
|
+
f"Field '{source}' is a list but contains elements "
|
|
52
|
+
f"with types {sorted(invalid_item_types)}; "
|
|
53
|
+
f"expected all elements to be of type "
|
|
54
|
+
f"{item_expected_type.__name__}",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
return None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _set_nested_value(
|
|
61
|
+
target: MutableMapping[str, Any],
|
|
62
|
+
path: tuple[str, ...],
|
|
63
|
+
value: Any,
|
|
64
|
+
) -> None:
|
|
65
|
+
"""Set a value in a nested dictionary structure following the given path.
|
|
66
|
+
|
|
67
|
+
Creates intermediate dictionaries as needed. The last element of the path
|
|
68
|
+
is used as the final key for the value.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
target: The root dictionary to modify.
|
|
72
|
+
path: Tuple of keys representing the path to the target location.
|
|
73
|
+
value: The value to set at the target location.
|
|
74
|
+
"""
|
|
75
|
+
current = target
|
|
76
|
+
for key in path[:-1]:
|
|
77
|
+
child = current.get(key)
|
|
78
|
+
if not isinstance(child, dict):
|
|
79
|
+
child = {}
|
|
80
|
+
current[key] = child
|
|
81
|
+
current = child
|
|
82
|
+
|
|
83
|
+
current[path[-1]] = value
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _validate_and_apply_leaf(
|
|
87
|
+
leaf: SchemaLeaf,
|
|
88
|
+
value: Any,
|
|
89
|
+
source: str,
|
|
90
|
+
extra: MutableMapping[str, Any],
|
|
91
|
+
problems: list[DataProblem],
|
|
92
|
+
) -> None:
|
|
93
|
+
"""Validate a value against a schema leaf and apply it if valid.
|
|
94
|
+
|
|
95
|
+
Performs strict type checking and list validation. If validation passes,
|
|
96
|
+
the value is written to the target location in the extra dictionary.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
leaf: The schema leaf to validate against.
|
|
100
|
+
value: The value to validate and apply.
|
|
101
|
+
source: The source field name (for error messages).
|
|
102
|
+
extra: The target dictionary to write the value to if validation passes.
|
|
103
|
+
problems: List to append validation problems to.
|
|
104
|
+
"""
|
|
105
|
+
# Use strict type checking (type() is) instead of isinstance() to
|
|
106
|
+
# prevent bool values from passing validation for int types (since
|
|
107
|
+
# bool is a subclass of int). This ensures that the actual
|
|
108
|
+
# runtime type matches the schema type exactly.
|
|
109
|
+
if type(value) is not leaf.expected_type:
|
|
110
|
+
problems.append(
|
|
111
|
+
DataProblem(
|
|
112
|
+
f"Field '{source}' has type {type(value).__name__}, "
|
|
113
|
+
f"expected {leaf.expected_type.__name__}",
|
|
114
|
+
),
|
|
115
|
+
)
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# For lists, validate that all elements strictly match the declared
|
|
119
|
+
# item_expected_type (homogeneous primitive list).
|
|
120
|
+
# Note: isinstance() check is needed for type narrowing (mypy), even though
|
|
121
|
+
# type(value) is list is already guaranteed by the check above.
|
|
122
|
+
if leaf.expected_type is list and isinstance(value, list):
|
|
123
|
+
list_problem = _validate_list_value(value, source, leaf.item_expected_type)
|
|
124
|
+
if list_problem is not None:
|
|
125
|
+
problems.append(list_problem)
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
_set_nested_value(extra, leaf.path, value)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _strip_empty(node: Any) -> Any:
|
|
132
|
+
"""Remove empty dictionaries and None values from a nested structure.
|
|
133
|
+
|
|
134
|
+
Recursively walks through a dictionary structure and removes:
|
|
135
|
+
- Empty dictionaries ({})
|
|
136
|
+
- None values
|
|
137
|
+
|
|
138
|
+
This helper is used by ``_apply_schema_internal`` on the final payload
|
|
139
|
+
to avoid leaving empty containers created during schema application.
|
|
140
|
+
|
|
141
|
+
Note:
|
|
142
|
+
This function is part of the internal implementation details and is
|
|
143
|
+
not considered a public API. Its signature and behaviour may change
|
|
144
|
+
between releases without preserving backward compatibility.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
node: The node to process (can be dict, list, or any other type).
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
The cleaned structure with empty dicts and None values removed.
|
|
151
|
+
"""
|
|
152
|
+
if isinstance(node, dict):
|
|
153
|
+
cleaned = {k: _strip_empty(v) for k, v in node.items()}
|
|
154
|
+
return {k: v for k, v in cleaned.items() if v != {} and v is not None}
|
|
155
|
+
return node
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _apply_schema_internal(
|
|
159
|
+
compiled: CompiledSchema,
|
|
160
|
+
extra_values: Mapping[str, Any],
|
|
161
|
+
) -> tuple[dict[str, Any], list[DataProblem]]:
|
|
162
|
+
"""Internal function to build structured ``extra`` from compiled schema.
|
|
163
|
+
|
|
164
|
+
The function applies a :class:`CompiledSchema` to user-provided ``extra``
|
|
165
|
+
values and returns a tuple ``(structured_extra, problems)`` where:
|
|
166
|
+
|
|
167
|
+
- ``structured_extra`` is a nested dictionary that follows the schema
|
|
168
|
+
structure and contains only fields that passed validation;
|
|
169
|
+
- ``problems`` is a list of :class:`DataProblem` describing all data
|
|
170
|
+
issues observed during processing.
|
|
171
|
+
|
|
172
|
+
Behaviour summary:
|
|
173
|
+
|
|
174
|
+
- If the compiled schema is effectively empty (no valid leaves),
|
|
175
|
+
all fields from ``extra_values`` are treated as redundant: the returned
|
|
176
|
+
payload is empty, and a :class:`DataProblem` is created for each field.
|
|
177
|
+
- For each ``source`` mentioned in the schema when there are valid leaves:
|
|
178
|
+
- if the source is missing from ``extra_values``, it is silently skipped;
|
|
179
|
+
- if the corresponding value is ``None``, a ``DataProblem`` is recorded
|
|
180
|
+
and the value is not written to the payload.
|
|
181
|
+
- Type checks are strict: the runtime type must exactly match the declared
|
|
182
|
+
Python type (``type(value) is leaf.expected_type``). This prevents
|
|
183
|
+
``bool`` from being accepted where ``int`` is expected.
|
|
184
|
+
- For list-typed leaves:
|
|
185
|
+
- empty lists are accepted;
|
|
186
|
+
- all elements must have the exact type declared by the leaf
|
|
187
|
+
``item_expected_type`` (for example, list[str], list[int]);
|
|
188
|
+
- non-primitive elements and elements of a different primitive type are
|
|
189
|
+
rejected with a ``DataProblem`` and the list value is not written.
|
|
190
|
+
- Redundant fields from ``extra_values`` (not referenced by any leaf
|
|
191
|
+
``source``) are always reported as problems: each such field generates
|
|
192
|
+
a :class:`DataProblem` indicating that it is not defined in the schema.
|
|
193
|
+
- A single ``source`` may be used by multiple leaves. The value is
|
|
194
|
+
validated independently for each leaf and written only to locations
|
|
195
|
+
where the type matches; mismatched locations produce ``DataProblem``
|
|
196
|
+
entries, but do not affect successful locations.
|
|
197
|
+
|
|
198
|
+
The function itself does not raise ``DataValidationError``; it only
|
|
199
|
+
accumulates :class:`DataProblem` instances for the caller to handle.
|
|
200
|
+
|
|
201
|
+
Note:
|
|
202
|
+
This function is used internally by :class:`SchemaLogger` and is not
|
|
203
|
+
considered part of the public API. Its signature and behaviour may
|
|
204
|
+
change between releases without preserving backward compatibility.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Tuple of (structured_extra, list[DataProblem]).
|
|
208
|
+
"""
|
|
209
|
+
extra: dict[str, Any] = {}
|
|
210
|
+
problems: list[DataProblem] = []
|
|
211
|
+
|
|
212
|
+
# Group leaves by source for efficient processing
|
|
213
|
+
source_to_leaves: dict[str, list[SchemaLeaf]] = defaultdict(list)
|
|
214
|
+
for leaf in compiled.leaves:
|
|
215
|
+
source_to_leaves[leaf.source].append(leaf)
|
|
216
|
+
|
|
217
|
+
used_sources = set(source_to_leaves.keys())
|
|
218
|
+
|
|
219
|
+
for source, leaves in source_to_leaves.items():
|
|
220
|
+
if source not in extra_values:
|
|
221
|
+
continue
|
|
222
|
+
|
|
223
|
+
value = extra_values[source]
|
|
224
|
+
|
|
225
|
+
# Check for None values explicitly (None values are not allowed)
|
|
226
|
+
# This check must be done once per source, not once per leaf
|
|
227
|
+
if value is None:
|
|
228
|
+
problems.append(
|
|
229
|
+
DataProblem(
|
|
230
|
+
f"Field '{source}' is None, but None values are not allowed",
|
|
231
|
+
),
|
|
232
|
+
)
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
for leaf in leaves:
|
|
236
|
+
_validate_and_apply_leaf(leaf, value, source, extra, problems)
|
|
237
|
+
|
|
238
|
+
# Report redundant fields for any keys not referenced by schema leaves.
|
|
239
|
+
# Optimization: if schema is empty (no used_sources), all fields are redundant,
|
|
240
|
+
# so we can skip the membership check for each key.
|
|
241
|
+
redundant_keys = (
|
|
242
|
+
extra_values.keys()
|
|
243
|
+
if not used_sources
|
|
244
|
+
else (key for key in extra_values.keys() if key not in used_sources)
|
|
245
|
+
)
|
|
246
|
+
for key in redundant_keys:
|
|
247
|
+
problems.append(
|
|
248
|
+
DataProblem(
|
|
249
|
+
f"Field '{key}' is not defined in schema and will be ignored",
|
|
250
|
+
),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
cleaned_extra = _strip_empty(extra)
|
|
254
|
+
return cleaned_extra, problems
|