sdcvalidator 4.5.0__tar.gz → 4.5.2__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.5.0 → sdcvalidator-4.5.2}/PKG-INFO +3 -3
  2. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/pyproject.toml +3 -3
  3. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/__init__.py +1 -1
  4. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/mcp_server.py +23 -3
  5. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/validator.py +50 -5
  6. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/PKG-INFO +3 -3
  7. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/SOURCES.txt +2 -1
  8. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/tests/test_mcp_server.py +52 -0
  9. sdcvalidator-4.5.2/tests/test_xsi_type.py +60 -0
  10. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/LICENSE +0 -0
  11. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/README.md +0 -0
  12. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/setup.cfg +0 -0
  13. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/cli.py +0 -0
  14. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/constants.py +0 -0
  15. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/converters.py +0 -0
  16. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/error_classifier.py +0 -0
  17. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/error_mapper.py +0 -0
  18. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/exceptions.py +0 -0
  19. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/instance_modifier.py +0 -0
  20. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/schema_checker.py +0 -0
  21. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator/xsd11_restriction.py +0 -0
  22. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
  23. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/entry_points.txt +0 -0
  24. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/requires.txt +0 -0
  25. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/src/sdcvalidator.egg-info/top_level.txt +0 -0
  26. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/tests/test_converters.py +0 -0
  27. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/tests/test_error_classifier.py +0 -0
  28. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/tests/test_error_mapper.py +0 -0
  29. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/tests/test_instance_modifier.py +0 -0
  30. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/tests/test_recovery.py +0 -0
  31. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/tests/test_schema_checker.py +0 -0
  32. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/tests/test_validator.py +0 -0
  33. {sdcvalidator-4.5.0 → sdcvalidator-4.5.2}/tests/test_xsd11_restriction.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdcvalidator
3
- Version: 4.5.0
3
+ Version: 4.5.2
4
4
  Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
5
- Author: Semantic Data Charter Foundation
5
+ Author: Axius SDC, Inc.
6
6
  License-Expression: Apache-2.0
7
- Classifier: Development Status :: 4 - Beta
7
+ Classifier: Development Status :: 5 - Production/Stable
8
8
  Classifier: Intended Audience :: Developers
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.10
@@ -4,16 +4,16 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sdcvalidator"
7
- version = "4.5.0"
7
+ version = "4.5.2"
8
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"
12
12
  authors = [
13
- {name = "Semantic Data Charter Foundation"},
13
+ {name = "Axius SDC, Inc."},
14
14
  ]
15
15
  classifiers = [
16
- "Development Status :: 4 - Beta",
16
+ "Development Status :: 5 - Production/Stable",
17
17
  "Intended Audience :: Developers",
18
18
  "Programming Language :: Python :: 3",
19
19
  "Programming Language :: Python :: 3.10",
@@ -14,7 +14,7 @@ Thin wrapper over xmlschema with two-tier error classification and the SDC4
14
14
  "quarantine-and-tag" recovery pattern (ISO 21090 ExceptionalValues).
15
15
  """
16
16
 
17
- __version__ = "4.5.0"
17
+ __version__ = "4.5.2"
18
18
 
19
19
  # etree_tostring is re-exported from xmlschema so callers can serialize the
20
20
  # ElementTree returned by validate_with_recovery() without importing xmlschema
@@ -36,7 +36,7 @@ from sdcvalidator.schema_checker import validate_sdc4_schema_compliance
36
36
  JSONRPC_VERSION = "2.0"
37
37
  MCP_PROTOCOL_VERSION = "2024-11-05"
38
38
  SERVER_NAME = "sdcvalidator"
39
- SERVER_VERSION = "4.2.1"
39
+ SERVER_VERSION = "4.5.2"
40
40
 
41
41
  # Validator cache: schema_path -> SDC4Validator
42
42
  _validators: dict[str, SDC4Validator] = {}
@@ -201,10 +201,24 @@ def _jsonrpc_error(id: Any, code: int, message: str, data: Any = None) -> dict:
201
201
  return {"jsonrpc": JSONRPC_VERSION, "id": id, "error": error}
202
202
 
203
203
 
204
- def _handle_request(request: dict) -> dict | None:
204
+ def _handle_request(request) -> dict | None:
205
+ # ★ Attacker-reachable input over stdio with no authentication. A JSON
206
+ # value that is not an object, or an object whose params is not one,
207
+ # used to raise AttributeError and kill the server (VSL rollout R9).
208
+ # Every shape the protocol does not allow is answered with -32600.
209
+ if not isinstance(request, dict):
210
+ return _jsonrpc_error(None, -32600, "Invalid Request: a JSON-RPC request is an object")
211
+ req_id = request.get("id")
212
+ if not isinstance(req_id, (str, int, type(None))):
213
+ return _jsonrpc_error(None, -32600, "Invalid Request: id must be a string, a number or null")
205
214
  method = request.get("method", "")
215
+ if not isinstance(method, str):
216
+ return _jsonrpc_error(req_id, -32600, "Invalid Request: method must be a string")
206
217
  params = request.get("params", {})
207
- req_id = request.get("id")
218
+ if params is None:
219
+ params = {}
220
+ if not isinstance(params, dict):
221
+ return _jsonrpc_error(req_id, -32600, "Invalid Request: params must be an object")
208
222
 
209
223
  if method == "initialize":
210
224
  result = {
@@ -228,6 +242,12 @@ def _handle_request(request: dict) -> dict | None:
228
242
  elif method == "tools/call":
229
243
  tool_name = params.get("name", "")
230
244
  tool_args = params.get("arguments", {})
245
+ if tool_args is None:
246
+ tool_args = {}
247
+ if not isinstance(tool_name, str) or not isinstance(tool_args, dict):
248
+ return _jsonrpc_error(
249
+ req_id, -32602, "Invalid params: name must be a string and arguments an object"
250
+ )
231
251
 
232
252
  handler = TOOL_HANDLERS.get(tool_name)
233
253
  if handler is None:
@@ -136,6 +136,51 @@ class SDC4Validator:
136
136
  else:
137
137
  raise TypeError(f"Unsupported xml_source type: {type(xml_source)}")
138
138
 
139
+ def _namespaces(
140
+ self, xml_source: Union[str, Path, ET.Element, XMLResource]
141
+ ) -> Optional[Dict[str, str]]:
142
+ """
143
+ The prefix map declared in the source, or None when it is not known.
144
+
145
+ ``_to_tree`` normalises every source into a stdlib ElementTree, which
146
+ resolves prefixes to Clark notation and discards the declarations. A
147
+ prefixed QName that appears as *content* rather than as a tag, which
148
+ is what ``xsi:type="sdc4:XdOrdinalType"`` is, can then no longer be
149
+ resolved and xmlschema raises ``XMLSchemaKeyError`` instead of
150
+ validating. The reference model relies on ``xsi:type`` wherever an
151
+ abstract element is filled by a concrete type, so an instance that is
152
+ valid against the reference model failed here before 4.5.1.
153
+
154
+ A path or an XMLResource still carries the declarations, so they are
155
+ read back and handed to xmlschema. A bare ``ET.Element`` has already
156
+ lost them; callers validating such an instance should pass the path
157
+ or an ``XMLResource`` instead.
158
+ """
159
+ if isinstance(xml_source, XMLResource):
160
+ return xml_source.get_namespaces(root_only=False)
161
+ if isinstance(xml_source, (str, Path)):
162
+ return XMLResource(str(xml_source)).get_namespaces(root_only=False)
163
+ return None
164
+
165
+ def _schema_errors(
166
+ self,
167
+ tree: ET.ElementTree,
168
+ xml_source: Union[str, Path, ET.Element, XMLResource],
169
+ ) -> Iterator[XMLSchemaValidationError]:
170
+ """
171
+ xmlschema's errors over ``tree``, with the source's prefix map.
172
+
173
+ The map is for resolving prefixed QNames in content during validation.
174
+ It is removed from each error afterwards so ``error.path`` keeps its
175
+ Clark form (``/{uri}local/...``): the recovery step locates elements by
176
+ that form, and every report has always carried it.
177
+ """
178
+ namespaces = self._namespaces(xml_source)
179
+ for error in self.schema.iter_errors(tree, namespaces=namespaces):
180
+ error.namespaces = None
181
+ error._path = None
182
+ yield error
183
+
139
184
  # ------------------------------------------------------------------ #
140
185
  # Classification-only validation (no modification)
141
186
  # ------------------------------------------------------------------ #
@@ -150,7 +195,7 @@ class SDC4Validator:
150
195
  ValidationResult with is_valid, structural_errors, semantic_errors.
151
196
  """
152
197
  tree = self._to_tree(xml_source)
153
- errors = list(self.schema.iter_errors(tree))
198
+ errors = list(self._schema_errors(tree, xml_source))
154
199
 
155
200
  classified = self.classifier.classify_all(errors)
156
201
 
@@ -170,7 +215,7 @@ class SDC4Validator:
170
215
  List of structural errors. Empty list means structure is valid.
171
216
  """
172
217
  tree = self._to_tree(xml_source)
173
- errors = list(self.schema.iter_errors(tree))
218
+ errors = list(self._schema_errors(tree, xml_source))
174
219
  return [e for e in errors if self.classifier.is_structural_error(e)]
175
220
 
176
221
  def iter_errors(
@@ -183,7 +228,7 @@ class SDC4Validator:
183
228
  List of error summary dictionaries (structural and semantic).
184
229
  """
185
230
  tree = self._to_tree(xml_source)
186
- errors = list(self.schema.iter_errors(tree))
231
+ errors = list(self._schema_errors(tree, xml_source))
187
232
  return [self.classifier.get_error_summary(e) for e in errors]
188
233
 
189
234
  def iter_errors_with_mapping(
@@ -198,7 +243,7 @@ class SDC4Validator:
198
243
  Dictionaries containing error details and the mapped EV type.
199
244
  """
200
245
  tree = self._to_tree(xml_source)
201
- for error in self.schema.iter_errors(tree):
246
+ for error in self._schema_errors(tree, xml_source):
202
247
  ev_type = self.error_mapper.map_error(error)
203
248
  if ev_type is None:
204
249
  continue
@@ -286,7 +331,7 @@ class SDC4Validator:
286
331
  if remove_existing_ev:
287
332
  self.instance_modifier.remove_existing_exceptional_values(root)
288
333
 
289
- errors = list(self.schema.iter_errors(tree))
334
+ errors = list(self._schema_errors(tree, xml_source))
290
335
 
291
336
  structural_errors = []
292
337
  semantic_errors = []
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sdcvalidator
3
- Version: 4.5.0
3
+ Version: 4.5.2
4
4
  Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
5
- Author: Semantic Data Charter Foundation
5
+ Author: Axius SDC, Inc.
6
6
  License-Expression: Apache-2.0
7
- Classifier: Development Status :: 4 - Beta
7
+ Classifier: Development Status :: 5 - Production/Stable
8
8
  Classifier: Intended Audience :: Developers
9
9
  Classifier: Programming Language :: Python :: 3
10
10
  Classifier: Programming Language :: Python :: 3.10
@@ -27,4 +27,5 @@ tests/test_mcp_server.py
27
27
  tests/test_recovery.py
28
28
  tests/test_schema_checker.py
29
29
  tests/test_validator.py
30
- tests/test_xsd11_restriction.py
30
+ tests/test_xsd11_restriction.py
31
+ tests/test_xsi_type.py
@@ -137,3 +137,55 @@ class TestConsistentSerialization:
137
137
  assert content[0]["type"] == "text"
138
138
  parsed = json.loads(content[0]["text"])
139
139
  assert parsed is not None
140
+
141
+ class TestMalformedRequestsDoNotKillTheServer:
142
+ """
143
+ VSL rollout inventory R9. Six inputs that are valid JSON but not a
144
+ JSON-RPC request raised AttributeError in _handle_request and ended the
145
+ process. Each is now answered with -32600 (or -32602 for bad tool params)
146
+ and the loop keeps reading.
147
+ """
148
+
149
+ @pytest.mark.parametrize("payload", [[], ["tools/list"], "tools/list", 7, None, True])
150
+ def test_a_non_object_request_is_invalid_request(self, payload):
151
+ response = _handle_request(payload)
152
+ assert response["error"]["code"] == -32600
153
+ assert response["id"] is None
154
+
155
+ @pytest.mark.parametrize("params", [[], "x", 3, True])
156
+ def test_non_object_params_are_invalid_request(self, params):
157
+ response = _handle_request({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": params})
158
+ assert response["error"]["code"] == -32600
159
+ assert response["id"] == 1
160
+
161
+ def test_null_params_are_treated_as_empty(self):
162
+ response = _handle_request({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": None})
163
+ assert "result" in response
164
+
165
+ def test_a_non_string_method_is_invalid_request(self):
166
+ response = _handle_request({"jsonrpc": "2.0", "id": 1, "method": ["tools/list"]})
167
+ assert response["error"]["code"] == -32600
168
+
169
+ @pytest.mark.parametrize("arguments", [[], "x", 3])
170
+ def test_non_object_tool_arguments_are_invalid_params(self, arguments):
171
+ response = _handle_request({
172
+ "jsonrpc": "2.0", "id": 1, "method": "tools/call",
173
+ "params": {"name": list(TOOL_HANDLERS)[0], "arguments": arguments},
174
+ })
175
+ assert response["error"]["code"] == -32602
176
+
177
+ def test_the_stdio_loop_survives_every_one_of_them(self, monkeypatch, capsys):
178
+ import io
179
+ import sys
180
+
181
+ from sdcvalidator.mcp_server import run_stdio
182
+
183
+ lines = ["[]", "\"tools/list\"", "7", "null",
184
+ json.dumps({"jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": []}),
185
+ json.dumps({"jsonrpc": "2.0", "id": 2, "method": "ping"})]
186
+ monkeypatch.setattr(sys, "stdin", io.StringIO("\n".join(lines) + "\n"))
187
+ run_stdio()
188
+ out = [json.loads(l) for l in capsys.readouterr().out.strip().splitlines()]
189
+ assert len(out) == 6
190
+ assert [o.get("error", {}).get("code") for o in out[:5]] == [-32600] * 5
191
+ assert out[5] == {"jsonrpc": "2.0", "id": 2, "result": {}}
@@ -0,0 +1,60 @@
1
+ """
2
+ Instances that fill a declared type through ``xsi:type``.
3
+
4
+ The SDC4 reference model declares ``sdc4:XdOrdinal`` and its siblings
5
+ abstract, so a schema-valid instance fills them with a concrete type through
6
+ ``xsi:type="sdc4:XdOrdinalType"``. Before 4.5.1 every input was normalised
7
+ into a stdlib ElementTree, which drops the namespace prefix declarations;
8
+ the prefixed QName in ``xsi:type`` then could not be resolved and xmlschema
9
+ raised ``XMLSchemaKeyError`` instead of validating. A path or an XMLResource
10
+ carries the declarations and must validate.
11
+ """
12
+
13
+ import io
14
+ import unittest
15
+ from pathlib import Path
16
+
17
+ from xmlschema import XMLResource
18
+
19
+ from sdcvalidator.validator import SDC4Validator
20
+
21
+ DATA = Path(__file__).parent / "test_data"
22
+ SCHEMA = DATA / "xsi_type_schema.xsd"
23
+ VALID = DATA / "xsi_type_instance.xml"
24
+ INVALID = DATA / "xsi_type_instance_invalid.xml"
25
+
26
+
27
+ class TestXsiTypeResolution(unittest.TestCase):
28
+ def setUp(self):
29
+ self.validator = SDC4Validator(SCHEMA, check_sdc4_compliance=False)
30
+
31
+ def test_a_path_validates(self):
32
+ result = self.validator.validate(str(VALID))
33
+ self.assertTrue(result.is_valid, result.structural_errors + result.semantic_errors)
34
+
35
+ def test_a_pathlib_path_validates(self):
36
+ self.assertTrue(self.validator.validate(VALID).is_valid)
37
+
38
+ def test_an_xmlresource_over_bytes_validates(self):
39
+ resource = XMLResource(io.BytesIO(VALID.read_bytes()))
40
+ self.assertTrue(self.validator.validate(resource).is_valid)
41
+
42
+ def test_the_report_and_recovery_paths_resolve_the_type_too(self):
43
+ report = self.validator.validate_and_report(str(VALID))
44
+ self.assertTrue(report["valid"], report)
45
+ self.assertEqual(report["error_count"], 0)
46
+ self.validator.validate_with_recovery(str(VALID), save=False)
47
+ self.assertEqual(self.validator.validate_structure(str(VALID)), [])
48
+ self.assertEqual(self.validator.iter_errors(str(VALID)), [])
49
+
50
+ def test_a_type_that_resolves_is_still_checked(self):
51
+ """Resolving the type must not mean skipping it: a bad ordinal is caught."""
52
+ result = self.validator.validate(str(INVALID))
53
+ self.assertFalse(result.is_valid)
54
+ self.assertEqual(result.error_count, 1)
55
+ report = self.validator.validate_and_report(XMLResource(io.BytesIO(INVALID.read_bytes())))
56
+ self.assertEqual(report["error_count"], 1)
57
+
58
+
59
+ if __name__ == "__main__":
60
+ unittest.main()
File without changes
File without changes
File without changes