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.
Files changed (22) hide show
  1. {sdcreader-4.3.0/src/sdcreader.egg-info → sdcreader-4.4.0}/PKG-INFO +5 -3
  2. {sdcreader-4.3.0 → sdcreader-4.4.0}/README.md +4 -2
  3. {sdcreader-4.3.0 → sdcreader-4.4.0}/pyproject.toml +1 -1
  4. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/__init__.py +3 -3
  5. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/mcp_server.py +2 -1
  6. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/record.py +42 -10
  7. {sdcreader-4.3.0 → sdcreader-4.4.0/src/sdcreader.egg-info}/PKG-INFO +5 -3
  8. {sdcreader-4.3.0 → sdcreader-4.4.0}/tests/test_record.py +40 -0
  9. {sdcreader-4.3.0 → sdcreader-4.4.0}/LICENSE +0 -0
  10. {sdcreader-4.3.0 → sdcreader-4.4.0}/NOTICE +0 -0
  11. {sdcreader-4.3.0 → sdcreader-4.4.0}/setup.cfg +0 -0
  12. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/mcp.py +0 -0
  13. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/model.py +0 -0
  14. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader/package.py +0 -0
  15. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/SOURCES.txt +0 -0
  16. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/dependency_links.txt +0 -0
  17. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/entry_points.txt +0 -0
  18. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/requires.txt +0 -0
  19. {sdcreader-4.3.0 → sdcreader-4.4.0}/src/sdcreader.egg-info/top_level.txt +0 -0
  20. {sdcreader-4.3.0 → sdcreader-4.4.0}/tests/test_mcp_scaffold.py +0 -0
  21. {sdcreader-4.3.0 → sdcreader-4.4.0}/tests/test_mcp_server.py +0 -0
  22. {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.0
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), an `occurrence` index for a component
101
- repeated under the same path, its `position`, its `elements` as the record holds them (a quantity's units nested:
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), an `occurrence` index for a component
76
- repeated under the same path, its `position`, its `elements` as the record holds them (a quantity's units nested:
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.3.0"
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.3.0"
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], "occurrence": v.occurrence, "elements": v.elements, "context": v.context,
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 under this path, 1 for the next, ...
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
- walk(child, path + [comp]) # a cluster, or a structural component (audit, party, ...) holding values
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, tuple(c.ct_id for c in path))
179
+ key = (comp.ct_id, parent)
150
180
  occurrence = seen.get(key, 0)
151
181
  seen[key] = occurrence + 1
152
- record.values.append(_value(child, comp, path, occurrence, len(record.values)))
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.endswith("-value") or k.startswith("xdtemporal-") for k in elements)
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.0
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), an `occurrence` index for a component
101
- repeated under the same path, its `position`, its `elements` as the record holds them (a quantity's units nested:
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