sdcvalidator 4.5.2__tar.gz → 4.6.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.5.2 → sdcvalidator-4.6.0}/PKG-INFO +33 -3
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/README.md +32 -2
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/pyproject.toml +1 -1
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/__init__.py +7 -2
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/exceptions.py +27 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/mcp_server.py +22 -7
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/validator.py +107 -44
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator.egg-info/PKG-INFO +33 -3
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator.egg-info/SOURCES.txt +2 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_recovery.py +1 -1
- sdcvalidator-4.6.0/tests/test_recovery_460.py +119 -0
- sdcvalidator-4.6.0/tests/test_registry_launch.py +79 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/LICENSE +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/setup.cfg +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/cli.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/constants.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/converters.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/error_classifier.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/error_mapper.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/instance_modifier.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/schema_checker.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator/xsd11_restriction.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator.egg-info/dependency_links.txt +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator.egg-info/entry_points.txt +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator.egg-info/requires.txt +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/src/sdcvalidator.egg-info/top_level.txt +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_converters.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_error_classifier.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_error_mapper.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_instance_modifier.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_mcp_server.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_schema_checker.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_validator.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_xsd11_restriction.py +0 -0
- {sdcvalidator-4.5.2 → sdcvalidator-4.6.0}/tests/test_xsi_type.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcvalidator
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.6.0
|
|
4
4
|
Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
|
|
5
5
|
Author: Axius SDC, Inc.
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -104,6 +104,32 @@ tier = classifier.classify(some_xmlschema_error)
|
|
|
104
104
|
# ErrorTier.STRUCTURAL or ErrorTier.SEMANTIC
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
### Exceptional Value recovery
|
|
108
|
+
|
|
109
|
+
A semantic (Tier 2) error does not have to reject a record. Recovery keeps the value and inserts an ISO 21090
|
|
110
|
+
ExceptionalValue beside it (Invalid, for a value that fails its type, pattern, range or enumeration), so the record
|
|
111
|
+
says which value is unusable and why, and the rest of it stays usable.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from sdcvalidator import SDC4Validator, SDC4UnrecoverableError
|
|
115
|
+
|
|
116
|
+
v = SDC4Validator("dm-xxxx.xsd", validation="lax")
|
|
117
|
+
result = v.recover("record.xml") # RecoveryResult; the source is never mutated
|
|
118
|
+
for r in result.recoveries: # Recovery: path, code ("INV"), name, reason, inserted
|
|
119
|
+
print(r.code, r.reason, r.path)
|
|
120
|
+
if not result.ok: # an error no ExceptionalValue covers
|
|
121
|
+
print(result.unrecoverable)
|
|
122
|
+
|
|
123
|
+
tree = v.validate_with_recovery("record.xml", save=False) # raises SDC4UnrecoverableError instead
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- An ExceptionalValue the record already carries (a refusal, a masked field, written where the record was made) is
|
|
127
|
+
kept, and covers its value; recovery never inserts a second one. `remove_existing_ev=True` removes them first.
|
|
128
|
+
- An error recovery cannot cover is returned in `unrecoverable` by `recover()`, and raised as
|
|
129
|
+
`SDC4UnrecoverableError` by `validate_with_recovery()`; it is never skipped.
|
|
130
|
+
- Structural (Tier 1) errors raise `SDC4StructuralValidationError`.
|
|
131
|
+
- Pass an `XMLResource` when the record carries prefixed QNames in content (`xsi:type="sdc4:..."`).
|
|
132
|
+
|
|
107
133
|
### XML/JSON conversion
|
|
108
134
|
|
|
109
135
|
```python
|
|
@@ -151,9 +177,13 @@ sdcvalidator-json2xml data.json schema.xsd -o output.xml
|
|
|
151
177
|
`sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
|
|
152
178
|
|
|
153
179
|
```bash
|
|
154
|
-
sdcvalidator
|
|
180
|
+
sdcvalidator # what an MCP registry client runs (uvx sdcvalidator)
|
|
181
|
+
sdcvalidator mcp # the same, spelled like the other Semantic Data Charter servers
|
|
182
|
+
sdcvalidator-mcp serve --mcp # still works for existing client configurations
|
|
155
183
|
```
|
|
156
184
|
|
|
185
|
+
A client configuration needs only `"command": "uvx", "args": ["sdcvalidator"]`.
|
|
186
|
+
|
|
157
187
|
### Tools
|
|
158
188
|
|
|
159
189
|
| Tool | Purpose |
|
|
@@ -169,7 +199,7 @@ sdcvalidator-mcp serve --mcp
|
|
|
169
199
|
| Tier | Type | Examples | Action |
|
|
170
200
|
|------|------|----------|--------|
|
|
171
201
|
| 1 | Structural | Unknown elements, cardinality violations, wrong nesting | **Reject** |
|
|
172
|
-
| 2 | Semantic | Type errors, pattern violations, enumeration mismatches | **Report
|
|
202
|
+
| 2 | Semantic | Type errors, pattern violations, enumeration mismatches | **Report**, or recover with an ExceptionalValue |
|
|
173
203
|
|
|
174
204
|
## SDC4 Schema Compliance
|
|
175
205
|
|
|
@@ -82,6 +82,32 @@ tier = classifier.classify(some_xmlschema_error)
|
|
|
82
82
|
# ErrorTier.STRUCTURAL or ErrorTier.SEMANTIC
|
|
83
83
|
```
|
|
84
84
|
|
|
85
|
+
### Exceptional Value recovery
|
|
86
|
+
|
|
87
|
+
A semantic (Tier 2) error does not have to reject a record. Recovery keeps the value and inserts an ISO 21090
|
|
88
|
+
ExceptionalValue beside it (Invalid, for a value that fails its type, pattern, range or enumeration), so the record
|
|
89
|
+
says which value is unusable and why, and the rest of it stays usable.
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
from sdcvalidator import SDC4Validator, SDC4UnrecoverableError
|
|
93
|
+
|
|
94
|
+
v = SDC4Validator("dm-xxxx.xsd", validation="lax")
|
|
95
|
+
result = v.recover("record.xml") # RecoveryResult; the source is never mutated
|
|
96
|
+
for r in result.recoveries: # Recovery: path, code ("INV"), name, reason, inserted
|
|
97
|
+
print(r.code, r.reason, r.path)
|
|
98
|
+
if not result.ok: # an error no ExceptionalValue covers
|
|
99
|
+
print(result.unrecoverable)
|
|
100
|
+
|
|
101
|
+
tree = v.validate_with_recovery("record.xml", save=False) # raises SDC4UnrecoverableError instead
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
- An ExceptionalValue the record already carries (a refusal, a masked field, written where the record was made) is
|
|
105
|
+
kept, and covers its value; recovery never inserts a second one. `remove_existing_ev=True` removes them first.
|
|
106
|
+
- An error recovery cannot cover is returned in `unrecoverable` by `recover()`, and raised as
|
|
107
|
+
`SDC4UnrecoverableError` by `validate_with_recovery()`; it is never skipped.
|
|
108
|
+
- Structural (Tier 1) errors raise `SDC4StructuralValidationError`.
|
|
109
|
+
- Pass an `XMLResource` when the record carries prefixed QNames in content (`xsi:type="sdc4:..."`).
|
|
110
|
+
|
|
85
111
|
### XML/JSON conversion
|
|
86
112
|
|
|
87
113
|
```python
|
|
@@ -129,9 +155,13 @@ sdcvalidator-json2xml data.json schema.xsd -o output.xml
|
|
|
129
155
|
`sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
|
|
130
156
|
|
|
131
157
|
```bash
|
|
132
|
-
sdcvalidator
|
|
158
|
+
sdcvalidator # what an MCP registry client runs (uvx sdcvalidator)
|
|
159
|
+
sdcvalidator mcp # the same, spelled like the other Semantic Data Charter servers
|
|
160
|
+
sdcvalidator-mcp serve --mcp # still works for existing client configurations
|
|
133
161
|
```
|
|
134
162
|
|
|
163
|
+
A client configuration needs only `"command": "uvx", "args": ["sdcvalidator"]`.
|
|
164
|
+
|
|
135
165
|
### Tools
|
|
136
166
|
|
|
137
167
|
| Tool | Purpose |
|
|
@@ -147,7 +177,7 @@ sdcvalidator-mcp serve --mcp
|
|
|
147
177
|
| Tier | Type | Examples | Action |
|
|
148
178
|
|------|------|----------|--------|
|
|
149
179
|
| 1 | Structural | Unknown elements, cardinality violations, wrong nesting | **Reject** |
|
|
150
|
-
| 2 | Semantic | Type errors, pattern violations, enumeration mismatches | **Report
|
|
180
|
+
| 2 | Semantic | Type errors, pattern violations, enumeration mismatches | **Report**, or recover with an ExceptionalValue |
|
|
151
181
|
|
|
152
182
|
## SDC4 Schema Compliance
|
|
153
183
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sdcvalidator"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.6.0"
|
|
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"
|
|
@@ -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.
|
|
17
|
+
__version__ = "4.6.0"
|
|
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
|
|
@@ -27,12 +27,14 @@ except ImportError: # pragma: no cover - fallback for older layouts
|
|
|
27
27
|
from .validator import (
|
|
28
28
|
SDC4Validator,
|
|
29
29
|
ValidationResult,
|
|
30
|
+
Recovery,
|
|
31
|
+
RecoveryResult,
|
|
30
32
|
validate_with_recovery,
|
|
31
33
|
)
|
|
32
34
|
from .error_classifier import ErrorClassifier
|
|
33
35
|
from .error_mapper import ErrorMapper
|
|
34
36
|
from .instance_modifier import InstanceModifier
|
|
35
|
-
from .exceptions import SDC4StructuralValidationError
|
|
37
|
+
from .exceptions import SDC4StructuralValidationError, SDC4UnrecoverableError
|
|
36
38
|
from .constants import (
|
|
37
39
|
ErrorTier,
|
|
38
40
|
ExceptionalValueType,
|
|
@@ -56,6 +58,9 @@ __all__ = [
|
|
|
56
58
|
"__version__",
|
|
57
59
|
"SDC4Validator",
|
|
58
60
|
"ValidationResult",
|
|
61
|
+
"Recovery",
|
|
62
|
+
"RecoveryResult",
|
|
63
|
+
"SDC4UnrecoverableError",
|
|
59
64
|
"ErrorClassifier",
|
|
60
65
|
"ErrorMapper",
|
|
61
66
|
"InstanceModifier",
|
|
@@ -61,3 +61,30 @@ class SDC4StructuralValidationError(Exception):
|
|
|
61
61
|
def error_count(self) -> int:
|
|
62
62
|
"""Return the number of structural errors."""
|
|
63
63
|
return len(self.errors)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class SDC4UnrecoverableError(Exception):
|
|
67
|
+
"""
|
|
68
|
+
Raised by `validate_with_recovery` when a semantic (Tier 2) error cannot be
|
|
69
|
+
covered by an ExceptionalValue: no ExceptionalValue maps to it, or the
|
|
70
|
+
element it names cannot be found (4.6.0).
|
|
71
|
+
|
|
72
|
+
Before 4.6.0 such an error was logged and skipped, and the record came back
|
|
73
|
+
as recovered with an invalid value nobody had marked. `SDC4Validator.recover()`
|
|
74
|
+
returns these errors in `RecoveryResult.unrecoverable` instead of raising.
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
def __init__(self, errors: List['XMLSchemaValidationError'], message: str = None):
|
|
78
|
+
self.errors = errors
|
|
79
|
+
if message is None:
|
|
80
|
+
lines = ["Recovery could not cover every error:"]
|
|
81
|
+
for i, error in enumerate(errors, 1):
|
|
82
|
+
lines.append(f" {i}. {error.reason or error}")
|
|
83
|
+
if error.path:
|
|
84
|
+
lines.append(f" at: {error.path}")
|
|
85
|
+
message = "\n".join(lines)
|
|
86
|
+
super().__init__(message)
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def error_count(self) -> int:
|
|
90
|
+
return len(self.errors)
|
|
@@ -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.
|
|
39
|
+
SERVER_VERSION = "4.6.0"
|
|
40
40
|
|
|
41
41
|
# Validator cache: schema_path -> SDC4Validator
|
|
42
42
|
_validators: dict[str, SDC4Validator] = {}
|
|
@@ -290,22 +290,37 @@ def run_stdio() -> None:
|
|
|
290
290
|
sys.stdout.flush()
|
|
291
291
|
|
|
292
292
|
|
|
293
|
-
def main() -> None:
|
|
294
|
-
"""Entry point
|
|
293
|
+
def main(argv: list[str] | None = None) -> None:
|
|
294
|
+
"""Entry point of the ``sdcvalidator`` and ``sdcvalidator-mcp`` console scripts.
|
|
295
|
+
|
|
296
|
+
Three spellings start the stdio server: no arguments at all (what an MCP
|
|
297
|
+
registry client runs: ``uvx sdcvalidator``), ``mcp`` (the spelling the other
|
|
298
|
+
Semantic Data Charter servers use) and ``serve --mcp`` (kept for existing
|
|
299
|
+
client configurations). Until 4.5.3 the bare command printed this help and
|
|
300
|
+
exited, so the registry entry never started a server (issue #15).
|
|
301
|
+
"""
|
|
295
302
|
import argparse
|
|
296
303
|
|
|
297
304
|
parser = argparse.ArgumentParser(
|
|
298
|
-
prog="sdcvalidator
|
|
299
|
-
description="SDC4 structural validator - MCP server",
|
|
305
|
+
prog="sdcvalidator",
|
|
306
|
+
description="SDC4 structural validator - MCP stdio server",
|
|
307
|
+
epilog="With no command the MCP server starts, reading JSON-RPC on stdin.",
|
|
300
308
|
)
|
|
301
309
|
subparsers = parser.add_subparsers(dest="command")
|
|
302
310
|
|
|
311
|
+
subparsers.add_parser("mcp", help="Run the MCP stdio server")
|
|
303
312
|
serve_parser = subparsers.add_parser("serve", help="Start the validation server")
|
|
304
313
|
serve_parser.add_argument("--mcp", action="store_true", help="Run as MCP stdio server")
|
|
305
314
|
|
|
306
|
-
args = parser.parse_args()
|
|
315
|
+
args = parser.parse_args(argv)
|
|
307
316
|
|
|
308
|
-
if args.command == "serve" and args.mcp:
|
|
317
|
+
if args.command is None or args.command == "mcp" or (args.command == "serve" and args.mcp):
|
|
318
|
+
if args.command is None and sys.stdin.isatty():
|
|
319
|
+
sys.stderr.write(
|
|
320
|
+
"sdcvalidator: MCP server reading JSON-RPC 2.0 on stdin "
|
|
321
|
+
"(sdcvalidator --help for options, Ctrl-D to stop)\n"
|
|
322
|
+
)
|
|
323
|
+
sys.stderr.flush()
|
|
309
324
|
run_stdio()
|
|
310
325
|
else:
|
|
311
326
|
parser.print_help()
|
|
@@ -31,7 +31,8 @@ from xmlschema.validators.exceptions import XMLSchemaValidationError
|
|
|
31
31
|
from .error_classifier import ErrorClassifier
|
|
32
32
|
from .error_mapper import ErrorMapper
|
|
33
33
|
from .instance_modifier import InstanceModifier
|
|
34
|
-
from .exceptions import SDC4StructuralValidationError
|
|
34
|
+
from .exceptions import SDC4StructuralValidationError, SDC4UnrecoverableError
|
|
35
|
+
from .constants import EXCEPTIONAL_VALUE_CODES, ExceptionalValueType
|
|
35
36
|
from .schema_checker import (
|
|
36
37
|
validate_sdc4_schema_compliance,
|
|
37
38
|
SDC4SchemaValidationError
|
|
@@ -52,6 +53,33 @@ class ValidationResult:
|
|
|
52
53
|
return len(self.structural_errors) + len(self.semantic_errors)
|
|
53
54
|
|
|
54
55
|
|
|
56
|
+
@dataclass
|
|
57
|
+
class Recovery:
|
|
58
|
+
"""One value an ExceptionalValue covers after recovery."""
|
|
59
|
+
path: str # the Clark-form path of the element whose value failed (as xmlschema reports it)
|
|
60
|
+
code: str # the ISO 21090 code: "INV"
|
|
61
|
+
name: str # its name: "Invalid"
|
|
62
|
+
reason: str # what failed, from the validator: the constraint, the pattern, the type
|
|
63
|
+
inserted: bool # True if recovery inserted the ExceptionalValue; False if the record already carried one
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class RecoveryResult:
|
|
68
|
+
"""What `SDC4Validator.recover()` did: the recovered tree, each value it covered, and each error it could not."""
|
|
69
|
+
tree: ET.ElementTree
|
|
70
|
+
recoveries: List[Recovery] = field(default_factory=list)
|
|
71
|
+
unrecoverable: List[XMLSchemaValidationError] = field(default_factory=list)
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def ok(self) -> bool:
|
|
75
|
+
"""True when every error is covered by an ExceptionalValue."""
|
|
76
|
+
return not self.unrecoverable
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def inserted(self) -> List[Recovery]:
|
|
80
|
+
return [r for r in self.recoveries if r.inserted]
|
|
81
|
+
|
|
82
|
+
|
|
55
83
|
class SDC4Validator:
|
|
56
84
|
"""
|
|
57
85
|
Validates XML instances against SDC4 data model schemas.
|
|
@@ -296,35 +324,37 @@ class SDC4Validator:
|
|
|
296
324
|
# ExceptionalValue recovery (quarantine-and-tag)
|
|
297
325
|
# ------------------------------------------------------------------ #
|
|
298
326
|
|
|
299
|
-
def
|
|
327
|
+
def recover(
|
|
300
328
|
self,
|
|
301
329
|
xml_source: Union[str, Path, ET.Element, XMLResource],
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
save: bool = True,
|
|
305
|
-
) -> ET.ElementTree:
|
|
330
|
+
remove_existing_ev: bool = False,
|
|
331
|
+
) -> RecoveryResult:
|
|
306
332
|
"""
|
|
307
|
-
Validate an XML instance and
|
|
333
|
+
Validate an XML instance and cover each semantic error with an ExceptionalValue.
|
|
308
334
|
|
|
309
335
|
Tier 1 (structural) errors cause a hard reject (SDC4StructuralValidationError).
|
|
310
|
-
Tier 2 (semantic) errors are quarantined
|
|
336
|
+
Tier 2 (semantic) errors are quarantined: the value stays in the record and an
|
|
337
|
+
ExceptionalValue is inserted beside it, once per value. A value whose element
|
|
338
|
+
already carries an ExceptionalValue (one written where the record was made: a
|
|
339
|
+
refusal, a masked field) is covered by it; nothing is inserted and nothing is
|
|
340
|
+
removed. An error that cannot be covered (no ExceptionalValue maps to it, or
|
|
341
|
+
the element cannot be found) is returned in `unrecoverable`, never skipped.
|
|
311
342
|
|
|
312
|
-
|
|
313
|
-
|
|
343
|
+
Args:
|
|
344
|
+
xml_source: A path, an Element, or an XMLResource (use an XMLResource when
|
|
345
|
+
the instance carries prefixed QNames in content, such as xsi:type).
|
|
346
|
+
remove_existing_ev: Remove every ExceptionalValue already in the record
|
|
347
|
+
first. Off by default since 4.6.0: those written at the source are
|
|
348
|
+
data, and removing them loses why a value is missing.
|
|
314
349
|
|
|
315
350
|
Returns:
|
|
316
|
-
|
|
351
|
+
RecoveryResult: the recovered tree (a copy; the source is never mutated),
|
|
352
|
+
the recoveries, and the unrecoverable errors.
|
|
317
353
|
|
|
318
354
|
Raises:
|
|
319
355
|
SDC4StructuralValidationError: If structural (Tier 1) errors are present.
|
|
320
|
-
ValueError: If save=True but output_path cannot be determined.
|
|
321
356
|
"""
|
|
322
|
-
original_file_path = None
|
|
323
|
-
if isinstance(xml_source, (str, Path)):
|
|
324
|
-
original_file_path = Path(xml_source)
|
|
325
|
-
|
|
326
357
|
tree = self._to_tree(xml_source)
|
|
327
|
-
# Work on a deep copy so the caller's source is never mutated.
|
|
328
358
|
root = copy.deepcopy(tree.getroot())
|
|
329
359
|
tree = ET.ElementTree(root)
|
|
330
360
|
|
|
@@ -332,37 +362,70 @@ class SDC4Validator:
|
|
|
332
362
|
self.instance_modifier.remove_existing_exceptional_values(root)
|
|
333
363
|
|
|
334
364
|
errors = list(self._schema_errors(tree, xml_source))
|
|
365
|
+
structural = [e for e in errors if self.error_mapper.is_structural_error(e)]
|
|
366
|
+
if structural:
|
|
367
|
+
raise SDC4StructuralValidationError(structural)
|
|
335
368
|
|
|
336
|
-
|
|
337
|
-
|
|
369
|
+
result = RecoveryResult(tree=tree)
|
|
370
|
+
covered: Dict[int, Recovery] = {}
|
|
338
371
|
for error in errors:
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
ev_type = self.error_mapper.map_error(error)
|
|
373
|
+
reason = " ".join((error.reason or error.message or "").split())
|
|
374
|
+
target = self.instance_modifier._find_element_by_xpath(root, error.path) if error.path else None
|
|
375
|
+
parent = self.instance_modifier._find_parent_element(root, target) if target is not None else None
|
|
376
|
+
if ev_type is None or parent is None:
|
|
377
|
+
result.unrecoverable.append(error)
|
|
378
|
+
continue
|
|
379
|
+
if id(parent) in covered: # a second failure in a value already covered
|
|
380
|
+
continue
|
|
381
|
+
existing = next((c for c in parent if isinstance(c.tag, str)
|
|
382
|
+
and c.tag.rsplit("}", 1)[-1] in EXCEPTIONAL_VALUE_CODES), None)
|
|
383
|
+
if existing is not None:
|
|
384
|
+
code = existing.tag.rsplit("}", 1)[-1]
|
|
385
|
+
rec = Recovery(path=error.path, code=code, name=ExceptionalValueType.from_code(code).ev_name,
|
|
386
|
+
reason=reason, inserted=False)
|
|
387
|
+
elif self.instance_modifier.insert_exceptional_value(
|
|
388
|
+
root=root, xpath=error.path, ev_type=ev_type, reason=error.reason):
|
|
389
|
+
rec = Recovery(path=error.path, code=ev_type.code, name=ev_type.ev_name, reason=reason, inserted=True)
|
|
341
390
|
else:
|
|
342
|
-
|
|
391
|
+
result.unrecoverable.append(error)
|
|
392
|
+
continue
|
|
393
|
+
covered[id(parent)] = rec
|
|
394
|
+
result.recoveries.append(rec)
|
|
395
|
+
return result
|
|
343
396
|
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
397
|
+
def validate_with_recovery(
|
|
398
|
+
self,
|
|
399
|
+
xml_source: Union[str, Path, ET.Element, XMLResource],
|
|
400
|
+
output_path: Optional[Union[str, Path]] = None,
|
|
401
|
+
remove_existing_ev: bool = False,
|
|
402
|
+
save: bool = True,
|
|
403
|
+
) -> ET.ElementTree:
|
|
404
|
+
"""
|
|
405
|
+
Validate an XML instance and insert ExceptionalValue elements for errors.
|
|
347
406
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
407
|
+
Tier 1 (structural) errors cause a hard reject (SDC4StructuralValidationError).
|
|
408
|
+
Tier 2 (semantic) errors are quarantined with ExceptionalValue tags (see
|
|
409
|
+
`recover()`, which also returns what was covered and why). An error that
|
|
410
|
+
cannot be covered raises SDC4UnrecoverableError rather than returning a
|
|
411
|
+
record with an invalid value nobody marked (4.6.0).
|
|
412
|
+
|
|
413
|
+
The recovered XML is saved unless save=False. When saving and no
|
|
414
|
+
output_path is given, defaults to '{stem}-ev{suffix}' beside the input.
|
|
415
|
+
|
|
416
|
+
Returns:
|
|
417
|
+
Modified XML ElementTree with ExceptionalValue elements inserted.
|
|
418
|
+
|
|
419
|
+
Raises:
|
|
420
|
+
SDC4StructuralValidationError: If structural (Tier 1) errors are present.
|
|
421
|
+
SDC4UnrecoverableError: If a semantic error cannot be covered.
|
|
422
|
+
ValueError: If save=True but output_path cannot be determined.
|
|
423
|
+
"""
|
|
424
|
+
original_file_path = Path(xml_source) if isinstance(xml_source, (str, Path)) else None
|
|
425
|
+
result = self.recover(xml_source, remove_existing_ev=remove_existing_ev)
|
|
426
|
+
if result.unrecoverable:
|
|
427
|
+
raise SDC4UnrecoverableError(result.unrecoverable)
|
|
428
|
+
tree = result.tree
|
|
366
429
|
|
|
367
430
|
if save:
|
|
368
431
|
if output_path is None:
|
|
@@ -396,7 +459,7 @@ class SDC4Validator:
|
|
|
396
459
|
self,
|
|
397
460
|
output_path: Union[str, Path],
|
|
398
461
|
xml_source: Union[str, Path, ET.Element, XMLResource],
|
|
399
|
-
remove_existing_ev: bool =
|
|
462
|
+
remove_existing_ev: bool = False,
|
|
400
463
|
encoding: str = 'UTF-8',
|
|
401
464
|
xml_declaration: bool = True,
|
|
402
465
|
):
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcvalidator
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.6.0
|
|
4
4
|
Summary: SDC4 structural validator with ExceptionalValue recovery — thin wrapper over xmlschema
|
|
5
5
|
Author: Axius SDC, Inc.
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -104,6 +104,32 @@ tier = classifier.classify(some_xmlschema_error)
|
|
|
104
104
|
# ErrorTier.STRUCTURAL or ErrorTier.SEMANTIC
|
|
105
105
|
```
|
|
106
106
|
|
|
107
|
+
### Exceptional Value recovery
|
|
108
|
+
|
|
109
|
+
A semantic (Tier 2) error does not have to reject a record. Recovery keeps the value and inserts an ISO 21090
|
|
110
|
+
ExceptionalValue beside it (Invalid, for a value that fails its type, pattern, range or enumeration), so the record
|
|
111
|
+
says which value is unusable and why, and the rest of it stays usable.
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from sdcvalidator import SDC4Validator, SDC4UnrecoverableError
|
|
115
|
+
|
|
116
|
+
v = SDC4Validator("dm-xxxx.xsd", validation="lax")
|
|
117
|
+
result = v.recover("record.xml") # RecoveryResult; the source is never mutated
|
|
118
|
+
for r in result.recoveries: # Recovery: path, code ("INV"), name, reason, inserted
|
|
119
|
+
print(r.code, r.reason, r.path)
|
|
120
|
+
if not result.ok: # an error no ExceptionalValue covers
|
|
121
|
+
print(result.unrecoverable)
|
|
122
|
+
|
|
123
|
+
tree = v.validate_with_recovery("record.xml", save=False) # raises SDC4UnrecoverableError instead
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- An ExceptionalValue the record already carries (a refusal, a masked field, written where the record was made) is
|
|
127
|
+
kept, and covers its value; recovery never inserts a second one. `remove_existing_ev=True` removes them first.
|
|
128
|
+
- An error recovery cannot cover is returned in `unrecoverable` by `recover()`, and raised as
|
|
129
|
+
`SDC4UnrecoverableError` by `validate_with_recovery()`; it is never skipped.
|
|
130
|
+
- Structural (Tier 1) errors raise `SDC4StructuralValidationError`.
|
|
131
|
+
- Pass an `XMLResource` when the record carries prefixed QNames in content (`xsi:type="sdc4:..."`).
|
|
132
|
+
|
|
107
133
|
### XML/JSON conversion
|
|
108
134
|
|
|
109
135
|
```python
|
|
@@ -151,9 +177,13 @@ sdcvalidator-json2xml data.json schema.xsd -o output.xml
|
|
|
151
177
|
`sdcvalidator` ships a stdio MCP (Model Context Protocol) server so any MCP-capable agent can validate SDC4 data without importing the Python library. It implements JSON-RPC 2.0 directly over stdio — no external MCP SDK dependency.
|
|
152
178
|
|
|
153
179
|
```bash
|
|
154
|
-
sdcvalidator
|
|
180
|
+
sdcvalidator # what an MCP registry client runs (uvx sdcvalidator)
|
|
181
|
+
sdcvalidator mcp # the same, spelled like the other Semantic Data Charter servers
|
|
182
|
+
sdcvalidator-mcp serve --mcp # still works for existing client configurations
|
|
155
183
|
```
|
|
156
184
|
|
|
185
|
+
A client configuration needs only `"command": "uvx", "args": ["sdcvalidator"]`.
|
|
186
|
+
|
|
157
187
|
### Tools
|
|
158
188
|
|
|
159
189
|
| Tool | Purpose |
|
|
@@ -169,7 +199,7 @@ sdcvalidator-mcp serve --mcp
|
|
|
169
199
|
| Tier | Type | Examples | Action |
|
|
170
200
|
|------|------|----------|--------|
|
|
171
201
|
| 1 | Structural | Unknown elements, cardinality violations, wrong nesting | **Reject** |
|
|
172
|
-
| 2 | Semantic | Type errors, pattern violations, enumeration mismatches | **Report
|
|
202
|
+
| 2 | Semantic | Type errors, pattern violations, enumeration mismatches | **Report**, or recover with an ExceptionalValue |
|
|
173
203
|
|
|
174
204
|
## SDC4 Schema Compliance
|
|
175
205
|
|
|
@@ -25,6 +25,8 @@ tests/test_error_mapper.py
|
|
|
25
25
|
tests/test_instance_modifier.py
|
|
26
26
|
tests/test_mcp_server.py
|
|
27
27
|
tests/test_recovery.py
|
|
28
|
+
tests/test_recovery_460.py
|
|
29
|
+
tests/test_registry_launch.py
|
|
28
30
|
tests/test_schema_checker.py
|
|
29
31
|
tests/test_validator.py
|
|
30
32
|
tests/test_xsd11_restriction.py
|
|
@@ -72,7 +72,7 @@ class TestSDC4Recovery(unittest.TestCase):
|
|
|
72
72
|
<label>Adult Population</label>
|
|
73
73
|
<xdcount-value>not_a_number</xdcount-value>
|
|
74
74
|
<xdcount-units>
|
|
75
|
-
<label>
|
|
75
|
+
<label>Adult Population_units</label>
|
|
76
76
|
<xdstring-value>people</xdstring-value>
|
|
77
77
|
</xdcount-units>
|
|
78
78
|
</sdc4:ms-q1ey1sf5otsa97e76kb06hco>
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
4.6.0: recovery keeps the ExceptionalValues a record already carries, reports
|
|
12
|
+
what it covered as data, and never skips an error it cannot cover.
|
|
13
|
+
|
|
14
|
+
Before 4.6.0, `validate_with_recovery` removed every ExceptionalValue in the
|
|
15
|
+
record first (so a refusal written where the record was made was lost whenever
|
|
16
|
+
another value failed) and logged and skipped an error no ExceptionalValue maps
|
|
17
|
+
to (so the record came back "recovered" with an invalid value nobody marked).
|
|
18
|
+
"""
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from xml.etree import ElementTree as ET
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
from sdcvalidator import (SDC4UnrecoverableError, SDC4Validator, Recovery, RecoveryResult)
|
|
25
|
+
|
|
26
|
+
SCHEMA = Path(__file__).parent / "test_data" / "dm-jsi5yxnvzsmsisgn2bvelkni.xsd"
|
|
27
|
+
SDC4 = "https://semanticdatacharter.com/ns/sdc4/"
|
|
28
|
+
ASKR = '<sdc4:ASKR><ev-name>Asked and Refused</ev-name></sdc4:ASKR>'
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def record(count="5", ev="", units_label="Adult Population_units"):
|
|
32
|
+
return f'''<?xml version="1.0" encoding="UTF-8"?>
|
|
33
|
+
<sdc4:dm-jsi5yxnvzsmsisgn2bvelkni xmlns:sdc4="{SDC4}">
|
|
34
|
+
<dm-label>StatePopulation</dm-label>
|
|
35
|
+
<dm-language>en-US</dm-language>
|
|
36
|
+
<dm-encoding>utf-8</dm-encoding>
|
|
37
|
+
<sdc4:ms-wnpz4qyrk369gnsivfsmysdf>
|
|
38
|
+
<label>StatePopulation Data Cluster</label>
|
|
39
|
+
<sdc4:ms-ahfdavxt7dpx960rqi1qtb0l>
|
|
40
|
+
<sdc4:ms-q1ey1sf5otsa97e76kb06hco>
|
|
41
|
+
<label>Adult Population</label>
|
|
42
|
+
{ev}
|
|
43
|
+
<xdcount-value>{count}</xdcount-value>
|
|
44
|
+
<xdcount-units>
|
|
45
|
+
<label>{units_label}</label>
|
|
46
|
+
<xdstring-value>people</xdstring-value>
|
|
47
|
+
</xdcount-units>
|
|
48
|
+
</sdc4:ms-q1ey1sf5otsa97e76kb06hco>
|
|
49
|
+
</sdc4:ms-ahfdavxt7dpx960rqi1qtb0l>
|
|
50
|
+
</sdc4:ms-wnpz4qyrk369gnsivfsmysdf>
|
|
51
|
+
</sdc4:dm-jsi5yxnvzsmsisgn2bvelkni>
|
|
52
|
+
'''
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@pytest.fixture(scope="module")
|
|
56
|
+
def validator():
|
|
57
|
+
if not SCHEMA.exists():
|
|
58
|
+
pytest.skip("SDC4 example schema not found")
|
|
59
|
+
return SDC4Validator(SCHEMA, validation="lax")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def element(xml):
|
|
63
|
+
return ET.fromstring(xml)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def evs(tree):
|
|
67
|
+
comp = next(e for e in tree.getroot().iter() if e.tag == f"{{{SDC4}}}ms-q1ey1sf5otsa97e76kb06hco")
|
|
68
|
+
return [c.tag.split("}")[1] for c in comp if c.tag.startswith(f"{{{SDC4}}}")]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def test_recover_reports_what_it_covered_as_data(validator):
|
|
72
|
+
r = validator.recover(element(record(count="not_a_number")))
|
|
73
|
+
assert isinstance(r, RecoveryResult) and r.ok
|
|
74
|
+
(rec,) = r.recoveries
|
|
75
|
+
assert isinstance(rec, Recovery)
|
|
76
|
+
assert (rec.code, rec.name, rec.inserted) == ("INV", "Invalid", True)
|
|
77
|
+
assert "not_a_number" in rec.reason and rec.path.endswith("xdcount-value")
|
|
78
|
+
assert evs(r.tree) == ["INV"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_a_valid_record_has_nothing_to_recover(validator):
|
|
82
|
+
r = validator.recover(element(record()))
|
|
83
|
+
assert r.ok and r.recoveries == [] and evs(r.tree) == []
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_an_exceptional_value_written_at_the_source_is_kept(validator):
|
|
87
|
+
tree = validator.validate_with_recovery(element(record(ev=ASKR)), save=False)
|
|
88
|
+
assert evs(tree) == ["ASKR"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_one_already_covering_a_bad_value_is_kept_and_nothing_is_added(validator):
|
|
92
|
+
r = validator.recover(element(record(count="not_a_number", ev=ASKR)))
|
|
93
|
+
(rec,) = r.recoveries
|
|
94
|
+
assert (rec.code, rec.inserted) == ("ASKR", False)
|
|
95
|
+
assert evs(r.tree) == ["ASKR"], "one ExceptionalValue per value"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_removing_existing_ones_is_still_available_when_asked(validator):
|
|
99
|
+
r = validator.recover(element(record(count="not_a_number", ev=ASKR)), remove_existing_ev=True)
|
|
100
|
+
assert evs(r.tree) == ["INV"] and r.recoveries[0].inserted
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_an_error_nothing_covers_is_returned_not_skipped(validator):
|
|
104
|
+
r = validator.recover(element(record(units_label="Count Units")))
|
|
105
|
+
assert not r.ok and len(r.unrecoverable) == 1
|
|
106
|
+
assert "fixed value" in r.unrecoverable[0].reason
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def test_validate_with_recovery_raises_rather_than_return_an_unmarked_invalid_value(validator):
|
|
110
|
+
with pytest.raises(SDC4UnrecoverableError) as exc:
|
|
111
|
+
validator.validate_with_recovery(element(record(units_label="Count Units")), save=False)
|
|
112
|
+
assert exc.value.error_count == 1 and "fixed value" in str(exc.value)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def test_the_source_is_never_mutated(validator):
|
|
116
|
+
root = element(record(count="not_a_number", ev=ASKR))
|
|
117
|
+
before = ET.tostring(root)
|
|
118
|
+
validator.recover(root)
|
|
119
|
+
assert ET.tostring(root) == before
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""
|
|
2
|
+
The MCP registry entry starts the server (issue #15).
|
|
3
|
+
|
|
4
|
+
``server.json`` declared no ``packageArguments``, so ``uvx sdcvalidator`` ran
|
|
5
|
+
``main()`` with no arguments, which printed the CLI help and exited. These
|
|
6
|
+
tests start the console script's entry point the way a registry client, a
|
|
7
|
+
hand-written configuration and the pre-4.5.3 configuration do, and check that
|
|
8
|
+
``server.json`` names the released version.
|
|
9
|
+
"""
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
import sys
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
|
|
18
|
+
import sdcvalidator
|
|
19
|
+
from sdcvalidator.mcp_server import SERVER_VERSION
|
|
20
|
+
|
|
21
|
+
ROOT = Path(__file__).resolve().parents[1]
|
|
22
|
+
SERVER_JSON = json.loads((ROOT / "server.json").read_text(encoding="utf-8"))
|
|
23
|
+
|
|
24
|
+
INITIALIZE = {"jsonrpc": "2.0", "id": 1, "method": "initialize",
|
|
25
|
+
"params": {"protocolVersion": "2024-11-05", "capabilities": {}, "clientInfo": {"name": "t", "version": "0"}}}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _launch(args):
|
|
29
|
+
proc = subprocess.run(
|
|
30
|
+
[sys.executable, "-m", "sdcvalidator.mcp_server", *args],
|
|
31
|
+
input=json.dumps(INITIALIZE) + "\n",
|
|
32
|
+
capture_output=True, text=True, timeout=60,
|
|
33
|
+
)
|
|
34
|
+
return proc
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _registry_args():
|
|
38
|
+
package = SERVER_JSON["packages"][0]
|
|
39
|
+
return [a["value"] for a in package.get("packageArguments", []) if a["type"] == "positional"]
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@pytest.mark.parametrize("args", [
|
|
43
|
+
pytest.param([], id="bare, what a registry client without packageArguments runs"),
|
|
44
|
+
pytest.param(_registry_args(), id="server.json packageArguments"),
|
|
45
|
+
pytest.param(["mcp"], id="mcp"),
|
|
46
|
+
pytest.param(["serve", "--mcp"], id="serve --mcp (pre-4.5.3 configurations)"),
|
|
47
|
+
])
|
|
48
|
+
def test_each_launch_spelling_answers_initialize(args):
|
|
49
|
+
proc = _launch(args)
|
|
50
|
+
assert proc.returncode == 0, proc.stderr
|
|
51
|
+
lines = [line for line in proc.stdout.splitlines() if line.strip()]
|
|
52
|
+
assert lines, f"no JSON-RPC response; stdout was help text?\n{proc.stdout}"
|
|
53
|
+
response = json.loads(lines[0])
|
|
54
|
+
assert response["id"] == 1
|
|
55
|
+
assert response["result"]["serverInfo"]["name"] == "sdcvalidator"
|
|
56
|
+
assert response["result"]["serverInfo"]["version"] == SERVER_VERSION
|
|
57
|
+
assert "usage:" not in proc.stdout
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_serve_without_mcp_still_prints_help():
|
|
61
|
+
proc = _launch(["serve"])
|
|
62
|
+
assert proc.returncode == 0
|
|
63
|
+
assert "usage:" in proc.stdout
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_server_json_declares_the_mcp_launch():
|
|
67
|
+
package = SERVER_JSON["packages"][0]
|
|
68
|
+
assert package["identifier"] == "sdcvalidator"
|
|
69
|
+
assert package["runtimeHint"] == "uvx"
|
|
70
|
+
assert _registry_args() == ["mcp"]
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_server_json_names_the_released_version():
|
|
74
|
+
# A regex, not tomllib: the test matrix includes Python 3.10.
|
|
75
|
+
pyproject = re.search(r'^version = "([^"]+)"', (ROOT / "pyproject.toml").read_text(encoding="utf-8"), re.M).group(1)
|
|
76
|
+
assert SERVER_JSON["version"] == pyproject
|
|
77
|
+
assert SERVER_JSON["packages"][0]["version"] == pyproject
|
|
78
|
+
assert sdcvalidator.__version__ == pyproject
|
|
79
|
+
assert SERVER_VERSION == pyproject
|
|
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
|
|
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
|