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