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.
Files changed (33) hide show
  1. {sdcvalidator-4.2.0/src/sdcvalidator.egg-info → sdcvalidator-4.3.0}/PKG-INFO +2 -2
  2. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/pyproject.toml +3 -2
  3. sdcvalidator-4.3.0/src/sdcvalidator/__init__.py +65 -0
  4. sdcvalidator-4.3.0/src/sdcvalidator/constants.py +162 -0
  5. sdcvalidator-4.3.0/src/sdcvalidator/error_mapper.py +343 -0
  6. sdcvalidator-4.3.0/src/sdcvalidator/exceptions.py +63 -0
  7. sdcvalidator-4.3.0/src/sdcvalidator/instance_modifier.py +424 -0
  8. sdcvalidator-4.3.0/src/sdcvalidator/mcp_server.py +295 -0
  9. sdcvalidator-4.3.0/src/sdcvalidator/validator.py +393 -0
  10. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0/src/sdcvalidator.egg-info}/PKG-INFO +2 -2
  11. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/SOURCES.txt +8 -0
  12. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/entry_points.txt +1 -0
  13. sdcvalidator-4.3.0/tests/test_error_mapper.py +153 -0
  14. sdcvalidator-4.3.0/tests/test_instance_modifier.py +75 -0
  15. sdcvalidator-4.3.0/tests/test_mcp_server.py +139 -0
  16. sdcvalidator-4.3.0/tests/test_recovery.py +149 -0
  17. sdcvalidator-4.2.0/src/sdcvalidator/__init__.py +0 -38
  18. sdcvalidator-4.2.0/src/sdcvalidator/constants.py +0 -62
  19. sdcvalidator-4.2.0/src/sdcvalidator/validator.py +0 -174
  20. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/LICENSE +0 -0
  21. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/README.md +0 -0
  22. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/setup.cfg +0 -0
  23. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator/cli.py +0 -0
  24. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator/converters.py +0 -0
  25. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator/error_classifier.py +0 -0
  26. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator/schema_checker.py +0 -0
  27. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
  28. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/requires.txt +0 -0
  29. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/src/sdcvalidator.egg-info/top_level.txt +0 -0
  30. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/tests/test_converters.py +0 -0
  31. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/tests/test_error_classifier.py +0 -0
  32. {sdcvalidator-4.2.0 → sdcvalidator-4.3.0}/tests/test_schema_checker.py +0 -0
  33. {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.2.0
4
- Summary: SDC4 structural validator — thin wrapper over xmlschema with error classification
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.2.0"
8
- description = "SDC4 structural validator — thin wrapper over xmlschema with error classification"
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)