sdmxlib 0.8.0__py3-none-any.whl
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.
- sdmxlib/__init__.py +155 -0
- sdmxlib/api/__init__.py +7 -0
- sdmxlib/api/client.py +246 -0
- sdmxlib/api/filters.py +67 -0
- sdmxlib/api/providers.py +66 -0
- sdmxlib/api/query.py +428 -0
- sdmxlib/api/registry.py +666 -0
- sdmxlib/api/session.py +124 -0
- sdmxlib/formats/__init__.py +166 -0
- sdmxlib/formats/sdmx_csv/__init__.py +5 -0
- sdmxlib/formats/sdmx_csv/reader.py +147 -0
- sdmxlib/formats/sdmx_json/__init__.py +1 -0
- sdmxlib/formats/sdmx_json/reader.py +897 -0
- sdmxlib/formats/sdmx_json/writer.py +698 -0
- sdmxlib/formats/sdmx_ml21/__init__.py +5 -0
- sdmxlib/formats/sdmx_ml21/namespaces.py +12 -0
- sdmxlib/formats/sdmx_ml21/reader.py +1147 -0
- sdmxlib/formats/sdmx_ml21/writer.py +709 -0
- sdmxlib/formats/sdmx_ml30/__init__.py +1 -0
- sdmxlib/formats/sdmx_ml30/namespaces.py +12 -0
- sdmxlib/formats/sdmx_ml30/reader.py +1138 -0
- sdmxlib/formats/sdmx_ml30/writer.py +730 -0
- sdmxlib/local/__init__.py +6 -0
- sdmxlib/local/_registry.py +368 -0
- sdmxlib/model/__init__.py +122 -0
- sdmxlib/model/annotations.py +103 -0
- sdmxlib/model/base.py +61 -0
- sdmxlib/model/category.py +88 -0
- sdmxlib/model/codelist.py +91 -0
- sdmxlib/model/collections.py +382 -0
- sdmxlib/model/concept.py +121 -0
- sdmxlib/model/constraint.py +92 -0
- sdmxlib/model/convert.py +268 -0
- sdmxlib/model/dataflow.py +49 -0
- sdmxlib/model/dataset.py +337 -0
- sdmxlib/model/datastructure.py +390 -0
- sdmxlib/model/expr.py +120 -0
- sdmxlib/model/hierarchy.py +111 -0
- sdmxlib/model/istring.py +81 -0
- sdmxlib/model/mapping.py +134 -0
- sdmxlib/model/message.py +70 -0
- sdmxlib/model/organisation.py +149 -0
- sdmxlib/model/provision.py +45 -0
- sdmxlib/model/ref.py +195 -0
- sdmxlib/model/registry.py +191 -0
- sdmxlib/model/representation.py +99 -0
- sdmxlib/model/urn.py +130 -0
- sdmxlib/model/validation.py +338 -0
- sdmxlib/polars.py +392 -0
- sdmxlib/py.typed +0 -0
- sdmxlib/storage/__init__.py +28 -0
- sdmxlib/storage/lazy.py +329 -0
- sdmxlib/storage/readers.py +310 -0
- sdmxlib/storage/schema.py +289 -0
- sdmxlib/storage/writers.py +503 -0
- sdmxlib-0.8.0.dist-info/METADATA +102 -0
- sdmxlib-0.8.0.dist-info/RECORD +58 -0
- sdmxlib-0.8.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,1147 @@
|
|
|
1
|
+
"""SDMX-ML 2.1 reader: lxml → domain model.
|
|
2
|
+
|
|
3
|
+
Collapses thirteen XML wrapper layers into clean domain objects.
|
|
4
|
+
All references within the message are auto-resolved.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from lxml import etree
|
|
11
|
+
|
|
12
|
+
from sdmxlib.model.annotations import Annotation, Annotations
|
|
13
|
+
from sdmxlib.model.category import Categorisation, Category, CategoryScheme
|
|
14
|
+
from sdmxlib.model.codelist import Code, Codelist
|
|
15
|
+
from sdmxlib.model.collections import ArtefactList, ItemList
|
|
16
|
+
from sdmxlib.model.concept import Concept, ConceptScheme
|
|
17
|
+
from sdmxlib.model.constraint import (
|
|
18
|
+
ConstraintAttachment,
|
|
19
|
+
ContentConstraint,
|
|
20
|
+
CubeRegion,
|
|
21
|
+
KeyValue,
|
|
22
|
+
ReferencePeriod,
|
|
23
|
+
ReleaseCalendar,
|
|
24
|
+
)
|
|
25
|
+
from sdmxlib.model.dataflow import Dataflow
|
|
26
|
+
from sdmxlib.model.datastructure import (
|
|
27
|
+
AttachmentLevel,
|
|
28
|
+
Attribute,
|
|
29
|
+
DataStructure,
|
|
30
|
+
Dimension,
|
|
31
|
+
Group,
|
|
32
|
+
Measure,
|
|
33
|
+
MeasureDimension,
|
|
34
|
+
TimeDimension,
|
|
35
|
+
)
|
|
36
|
+
from sdmxlib.model.hierarchy import HierarchicalCode, Hierarchy
|
|
37
|
+
from sdmxlib.model.istring import InternationalString
|
|
38
|
+
from sdmxlib.model.mapping import (
|
|
39
|
+
CategoryMap,
|
|
40
|
+
CategorySchemeMap,
|
|
41
|
+
CodelistMap,
|
|
42
|
+
CodeMap,
|
|
43
|
+
ComponentMap,
|
|
44
|
+
ConceptMap,
|
|
45
|
+
ConceptSchemeMap,
|
|
46
|
+
DataflowMap,
|
|
47
|
+
DataStructureMap,
|
|
48
|
+
StructureSet,
|
|
49
|
+
)
|
|
50
|
+
from sdmxlib.model.message import StructureMessage
|
|
51
|
+
from sdmxlib.model.organisation import Agency, AgencyScheme, DataProvider, DataProviderScheme
|
|
52
|
+
from sdmxlib.model.provision import ProvisionAgreement
|
|
53
|
+
from sdmxlib.model.ref import AnyRef, Ref
|
|
54
|
+
from sdmxlib.model.registry import Registry
|
|
55
|
+
from sdmxlib.model.representation import Facet
|
|
56
|
+
from sdmxlib.model.urn import SdmxUrn
|
|
57
|
+
|
|
58
|
+
from .namespaces import COM, MES, STR, XML_LANG
|
|
59
|
+
|
|
60
|
+
# ── Clark-notation tag builders ───────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _c(tag: str) -> str:
|
|
64
|
+
return f"{{{COM}}}{tag}"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _s(tag: str) -> str:
|
|
68
|
+
return f"{{{STR}}}{tag}"
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _m(tag: str) -> str:
|
|
72
|
+
return f"{{{MES}}}{tag}"
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ── Common XML helpers ────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _read_istring(el: etree._Element, child_tag: str = "Name") -> InternationalString:
|
|
79
|
+
"""Read com:Name (or com:Description) children as InternationalString."""
|
|
80
|
+
langs: dict[str, str] = {}
|
|
81
|
+
for child in el.findall(_c(child_tag)):
|
|
82
|
+
lang = child.get(XML_LANG, "")
|
|
83
|
+
if lang and child.text:
|
|
84
|
+
langs[lang] = child.text
|
|
85
|
+
return InternationalString(**langs)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _read_annotations(el: etree._Element) -> Annotations:
|
|
89
|
+
"""Read com:Annotations block."""
|
|
90
|
+
anns_el = el.find(_c("Annotations"))
|
|
91
|
+
if anns_el is None:
|
|
92
|
+
return Annotations()
|
|
93
|
+
items: list[Annotation] = []
|
|
94
|
+
for ann_el in anns_el.findall(_c("Annotation")):
|
|
95
|
+
title_el = ann_el.find(_c("AnnotationTitle"))
|
|
96
|
+
type_el = ann_el.find(_c("AnnotationType"))
|
|
97
|
+
url_el = ann_el.find(_c("AnnotationURL"))
|
|
98
|
+
text_langs: dict[str, str] = {}
|
|
99
|
+
for text_el in ann_el.findall(_c("AnnotationText")):
|
|
100
|
+
lang = text_el.get(XML_LANG, "")
|
|
101
|
+
if lang and text_el.text:
|
|
102
|
+
text_langs[lang] = text_el.text
|
|
103
|
+
items.append(
|
|
104
|
+
Annotation(
|
|
105
|
+
id=ann_el.get("id"),
|
|
106
|
+
title=title_el.text if title_el is not None else None,
|
|
107
|
+
type=type_el.text if type_el is not None else None,
|
|
108
|
+
url=url_el.text if url_el is not None else None,
|
|
109
|
+
text=InternationalString(**text_langs),
|
|
110
|
+
)
|
|
111
|
+
)
|
|
112
|
+
return Annotations(items)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _read_datetime_attr(el: etree._Element, attr: str) -> datetime | None:
|
|
116
|
+
"""Parse an optional xs:dateTime attribute (e.g. validFrom, validTo)."""
|
|
117
|
+
val = el.get(attr)
|
|
118
|
+
if not val:
|
|
119
|
+
return None
|
|
120
|
+
return datetime.fromisoformat(val)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ── Agency lookup ─────────────────────────────────────────────────────────────
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _resolve_agency(agency_id: str, agencies: dict[str, Agency]) -> Agency:
|
|
127
|
+
"""Return the rich Agency if parsed from an AgencyScheme, else a minimal one."""
|
|
128
|
+
return agencies.get(agency_id, Agency(id=agency_id))
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ── Ref / URN helpers ─────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _ref_el_to_urn(ref_el: etree._Element) -> SdmxUrn[Any] | None:
|
|
135
|
+
"""Convert a Ref element to a SdmxUrn.
|
|
136
|
+
|
|
137
|
+
Handles both URN-attribute refs and component-attribute refs, including
|
|
138
|
+
item refs that carry maintainableParentID.
|
|
139
|
+
"""
|
|
140
|
+
if urn_str := ref_el.get("urn"):
|
|
141
|
+
return SdmxUrn.parse(urn_str)
|
|
142
|
+
|
|
143
|
+
agency = ref_el.get("agencyID", "")
|
|
144
|
+
id_ = ref_el.get("id", "")
|
|
145
|
+
version = ref_el.get("version", "latest")
|
|
146
|
+
package = ref_el.get("package", "")
|
|
147
|
+
cls = ref_el.get("class", "")
|
|
148
|
+
|
|
149
|
+
if not (agency and id_ and package and cls):
|
|
150
|
+
return None
|
|
151
|
+
|
|
152
|
+
parent_id = ref_el.get("maintainableParentID")
|
|
153
|
+
parent_version = ref_el.get("maintainableParentVersion", "latest")
|
|
154
|
+
|
|
155
|
+
if parent_id:
|
|
156
|
+
# Item URN: id_ is the item, parent_id is the maintainable container
|
|
157
|
+
return SdmxUrn.make(
|
|
158
|
+
object,
|
|
159
|
+
package=package,
|
|
160
|
+
artefact_class=cls,
|
|
161
|
+
agency=agency,
|
|
162
|
+
id=parent_id,
|
|
163
|
+
version=parent_version,
|
|
164
|
+
item_id=id_,
|
|
165
|
+
)
|
|
166
|
+
return SdmxUrn.make(
|
|
167
|
+
object,
|
|
168
|
+
package=package,
|
|
169
|
+
artefact_class=cls,
|
|
170
|
+
agency=agency,
|
|
171
|
+
id=id_,
|
|
172
|
+
version=version,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _resolve_ref(container_el: etree._Element, index: dict[str, Any]) -> AnyRef | None:
|
|
177
|
+
"""Find a direct Ref child and resolve it against the index."""
|
|
178
|
+
ref_el = container_el.find("Ref")
|
|
179
|
+
if ref_el is None:
|
|
180
|
+
return None
|
|
181
|
+
urn = _ref_el_to_urn(ref_el)
|
|
182
|
+
if urn is None:
|
|
183
|
+
return None
|
|
184
|
+
obj = index.get(str(urn))
|
|
185
|
+
if obj is not None:
|
|
186
|
+
return Ref.of(urn, obj)
|
|
187
|
+
return Ref.unresolved(urn)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ── Index helpers ─────────────────────────────────────────────────────────────
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _index_artefact(index: dict[str, Any], obj: Any, artefact_type: type[Any]) -> None:
|
|
194
|
+
"""Add a maintainable artefact to the resolution index under both versioned and latest keys."""
|
|
195
|
+
package = artefact_type.sdmx_package
|
|
196
|
+
cls = artefact_type.sdmx_class
|
|
197
|
+
urn = SdmxUrn.make(
|
|
198
|
+
artefact_type,
|
|
199
|
+
package=package,
|
|
200
|
+
artefact_class=cls,
|
|
201
|
+
agency=obj.agency_id,
|
|
202
|
+
id=obj.id,
|
|
203
|
+
version=obj.version,
|
|
204
|
+
)
|
|
205
|
+
index[str(urn)] = obj
|
|
206
|
+
urn_latest = SdmxUrn.make(artefact_type, package=package, artefact_class=cls, agency=obj.agency_id, id=obj.id)
|
|
207
|
+
index[str(urn_latest)] = obj
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _index_item(
|
|
211
|
+
index: dict[str, Any],
|
|
212
|
+
item: Any,
|
|
213
|
+
parent: Any,
|
|
214
|
+
package: str,
|
|
215
|
+
cls: str,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Add an item (e.g. Concept) to the index under its item URN."""
|
|
218
|
+
urn = SdmxUrn.make(
|
|
219
|
+
object,
|
|
220
|
+
package=package,
|
|
221
|
+
artefact_class=cls,
|
|
222
|
+
agency=parent.agency_id,
|
|
223
|
+
id=parent.id,
|
|
224
|
+
version=parent.version,
|
|
225
|
+
item_id=item.id,
|
|
226
|
+
)
|
|
227
|
+
index[str(urn)] = item
|
|
228
|
+
urn_latest = SdmxUrn.make(
|
|
229
|
+
object,
|
|
230
|
+
package=package,
|
|
231
|
+
artefact_class=cls,
|
|
232
|
+
agency=parent.agency_id,
|
|
233
|
+
id=parent.id,
|
|
234
|
+
item_id=item.id,
|
|
235
|
+
)
|
|
236
|
+
index[str(urn_latest)] = item
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
# ── Representation ────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _read_text_format(tf_el: etree._Element) -> Facet:
|
|
243
|
+
def _int(v: str | None) -> int | None:
|
|
244
|
+
return int(v) if v is not None else None
|
|
245
|
+
|
|
246
|
+
def _float(v: str | None) -> float | None:
|
|
247
|
+
return float(v) if v is not None else None
|
|
248
|
+
|
|
249
|
+
def _bool(v: str | None) -> bool | None:
|
|
250
|
+
if v is None:
|
|
251
|
+
return None
|
|
252
|
+
return v.lower() in ("true", "1")
|
|
253
|
+
|
|
254
|
+
return Facet(
|
|
255
|
+
type=tf_el.get("textType"),
|
|
256
|
+
max_length=_int(tf_el.get("maxLength")),
|
|
257
|
+
min_length=_int(tf_el.get("minLength")),
|
|
258
|
+
max_value=_float(tf_el.get("maxValue")),
|
|
259
|
+
min_value=_float(tf_el.get("minValue")),
|
|
260
|
+
decimals=_int(tf_el.get("decimals")),
|
|
261
|
+
pattern=tf_el.get("pattern"),
|
|
262
|
+
start_value=_float(tf_el.get("startValue")),
|
|
263
|
+
end_value=_float(tf_el.get("endValue")),
|
|
264
|
+
is_sequence=_bool(tf_el.get("isSequence")),
|
|
265
|
+
is_multi_lingual=_bool(tf_el.get("isMultiLingual")),
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _read_representation(local_rep_el: etree._Element, index: dict[str, Any] | None = None) -> AnyRef | Facet | None:
|
|
270
|
+
"""Parse a LocalRepresentation or CoreRepresentation element.
|
|
271
|
+
|
|
272
|
+
When *index* is provided the codelist ref is resolved against it.
|
|
273
|
+
Without *index* (concept CoreRepresentation parsing) an unresolved Ref is returned.
|
|
274
|
+
"""
|
|
275
|
+
enum_el = local_rep_el.find(_s("Enumeration"))
|
|
276
|
+
if enum_el is not None:
|
|
277
|
+
if index is not None:
|
|
278
|
+
return _resolve_ref(enum_el, index)
|
|
279
|
+
ref_el = enum_el.find("Ref")
|
|
280
|
+
if ref_el is not None:
|
|
281
|
+
urn = _ref_el_to_urn(ref_el)
|
|
282
|
+
if urn is not None:
|
|
283
|
+
return Ref.unresolved(urn)
|
|
284
|
+
|
|
285
|
+
tf_el = local_rep_el.find(_s("TextFormat"))
|
|
286
|
+
if tf_el is not None:
|
|
287
|
+
return _read_text_format(tf_el)
|
|
288
|
+
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
# ── OrganisationSchemes ───────────────────────────────────────────────────────
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _read_organisation_schemes(
|
|
296
|
+
structures: etree._Element,
|
|
297
|
+
) -> tuple[list[AgencyScheme], list[DataProviderScheme]]:
|
|
298
|
+
container = structures.find(_s("OrganisationSchemes"))
|
|
299
|
+
if container is None:
|
|
300
|
+
return [], []
|
|
301
|
+
|
|
302
|
+
agency_schemes: list[AgencyScheme] = []
|
|
303
|
+
provider_schemes: list[DataProviderScheme] = []
|
|
304
|
+
|
|
305
|
+
for as_el in container.findall(_s("AgencyScheme")):
|
|
306
|
+
agencies = [
|
|
307
|
+
Agency(
|
|
308
|
+
id=a.get("id", ""),
|
|
309
|
+
name=_read_istring(a),
|
|
310
|
+
description=_read_istring(a, "Description"),
|
|
311
|
+
annotations=_read_annotations(a),
|
|
312
|
+
)
|
|
313
|
+
for a in as_el.findall(_s("Agency"))
|
|
314
|
+
]
|
|
315
|
+
scheme = AgencyScheme(
|
|
316
|
+
id=as_el.get("id", ""),
|
|
317
|
+
maintainer=as_el.get("agencyID", ""),
|
|
318
|
+
version=as_el.get("version", "1.0"),
|
|
319
|
+
name=_read_istring(as_el),
|
|
320
|
+
description=_read_istring(as_el, "Description"),
|
|
321
|
+
is_final=as_el.get("isFinal", "false").lower() == "true",
|
|
322
|
+
agencies=ItemList(agencies),
|
|
323
|
+
annotations=_read_annotations(as_el),
|
|
324
|
+
valid_from=_read_datetime_attr(as_el, "validFrom"),
|
|
325
|
+
valid_to=_read_datetime_attr(as_el, "validTo"),
|
|
326
|
+
)
|
|
327
|
+
agency_schemes.append(scheme)
|
|
328
|
+
|
|
329
|
+
for ps_el in container.findall(_s("DataProviderScheme")):
|
|
330
|
+
providers = [
|
|
331
|
+
DataProvider(
|
|
332
|
+
id=p.get("id", ""),
|
|
333
|
+
name=_read_istring(p),
|
|
334
|
+
description=_read_istring(p, "Description"),
|
|
335
|
+
annotations=_read_annotations(p),
|
|
336
|
+
)
|
|
337
|
+
for p in ps_el.findall(_s("DataProvider"))
|
|
338
|
+
]
|
|
339
|
+
scheme_p = DataProviderScheme(
|
|
340
|
+
id=ps_el.get("id", ""),
|
|
341
|
+
maintainer=ps_el.get("agencyID", ""),
|
|
342
|
+
version=ps_el.get("version", "1.0"),
|
|
343
|
+
name=_read_istring(ps_el),
|
|
344
|
+
description=_read_istring(ps_el, "Description"),
|
|
345
|
+
is_final=ps_el.get("isFinal", "false").lower() == "true",
|
|
346
|
+
providers=ItemList(providers),
|
|
347
|
+
annotations=_read_annotations(ps_el),
|
|
348
|
+
valid_from=_read_datetime_attr(ps_el, "validFrom"),
|
|
349
|
+
valid_to=_read_datetime_attr(ps_el, "validTo"),
|
|
350
|
+
)
|
|
351
|
+
provider_schemes.append(scheme_p)
|
|
352
|
+
|
|
353
|
+
return agency_schemes, provider_schemes
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
# ── Codelists ─────────────────────────────────────────────────────────────────
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _read_codelists(structures: etree._Element, agencies: dict[str, Agency]) -> list[Codelist]:
|
|
360
|
+
container = structures.find(_s("Codelists"))
|
|
361
|
+
if container is None:
|
|
362
|
+
return []
|
|
363
|
+
result: list[Codelist] = []
|
|
364
|
+
for cl_el in container.findall(_s("Codelist")):
|
|
365
|
+
codes: list[Code] = []
|
|
366
|
+
for code_el in cl_el.findall(_s("Code")):
|
|
367
|
+
parent_id: str | None = None
|
|
368
|
+
parent_el = code_el.find(_s("Parent"))
|
|
369
|
+
if parent_el is not None:
|
|
370
|
+
ref_el = parent_el.find("Ref")
|
|
371
|
+
if ref_el is not None:
|
|
372
|
+
parent_id = ref_el.get("id")
|
|
373
|
+
codes.append(
|
|
374
|
+
Code(
|
|
375
|
+
id=code_el.get("id", ""),
|
|
376
|
+
name=_read_istring(code_el),
|
|
377
|
+
description=_read_istring(code_el, "Description"),
|
|
378
|
+
parent_id=parent_id,
|
|
379
|
+
annotations=_read_annotations(code_el),
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
result.append(
|
|
383
|
+
Codelist(
|
|
384
|
+
id=cl_el.get("id", ""),
|
|
385
|
+
maintainer=_resolve_agency(cl_el.get("agencyID", ""), agencies),
|
|
386
|
+
version=cl_el.get("version", "1.0"),
|
|
387
|
+
name=_read_istring(cl_el),
|
|
388
|
+
description=_read_istring(cl_el, "Description"),
|
|
389
|
+
is_final=cl_el.get("isFinal", "false").lower() == "true",
|
|
390
|
+
codes=ItemList(codes),
|
|
391
|
+
annotations=_read_annotations(cl_el),
|
|
392
|
+
valid_from=_read_datetime_attr(cl_el, "validFrom"),
|
|
393
|
+
valid_to=_read_datetime_attr(cl_el, "validTo"),
|
|
394
|
+
)
|
|
395
|
+
)
|
|
396
|
+
return result
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
# ── ConceptSchemes ────────────────────────────────────────────────────────────
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def _read_concept_schemes(structures: etree._Element, agencies: dict[str, Agency]) -> list[ConceptScheme]:
|
|
403
|
+
container = structures.find(_s("Concepts"))
|
|
404
|
+
if container is None:
|
|
405
|
+
return []
|
|
406
|
+
result: list[ConceptScheme] = []
|
|
407
|
+
for cs_el in container.findall(_s("ConceptScheme")):
|
|
408
|
+
concepts = []
|
|
409
|
+
for concept_el in cs_el.findall(_s("Concept")):
|
|
410
|
+
core_rep = None
|
|
411
|
+
core_rep_el = concept_el.find(_s("CoreRepresentation"))
|
|
412
|
+
if core_rep_el is not None:
|
|
413
|
+
core_rep = _read_representation(core_rep_el)
|
|
414
|
+
concepts.append(
|
|
415
|
+
Concept(
|
|
416
|
+
id=concept_el.get("id", ""),
|
|
417
|
+
name=_read_istring(concept_el),
|
|
418
|
+
description=_read_istring(concept_el, "Description"),
|
|
419
|
+
core_representation=core_rep,
|
|
420
|
+
annotations=_read_annotations(concept_el),
|
|
421
|
+
)
|
|
422
|
+
)
|
|
423
|
+
result.append(
|
|
424
|
+
ConceptScheme(
|
|
425
|
+
id=cs_el.get("id", ""),
|
|
426
|
+
maintainer=_resolve_agency(cs_el.get("agencyID", ""), agencies),
|
|
427
|
+
version=cs_el.get("version", "1.0"),
|
|
428
|
+
name=_read_istring(cs_el),
|
|
429
|
+
description=_read_istring(cs_el, "Description"),
|
|
430
|
+
concepts=ItemList(concepts),
|
|
431
|
+
annotations=_read_annotations(cs_el),
|
|
432
|
+
valid_from=_read_datetime_attr(cs_el, "validFrom"),
|
|
433
|
+
valid_to=_read_datetime_attr(cs_el, "validTo"),
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
return result
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
# ── DataStructures ────────────────────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _read_dimension(dim_el: etree._Element, index: dict[str, Any]) -> Dimension:
|
|
443
|
+
concept_ref: AnyRef | None = None
|
|
444
|
+
ci_el = dim_el.find(_s("ConceptIdentity"))
|
|
445
|
+
if ci_el is not None:
|
|
446
|
+
concept_ref = _resolve_ref(ci_el, index)
|
|
447
|
+
|
|
448
|
+
representation = None
|
|
449
|
+
local_rep_el = dim_el.find(_s("LocalRepresentation"))
|
|
450
|
+
if local_rep_el is not None:
|
|
451
|
+
representation = _read_representation(local_rep_el, index)
|
|
452
|
+
|
|
453
|
+
return Dimension(
|
|
454
|
+
id=dim_el.get("id", ""),
|
|
455
|
+
concept=concept_ref,
|
|
456
|
+
representation=representation,
|
|
457
|
+
annotations=_read_annotations(dim_el),
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def _read_measure_dimension(md_el: etree._Element, index: dict[str, Any]) -> MeasureDimension:
|
|
462
|
+
concept_ref: AnyRef | None = None
|
|
463
|
+
ci_el = md_el.find(_s("ConceptIdentity"))
|
|
464
|
+
if ci_el is not None:
|
|
465
|
+
concept_ref = _resolve_ref(ci_el, index)
|
|
466
|
+
|
|
467
|
+
representation = None
|
|
468
|
+
local_rep_el = md_el.find(_s("LocalRepresentation"))
|
|
469
|
+
if local_rep_el is not None:
|
|
470
|
+
representation = _read_representation(local_rep_el, index)
|
|
471
|
+
|
|
472
|
+
return MeasureDimension(
|
|
473
|
+
id=md_el.get("id", ""),
|
|
474
|
+
concept=concept_ref,
|
|
475
|
+
representation=representation,
|
|
476
|
+
annotations=_read_annotations(md_el),
|
|
477
|
+
)
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def _read_time_dimension(td_el: etree._Element, index: dict[str, Any]) -> TimeDimension:
|
|
481
|
+
concept_ref: AnyRef | None = None
|
|
482
|
+
ci_el = td_el.find(_s("ConceptIdentity"))
|
|
483
|
+
if ci_el is not None:
|
|
484
|
+
concept_ref = _resolve_ref(ci_el, index)
|
|
485
|
+
|
|
486
|
+
facet_rep: Facet | None = None
|
|
487
|
+
local_rep_el = td_el.find(_s("LocalRepresentation"))
|
|
488
|
+
if local_rep_el is not None:
|
|
489
|
+
tf_el = local_rep_el.find(_s("TextFormat"))
|
|
490
|
+
if tf_el is not None:
|
|
491
|
+
facet_rep = _read_text_format(tf_el)
|
|
492
|
+
|
|
493
|
+
return TimeDimension(
|
|
494
|
+
id=td_el.get("id", "TIME_PERIOD"),
|
|
495
|
+
concept=concept_ref,
|
|
496
|
+
representation=facet_rep,
|
|
497
|
+
annotations=_read_annotations(td_el),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _read_attachment(
|
|
502
|
+
ar_el: etree._Element,
|
|
503
|
+
) -> tuple[AttachmentLevel | None, list[str] | None, str | None]:
|
|
504
|
+
"""Parse AttributeRelationship → (level, related_dimensions, related_measure)."""
|
|
505
|
+
pm_el = ar_el.find(_s("PrimaryMeasure"))
|
|
506
|
+
if pm_el is not None:
|
|
507
|
+
ref_el = pm_el.find("Ref")
|
|
508
|
+
measure_id = ref_el.get("id") if ref_el is not None else "OBS_VALUE"
|
|
509
|
+
return AttachmentLevel.OBSERVATION, None, measure_id
|
|
510
|
+
|
|
511
|
+
dim_refs = ar_el.findall(_s("Dimension"))
|
|
512
|
+
if dim_refs:
|
|
513
|
+
related = [ref_el.get("id", "") for d in dim_refs if (ref_el := d.find("Ref")) is not None]
|
|
514
|
+
return AttachmentLevel.SERIES, related or None, None
|
|
515
|
+
|
|
516
|
+
if ar_el.find(_s("Group")) is not None:
|
|
517
|
+
return AttachmentLevel.GROUP, None, None
|
|
518
|
+
|
|
519
|
+
return AttachmentLevel.DATASET, None, None
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def _read_attribute(attr_el: etree._Element, index: dict[str, Any]) -> Attribute:
|
|
523
|
+
concept_ref: AnyRef | None = None
|
|
524
|
+
ci_el = attr_el.find(_s("ConceptIdentity"))
|
|
525
|
+
if ci_el is not None:
|
|
526
|
+
concept_ref = _resolve_ref(ci_el, index)
|
|
527
|
+
|
|
528
|
+
representation = None
|
|
529
|
+
local_rep_el = attr_el.find(_s("LocalRepresentation"))
|
|
530
|
+
if local_rep_el is not None:
|
|
531
|
+
representation = _read_representation(local_rep_el, index)
|
|
532
|
+
|
|
533
|
+
attachment: AttachmentLevel | None = None
|
|
534
|
+
related_dims: list[str] | None = None
|
|
535
|
+
related_measure: str | None = None
|
|
536
|
+
ar_el = attr_el.find(_s("AttributeRelationship"))
|
|
537
|
+
if ar_el is not None:
|
|
538
|
+
attachment, related_dims, related_measure = _read_attachment(ar_el)
|
|
539
|
+
|
|
540
|
+
return Attribute(
|
|
541
|
+
id=attr_el.get("id", ""),
|
|
542
|
+
usage=attr_el.get("assignmentStatus"),
|
|
543
|
+
attachment=attachment,
|
|
544
|
+
related_dimensions=related_dims,
|
|
545
|
+
related_measure=related_measure,
|
|
546
|
+
concept=concept_ref,
|
|
547
|
+
representation=representation,
|
|
548
|
+
annotations=_read_annotations(attr_el),
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _read_measure(pm_el: etree._Element, index: dict[str, Any]) -> Measure:
|
|
553
|
+
concept_ref: AnyRef | None = None
|
|
554
|
+
ci_el = pm_el.find(_s("ConceptIdentity"))
|
|
555
|
+
if ci_el is not None:
|
|
556
|
+
concept_ref = _resolve_ref(ci_el, index)
|
|
557
|
+
|
|
558
|
+
representation = None
|
|
559
|
+
local_rep_el = pm_el.find(_s("LocalRepresentation"))
|
|
560
|
+
if local_rep_el is not None:
|
|
561
|
+
representation = _read_representation(local_rep_el, index)
|
|
562
|
+
|
|
563
|
+
return Measure(
|
|
564
|
+
id=pm_el.get("id", "OBS_VALUE"),
|
|
565
|
+
concept=concept_ref,
|
|
566
|
+
representation=representation,
|
|
567
|
+
annotations=_read_annotations(pm_el),
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
def _read_groups(components_el: etree._Element) -> list[Group]:
|
|
572
|
+
result: list[Group] = []
|
|
573
|
+
for group_el in components_el.findall(_s("Group")):
|
|
574
|
+
dim_ids: list[str] = []
|
|
575
|
+
for gd_el in group_el.findall(_s("GroupDimension")):
|
|
576
|
+
dr_el = gd_el.find(_s("DimensionReference"))
|
|
577
|
+
if dr_el is not None:
|
|
578
|
+
ref_el = dr_el.find("Ref")
|
|
579
|
+
if ref_el is not None:
|
|
580
|
+
dim_ids.append(ref_el.get("id", ""))
|
|
581
|
+
result.append(Group(id=group_el.get("id", ""), dimension_ids=dim_ids))
|
|
582
|
+
return result
|
|
583
|
+
|
|
584
|
+
|
|
585
|
+
def _read_dsd_components(
|
|
586
|
+
components_el: etree._Element, index: dict[str, Any]
|
|
587
|
+
) -> tuple[list[Dimension], TimeDimension | None, MeasureDimension | None, list[Attribute], list[Measure], list[Group]]:
|
|
588
|
+
"""Parse DataStructureComponents into component lists."""
|
|
589
|
+
dimensions: list[Dimension] = []
|
|
590
|
+
time_dimension: TimeDimension | None = None
|
|
591
|
+
measure_dimension: MeasureDimension | None = None
|
|
592
|
+
attributes: list[Attribute] = []
|
|
593
|
+
measures: list[Measure] = []
|
|
594
|
+
|
|
595
|
+
dim_list_el = components_el.find(_s("DimensionList"))
|
|
596
|
+
if dim_list_el is not None:
|
|
597
|
+
dimensions.extend(_read_dimension(d, index) for d in dim_list_el.findall(_s("Dimension")))
|
|
598
|
+
td_el = dim_list_el.find(_s("TimeDimension"))
|
|
599
|
+
if td_el is not None:
|
|
600
|
+
time_dimension = _read_time_dimension(td_el, index)
|
|
601
|
+
md_el = dim_list_el.find(_s("MeasureDimension"))
|
|
602
|
+
if md_el is not None:
|
|
603
|
+
measure_dimension = _read_measure_dimension(md_el, index)
|
|
604
|
+
|
|
605
|
+
attr_list_el = components_el.find(_s("AttributeList"))
|
|
606
|
+
if attr_list_el is not None:
|
|
607
|
+
attributes.extend(_read_attribute(a, index) for a in attr_list_el.findall(_s("Attribute")))
|
|
608
|
+
|
|
609
|
+
measure_list_el = components_el.find(_s("MeasureList"))
|
|
610
|
+
if measure_list_el is not None:
|
|
611
|
+
pm_el = measure_list_el.find(_s("PrimaryMeasure"))
|
|
612
|
+
if pm_el is not None:
|
|
613
|
+
measures.append(_read_measure(pm_el, index))
|
|
614
|
+
|
|
615
|
+
groups = _read_groups(components_el)
|
|
616
|
+
return dimensions, time_dimension, measure_dimension, attributes, measures, groups
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
def _read_data_structures(
|
|
620
|
+
structures: etree._Element,
|
|
621
|
+
index: dict[str, Any],
|
|
622
|
+
agencies: dict[str, Agency],
|
|
623
|
+
) -> list[DataStructure]:
|
|
624
|
+
container = structures.find(_s("DataStructures"))
|
|
625
|
+
if container is None:
|
|
626
|
+
return []
|
|
627
|
+
result: list[DataStructure] = []
|
|
628
|
+
for dsd_el in container.findall(_s("DataStructure")):
|
|
629
|
+
dimensions: list[Dimension] = []
|
|
630
|
+
time_dimension: TimeDimension | None = None
|
|
631
|
+
measure_dimension: MeasureDimension | None = None
|
|
632
|
+
attributes: list[Attribute] = []
|
|
633
|
+
measures: list[Measure] = []
|
|
634
|
+
groups: list[Group] = []
|
|
635
|
+
|
|
636
|
+
components_el = dsd_el.find(_s("DataStructureComponents"))
|
|
637
|
+
if components_el is not None:
|
|
638
|
+
dimensions, time_dimension, measure_dimension, attributes, measures, groups = _read_dsd_components(
|
|
639
|
+
components_el, index
|
|
640
|
+
)
|
|
641
|
+
|
|
642
|
+
result.append(
|
|
643
|
+
DataStructure(
|
|
644
|
+
id=dsd_el.get("id", ""),
|
|
645
|
+
maintainer=_resolve_agency(dsd_el.get("agencyID", ""), agencies),
|
|
646
|
+
version=dsd_el.get("version", "1.0"),
|
|
647
|
+
name=_read_istring(dsd_el),
|
|
648
|
+
description=_read_istring(dsd_el, "Description"),
|
|
649
|
+
is_final=dsd_el.get("isFinal", "false").lower() == "true",
|
|
650
|
+
dimensions=ItemList(dimensions),
|
|
651
|
+
time_dimension=time_dimension,
|
|
652
|
+
measure_dimension=measure_dimension,
|
|
653
|
+
attributes=ItemList(attributes),
|
|
654
|
+
measures=ItemList(measures),
|
|
655
|
+
groups=ItemList(groups) if groups else None,
|
|
656
|
+
annotations=_read_annotations(dsd_el),
|
|
657
|
+
valid_from=_read_datetime_attr(dsd_el, "validFrom"),
|
|
658
|
+
valid_to=_read_datetime_attr(dsd_el, "validTo"),
|
|
659
|
+
)
|
|
660
|
+
)
|
|
661
|
+
return result
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
# ── Dataflows ─────────────────────────────────────────────────────────────────
|
|
665
|
+
|
|
666
|
+
|
|
667
|
+
def _read_dataflows(
|
|
668
|
+
structures: etree._Element,
|
|
669
|
+
index: dict[str, Any],
|
|
670
|
+
agencies: dict[str, Agency],
|
|
671
|
+
) -> list[Dataflow]:
|
|
672
|
+
container = structures.find(_s("Dataflows"))
|
|
673
|
+
if container is None:
|
|
674
|
+
return []
|
|
675
|
+
result: list[Dataflow] = []
|
|
676
|
+
for df_el in container.findall(_s("Dataflow")):
|
|
677
|
+
structure_ref: AnyRef | None = None
|
|
678
|
+
struct_el = df_el.find(_s("Structure"))
|
|
679
|
+
if struct_el is not None:
|
|
680
|
+
structure_ref = _resolve_ref(struct_el, index)
|
|
681
|
+
result.append(
|
|
682
|
+
Dataflow(
|
|
683
|
+
id=df_el.get("id", ""),
|
|
684
|
+
maintainer=_resolve_agency(df_el.get("agencyID", ""), agencies),
|
|
685
|
+
version=df_el.get("version", "1.0"),
|
|
686
|
+
name=_read_istring(df_el),
|
|
687
|
+
description=_read_istring(df_el, "Description"),
|
|
688
|
+
annotations=_read_annotations(df_el),
|
|
689
|
+
structure=structure_ref,
|
|
690
|
+
valid_from=_read_datetime_attr(df_el, "validFrom"),
|
|
691
|
+
valid_to=_read_datetime_attr(df_el, "validTo"),
|
|
692
|
+
)
|
|
693
|
+
)
|
|
694
|
+
return result
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
# ── CategorySchemes & Categorisations ─────────────────────────────────────────
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def _read_categories(parent_el: etree._Element) -> list[Category]:
|
|
701
|
+
"""Recursively read Category elements."""
|
|
702
|
+
return [
|
|
703
|
+
Category(
|
|
704
|
+
id=cat_el.get("id", ""),
|
|
705
|
+
name=_read_istring(cat_el),
|
|
706
|
+
description=_read_istring(cat_el, "Description"),
|
|
707
|
+
categories=ItemList(_read_categories(cat_el)),
|
|
708
|
+
annotations=_read_annotations(cat_el),
|
|
709
|
+
)
|
|
710
|
+
for cat_el in parent_el.findall(_s("Category"))
|
|
711
|
+
]
|
|
712
|
+
|
|
713
|
+
|
|
714
|
+
def _read_category_schemes(structures: etree._Element, agencies: dict[str, Agency]) -> list[CategoryScheme]:
|
|
715
|
+
container = structures.find(_s("CategorySchemes"))
|
|
716
|
+
if container is None:
|
|
717
|
+
return []
|
|
718
|
+
return [
|
|
719
|
+
CategoryScheme(
|
|
720
|
+
id=cs_el.get("id", ""),
|
|
721
|
+
maintainer=_resolve_agency(cs_el.get("agencyID", ""), agencies),
|
|
722
|
+
version=cs_el.get("version", "1.0"),
|
|
723
|
+
name=_read_istring(cs_el),
|
|
724
|
+
description=_read_istring(cs_el, "Description"),
|
|
725
|
+
categories=ItemList(_read_categories(cs_el)),
|
|
726
|
+
annotations=_read_annotations(cs_el),
|
|
727
|
+
valid_from=_read_datetime_attr(cs_el, "validFrom"),
|
|
728
|
+
valid_to=_read_datetime_attr(cs_el, "validTo"),
|
|
729
|
+
)
|
|
730
|
+
for cs_el in container.findall(_s("CategoryScheme"))
|
|
731
|
+
]
|
|
732
|
+
|
|
733
|
+
|
|
734
|
+
def _read_categorisations(
|
|
735
|
+
structures: etree._Element,
|
|
736
|
+
index: dict[str, Any],
|
|
737
|
+
agencies: dict[str, Agency],
|
|
738
|
+
) -> list[Categorisation]:
|
|
739
|
+
container = structures.find(_s("Categorisations"))
|
|
740
|
+
if container is None:
|
|
741
|
+
return []
|
|
742
|
+
result: list[Categorisation] = []
|
|
743
|
+
for cat_el in container.findall(_s("Categorisation")):
|
|
744
|
+
source_ref: AnyRef | None = None
|
|
745
|
+
source_el = cat_el.find(_s("Source"))
|
|
746
|
+
if source_el is not None:
|
|
747
|
+
source_ref = _resolve_ref(source_el, index)
|
|
748
|
+
|
|
749
|
+
target_ref: AnyRef | None = None
|
|
750
|
+
target_el = cat_el.find(_s("Target"))
|
|
751
|
+
if target_el is not None:
|
|
752
|
+
target_ref = _resolve_ref(target_el, index)
|
|
753
|
+
|
|
754
|
+
result.append(
|
|
755
|
+
Categorisation(
|
|
756
|
+
id=cat_el.get("id", ""),
|
|
757
|
+
maintainer=_resolve_agency(cat_el.get("agencyID", ""), agencies),
|
|
758
|
+
version=cat_el.get("version", "1.0"),
|
|
759
|
+
name=_read_istring(cat_el),
|
|
760
|
+
description=_read_istring(cat_el, "Description"),
|
|
761
|
+
annotations=_read_annotations(cat_el),
|
|
762
|
+
source=source_ref,
|
|
763
|
+
target=target_ref,
|
|
764
|
+
valid_from=_read_datetime_attr(cat_el, "validFrom"),
|
|
765
|
+
valid_to=_read_datetime_attr(cat_el, "validTo"),
|
|
766
|
+
)
|
|
767
|
+
)
|
|
768
|
+
return result
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
# ── ProvisionAgreements ───────────────────────────────────────────────────────
|
|
772
|
+
|
|
773
|
+
|
|
774
|
+
def _read_provision_agreements(
|
|
775
|
+
structures: etree._Element,
|
|
776
|
+
index: dict[str, Any],
|
|
777
|
+
agencies: dict[str, Agency],
|
|
778
|
+
) -> list[ProvisionAgreement]:
|
|
779
|
+
container = structures.find(_s("ProvisionAgreements"))
|
|
780
|
+
if container is None:
|
|
781
|
+
return []
|
|
782
|
+
result: list[ProvisionAgreement] = []
|
|
783
|
+
for pa_el in container.findall(_s("ProvisionAgreement")):
|
|
784
|
+
dataflow_ref: AnyRef | None = None
|
|
785
|
+
su_el = pa_el.find(_s("StructureUsage"))
|
|
786
|
+
if su_el is not None:
|
|
787
|
+
dataflow_ref = _resolve_ref(su_el, index)
|
|
788
|
+
|
|
789
|
+
provider_ref: AnyRef | None = None
|
|
790
|
+
dp_el = pa_el.find(_s("DataProvider"))
|
|
791
|
+
if dp_el is not None:
|
|
792
|
+
provider_ref = _resolve_ref(dp_el, index)
|
|
793
|
+
|
|
794
|
+
result.append(
|
|
795
|
+
ProvisionAgreement(
|
|
796
|
+
id=pa_el.get("id", ""),
|
|
797
|
+
maintainer=_resolve_agency(pa_el.get("agencyID", ""), agencies),
|
|
798
|
+
version=pa_el.get("version", "1.0"),
|
|
799
|
+
name=_read_istring(pa_el),
|
|
800
|
+
description=_read_istring(pa_el, "Description"),
|
|
801
|
+
annotations=_read_annotations(pa_el),
|
|
802
|
+
dataflow=dataflow_ref,
|
|
803
|
+
data_provider=provider_ref,
|
|
804
|
+
valid_from=_read_datetime_attr(pa_el, "validFrom"),
|
|
805
|
+
valid_to=_read_datetime_attr(pa_el, "validTo"),
|
|
806
|
+
)
|
|
807
|
+
)
|
|
808
|
+
return result
|
|
809
|
+
|
|
810
|
+
|
|
811
|
+
# ── Constraints ───────────────────────────────────────────────────────────────
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _read_reference_period(cc_el: etree._Element) -> ReferencePeriod | None:
|
|
815
|
+
rp_el = cc_el.find(_s("ReferencePeriod"))
|
|
816
|
+
if rp_el is None:
|
|
817
|
+
return None
|
|
818
|
+
return ReferencePeriod(
|
|
819
|
+
start_period=datetime.fromisoformat(rp_el.get("startTime", "")),
|
|
820
|
+
end_period=datetime.fromisoformat(rp_el.get("endTime", "")),
|
|
821
|
+
)
|
|
822
|
+
|
|
823
|
+
|
|
824
|
+
def _read_release_calendar(cc_el: etree._Element) -> ReleaseCalendar | None:
|
|
825
|
+
rc_el = cc_el.find(_s("ReleaseCalendar"))
|
|
826
|
+
if rc_el is None:
|
|
827
|
+
return None
|
|
828
|
+
periodicity_el = rc_el.find(_s("Periodicity"))
|
|
829
|
+
offset_el = rc_el.find(_s("Offset"))
|
|
830
|
+
tolerance_el = rc_el.find(_s("Tolerance"))
|
|
831
|
+
return ReleaseCalendar(
|
|
832
|
+
periodicity=periodicity_el.text if periodicity_el is not None and periodicity_el.text else "",
|
|
833
|
+
offset=offset_el.text if offset_el is not None and offset_el.text else "",
|
|
834
|
+
tolerance=tolerance_el.text if tolerance_el is not None and tolerance_el.text else "",
|
|
835
|
+
)
|
|
836
|
+
|
|
837
|
+
|
|
838
|
+
def _read_constraints(
|
|
839
|
+
structures: etree._Element,
|
|
840
|
+
index: dict[str, Any],
|
|
841
|
+
agencies: dict[str, Agency],
|
|
842
|
+
) -> list[ContentConstraint]:
|
|
843
|
+
container = structures.find(_s("Constraints"))
|
|
844
|
+
if container is None:
|
|
845
|
+
return []
|
|
846
|
+
result: list[ContentConstraint] = []
|
|
847
|
+
for cc_el in container.findall(_s("ContentConstraint")):
|
|
848
|
+
# ConstraintAttachment
|
|
849
|
+
attach = ConstraintAttachment()
|
|
850
|
+
att_el = cc_el.find(_s("ConstraintAttachment"))
|
|
851
|
+
if att_el is not None:
|
|
852
|
+
df_el = att_el.find(_s("Dataflow"))
|
|
853
|
+
if df_el is not None:
|
|
854
|
+
ref = _resolve_ref(df_el, index)
|
|
855
|
+
attach = ConstraintAttachment(dataflow=ref)
|
|
856
|
+
else:
|
|
857
|
+
ds_el = att_el.find(_s("DataStructure"))
|
|
858
|
+
if ds_el is not None:
|
|
859
|
+
ref = _resolve_ref(ds_el, index)
|
|
860
|
+
attach = ConstraintAttachment(data_structure=ref)
|
|
861
|
+
else:
|
|
862
|
+
pa_el = att_el.find(_s("ProvisionAgreement"))
|
|
863
|
+
if pa_el is not None:
|
|
864
|
+
ref = _resolve_ref(pa_el, index)
|
|
865
|
+
attach = ConstraintAttachment(provision_agreement=ref)
|
|
866
|
+
|
|
867
|
+
# CubeRegions
|
|
868
|
+
regions: list[CubeRegion] = []
|
|
869
|
+
for region_el in cc_el.findall(_s("CubeRegion")):
|
|
870
|
+
include_str = region_el.get("include", "true")
|
|
871
|
+
include = include_str.lower() != "false"
|
|
872
|
+
keys: list[KeyValue] = []
|
|
873
|
+
for kv_el in region_el.findall(_c("KeyValue")):
|
|
874
|
+
dim_id = kv_el.get("id", "")
|
|
875
|
+
values = frozenset(v.text for v in kv_el.findall(_c("Value")) if v.text)
|
|
876
|
+
keys.append(KeyValue(dimension_id=dim_id, values=values))
|
|
877
|
+
regions.append(CubeRegion(include=include, keys=tuple(keys)))
|
|
878
|
+
|
|
879
|
+
result.append(
|
|
880
|
+
ContentConstraint(
|
|
881
|
+
id=cc_el.get("id", ""),
|
|
882
|
+
maintainer=_resolve_agency(cc_el.get("agencyID", ""), agencies),
|
|
883
|
+
version=cc_el.get("version", "1.0"),
|
|
884
|
+
name=_read_istring(cc_el),
|
|
885
|
+
description=_read_istring(cc_el, "Description"),
|
|
886
|
+
is_final=cc_el.get("isFinal", "false").lower() == "true",
|
|
887
|
+
constraint_type=cc_el.get("type", "Allowed"),
|
|
888
|
+
attachment=attach,
|
|
889
|
+
regions=tuple(regions),
|
|
890
|
+
reference_period=_read_reference_period(cc_el),
|
|
891
|
+
release_calendar=_read_release_calendar(cc_el),
|
|
892
|
+
annotations=_read_annotations(cc_el),
|
|
893
|
+
valid_from=_read_datetime_attr(cc_el, "validFrom"),
|
|
894
|
+
valid_to=_read_datetime_attr(cc_el, "validTo"),
|
|
895
|
+
)
|
|
896
|
+
)
|
|
897
|
+
return result
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
# ── StructureSets ─────────────────────────────────────────────────────────────
|
|
901
|
+
|
|
902
|
+
|
|
903
|
+
def _read_source_target_refs(el: etree._Element, index: dict[str, Any]) -> tuple[AnyRef | None, AnyRef | None]:
|
|
904
|
+
"""Read Source and Target ref children from a map element."""
|
|
905
|
+
source_ref = None
|
|
906
|
+
src_el = el.find(_s("Source"))
|
|
907
|
+
if src_el is not None:
|
|
908
|
+
source_ref = _resolve_ref(src_el, index)
|
|
909
|
+
target_ref = None
|
|
910
|
+
tgt_el = el.find(_s("Target"))
|
|
911
|
+
if tgt_el is not None:
|
|
912
|
+
target_ref = _resolve_ref(tgt_el, index)
|
|
913
|
+
return source_ref, target_ref
|
|
914
|
+
|
|
915
|
+
|
|
916
|
+
def _read_id_pair_maps(parent: etree._Element, tag: str, cls: type[Any]) -> list[Any]:
|
|
917
|
+
"""Read item maps that have Source/Target children with Ref id attributes."""
|
|
918
|
+
results: list[Any] = []
|
|
919
|
+
for m_el in parent.findall(_s(tag)):
|
|
920
|
+
m_src = m_el.find(_s("Source"))
|
|
921
|
+
m_tgt = m_el.find(_s("Target"))
|
|
922
|
+
if m_src is not None and m_tgt is not None:
|
|
923
|
+
src_ref_el = m_src.find("Ref")
|
|
924
|
+
tgt_ref_el = m_tgt.find("Ref")
|
|
925
|
+
if src_ref_el is not None and tgt_ref_el is not None:
|
|
926
|
+
results.append(
|
|
927
|
+
cls(
|
|
928
|
+
source_id=src_ref_el.get("id", ""),
|
|
929
|
+
target_id=tgt_ref_el.get("id", ""),
|
|
930
|
+
)
|
|
931
|
+
)
|
|
932
|
+
return results
|
|
933
|
+
|
|
934
|
+
|
|
935
|
+
def _read_scheme_map(
|
|
936
|
+
parent: etree._Element,
|
|
937
|
+
scheme_tag: str,
|
|
938
|
+
scheme_cls: type[Any],
|
|
939
|
+
item_tag: str,
|
|
940
|
+
item_cls: type[Any],
|
|
941
|
+
index: dict[str, Any],
|
|
942
|
+
) -> list[Any]:
|
|
943
|
+
"""Read a list of scheme maps (CodelistMap, ConceptSchemeMap, etc.)."""
|
|
944
|
+
results: list[Any] = []
|
|
945
|
+
for el in parent.findall(_s(scheme_tag)):
|
|
946
|
+
source_ref, target_ref = _read_source_target_refs(el, index)
|
|
947
|
+
item_maps = _read_id_pair_maps(el, item_tag, item_cls)
|
|
948
|
+
results.append(
|
|
949
|
+
scheme_cls(
|
|
950
|
+
id=el.get("id", ""),
|
|
951
|
+
name=_read_istring(el),
|
|
952
|
+
description=_read_istring(el, "Description"),
|
|
953
|
+
source=source_ref,
|
|
954
|
+
target=target_ref,
|
|
955
|
+
maps=tuple(item_maps),
|
|
956
|
+
)
|
|
957
|
+
)
|
|
958
|
+
return results
|
|
959
|
+
|
|
960
|
+
|
|
961
|
+
def _read_structure_sets(
|
|
962
|
+
structures: etree._Element,
|
|
963
|
+
index: dict[str, Any],
|
|
964
|
+
agencies: dict[str, Agency],
|
|
965
|
+
) -> list[StructureSet]:
|
|
966
|
+
container = structures.find(_s("StructureSets"))
|
|
967
|
+
if container is None:
|
|
968
|
+
return []
|
|
969
|
+
result: list[StructureSet] = []
|
|
970
|
+
for ss_el in container.findall(_s("StructureSet")):
|
|
971
|
+
# DataflowMaps (no item maps)
|
|
972
|
+
dataflow_maps: list[DataflowMap] = []
|
|
973
|
+
for dfm_el in ss_el.findall(_s("DataflowMap")):
|
|
974
|
+
source_ref, target_ref = _read_source_target_refs(dfm_el, index)
|
|
975
|
+
dataflow_maps.append(
|
|
976
|
+
DataflowMap(
|
|
977
|
+
id=dfm_el.get("id", ""),
|
|
978
|
+
name=_read_istring(dfm_el),
|
|
979
|
+
description=_read_istring(dfm_el, "Description"),
|
|
980
|
+
source=source_ref,
|
|
981
|
+
target=target_ref,
|
|
982
|
+
)
|
|
983
|
+
)
|
|
984
|
+
|
|
985
|
+
result.append(
|
|
986
|
+
StructureSet(
|
|
987
|
+
id=ss_el.get("id", ""),
|
|
988
|
+
maintainer=_resolve_agency(ss_el.get("agencyID", ""), agencies),
|
|
989
|
+
version=ss_el.get("version", "1.0"),
|
|
990
|
+
name=_read_istring(ss_el),
|
|
991
|
+
description=_read_istring(ss_el, "Description"),
|
|
992
|
+
is_final=ss_el.get("isFinal", "false").lower() == "true",
|
|
993
|
+
codelist_maps=tuple(_read_scheme_map(ss_el, "CodelistMap", CodelistMap, "CodeMap", CodeMap, index)),
|
|
994
|
+
concept_scheme_maps=tuple(
|
|
995
|
+
_read_scheme_map(ss_el, "ConceptSchemeMap", ConceptSchemeMap, "ConceptMap", ConceptMap, index)
|
|
996
|
+
),
|
|
997
|
+
category_scheme_maps=tuple(
|
|
998
|
+
_read_scheme_map(ss_el, "CategorySchemeMap", CategorySchemeMap, "CategoryMap", CategoryMap, index)
|
|
999
|
+
),
|
|
1000
|
+
data_structure_maps=tuple(
|
|
1001
|
+
_read_scheme_map(ss_el, "DataStructureMap", DataStructureMap, "ComponentMap", ComponentMap, index)
|
|
1002
|
+
),
|
|
1003
|
+
dataflow_maps=tuple(dataflow_maps),
|
|
1004
|
+
annotations=_read_annotations(ss_el),
|
|
1005
|
+
valid_from=_read_datetime_attr(ss_el, "validFrom"),
|
|
1006
|
+
valid_to=_read_datetime_attr(ss_el, "validTo"),
|
|
1007
|
+
)
|
|
1008
|
+
)
|
|
1009
|
+
return result
|
|
1010
|
+
|
|
1011
|
+
|
|
1012
|
+
# ── Hierarchies ───────────────────────────────────────────────────────────────
|
|
1013
|
+
|
|
1014
|
+
|
|
1015
|
+
def _read_hierarchical_code(hc_el: etree._Element) -> HierarchicalCode:
|
|
1016
|
+
"""Recursively parse a HierarchicalCode element."""
|
|
1017
|
+
code_ref: SdmxUrn[Any] | None = None
|
|
1018
|
+
code_id_el = hc_el.find(_s("CodeID"))
|
|
1019
|
+
if code_id_el is not None:
|
|
1020
|
+
ref_el = code_id_el.find("Ref")
|
|
1021
|
+
if ref_el is not None:
|
|
1022
|
+
agency = ref_el.get("agencyID", "")
|
|
1023
|
+
parent_id = ref_el.get("maintainableParentID", "")
|
|
1024
|
+
item_id = ref_el.get("id", "")
|
|
1025
|
+
version = ref_el.get("version", "latest")
|
|
1026
|
+
if agency and parent_id and item_id:
|
|
1027
|
+
code_ref = SdmxUrn.make(
|
|
1028
|
+
object,
|
|
1029
|
+
package="codelist",
|
|
1030
|
+
artefact_class="Code",
|
|
1031
|
+
agency=agency,
|
|
1032
|
+
id=parent_id,
|
|
1033
|
+
version=version,
|
|
1034
|
+
item_id=item_id,
|
|
1035
|
+
)
|
|
1036
|
+
|
|
1037
|
+
children = [_read_hierarchical_code(child_el) for child_el in hc_el.findall(_s("HierarchicalCode"))]
|
|
1038
|
+
|
|
1039
|
+
return HierarchicalCode(
|
|
1040
|
+
id=hc_el.get("id", ""),
|
|
1041
|
+
code=code_ref,
|
|
1042
|
+
name=_read_istring(hc_el),
|
|
1043
|
+
description=_read_istring(hc_el, "Description"),
|
|
1044
|
+
children=children,
|
|
1045
|
+
)
|
|
1046
|
+
|
|
1047
|
+
|
|
1048
|
+
def _read_hierarchies(
|
|
1049
|
+
structures: etree._Element,
|
|
1050
|
+
agencies: dict[str, Agency],
|
|
1051
|
+
) -> list[Hierarchy]:
|
|
1052
|
+
container = structures.find(_s("HierarchicalCodelists"))
|
|
1053
|
+
if container is None:
|
|
1054
|
+
return []
|
|
1055
|
+
result: list[Hierarchy] = []
|
|
1056
|
+
for hcl_el in container.findall(_s("HierarchicalCodelist")):
|
|
1057
|
+
hcl_agency = hcl_el.get("agencyID", "")
|
|
1058
|
+
hcl_version = hcl_el.get("version", "1.0")
|
|
1059
|
+
for h_el in hcl_el.findall(_s("Hierarchy")):
|
|
1060
|
+
tree = [_read_hierarchical_code(hc_el) for hc_el in h_el.findall(_s("HierarchicalCode"))]
|
|
1061
|
+
result.append(
|
|
1062
|
+
Hierarchy(
|
|
1063
|
+
id=h_el.get("id", ""),
|
|
1064
|
+
maintainer=_resolve_agency(hcl_agency, agencies),
|
|
1065
|
+
version=hcl_version,
|
|
1066
|
+
name=_read_istring(h_el),
|
|
1067
|
+
description=_read_istring(h_el, "Description"),
|
|
1068
|
+
annotations=_read_annotations(h_el),
|
|
1069
|
+
tree=tree,
|
|
1070
|
+
)
|
|
1071
|
+
)
|
|
1072
|
+
return result
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def read(data: bytes, *, registry: Registry | None = None) -> StructureMessage:
|
|
1079
|
+
"""Parse SDMX-ML 2.1 bytes into a StructureMessage.
|
|
1080
|
+
|
|
1081
|
+
References within the message are auto-resolved.
|
|
1082
|
+
|
|
1083
|
+
If *registry* is provided all parsed artefacts are added to it via
|
|
1084
|
+
``registry.add_all()``, interning their Refs for cross-message resolution.
|
|
1085
|
+
"""
|
|
1086
|
+
root = etree.fromstring(data)
|
|
1087
|
+
structures = root.find(_m("Structures"))
|
|
1088
|
+
if structures is None:
|
|
1089
|
+
return StructureMessage()
|
|
1090
|
+
|
|
1091
|
+
# ── Phase 1: parse leaf artefacts (no refs to other artefacts) ──────────
|
|
1092
|
+
agency_schemes, data_provider_schemes = _read_organisation_schemes(structures)
|
|
1093
|
+
|
|
1094
|
+
# Build agency lookup for opportunistic resolution in subsequent phases
|
|
1095
|
+
agencies: dict[str, Agency] = {a.id: a for s in agency_schemes for a in s.agencies}
|
|
1096
|
+
|
|
1097
|
+
codelists = _read_codelists(structures, agencies)
|
|
1098
|
+
concept_schemes = _read_concept_schemes(structures, agencies)
|
|
1099
|
+
category_schemes = _read_category_schemes(structures, agencies)
|
|
1100
|
+
|
|
1101
|
+
# ── Phase 2: build resolution index ─────────────────────────────────────
|
|
1102
|
+
index: dict[str, Any] = {}
|
|
1103
|
+
|
|
1104
|
+
for cl in codelists:
|
|
1105
|
+
_index_artefact(index, cl, Codelist)
|
|
1106
|
+
|
|
1107
|
+
for cs in concept_schemes:
|
|
1108
|
+
_index_artefact(index, cs, ConceptScheme)
|
|
1109
|
+
for concept in cs.concepts:
|
|
1110
|
+
_index_item(index, concept, cs, ConceptScheme.sdmx_package, "Concept")
|
|
1111
|
+
|
|
1112
|
+
for cat_s in category_schemes:
|
|
1113
|
+
_index_artefact(index, cat_s, CategoryScheme)
|
|
1114
|
+
|
|
1115
|
+
# ── Phase 3: parse artefacts that reference codelists / concepts ─────────
|
|
1116
|
+
data_structures = _read_data_structures(structures, index, agencies)
|
|
1117
|
+
for dsd in data_structures:
|
|
1118
|
+
_index_artefact(index, dsd, DataStructure)
|
|
1119
|
+
|
|
1120
|
+
dataflows = _read_dataflows(structures, index, agencies)
|
|
1121
|
+
for df in dataflows:
|
|
1122
|
+
_index_artefact(index, df, Dataflow)
|
|
1123
|
+
|
|
1124
|
+
# ── Phase 4: parse artefacts that reference DSDs / dataflows ────────────
|
|
1125
|
+
categorisations = _read_categorisations(structures, index, agencies)
|
|
1126
|
+
provision_agreements = _read_provision_agreements(structures, index, agencies)
|
|
1127
|
+
constraints = _read_constraints(structures, index, agencies)
|
|
1128
|
+
structure_sets = _read_structure_sets(structures, index, agencies)
|
|
1129
|
+
hierarchies = _read_hierarchies(structures, agencies)
|
|
1130
|
+
|
|
1131
|
+
msg = StructureMessage(
|
|
1132
|
+
codelists=ArtefactList(codelists),
|
|
1133
|
+
concept_schemes=ArtefactList(concept_schemes),
|
|
1134
|
+
data_structures=ArtefactList(data_structures),
|
|
1135
|
+
dataflows=ArtefactList(dataflows),
|
|
1136
|
+
category_schemes=ArtefactList(category_schemes),
|
|
1137
|
+
categorisations=ArtefactList(categorisations),
|
|
1138
|
+
provision_agreements=ArtefactList(provision_agreements),
|
|
1139
|
+
agency_schemes=ArtefactList(agency_schemes),
|
|
1140
|
+
data_provider_schemes=ArtefactList(data_provider_schemes),
|
|
1141
|
+
constraints=ArtefactList(constraints),
|
|
1142
|
+
structure_sets=ArtefactList(structure_sets),
|
|
1143
|
+
hierarchies=ArtefactList(hierarchies),
|
|
1144
|
+
)
|
|
1145
|
+
if registry is not None:
|
|
1146
|
+
registry.add_all(msg.all_artefacts())
|
|
1147
|
+
return msg
|