sdcvalidator 4.1.0__tar.gz → 4.2.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.
- {sdcvalidator-4.1.0/src/sdcvalidator.egg-info → sdcvalidator-4.2.1}/PKG-INFO +16 -2
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/README.md +15 -1
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/pyproject.toml +2 -1
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/__init__.py +1 -1
- sdcvalidator-4.2.1/src/sdcvalidator/mcp_server.py +295 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/validator.py +4 -1
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1/src/sdcvalidator.egg-info}/PKG-INFO +16 -2
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/SOURCES.txt +2 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/entry_points.txt +1 -0
- sdcvalidator-4.2.1/tests/test_mcp_server.py +139 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/tests/test_validator.py +152 -6
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/LICENSE +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/setup.cfg +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/cli.py +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/constants.py +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/converters.py +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/error_classifier.py +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator/schema_checker.py +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/requires.txt +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/src/sdcvalidator.egg-info/top_level.txt +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/tests/test_converters.py +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/tests/test_error_classifier.py +0 -0
- {sdcvalidator-4.1.0 → sdcvalidator-4.2.1}/tests/test_schema_checker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcvalidator
|
|
3
|
-
Version: 4.1
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: SDC4 structural validator — thin wrapper over xmlschema with error classification
|
|
5
5
|
Author: Semantic Data Charter Foundation
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -32,6 +32,7 @@ Dynamic: license-file
|
|
|
32
32
|
- Validates XML instances against SDC4 XSD schemas
|
|
33
33
|
- Classifies errors into **structural** (Tier 1: reject) vs **semantic** (Tier 2: report)
|
|
34
34
|
- Checks SDC4 schema compliance (no `xsd:extension` — only `xsd:restriction`)
|
|
35
|
+
- **Strict validation by default** — catches invalid restriction derivations (wrong element names, type mismatches) at schema load time
|
|
35
36
|
- Converts between XML and JSON using schema-aware conversion
|
|
36
37
|
|
|
37
38
|
## Install
|
|
@@ -53,7 +54,7 @@ pip install -e .
|
|
|
53
54
|
```python
|
|
54
55
|
from sdcvalidator import SDC4Validator, ErrorTier
|
|
55
56
|
|
|
56
|
-
# Validate an XML instance
|
|
57
|
+
# Validate an XML instance (strict mode by default)
|
|
57
58
|
validator = SDC4Validator("my_schema.xsd")
|
|
58
59
|
result = validator.validate("my_instance.xml")
|
|
59
60
|
|
|
@@ -66,6 +67,19 @@ else:
|
|
|
66
67
|
print(f"SEMANTIC: {err.reason}")
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
### Validation modes
|
|
71
|
+
|
|
72
|
+
The `validation` parameter controls how strictly the XSD schema itself is checked when loaded:
|
|
73
|
+
|
|
74
|
+
- **`'strict'`** (default) — Raises `XMLSchemaParseError` if the schema contains invalid restriction derivations (e.g., element names that don't match the base type). This is the recommended mode.
|
|
75
|
+
- **`'lax'`** — Silently collects schema derivation errors without raising. Use only for pre-existing schemas known to have issues.
|
|
76
|
+
- **`'skip'`** — Skips schema-level validation entirely.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Explicit lax mode for legacy schemas
|
|
80
|
+
validator = SDC4Validator("legacy_schema.xsd", validation='lax')
|
|
81
|
+
```
|
|
82
|
+
|
|
69
83
|
### Schema compliance checking
|
|
70
84
|
|
|
71
85
|
```python
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
- Validates XML instances against SDC4 XSD schemas
|
|
11
11
|
- Classifies errors into **structural** (Tier 1: reject) vs **semantic** (Tier 2: report)
|
|
12
12
|
- Checks SDC4 schema compliance (no `xsd:extension` — only `xsd:restriction`)
|
|
13
|
+
- **Strict validation by default** — catches invalid restriction derivations (wrong element names, type mismatches) at schema load time
|
|
13
14
|
- Converts between XML and JSON using schema-aware conversion
|
|
14
15
|
|
|
15
16
|
## Install
|
|
@@ -31,7 +32,7 @@ pip install -e .
|
|
|
31
32
|
```python
|
|
32
33
|
from sdcvalidator import SDC4Validator, ErrorTier
|
|
33
34
|
|
|
34
|
-
# Validate an XML instance
|
|
35
|
+
# Validate an XML instance (strict mode by default)
|
|
35
36
|
validator = SDC4Validator("my_schema.xsd")
|
|
36
37
|
result = validator.validate("my_instance.xml")
|
|
37
38
|
|
|
@@ -44,6 +45,19 @@ else:
|
|
|
44
45
|
print(f"SEMANTIC: {err.reason}")
|
|
45
46
|
```
|
|
46
47
|
|
|
48
|
+
### Validation modes
|
|
49
|
+
|
|
50
|
+
The `validation` parameter controls how strictly the XSD schema itself is checked when loaded:
|
|
51
|
+
|
|
52
|
+
- **`'strict'`** (default) — Raises `XMLSchemaParseError` if the schema contains invalid restriction derivations (e.g., element names that don't match the base type). This is the recommended mode.
|
|
53
|
+
- **`'lax'`** — Silently collects schema derivation errors without raising. Use only for pre-existing schemas known to have issues.
|
|
54
|
+
- **`'skip'`** — Skips schema-level validation entirely.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# Explicit lax mode for legacy schemas
|
|
58
|
+
validator = SDC4Validator("legacy_schema.xsd", validation='lax')
|
|
59
|
+
```
|
|
60
|
+
|
|
47
61
|
### Schema compliance checking
|
|
48
62
|
|
|
49
63
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sdcvalidator"
|
|
7
|
-
version = "4.1
|
|
7
|
+
version = "4.2.1"
|
|
8
8
|
description = "SDC4 structural validator — thin wrapper over xmlschema with error classification"
|
|
9
9
|
license = "Apache-2.0"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -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"]
|
|
@@ -13,7 +13,7 @@ sdcvalidator — SDC4 structural validator.
|
|
|
13
13
|
Thin wrapper over xmlschema with two-tier error classification.
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
|
-
__version__ = "4.1
|
|
16
|
+
__version__ = "4.2.1"
|
|
17
17
|
|
|
18
18
|
from .validator import SDC4Validator, ValidationResult
|
|
19
19
|
from .error_classifier import ErrorClassifier
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 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
|
+
MCP server exposing sdcvalidator tools to any agent framework.
|
|
12
|
+
|
|
13
|
+
Implements the MCP (Model Context Protocol) as a raw JSON-RPC 2.0
|
|
14
|
+
stdio server. No external MCP SDK dependency.
|
|
15
|
+
|
|
16
|
+
Start the server::
|
|
17
|
+
|
|
18
|
+
sdcvalidator serve --mcp
|
|
19
|
+
|
|
20
|
+
MCP Tools exposed:
|
|
21
|
+
- validate_instance: validate an XML instance against its XSD schema
|
|
22
|
+
- validate_and_report: detailed validation report with error classification
|
|
23
|
+
- check_schema_compliance: verify schema follows SDC4 principles
|
|
24
|
+
|
|
25
|
+
Protocol: JSON-RPC 2.0 over stdio (one JSON object per line).
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import json
|
|
29
|
+
import sys
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from sdcvalidator.validator import SDC4Validator
|
|
33
|
+
from sdcvalidator.schema_checker import validate_sdc4_schema_compliance
|
|
34
|
+
|
|
35
|
+
# Protocol constants
|
|
36
|
+
JSONRPC_VERSION = "2.0"
|
|
37
|
+
MCP_PROTOCOL_VERSION = "2024-11-05"
|
|
38
|
+
SERVER_NAME = "sdcvalidator"
|
|
39
|
+
SERVER_VERSION = "4.2.1"
|
|
40
|
+
|
|
41
|
+
# Validator cache: schema_path -> SDC4Validator
|
|
42
|
+
_validators: dict[str, SDC4Validator] = {}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _get_validator(schema_path: str, check_compliance: bool = True) -> SDC4Validator:
|
|
46
|
+
"""Get or create a validator for the given schema."""
|
|
47
|
+
key = f"{schema_path}:{check_compliance}"
|
|
48
|
+
if key not in _validators:
|
|
49
|
+
_validators[key] = SDC4Validator(
|
|
50
|
+
schema_path,
|
|
51
|
+
check_sdc4_compliance=check_compliance,
|
|
52
|
+
)
|
|
53
|
+
return _validators[key]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# --- Tool definitions ---
|
|
57
|
+
|
|
58
|
+
TOOLS = [
|
|
59
|
+
{
|
|
60
|
+
"name": "validate_instance",
|
|
61
|
+
"description": (
|
|
62
|
+
"Validate an XML instance against its SDC4 XSD schema. "
|
|
63
|
+
"Returns pass/fail with error count and classified errors."
|
|
64
|
+
),
|
|
65
|
+
"inputSchema": {
|
|
66
|
+
"type": "object",
|
|
67
|
+
"properties": {
|
|
68
|
+
"schema_path": {
|
|
69
|
+
"type": "string",
|
|
70
|
+
"description": "Path to the SDC4 data model XSD file.",
|
|
71
|
+
},
|
|
72
|
+
"instance_path": {
|
|
73
|
+
"type": "string",
|
|
74
|
+
"description": "Path to the XML instance file to validate.",
|
|
75
|
+
},
|
|
76
|
+
"check_compliance": {
|
|
77
|
+
"type": "boolean",
|
|
78
|
+
"description": "Check schema follows SDC4 principles (no xsd:extension). Default true.",
|
|
79
|
+
"default": True,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
"required": ["schema_path", "instance_path"],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
"name": "validate_and_report",
|
|
87
|
+
"description": (
|
|
88
|
+
"Validate an XML instance and return a detailed report with "
|
|
89
|
+
"two-tier error classification (structural vs semantic)."
|
|
90
|
+
),
|
|
91
|
+
"inputSchema": {
|
|
92
|
+
"type": "object",
|
|
93
|
+
"properties": {
|
|
94
|
+
"schema_path": {
|
|
95
|
+
"type": "string",
|
|
96
|
+
"description": "Path to the SDC4 data model XSD file.",
|
|
97
|
+
},
|
|
98
|
+
"instance_path": {
|
|
99
|
+
"type": "string",
|
|
100
|
+
"description": "Path to the XML instance file to validate.",
|
|
101
|
+
},
|
|
102
|
+
"check_compliance": {
|
|
103
|
+
"type": "boolean",
|
|
104
|
+
"description": "Check schema follows SDC4 principles. Default true.",
|
|
105
|
+
"default": True,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
"required": ["schema_path", "instance_path"],
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
"name": "check_schema_compliance",
|
|
113
|
+
"description": (
|
|
114
|
+
"Check if an XSD schema follows SDC4 principles "
|
|
115
|
+
"(no xsd:extension, restriction only). Does not validate instances."
|
|
116
|
+
),
|
|
117
|
+
"inputSchema": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"properties": {
|
|
120
|
+
"schema_path": {
|
|
121
|
+
"type": "string",
|
|
122
|
+
"description": "Path to the XSD schema file to check.",
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
"required": ["schema_path"],
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# --- Tool handlers ---
|
|
132
|
+
|
|
133
|
+
def _handle_validate_instance(args: dict[str, Any]) -> Any:
|
|
134
|
+
schema_path = args["schema_path"]
|
|
135
|
+
instance_path = args["instance_path"]
|
|
136
|
+
check_compliance = args.get("check_compliance", True)
|
|
137
|
+
|
|
138
|
+
try:
|
|
139
|
+
validator = _get_validator(schema_path, check_compliance)
|
|
140
|
+
except Exception as exc:
|
|
141
|
+
return {
|
|
142
|
+
"valid": False,
|
|
143
|
+
"error": f"Schema error: {exc}",
|
|
144
|
+
"error_count": 0,
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
result = validator.validate(instance_path)
|
|
148
|
+
return {
|
|
149
|
+
"valid": result.is_valid,
|
|
150
|
+
"error_count": result.error_count,
|
|
151
|
+
"structural_error_count": len(result.structural_errors),
|
|
152
|
+
"semantic_error_count": len(result.semantic_errors),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _handle_validate_and_report(args: dict[str, Any]) -> Any:
|
|
157
|
+
schema_path = args["schema_path"]
|
|
158
|
+
instance_path = args["instance_path"]
|
|
159
|
+
check_compliance = args.get("check_compliance", True)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
validator = _get_validator(schema_path, check_compliance)
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
return {
|
|
165
|
+
"valid": False,
|
|
166
|
+
"error": f"Schema error: {exc}",
|
|
167
|
+
"error_count": 0,
|
|
168
|
+
"structural_errors": [],
|
|
169
|
+
"semantic_errors": [],
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return validator.validate_and_report(instance_path)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _handle_check_schema_compliance(args: dict[str, Any]) -> Any:
|
|
176
|
+
schema_path = args["schema_path"]
|
|
177
|
+
is_valid, errors = validate_sdc4_schema_compliance(schema_path)
|
|
178
|
+
return {
|
|
179
|
+
"compliant": is_valid,
|
|
180
|
+
"errors": errors,
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
TOOL_HANDLERS = {
|
|
185
|
+
"validate_instance": _handle_validate_instance,
|
|
186
|
+
"validate_and_report": _handle_validate_and_report,
|
|
187
|
+
"check_schema_compliance": _handle_check_schema_compliance,
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# --- JSON-RPC 2.0 stdio server ---
|
|
192
|
+
|
|
193
|
+
def _jsonrpc_response(id: Any, result: Any) -> dict:
|
|
194
|
+
return {"jsonrpc": JSONRPC_VERSION, "id": id, "result": result}
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _jsonrpc_error(id: Any, code: int, message: str, data: Any = None) -> dict:
|
|
198
|
+
error = {"code": code, "message": message}
|
|
199
|
+
if data is not None:
|
|
200
|
+
error["data"] = data
|
|
201
|
+
return {"jsonrpc": JSONRPC_VERSION, "id": id, "error": error}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def _handle_request(request: dict) -> dict | None:
|
|
205
|
+
method = request.get("method", "")
|
|
206
|
+
params = request.get("params", {})
|
|
207
|
+
req_id = request.get("id")
|
|
208
|
+
|
|
209
|
+
if method == "initialize":
|
|
210
|
+
result = {
|
|
211
|
+
"protocolVersion": MCP_PROTOCOL_VERSION,
|
|
212
|
+
"capabilities": {"tools": {"listChanged": False}},
|
|
213
|
+
"serverInfo": {"name": SERVER_NAME, "version": SERVER_VERSION},
|
|
214
|
+
"instructions": (
|
|
215
|
+
"SDC4 structural validator. Validates XML instances against "
|
|
216
|
+
"XSD schemas with two-tier error classification (structural vs semantic). "
|
|
217
|
+
"Independent from sdcgovernance - agents call each library separately."
|
|
218
|
+
),
|
|
219
|
+
}
|
|
220
|
+
return _jsonrpc_response(req_id, result)
|
|
221
|
+
|
|
222
|
+
elif method == "notifications/initialized":
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
elif method == "tools/list":
|
|
226
|
+
return _jsonrpc_response(req_id, {"tools": TOOLS})
|
|
227
|
+
|
|
228
|
+
elif method == "tools/call":
|
|
229
|
+
tool_name = params.get("name", "")
|
|
230
|
+
tool_args = params.get("arguments", {})
|
|
231
|
+
|
|
232
|
+
handler = TOOL_HANDLERS.get(tool_name)
|
|
233
|
+
if handler is None:
|
|
234
|
+
return _jsonrpc_error(req_id, -32601, f"Unknown tool: {tool_name}")
|
|
235
|
+
|
|
236
|
+
try:
|
|
237
|
+
result = handler(tool_args)
|
|
238
|
+
return _jsonrpc_response(req_id, {
|
|
239
|
+
"content": [{"type": "text", "text": json.dumps(result, default=str)}],
|
|
240
|
+
})
|
|
241
|
+
except Exception as exc:
|
|
242
|
+
return _jsonrpc_error(req_id, -32000, f"Tool execution error: {exc}")
|
|
243
|
+
|
|
244
|
+
elif method == "ping":
|
|
245
|
+
return _jsonrpc_response(req_id, {})
|
|
246
|
+
|
|
247
|
+
else:
|
|
248
|
+
if req_id is not None:
|
|
249
|
+
return _jsonrpc_error(req_id, -32601, f"Method not found: {method}")
|
|
250
|
+
return None
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def run_stdio() -> None:
|
|
254
|
+
"""Run the MCP server on stdio."""
|
|
255
|
+
for line in sys.stdin:
|
|
256
|
+
line = line.strip()
|
|
257
|
+
if not line:
|
|
258
|
+
continue
|
|
259
|
+
try:
|
|
260
|
+
request = json.loads(line)
|
|
261
|
+
except json.JSONDecodeError as exc:
|
|
262
|
+
response = _jsonrpc_error(None, -32700, f"Parse error: {exc}")
|
|
263
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
264
|
+
sys.stdout.flush()
|
|
265
|
+
continue
|
|
266
|
+
|
|
267
|
+
response = _handle_request(request)
|
|
268
|
+
if response is not None:
|
|
269
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
270
|
+
sys.stdout.flush()
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def main() -> None:
|
|
274
|
+
"""Entry point for the sdcvalidator MCP server."""
|
|
275
|
+
import argparse
|
|
276
|
+
|
|
277
|
+
parser = argparse.ArgumentParser(
|
|
278
|
+
prog="sdcvalidator-mcp",
|
|
279
|
+
description="SDC4 structural validator - MCP server",
|
|
280
|
+
)
|
|
281
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
282
|
+
|
|
283
|
+
serve_parser = subparsers.add_parser("serve", help="Start the validation server")
|
|
284
|
+
serve_parser.add_argument("--mcp", action="store_true", help="Run as MCP stdio server")
|
|
285
|
+
|
|
286
|
+
args = parser.parse_args()
|
|
287
|
+
|
|
288
|
+
if args.command == "serve" and args.mcp:
|
|
289
|
+
run_stdio()
|
|
290
|
+
else:
|
|
291
|
+
parser.print_help()
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
if __name__ == "__main__":
|
|
295
|
+
main()
|
|
@@ -52,7 +52,7 @@ class SDC4Validator:
|
|
|
52
52
|
self,
|
|
53
53
|
schema: Union[str, Path, XMLSchema11],
|
|
54
54
|
check_sdc4_compliance: bool = True,
|
|
55
|
-
validation: str = '
|
|
55
|
+
validation: str = 'strict',
|
|
56
56
|
):
|
|
57
57
|
"""
|
|
58
58
|
Initialize the SDC4 validator.
|
|
@@ -62,6 +62,9 @@ class SDC4Validator:
|
|
|
62
62
|
check_sdc4_compliance: If True, validate schema follows SDC4
|
|
63
63
|
principles (no xsd:extension). Default: True.
|
|
64
64
|
validation: Schema validation mode ('strict', 'lax', 'skip').
|
|
65
|
+
Default is 'strict' to catch invalid restriction derivations
|
|
66
|
+
(e.g. wrong element names, type mismatches). Lax mode silently
|
|
67
|
+
accepts these errors.
|
|
65
68
|
|
|
66
69
|
Raises:
|
|
67
70
|
SDC4SchemaValidationError: If compliance check fails.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcvalidator
|
|
3
|
-
Version: 4.1
|
|
3
|
+
Version: 4.2.1
|
|
4
4
|
Summary: SDC4 structural validator — thin wrapper over xmlschema with error classification
|
|
5
5
|
Author: Semantic Data Charter Foundation
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -32,6 +32,7 @@ Dynamic: license-file
|
|
|
32
32
|
- Validates XML instances against SDC4 XSD schemas
|
|
33
33
|
- Classifies errors into **structural** (Tier 1: reject) vs **semantic** (Tier 2: report)
|
|
34
34
|
- Checks SDC4 schema compliance (no `xsd:extension` — only `xsd:restriction`)
|
|
35
|
+
- **Strict validation by default** — catches invalid restriction derivations (wrong element names, type mismatches) at schema load time
|
|
35
36
|
- Converts between XML and JSON using schema-aware conversion
|
|
36
37
|
|
|
37
38
|
## Install
|
|
@@ -53,7 +54,7 @@ pip install -e .
|
|
|
53
54
|
```python
|
|
54
55
|
from sdcvalidator import SDC4Validator, ErrorTier
|
|
55
56
|
|
|
56
|
-
# Validate an XML instance
|
|
57
|
+
# Validate an XML instance (strict mode by default)
|
|
57
58
|
validator = SDC4Validator("my_schema.xsd")
|
|
58
59
|
result = validator.validate("my_instance.xml")
|
|
59
60
|
|
|
@@ -66,6 +67,19 @@ else:
|
|
|
66
67
|
print(f"SEMANTIC: {err.reason}")
|
|
67
68
|
```
|
|
68
69
|
|
|
70
|
+
### Validation modes
|
|
71
|
+
|
|
72
|
+
The `validation` parameter controls how strictly the XSD schema itself is checked when loaded:
|
|
73
|
+
|
|
74
|
+
- **`'strict'`** (default) — Raises `XMLSchemaParseError` if the schema contains invalid restriction derivations (e.g., element names that don't match the base type). This is the recommended mode.
|
|
75
|
+
- **`'lax'`** — Silently collects schema derivation errors without raising. Use only for pre-existing schemas known to have issues.
|
|
76
|
+
- **`'skip'`** — Skips schema-level validation entirely.
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
# Explicit lax mode for legacy schemas
|
|
80
|
+
validator = SDC4Validator("legacy_schema.xsd", validation='lax')
|
|
81
|
+
```
|
|
82
|
+
|
|
69
83
|
### Schema compliance checking
|
|
70
84
|
|
|
71
85
|
```python
|
|
@@ -6,6 +6,7 @@ src/sdcvalidator/cli.py
|
|
|
6
6
|
src/sdcvalidator/constants.py
|
|
7
7
|
src/sdcvalidator/converters.py
|
|
8
8
|
src/sdcvalidator/error_classifier.py
|
|
9
|
+
src/sdcvalidator/mcp_server.py
|
|
9
10
|
src/sdcvalidator/schema_checker.py
|
|
10
11
|
src/sdcvalidator/validator.py
|
|
11
12
|
src/sdcvalidator.egg-info/PKG-INFO
|
|
@@ -16,5 +17,6 @@ src/sdcvalidator.egg-info/requires.txt
|
|
|
16
17
|
src/sdcvalidator.egg-info/top_level.txt
|
|
17
18
|
tests/test_converters.py
|
|
18
19
|
tests/test_error_classifier.py
|
|
20
|
+
tests/test_mcp_server.py
|
|
19
21
|
tests/test_schema_checker.py
|
|
20
22
|
tests/test_validator.py
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#
|
|
2
|
+
# Copyright 2026 Semantic Data Charter Foundation
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
"""
|
|
6
|
+
Tests for MCP server - JSON-RPC 2.0 protocol and tool definitions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import pytest
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from sdcvalidator.mcp_server import _handle_request, TOOLS, TOOL_HANDLERS
|
|
13
|
+
|
|
14
|
+
TEST_DATA_DIR = Path(__file__).parent / "test_data"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def call_tool(name: str, arguments: dict) -> dict:
|
|
18
|
+
"""Helper: call an MCP tool via JSON-RPC and return parsed result."""
|
|
19
|
+
request = {
|
|
20
|
+
"jsonrpc": "2.0",
|
|
21
|
+
"id": 1,
|
|
22
|
+
"method": "tools/call",
|
|
23
|
+
"params": {"name": name, "arguments": arguments},
|
|
24
|
+
}
|
|
25
|
+
response = _handle_request(request)
|
|
26
|
+
assert "result" in response, f"Expected result, got: {response}"
|
|
27
|
+
content = response["result"]["content"]
|
|
28
|
+
assert len(content) == 1
|
|
29
|
+
assert content[0]["type"] == "text"
|
|
30
|
+
return json.loads(content[0]["text"])
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class TestMcpProtocol:
|
|
34
|
+
"""JSON-RPC 2.0 MCP protocol handling."""
|
|
35
|
+
|
|
36
|
+
def test_initialize(self):
|
|
37
|
+
request = {"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {}}
|
|
38
|
+
response = _handle_request(request)
|
|
39
|
+
assert response["id"] == 1
|
|
40
|
+
result = response["result"]
|
|
41
|
+
assert result["serverInfo"]["name"] == "sdcvalidator"
|
|
42
|
+
assert "tools" in result["capabilities"]
|
|
43
|
+
|
|
44
|
+
def test_initialized_notification(self):
|
|
45
|
+
request = {"jsonrpc": "2.0", "method": "notifications/initialized"}
|
|
46
|
+
response = _handle_request(request)
|
|
47
|
+
assert response is None
|
|
48
|
+
|
|
49
|
+
def test_tools_list(self):
|
|
50
|
+
request = {"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}
|
|
51
|
+
response = _handle_request(request)
|
|
52
|
+
tools = response["result"]["tools"]
|
|
53
|
+
assert len(tools) == 3
|
|
54
|
+
names = {t["name"] for t in tools}
|
|
55
|
+
assert names == {"validate_instance", "validate_and_report", "check_schema_compliance"}
|
|
56
|
+
|
|
57
|
+
def test_tools_have_input_schemas(self):
|
|
58
|
+
request = {"jsonrpc": "2.0", "id": 3, "method": "tools/list", "params": {}}
|
|
59
|
+
response = _handle_request(request)
|
|
60
|
+
for tool in response["result"]["tools"]:
|
|
61
|
+
assert "inputSchema" in tool
|
|
62
|
+
assert tool["inputSchema"]["type"] == "object"
|
|
63
|
+
|
|
64
|
+
def test_ping(self):
|
|
65
|
+
request = {"jsonrpc": "2.0", "id": 4, "method": "ping", "params": {}}
|
|
66
|
+
response = _handle_request(request)
|
|
67
|
+
assert response["id"] == 4
|
|
68
|
+
assert response["result"] == {}
|
|
69
|
+
|
|
70
|
+
def test_unknown_method(self):
|
|
71
|
+
request = {"jsonrpc": "2.0", "id": 5, "method": "nonexistent", "params": {}}
|
|
72
|
+
response = _handle_request(request)
|
|
73
|
+
assert "error" in response
|
|
74
|
+
assert response["error"]["code"] == -32601
|
|
75
|
+
|
|
76
|
+
def test_unknown_tool(self):
|
|
77
|
+
request = {
|
|
78
|
+
"jsonrpc": "2.0", "id": 6,
|
|
79
|
+
"method": "tools/call",
|
|
80
|
+
"params": {"name": "nonexistent_tool", "arguments": {}},
|
|
81
|
+
}
|
|
82
|
+
response = _handle_request(request)
|
|
83
|
+
assert "error" in response
|
|
84
|
+
assert "Unknown tool" in response["error"]["message"]
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class TestCheckSchemaCompliance:
|
|
88
|
+
"""check_schema_compliance MCP tool."""
|
|
89
|
+
|
|
90
|
+
def test_valid_schema(self):
|
|
91
|
+
data = call_tool("check_schema_compliance", {
|
|
92
|
+
"schema_path": str(TEST_DATA_DIR / "valid_sdc4_schema.xsd"),
|
|
93
|
+
})
|
|
94
|
+
assert data["compliant"] is True
|
|
95
|
+
assert data["errors"] == []
|
|
96
|
+
|
|
97
|
+
def test_invalid_schema_with_extension(self):
|
|
98
|
+
data = call_tool("check_schema_compliance", {
|
|
99
|
+
"schema_path": str(TEST_DATA_DIR / "invalid_sdc4_schema_with_extension.xsd"),
|
|
100
|
+
})
|
|
101
|
+
assert data["compliant"] is False
|
|
102
|
+
assert len(data["errors"]) > 0
|
|
103
|
+
assert any("extension" in e.lower() for e in data["errors"])
|
|
104
|
+
|
|
105
|
+
def test_non_sdc4_schema_passes(self):
|
|
106
|
+
data = call_tool("check_schema_compliance", {
|
|
107
|
+
"schema_path": str(TEST_DATA_DIR / "non_sdc4_schema_with_extension.xsd"),
|
|
108
|
+
})
|
|
109
|
+
assert data["compliant"] is True
|
|
110
|
+
|
|
111
|
+
def test_nonexistent_file(self):
|
|
112
|
+
data = call_tool("check_schema_compliance", {
|
|
113
|
+
"schema_path": "/nonexistent/path/schema.xsd",
|
|
114
|
+
})
|
|
115
|
+
assert data["compliant"] is False
|
|
116
|
+
assert any("not found" in e.lower() for e in data["errors"])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class TestConsistentSerialization:
|
|
120
|
+
"""Verify all tools return consistent JSON-RPC format."""
|
|
121
|
+
|
|
122
|
+
def test_all_tools_return_single_text_content(self):
|
|
123
|
+
"""Every tool returns exactly one content block with type=text."""
|
|
124
|
+
test_calls = [
|
|
125
|
+
("check_schema_compliance", {"schema_path": str(TEST_DATA_DIR / "valid_sdc4_schema.xsd")}),
|
|
126
|
+
]
|
|
127
|
+
for tool_name, args in test_calls:
|
|
128
|
+
request = {
|
|
129
|
+
"jsonrpc": "2.0", "id": 1,
|
|
130
|
+
"method": "tools/call",
|
|
131
|
+
"params": {"name": tool_name, "arguments": args},
|
|
132
|
+
}
|
|
133
|
+
response = _handle_request(request)
|
|
134
|
+
assert "result" in response, f"{tool_name} returned error: {response}"
|
|
135
|
+
content = response["result"]["content"]
|
|
136
|
+
assert len(content) == 1, f"{tool_name} returned {len(content)} content blocks"
|
|
137
|
+
assert content[0]["type"] == "text"
|
|
138
|
+
parsed = json.loads(content[0]["text"])
|
|
139
|
+
assert parsed is not None
|
|
@@ -45,7 +45,9 @@ class TestSDC4Validator(unittest.TestCase):
|
|
|
45
45
|
xml_path = Path(f.name)
|
|
46
46
|
|
|
47
47
|
try:
|
|
48
|
-
|
|
48
|
+
# Use lax for this pre-existing fixture (has restriction derivation
|
|
49
|
+
# issues under strict mode that are unrelated to this test's purpose)
|
|
50
|
+
validator = SDC4Validator(self.schema_path, validation='lax')
|
|
49
51
|
result = validator.validate(xml_path)
|
|
50
52
|
|
|
51
53
|
self.assertFalse(result.is_valid)
|
|
@@ -68,7 +70,7 @@ class TestSDC4Validator(unittest.TestCase):
|
|
|
68
70
|
xml_path = Path(f.name)
|
|
69
71
|
|
|
70
72
|
try:
|
|
71
|
-
validator = SDC4Validator(self.schema_path)
|
|
73
|
+
validator = SDC4Validator(self.schema_path, validation='lax')
|
|
72
74
|
result = validator.validate(xml_path)
|
|
73
75
|
|
|
74
76
|
self.assertFalse(result.is_valid)
|
|
@@ -110,7 +112,7 @@ class TestSDC4Validator(unittest.TestCase):
|
|
|
110
112
|
xml_path = Path(f.name)
|
|
111
113
|
|
|
112
114
|
try:
|
|
113
|
-
validator = SDC4Validator(self.schema_path)
|
|
115
|
+
validator = SDC4Validator(self.schema_path, validation='lax')
|
|
114
116
|
result = validator.validate(xml_path)
|
|
115
117
|
|
|
116
118
|
self.assertFalse(result.is_valid)
|
|
@@ -136,7 +138,7 @@ class TestSDC4Validator(unittest.TestCase):
|
|
|
136
138
|
xml_path = Path(f.name)
|
|
137
139
|
|
|
138
140
|
try:
|
|
139
|
-
validator = SDC4Validator(self.schema_path)
|
|
141
|
+
validator = SDC4Validator(self.schema_path, validation='lax')
|
|
140
142
|
structural_errors = validator.validate_structure(xml_path)
|
|
141
143
|
self.assertGreater(len(structural_errors), 0)
|
|
142
144
|
finally:
|
|
@@ -160,7 +162,7 @@ class TestSDC4Validator(unittest.TestCase):
|
|
|
160
162
|
xml_path = Path(f.name)
|
|
161
163
|
|
|
162
164
|
try:
|
|
163
|
-
validator = SDC4Validator(self.schema_path)
|
|
165
|
+
validator = SDC4Validator(self.schema_path, validation='lax')
|
|
164
166
|
structural_errors = validator.validate_structure(xml_path)
|
|
165
167
|
self.assertEqual(len(structural_errors), 0)
|
|
166
168
|
finally:
|
|
@@ -182,7 +184,7 @@ class TestSDC4Validator(unittest.TestCase):
|
|
|
182
184
|
xml_path = Path(f.name)
|
|
183
185
|
|
|
184
186
|
try:
|
|
185
|
-
validator = SDC4Validator(self.schema_path)
|
|
187
|
+
validator = SDC4Validator(self.schema_path, validation='lax')
|
|
186
188
|
report = validator.validate_and_report(xml_path)
|
|
187
189
|
|
|
188
190
|
self.assertFalse(report['valid'])
|
|
@@ -207,5 +209,149 @@ class TestSDC4Validator(unittest.TestCase):
|
|
|
207
209
|
self.assertEqual(result2.error_count, 3)
|
|
208
210
|
|
|
209
211
|
|
|
212
|
+
class TestStrictModeDefault(unittest.TestCase):
|
|
213
|
+
"""Tests that strict mode is the default and catches invalid restrictions."""
|
|
214
|
+
|
|
215
|
+
def test_default_validation_is_strict(self):
|
|
216
|
+
"""SDC4Validator defaults to validation='strict'."""
|
|
217
|
+
# valid_sdc4_schema.xsd is self-contained (no sdc4.xsd include)
|
|
218
|
+
# and has correct restriction derivation — should load fine in strict mode
|
|
219
|
+
schema_path = Path(__file__).parent / 'test_data' / 'valid_sdc4_schema.xsd'
|
|
220
|
+
validator = SDC4Validator(schema_path)
|
|
221
|
+
assert validator is not None
|
|
222
|
+
assert validator.schema is not None
|
|
223
|
+
|
|
224
|
+
def test_strict_mode_catches_invalid_restriction(self):
|
|
225
|
+
"""Strict mode rejects schemas with invalid restriction derivations."""
|
|
226
|
+
# A schema where the restriction's element sequence doesn't match
|
|
227
|
+
# the base type — strict mode should raise during schema loading
|
|
228
|
+
bad_schema = '''\
|
|
229
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
230
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
231
|
+
xmlns:sdc4="https://semanticdatacharter.com/ns/sdc4/"
|
|
232
|
+
targetNamespace="https://semanticdatacharter.com/ns/sdc4/"
|
|
233
|
+
elementFormDefault="qualified">
|
|
234
|
+
|
|
235
|
+
<xsd:complexType name="BaseType">
|
|
236
|
+
<xsd:sequence>
|
|
237
|
+
<xsd:element name="label" type="xsd:string"/>
|
|
238
|
+
<xsd:element name="value" type="xsd:string"/>
|
|
239
|
+
</xsd:sequence>
|
|
240
|
+
</xsd:complexType>
|
|
241
|
+
|
|
242
|
+
<xsd:complexType name="BadRestriction">
|
|
243
|
+
<xsd:complexContent>
|
|
244
|
+
<xsd:restriction base="sdc4:BaseType">
|
|
245
|
+
<xsd:sequence>
|
|
246
|
+
<xsd:element name="label" type="xsd:string"/>
|
|
247
|
+
<xsd:element name="wrong-name" type="xsd:string"/>
|
|
248
|
+
</xsd:sequence>
|
|
249
|
+
</xsd:restriction>
|
|
250
|
+
</xsd:complexContent>
|
|
251
|
+
</xsd:complexType>
|
|
252
|
+
|
|
253
|
+
</xsd:schema>
|
|
254
|
+
'''
|
|
255
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.xsd', delete=False) as f:
|
|
256
|
+
f.write(bad_schema)
|
|
257
|
+
schema_path = Path(f.name)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
from xmlschema.validators.exceptions import XMLSchemaParseError
|
|
261
|
+
with self.assertRaises(XMLSchemaParseError):
|
|
262
|
+
SDC4Validator(schema_path, check_sdc4_compliance=False, validation='strict')
|
|
263
|
+
finally:
|
|
264
|
+
schema_path.unlink()
|
|
265
|
+
|
|
266
|
+
def test_lax_mode_silently_accepts_invalid_restriction(self):
|
|
267
|
+
"""Lax mode silently accepts the same invalid restriction (demonstrating the bug)."""
|
|
268
|
+
bad_schema = '''\
|
|
269
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
270
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
271
|
+
xmlns:sdc4="https://semanticdatacharter.com/ns/sdc4/"
|
|
272
|
+
targetNamespace="https://semanticdatacharter.com/ns/sdc4/"
|
|
273
|
+
elementFormDefault="qualified">
|
|
274
|
+
|
|
275
|
+
<xsd:complexType name="BaseType">
|
|
276
|
+
<xsd:sequence>
|
|
277
|
+
<xsd:element name="label" type="xsd:string"/>
|
|
278
|
+
<xsd:element name="value" type="xsd:string"/>
|
|
279
|
+
</xsd:sequence>
|
|
280
|
+
</xsd:complexType>
|
|
281
|
+
|
|
282
|
+
<xsd:complexType name="BadRestriction">
|
|
283
|
+
<xsd:complexContent>
|
|
284
|
+
<xsd:restriction base="sdc4:BaseType">
|
|
285
|
+
<xsd:sequence>
|
|
286
|
+
<xsd:element name="label" type="xsd:string"/>
|
|
287
|
+
<xsd:element name="wrong-name" type="xsd:string"/>
|
|
288
|
+
</xsd:sequence>
|
|
289
|
+
</xsd:restriction>
|
|
290
|
+
</xsd:complexContent>
|
|
291
|
+
</xsd:complexType>
|
|
292
|
+
|
|
293
|
+
</xsd:schema>
|
|
294
|
+
'''
|
|
295
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.xsd', delete=False) as f:
|
|
296
|
+
f.write(bad_schema)
|
|
297
|
+
schema_path = Path(f.name)
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
# Lax mode accepts this — which is the bug strict mode fixes
|
|
301
|
+
validator = SDC4Validator(
|
|
302
|
+
schema_path, check_sdc4_compliance=False, validation='lax'
|
|
303
|
+
)
|
|
304
|
+
self.assertIsNotNone(validator)
|
|
305
|
+
finally:
|
|
306
|
+
schema_path.unlink()
|
|
307
|
+
|
|
308
|
+
def test_valid_restriction_passes_strict(self):
|
|
309
|
+
"""A correct restriction passes strict mode validation."""
|
|
310
|
+
good_schema = '''\
|
|
311
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
312
|
+
<xsd:schema xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
313
|
+
xmlns:sdc4="https://semanticdatacharter.com/ns/sdc4/"
|
|
314
|
+
targetNamespace="https://semanticdatacharter.com/ns/sdc4/"
|
|
315
|
+
elementFormDefault="qualified">
|
|
316
|
+
|
|
317
|
+
<xsd:complexType name="BaseType">
|
|
318
|
+
<xsd:sequence>
|
|
319
|
+
<xsd:element name="label" type="xsd:string"/>
|
|
320
|
+
<xsd:element name="value" type="xsd:string"/>
|
|
321
|
+
</xsd:sequence>
|
|
322
|
+
</xsd:complexType>
|
|
323
|
+
|
|
324
|
+
<xsd:complexType name="GoodRestriction">
|
|
325
|
+
<xsd:complexContent>
|
|
326
|
+
<xsd:restriction base="sdc4:BaseType">
|
|
327
|
+
<xsd:sequence>
|
|
328
|
+
<xsd:element name="label" type="xsd:string" fixed="Test"/>
|
|
329
|
+
<xsd:element name="value">
|
|
330
|
+
<xsd:simpleType>
|
|
331
|
+
<xsd:restriction base="xsd:string">
|
|
332
|
+
<xsd:maxLength value="100"/>
|
|
333
|
+
</xsd:restriction>
|
|
334
|
+
</xsd:simpleType>
|
|
335
|
+
</xsd:element>
|
|
336
|
+
</xsd:sequence>
|
|
337
|
+
</xsd:restriction>
|
|
338
|
+
</xsd:complexContent>
|
|
339
|
+
</xsd:complexType>
|
|
340
|
+
|
|
341
|
+
</xsd:schema>
|
|
342
|
+
'''
|
|
343
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.xsd', delete=False) as f:
|
|
344
|
+
f.write(good_schema)
|
|
345
|
+
schema_path = Path(f.name)
|
|
346
|
+
|
|
347
|
+
try:
|
|
348
|
+
validator = SDC4Validator(
|
|
349
|
+
schema_path, check_sdc4_compliance=False, validation='strict'
|
|
350
|
+
)
|
|
351
|
+
self.assertIsNotNone(validator)
|
|
352
|
+
finally:
|
|
353
|
+
schema_path.unlink()
|
|
354
|
+
|
|
355
|
+
|
|
210
356
|
if __name__ == '__main__':
|
|
211
357
|
unittest.main()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|