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,709 @@
1
+ """SDMX-ML 2.1 writer: domain model → lxml → bytes.
2
+
3
+ Serialises a StructureMessage (or any subset of its artefacts) to a
4
+ valid SDMX-ML 2.1 Structure message.
5
+
6
+ Example:
7
+ from sdmxlib.formats.sdmx_ml21.writer import write
8
+
9
+ xml_bytes = write(msg)
10
+ """
11
+
12
+ from typing import Any
13
+
14
+ from lxml import etree
15
+
16
+ from sdmxlib.model.category import Categorisation, Category, CategoryScheme
17
+ from sdmxlib.model.codelist import Code, Codelist
18
+ from sdmxlib.model.concept import Concept, 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, 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
+ from sdmxlib.model.urn import SdmxUrn
46
+
47
+ from .namespaces import COM, MES, STR
48
+
49
+ # ── Namespace helpers ─────────────────────────────────────────────────────────
50
+
51
+ _NSMAP: dict[Any, str] = {
52
+ "mes": MES,
53
+ "str": STR,
54
+ "com": COM,
55
+ "xsi": "http://www.w3.org/2001/XMLSchema-instance",
56
+ }
57
+
58
+ _XML_LANG = "{http://www.w3.org/XML/1998/namespace}lang"
59
+
60
+
61
+ def _m(tag: str) -> str:
62
+ return f"{{{MES}}}{tag}"
63
+
64
+
65
+ def _s(tag: str) -> str:
66
+ return f"{{{STR}}}{tag}"
67
+
68
+
69
+ def _c(tag: str) -> str:
70
+ return f"{{{COM}}}{tag}"
71
+
72
+
73
+ # ── Common writers ────────────────────────────────────────────────────────────
74
+
75
+
76
+ def _write_istring(
77
+ parent: etree._Element, istring: InternationalString | dict[str, str], child_tag: str = "Name"
78
+ ) -> None:
79
+ """Append com:Name (or com:Description) children for each language."""
80
+ for lang in istring:
81
+ el = etree.SubElement(parent, _c(child_tag))
82
+ el.set(_XML_LANG, lang)
83
+ el.text = istring[lang] # type: ignore[index]
84
+
85
+
86
+ def _write_maintainable_attrs(el: etree._Element, obj: Any) -> None:
87
+ """Set id, agencyID, version, isFinal on a maintainable element."""
88
+ el.set("id", obj.id)
89
+ el.set("agencyID", obj.agency_id)
90
+ el.set("version", obj.version)
91
+ if getattr(obj, "is_final", False):
92
+ el.set("isFinal", "true")
93
+ if getattr(obj, "valid_from", None) is not None:
94
+ el.set("validFrom", obj.valid_from.isoformat())
95
+ if getattr(obj, "valid_to", None) is not None:
96
+ el.set("validTo", obj.valid_to.isoformat())
97
+
98
+
99
+ def _urn_to_ref_el(urn: SdmxUrn[Any]) -> etree._Element:
100
+ """Build a <Ref> element from an SdmxUrn."""
101
+ ref = etree.Element("Ref")
102
+ ref.set("agencyID", urn.agency)
103
+ ref.set("id", urn.id)
104
+ if urn.version != "latest":
105
+ ref.set("version", urn.version)
106
+ if urn.item_id is not None:
107
+ ref.set("maintainableParentID", urn.id)
108
+ ref.set("id", urn.item_id)
109
+ ref.set("maintainableParentVersion", urn.version if urn.version != "latest" else "1.0")
110
+ ref.set("class", urn.artefact_class)
111
+ ref.set("package", urn.package)
112
+ return ref
113
+
114
+
115
+ def _write_ref(parent: etree._Element, ref: AnyRef) -> None:
116
+ """Append a <Ref> child carrying the reference URN."""
117
+ parent.append(_urn_to_ref_el(ref.urn))
118
+
119
+
120
+ def _write_text_format(parent: etree._Element, tf: Facet) -> None: # noqa: C901
121
+ """Append a str:TextFormat element."""
122
+ el = etree.SubElement(parent, _s("TextFormat"))
123
+ if tf.type is not None:
124
+ el.set("textType", tf.type)
125
+ if tf.max_length is not None:
126
+ el.set("maxLength", str(tf.max_length))
127
+ if tf.min_length is not None:
128
+ el.set("minLength", str(tf.min_length))
129
+ if tf.max_value is not None:
130
+ el.set("maxValue", str(tf.max_value))
131
+ if tf.min_value is not None:
132
+ el.set("minValue", str(tf.min_value))
133
+ if tf.decimals is not None:
134
+ el.set("decimals", str(tf.decimals))
135
+ if tf.pattern is not None:
136
+ el.set("pattern", tf.pattern)
137
+ if tf.start_value is not None:
138
+ el.set("startValue", str(tf.start_value))
139
+ if tf.end_value is not None:
140
+ el.set("endValue", str(tf.end_value))
141
+ if tf.is_sequence is not None:
142
+ el.set("isSequence", "true" if tf.is_sequence else "false")
143
+ if tf.is_multi_lingual is not None:
144
+ el.set("isMultiLingual", "true" if tf.is_multi_lingual else "false")
145
+
146
+
147
+ def _write_representation(
148
+ parent: etree._Element,
149
+ representation: AnyRef | Facet | None,
150
+ tag: str = "LocalRepresentation",
151
+ ) -> None:
152
+ """Append a str:LocalRepresentation (or str:CoreRepresentation) element."""
153
+ if representation is None:
154
+ return
155
+ rep_el = etree.SubElement(parent, _s(tag))
156
+ match representation:
157
+ case Facet():
158
+ _write_text_format(rep_el, representation)
159
+ case Ref():
160
+ enum_el = etree.SubElement(rep_el, _s("Enumeration"))
161
+ enum_el.append(_urn_to_ref_el(representation.urn))
162
+
163
+
164
+ def _write_concept_identity(parent: etree._Element, ref: AnyRef) -> None:
165
+ """Append a str:ConceptIdentity with a <Ref> child."""
166
+ ci = etree.SubElement(parent, _s("ConceptIdentity"))
167
+ _write_ref(ci, ref)
168
+
169
+
170
+ # ── Codelists ─────────────────────────────────────────────────────────────────
171
+
172
+
173
+ def _write_code(parent: etree._Element, code: Code) -> None:
174
+ el = etree.SubElement(parent, _s("Code"))
175
+ el.set("id", code.id)
176
+ _write_istring(el, code.name)
177
+ if code.description:
178
+ _write_istring(el, code.description, "Description")
179
+ if code.parent_id is not None:
180
+ parent_el = etree.SubElement(el, _s("Parent"))
181
+ ref = etree.SubElement(parent_el, "Ref")
182
+ ref.set("id", code.parent_id)
183
+
184
+
185
+ def _write_agency(parent: etree._Element, agency: Agency) -> None:
186
+ el = etree.SubElement(parent, _s("Agency"))
187
+ el.set("id", agency.id)
188
+ if agency.name:
189
+ _write_istring(el, agency.name)
190
+ if agency.description:
191
+ _write_istring(el, agency.description, "Description")
192
+
193
+
194
+ def _write_agency_scheme(parent: etree._Element, scheme: AgencyScheme) -> None:
195
+ el = etree.SubElement(parent, _s("AgencyScheme"))
196
+ _write_maintainable_attrs(el, scheme)
197
+ if scheme.name:
198
+ _write_istring(el, scheme.name)
199
+ if scheme.description:
200
+ _write_istring(el, scheme.description, "Description")
201
+ for agency in scheme.agencies:
202
+ _write_agency(el, agency)
203
+
204
+
205
+ def _write_data_provider(parent: etree._Element, provider: DataProvider) -> None:
206
+ el = etree.SubElement(parent, _s("DataProvider"))
207
+ el.set("id", provider.id)
208
+ if provider.name:
209
+ _write_istring(el, provider.name)
210
+ if provider.description:
211
+ _write_istring(el, provider.description, "Description")
212
+
213
+
214
+ def _write_data_provider_scheme(parent: etree._Element, scheme: DataProviderScheme) -> None:
215
+ el = etree.SubElement(parent, _s("DataProviderScheme"))
216
+ _write_maintainable_attrs(el, scheme)
217
+ if scheme.name:
218
+ _write_istring(el, scheme.name)
219
+ if scheme.description:
220
+ _write_istring(el, scheme.description, "Description")
221
+ for provider in scheme.providers:
222
+ _write_data_provider(el, provider)
223
+
224
+
225
+ def _write_codelist(parent: etree._Element, cl: Codelist) -> None:
226
+ el = etree.SubElement(parent, _s("Codelist"))
227
+ _write_maintainable_attrs(el, cl)
228
+ _write_istring(el, cl.name)
229
+ if cl.description:
230
+ _write_istring(el, cl.description, "Description")
231
+ for code in cl.codes:
232
+ _write_code(el, code)
233
+
234
+
235
+ # ── ConceptSchemes ────────────────────────────────────────────────────────────
236
+
237
+
238
+ def _write_concept(parent: etree._Element, concept: Concept) -> None:
239
+ el = etree.SubElement(parent, _s("Concept"))
240
+ el.set("id", concept.id)
241
+ _write_istring(el, concept.name)
242
+ if concept.description:
243
+ _write_istring(el, concept.description, "Description")
244
+ if concept.core_representation is not None:
245
+ _write_representation(el, concept.core_representation, tag="CoreRepresentation")
246
+
247
+
248
+ def _write_concept_scheme(parent: etree._Element, cs: ConceptScheme) -> None:
249
+ el = etree.SubElement(parent, _s("ConceptScheme"))
250
+ _write_maintainable_attrs(el, cs)
251
+ _write_istring(el, cs.name)
252
+ if cs.description:
253
+ _write_istring(el, cs.description, "Description")
254
+ for concept in cs.concepts:
255
+ _write_concept(el, concept)
256
+
257
+
258
+ # ── DataStructures ────────────────────────────────────────────────────────────
259
+
260
+
261
+ def _write_dimension(parent: etree._Element, dim: Dimension, position: int) -> None:
262
+ el = etree.SubElement(parent, _s("Dimension"))
263
+ el.set("id", dim.id)
264
+ el.set("position", str(position))
265
+ if isinstance(dim.concept, Ref):
266
+ _write_concept_identity(el, dim.concept)
267
+ if isinstance(dim.representation, (Ref, Facet)):
268
+ _write_representation(el, dim.representation)
269
+
270
+
271
+ def _write_time_dimension(parent: etree._Element, td: TimeDimension) -> None:
272
+ el = etree.SubElement(parent, _s("TimeDimension"))
273
+ el.set("id", td.id)
274
+ if isinstance(td.concept, Ref):
275
+ _write_concept_identity(el, td.concept)
276
+ if td.representation is not None:
277
+ local_rep = etree.SubElement(el, _s("LocalRepresentation"))
278
+ _write_text_format(local_rep, td.representation)
279
+
280
+
281
+ def _write_attribute_relationship(parent: etree._Element, attr: Attribute) -> None:
282
+ ar = etree.SubElement(parent, _s("AttributeRelationship"))
283
+ match attr.attachment:
284
+ case AttachmentLevel.OBSERVATION:
285
+ pm = etree.SubElement(ar, _s("PrimaryMeasure"))
286
+ ref = etree.SubElement(pm, "Ref")
287
+ ref.set("id", attr.related_measure or "OBS_VALUE")
288
+ case AttachmentLevel.SERIES:
289
+ for dim_id in attr.related_dimensions or []:
290
+ dim_el = etree.SubElement(ar, _s("Dimension"))
291
+ ref = etree.SubElement(dim_el, "Ref")
292
+ ref.set("id", dim_id)
293
+ case AttachmentLevel.GROUP:
294
+ etree.SubElement(ar, _s("Group"))
295
+ case _:
296
+ etree.SubElement(ar, _s("None"))
297
+
298
+
299
+ def _write_attribute(parent: etree._Element, attr: Attribute) -> None:
300
+ el = etree.SubElement(parent, _s("Attribute"))
301
+ el.set("id", attr.id)
302
+ if attr.usage is not None:
303
+ el.set("assignmentStatus", attr.usage)
304
+ if isinstance(attr.concept, Ref):
305
+ _write_concept_identity(el, attr.concept)
306
+ if isinstance(attr.representation, (Ref, Facet)):
307
+ _write_representation(el, attr.representation)
308
+ _write_attribute_relationship(el, attr)
309
+
310
+
311
+ def _write_measure(parent: etree._Element, measure: Measure) -> None:
312
+ el = etree.SubElement(parent, _s("PrimaryMeasure"))
313
+ el.set("id", measure.id)
314
+ if isinstance(measure.concept, Ref):
315
+ _write_concept_identity(el, measure.concept)
316
+ if isinstance(measure.representation, (Ref, Facet)):
317
+ _write_representation(el, measure.representation)
318
+
319
+
320
+ def _write_group(parent: etree._Element, group: Group) -> None:
321
+ el = etree.SubElement(parent, _s("Group"))
322
+ el.set("id", group.id)
323
+ for dim_id in group.dimension_ids:
324
+ gd = etree.SubElement(el, _s("GroupDimension"))
325
+ dr = etree.SubElement(gd, _s("DimensionReference"))
326
+ ref = etree.SubElement(dr, "Ref")
327
+ ref.set("id", dim_id)
328
+
329
+
330
+ def _write_data_structure(parent: etree._Element, dsd: DataStructure) -> None:
331
+ el = etree.SubElement(parent, _s("DataStructure"))
332
+ _write_maintainable_attrs(el, dsd)
333
+ _write_istring(el, dsd.name)
334
+ if dsd.description:
335
+ _write_istring(el, dsd.description, "Description")
336
+
337
+ components = etree.SubElement(el, _s("DataStructureComponents"))
338
+
339
+ dim_list = etree.SubElement(components, _s("DimensionList"))
340
+ dim_list.set("id", "DimensionDescriptor")
341
+ for i, dim in enumerate(dsd.dimensions, start=1):
342
+ _write_dimension(dim_list, dim, i)
343
+ if dsd.time_dimension is not None:
344
+ _write_time_dimension(dim_list, dsd.time_dimension)
345
+
346
+ if dsd.groups:
347
+ for group in dsd.groups:
348
+ _write_group(components, group)
349
+
350
+ if dsd.attributes:
351
+ attr_list = etree.SubElement(components, _s("AttributeList"))
352
+ attr_list.set("id", "AttributeDescriptor")
353
+ for attr in dsd.attributes:
354
+ _write_attribute(attr_list, attr)
355
+
356
+ if dsd.measures:
357
+ measure_list = etree.SubElement(components, _s("MeasureList"))
358
+ measure_list.set("id", "MeasureDescriptor")
359
+ for measure in dsd.measures:
360
+ _write_measure(measure_list, measure)
361
+
362
+
363
+ # ── Dataflows ─────────────────────────────────────────────────────────────────
364
+
365
+
366
+ def _write_dataflow(parent: etree._Element, df: Dataflow) -> None:
367
+ el = etree.SubElement(parent, _s("Dataflow"))
368
+ _write_maintainable_attrs(el, df)
369
+ _write_istring(el, df.name)
370
+ if df.description:
371
+ _write_istring(el, df.description, "Description")
372
+ if isinstance(df.structure, Ref):
373
+ struct_el = etree.SubElement(el, _s("Structure"))
374
+ _write_ref(struct_el, df.structure)
375
+
376
+
377
+ # ── CategorySchemes & Categorisations ─────────────────────────────────────────
378
+
379
+
380
+ def _write_category(parent: etree._Element, cat: Category) -> None:
381
+ el = etree.SubElement(parent, _s("Category"))
382
+ el.set("id", cat.id)
383
+ _write_istring(el, cat.name)
384
+ if cat.description:
385
+ _write_istring(el, cat.description, "Description")
386
+ for child in cat.categories:
387
+ _write_category(el, child)
388
+
389
+
390
+ def _write_category_scheme(parent: etree._Element, cs: CategoryScheme) -> None:
391
+ el = etree.SubElement(parent, _s("CategoryScheme"))
392
+ _write_maintainable_attrs(el, cs)
393
+ _write_istring(el, cs.name)
394
+ if cs.description:
395
+ _write_istring(el, cs.description, "Description")
396
+ for cat in cs.categories:
397
+ _write_category(el, cat)
398
+
399
+
400
+ def _write_categorisation(parent: etree._Element, cat: Categorisation) -> None:
401
+ el = etree.SubElement(parent, _s("Categorisation"))
402
+ _write_maintainable_attrs(el, cat)
403
+ _write_istring(el, cat.name)
404
+ if cat.description:
405
+ _write_istring(el, cat.description, "Description")
406
+ if cat.source is not None:
407
+ source_el = etree.SubElement(el, _s("Source"))
408
+ _write_ref(source_el, cat.source)
409
+ if cat.target is not None:
410
+ target_el = etree.SubElement(el, _s("Target"))
411
+ _write_ref(target_el, cat.target)
412
+
413
+
414
+ # ── ProvisionAgreements ───────────────────────────────────────────────────────
415
+
416
+
417
+ def _write_provision_agreement(parent: etree._Element, pa: ProvisionAgreement) -> None:
418
+ el = etree.SubElement(parent, _s("ProvisionAgreement"))
419
+ _write_maintainable_attrs(el, pa)
420
+ _write_istring(el, pa.name)
421
+ if pa.description:
422
+ _write_istring(el, pa.description, "Description")
423
+ if pa.dataflow is not None:
424
+ su_el = etree.SubElement(el, _s("StructureUsage"))
425
+ _write_ref(su_el, pa.dataflow)
426
+ if pa.data_provider is not None:
427
+ dp_el = etree.SubElement(el, _s("DataProvider"))
428
+ _write_ref(dp_el, pa.data_provider)
429
+
430
+
431
+ # ── Constraints ───────────────────────────────────────────────────────────────
432
+
433
+
434
+ def _write_cube_region(parent: etree._Element, region: CubeRegion) -> None:
435
+ el = etree.SubElement(parent, _s("CubeRegion"))
436
+ el.set("include", "true" if region.include else "false")
437
+ for kv in region.keys:
438
+ kv_el = etree.SubElement(el, _c("KeyValue"))
439
+ kv_el.set("id", kv.dimension_id)
440
+ for value in sorted(kv.values):
441
+ v_el = etree.SubElement(kv_el, _c("Value"))
442
+ v_el.text = value
443
+
444
+
445
+ def _write_constraint_attachment(parent: etree._Element, attachment: ConstraintAttachment) -> None:
446
+ att_el = etree.SubElement(parent, _s("ConstraintAttachment"))
447
+ if attachment.dataflow is not None:
448
+ df_el = etree.SubElement(att_el, _s("Dataflow"))
449
+ _write_ref(df_el, attachment.dataflow)
450
+ elif attachment.data_structure is not None:
451
+ ds_el = etree.SubElement(att_el, _s("DataStructure"))
452
+ _write_ref(ds_el, attachment.data_structure)
453
+ elif attachment.provision_agreement is not None:
454
+ pa_el = etree.SubElement(att_el, _s("ProvisionAgreement"))
455
+ _write_ref(pa_el, attachment.provision_agreement)
456
+
457
+
458
+ def _write_content_constraint(parent: etree._Element, cc: ContentConstraint) -> None:
459
+ el = etree.SubElement(parent, _s("ContentConstraint"))
460
+ _write_maintainable_attrs(el, cc)
461
+ el.set("type", cc.constraint_type)
462
+ _write_istring(el, cc.name)
463
+ if cc.description:
464
+ _write_istring(el, cc.description, "Description")
465
+ _write_constraint_attachment(el, cc.attachment)
466
+ for region in cc.regions:
467
+ _write_cube_region(el, region)
468
+ if cc.reference_period is not None:
469
+ rp_el = etree.SubElement(el, _s("ReferencePeriod"))
470
+ rp_el.set("startTime", cc.reference_period.start_period.isoformat())
471
+ rp_el.set("endTime", cc.reference_period.end_period.isoformat())
472
+ if cc.release_calendar is not None:
473
+ rc_el = etree.SubElement(el, _s("ReleaseCalendar"))
474
+ etree.SubElement(rc_el, _s("Periodicity")).text = cc.release_calendar.periodicity
475
+ etree.SubElement(rc_el, _s("Offset")).text = cc.release_calendar.offset
476
+ etree.SubElement(rc_el, _s("Tolerance")).text = cc.release_calendar.tolerance
477
+
478
+
479
+ # ── StructureSets ─────────────────────────────────────────────────────────────
480
+
481
+
482
+ def _write_codelist_map(parent: etree._Element, cm: CodelistMap) -> None:
483
+ el = etree.SubElement(parent, _s("CodelistMap"))
484
+ el.set("id", cm.id)
485
+ _write_istring(el, cm.name)
486
+ if cm.description:
487
+ _write_istring(el, cm.description, "Description")
488
+ if cm.source is not None:
489
+ src_el = etree.SubElement(el, _s("Source"))
490
+ _write_ref(src_el, cm.source)
491
+ if cm.target is not None:
492
+ tgt_el = etree.SubElement(el, _s("Target"))
493
+ _write_ref(tgt_el, cm.target)
494
+ for code_map in cm.maps:
495
+ m_el = etree.SubElement(el, _s("CodeMap"))
496
+ src_el = etree.SubElement(m_el, _s("Source"))
497
+ etree.SubElement(src_el, "Ref").set("id", code_map.source_id)
498
+ tgt_el = etree.SubElement(m_el, _s("Target"))
499
+ etree.SubElement(tgt_el, "Ref").set("id", code_map.target_id)
500
+
501
+
502
+ def _write_concept_scheme_map(parent: etree._Element, csm: ConceptSchemeMap) -> None:
503
+ el = etree.SubElement(parent, _s("ConceptSchemeMap"))
504
+ el.set("id", csm.id)
505
+ _write_istring(el, csm.name)
506
+ if csm.description:
507
+ _write_istring(el, csm.description, "Description")
508
+ if csm.source is not None:
509
+ src_el = etree.SubElement(el, _s("Source"))
510
+ _write_ref(src_el, csm.source)
511
+ if csm.target is not None:
512
+ tgt_el = etree.SubElement(el, _s("Target"))
513
+ _write_ref(tgt_el, csm.target)
514
+ for concept_map in csm.maps:
515
+ m_el = etree.SubElement(el, _s("ConceptMap"))
516
+ src_el = etree.SubElement(m_el, _s("Source"))
517
+ etree.SubElement(src_el, "Ref").set("id", concept_map.source_id)
518
+ tgt_el = etree.SubElement(m_el, _s("Target"))
519
+ etree.SubElement(tgt_el, "Ref").set("id", concept_map.target_id)
520
+
521
+
522
+ def _write_category_scheme_map(parent: etree._Element, csm: CategorySchemeMap) -> None:
523
+ el = etree.SubElement(parent, _s("CategorySchemeMap"))
524
+ el.set("id", csm.id)
525
+ _write_istring(el, csm.name)
526
+ if csm.description:
527
+ _write_istring(el, csm.description, "Description")
528
+ if csm.source is not None:
529
+ src_el = etree.SubElement(el, _s("Source"))
530
+ _write_ref(src_el, csm.source)
531
+ if csm.target is not None:
532
+ tgt_el = etree.SubElement(el, _s("Target"))
533
+ _write_ref(tgt_el, csm.target)
534
+ for category_map in csm.maps:
535
+ m_el = etree.SubElement(el, _s("CategoryMap"))
536
+ src_el = etree.SubElement(m_el, _s("Source"))
537
+ etree.SubElement(src_el, "Ref").set("id", category_map.source_id)
538
+ tgt_el = etree.SubElement(m_el, _s("Target"))
539
+ etree.SubElement(tgt_el, "Ref").set("id", category_map.target_id)
540
+
541
+
542
+ def _write_data_structure_map(parent: etree._Element, dsm: DataStructureMap) -> None:
543
+ el = etree.SubElement(parent, _s("DataStructureMap"))
544
+ el.set("id", dsm.id)
545
+ _write_istring(el, dsm.name)
546
+ if dsm.description:
547
+ _write_istring(el, dsm.description, "Description")
548
+ if dsm.source is not None:
549
+ src_el = etree.SubElement(el, _s("Source"))
550
+ _write_ref(src_el, dsm.source)
551
+ if dsm.target is not None:
552
+ tgt_el = etree.SubElement(el, _s("Target"))
553
+ _write_ref(tgt_el, dsm.target)
554
+ for comp_map in dsm.maps:
555
+ m_el = etree.SubElement(el, _s("ComponentMap"))
556
+ src_el = etree.SubElement(m_el, _s("Source"))
557
+ etree.SubElement(src_el, "Ref").set("id", comp_map.source_id)
558
+ tgt_el = etree.SubElement(m_el, _s("Target"))
559
+ etree.SubElement(tgt_el, "Ref").set("id", comp_map.target_id)
560
+
561
+
562
+ def _write_dataflow_map(parent: etree._Element, dfm: DataflowMap) -> None:
563
+ el = etree.SubElement(parent, _s("DataflowMap"))
564
+ el.set("id", dfm.id)
565
+ _write_istring(el, dfm.name)
566
+ if dfm.description:
567
+ _write_istring(el, dfm.description, "Description")
568
+ if dfm.source is not None:
569
+ src_el = etree.SubElement(el, _s("Source"))
570
+ _write_ref(src_el, dfm.source)
571
+ if dfm.target is not None:
572
+ tgt_el = etree.SubElement(el, _s("Target"))
573
+ _write_ref(tgt_el, dfm.target)
574
+
575
+
576
+ def _write_structure_set(parent: etree._Element, ss: StructureSet) -> None:
577
+ el = etree.SubElement(parent, _s("StructureSet"))
578
+ _write_maintainable_attrs(el, ss)
579
+ _write_istring(el, ss.name)
580
+ if ss.description:
581
+ _write_istring(el, ss.description, "Description")
582
+ for cm in ss.codelist_maps:
583
+ _write_codelist_map(el, cm)
584
+ for csm in ss.concept_scheme_maps:
585
+ _write_concept_scheme_map(el, csm)
586
+ for csm in ss.category_scheme_maps:
587
+ _write_category_scheme_map(el, csm)
588
+ for dsm in ss.data_structure_maps:
589
+ _write_data_structure_map(el, dsm)
590
+ for dfm in ss.dataflow_maps:
591
+ _write_dataflow_map(el, dfm)
592
+
593
+
594
+ # ── Hierarchies ───────────────────────────────────────────────────────────────
595
+
596
+
597
+ def _write_hierarchical_code(parent: etree._Element, hc: HierarchicalCode) -> None:
598
+ el = etree.SubElement(parent, _s("HierarchicalCode"))
599
+ el.set("id", hc.id)
600
+ _write_istring(el, hc.name)
601
+ if hc.description:
602
+ _write_istring(el, hc.description, "Description")
603
+ if hc.code is not None and isinstance(hc.code, Ref):
604
+ code_id_el = etree.SubElement(el, _s("CodeID"))
605
+ _write_ref(code_id_el, hc.code)
606
+ for child in hc.children:
607
+ _write_hierarchical_code(el, child)
608
+
609
+
610
+ def _write_hierarchy(parent: etree._Element, h: Hierarchy) -> None:
611
+ hcl_el = etree.SubElement(parent, _s("HierarchicalCodelist"))
612
+ _write_maintainable_attrs(hcl_el, h)
613
+ _write_istring(hcl_el, h.name)
614
+ if h.description:
615
+ _write_istring(hcl_el, h.description, "Description")
616
+ hier_el = etree.SubElement(hcl_el, _s("Hierarchy"))
617
+ hier_el.set("id", h.id)
618
+ _write_istring(hier_el, h.name)
619
+ if h.description:
620
+ _write_istring(hier_el, h.description, "Description")
621
+ for hc in h.tree:
622
+ _write_hierarchical_code(hier_el, hc)
623
+
624
+
625
+ # ── Entry point ───────────────────────────────────────────────────────────────
626
+
627
+
628
+ def write(msg: StructureMessage, *, pretty_print: bool = False) -> bytes: # noqa: C901, PLR0912
629
+ """Serialise a StructureMessage to SDMX-ML 2.1 XML bytes.
630
+
631
+ Example:
632
+ from sdmxlib.formats.sdmx_ml21.writer import write
633
+
634
+ xml_bytes = write(msg)
635
+ xml_bytes = write(msg, pretty_print=True) # indented output
636
+ """
637
+ root = etree.Element(_m("Structure"), nsmap=_NSMAP)
638
+ root.set(
639
+ "{http://www.w3.org/2001/XMLSchema-instance}schemaLocation",
640
+ f"{MES} SDMXMessage.xsd",
641
+ )
642
+
643
+ header = etree.SubElement(root, _m("Header"))
644
+ etree.SubElement(header, _m("ID")).text = "SDMX_MSG"
645
+ etree.SubElement(header, _m("Test")).text = "false"
646
+ etree.SubElement(header, _m("Prepared")).text = "2000-01-01T00:00:00"
647
+ sender = etree.SubElement(header, _m("Sender"))
648
+ sender.set("id", "sdmxlib")
649
+
650
+ structures = etree.SubElement(root, _m("Structures"))
651
+
652
+ if msg.agency_schemes or msg.data_provider_schemes:
653
+ container = etree.SubElement(structures, _s("OrganisationSchemes"))
654
+ for scheme in msg.agency_schemes:
655
+ _write_agency_scheme(container, scheme)
656
+ for scheme in msg.data_provider_schemes:
657
+ _write_data_provider_scheme(container, scheme)
658
+
659
+ if msg.codelists:
660
+ container = etree.SubElement(structures, _s("Codelists"))
661
+ for cl in msg.codelists:
662
+ _write_codelist(container, cl)
663
+
664
+ if msg.concept_schemes:
665
+ container = etree.SubElement(structures, _s("Concepts"))
666
+ for cs in msg.concept_schemes:
667
+ _write_concept_scheme(container, cs)
668
+
669
+ if msg.category_schemes:
670
+ container = etree.SubElement(structures, _s("CategorySchemes"))
671
+ for cs in msg.category_schemes:
672
+ _write_category_scheme(container, cs)
673
+
674
+ if msg.data_structures:
675
+ container = etree.SubElement(structures, _s("DataStructures"))
676
+ for dsd in msg.data_structures:
677
+ _write_data_structure(container, dsd)
678
+
679
+ if msg.dataflows:
680
+ container = etree.SubElement(structures, _s("Dataflows"))
681
+ for df in msg.dataflows:
682
+ _write_dataflow(container, df)
683
+
684
+ if msg.categorisations:
685
+ container = etree.SubElement(structures, _s("Categorisations"))
686
+ for cat in msg.categorisations:
687
+ _write_categorisation(container, cat)
688
+
689
+ if msg.provision_agreements:
690
+ container = etree.SubElement(structures, _s("ProvisionAgreements"))
691
+ for pa in msg.provision_agreements:
692
+ _write_provision_agreement(container, pa)
693
+
694
+ if msg.constraints:
695
+ container = etree.SubElement(structures, _s("Constraints"))
696
+ for cc in msg.constraints:
697
+ _write_content_constraint(container, cc)
698
+
699
+ if msg.structure_sets:
700
+ container = etree.SubElement(structures, _s("StructureSets"))
701
+ for ss in msg.structure_sets:
702
+ _write_structure_set(container, ss)
703
+
704
+ if msg.hierarchies:
705
+ container = etree.SubElement(structures, _s("HierarchicalCodelists"))
706
+ for h in msg.hierarchies:
707
+ _write_hierarchy(container, h)
708
+
709
+ return etree.tostring(root, xml_declaration=True, encoding="UTF-8", pretty_print=pretty_print)