sdcvalidator 4.2.0__tar.gz → 4.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.
- {sdcvalidator-4.2.0/src/sdcvalidator.egg-info → sdcvalidator-4.3.0}/PKG-INFO +2 -2
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/pyproject.toml +3 -2
- sdcvalidator-4.3.0/src/sdcvalidator/__init__.py +65 -0
- sdcvalidator-4.3.0/src/sdcvalidator/constants.py +162 -0
- sdcvalidator-4.3.0/src/sdcvalidator/error_mapper.py +343 -0
- sdcvalidator-4.3.0/src/sdcvalidator/exceptions.py +63 -0
- sdcvalidator-4.3.0/src/sdcvalidator/instance_modifier.py +424 -0
- sdcvalidator-4.3.0/src/sdcvalidator/mcp_server.py +295 -0
- sdcvalidator-4.3.0/src/sdcvalidator/validator.py +393 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0/src/sdcvalidator.egg-info}/PKG-INFO +2 -2
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/SOURCES.txt +8 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/entry_points.txt +1 -0
- sdcvalidator-4.3.0/tests/test_error_mapper.py +153 -0
- sdcvalidator-4.3.0/tests/test_instance_modifier.py +75 -0
- sdcvalidator-4.3.0/tests/test_mcp_server.py +139 -0
- sdcvalidator-4.3.0/tests/test_recovery.py +149 -0
- sdcvalidator-4.2.0/src/sdcvalidator/__init__.py +0 -38
- sdcvalidator-4.2.0/src/sdcvalidator/constants.py +0 -62
- sdcvalidator-4.2.0/src/sdcvalidator/validator.py +0 -174
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/LICENSE +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/README.md +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/setup.cfg +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator/cli.py +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator/converters.py +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator/error_classifier.py +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator/schema_checker.py +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/requires.txt +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/top_level.txt +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/tests/test_converters.py +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/tests/test_error_classifier.py +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/tests/test_schema_checker.py +0 -0
- {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/tests/test_validator.py +0 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcvalidator
|
|
3
|
-
Version: 4.
|
|
4
|
-
Summary: SDC4 structural validator — thin wrapper over xmlschema
|
|
3
|
+
Version: 4.3.0
|
|
4
|
+
Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
|
|
5
5
|
Author: Semantic Data Charter Foundation
|
|
6
6
|
License-Expression: Apache-2.0
|
|
7
7
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sdcvalidator"
|
|
7
|
-
version = "4.
|
|
8
|
-
description = "SDC4 structural validator — thin wrapper over xmlschema
|
|
7
|
+
version = "4.3.0"
|
|
8
|
+
description = "SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema"
|
|
9
9
|
license = "Apache-2.0"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
readme = "README.md"
|
|
@@ -30,6 +30,7 @@ dev = ["pytest>=7.0", "pytest-cov"]
|
|
|
30
30
|
sdcvalidate = "sdcvalidator.cli:validate_main"
|
|
31
31
|
sdcvalidator-xml2json = "sdcvalidator.cli:xml2json_main"
|
|
32
32
|
sdcvalidator-json2xml = "sdcvalidator.cli:json2xml_main"
|
|
33
|
+
sdcvalidator-mcp = "sdcvalidator.mcp_server:main"
|
|
33
34
|
|
|
34
35
|
[tool.setuptools.packages.find]
|
|
35
36
|
where = ["src"]
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Semantic Data Charter Foundation
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
"""
|
|
11
|
+
sdcvalidator — SDC4 structural validator with ExceptionalValue recovery.
|
|
12
|
+
|
|
13
|
+
Thin wrapper over xmlschema with two-tier error classification and the SDC4
|
|
14
|
+
"quarantine-and-tag" recovery pattern (ISO 21090 ExceptionalValues).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
__version__ = "4.3.0"
|
|
18
|
+
|
|
19
|
+
# etree_tostring is re-exported from xmlschema so callers can serialize the
|
|
20
|
+
# ElementTree returned by validate_with_recovery() without importing xmlschema
|
|
21
|
+
# directly. The import location moved across xmlschema versions; try both.
|
|
22
|
+
try: # xmlschema >= 2.x
|
|
23
|
+
from xmlschema import etree_tostring
|
|
24
|
+
except ImportError: # pragma: no cover - fallback for older layouts
|
|
25
|
+
from xmlschema.etree import etree_tostring
|
|
26
|
+
|
|
27
|
+
from .validator import (
|
|
28
|
+
SDC4Validator,
|
|
29
|
+
ValidationResult,
|
|
30
|
+
validate_with_recovery,
|
|
31
|
+
)
|
|
32
|
+
from .error_classifier import ErrorClassifier
|
|
33
|
+
from .error_mapper import ErrorMapper
|
|
34
|
+
from .instance_modifier import InstanceModifier
|
|
35
|
+
from .exceptions import SDC4StructuralValidationError
|
|
36
|
+
from .constants import (
|
|
37
|
+
ErrorTier,
|
|
38
|
+
ExceptionalValueType,
|
|
39
|
+
EXCEPTIONAL_VALUE_TYPES,
|
|
40
|
+
)
|
|
41
|
+
from .schema_checker import (
|
|
42
|
+
validate_sdc4_schema_compliance,
|
|
43
|
+
assert_sdc4_schema_compliance,
|
|
44
|
+
SDC4SchemaValidationError,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
# Intentionally NOT exporting XMLSchema11 — SDCStudio's fallback import handles this.
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
"__version__",
|
|
51
|
+
"SDC4Validator",
|
|
52
|
+
"ValidationResult",
|
|
53
|
+
"ErrorClassifier",
|
|
54
|
+
"ErrorMapper",
|
|
55
|
+
"InstanceModifier",
|
|
56
|
+
"ErrorTier",
|
|
57
|
+
"ExceptionalValueType",
|
|
58
|
+
"EXCEPTIONAL_VALUE_TYPES",
|
|
59
|
+
"SDC4StructuralValidationError",
|
|
60
|
+
"validate_with_recovery",
|
|
61
|
+
"etree_tostring",
|
|
62
|
+
"validate_sdc4_schema_compliance",
|
|
63
|
+
"assert_sdc4_schema_compliance",
|
|
64
|
+
"SDC4SchemaValidationError",
|
|
65
|
+
]
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Semantic Data Charter Foundation
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
"""
|
|
11
|
+
Constants and type definitions for SDC4 validation.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from enum import Enum
|
|
15
|
+
from typing import Dict
|
|
16
|
+
|
|
17
|
+
# SDC4 Namespace URIs
|
|
18
|
+
SDC4_NAMESPACE = "https://semanticdatacharter.com/ns/sdc4/"
|
|
19
|
+
SDC4_META_NAMESPACE = "https://semanticdatacharter.com/ontology/sdc4-meta/"
|
|
20
|
+
XSD_NAMESPACE = "http://www.w3.org/2001/XMLSchema"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ErrorTier(Enum):
|
|
24
|
+
"""
|
|
25
|
+
Two-tier error classification for SDC4 validation.
|
|
26
|
+
|
|
27
|
+
STRUCTURAL (Tier 1): Unknown elements, wrong nesting, cardinality -> REJECT
|
|
28
|
+
SEMANTIC (Tier 2): Type errors, pattern violations, range constraints -> REPORT
|
|
29
|
+
"""
|
|
30
|
+
STRUCTURAL = "structural"
|
|
31
|
+
SEMANTIC = "semantic"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# Data-bearing elements in SDC4 schemas
|
|
35
|
+
DATA_BEARING_ELEMENTS = {
|
|
36
|
+
'xdstring-value',
|
|
37
|
+
'xdcount-value',
|
|
38
|
+
'xdquantity-value',
|
|
39
|
+
'xdboolean-value',
|
|
40
|
+
'xdfile-value',
|
|
41
|
+
'xdlink-value',
|
|
42
|
+
'xdtemporal-value',
|
|
43
|
+
'xdordinal-value',
|
|
44
|
+
'xdratio-value',
|
|
45
|
+
'xdinterval-value',
|
|
46
|
+
'xdtoken-value',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# Structural/metadata elements that should FAIL validation (not recoverable)
|
|
50
|
+
STRUCTURAL_ELEMENTS = {
|
|
51
|
+
'label', # Component labels
|
|
52
|
+
'act', # Audit/control/trust
|
|
53
|
+
'vtb', # Valid time begin
|
|
54
|
+
'vte', # Valid time end
|
|
55
|
+
'tr', # Transaction time
|
|
56
|
+
'modified', # Modification timestamp
|
|
57
|
+
'latitude', # Geographic coordinate
|
|
58
|
+
'longitude', # Geographic coordinate
|
|
59
|
+
'normal-status', # Quantified metadata
|
|
60
|
+
'magnitude-status', # Quantified metadata
|
|
61
|
+
'accuracy_margin', # Quantified metadata
|
|
62
|
+
'precision_digits', # Quantified metadata
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ExceptionalValueType(Enum):
|
|
67
|
+
"""
|
|
68
|
+
SDC4 ExceptionalValue types based on ISO 21090 NULL Flavors.
|
|
69
|
+
|
|
70
|
+
These types indicate why data is missing or invalid. All types inherit
|
|
71
|
+
from ExceptionalValueType in the SDC4 reference model.
|
|
72
|
+
"""
|
|
73
|
+
|
|
74
|
+
# Primary types for validation errors
|
|
75
|
+
INV = ("INV", "Invalid",
|
|
76
|
+
"The value as represented in the instance is not a member of the "
|
|
77
|
+
"set of permitted data values in the constrained value domain of a variable.")
|
|
78
|
+
|
|
79
|
+
OTH = ("OTH", "Other",
|
|
80
|
+
"The actual value is not a member of the permitted data values in the "
|
|
81
|
+
"variable (e.g., when the value of the variable is not by the coding system).")
|
|
82
|
+
|
|
83
|
+
UNC = ("UNC", "Unencoded",
|
|
84
|
+
"No attempt has been made to encode the information correctly but the "
|
|
85
|
+
"raw source information is represented, usually in free text.")
|
|
86
|
+
|
|
87
|
+
# Missing data types
|
|
88
|
+
NI = ("NI", "No Information",
|
|
89
|
+
"The value is exceptional (missing, omitted, incomplete, improper). "
|
|
90
|
+
"No information as to the reason for being an exceptional value is provided. "
|
|
91
|
+
"This is the most general exceptional value and the default.")
|
|
92
|
+
|
|
93
|
+
NA = ("NA", "Not Applicable",
|
|
94
|
+
"No proper value is applicable in this context (e.g., the number of "
|
|
95
|
+
"cigarettes smoked per day by a non-smoker subject).")
|
|
96
|
+
|
|
97
|
+
UNK = ("UNK", "Unknown",
|
|
98
|
+
"A proper value is applicable, but not known.")
|
|
99
|
+
|
|
100
|
+
ASKU = ("ASKU", "Asked but Unknown",
|
|
101
|
+
"Information was sought but not found (e.g., patient was asked but did not know).")
|
|
102
|
+
|
|
103
|
+
ASKR = ("ASKR", "Asked and Refused",
|
|
104
|
+
"Information was sought but refused to be provided (e.g., patient was "
|
|
105
|
+
"asked but refused to answer).")
|
|
106
|
+
|
|
107
|
+
NASK = ("NASK", "Not Asked",
|
|
108
|
+
"This information has not been sought (e.g., patient was not asked).")
|
|
109
|
+
|
|
110
|
+
NAV = ("NAV", "Not Available",
|
|
111
|
+
"This information is not available and the specific reason is not known.")
|
|
112
|
+
|
|
113
|
+
MSK = ("MSK", "Masked",
|
|
114
|
+
"There is information on this item available but it has not been provided "
|
|
115
|
+
"by the sender due to security, privacy or other reasons.")
|
|
116
|
+
|
|
117
|
+
# Special value types
|
|
118
|
+
DER = ("DER", "Derived",
|
|
119
|
+
"An actual value may exist, but it must be derived from the provided "
|
|
120
|
+
"information; usually an expression is provided directly.")
|
|
121
|
+
|
|
122
|
+
PINF = ("PINF", "Positive Infinity", "Positive infinity of numbers.")
|
|
123
|
+
|
|
124
|
+
NINF = ("NINF", "Negative Infinity", "Negative infinity of numbers.")
|
|
125
|
+
|
|
126
|
+
TRC = ("TRC", "Trace",
|
|
127
|
+
"The content is greater or less than zero but too small to be quantified.")
|
|
128
|
+
|
|
129
|
+
QS = ("QS", "Sufficient Quantity",
|
|
130
|
+
"The specific quantity is not known, but is known to non-zero and it is "
|
|
131
|
+
"not specified because it makes up the bulk of the material.")
|
|
132
|
+
|
|
133
|
+
def __init__(self, code: str, name: str, description: str):
|
|
134
|
+
self.code = code
|
|
135
|
+
self.ev_name = name
|
|
136
|
+
self.description = description
|
|
137
|
+
|
|
138
|
+
@classmethod
|
|
139
|
+
def from_code(cls, code: str) -> 'ExceptionalValueType':
|
|
140
|
+
"""Get ExceptionalValueType from its code string."""
|
|
141
|
+
for ev_type in cls:
|
|
142
|
+
if ev_type.code == code:
|
|
143
|
+
return ev_type
|
|
144
|
+
raise ValueError(f"Unknown ExceptionalValue code: {code}")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Mapping of ExceptionalValue types to their codes and names
|
|
148
|
+
EXCEPTIONAL_VALUE_TYPES: Dict[str, tuple] = {
|
|
149
|
+
ev_type.code: (ev_type.ev_name, ev_type.description)
|
|
150
|
+
for ev_type in ExceptionalValueType
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
# All ExceptionalValue code strings (used to detect existing EV elements)
|
|
154
|
+
EXCEPTIONAL_VALUE_CODES = [ev_type.code for ev_type in ExceptionalValueType]
|
|
155
|
+
|
|
156
|
+
# XPath pattern for locating data-bearing elements in SDC4 instances
|
|
157
|
+
XDANY_TYPE_PATTERN = r"xd(?:string|count|quantity|boolean|file|link|temporal|ordinal|ratio|interval)-value"
|
|
158
|
+
|
|
159
|
+
# Element sequence position for ExceptionalValue in XdAnyType
|
|
160
|
+
# Per SDC4 schema: label, act, ExceptionalValue, vtb, vte, tr, modified, latitude, longitude, ...
|
|
161
|
+
EXCEPTIONAL_VALUE_INSERT_AFTER = ['label', 'act']
|
|
162
|
+
EXCEPTIONAL_VALUE_INSERT_BEFORE = ['vtb', 'vte', 'tr', 'modified', 'latitude', 'longitude']
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Semantic Data Charter Foundation
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
"""
|
|
11
|
+
Maps XML Schema validation errors to SDC4 ExceptionalValue types.
|
|
12
|
+
|
|
13
|
+
Tiering (structural vs semantic) is delegated to ErrorClassifier so there is a
|
|
14
|
+
single source of truth for that decision; this module adds only the mapping
|
|
15
|
+
from a semantic error to the appropriate ExceptionalValue type.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from typing import Optional, Callable, Dict, List
|
|
20
|
+
from xmlschema.validators.exceptions import (
|
|
21
|
+
XMLSchemaValidationError,
|
|
22
|
+
XMLSchemaDecodeError,
|
|
23
|
+
XMLSchemaChildrenValidationError,
|
|
24
|
+
)
|
|
25
|
+
from .constants import ExceptionalValueType, STRUCTURAL_ELEMENTS
|
|
26
|
+
from .error_classifier import ErrorClassifier
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ErrorMapper:
|
|
30
|
+
"""
|
|
31
|
+
Maps XMLSchema validation errors to appropriate SDC4 ExceptionalValue types.
|
|
32
|
+
|
|
33
|
+
The mapper uses a rule-based system to classify errors. Rules can be customized
|
|
34
|
+
or extended for domain-specific requirements.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, classifier: Optional[ErrorClassifier] = None):
|
|
38
|
+
"""
|
|
39
|
+
Initialize the error mapper with default rules.
|
|
40
|
+
|
|
41
|
+
:param classifier: Optional ErrorClassifier used for the structural-vs-semantic
|
|
42
|
+
decision. Defaults to a fresh ErrorClassifier so the tiering
|
|
43
|
+
logic stays identical to SDC4Validator.validate().
|
|
44
|
+
"""
|
|
45
|
+
self._classifier = classifier or ErrorClassifier()
|
|
46
|
+
self._rules: List[tuple[Callable, ExceptionalValueType]] = []
|
|
47
|
+
self._register_default_rules()
|
|
48
|
+
|
|
49
|
+
def _register_default_rules(self):
|
|
50
|
+
"""Register the default error mapping rules."""
|
|
51
|
+
# Order matters - more specific rules should come first
|
|
52
|
+
|
|
53
|
+
# Missing required elements/attributes
|
|
54
|
+
self.add_rule(
|
|
55
|
+
lambda err: self._is_missing_required(err),
|
|
56
|
+
ExceptionalValueType.NI
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
# Type violations (wrong data type, invalid format)
|
|
60
|
+
self.add_rule(
|
|
61
|
+
lambda err: self._is_type_violation(err),
|
|
62
|
+
ExceptionalValueType.INV
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
# Pattern, facet, or constraint violations
|
|
66
|
+
self.add_rule(
|
|
67
|
+
lambda err: self._is_constraint_violation(err),
|
|
68
|
+
ExceptionalValueType.INV
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
# Enumeration violations
|
|
72
|
+
self.add_rule(
|
|
73
|
+
lambda err: self._is_enumeration_violation(err),
|
|
74
|
+
ExceptionalValueType.OTH
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
# Unexpected elements/attributes in strict contexts
|
|
78
|
+
self.add_rule(
|
|
79
|
+
lambda err: self._is_unexpected_content(err),
|
|
80
|
+
ExceptionalValueType.NA
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
# Encoding/format errors
|
|
84
|
+
self.add_rule(
|
|
85
|
+
lambda err: self._is_encoding_error(err),
|
|
86
|
+
ExceptionalValueType.UNC
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Default fallback
|
|
90
|
+
self.add_rule(
|
|
91
|
+
lambda err: True, # Matches everything
|
|
92
|
+
ExceptionalValueType.NI
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def add_rule(self, condition: Callable[[XMLSchemaValidationError], bool],
|
|
96
|
+
ev_type: ExceptionalValueType):
|
|
97
|
+
"""
|
|
98
|
+
Add a custom mapping rule.
|
|
99
|
+
|
|
100
|
+
:param condition: A callable that takes an error and returns True if the rule matches.
|
|
101
|
+
:param ev_type: The ExceptionalValueType to return when the rule matches.
|
|
102
|
+
"""
|
|
103
|
+
self._rules.append((condition, ev_type))
|
|
104
|
+
|
|
105
|
+
def map_error(self, error: XMLSchemaValidationError) -> Optional[ExceptionalValueType]:
|
|
106
|
+
"""
|
|
107
|
+
Map a validation error to an ExceptionalValue type.
|
|
108
|
+
|
|
109
|
+
Only data-bearing elements (xdstring-value, xdcount-value, etc.) can receive
|
|
110
|
+
ExceptionalValue tags. Structural/metadata elements (label, vtb, vte, tr, etc.)
|
|
111
|
+
should fail validation.
|
|
112
|
+
|
|
113
|
+
:param error: The XML Schema validation error.
|
|
114
|
+
:return: The appropriate ExceptionalValueType, or None if this is a structural
|
|
115
|
+
element that should fail validation.
|
|
116
|
+
"""
|
|
117
|
+
# Extract element name from the error path
|
|
118
|
+
element_name = self._extract_element_name(error.path)
|
|
119
|
+
|
|
120
|
+
# Check if this is a structural element - these should fail validation
|
|
121
|
+
if element_name and element_name in STRUCTURAL_ELEMENTS:
|
|
122
|
+
# Don't map to ExceptionalValue - let validation fail
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
# For SDC4 data-bearing elements or unknown elements, map to appropriate ExceptionalValue
|
|
126
|
+
# Note: We're permissive here - only STRUCTURAL_ELEMENTS fail validation
|
|
127
|
+
# This allows the system to work with custom element names and future SDC versions
|
|
128
|
+
for condition, ev_type in self._rules:
|
|
129
|
+
if condition(error):
|
|
130
|
+
return ev_type
|
|
131
|
+
|
|
132
|
+
# Should never reach here due to default rule, but just in case
|
|
133
|
+
return ExceptionalValueType.NI
|
|
134
|
+
|
|
135
|
+
def is_structural_error(self, error: XMLSchemaValidationError) -> bool:
|
|
136
|
+
"""
|
|
137
|
+
Classify an error as structural (Tier 1) vs semantic (Tier 2).
|
|
138
|
+
|
|
139
|
+
Delegated to ErrorClassifier so the tiering decision is identical to the
|
|
140
|
+
one used by SDC4Validator.validate().
|
|
141
|
+
|
|
142
|
+
:param error: The validation error to classify.
|
|
143
|
+
:return: True if structural (reject), False if semantic (quarantine).
|
|
144
|
+
"""
|
|
145
|
+
return self._classifier.is_structural_error(error)
|
|
146
|
+
|
|
147
|
+
def _extract_element_name(self, xpath: Optional[str]) -> Optional[str]:
|
|
148
|
+
"""
|
|
149
|
+
Extract the element name from an XPath expression.
|
|
150
|
+
|
|
151
|
+
Handles paths like:
|
|
152
|
+
- /DataModel/xdstring-value
|
|
153
|
+
- /ns:DataModel/ns:xdstring-value[1]
|
|
154
|
+
- //xdcount-value
|
|
155
|
+
|
|
156
|
+
:param xpath: The XPath expression from the validation error.
|
|
157
|
+
:return: The local element name (without namespace prefix), or None.
|
|
158
|
+
"""
|
|
159
|
+
if not xpath:
|
|
160
|
+
return None
|
|
161
|
+
|
|
162
|
+
# Get the last path component
|
|
163
|
+
parts = xpath.strip('/').split('/')
|
|
164
|
+
if not parts:
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
last_part = parts[-1]
|
|
168
|
+
|
|
169
|
+
# Remove namespace prefix (e.g., 'sdc4:xdstring-value' -> 'xdstring-value')
|
|
170
|
+
if ':' in last_part:
|
|
171
|
+
last_part = last_part.split(':')[-1]
|
|
172
|
+
|
|
173
|
+
# Remove predicates (e.g., 'xdstring-value[1]' -> 'xdstring-value')
|
|
174
|
+
if '[' in last_part:
|
|
175
|
+
last_part = last_part[:last_part.index('[')]
|
|
176
|
+
|
|
177
|
+
return last_part if last_part else None
|
|
178
|
+
|
|
179
|
+
# =========================================================================
|
|
180
|
+
# Error classification helper methods
|
|
181
|
+
# =========================================================================
|
|
182
|
+
|
|
183
|
+
def _is_missing_required(self, error: XMLSchemaValidationError) -> bool:
|
|
184
|
+
"""Check if error indicates missing required element/attribute."""
|
|
185
|
+
if not error.reason:
|
|
186
|
+
return False
|
|
187
|
+
|
|
188
|
+
reason = error.reason.lower()
|
|
189
|
+
# Use simple substring checks where possible; regex only where needed
|
|
190
|
+
keywords = [
|
|
191
|
+
'missing required',
|
|
192
|
+
'is not complete',
|
|
193
|
+
]
|
|
194
|
+
if any(kw in reason for kw in keywords):
|
|
195
|
+
return True
|
|
196
|
+
|
|
197
|
+
patterns = [
|
|
198
|
+
r'required \S+ is missing',
|
|
199
|
+
r'element \S+ is required',
|
|
200
|
+
r'minimum \S+ is \d+',
|
|
201
|
+
]
|
|
202
|
+
return any(re.search(pattern, reason) for pattern in patterns)
|
|
203
|
+
|
|
204
|
+
def _is_type_violation(self, error: XMLSchemaValidationError) -> bool:
|
|
205
|
+
"""Check if error indicates wrong data type."""
|
|
206
|
+
# Decode errors are always type issues
|
|
207
|
+
if isinstance(error, XMLSchemaDecodeError):
|
|
208
|
+
return True
|
|
209
|
+
|
|
210
|
+
if not error.reason:
|
|
211
|
+
return False
|
|
212
|
+
|
|
213
|
+
reason = error.reason.lower()
|
|
214
|
+
keywords = [
|
|
215
|
+
'not a valid value',
|
|
216
|
+
'invalid value',
|
|
217
|
+
'is not valid for type',
|
|
218
|
+
'cannot be converted',
|
|
219
|
+
'expected type',
|
|
220
|
+
'wrong type',
|
|
221
|
+
'malformed',
|
|
222
|
+
]
|
|
223
|
+
if any(kw in reason for kw in keywords):
|
|
224
|
+
return True
|
|
225
|
+
|
|
226
|
+
patterns = [
|
|
227
|
+
r'type \S+ does not match',
|
|
228
|
+
r'invalid\S* format',
|
|
229
|
+
]
|
|
230
|
+
return any(re.search(pattern, reason) for pattern in patterns)
|
|
231
|
+
|
|
232
|
+
def _is_constraint_violation(self, error: XMLSchemaValidationError) -> bool:
|
|
233
|
+
"""Check if error indicates constraint/facet violation."""
|
|
234
|
+
if not error.reason:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
reason = error.reason.lower()
|
|
238
|
+
keywords = [
|
|
239
|
+
'does not match pattern',
|
|
240
|
+
'length constraint',
|
|
241
|
+
'minlength', 'maxlength',
|
|
242
|
+
'mininclusive', 'maxinclusive',
|
|
243
|
+
'minexclusive', 'maxexclusive',
|
|
244
|
+
'totaldigits', 'fractiondigits',
|
|
245
|
+
'constraint',
|
|
246
|
+
]
|
|
247
|
+
if any(kw in reason for kw in keywords):
|
|
248
|
+
return True
|
|
249
|
+
|
|
250
|
+
patterns = [
|
|
251
|
+
r'pattern\S* not matched',
|
|
252
|
+
r'assertion\S* failed',
|
|
253
|
+
r'exceeds\S* maximum',
|
|
254
|
+
r'below\S* minimum',
|
|
255
|
+
]
|
|
256
|
+
return any(re.search(pattern, reason) for pattern in patterns)
|
|
257
|
+
|
|
258
|
+
def _is_enumeration_violation(self, error: XMLSchemaValidationError) -> bool:
|
|
259
|
+
"""Check if error indicates enumeration violation."""
|
|
260
|
+
if not error.reason:
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
reason = error.reason.lower()
|
|
264
|
+
keywords = [
|
|
265
|
+
'not in enumeration',
|
|
266
|
+
'invalid enumeration',
|
|
267
|
+
]
|
|
268
|
+
if any(kw in reason for kw in keywords):
|
|
269
|
+
return True
|
|
270
|
+
|
|
271
|
+
patterns = [
|
|
272
|
+
r'not\S* allowed value',
|
|
273
|
+
r'not\S* permitted value',
|
|
274
|
+
r'value\S* not\S* allowed',
|
|
275
|
+
]
|
|
276
|
+
return any(re.search(pattern, reason) for pattern in patterns)
|
|
277
|
+
|
|
278
|
+
def _is_unexpected_content(self, error: XMLSchemaValidationError) -> bool:
|
|
279
|
+
"""Check if error indicates unexpected element/attribute."""
|
|
280
|
+
if isinstance(error, XMLSchemaChildrenValidationError):
|
|
281
|
+
# Check if it's an unexpected child element
|
|
282
|
+
if error.invalid_tag is not None:
|
|
283
|
+
return True
|
|
284
|
+
|
|
285
|
+
if not error.reason:
|
|
286
|
+
return False
|
|
287
|
+
|
|
288
|
+
reason = error.reason.lower()
|
|
289
|
+
keywords = [
|
|
290
|
+
'unexpected',
|
|
291
|
+
'not allowed',
|
|
292
|
+
'not permitted',
|
|
293
|
+
'extra element',
|
|
294
|
+
'unknown element',
|
|
295
|
+
'not expected',
|
|
296
|
+
]
|
|
297
|
+
return any(kw in reason for kw in keywords)
|
|
298
|
+
|
|
299
|
+
def _is_encoding_error(self, error: XMLSchemaValidationError) -> bool:
|
|
300
|
+
"""Check if error indicates encoding/format problem."""
|
|
301
|
+
if not error.reason:
|
|
302
|
+
return False
|
|
303
|
+
|
|
304
|
+
reason = error.reason.lower()
|
|
305
|
+
keywords = [
|
|
306
|
+
'encoding error',
|
|
307
|
+
'decode error',
|
|
308
|
+
'invalid character',
|
|
309
|
+
'whitespace',
|
|
310
|
+
]
|
|
311
|
+
if any(kw in reason for kw in keywords):
|
|
312
|
+
return True
|
|
313
|
+
|
|
314
|
+
return bool(re.search(r'character\S* not\S* allowed', reason))
|
|
315
|
+
|
|
316
|
+
def get_error_summary(self, error: XMLSchemaValidationError,
|
|
317
|
+
ev_type: Optional[ExceptionalValueType]) -> Dict[str, str]:
|
|
318
|
+
"""
|
|
319
|
+
Generate a summary of the error mapping.
|
|
320
|
+
|
|
321
|
+
:param error: The validation error.
|
|
322
|
+
:param ev_type: The mapped ExceptionalValueType, or None for structural elements.
|
|
323
|
+
:return: A dictionary with error details.
|
|
324
|
+
"""
|
|
325
|
+
if ev_type is None:
|
|
326
|
+
# Structural/metadata element error - no ExceptionalValue
|
|
327
|
+
return {
|
|
328
|
+
'xpath': error.path or 'unknown',
|
|
329
|
+
'error_type': type(error).__name__,
|
|
330
|
+
'reason': error.reason or 'No reason provided',
|
|
331
|
+
'exceptional_value_type': None,
|
|
332
|
+
'exceptional_value_name': None,
|
|
333
|
+
'description': 'Structural/metadata element - validation fails',
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return {
|
|
337
|
+
'xpath': error.path or 'unknown',
|
|
338
|
+
'error_type': type(error).__name__,
|
|
339
|
+
'reason': error.reason or 'No reason provided',
|
|
340
|
+
'exceptional_value_type': ev_type.code,
|
|
341
|
+
'exceptional_value_name': ev_type.ev_name,
|
|
342
|
+
'description': ev_type.description,
|
|
343
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2025 Semantic Data Charter Foundation
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
"""
|
|
11
|
+
SDC4-specific exceptions for validation.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from typing import List, TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from xmlschema.validators.exceptions import XMLSchemaValidationError
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SDC4StructuralValidationError(Exception):
|
|
21
|
+
"""
|
|
22
|
+
Raised when structural validation fails (Tier 1 rejection).
|
|
23
|
+
|
|
24
|
+
This error indicates the XML document has structural violations
|
|
25
|
+
(unknown elements, wrong nesting, cardinality violations) that
|
|
26
|
+
cannot be recovered via ExceptionalValue quarantine.
|
|
27
|
+
|
|
28
|
+
Structural errors include:
|
|
29
|
+
- Unknown/unexpected elements not defined in the XSD
|
|
30
|
+
- Missing required child elements (incomplete content)
|
|
31
|
+
- Cardinality violations (minOccurs/maxOccurs)
|
|
32
|
+
- Incorrect element nesting
|
|
33
|
+
|
|
34
|
+
These errors represent potential security risks (e.g., mass assignment
|
|
35
|
+
attacks) and must be rejected outright rather than quarantined.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(self, errors: List['XMLSchemaValidationError'], message: str = None):
|
|
39
|
+
"""
|
|
40
|
+
Initialize the structural validation error.
|
|
41
|
+
|
|
42
|
+
:param errors: List of structural validation errors that caused the rejection.
|
|
43
|
+
:param message: Optional custom message. If None, a message is built from errors.
|
|
44
|
+
"""
|
|
45
|
+
self.errors = errors
|
|
46
|
+
if message is None:
|
|
47
|
+
message = self._build_message()
|
|
48
|
+
super().__init__(message)
|
|
49
|
+
|
|
50
|
+
def _build_message(self) -> str:
|
|
51
|
+
"""Build a detailed error message from the list of structural errors."""
|
|
52
|
+
lines = ["Structural validation failed (Tier 1 rejection):"]
|
|
53
|
+
for i, error in enumerate(self.errors, 1):
|
|
54
|
+
reason = error.reason or str(error)
|
|
55
|
+
lines.append(f" {i}. {reason}")
|
|
56
|
+
if error.path:
|
|
57
|
+
lines.append(f" at: {error.path}")
|
|
58
|
+
return "\n".join(lines)
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def error_count(self) -> int:
|
|
62
|
+
"""Return the number of structural errors."""
|
|
63
|
+
return len(self.errors)
|