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,698 @@
|
|
|
1
|
+
"""SDMX-JSON 2.0 writer: domain model → JSON bytes.
|
|
2
|
+
|
|
3
|
+
Serialises a StructureMessage to a valid SDMX-JSON 2.0 Structure message.
|
|
4
|
+
|
|
5
|
+
Example:
|
|
6
|
+
from sdmxlib.formats.sdmx_json.writer import write
|
|
7
|
+
|
|
8
|
+
json_bytes = write(msg)
|
|
9
|
+
json_bytes = write(msg, pretty_print=True)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
from datetime import UTC, datetime
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from sdmxlib.model.category import Categorisation, CategoryScheme
|
|
17
|
+
from sdmxlib.model.codelist import Codelist
|
|
18
|
+
from sdmxlib.model.concept import ConceptScheme
|
|
19
|
+
from sdmxlib.model.constraint import ConstraintAttachment, ContentConstraint, CubeRegion
|
|
20
|
+
from sdmxlib.model.dataflow import Dataflow
|
|
21
|
+
from sdmxlib.model.datastructure import (
|
|
22
|
+
AttachmentLevel,
|
|
23
|
+
Attribute,
|
|
24
|
+
DataStructure,
|
|
25
|
+
Dimension,
|
|
26
|
+
Group,
|
|
27
|
+
Measure,
|
|
28
|
+
TimeDimension,
|
|
29
|
+
)
|
|
30
|
+
from sdmxlib.model.hierarchy import HierarchicalCode, Hierarchy
|
|
31
|
+
from sdmxlib.model.istring import InternationalString
|
|
32
|
+
from sdmxlib.model.mapping import (
|
|
33
|
+
CategorySchemeMap,
|
|
34
|
+
CodelistMap,
|
|
35
|
+
ConceptSchemeMap,
|
|
36
|
+
DataflowMap,
|
|
37
|
+
DataStructureMap,
|
|
38
|
+
StructureSet,
|
|
39
|
+
)
|
|
40
|
+
from sdmxlib.model.message import StructureMessage
|
|
41
|
+
from sdmxlib.model.organisation import Agency, AgencyScheme, Contact, DataProvider, DataProviderScheme
|
|
42
|
+
from sdmxlib.model.provision import ProvisionAgreement
|
|
43
|
+
from sdmxlib.model.ref import AnyRef, Ref
|
|
44
|
+
from sdmxlib.model.representation import Facet
|
|
45
|
+
|
|
46
|
+
# ── Common helpers ────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _istring_dict(s: InternationalString | dict[str, str]) -> dict[str, str]:
|
|
50
|
+
return {lang: s[lang] for lang in s} # type: ignore[index]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _urn_str(ref: AnyRef | None) -> str | None:
|
|
54
|
+
if ref is None:
|
|
55
|
+
return None
|
|
56
|
+
return str(ref.urn)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ── Representation ────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _write_text_format(rep: Facet) -> dict[str, Any] | None:
|
|
63
|
+
"""Serialise a Facet to a ``textFormat`` dict, omitting None fields."""
|
|
64
|
+
pairs = [
|
|
65
|
+
("textType", rep.type),
|
|
66
|
+
("maxLength", rep.max_length),
|
|
67
|
+
("minLength", rep.min_length),
|
|
68
|
+
("maxValue", rep.max_value),
|
|
69
|
+
("minValue", rep.min_value),
|
|
70
|
+
("decimals", rep.decimals),
|
|
71
|
+
("pattern", rep.pattern),
|
|
72
|
+
("startValue", rep.start_value),
|
|
73
|
+
("endValue", rep.end_value),
|
|
74
|
+
("isSequence", rep.is_sequence),
|
|
75
|
+
("isMultiLingual", rep.is_multi_lingual),
|
|
76
|
+
]
|
|
77
|
+
tf = {k: v for k, v in pairs if v is not None}
|
|
78
|
+
return {"textFormat": tf} if tf else None
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _write_representation(rep: AnyRef | Facet | None) -> dict[str, Any] | None:
|
|
82
|
+
if rep is None:
|
|
83
|
+
return None
|
|
84
|
+
if isinstance(rep, Ref):
|
|
85
|
+
return {"enumeration": str(rep.urn)}
|
|
86
|
+
return _write_text_format(rep)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# ── OrganisationSchemes ───────────────────────────────────────────────────────
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _write_contact(contact: Contact) -> dict[str, Any]:
|
|
93
|
+
obj: dict[str, Any] = {}
|
|
94
|
+
if contact.name:
|
|
95
|
+
obj["name"] = _istring_dict(contact.name)
|
|
96
|
+
if contact.department:
|
|
97
|
+
obj["department"] = _istring_dict(contact.department)
|
|
98
|
+
if contact.role:
|
|
99
|
+
obj["role"] = _istring_dict(contact.role)
|
|
100
|
+
if contact.phones:
|
|
101
|
+
obj["phones"] = list(contact.phones)
|
|
102
|
+
if contact.faxes:
|
|
103
|
+
obj["faxes"] = list(contact.faxes)
|
|
104
|
+
if contact.uris:
|
|
105
|
+
obj["uris"] = list(contact.uris)
|
|
106
|
+
if contact.emails:
|
|
107
|
+
obj["emails"] = list(contact.emails)
|
|
108
|
+
return obj
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _write_agency(agency: Agency) -> dict[str, Any]:
|
|
112
|
+
obj: dict[str, Any] = {"id": agency.id}
|
|
113
|
+
if agency.name:
|
|
114
|
+
obj["name"] = _istring_dict(agency.name)
|
|
115
|
+
if agency.description:
|
|
116
|
+
obj["description"] = _istring_dict(agency.description)
|
|
117
|
+
if agency.contacts:
|
|
118
|
+
obj["contacts"] = [_write_contact(c) for c in agency.contacts]
|
|
119
|
+
return obj
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _write_agency_scheme(scheme: AgencyScheme) -> dict[str, Any]:
|
|
123
|
+
obj: dict[str, Any] = {
|
|
124
|
+
"id": scheme.id,
|
|
125
|
+
"agencyID": scheme.agency_id,
|
|
126
|
+
"version": scheme.version,
|
|
127
|
+
}
|
|
128
|
+
if scheme.name:
|
|
129
|
+
obj["name"] = _istring_dict(scheme.name)
|
|
130
|
+
if scheme.description:
|
|
131
|
+
obj["description"] = _istring_dict(scheme.description)
|
|
132
|
+
if scheme.is_final:
|
|
133
|
+
obj["isFinal"] = True
|
|
134
|
+
if scheme.valid_from is not None:
|
|
135
|
+
obj["validFrom"] = scheme.valid_from.isoformat()
|
|
136
|
+
if scheme.valid_to is not None:
|
|
137
|
+
obj["validTo"] = scheme.valid_to.isoformat()
|
|
138
|
+
obj["items"] = [_write_agency(a) for a in scheme.agencies]
|
|
139
|
+
return obj
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _write_data_provider(provider: DataProvider) -> dict[str, Any]:
|
|
143
|
+
obj: dict[str, Any] = {"id": provider.id}
|
|
144
|
+
if provider.name:
|
|
145
|
+
obj["name"] = _istring_dict(provider.name)
|
|
146
|
+
if provider.description:
|
|
147
|
+
obj["description"] = _istring_dict(provider.description)
|
|
148
|
+
if provider.contacts:
|
|
149
|
+
obj["contacts"] = [_write_contact(c) for c in provider.contacts]
|
|
150
|
+
return obj
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _write_data_provider_scheme(scheme: DataProviderScheme) -> dict[str, Any]:
|
|
154
|
+
obj: dict[str, Any] = {
|
|
155
|
+
"id": scheme.id,
|
|
156
|
+
"agencyID": scheme.agency_id,
|
|
157
|
+
"version": scheme.version,
|
|
158
|
+
}
|
|
159
|
+
if scheme.name:
|
|
160
|
+
obj["name"] = _istring_dict(scheme.name)
|
|
161
|
+
if scheme.description:
|
|
162
|
+
obj["description"] = _istring_dict(scheme.description)
|
|
163
|
+
if scheme.is_final:
|
|
164
|
+
obj["isFinal"] = True
|
|
165
|
+
if scheme.valid_from is not None:
|
|
166
|
+
obj["validFrom"] = scheme.valid_from.isoformat()
|
|
167
|
+
if scheme.valid_to is not None:
|
|
168
|
+
obj["validTo"] = scheme.valid_to.isoformat()
|
|
169
|
+
obj["items"] = [_write_data_provider(p) for p in scheme.providers]
|
|
170
|
+
return obj
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
# ── Codelists ─────────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _write_codelist(cl: Codelist) -> dict[str, Any]:
|
|
177
|
+
obj: dict[str, Any] = {
|
|
178
|
+
"id": cl.id,
|
|
179
|
+
"agencyID": cl.agency_id,
|
|
180
|
+
"version": cl.version,
|
|
181
|
+
"name": _istring_dict(cl.name),
|
|
182
|
+
}
|
|
183
|
+
if cl.description:
|
|
184
|
+
obj["description"] = _istring_dict(cl.description)
|
|
185
|
+
if cl.is_final:
|
|
186
|
+
obj["isFinal"] = True
|
|
187
|
+
if cl.valid_from is not None:
|
|
188
|
+
obj["validFrom"] = cl.valid_from.isoformat()
|
|
189
|
+
if cl.valid_to is not None:
|
|
190
|
+
obj["validTo"] = cl.valid_to.isoformat()
|
|
191
|
+
items = []
|
|
192
|
+
for code in cl.codes:
|
|
193
|
+
item: dict[str, Any] = {"id": code.id, "name": _istring_dict(code.name)}
|
|
194
|
+
if code.description:
|
|
195
|
+
item["description"] = _istring_dict(code.description)
|
|
196
|
+
if code.parent_id is not None:
|
|
197
|
+
item["parent"] = code.parent_id
|
|
198
|
+
items.append(item)
|
|
199
|
+
obj["items"] = items
|
|
200
|
+
return obj
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
# ── ConceptSchemes ────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def _write_concept_scheme(cs: ConceptScheme) -> dict[str, Any]:
|
|
207
|
+
obj: dict[str, Any] = {
|
|
208
|
+
"id": cs.id,
|
|
209
|
+
"agencyID": cs.agency_id,
|
|
210
|
+
"version": cs.version,
|
|
211
|
+
"name": _istring_dict(cs.name),
|
|
212
|
+
}
|
|
213
|
+
if cs.description:
|
|
214
|
+
obj["description"] = _istring_dict(cs.description)
|
|
215
|
+
if cs.valid_from is not None:
|
|
216
|
+
obj["validFrom"] = cs.valid_from.isoformat()
|
|
217
|
+
if cs.valid_to is not None:
|
|
218
|
+
obj["validTo"] = cs.valid_to.isoformat()
|
|
219
|
+
obj["items"] = [
|
|
220
|
+
{
|
|
221
|
+
"id": c.id,
|
|
222
|
+
"name": _istring_dict(c.name),
|
|
223
|
+
**({"description": _istring_dict(c.description)} if c.description else {}),
|
|
224
|
+
}
|
|
225
|
+
for c in cs.concepts
|
|
226
|
+
]
|
|
227
|
+
return obj
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ── CategorySchemes ───────────────────────────────────────────────────────────
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _write_categories(categories: Any) -> list[dict[str, Any]]:
|
|
234
|
+
result = []
|
|
235
|
+
for cat in categories:
|
|
236
|
+
item: dict[str, Any] = {"id": cat.id, "name": _istring_dict(cat.name)}
|
|
237
|
+
if cat.description:
|
|
238
|
+
item["description"] = _istring_dict(cat.description)
|
|
239
|
+
sub = _write_categories(cat.categories)
|
|
240
|
+
if sub:
|
|
241
|
+
item["items"] = sub
|
|
242
|
+
result.append(item)
|
|
243
|
+
return result
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _write_category_scheme(cs: CategoryScheme) -> dict[str, Any]:
|
|
247
|
+
obj: dict[str, Any] = {
|
|
248
|
+
"id": cs.id,
|
|
249
|
+
"agencyID": cs.agency_id,
|
|
250
|
+
"version": cs.version,
|
|
251
|
+
"name": _istring_dict(cs.name),
|
|
252
|
+
}
|
|
253
|
+
if cs.description:
|
|
254
|
+
obj["description"] = _istring_dict(cs.description)
|
|
255
|
+
if cs.valid_from is not None:
|
|
256
|
+
obj["validFrom"] = cs.valid_from.isoformat()
|
|
257
|
+
if cs.valid_to is not None:
|
|
258
|
+
obj["validTo"] = cs.valid_to.isoformat()
|
|
259
|
+
obj["items"] = _write_categories(cs.categories)
|
|
260
|
+
return obj
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
# ── DataStructures ────────────────────────────────────────────────────────────
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _write_attribute_relationship(attr: Attribute) -> dict[str, Any]:
|
|
267
|
+
match attr.attachment:
|
|
268
|
+
case AttachmentLevel.OBSERVATION:
|
|
269
|
+
return {"observation": {}}
|
|
270
|
+
case AttachmentLevel.SERIES:
|
|
271
|
+
return {"dimensions": [{"componentId": d} for d in attr.related_dimensions or []]}
|
|
272
|
+
case AttachmentLevel.GROUP:
|
|
273
|
+
return {"group": {}}
|
|
274
|
+
case _:
|
|
275
|
+
return {"dataSet": {}}
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _write_dimension(dim: Dimension, position: int) -> dict[str, Any]:
|
|
279
|
+
obj: dict[str, Any] = {"id": dim.id, "position": position}
|
|
280
|
+
if isinstance(dim.concept, Ref):
|
|
281
|
+
obj["conceptIdentity"] = str(dim.concept.urn)
|
|
282
|
+
if isinstance(dim.representation, (Ref, Facet)):
|
|
283
|
+
rep = _write_representation(dim.representation)
|
|
284
|
+
if rep is not None:
|
|
285
|
+
obj["localRepresentation"] = rep
|
|
286
|
+
return obj
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _write_time_dimension(td: TimeDimension) -> dict[str, Any]:
|
|
290
|
+
obj: dict[str, Any] = {"id": td.id}
|
|
291
|
+
if isinstance(td.concept, Ref):
|
|
292
|
+
obj["conceptIdentity"] = str(td.concept.urn)
|
|
293
|
+
rep = _write_representation(td.representation)
|
|
294
|
+
if rep is not None:
|
|
295
|
+
obj["localRepresentation"] = rep
|
|
296
|
+
return obj
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _write_attribute(attr: Attribute) -> dict[str, Any]:
|
|
300
|
+
obj: dict[str, Any] = {"id": attr.id}
|
|
301
|
+
if attr.usage is not None:
|
|
302
|
+
obj["usage"] = attr.usage
|
|
303
|
+
if isinstance(attr.concept, Ref):
|
|
304
|
+
obj["conceptIdentity"] = str(attr.concept.urn)
|
|
305
|
+
if isinstance(attr.representation, (Ref, Facet)):
|
|
306
|
+
rep = _write_representation(attr.representation)
|
|
307
|
+
if rep is not None:
|
|
308
|
+
obj["localRepresentation"] = rep
|
|
309
|
+
obj["attributeRelationship"] = _write_attribute_relationship(attr)
|
|
310
|
+
return obj
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def _write_measure(meas: Measure) -> dict[str, Any]:
|
|
314
|
+
obj: dict[str, Any] = {"id": meas.id}
|
|
315
|
+
if isinstance(meas.concept, Ref):
|
|
316
|
+
obj["conceptIdentity"] = str(meas.concept.urn)
|
|
317
|
+
if isinstance(meas.representation, (Ref, Facet)):
|
|
318
|
+
rep = _write_representation(meas.representation)
|
|
319
|
+
if rep is not None:
|
|
320
|
+
obj["localRepresentation"] = rep
|
|
321
|
+
return obj
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def _write_group(grp: Group) -> dict[str, Any]:
|
|
325
|
+
return {
|
|
326
|
+
"id": grp.id,
|
|
327
|
+
"groupDimensions": [{"componentId": d} for d in grp.dimension_ids],
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def _write_data_structure(dsd: DataStructure) -> dict[str, Any]:
|
|
332
|
+
obj: dict[str, Any] = {
|
|
333
|
+
"id": dsd.id,
|
|
334
|
+
"agencyID": dsd.agency_id,
|
|
335
|
+
"version": dsd.version,
|
|
336
|
+
"name": _istring_dict(dsd.name),
|
|
337
|
+
}
|
|
338
|
+
if dsd.description:
|
|
339
|
+
obj["description"] = _istring_dict(dsd.description)
|
|
340
|
+
if dsd.is_final:
|
|
341
|
+
obj["isFinal"] = True
|
|
342
|
+
if dsd.valid_from is not None:
|
|
343
|
+
obj["validFrom"] = dsd.valid_from.isoformat()
|
|
344
|
+
if dsd.valid_to is not None:
|
|
345
|
+
obj["validTo"] = dsd.valid_to.isoformat()
|
|
346
|
+
|
|
347
|
+
components: dict[str, Any] = {}
|
|
348
|
+
|
|
349
|
+
dim_list: dict[str, Any] = {
|
|
350
|
+
"dimensions": [_write_dimension(d, i) for i, d in enumerate(dsd.dimensions, start=1)],
|
|
351
|
+
}
|
|
352
|
+
if dsd.time_dimension is not None:
|
|
353
|
+
dim_list["timeDimension"] = _write_time_dimension(dsd.time_dimension)
|
|
354
|
+
components["dimensionList"] = dim_list
|
|
355
|
+
|
|
356
|
+
if dsd.attributes:
|
|
357
|
+
components["attributeList"] = {
|
|
358
|
+
"attributes": [_write_attribute(a) for a in dsd.attributes],
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if dsd.measures:
|
|
362
|
+
components["measureList"] = {
|
|
363
|
+
"measures": [_write_measure(m) for m in dsd.measures],
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
if dsd.groups:
|
|
367
|
+
components["groups"] = [_write_group(g) for g in dsd.groups]
|
|
368
|
+
|
|
369
|
+
obj["dataStructureComponents"] = components
|
|
370
|
+
return obj
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
# ── Dataflows ─────────────────────────────────────────────────────────────────
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def _write_dataflow(df: Dataflow) -> dict[str, Any]:
|
|
377
|
+
obj: dict[str, Any] = {
|
|
378
|
+
"id": df.id,
|
|
379
|
+
"agencyID": df.agency_id,
|
|
380
|
+
"version": df.version,
|
|
381
|
+
"name": _istring_dict(df.name),
|
|
382
|
+
}
|
|
383
|
+
if df.description:
|
|
384
|
+
obj["description"] = _istring_dict(df.description)
|
|
385
|
+
if df.valid_from is not None:
|
|
386
|
+
obj["validFrom"] = df.valid_from.isoformat()
|
|
387
|
+
if df.valid_to is not None:
|
|
388
|
+
obj["validTo"] = df.valid_to.isoformat()
|
|
389
|
+
if isinstance(df.structure, Ref):
|
|
390
|
+
obj["structure"] = str(df.structure.urn)
|
|
391
|
+
return obj
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
# ── Categorisations ───────────────────────────────────────────────────────────
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _write_categorisation(cat: Categorisation) -> dict[str, Any]:
|
|
398
|
+
obj: dict[str, Any] = {
|
|
399
|
+
"id": cat.id,
|
|
400
|
+
"agencyID": cat.agency_id,
|
|
401
|
+
"version": cat.version,
|
|
402
|
+
"name": _istring_dict(cat.name),
|
|
403
|
+
}
|
|
404
|
+
if cat.description:
|
|
405
|
+
obj["description"] = _istring_dict(cat.description)
|
|
406
|
+
if cat.valid_from is not None:
|
|
407
|
+
obj["validFrom"] = cat.valid_from.isoformat()
|
|
408
|
+
if cat.valid_to is not None:
|
|
409
|
+
obj["validTo"] = cat.valid_to.isoformat()
|
|
410
|
+
if cat.source is not None:
|
|
411
|
+
obj["source"] = str(cat.source.urn)
|
|
412
|
+
if cat.target is not None:
|
|
413
|
+
obj["target"] = str(cat.target.urn)
|
|
414
|
+
return obj
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
# ── ProvisionAgreements ───────────────────────────────────────────────────────
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _write_provision_agreement(pa: ProvisionAgreement) -> dict[str, Any]:
|
|
421
|
+
obj: dict[str, Any] = {
|
|
422
|
+
"id": pa.id,
|
|
423
|
+
"agencyID": pa.agency_id,
|
|
424
|
+
"version": pa.version,
|
|
425
|
+
"name": _istring_dict(pa.name),
|
|
426
|
+
}
|
|
427
|
+
if pa.description:
|
|
428
|
+
obj["description"] = _istring_dict(pa.description)
|
|
429
|
+
if pa.valid_from is not None:
|
|
430
|
+
obj["validFrom"] = pa.valid_from.isoformat()
|
|
431
|
+
if pa.valid_to is not None:
|
|
432
|
+
obj["validTo"] = pa.valid_to.isoformat()
|
|
433
|
+
if pa.dataflow is not None:
|
|
434
|
+
obj["structureUsage"] = str(pa.dataflow.urn)
|
|
435
|
+
if pa.data_provider is not None:
|
|
436
|
+
obj["dataProvider"] = str(pa.data_provider.urn)
|
|
437
|
+
return obj
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ── Constraints ───────────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def _write_constraint_attachment(attach: ConstraintAttachment) -> dict[str, Any] | None:
|
|
444
|
+
ca: dict[str, Any] = {}
|
|
445
|
+
if attach.dataflow is not None:
|
|
446
|
+
ca["dataflow"] = str(attach.dataflow.urn)
|
|
447
|
+
elif attach.data_structure is not None:
|
|
448
|
+
ca["dataStructure"] = str(attach.data_structure.urn)
|
|
449
|
+
elif attach.provision_agreement is not None:
|
|
450
|
+
ca["provisionAgreement"] = str(attach.provision_agreement.urn)
|
|
451
|
+
return ca or None
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
def _write_cube_regions(regions: tuple[CubeRegion, ...]) -> list[dict[str, Any]]:
|
|
455
|
+
return [
|
|
456
|
+
{
|
|
457
|
+
"include": region.include,
|
|
458
|
+
"keyValues": [{"id": kv.dimension_id, "values": sorted(kv.values)} for kv in region.keys],
|
|
459
|
+
}
|
|
460
|
+
for region in regions
|
|
461
|
+
]
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
def _write_content_constraint(cc: ContentConstraint) -> dict[str, Any]:
|
|
465
|
+
obj: dict[str, Any] = {
|
|
466
|
+
"id": cc.id,
|
|
467
|
+
"agencyID": cc.agency_id,
|
|
468
|
+
"version": cc.version,
|
|
469
|
+
"type": cc.constraint_type,
|
|
470
|
+
"name": _istring_dict(cc.name),
|
|
471
|
+
}
|
|
472
|
+
if cc.description:
|
|
473
|
+
obj["description"] = _istring_dict(cc.description)
|
|
474
|
+
if cc.is_final:
|
|
475
|
+
obj["isFinal"] = True
|
|
476
|
+
if cc.valid_from is not None:
|
|
477
|
+
obj["validFrom"] = cc.valid_from.isoformat()
|
|
478
|
+
if cc.valid_to is not None:
|
|
479
|
+
obj["validTo"] = cc.valid_to.isoformat()
|
|
480
|
+
|
|
481
|
+
ca = _write_constraint_attachment(cc.attachment)
|
|
482
|
+
if ca is not None:
|
|
483
|
+
obj["constraintAttachment"] = ca
|
|
484
|
+
if cc.regions:
|
|
485
|
+
obj["cubeRegions"] = _write_cube_regions(cc.regions)
|
|
486
|
+
if cc.reference_period is not None:
|
|
487
|
+
obj["referencePeriod"] = {
|
|
488
|
+
"startPeriod": cc.reference_period.start_period.isoformat(),
|
|
489
|
+
"endPeriod": cc.reference_period.end_period.isoformat(),
|
|
490
|
+
}
|
|
491
|
+
if cc.release_calendar is not None:
|
|
492
|
+
obj["releaseCalendar"] = {
|
|
493
|
+
"periodicity": cc.release_calendar.periodicity,
|
|
494
|
+
"offset": cc.release_calendar.offset,
|
|
495
|
+
"tolerance": cc.release_calendar.tolerance,
|
|
496
|
+
}
|
|
497
|
+
return obj
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
# ── StructureSets ─────────────────────────────────────────────────────────────
|
|
501
|
+
|
|
502
|
+
|
|
503
|
+
def _write_codelist_map(cm: CodelistMap) -> dict[str, Any]:
|
|
504
|
+
obj: dict[str, Any] = {
|
|
505
|
+
"id": cm.id,
|
|
506
|
+
"name": _istring_dict(cm.name),
|
|
507
|
+
}
|
|
508
|
+
if cm.description:
|
|
509
|
+
obj["description"] = _istring_dict(cm.description)
|
|
510
|
+
if cm.source is not None:
|
|
511
|
+
obj["source"] = str(cm.source.urn)
|
|
512
|
+
if cm.target is not None:
|
|
513
|
+
obj["target"] = str(cm.target.urn)
|
|
514
|
+
if cm.maps:
|
|
515
|
+
obj["codeMaps"] = [{"source": m.source_id, "target": m.target_id} for m in cm.maps]
|
|
516
|
+
return obj
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def _write_structure_set(ss: StructureSet) -> dict[str, Any]:
|
|
520
|
+
obj: dict[str, Any] = {
|
|
521
|
+
"id": ss.id,
|
|
522
|
+
"agencyID": ss.agency_id,
|
|
523
|
+
"version": ss.version,
|
|
524
|
+
"name": _istring_dict(ss.name),
|
|
525
|
+
}
|
|
526
|
+
if ss.description:
|
|
527
|
+
obj["description"] = _istring_dict(ss.description)
|
|
528
|
+
if ss.is_final:
|
|
529
|
+
obj["isFinal"] = True
|
|
530
|
+
if ss.valid_from is not None:
|
|
531
|
+
obj["validFrom"] = ss.valid_from.isoformat()
|
|
532
|
+
if ss.valid_to is not None:
|
|
533
|
+
obj["validTo"] = ss.valid_to.isoformat()
|
|
534
|
+
if ss.codelist_maps:
|
|
535
|
+
obj["codelistMaps"] = [_write_codelist_map(cm) for cm in ss.codelist_maps]
|
|
536
|
+
if ss.concept_scheme_maps:
|
|
537
|
+
obj["conceptSchemeMaps"] = [_write_concept_scheme_map(csm) for csm in ss.concept_scheme_maps]
|
|
538
|
+
if ss.category_scheme_maps:
|
|
539
|
+
obj["categorySchemeMaps"] = [_write_category_scheme_map(catm) for catm in ss.category_scheme_maps]
|
|
540
|
+
if ss.data_structure_maps:
|
|
541
|
+
obj["dataStructureMaps"] = [_write_data_structure_map(dsm) for dsm in ss.data_structure_maps]
|
|
542
|
+
if ss.dataflow_maps:
|
|
543
|
+
obj["dataflowMaps"] = [_write_dataflow_map(dfm) for dfm in ss.dataflow_maps]
|
|
544
|
+
return obj
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def _write_concept_scheme_map(csm: ConceptSchemeMap) -> dict[str, Any]:
|
|
548
|
+
obj: dict[str, Any] = {
|
|
549
|
+
"id": csm.id,
|
|
550
|
+
"name": _istring_dict(csm.name),
|
|
551
|
+
}
|
|
552
|
+
if csm.description:
|
|
553
|
+
obj["description"] = _istring_dict(csm.description)
|
|
554
|
+
if csm.source is not None:
|
|
555
|
+
obj["source"] = str(csm.source.urn)
|
|
556
|
+
if csm.target is not None:
|
|
557
|
+
obj["target"] = str(csm.target.urn)
|
|
558
|
+
if csm.maps:
|
|
559
|
+
obj["conceptMaps"] = [{"sourceId": m.source_id, "targetId": m.target_id} for m in csm.maps]
|
|
560
|
+
return obj
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _write_category_scheme_map(catm: CategorySchemeMap) -> dict[str, Any]:
|
|
564
|
+
obj: dict[str, Any] = {
|
|
565
|
+
"id": catm.id,
|
|
566
|
+
"name": _istring_dict(catm.name),
|
|
567
|
+
}
|
|
568
|
+
if catm.description:
|
|
569
|
+
obj["description"] = _istring_dict(catm.description)
|
|
570
|
+
if catm.source is not None:
|
|
571
|
+
obj["source"] = str(catm.source.urn)
|
|
572
|
+
if catm.target is not None:
|
|
573
|
+
obj["target"] = str(catm.target.urn)
|
|
574
|
+
if catm.maps:
|
|
575
|
+
obj["categoryMaps"] = [{"sourceId": m.source_id, "targetId": m.target_id} for m in catm.maps]
|
|
576
|
+
return obj
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def _write_data_structure_map(dsm: DataStructureMap) -> dict[str, Any]:
|
|
580
|
+
obj: dict[str, Any] = {
|
|
581
|
+
"id": dsm.id,
|
|
582
|
+
"name": _istring_dict(dsm.name),
|
|
583
|
+
}
|
|
584
|
+
if dsm.description:
|
|
585
|
+
obj["description"] = _istring_dict(dsm.description)
|
|
586
|
+
if dsm.source is not None:
|
|
587
|
+
obj["source"] = str(dsm.source.urn)
|
|
588
|
+
if dsm.target is not None:
|
|
589
|
+
obj["target"] = str(dsm.target.urn)
|
|
590
|
+
if dsm.maps:
|
|
591
|
+
obj["componentMaps"] = [{"sourceId": m.source_id, "targetId": m.target_id} for m in dsm.maps]
|
|
592
|
+
return obj
|
|
593
|
+
|
|
594
|
+
|
|
595
|
+
def _write_dataflow_map(dfm: DataflowMap) -> dict[str, Any]:
|
|
596
|
+
obj: dict[str, Any] = {
|
|
597
|
+
"id": dfm.id,
|
|
598
|
+
"name": _istring_dict(dfm.name),
|
|
599
|
+
}
|
|
600
|
+
if dfm.description:
|
|
601
|
+
obj["description"] = _istring_dict(dfm.description)
|
|
602
|
+
if dfm.source is not None:
|
|
603
|
+
obj["source"] = str(dfm.source.urn)
|
|
604
|
+
if dfm.target is not None:
|
|
605
|
+
obj["target"] = str(dfm.target.urn)
|
|
606
|
+
return obj
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
# ── Hierarchies ──────────────────────────────────────────────────────────────
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def _write_hierarchical_code(hc: HierarchicalCode) -> dict[str, Any]:
|
|
613
|
+
"""Recursively serialise a HierarchicalCode to a JSON dict."""
|
|
614
|
+
obj: dict[str, Any] = {"id": hc.id}
|
|
615
|
+
if hc.code is not None:
|
|
616
|
+
code_ref = hc.code
|
|
617
|
+
obj["code"] = str(code_ref.urn if isinstance(code_ref, Ref) else code_ref)
|
|
618
|
+
if hc.name:
|
|
619
|
+
obj["name"] = _istring_dict(hc.name)
|
|
620
|
+
if hc.description:
|
|
621
|
+
obj["description"] = _istring_dict(hc.description)
|
|
622
|
+
if hc.children:
|
|
623
|
+
obj["hierarchicalCodes"] = [_write_hierarchical_code(child) for child in hc.children]
|
|
624
|
+
return obj
|
|
625
|
+
|
|
626
|
+
|
|
627
|
+
def _write_hierarchy(h: Hierarchy) -> dict[str, Any]:
|
|
628
|
+
obj: dict[str, Any] = {
|
|
629
|
+
"id": h.id,
|
|
630
|
+
"agencyID": h.agency_id,
|
|
631
|
+
"version": h.version,
|
|
632
|
+
}
|
|
633
|
+
if h.name:
|
|
634
|
+
obj["name"] = _istring_dict(h.name)
|
|
635
|
+
if h.description:
|
|
636
|
+
obj["description"] = _istring_dict(h.description)
|
|
637
|
+
if h.is_final:
|
|
638
|
+
obj["isFinal"] = True
|
|
639
|
+
if h.valid_from is not None:
|
|
640
|
+
obj["validFrom"] = h.valid_from.isoformat()
|
|
641
|
+
if h.valid_to is not None:
|
|
642
|
+
obj["validTo"] = h.valid_to.isoformat()
|
|
643
|
+
if h.tree:
|
|
644
|
+
obj["hierarchicalCodes"] = [_write_hierarchical_code(hc) for hc in h.tree]
|
|
645
|
+
return obj
|
|
646
|
+
|
|
647
|
+
|
|
648
|
+
# ── Entry point ───────────────────────────────────────────────────────────────
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
def write(msg: StructureMessage, *, pretty_print: bool = False) -> bytes: # noqa: C901
|
|
652
|
+
"""Serialise a StructureMessage to SDMX-JSON 2.0 bytes.
|
|
653
|
+
|
|
654
|
+
Args:
|
|
655
|
+
msg: The StructureMessage to serialise.
|
|
656
|
+
pretty_print: If True, indent the output with 2-space indentation.
|
|
657
|
+
|
|
658
|
+
Returns:
|
|
659
|
+
UTF-8 encoded JSON bytes of a valid SDMX-JSON 2.0 Structure message.
|
|
660
|
+
"""
|
|
661
|
+
data: dict[str, Any] = {}
|
|
662
|
+
|
|
663
|
+
if msg.agency_schemes:
|
|
664
|
+
data["agencySchemes"] = [_write_agency_scheme(s) for s in msg.agency_schemes]
|
|
665
|
+
if msg.data_provider_schemes:
|
|
666
|
+
data["dataProviderSchemes"] = [_write_data_provider_scheme(s) for s in msg.data_provider_schemes]
|
|
667
|
+
if msg.codelists:
|
|
668
|
+
data["codelists"] = [_write_codelist(cl) for cl in msg.codelists]
|
|
669
|
+
if msg.concept_schemes:
|
|
670
|
+
data["conceptSchemes"] = [_write_concept_scheme(cs) for cs in msg.concept_schemes]
|
|
671
|
+
if msg.category_schemes:
|
|
672
|
+
data["categorySchemes"] = [_write_category_scheme(cs) for cs in msg.category_schemes]
|
|
673
|
+
if msg.data_structures:
|
|
674
|
+
data["dataStructures"] = [_write_data_structure(dsd) for dsd in msg.data_structures]
|
|
675
|
+
if msg.dataflows:
|
|
676
|
+
data["dataflows"] = [_write_dataflow(df) for df in msg.dataflows]
|
|
677
|
+
if msg.categorisations:
|
|
678
|
+
data["categorisations"] = [_write_categorisation(cat) for cat in msg.categorisations]
|
|
679
|
+
if msg.provision_agreements:
|
|
680
|
+
data["provisionAgreements"] = [_write_provision_agreement(pa) for pa in msg.provision_agreements]
|
|
681
|
+
if msg.constraints:
|
|
682
|
+
data["dataConstraints"] = [_write_content_constraint(cc) for cc in msg.constraints]
|
|
683
|
+
if msg.structure_sets:
|
|
684
|
+
data["structureSets"] = [_write_structure_set(ss) for ss in msg.structure_sets]
|
|
685
|
+
if msg.hierarchies:
|
|
686
|
+
data["hierarchies"] = [_write_hierarchy(h) for h in msg.hierarchies]
|
|
687
|
+
|
|
688
|
+
doc: dict[str, Any] = {
|
|
689
|
+
"meta": {
|
|
690
|
+
"id": "SDMXLIB",
|
|
691
|
+
"prepared": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%S"),
|
|
692
|
+
"sender": {"id": "SDMXLIB"},
|
|
693
|
+
},
|
|
694
|
+
"data": data,
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
indent = 2 if pretty_print else None
|
|
698
|
+
return json.dumps(doc, ensure_ascii=False, indent=indent).encode("utf-8")
|