sdcreader 4.3.0__tar.gz → 4.4.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.
- {sdcreader-4.3.0/src/sdcreader.egg-info → sdcreader-4.4.0}/PKG-INFO +5 -3
- {sdcreader-4.3.0 → sdcreader-4.4.0}/README.md +4 -2
- {sdcreader-4.3.0 → sdcreader-4.4.0}/pyproject.toml +1 -1
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/__init__.py +3 -3
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/mcp_server.py +2 -1
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/record.py +42 -10
- {sdcreader-4.3.0 → sdcreader-4.4.0/src/sdcreader.egg-info}/PKG-INFO +5 -3
- {sdcreader-4.3.0 → sdcreader-4.4.0}/tests/test_record.py +40 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/LICENSE +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/NOTICE +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/setup.cfg +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/mcp.py +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/model.py +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/package.py +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/SOURCES.txt +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/dependency_links.txt +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/entry_points.txt +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/requires.txt +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/top_level.txt +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/tests/test_mcp_scaffold.py +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/tests/test_mcp_server.py +0 -0
- {sdcreader-4.3.0 → sdcreader-4.4.0}/tests/test_reader.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcreader
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: Read a published Semantic Data Charter model's package for projection into other standards (CDIF, DCAT-US, DCAT-AP, HealthDCAT-AP); MCP server included.
|
|
5
5
|
Author: Axius SDC, Inc.
|
|
6
6
|
License: Apache-2.0
|
|
@@ -97,8 +97,10 @@ that defines it. `source` is the record's bytes, its text or a path; a record of
|
|
|
97
97
|
|
|
98
98
|
- **`Record`**: `attributes` (`instance_id`, `instance_version`, `creation_timestamp`, `current-state`, ...),
|
|
99
99
|
`values` in document order, and `exceptional`, the values that carry an Exceptional Value.
|
|
100
|
-
- **`RecordValue`**: the `component`, its `path` of clusters (root first),
|
|
101
|
-
|
|
100
|
+
- **`RecordValue`**: the `component`, its `path` of clusters (root first), `instances` parallel to the path (which
|
|
101
|
+
occurrence each cluster is among the same cluster's repeats under the same parent; `instance_key` pairs them with the
|
|
102
|
+
clusters' identifiers, one key per cluster instance), an `occurrence` index for a component repeated within its
|
|
103
|
+
cluster instance, its `position`, its `elements` as the record holds them (a quantity's units nested:
|
|
102
104
|
`{"xdquantity-value": "1", "xdquantity-units": {"label": "Trade Units", "xdstring-value": "EA"}}`), and its
|
|
103
105
|
`context` (`act`, `vtb`, `vte`, `tr`, `modified`, `latitude`, `longitude`).
|
|
104
106
|
- **`ExceptionalValue`**: the ISO 21090 null flavor (`code`, `name`) and its `origin`: `source` when it stands in
|
|
@@ -72,8 +72,10 @@ that defines it. `source` is the record's bytes, its text or a path; a record of
|
|
|
72
72
|
|
|
73
73
|
- **`Record`**: `attributes` (`instance_id`, `instance_version`, `creation_timestamp`, `current-state`, ...),
|
|
74
74
|
`values` in document order, and `exceptional`, the values that carry an Exceptional Value.
|
|
75
|
-
- **`RecordValue`**: the `component`, its `path` of clusters (root first),
|
|
76
|
-
|
|
75
|
+
- **`RecordValue`**: the `component`, its `path` of clusters (root first), `instances` parallel to the path (which
|
|
76
|
+
occurrence each cluster is among the same cluster's repeats under the same parent; `instance_key` pairs them with the
|
|
77
|
+
clusters' identifiers, one key per cluster instance), an `occurrence` index for a component repeated within its
|
|
78
|
+
cluster instance, its `position`, its `elements` as the record holds them (a quantity's units nested:
|
|
77
79
|
`{"xdquantity-value": "1", "xdquantity-units": {"label": "Trade Units", "xdstring-value": "EA"}}`), and its
|
|
78
80
|
`context` (`act`, `vtb`, `vte`, `tr`, `modified`, `latitude`, `longitude`).
|
|
79
81
|
- **`ExceptionalValue`**: the ISO 21090 null flavor (`code`, `name`) and its `origin`: `source` when it stands in
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sdcreader"
|
|
7
|
-
version = "4.
|
|
7
|
+
version = "4.4.0"
|
|
8
8
|
description = "Read a published Semantic Data Charter model's package for projection into other standards (CDIF, DCAT-US, DCAT-AP, HealthDCAT-AP); MCP server included."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -14,10 +14,10 @@ to the component that defines it, Exceptional Values included, for record-level
|
|
|
14
14
|
from .model import (CLOSE, DC_DEFAULTS, EXACT, HAS_UNIT, IDENTIFIER, LEAF_TYPES, NUMERIC_TYPES, PUBLISHER, QUANTIFIED_TYPES, SEE_ALSO,
|
|
15
15
|
SOURCE, Code, Component, Leaf, Model, enumeration_iri, read_header, read_model)
|
|
16
16
|
from .package import DEFAULT_HOST, ModelPackage, PackageError, fetch_package, load_package
|
|
17
|
-
from .record import CONTEXT_ELEMENTS, EXCEPTIONAL_VALUES, RECORD_ATTRIBUTES, ExceptionalValue, Record, RecordError, RecordValue, read_record
|
|
17
|
+
from .record import CONTEXT_ELEMENTS, EXCEPTIONAL_VALUES, RECORD_ATTRIBUTES, VALUE_ELEMENTS, ExceptionalValue, Record, RecordError, RecordValue, read_record
|
|
18
18
|
|
|
19
|
-
__version__ = "4.
|
|
19
|
+
__version__ = "4.4.0"
|
|
20
20
|
__all__ = ["ModelPackage", "PackageError", "DEFAULT_HOST", "load_package", "fetch_package", "read_model", "read_header", "enumeration_iri",
|
|
21
21
|
"Model", "Component", "Leaf", "Code", "LEAF_TYPES", "NUMERIC_TYPES", "QUANTIFIED_TYPES", "DC_DEFAULTS", "IDENTIFIER", "SOURCE",
|
|
22
22
|
"PUBLISHER", "EXACT", "CLOSE", "SEE_ALSO", "HAS_UNIT", "read_record", "Record", "RecordValue", "RecordError", "ExceptionalValue",
|
|
23
|
-
"EXCEPTIONAL_VALUES", "RECORD_ATTRIBUTES", "CONTEXT_ELEMENTS", "__version__"]
|
|
23
|
+
"EXCEPTIONAL_VALUES", "RECORD_ATTRIBUTES", "CONTEXT_ELEMENTS", "VALUE_ELEMENTS", "__version__"]
|
|
@@ -311,7 +311,8 @@ def _handle_read_record(args: dict[str, Any]) -> Any:
|
|
|
311
311
|
values = [
|
|
312
312
|
{
|
|
313
313
|
"position": v.position, "ct_id": v.component.ct_id, "label": v.component.label, "sdc_type": v.component.sdc_type,
|
|
314
|
-
"path": [p.label for p in v.path], "
|
|
314
|
+
"path": [p.label for p in v.path], "instances": list(v.instances), "occurrence": v.occurrence,
|
|
315
|
+
"elements": v.elements, "context": v.context,
|
|
315
316
|
"exceptional": ({"code": v.exceptional.code, "name": v.exceptional.name, "origin": v.exceptional.origin}
|
|
316
317
|
if v.exceptional else None),
|
|
317
318
|
}
|
|
@@ -39,6 +39,18 @@ RECORD_ATTRIBUTES = ("dm-label", "dm-language", "dm-encoding", "creation_timesta
|
|
|
39
39
|
#: Per-value context elements: what the record says about a value rather than the value itself.
|
|
40
40
|
CONTEXT_ELEMENTS = ("act", "vtb", "vte", "tr", "modified", "latitude", "longitude")
|
|
41
41
|
|
|
42
|
+
#: The elements that hold a value, by the SDC4 reference model (sdc4.xsd), for every value type. A recovery Exceptional
|
|
43
|
+
#: Value stands beside one of these; one written at the source stands in place of them. Units, language, magnitude
|
|
44
|
+
#: status, accuracy, reference ranges and a link's relation describe a value and are not in this set.
|
|
45
|
+
VALUE_ELEMENTS = frozenset({
|
|
46
|
+
"xdstring-value", "xdtoken-value", "true-value", "false-value",
|
|
47
|
+
"xdcount-value", "xdquantity-value", "xdfloat-value", "xddouble-value",
|
|
48
|
+
"xdtemporal-date", "xdtemporal-time", "xdtemporal-datetime", "xdtemporal-duration", "xdtemporal-year",
|
|
49
|
+
"xdtemporal-year-month", "xdtemporal-month", "xdtemporal-month-day", "xdtemporal-day",
|
|
50
|
+
"ordinal", "symbol", "link", "uri", "media-content", "lower", "upper",
|
|
51
|
+
"xdstringlist-value", "xdtokenlist-value", "xddecimallist-value", "xdintegerlist-value",
|
|
52
|
+
})
|
|
53
|
+
|
|
42
54
|
|
|
43
55
|
class RecordError(Exception):
|
|
44
56
|
pass
|
|
@@ -48,19 +60,32 @@ class RecordError(Exception):
|
|
|
48
60
|
class ExceptionalValue:
|
|
49
61
|
code: str # "ASKR"
|
|
50
62
|
name: str # "Asked and Refused"
|
|
51
|
-
origin: str # "source": in place of the value; "recovery": beside a value that failed its constraint
|
|
63
|
+
origin: str # "source": in place of the value; "recovery": beside a value that failed its constraint.
|
|
64
|
+
# Inferred from the record's shape, so a best guess: a published model may require the
|
|
65
|
+
# value element (SDCStudio's generated schemas do, though the reference model leaves it
|
|
66
|
+
# optional), and then a source Exceptional Value sits beside a value too. Whoever inserted
|
|
67
|
+
# Exceptional Values (a validator's recovery) knows the origin for certain.
|
|
52
68
|
|
|
53
69
|
|
|
54
70
|
@dataclass
|
|
55
71
|
class RecordValue:
|
|
56
72
|
component: Component
|
|
57
73
|
path: list[Component] # the clusters above it, root first
|
|
58
|
-
occurrence: int # 0 for the first time this component appears
|
|
74
|
+
occurrence: int # 0 for the first time this component appears in its cluster instance, 1 for the next
|
|
59
75
|
position: int # document order among the record's values, 0-based
|
|
60
76
|
elements: dict # the value elements as the record holds them: name -> text, or name -> dict for a
|
|
61
77
|
# nested element (a quantity's units: {"label": "Trade Units", "xdstring-value": "EA"})
|
|
62
78
|
context: dict[str, str] # act, vtb, vte, tr, modified, latitude, longitude, where present
|
|
63
79
|
exceptional: ExceptionalValue | None = None
|
|
80
|
+
instances: tuple[int, ...] = () # parallel to `path`: which occurrence each cluster is among the same cluster's
|
|
81
|
+
# repeats under the same parent instance (4.4.0). Two lines of one Line cluster
|
|
82
|
+
# in one Lines cluster are (..., 0) and (..., 1).
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def instance_key(self) -> tuple[tuple[str, int], ...]:
|
|
86
|
+
"""The value's cluster instance, root first: (ct_id, occurrence) for every cluster above it. Values with equal
|
|
87
|
+
keys belong to the same cluster instance; a mapper writes one target element per key."""
|
|
88
|
+
return tuple(zip((c.ct_id for c in self.path), self.instances))
|
|
64
89
|
|
|
65
90
|
@property
|
|
66
91
|
def rejected(self) -> dict:
|
|
@@ -131,9 +156,11 @@ def read_record(source: bytes | str | Path, model: Model) -> Record:
|
|
|
131
156
|
attributes[n] = _text(el)
|
|
132
157
|
|
|
133
158
|
record = Record(model=model, attributes=attributes)
|
|
134
|
-
seen: dict[tuple, int] = {}
|
|
159
|
+
seen: dict[tuple, int] = {} # leaf occurrences, per cluster instance
|
|
160
|
+
clusters: dict[tuple, int] = {} # cluster occurrences, per parent cluster instance
|
|
135
161
|
|
|
136
|
-
def walk(el, path: list[Component]):
|
|
162
|
+
def walk(el, path: list[Component], instances: tuple[int, ...]):
|
|
163
|
+
parent = tuple(zip((c.ct_id for c in path), instances))
|
|
137
164
|
for child in el:
|
|
138
165
|
if not _is_sdc4(child):
|
|
139
166
|
continue
|
|
@@ -142,16 +169,21 @@ def read_record(source: bytes | str | Path, model: Model) -> Record:
|
|
|
142
169
|
continue
|
|
143
170
|
comp = model.components.get(n[3:])
|
|
144
171
|
if comp is None: # an adapter: pass through to the component inside
|
|
145
|
-
walk(child, path)
|
|
172
|
+
walk(child, path, instances)
|
|
146
173
|
elif comp.sdc_type == "Cluster" or comp.sdc_type not in LEAF_TYPES:
|
|
147
|
-
|
|
174
|
+
key = (parent, comp.ct_id) # a cluster, or a structural component (audit, party, ...) holding values
|
|
175
|
+
k = clusters.get(key, 0)
|
|
176
|
+
clusters[key] = k + 1
|
|
177
|
+
walk(child, path + [comp], instances + (k,))
|
|
148
178
|
else:
|
|
149
|
-
key = (comp.ct_id,
|
|
179
|
+
key = (comp.ct_id, parent)
|
|
150
180
|
occurrence = seen.get(key, 0)
|
|
151
181
|
seen[key] = occurrence + 1
|
|
152
|
-
|
|
182
|
+
v = _value(child, comp, path, occurrence, len(record.values))
|
|
183
|
+
v.instances = instances
|
|
184
|
+
record.values.append(v)
|
|
153
185
|
|
|
154
|
-
walk(root, [])
|
|
186
|
+
walk(root, [], ())
|
|
155
187
|
return record
|
|
156
188
|
|
|
157
189
|
|
|
@@ -173,7 +205,7 @@ def _value(el, comp: Component, path: list[Component], occurrence: int, position
|
|
|
173
205
|
elements[n] = _nested(c)
|
|
174
206
|
exceptional = None
|
|
175
207
|
if ev_code:
|
|
176
|
-
value_present = any(k
|
|
208
|
+
value_present = any(k in VALUE_ELEMENTS for k in elements)
|
|
177
209
|
exceptional = ExceptionalValue(code=ev_code, name=EXCEPTIONAL_VALUES[ev_code],
|
|
178
210
|
origin="recovery" if value_present else "source")
|
|
179
211
|
return RecordValue(component=comp, path=path, occurrence=occurrence, position=position, elements=elements,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sdcreader
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.4.0
|
|
4
4
|
Summary: Read a published Semantic Data Charter model's package for projection into other standards (CDIF, DCAT-US, DCAT-AP, HealthDCAT-AP); MCP server included.
|
|
5
5
|
Author: Axius SDC, Inc.
|
|
6
6
|
License: Apache-2.0
|
|
@@ -97,8 +97,10 @@ that defines it. `source` is the record's bytes, its text or a path; a record of
|
|
|
97
97
|
|
|
98
98
|
- **`Record`**: `attributes` (`instance_id`, `instance_version`, `creation_timestamp`, `current-state`, ...),
|
|
99
99
|
`values` in document order, and `exceptional`, the values that carry an Exceptional Value.
|
|
100
|
-
- **`RecordValue`**: the `component`, its `path` of clusters (root first),
|
|
101
|
-
|
|
100
|
+
- **`RecordValue`**: the `component`, its `path` of clusters (root first), `instances` parallel to the path (which
|
|
101
|
+
occurrence each cluster is among the same cluster's repeats under the same parent; `instance_key` pairs them with the
|
|
102
|
+
clusters' identifiers, one key per cluster instance), an `occurrence` index for a component repeated within its
|
|
103
|
+
cluster instance, its `position`, its `elements` as the record holds them (a quantity's units nested:
|
|
102
104
|
`{"xdquantity-value": "1", "xdquantity-units": {"label": "Trade Units", "xdstring-value": "EA"}}`), and its
|
|
103
105
|
`context` (`act`, `vtb`, `vte`, `tr`, `modified`, `latitude`, `longitude`).
|
|
104
106
|
- **`ExceptionalValue`**: the ISO 21090 null flavor (`code`, `name`) and its `origin`: `source` when it stands in
|
|
@@ -128,3 +128,43 @@ def test_context_elements_are_kept_apart_from_the_value(model):
|
|
|
128
128
|
r = read_record(etree.tostring(tree), model)
|
|
129
129
|
v = next(v for v in r.values if v.context)
|
|
130
130
|
assert v.context == {"vtb": "2026-01-01T00:00:00"} and "vtb" not in v.elements
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
@pytest.mark.parametrize("sdc_type, value_el", [("XdOrdinal", "ordinal"), ("XdLink", "link"), ("XdFile", "uri"),
|
|
134
|
+
("XdInterval", "lower"), ("XdBoolean", "true-value")])
|
|
135
|
+
def test_recovery_is_recognized_for_every_value_element(model, sdc_type, value_el):
|
|
136
|
+
"""4.3.1: a recovery Exceptional Value beside a value whose element does not end in -value (an ordinal, a link,
|
|
137
|
+
a file, an interval) was read as written at the source, and its rejected value was lost."""
|
|
138
|
+
from sdcreader.record import VALUE_ELEMENTS, _value
|
|
139
|
+
from sdcreader.model import Component
|
|
140
|
+
assert value_el in VALUE_ELEMENTS
|
|
141
|
+
el = etree.Element(f"{{{SDC4}}}ms-x")
|
|
142
|
+
etree.SubElement(el, "label").text = "L"
|
|
143
|
+
etree.SubElement(el, f"{{{SDC4}}}INV").append(etree.Element("ev-name"))
|
|
144
|
+
etree.SubElement(el, value_el).text = "bad"
|
|
145
|
+
comp = Component(ct_id="x", sdc_type=sdc_type, label="L", description="", data_type="", constraints={}, links={})
|
|
146
|
+
v = _value(el, comp, [], 0, 0)
|
|
147
|
+
assert v.exceptional.origin == "recovery" and v.rejected == {value_el: "bad"}
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_cluster_instances_number_repeats_under_the_same_parent(model):
|
|
151
|
+
"""4.4.0: a cluster repeated under the same parent instance is numbered, and every value in it carries the number,
|
|
152
|
+
so a mapper can write one target element per instance."""
|
|
153
|
+
tree = etree.parse(str(RECORD))
|
|
154
|
+
_, comp = _first_string(tree)
|
|
155
|
+
cluster = comp.getparent().getparent() # the cluster holding the first value (through its adapter)
|
|
156
|
+
twin = etree.fromstring(etree.tostring(cluster))
|
|
157
|
+
cluster.addnext(twin)
|
|
158
|
+
r = read_record(etree.tostring(tree), model)
|
|
159
|
+
ct = comp.tag.split("ms-", 1)[1]
|
|
160
|
+
first, second = [v for v in r.values if v.component.ct_id == ct][:2]
|
|
161
|
+
assert first.path == second.path
|
|
162
|
+
assert first.instances[-1] == 0 and second.instances[-1] == 1
|
|
163
|
+
assert first.instance_key != second.instance_key
|
|
164
|
+
assert first.occurrence == second.occurrence == 0, "occurrence counts within a cluster instance"
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def test_every_value_has_an_instance_number_per_cluster_in_its_path(model):
|
|
168
|
+
r = read_record(RECORD, model)
|
|
169
|
+
assert all(len(v.instances) == len(v.path) for v in r.values)
|
|
170
|
+
assert all(v.instance_key[0] == (model.root.ct_id, 0) for v in r.values)
|
|
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
|