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,897 @@
|
|
|
1
|
+
"""SDMX-JSON 2.0 reader: JSON → domain model.
|
|
2
|
+
|
|
3
|
+
Parses SDMX-JSON 2.0 Structure messages into the sdmxlib domain model.
|
|
4
|
+
All references within the message are auto-resolved where possible.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from sdmxlib.model.annotations import Annotation, Annotations
|
|
12
|
+
from sdmxlib.model.category import Categorisation, Category, CategoryScheme
|
|
13
|
+
from sdmxlib.model.codelist import Code, Codelist
|
|
14
|
+
from sdmxlib.model.collections import ArtefactList, ItemList
|
|
15
|
+
from sdmxlib.model.concept import Concept, ConceptScheme
|
|
16
|
+
from sdmxlib.model.constraint import (
|
|
17
|
+
ConstraintAttachment,
|
|
18
|
+
ContentConstraint,
|
|
19
|
+
CubeRegion,
|
|
20
|
+
KeyValue,
|
|
21
|
+
ReferencePeriod,
|
|
22
|
+
ReleaseCalendar,
|
|
23
|
+
)
|
|
24
|
+
from sdmxlib.model.dataflow import Dataflow
|
|
25
|
+
from sdmxlib.model.datastructure import (
|
|
26
|
+
AttachmentLevel,
|
|
27
|
+
Attribute,
|
|
28
|
+
DataStructure,
|
|
29
|
+
Dimension,
|
|
30
|
+
Group,
|
|
31
|
+
Measure,
|
|
32
|
+
TimeDimension,
|
|
33
|
+
)
|
|
34
|
+
from sdmxlib.model.hierarchy import HierarchicalCode, Hierarchy
|
|
35
|
+
from sdmxlib.model.istring import InternationalString
|
|
36
|
+
from sdmxlib.model.mapping import (
|
|
37
|
+
CategoryMap,
|
|
38
|
+
CategorySchemeMap,
|
|
39
|
+
CodelistMap,
|
|
40
|
+
CodeMap,
|
|
41
|
+
ComponentMap,
|
|
42
|
+
ConceptMap,
|
|
43
|
+
ConceptSchemeMap,
|
|
44
|
+
DataflowMap,
|
|
45
|
+
DataStructureMap,
|
|
46
|
+
StructureSet,
|
|
47
|
+
)
|
|
48
|
+
from sdmxlib.model.message import StructureMessage
|
|
49
|
+
from sdmxlib.model.organisation import Agency, AgencyScheme, Contact, DataProvider, DataProviderScheme
|
|
50
|
+
from sdmxlib.model.provision import ProvisionAgreement
|
|
51
|
+
from sdmxlib.model.ref import AnyRef, Ref
|
|
52
|
+
from sdmxlib.model.registry import Registry
|
|
53
|
+
from sdmxlib.model.representation import Facet
|
|
54
|
+
from sdmxlib.model.urn import SdmxUrn
|
|
55
|
+
|
|
56
|
+
# ── Common helpers ────────────────────────────────────────────────────────────
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _istring(obj: dict[str, Any], key: str) -> InternationalString:
|
|
60
|
+
"""Extract a multilingual string field from a JSON object.
|
|
61
|
+
|
|
62
|
+
Tries ``key`` first, then ``key + "s"`` (e.g. ``"name"`` → ``"names"``),
|
|
63
|
+
because the SDMX Global Registry sends ``"names": {"en": "..."}`` while
|
|
64
|
+
locally-authored fixtures may use ``"name": {"en": "..."}``.
|
|
65
|
+
"""
|
|
66
|
+
val = obj.get(key)
|
|
67
|
+
if not isinstance(val, dict) or not val:
|
|
68
|
+
val = obj.get(key + "s")
|
|
69
|
+
if not isinstance(val, dict) or not val:
|
|
70
|
+
return InternationalString()
|
|
71
|
+
return InternationalString.of({k: v for k, v in val.items() if isinstance(v, str)})
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _read_datetime_str(obj: dict[str, Any], key: str) -> datetime | None:
|
|
75
|
+
"""Parse an optional datetime string field."""
|
|
76
|
+
val = obj.get(key)
|
|
77
|
+
if not isinstance(val, str) or not val:
|
|
78
|
+
return None
|
|
79
|
+
return datetime.fromisoformat(val)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _read_annotations(obj: dict[str, Any]) -> Annotations:
|
|
83
|
+
"""Parse the optional ``annotations`` array on any artefact."""
|
|
84
|
+
raw = obj.get("annotations")
|
|
85
|
+
if not raw:
|
|
86
|
+
return Annotations()
|
|
87
|
+
items: list[Annotation] = []
|
|
88
|
+
for ann in raw:
|
|
89
|
+
text_raw = ann.get("text")
|
|
90
|
+
text = InternationalString.of(text_raw) if isinstance(text_raw, dict) else InternationalString()
|
|
91
|
+
items.append(
|
|
92
|
+
Annotation(
|
|
93
|
+
id=ann.get("id"),
|
|
94
|
+
title=ann.get("title"),
|
|
95
|
+
type=ann.get("type"),
|
|
96
|
+
url=ann.get("url"),
|
|
97
|
+
text=text,
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
return Annotations(items)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ── Ref / URN helpers ─────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _resolve_urn(urn_val: Any, index: dict[str, Any]) -> AnyRef | None:
|
|
107
|
+
"""Parse a URN string and resolve it against the artefact index."""
|
|
108
|
+
if not isinstance(urn_val, str) or not urn_val.startswith("urn:"):
|
|
109
|
+
return None
|
|
110
|
+
try:
|
|
111
|
+
urn = SdmxUrn.parse(urn_val)
|
|
112
|
+
except (ValueError, TypeError):
|
|
113
|
+
return None
|
|
114
|
+
obj = index.get(str(urn))
|
|
115
|
+
if obj is not None:
|
|
116
|
+
return Ref.of(urn, obj)
|
|
117
|
+
return Ref.unresolved(urn)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ── Index helpers ─────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _index_artefact(index: dict[str, Any], obj: Any, artefact_type: type[Any]) -> None:
|
|
124
|
+
"""Add a maintainable artefact to the resolution index."""
|
|
125
|
+
package = artefact_type.sdmx_package
|
|
126
|
+
cls = artefact_type.sdmx_class
|
|
127
|
+
urn = SdmxUrn.make(
|
|
128
|
+
artefact_type,
|
|
129
|
+
package=package,
|
|
130
|
+
artefact_class=cls,
|
|
131
|
+
agency=obj.agency_id,
|
|
132
|
+
id=obj.id,
|
|
133
|
+
version=obj.version,
|
|
134
|
+
)
|
|
135
|
+
index[str(urn)] = obj
|
|
136
|
+
urn_latest = SdmxUrn.make(artefact_type, package=package, artefact_class=cls, agency=obj.agency_id, id=obj.id)
|
|
137
|
+
index[str(urn_latest)] = obj
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _index_item(index: dict[str, Any], item: Any, parent: Any, package: str, cls: str) -> None:
|
|
141
|
+
"""Add an item (e.g. Concept) to the index under its item URN."""
|
|
142
|
+
urn = SdmxUrn.make(
|
|
143
|
+
object,
|
|
144
|
+
package=package,
|
|
145
|
+
artefact_class=cls,
|
|
146
|
+
agency=parent.agency_id,
|
|
147
|
+
id=parent.id,
|
|
148
|
+
version=parent.version,
|
|
149
|
+
item_id=item.id,
|
|
150
|
+
)
|
|
151
|
+
index[str(urn)] = item
|
|
152
|
+
urn_latest = SdmxUrn.make(
|
|
153
|
+
object,
|
|
154
|
+
package=package,
|
|
155
|
+
artefact_class=cls,
|
|
156
|
+
agency=parent.agency_id,
|
|
157
|
+
id=parent.id,
|
|
158
|
+
item_id=item.id,
|
|
159
|
+
)
|
|
160
|
+
index[str(urn_latest)] = item
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
# ── Representation ────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _read_representation(rep: dict[str, Any] | None, index: dict[str, Any]) -> AnyRef | Facet | None:
|
|
167
|
+
"""Parse a ``localRepresentation`` object → Ref[Codelist] | Facet | None."""
|
|
168
|
+
if not rep:
|
|
169
|
+
return None
|
|
170
|
+
|
|
171
|
+
enum_val = rep.get("enumeration")
|
|
172
|
+
if enum_val is not None:
|
|
173
|
+
return _resolve_urn(enum_val, index)
|
|
174
|
+
|
|
175
|
+
tf_raw = rep.get("textFormat")
|
|
176
|
+
if isinstance(tf_raw, dict):
|
|
177
|
+
return Facet(
|
|
178
|
+
type=tf_raw.get("textType"),
|
|
179
|
+
max_length=tf_raw.get("maxLength"),
|
|
180
|
+
min_length=tf_raw.get("minLength"),
|
|
181
|
+
max_value=tf_raw.get("maxValue"),
|
|
182
|
+
min_value=tf_raw.get("minValue"),
|
|
183
|
+
decimals=tf_raw.get("decimals"),
|
|
184
|
+
pattern=tf_raw.get("pattern"),
|
|
185
|
+
start_value=tf_raw.get("startValue"),
|
|
186
|
+
end_value=tf_raw.get("endValue"),
|
|
187
|
+
is_sequence=tf_raw.get("isSequence"),
|
|
188
|
+
is_multi_lingual=tf_raw.get("isMultiLingual"),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _resolve_agency(agency_id: str, agencies: dict[str, Agency]) -> Agency:
|
|
195
|
+
"""Return the rich Agency if parsed from an AgencyScheme, else a minimal one."""
|
|
196
|
+
return agencies.get(agency_id, Agency(id=agency_id))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
# ── OrganisationSchemes ───────────────────────────────────────────────────────
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _read_contact(obj: dict[str, Any]) -> Contact:
|
|
203
|
+
return Contact(
|
|
204
|
+
name=_istring(obj, "name"),
|
|
205
|
+
department=_istring(obj, "department"),
|
|
206
|
+
role=_istring(obj, "role"),
|
|
207
|
+
phones=tuple(obj.get("phones") or []),
|
|
208
|
+
faxes=tuple(obj.get("faxes") or []),
|
|
209
|
+
uris=tuple(obj.get("uris") or []),
|
|
210
|
+
emails=tuple(obj.get("emails") or []),
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def _read_organisation_schemes(
|
|
215
|
+
data: dict[str, Any],
|
|
216
|
+
) -> tuple[list[AgencyScheme], list[DataProviderScheme]]:
|
|
217
|
+
agency_schemes: list[AgencyScheme] = []
|
|
218
|
+
provider_schemes: list[DataProviderScheme] = []
|
|
219
|
+
|
|
220
|
+
for as_obj in data.get("agencySchemes") or []:
|
|
221
|
+
agencies = [
|
|
222
|
+
Agency(
|
|
223
|
+
id=item["id"],
|
|
224
|
+
name=_istring(item, "name"),
|
|
225
|
+
description=_istring(item, "description"),
|
|
226
|
+
contacts=tuple(_read_contact(c) for c in (item.get("contacts") or [])),
|
|
227
|
+
annotations=_read_annotations(item),
|
|
228
|
+
)
|
|
229
|
+
for item in (as_obj.get("items") or [])
|
|
230
|
+
]
|
|
231
|
+
agency_schemes.append(
|
|
232
|
+
AgencyScheme(
|
|
233
|
+
id=as_obj["id"],
|
|
234
|
+
maintainer=as_obj.get("agencyID", ""),
|
|
235
|
+
version=as_obj.get("version", "1.0"),
|
|
236
|
+
name=_istring(as_obj, "name"),
|
|
237
|
+
description=_istring(as_obj, "description"),
|
|
238
|
+
is_final=bool(as_obj.get("isFinal", False)),
|
|
239
|
+
agencies=ItemList(agencies),
|
|
240
|
+
annotations=_read_annotations(as_obj),
|
|
241
|
+
valid_from=_read_datetime_str(as_obj, "validFrom"),
|
|
242
|
+
valid_to=_read_datetime_str(as_obj, "validTo"),
|
|
243
|
+
)
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
for ps_obj in data.get("dataProviderSchemes") or []:
|
|
247
|
+
providers = [
|
|
248
|
+
DataProvider(
|
|
249
|
+
id=item["id"],
|
|
250
|
+
name=_istring(item, "name"),
|
|
251
|
+
description=_istring(item, "description"),
|
|
252
|
+
contacts=tuple(_read_contact(c) for c in (item.get("contacts") or [])),
|
|
253
|
+
annotations=_read_annotations(item),
|
|
254
|
+
)
|
|
255
|
+
for item in (ps_obj.get("items") or [])
|
|
256
|
+
]
|
|
257
|
+
provider_schemes.append(
|
|
258
|
+
DataProviderScheme(
|
|
259
|
+
id=ps_obj["id"],
|
|
260
|
+
maintainer=ps_obj.get("agencyID", ""),
|
|
261
|
+
version=ps_obj.get("version", "1.0"),
|
|
262
|
+
name=_istring(ps_obj, "name"),
|
|
263
|
+
description=_istring(ps_obj, "description"),
|
|
264
|
+
is_final=bool(ps_obj.get("isFinal", False)),
|
|
265
|
+
providers=ItemList(providers),
|
|
266
|
+
annotations=_read_annotations(ps_obj),
|
|
267
|
+
valid_from=_read_datetime_str(ps_obj, "validFrom"),
|
|
268
|
+
valid_to=_read_datetime_str(ps_obj, "validTo"),
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
return agency_schemes, provider_schemes
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# ── Codelists ─────────────────────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _read_codelists(data: dict[str, Any], agencies: dict[str, Agency]) -> list[Codelist]:
|
|
279
|
+
result: list[Codelist] = []
|
|
280
|
+
for cl in data.get("codelists") or []:
|
|
281
|
+
codes = [
|
|
282
|
+
Code(
|
|
283
|
+
id=item["id"],
|
|
284
|
+
name=_istring(item, "name"),
|
|
285
|
+
description=_istring(item, "description"),
|
|
286
|
+
parent_id=item.get("parent"),
|
|
287
|
+
annotations=_read_annotations(item),
|
|
288
|
+
)
|
|
289
|
+
for item in (cl.get("codes") or cl.get("items") or [])
|
|
290
|
+
]
|
|
291
|
+
result.append(
|
|
292
|
+
Codelist(
|
|
293
|
+
id=cl["id"],
|
|
294
|
+
maintainer=_resolve_agency(cl.get("agencyID", ""), agencies),
|
|
295
|
+
version=cl.get("version", "1.0"),
|
|
296
|
+
name=_istring(cl, "name"),
|
|
297
|
+
description=_istring(cl, "description"),
|
|
298
|
+
is_final=bool(cl.get("isFinal", False)),
|
|
299
|
+
codes=ItemList(codes),
|
|
300
|
+
annotations=_read_annotations(cl),
|
|
301
|
+
valid_from=_read_datetime_str(cl, "validFrom"),
|
|
302
|
+
valid_to=_read_datetime_str(cl, "validTo"),
|
|
303
|
+
)
|
|
304
|
+
)
|
|
305
|
+
return result
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
# ── ConceptSchemes ────────────────────────────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _read_concept_schemes(data: dict[str, Any], agencies: dict[str, Agency]) -> list[ConceptScheme]:
|
|
312
|
+
result: list[ConceptScheme] = []
|
|
313
|
+
for cs in data.get("conceptSchemes") or []:
|
|
314
|
+
concepts = [
|
|
315
|
+
Concept(
|
|
316
|
+
id=item["id"],
|
|
317
|
+
name=_istring(item, "name"),
|
|
318
|
+
description=_istring(item, "description"),
|
|
319
|
+
annotations=_read_annotations(item),
|
|
320
|
+
)
|
|
321
|
+
for item in cs.get("items") or []
|
|
322
|
+
]
|
|
323
|
+
result.append(
|
|
324
|
+
ConceptScheme(
|
|
325
|
+
id=cs["id"],
|
|
326
|
+
maintainer=_resolve_agency(cs.get("agencyID", ""), agencies),
|
|
327
|
+
version=cs.get("version", "1.0"),
|
|
328
|
+
name=_istring(cs, "name"),
|
|
329
|
+
description=_istring(cs, "description"),
|
|
330
|
+
concepts=ItemList(concepts),
|
|
331
|
+
annotations=_read_annotations(cs),
|
|
332
|
+
valid_from=_read_datetime_str(cs, "validFrom"),
|
|
333
|
+
valid_to=_read_datetime_str(cs, "validTo"),
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
return result
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
# ── CategorySchemes ───────────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _read_categories(items: list[dict[str, Any]]) -> list[Category]:
|
|
343
|
+
"""Recursively build Category objects from a JSON items array."""
|
|
344
|
+
return [
|
|
345
|
+
Category(
|
|
346
|
+
id=item["id"],
|
|
347
|
+
name=_istring(item, "name"),
|
|
348
|
+
description=_istring(item, "description"),
|
|
349
|
+
categories=ItemList(_read_categories(item.get("items") or [])),
|
|
350
|
+
annotations=_read_annotations(item),
|
|
351
|
+
)
|
|
352
|
+
for item in items
|
|
353
|
+
]
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _read_category_schemes(data: dict[str, Any], agencies: dict[str, Agency]) -> list[CategoryScheme]:
|
|
357
|
+
return [
|
|
358
|
+
CategoryScheme(
|
|
359
|
+
id=cs["id"],
|
|
360
|
+
maintainer=_resolve_agency(cs.get("agencyID", ""), agencies),
|
|
361
|
+
version=cs.get("version", "1.0"),
|
|
362
|
+
name=_istring(cs, "name"),
|
|
363
|
+
description=_istring(cs, "description"),
|
|
364
|
+
categories=ItemList(_read_categories(cs.get("items") or [])),
|
|
365
|
+
annotations=_read_annotations(cs),
|
|
366
|
+
valid_from=_read_datetime_str(cs, "validFrom"),
|
|
367
|
+
valid_to=_read_datetime_str(cs, "validTo"),
|
|
368
|
+
)
|
|
369
|
+
for cs in data.get("categorySchemes") or []
|
|
370
|
+
]
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ── DataStructures ────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _read_attachment(ar: dict[str, Any]) -> tuple[AttachmentLevel | None, list[str] | None, str | None]:
|
|
377
|
+
"""Parse ``attributeRelationship`` → (level, related_dimensions, related_measure)."""
|
|
378
|
+
if "observation" in ar or "primaryMeasure" in ar:
|
|
379
|
+
return AttachmentLevel.OBSERVATION, None, "OBS_VALUE"
|
|
380
|
+
if "dataSet" in ar or "none" in ar:
|
|
381
|
+
return AttachmentLevel.DATASET, None, None
|
|
382
|
+
dims = ar.get("dimensions")
|
|
383
|
+
if dims:
|
|
384
|
+
related = [d if isinstance(d, str) else (d.get("componentId") or d.get("id", "")) for d in dims]
|
|
385
|
+
return AttachmentLevel.SERIES, related or None, None
|
|
386
|
+
if "group" in ar:
|
|
387
|
+
return AttachmentLevel.GROUP, None, None
|
|
388
|
+
return AttachmentLevel.DATASET, None, None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def _read_dimension(dim: dict[str, Any], index: dict[str, Any]) -> Dimension:
|
|
392
|
+
concept_ref = _resolve_urn(dim.get("conceptIdentity"), index)
|
|
393
|
+
return Dimension(
|
|
394
|
+
id=dim["id"],
|
|
395
|
+
concept=concept_ref,
|
|
396
|
+
representation=_read_representation(dim.get("localRepresentation"), index),
|
|
397
|
+
annotations=_read_annotations(dim),
|
|
398
|
+
)
|
|
399
|
+
|
|
400
|
+
|
|
401
|
+
def _read_time_dimension(td: dict[str, Any], index: dict[str, Any]) -> TimeDimension:
|
|
402
|
+
rep = _read_representation(td.get("localRepresentation"), index)
|
|
403
|
+
concept_ref = _resolve_urn(td.get("conceptIdentity"), index)
|
|
404
|
+
return TimeDimension(
|
|
405
|
+
id=td.get("id", "TIME_PERIOD"),
|
|
406
|
+
concept=concept_ref,
|
|
407
|
+
representation=rep if isinstance(rep, Facet) else None,
|
|
408
|
+
annotations=_read_annotations(td),
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _read_attribute(attr: dict[str, Any], index: dict[str, Any]) -> Attribute:
|
|
413
|
+
concept_ref = _resolve_urn(attr.get("conceptIdentity"), index)
|
|
414
|
+
attachment: AttachmentLevel | None = None
|
|
415
|
+
related_dims: list[str] | None = None
|
|
416
|
+
related_measure: str | None = None
|
|
417
|
+
ar = attr.get("attributeRelationship")
|
|
418
|
+
if isinstance(ar, dict):
|
|
419
|
+
attachment, related_dims, related_measure = _read_attachment(ar)
|
|
420
|
+
return Attribute(
|
|
421
|
+
id=attr["id"],
|
|
422
|
+
usage=attr.get("usage") or attr.get("assignmentStatus"),
|
|
423
|
+
attachment=attachment,
|
|
424
|
+
related_dimensions=related_dims,
|
|
425
|
+
related_measure=related_measure,
|
|
426
|
+
concept=concept_ref,
|
|
427
|
+
representation=_read_representation(attr.get("localRepresentation"), index),
|
|
428
|
+
annotations=_read_annotations(attr),
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _read_measure(meas: dict[str, Any], index: dict[str, Any]) -> Measure:
|
|
433
|
+
concept_ref = _resolve_urn(meas.get("conceptIdentity"), index)
|
|
434
|
+
return Measure(
|
|
435
|
+
id=meas.get("id", "OBS_VALUE"),
|
|
436
|
+
concept=concept_ref,
|
|
437
|
+
representation=_read_representation(meas.get("localRepresentation"), index),
|
|
438
|
+
annotations=_read_annotations(meas),
|
|
439
|
+
)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
def _read_groups(components: dict[str, Any]) -> list[Group]:
|
|
443
|
+
result: list[Group] = []
|
|
444
|
+
for grp in components.get("groups") or []:
|
|
445
|
+
dim_ids = [gd.get("componentId") or gd.get("id", "") for gd in grp.get("groupDimensions") or []]
|
|
446
|
+
result.append(Group(id=grp["id"], dimension_ids=dim_ids))
|
|
447
|
+
return result
|
|
448
|
+
|
|
449
|
+
|
|
450
|
+
def _read_data_structures(
|
|
451
|
+
data: dict[str, Any],
|
|
452
|
+
index: dict[str, Any],
|
|
453
|
+
agencies: dict[str, Agency],
|
|
454
|
+
) -> list[DataStructure]:
|
|
455
|
+
result: list[DataStructure] = []
|
|
456
|
+
for dsd in data.get("dataStructures") or []:
|
|
457
|
+
components = dsd.get("dataStructureComponents") or {}
|
|
458
|
+
|
|
459
|
+
dim_list = components.get("dimensionList") or {}
|
|
460
|
+
dimensions = [_read_dimension(d, index) for d in dim_list.get("dimensions") or []]
|
|
461
|
+
|
|
462
|
+
td_raw = dim_list.get("timeDimension")
|
|
463
|
+
time_dimension = _read_time_dimension(td_raw, index) if td_raw else None
|
|
464
|
+
|
|
465
|
+
attr_list = components.get("attributeList") or {}
|
|
466
|
+
attributes = [_read_attribute(a, index) for a in attr_list.get("attributes") or []]
|
|
467
|
+
|
|
468
|
+
measure_list = components.get("measureList") or {}
|
|
469
|
+
# SDMX 3.0 style: measures array; SDMX 2.1 fallback: primaryMeasure object
|
|
470
|
+
raw_measures = measure_list.get("measures") or []
|
|
471
|
+
if not raw_measures and (pm := measure_list.get("primaryMeasure")):
|
|
472
|
+
raw_measures = [pm]
|
|
473
|
+
measures = [_read_measure(m, index) for m in raw_measures]
|
|
474
|
+
|
|
475
|
+
groups = _read_groups(components)
|
|
476
|
+
|
|
477
|
+
result.append(
|
|
478
|
+
DataStructure(
|
|
479
|
+
id=dsd["id"],
|
|
480
|
+
maintainer=_resolve_agency(dsd.get("agencyID", ""), agencies),
|
|
481
|
+
version=dsd.get("version", "1.0"),
|
|
482
|
+
name=_istring(dsd, "name"),
|
|
483
|
+
description=_istring(dsd, "description"),
|
|
484
|
+
is_final=bool(dsd.get("isFinal", False)),
|
|
485
|
+
dimensions=ItemList(dimensions),
|
|
486
|
+
time_dimension=time_dimension,
|
|
487
|
+
attributes=ItemList(attributes),
|
|
488
|
+
measures=ItemList(measures),
|
|
489
|
+
groups=ItemList(groups) if groups else None,
|
|
490
|
+
annotations=_read_annotations(dsd),
|
|
491
|
+
valid_from=_read_datetime_str(dsd, "validFrom"),
|
|
492
|
+
valid_to=_read_datetime_str(dsd, "validTo"),
|
|
493
|
+
)
|
|
494
|
+
)
|
|
495
|
+
return result
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
# ── Dataflows ─────────────────────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
def _read_dataflows(
|
|
502
|
+
data: dict[str, Any],
|
|
503
|
+
index: dict[str, Any],
|
|
504
|
+
agencies: dict[str, Agency],
|
|
505
|
+
) -> list[Dataflow]:
|
|
506
|
+
result: list[Dataflow] = []
|
|
507
|
+
for df in data.get("dataflows") or []:
|
|
508
|
+
structure_ref = _resolve_urn(df.get("structure"), index)
|
|
509
|
+
result.append(
|
|
510
|
+
Dataflow(
|
|
511
|
+
id=df["id"],
|
|
512
|
+
maintainer=_resolve_agency(df.get("agencyID", ""), agencies),
|
|
513
|
+
version=df.get("version", "1.0"),
|
|
514
|
+
name=_istring(df, "name"),
|
|
515
|
+
description=_istring(df, "description"),
|
|
516
|
+
annotations=_read_annotations(df),
|
|
517
|
+
structure=structure_ref,
|
|
518
|
+
valid_from=_read_datetime_str(df, "validFrom"),
|
|
519
|
+
valid_to=_read_datetime_str(df, "validTo"),
|
|
520
|
+
)
|
|
521
|
+
)
|
|
522
|
+
return result
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
# ── Categorisations ───────────────────────────────────────────────────────────
|
|
526
|
+
|
|
527
|
+
|
|
528
|
+
def _read_categorisations(
|
|
529
|
+
data: dict[str, Any],
|
|
530
|
+
index: dict[str, Any],
|
|
531
|
+
agencies: dict[str, Agency],
|
|
532
|
+
) -> list[Categorisation]:
|
|
533
|
+
return [
|
|
534
|
+
Categorisation(
|
|
535
|
+
id=cat["id"],
|
|
536
|
+
maintainer=_resolve_agency(cat.get("agencyID", ""), agencies),
|
|
537
|
+
version=cat.get("version", "1.0"),
|
|
538
|
+
name=_istring(cat, "name"),
|
|
539
|
+
description=_istring(cat, "description"),
|
|
540
|
+
annotations=_read_annotations(cat),
|
|
541
|
+
source=_resolve_urn(cat.get("source"), index),
|
|
542
|
+
target=_resolve_urn(cat.get("target"), index),
|
|
543
|
+
valid_from=_read_datetime_str(cat, "validFrom"),
|
|
544
|
+
valid_to=_read_datetime_str(cat, "validTo"),
|
|
545
|
+
)
|
|
546
|
+
for cat in data.get("categorisations") or []
|
|
547
|
+
]
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
# ── ProvisionAgreements ───────────────────────────────────────────────────────
|
|
551
|
+
|
|
552
|
+
|
|
553
|
+
def _read_provision_agreements(
|
|
554
|
+
data: dict[str, Any],
|
|
555
|
+
index: dict[str, Any],
|
|
556
|
+
agencies: dict[str, Agency],
|
|
557
|
+
) -> list[ProvisionAgreement]:
|
|
558
|
+
return [
|
|
559
|
+
ProvisionAgreement(
|
|
560
|
+
id=pa["id"],
|
|
561
|
+
maintainer=_resolve_agency(pa.get("agencyID", ""), agencies),
|
|
562
|
+
version=pa.get("version", "1.0"),
|
|
563
|
+
name=_istring(pa, "name"),
|
|
564
|
+
description=_istring(pa, "description"),
|
|
565
|
+
annotations=_read_annotations(pa),
|
|
566
|
+
dataflow=_resolve_urn(pa.get("structureUsage") or pa.get("dataflow"), index),
|
|
567
|
+
data_provider=_resolve_urn(pa.get("dataProvider"), index),
|
|
568
|
+
valid_from=_read_datetime_str(pa, "validFrom"),
|
|
569
|
+
valid_to=_read_datetime_str(pa, "validTo"),
|
|
570
|
+
)
|
|
571
|
+
for pa in data.get("provisionAgreements") or []
|
|
572
|
+
]
|
|
573
|
+
|
|
574
|
+
|
|
575
|
+
def _read_constraints(
|
|
576
|
+
data: dict[str, Any],
|
|
577
|
+
index: dict[str, Any],
|
|
578
|
+
agencies: dict[str, Agency],
|
|
579
|
+
) -> list[ContentConstraint]:
|
|
580
|
+
"""Parse dataConstraints (SDMX-JSON key) into ContentConstraint objects."""
|
|
581
|
+
result: list[ContentConstraint] = []
|
|
582
|
+
for cc in data.get("dataConstraints") or []:
|
|
583
|
+
# ConstraintAttachment
|
|
584
|
+
attach = ConstraintAttachment()
|
|
585
|
+
ca = cc.get("constraintAttachment")
|
|
586
|
+
if ca:
|
|
587
|
+
if dataflow_urn := ca.get("dataflow"):
|
|
588
|
+
ref = _resolve_urn(dataflow_urn, index)
|
|
589
|
+
attach = ConstraintAttachment(dataflow=ref)
|
|
590
|
+
elif ds_urn := ca.get("dataStructure"):
|
|
591
|
+
ref = _resolve_urn(ds_urn, index)
|
|
592
|
+
attach = ConstraintAttachment(data_structure=ref)
|
|
593
|
+
elif pa_urn := ca.get("provisionAgreement"):
|
|
594
|
+
ref = _resolve_urn(pa_urn, index)
|
|
595
|
+
attach = ConstraintAttachment(provision_agreement=ref)
|
|
596
|
+
|
|
597
|
+
regions: list[CubeRegion] = []
|
|
598
|
+
for region in cc.get("cubeRegions") or []:
|
|
599
|
+
include = region.get("include", True)
|
|
600
|
+
keys: list[KeyValue] = []
|
|
601
|
+
for kv in region.get("keyValues") or []:
|
|
602
|
+
dim_id = kv.get("id", "")
|
|
603
|
+
values = frozenset(str(v) for v in kv.get("values") or [])
|
|
604
|
+
keys.append(KeyValue(dimension_id=dim_id, values=values))
|
|
605
|
+
regions.append(CubeRegion(include=include, keys=tuple(keys)))
|
|
606
|
+
|
|
607
|
+
reference_period: ReferencePeriod | None = None
|
|
608
|
+
rp_raw = cc.get("referencePeriod")
|
|
609
|
+
if isinstance(rp_raw, dict):
|
|
610
|
+
reference_period = ReferencePeriod(
|
|
611
|
+
start_period=datetime.fromisoformat(rp_raw["startPeriod"]),
|
|
612
|
+
end_period=datetime.fromisoformat(rp_raw["endPeriod"]),
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
release_calendar: ReleaseCalendar | None = None
|
|
616
|
+
rc_raw = cc.get("releaseCalendar")
|
|
617
|
+
if isinstance(rc_raw, dict):
|
|
618
|
+
release_calendar = ReleaseCalendar(
|
|
619
|
+
periodicity=rc_raw["periodicity"],
|
|
620
|
+
offset=rc_raw["offset"],
|
|
621
|
+
tolerance=rc_raw["tolerance"],
|
|
622
|
+
)
|
|
623
|
+
|
|
624
|
+
result.append(
|
|
625
|
+
ContentConstraint(
|
|
626
|
+
id=cc.get("id", ""),
|
|
627
|
+
maintainer=_resolve_agency(cc.get("agencyID", ""), agencies),
|
|
628
|
+
version=cc.get("version", "1.0"),
|
|
629
|
+
name=_istring(cc, "name"),
|
|
630
|
+
description=_istring(cc, "description"),
|
|
631
|
+
is_final=cc.get("isFinal", False),
|
|
632
|
+
constraint_type=cc.get("type", "Allowed"),
|
|
633
|
+
attachment=attach,
|
|
634
|
+
regions=tuple(regions),
|
|
635
|
+
reference_period=reference_period,
|
|
636
|
+
release_calendar=release_calendar,
|
|
637
|
+
annotations=_read_annotations(cc),
|
|
638
|
+
valid_from=_read_datetime_str(cc, "validFrom"),
|
|
639
|
+
valid_to=_read_datetime_str(cc, "validTo"),
|
|
640
|
+
)
|
|
641
|
+
)
|
|
642
|
+
return result
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
def _read_structure_sets(
|
|
646
|
+
data: dict[str, Any],
|
|
647
|
+
index: dict[str, Any],
|
|
648
|
+
agencies: dict[str, Agency],
|
|
649
|
+
) -> list[StructureSet]:
|
|
650
|
+
"""Parse structureSets (SDMX-JSON key) into StructureSet objects."""
|
|
651
|
+
result: list[StructureSet] = []
|
|
652
|
+
for ss in data.get("structureSets") or []:
|
|
653
|
+
maps: list[CodelistMap] = []
|
|
654
|
+
for cm in ss.get("codelistMaps") or []:
|
|
655
|
+
source_ref = _resolve_urn(cm.get("source"), index)
|
|
656
|
+
target_ref = _resolve_urn(cm.get("target"), index)
|
|
657
|
+
code_maps = [
|
|
658
|
+
CodeMap(source_id=m["source"], target_id=m["target"])
|
|
659
|
+
for m in cm.get("codeMaps") or []
|
|
660
|
+
if "source" in m and "target" in m
|
|
661
|
+
]
|
|
662
|
+
maps.append(
|
|
663
|
+
CodelistMap(
|
|
664
|
+
id=cm.get("id", ""),
|
|
665
|
+
name=_istring(cm, "name"),
|
|
666
|
+
description=_istring(cm, "description"),
|
|
667
|
+
source=source_ref,
|
|
668
|
+
target=target_ref,
|
|
669
|
+
maps=tuple(code_maps),
|
|
670
|
+
)
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
concept_scheme_maps: list[ConceptSchemeMap] = []
|
|
674
|
+
for csm in ss.get("conceptSchemeMaps") or []:
|
|
675
|
+
source_ref = _resolve_urn(csm.get("source"), index)
|
|
676
|
+
target_ref = _resolve_urn(csm.get("target"), index)
|
|
677
|
+
concept_maps = [
|
|
678
|
+
ConceptMap(source_id=m["sourceId"], target_id=m["targetId"])
|
|
679
|
+
for m in csm.get("conceptMaps") or []
|
|
680
|
+
if "sourceId" in m and "targetId" in m
|
|
681
|
+
]
|
|
682
|
+
concept_scheme_maps.append(
|
|
683
|
+
ConceptSchemeMap(
|
|
684
|
+
id=csm.get("id", ""),
|
|
685
|
+
name=_istring(csm, "name"),
|
|
686
|
+
description=_istring(csm, "description"),
|
|
687
|
+
source=source_ref,
|
|
688
|
+
target=target_ref,
|
|
689
|
+
maps=tuple(concept_maps),
|
|
690
|
+
)
|
|
691
|
+
)
|
|
692
|
+
|
|
693
|
+
category_scheme_maps: list[CategorySchemeMap] = []
|
|
694
|
+
for catm in ss.get("categorySchemeMaps") or []:
|
|
695
|
+
source_ref = _resolve_urn(catm.get("source"), index)
|
|
696
|
+
target_ref = _resolve_urn(catm.get("target"), index)
|
|
697
|
+
cat_maps = [
|
|
698
|
+
CategoryMap(source_id=m["sourceId"], target_id=m["targetId"])
|
|
699
|
+
for m in catm.get("categoryMaps") or []
|
|
700
|
+
if "sourceId" in m and "targetId" in m
|
|
701
|
+
]
|
|
702
|
+
category_scheme_maps.append(
|
|
703
|
+
CategorySchemeMap(
|
|
704
|
+
id=catm.get("id", ""),
|
|
705
|
+
name=_istring(catm, "name"),
|
|
706
|
+
description=_istring(catm, "description"),
|
|
707
|
+
source=source_ref,
|
|
708
|
+
target=target_ref,
|
|
709
|
+
maps=tuple(cat_maps),
|
|
710
|
+
)
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
data_structure_maps: list[DataStructureMap] = []
|
|
714
|
+
for dsm in ss.get("dataStructureMaps") or []:
|
|
715
|
+
source_ref = _resolve_urn(dsm.get("source"), index)
|
|
716
|
+
target_ref = _resolve_urn(dsm.get("target"), index)
|
|
717
|
+
comp_maps = [
|
|
718
|
+
ComponentMap(source_id=m["sourceId"], target_id=m["targetId"])
|
|
719
|
+
for m in dsm.get("componentMaps") or []
|
|
720
|
+
if "sourceId" in m and "targetId" in m
|
|
721
|
+
]
|
|
722
|
+
data_structure_maps.append(
|
|
723
|
+
DataStructureMap(
|
|
724
|
+
id=dsm.get("id", ""),
|
|
725
|
+
name=_istring(dsm, "name"),
|
|
726
|
+
description=_istring(dsm, "description"),
|
|
727
|
+
source=source_ref,
|
|
728
|
+
target=target_ref,
|
|
729
|
+
maps=tuple(comp_maps),
|
|
730
|
+
)
|
|
731
|
+
)
|
|
732
|
+
|
|
733
|
+
dataflow_maps: list[DataflowMap] = []
|
|
734
|
+
for dfm in ss.get("dataflowMaps") or []:
|
|
735
|
+
source_ref = _resolve_urn(dfm.get("source"), index)
|
|
736
|
+
target_ref = _resolve_urn(dfm.get("target"), index)
|
|
737
|
+
dataflow_maps.append(
|
|
738
|
+
DataflowMap(
|
|
739
|
+
id=dfm.get("id", ""),
|
|
740
|
+
name=_istring(dfm, "name"),
|
|
741
|
+
description=_istring(dfm, "description"),
|
|
742
|
+
source=source_ref,
|
|
743
|
+
target=target_ref,
|
|
744
|
+
)
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
result.append(
|
|
748
|
+
StructureSet(
|
|
749
|
+
id=ss.get("id", ""),
|
|
750
|
+
maintainer=_resolve_agency(ss.get("agencyID", ""), agencies),
|
|
751
|
+
version=ss.get("version", "1.0"),
|
|
752
|
+
name=_istring(ss, "name"),
|
|
753
|
+
description=_istring(ss, "description"),
|
|
754
|
+
is_final=ss.get("isFinal", False),
|
|
755
|
+
codelist_maps=tuple(maps),
|
|
756
|
+
concept_scheme_maps=tuple(concept_scheme_maps),
|
|
757
|
+
category_scheme_maps=tuple(category_scheme_maps),
|
|
758
|
+
data_structure_maps=tuple(data_structure_maps),
|
|
759
|
+
dataflow_maps=tuple(dataflow_maps),
|
|
760
|
+
annotations=_read_annotations(ss),
|
|
761
|
+
valid_from=_read_datetime_str(ss, "validFrom"),
|
|
762
|
+
valid_to=_read_datetime_str(ss, "validTo"),
|
|
763
|
+
)
|
|
764
|
+
)
|
|
765
|
+
return result
|
|
766
|
+
|
|
767
|
+
|
|
768
|
+
# ── Hierarchies ──────────────────────────────────────────────────────────────
|
|
769
|
+
|
|
770
|
+
|
|
771
|
+
def _read_hierarchical_code(obj: dict[str, Any]) -> HierarchicalCode:
|
|
772
|
+
"""Recursively parse a hierarchicalCode JSON object."""
|
|
773
|
+
code_ref: Ref[Code] | None = None
|
|
774
|
+
code_urn = obj.get("code")
|
|
775
|
+
if isinstance(code_urn, str) and code_urn.startswith("urn:"):
|
|
776
|
+
try:
|
|
777
|
+
urn = SdmxUrn.parse(code_urn)
|
|
778
|
+
code_ref = Ref[Code].unresolved(urn)
|
|
779
|
+
except (ValueError, TypeError):
|
|
780
|
+
pass
|
|
781
|
+
|
|
782
|
+
children = [_read_hierarchical_code(child) for child in obj.get("hierarchicalCodes") or []]
|
|
783
|
+
|
|
784
|
+
return HierarchicalCode(
|
|
785
|
+
id=obj["id"],
|
|
786
|
+
code=code_ref,
|
|
787
|
+
name=_istring(obj, "name"),
|
|
788
|
+
description=_istring(obj, "description"),
|
|
789
|
+
children=children,
|
|
790
|
+
annotations=_read_annotations(obj),
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
|
|
794
|
+
def _read_hierarchies(
|
|
795
|
+
data: dict[str, Any],
|
|
796
|
+
agencies: dict[str, Agency],
|
|
797
|
+
) -> list[Hierarchy]:
|
|
798
|
+
"""Parse hierarchies array into Hierarchy objects."""
|
|
799
|
+
result: list[Hierarchy] = []
|
|
800
|
+
for h in data.get("hierarchies") or []:
|
|
801
|
+
tree = [_read_hierarchical_code(hc) for hc in h.get("hierarchicalCodes") or []]
|
|
802
|
+
result.append(
|
|
803
|
+
Hierarchy(
|
|
804
|
+
id=h["id"],
|
|
805
|
+
maintainer=_resolve_agency(h.get("agencyID", ""), agencies),
|
|
806
|
+
version=h.get("version", "1.0"),
|
|
807
|
+
name=_istring(h, "name"),
|
|
808
|
+
description=_istring(h, "description"),
|
|
809
|
+
is_final=bool(h.get("isFinal", False)),
|
|
810
|
+
tree=tree,
|
|
811
|
+
annotations=_read_annotations(h),
|
|
812
|
+
valid_from=_read_datetime_str(h, "validFrom"),
|
|
813
|
+
valid_to=_read_datetime_str(h, "validTo"),
|
|
814
|
+
)
|
|
815
|
+
)
|
|
816
|
+
return result
|
|
817
|
+
|
|
818
|
+
|
|
819
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
820
|
+
|
|
821
|
+
|
|
822
|
+
def read(data: bytes, *, registry: Registry | None = None) -> StructureMessage:
|
|
823
|
+
"""Parse SDMX-JSON 2.0 bytes into a StructureMessage.
|
|
824
|
+
|
|
825
|
+
References within the message are auto-resolved where URNs match
|
|
826
|
+
artefacts present in the same message.
|
|
827
|
+
|
|
828
|
+
If *registry* is provided all parsed artefacts are added to it via
|
|
829
|
+
``registry.add_all()``, interning their Refs for cross-message resolution.
|
|
830
|
+
|
|
831
|
+
Args:
|
|
832
|
+
data: Raw JSON bytes of an SDMX-JSON 2.0 Structure message.
|
|
833
|
+
registry: Optional Registry to intern artefacts into after parsing.
|
|
834
|
+
|
|
835
|
+
Returns:
|
|
836
|
+
A fully constructed StructureMessage with resolved cross-references.
|
|
837
|
+
"""
|
|
838
|
+
doc = json.loads(data)
|
|
839
|
+
payload: dict[str, Any] = doc.get("data") or {}
|
|
840
|
+
|
|
841
|
+
# ── Phase 1: parse leaf artefacts (no refs to other artefacts) ──────────
|
|
842
|
+
agency_schemes, data_provider_schemes = _read_organisation_schemes(payload)
|
|
843
|
+
|
|
844
|
+
# Build agency lookup for opportunistic resolution in subsequent phases
|
|
845
|
+
agencies: dict[str, Agency] = {a.id: a for s in agency_schemes for a in s.agencies}
|
|
846
|
+
|
|
847
|
+
codelists = _read_codelists(payload, agencies)
|
|
848
|
+
concept_schemes = _read_concept_schemes(payload, agencies)
|
|
849
|
+
category_schemes = _read_category_schemes(payload, agencies)
|
|
850
|
+
|
|
851
|
+
# ── Phase 2: build resolution index ─────────────────────────────────────
|
|
852
|
+
index: dict[str, Any] = {}
|
|
853
|
+
|
|
854
|
+
for cl in codelists:
|
|
855
|
+
_index_artefact(index, cl, Codelist)
|
|
856
|
+
|
|
857
|
+
for cs in concept_schemes:
|
|
858
|
+
_index_artefact(index, cs, ConceptScheme)
|
|
859
|
+
for concept in cs.concepts:
|
|
860
|
+
_index_item(index, concept, cs, ConceptScheme.sdmx_package, "Concept")
|
|
861
|
+
|
|
862
|
+
for cat_s in category_schemes:
|
|
863
|
+
_index_artefact(index, cat_s, CategoryScheme)
|
|
864
|
+
|
|
865
|
+
# ── Phase 3: parse artefacts that reference codelists / concepts ─────────
|
|
866
|
+
data_structures = _read_data_structures(payload, index, agencies)
|
|
867
|
+
for dsd in data_structures:
|
|
868
|
+
_index_artefact(index, dsd, DataStructure)
|
|
869
|
+
|
|
870
|
+
dataflows = _read_dataflows(payload, index, agencies)
|
|
871
|
+
for df in dataflows:
|
|
872
|
+
_index_artefact(index, df, Dataflow)
|
|
873
|
+
|
|
874
|
+
# ── Phase 4: parse artefacts that reference DSDs / dataflows ────────────
|
|
875
|
+
categorisations = _read_categorisations(payload, index, agencies)
|
|
876
|
+
provision_agreements = _read_provision_agreements(payload, index, agencies)
|
|
877
|
+
constraints = _read_constraints(payload, index, agencies)
|
|
878
|
+
structure_sets = _read_structure_sets(payload, index, agencies)
|
|
879
|
+
hierarchies = _read_hierarchies(payload, agencies)
|
|
880
|
+
|
|
881
|
+
msg = StructureMessage(
|
|
882
|
+
codelists=ArtefactList(codelists),
|
|
883
|
+
concept_schemes=ArtefactList(concept_schemes),
|
|
884
|
+
data_structures=ArtefactList(data_structures),
|
|
885
|
+
dataflows=ArtefactList(dataflows),
|
|
886
|
+
category_schemes=ArtefactList(category_schemes),
|
|
887
|
+
categorisations=ArtefactList(categorisations),
|
|
888
|
+
provision_agreements=ArtefactList(provision_agreements),
|
|
889
|
+
agency_schemes=ArtefactList(agency_schemes),
|
|
890
|
+
data_provider_schemes=ArtefactList(data_provider_schemes),
|
|
891
|
+
constraints=ArtefactList(constraints),
|
|
892
|
+
structure_sets=ArtefactList(structure_sets),
|
|
893
|
+
hierarchies=ArtefactList(hierarchies),
|
|
894
|
+
)
|
|
895
|
+
if registry is not None:
|
|
896
|
+
registry.add_all(msg.all_artefacts())
|
|
897
|
+
return msg
|