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,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)
|