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.
Files changed (58) hide show
  1. sdmxlib/__init__.py +155 -0
  2. sdmxlib/api/__init__.py +7 -0
  3. sdmxlib/api/client.py +246 -0
  4. sdmxlib/api/filters.py +67 -0
  5. sdmxlib/api/providers.py +66 -0
  6. sdmxlib/api/query.py +428 -0
  7. sdmxlib/api/registry.py +666 -0
  8. sdmxlib/api/session.py +124 -0
  9. sdmxlib/formats/__init__.py +166 -0
  10. sdmxlib/formats/sdmx_csv/__init__.py +5 -0
  11. sdmxlib/formats/sdmx_csv/reader.py +147 -0
  12. sdmxlib/formats/sdmx_json/__init__.py +1 -0
  13. sdmxlib/formats/sdmx_json/reader.py +897 -0
  14. sdmxlib/formats/sdmx_json/writer.py +698 -0
  15. sdmxlib/formats/sdmx_ml21/__init__.py +5 -0
  16. sdmxlib/formats/sdmx_ml21/namespaces.py +12 -0
  17. sdmxlib/formats/sdmx_ml21/reader.py +1147 -0
  18. sdmxlib/formats/sdmx_ml21/writer.py +709 -0
  19. sdmxlib/formats/sdmx_ml30/__init__.py +1 -0
  20. sdmxlib/formats/sdmx_ml30/namespaces.py +12 -0
  21. sdmxlib/formats/sdmx_ml30/reader.py +1138 -0
  22. sdmxlib/formats/sdmx_ml30/writer.py +730 -0
  23. sdmxlib/local/__init__.py +6 -0
  24. sdmxlib/local/_registry.py +368 -0
  25. sdmxlib/model/__init__.py +122 -0
  26. sdmxlib/model/annotations.py +103 -0
  27. sdmxlib/model/base.py +61 -0
  28. sdmxlib/model/category.py +88 -0
  29. sdmxlib/model/codelist.py +91 -0
  30. sdmxlib/model/collections.py +382 -0
  31. sdmxlib/model/concept.py +121 -0
  32. sdmxlib/model/constraint.py +92 -0
  33. sdmxlib/model/convert.py +268 -0
  34. sdmxlib/model/dataflow.py +49 -0
  35. sdmxlib/model/dataset.py +337 -0
  36. sdmxlib/model/datastructure.py +390 -0
  37. sdmxlib/model/expr.py +120 -0
  38. sdmxlib/model/hierarchy.py +111 -0
  39. sdmxlib/model/istring.py +81 -0
  40. sdmxlib/model/mapping.py +134 -0
  41. sdmxlib/model/message.py +70 -0
  42. sdmxlib/model/organisation.py +149 -0
  43. sdmxlib/model/provision.py +45 -0
  44. sdmxlib/model/ref.py +195 -0
  45. sdmxlib/model/registry.py +191 -0
  46. sdmxlib/model/representation.py +99 -0
  47. sdmxlib/model/urn.py +130 -0
  48. sdmxlib/model/validation.py +338 -0
  49. sdmxlib/polars.py +392 -0
  50. sdmxlib/py.typed +0 -0
  51. sdmxlib/storage/__init__.py +28 -0
  52. sdmxlib/storage/lazy.py +329 -0
  53. sdmxlib/storage/readers.py +310 -0
  54. sdmxlib/storage/schema.py +289 -0
  55. sdmxlib/storage/writers.py +503 -0
  56. sdmxlib-0.8.0.dist-info/METADATA +102 -0
  57. sdmxlib-0.8.0.dist-info/RECORD +58 -0
  58. 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")
@@ -0,0 +1,5 @@
1
+ """SDMX-ML 2.1 format reader."""
2
+
3
+ from sdmxlib.formats.sdmx_ml21.reader import read
4
+
5
+ __all__ = ["read"]