pysdmx 1.3.0__py3-none-any.whl → 1.4.0rc1__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 (100) hide show
  1. pysdmx/__extras_check.py +3 -2
  2. pysdmx/__init__.py +1 -1
  3. pysdmx/api/fmr/__init__.py +4 -4
  4. pysdmx/api/gds/__init__.py +328 -0
  5. pysdmx/api/qb/gds.py +153 -0
  6. pysdmx/api/qb/service.py +91 -3
  7. pysdmx/api/qb/structure.py +1 -0
  8. pysdmx/api/qb/util.py +1 -0
  9. pysdmx/io/__init__.py +2 -1
  10. pysdmx/io/csv/sdmx10/reader/__init__.py +4 -2
  11. pysdmx/io/csv/sdmx10/writer/__init__.py +15 -2
  12. pysdmx/io/csv/sdmx20/reader/__init__.py +5 -2
  13. pysdmx/io/csv/sdmx20/writer/__init__.py +13 -2
  14. pysdmx/io/format.py +4 -0
  15. pysdmx/io/input_processor.py +12 -3
  16. pysdmx/io/json/fusion/messages/core.py +2 -0
  17. pysdmx/io/json/fusion/messages/report.py +13 -7
  18. pysdmx/io/json/gds/messages/__init__.py +35 -0
  19. pysdmx/io/json/gds/messages/agencies.py +41 -0
  20. pysdmx/io/json/gds/messages/catalog.py +79 -0
  21. pysdmx/io/json/gds/messages/sdmx_api.py +23 -0
  22. pysdmx/io/json/gds/messages/services.py +49 -0
  23. pysdmx/io/json/gds/messages/urn_resolver.py +43 -0
  24. pysdmx/io/json/gds/reader/__init__.py +12 -0
  25. pysdmx/io/json/sdmxjson2/messages/__init__.py +12 -4
  26. pysdmx/io/json/sdmxjson2/messages/agency.py +72 -0
  27. pysdmx/io/json/sdmxjson2/messages/category.py +22 -29
  28. pysdmx/io/json/sdmxjson2/messages/code.py +68 -64
  29. pysdmx/io/json/sdmxjson2/messages/concept.py +9 -18
  30. pysdmx/io/json/sdmxjson2/messages/constraint.py +2 -13
  31. pysdmx/io/json/sdmxjson2/messages/core.py +113 -21
  32. pysdmx/io/json/sdmxjson2/messages/dataflow.py +51 -21
  33. pysdmx/io/json/sdmxjson2/messages/dsd.py +110 -36
  34. pysdmx/io/json/sdmxjson2/messages/map.py +61 -49
  35. pysdmx/io/json/sdmxjson2/messages/pa.py +9 -17
  36. pysdmx/io/json/sdmxjson2/messages/provider.py +88 -0
  37. pysdmx/io/json/sdmxjson2/messages/report.py +84 -14
  38. pysdmx/io/json/sdmxjson2/messages/schema.py +14 -5
  39. pysdmx/io/json/sdmxjson2/messages/structure.py +105 -36
  40. pysdmx/io/json/sdmxjson2/messages/vtl.py +42 -96
  41. pysdmx/io/pd.py +2 -9
  42. pysdmx/io/reader.py +72 -27
  43. pysdmx/io/serde.py +11 -0
  44. pysdmx/io/writer.py +134 -0
  45. pysdmx/io/xml/{sdmx21/reader/__data_aux.py → __data_aux.py} +9 -2
  46. pysdmx/io/xml/{sdmx21/reader/__parse_xml.py → __parse_xml.py} +30 -6
  47. pysdmx/io/xml/__ss_aux_reader.py +96 -0
  48. pysdmx/io/xml/__structure_aux_reader.py +1174 -0
  49. pysdmx/io/xml/__structure_aux_writer.py +1233 -0
  50. pysdmx/io/xml/{sdmx21/__tokens.py → __tokens.py} +33 -1
  51. pysdmx/io/xml/{sdmx21/writer/__write_aux.py → __write_aux.py} +129 -37
  52. pysdmx/io/xml/{sdmx21/writer/__write_data_aux.py → __write_data_aux.py} +1 -1
  53. pysdmx/io/xml/__write_structure_specific_aux.py +254 -0
  54. pysdmx/io/xml/{sdmx21/reader/doc_validation.py → doc_validation.py} +10 -2
  55. pysdmx/io/xml/{sdmx21/reader/header.py → header.py} +11 -3
  56. pysdmx/io/xml/sdmx21/reader/error.py +2 -2
  57. pysdmx/io/xml/sdmx21/reader/generic.py +12 -8
  58. pysdmx/io/xml/sdmx21/reader/structure.py +5 -840
  59. pysdmx/io/xml/sdmx21/reader/structure_specific.py +13 -97
  60. pysdmx/io/xml/sdmx21/reader/submission.py +2 -2
  61. pysdmx/io/xml/sdmx21/writer/error.py +1 -1
  62. pysdmx/io/xml/sdmx21/writer/generic.py +13 -7
  63. pysdmx/io/xml/sdmx21/writer/structure.py +16 -828
  64. pysdmx/io/xml/sdmx21/writer/structure_specific.py +13 -238
  65. pysdmx/io/xml/sdmx30/__init__.py +1 -0
  66. pysdmx/io/xml/sdmx30/reader/__init__.py +1 -0
  67. pysdmx/io/xml/sdmx30/reader/structure.py +39 -0
  68. pysdmx/io/xml/sdmx30/reader/structure_specific.py +39 -0
  69. pysdmx/io/xml/sdmx30/writer/__init__.py +1 -0
  70. pysdmx/io/xml/sdmx30/writer/structure.py +67 -0
  71. pysdmx/io/xml/sdmx30/writer/structure_specific.py +108 -0
  72. pysdmx/model/__base.py +99 -34
  73. pysdmx/model/__init__.py +4 -0
  74. pysdmx/model/category.py +20 -0
  75. pysdmx/model/code.py +29 -8
  76. pysdmx/model/concept.py +52 -11
  77. pysdmx/model/dataflow.py +117 -33
  78. pysdmx/model/dataset.py +66 -14
  79. pysdmx/model/gds.py +161 -0
  80. pysdmx/model/map.py +51 -8
  81. pysdmx/model/message.py +235 -55
  82. pysdmx/model/metadata.py +79 -16
  83. pysdmx/model/submission.py +12 -7
  84. pysdmx/model/vtl.py +30 -13
  85. pysdmx/toolkit/__init__.py +1 -1
  86. pysdmx/toolkit/pd/__init__.py +85 -0
  87. pysdmx/toolkit/vtl/__init__.py +2 -1
  88. pysdmx/toolkit/vtl/_validations.py +1 -1
  89. pysdmx/toolkit/vtl/{generate_vtl_script.py → script_generation.py} +30 -4
  90. pysdmx/toolkit/vtl/validation.py +119 -0
  91. pysdmx/util/_model_utils.py +1 -1
  92. pysdmx-1.4.0rc1.dist-info/METADATA +119 -0
  93. pysdmx-1.4.0rc1.dist-info/RECORD +140 -0
  94. pysdmx/io/json/sdmxjson2/messages/org.py +0 -140
  95. pysdmx/toolkit/vtl/model_validations.py +0 -50
  96. pysdmx-1.3.0.dist-info/METADATA +0 -76
  97. pysdmx-1.3.0.dist-info/RECORD +0 -116
  98. /pysdmx/io/xml/{sdmx21/writer/config.py → config.py} +0 -0
  99. {pysdmx-1.3.0.dist-info → pysdmx-1.4.0rc1.dist-info}/LICENSE +0 -0
  100. {pysdmx-1.3.0.dist-info → pysdmx-1.4.0rc1.dist-info}/WHEEL +0 -0
@@ -1,258 +1,29 @@
1
1
  # mypy: disable-error-code="union-attr"
2
2
  """Module for writing SDMX-ML 2.1 Structure Specific data messages."""
3
3
 
4
- from typing import Any, Dict, List, Optional, Sequence
5
-
6
- import pandas as pd
4
+ from pathlib import Path
5
+ from typing import Dict, Optional, Sequence, Union
7
6
 
8
7
  from pysdmx.io.format import Format
9
8
  from pysdmx.io.pd import PandasDataset
10
- from pysdmx.io.xml.sdmx21.writer.__write_aux import (
11
- ABBR_MSG,
12
- ALL_DIM,
13
- __escape_xml,
9
+ from pysdmx.io.xml.__write_aux import (
14
10
  __write_header,
15
11
  create_namespaces,
16
12
  get_end_message,
17
- get_structure,
18
13
  )
19
- from pysdmx.io.xml.sdmx21.writer.__write_data_aux import (
14
+ from pysdmx.io.xml.__write_data_aux import (
20
15
  check_content_dataset,
21
16
  check_dimension_at_observation,
22
- get_codes,
23
- writing_validation,
24
17
  )
25
- from pysdmx.io.xml.sdmx21.writer.config import CHUNKSIZE
18
+ from pysdmx.io.xml.__write_structure_specific_aux import (
19
+ __write_data_structure_specific,
20
+ )
26
21
  from pysdmx.model.message import Header
27
- from pysdmx.util import parse_short_urn
28
-
29
-
30
- def __memory_optimization_writing(
31
- dataset: PandasDataset, prettyprint: bool
32
- ) -> str:
33
- """Memory optimization for writing data."""
34
- outfile = ""
35
- length_ = len(dataset.data)
36
- if len(dataset.data) > CHUNKSIZE:
37
- previous = 0
38
- next_ = CHUNKSIZE
39
- while previous <= length_:
40
- # Sliding a window for efficient access to the data
41
- # and avoid memory issues
42
- outfile += __obs_processing(
43
- dataset.data.iloc[previous:next_], prettyprint
44
- )
45
- previous = next_
46
- next_ += CHUNKSIZE
47
-
48
- if next_ >= length_:
49
- outfile += __obs_processing(
50
- dataset.data.iloc[previous:], prettyprint
51
- )
52
- previous = next_
53
- else:
54
- outfile += __obs_processing(dataset.data, prettyprint)
55
-
56
- return outfile
57
-
58
-
59
- def __write_data_structure_specific(
60
- datasets: Dict[str, PandasDataset],
61
- dim_mapping: Dict[str, str],
62
- prettyprint: bool = True,
63
- ) -> str:
64
- """Write data to SDMX-ML 2.1 Structure-Specific format.
65
-
66
- Args:
67
- datasets: dict. Datasets to be written.
68
- dim_mapping: dict. URN-DimensionAtObservation mapping.
69
- prettyprint: bool. Prettyprint or not.
70
-
71
- Returns:
72
- The data in SDMX-ML 2.1 Structure-Specific format, as string.
73
- """
74
- outfile = ""
75
-
76
- for i, (short_urn, dataset) in enumerate(datasets.items()):
77
- dataset.data = dataset.data.fillna("").astype(str)
78
- outfile += __write_data_single_dataset(
79
- dataset=dataset,
80
- prettyprint=prettyprint,
81
- count=i + 1,
82
- dim=dim_mapping[short_urn],
83
- )
84
-
85
- return outfile
86
-
87
-
88
- def __write_data_single_dataset(
89
- dataset: PandasDataset,
90
- prettyprint: bool = True,
91
- count: int = 1,
92
- dim: str = ALL_DIM,
93
- ) -> str:
94
- """Write data to SDMX-ML 2.1 Structure-Specific format.
95
-
96
- Args:
97
- dataset: PandasDataset. Dataset to be written.
98
- prettyprint: bool. Prettyprint or not.
99
- count: int. Count for namespace.
100
- dim: str. Dimension to be written.
101
-
102
- Returns:
103
- The data in SDMX-ML 2.1 Structure-Specific format, as string.
104
- """
105
-
106
- def __remove_optional_attributes_empty_data(str_to_check: str) -> str:
107
- """This function removes data when optional attributes are found."""
108
- for att in dataset.structure.components.attributes:
109
- if not att.required:
110
- str_to_check = str_to_check.replace(f"{att.id}='' ", "")
111
- str_to_check = str_to_check.replace(f'{att.id}="" ', "")
112
- return str_to_check
113
-
114
- outfile = ""
115
- structure_urn = get_structure(dataset)
116
- id_structure = parse_short_urn(structure_urn).id
117
- sdmx_type = parse_short_urn(structure_urn).id
118
-
119
- nl = "\n" if prettyprint else ""
120
- child1 = "\t" if prettyprint else ""
121
-
122
- attached_attributes_str = ""
123
- for k, v in dataset.attributes.items():
124
- attached_attributes_str += f"{k}={str(v)!r} "
125
-
126
- # Datasets
127
- outfile += (
128
- f"{nl}{child1}<{ABBR_MSG}:DataSet {attached_attributes_str}"
129
- f"ss:structureRef={id_structure!r} "
130
- f'xsi:type="ns{count}:DataSetType" '
131
- f'ss:dataScope="{sdmx_type}" '
132
- f'action="{dataset.action.value}">{nl}'
133
- )
134
- data = ""
135
- if dim == ALL_DIM:
136
- data += __memory_optimization_writing(dataset, prettyprint)
137
- else:
138
- writing_validation(dataset)
139
- series_codes, obs_codes = get_codes(
140
- dimension_code=dim,
141
- structure=dataset.structure, # type: ignore[arg-type]
142
- data=dataset.data,
143
- )
144
-
145
- data += __series_processing(
146
- data=dataset.data,
147
- series_codes=series_codes,
148
- obs_codes=obs_codes,
149
- prettyprint=prettyprint,
150
- )
151
-
152
- # Remove optional attributes empty data
153
- data = __remove_optional_attributes_empty_data(data)
154
-
155
- # Adding to outfile
156
- outfile += data
157
-
158
- outfile += f"{child1}</{ABBR_MSG}:DataSet>"
159
-
160
- return outfile.replace("'", '"')
161
-
162
-
163
- def __obs_processing(data: pd.DataFrame, prettyprint: bool = True) -> str:
164
- def __format_obs_str(element: Dict[str, Any]) -> str:
165
- """Formats the observation as key=value pairs."""
166
- nl = "\n" if prettyprint else ""
167
- child2 = "\t\t" if prettyprint else ""
168
-
169
- out = f"{child2}<Obs "
170
-
171
- for k, v in element.items():
172
- out += f"{k}={__escape_xml(str(v))!r} "
173
-
174
- out += f"/>{nl}"
175
-
176
- return out
177
-
178
- parser = lambda x: __format_obs_str(x) # noqa: E731
179
-
180
- iterator = map(parser, data.to_dict(orient="records"))
181
-
182
- return "".join(iterator)
183
-
184
-
185
- def __series_processing(
186
- data: pd.DataFrame,
187
- series_codes: List[str],
188
- obs_codes: List[str],
189
- prettyprint: bool = True,
190
- ) -> str:
191
- def __generate_series_str() -> str:
192
- """Generates the series item with its observations."""
193
- out_list: List[str] = []
194
- data.groupby(by=series_codes)[obs_codes].apply(
195
- lambda x: __format_dict_ser(out_list, x)
196
- )
197
-
198
- return "".join(out_list)
199
-
200
- def __format_dict_ser(
201
- output_list: List[str],
202
- obs: Any,
203
- ) -> Any:
204
- """Formats the series as key=value pairs."""
205
- # Creating the observation dict,
206
- # we always get the first element on Series
207
- # as we are grouping by it
208
- data_dict["Series"][0]["Obs"] = obs.to_dict(orient="records")
209
- output_list.append(__format_ser_str(data_dict["Series"][0]))
210
- # We remove the data for series as it is no longer necessary
211
- del data_dict["Series"][0]
212
-
213
- def __format_ser_str(data_info: Dict[Any, Any]) -> str:
214
- """Formats the series as key=value pairs."""
215
- child2 = "\t\t" if prettyprint else ""
216
- child3 = "\t\t\t" if prettyprint else ""
217
- nl = "\n" if prettyprint else ""
218
-
219
- out_element = f"{child2}<Series "
220
-
221
- for k, v in data_info.items():
222
- if k != "Obs":
223
- out_element += f"{k}={__escape_xml(str(v))!r} "
224
-
225
- out_element += f">{nl}"
226
-
227
- for obs in data_info["Obs"]:
228
- out_element += f"{child3}<Obs "
229
-
230
- for k, v in obs.items():
231
- out_element += f"{k}={__escape_xml(str(v))!r} "
232
-
233
- out_element += f"/>{nl}"
234
-
235
- out_element += f"{child2}</Series>{nl}"
236
-
237
- return out_element
238
-
239
- # Getting each datapoint from data and creating dict
240
- data = data.sort_values(series_codes, axis=0)
241
- data_dict = {
242
- "Series": data[series_codes]
243
- .drop_duplicates()
244
- .reset_index(drop=True)
245
- .to_dict(orient="records")
246
- }
247
-
248
- out = __generate_series_str()
249
-
250
- return out
251
22
 
252
23
 
253
24
  def write(
254
25
  datasets: Sequence[PandasDataset],
255
- output_path: str = "",
26
+ output_path: Optional[Union[str, Path]] = None,
256
27
  prettyprint: bool = True,
257
28
  header: Optional[Header] = None,
258
29
  dimension_at_observation: Optional[Dict[str, str]] = None,
@@ -309,7 +80,11 @@ def write(
309
80
 
310
81
  outfile += get_end_message(type_, prettyprint)
311
82
 
312
- if output_path == "":
83
+ output_path = (
84
+ str(output_path) if isinstance(output_path, Path) else output_path
85
+ )
86
+
87
+ if output_path is None or output_path == "":
313
88
  return outfile
314
89
 
315
90
  with open(output_path, "w", encoding="UTF-8", errors="replace") as f:
@@ -0,0 +1 @@
1
+ """SDMX 3.0 XML reader and writer."""
@@ -0,0 +1 @@
1
+ """SDMX 3.0 XML reader module."""
@@ -0,0 +1,39 @@
1
+ """Parsers for reading metadata."""
2
+
3
+ from typing import Sequence, Union
4
+
5
+ from pysdmx.errors import Invalid
6
+ from pysdmx.io.xml.__parse_xml import parse_xml
7
+ from pysdmx.io.xml.__structure_aux_reader import StructureParser
8
+ from pysdmx.io.xml.__tokens import (
9
+ STRUCTURE,
10
+ STRUCTURES,
11
+ )
12
+ from pysdmx.model.__base import (
13
+ ItemScheme,
14
+ )
15
+ from pysdmx.model.dataflow import (
16
+ Dataflow,
17
+ DataStructureDefinition,
18
+ )
19
+
20
+
21
+ def read(
22
+ input_str: str,
23
+ validate: bool = True,
24
+ ) -> Sequence[Union[ItemScheme, DataStructureDefinition, Dataflow]]:
25
+ """Reads an SDMX-ML 3.0 Structure data and returns the structures.
26
+
27
+ Args:
28
+ input_str: SDMX-ML structure message to read.
29
+ validate: If True, the XML data will be validated against the XSD.
30
+
31
+ Returns:
32
+ dict: Dictionary with the parsed structures.
33
+ """
34
+ dict_info = parse_xml(input_str, validate)
35
+ if STRUCTURE not in dict_info:
36
+ raise Invalid("This SDMX document is not SDMX-ML 3.0 Structure.")
37
+ return StructureParser(is_sdmx_30=True).format_structures(
38
+ dict_info[STRUCTURE][STRUCTURES]
39
+ )
@@ -0,0 +1,39 @@
1
+ """SDMX XML 3.0 StructureSpecificData reader module."""
2
+
3
+ from typing import Sequence
4
+
5
+ from pysdmx.errors import Invalid
6
+ from pysdmx.io.pd import PandasDataset
7
+ from pysdmx.io.xml.__data_aux import (
8
+ get_data_objects,
9
+ )
10
+ from pysdmx.io.xml.__parse_xml import parse_xml
11
+ from pysdmx.io.xml.__ss_aux_reader import (
12
+ _parse_structure_specific_data,
13
+ )
14
+ from pysdmx.io.xml.__tokens import (
15
+ STR_REF,
16
+ STR_SPE,
17
+ )
18
+
19
+
20
+ def read(input_str: str, validate: bool = True) -> Sequence[PandasDataset]:
21
+ """Reads an SDMX-ML 3.0 and returns a Sequence of Datasets.
22
+
23
+ Args:
24
+ input_str: SDMX-ML data to read.
25
+ validate: If True, the XML data will be validated against the XSD.
26
+ """
27
+ dict_info = parse_xml(input_str, validate=validate)
28
+ if STR_SPE not in dict_info:
29
+ raise Invalid(
30
+ "This SDMX document is not an SDMX-ML StructureSpecificData."
31
+ )
32
+ dataset_info, str_info = get_data_objects(dict_info[STR_SPE])
33
+ datasets = []
34
+ for dataset in dataset_info:
35
+ ds = _parse_structure_specific_data(
36
+ dataset, str_info[dataset[STR_REF]]
37
+ )
38
+ datasets.append(ds)
39
+ return datasets
@@ -0,0 +1 @@
1
+ """SDMX 3.0 writer package."""
@@ -0,0 +1,67 @@
1
+ """Module for writing metadata to XML files."""
2
+
3
+ from pathlib import Path
4
+ from typing import Dict, Optional, Sequence, Union
5
+
6
+ from pysdmx.io.format import Format
7
+ from pysdmx.io.xml.__structure_aux_writer import (
8
+ STR_DICT_TYPE_LIST_30,
9
+ __write_structures,
10
+ )
11
+ from pysdmx.io.xml.__write_aux import (
12
+ __write_header,
13
+ create_namespaces,
14
+ get_end_message,
15
+ )
16
+ from pysdmx.model.__base import MaintainableArtefact
17
+ from pysdmx.model.message import Header
18
+
19
+
20
+ def write(
21
+ structures: Sequence[MaintainableArtefact],
22
+ output_path: Optional[Union[str, Path]] = None,
23
+ prettyprint: bool = True,
24
+ header: Optional[Header] = None,
25
+ ) -> Optional[str]:
26
+ """This function writes a SDMX-ML file from the Message Content.
27
+
28
+ Args:
29
+ structures: The content to be written
30
+ output_path: The path to save the file
31
+ prettyprint: Prettyprint or not
32
+ header: The header to be used (generated if None)
33
+
34
+ Returns:
35
+ The XML string if output_path is empty, None otherwise
36
+ """
37
+ type_ = Format.STRUCTURE_SDMX_ML_3_0
38
+ elements = {structure.short_urn: structure for structure in structures}
39
+ if header is None:
40
+ header = Header()
41
+
42
+ content: Dict[str, Dict[str, MaintainableArtefact]] = {}
43
+ for urn, element in elements.items():
44
+ list_ = STR_DICT_TYPE_LIST_30[type(element)]
45
+ if list_ not in content:
46
+ content[list_] = {}
47
+ content[list_][urn] = element
48
+
49
+ # Generating the initial tag with namespaces
50
+ outfile = create_namespaces(type_, prettyprint=prettyprint)
51
+ # Generating the header
52
+ outfile += __write_header(header, prettyprint, data_message=False)
53
+ # Writing the content
54
+ outfile += __write_structures(content, prettyprint, references_30=True)
55
+
56
+ outfile += get_end_message(type_, prettyprint)
57
+
58
+ output_path = (
59
+ str(output_path) if isinstance(output_path, Path) else output_path
60
+ )
61
+
62
+ if output_path is None or output_path == "":
63
+ return outfile
64
+
65
+ with open(output_path, "w", encoding="UTF-8", errors="replace") as f:
66
+ f.write(outfile)
67
+ return None
@@ -0,0 +1,108 @@
1
+ # mypy: disable-error-code="union-attr"
2
+ """Module for writing SDMX-ML 3.0 Structure Specific data messages."""
3
+
4
+ from pathlib import Path
5
+ from typing import Dict, Optional, Sequence, Union
6
+
7
+ from pysdmx.io.format import Format
8
+ from pysdmx.io.pd import PandasDataset
9
+ from pysdmx.io.xml.__tokens import (
10
+ DSD_LOW,
11
+ PROV_AGREEMENT,
12
+ REGISTRY_LOW,
13
+ )
14
+ from pysdmx.io.xml.__write_aux import (
15
+ __write_header,
16
+ create_namespaces,
17
+ get_end_message,
18
+ )
19
+ from pysdmx.io.xml.__write_data_aux import (
20
+ check_content_dataset,
21
+ check_dimension_at_observation,
22
+ )
23
+ from pysdmx.io.xml.__write_structure_specific_aux import (
24
+ __write_data_structure_specific,
25
+ )
26
+ from pysdmx.model.message import Header
27
+ from pysdmx.util import parse_short_urn
28
+
29
+
30
+ def write(
31
+ datasets: Sequence[PandasDataset],
32
+ output_path: Optional[Union[str, Path]] = None,
33
+ prettyprint: bool = True,
34
+ header: Optional[Header] = None,
35
+ dimension_at_observation: Optional[Dict[str, str]] = None,
36
+ ) -> Optional[str]:
37
+ """Write data to SDMX-ML 3.0 Structure Specific format.
38
+
39
+ Args:
40
+ datasets: The datasets to be written.
41
+ output_path: The path to save the file.
42
+ prettyprint: Prettyprint or not.
43
+ header: The header to be used (generated if None).
44
+ dimension_at_observation:
45
+ The mapping between the dataset and the dimension at observation.
46
+
47
+ Returns:
48
+ The XML string if path is empty, None otherwise.
49
+ """
50
+ ss_namespaces = ""
51
+ type_ = Format.DATA_SDMX_ML_3_0
52
+
53
+ # Checking if we have datasets,
54
+ # we need to ensure we can write them correctly
55
+ check_content_dataset(datasets)
56
+ content = {dataset.short_urn: dataset for dataset in datasets}
57
+
58
+ if header is None:
59
+ header = Header()
60
+
61
+ # Checking the dimension at observation mapping
62
+ dim_mapping = check_dimension_at_observation(
63
+ content, dimension_at_observation
64
+ )
65
+ header.structure = dim_mapping
66
+ add_namespace_structure = True
67
+ for i, (short_urn, dimension) in enumerate(header.structure.items()):
68
+ reference = parse_short_urn(short_urn)
69
+ pre_urn = (
70
+ REGISTRY_LOW if reference.sdmx_type == PROV_AGREEMENT else DSD_LOW
71
+ )
72
+ ss_namespaces += (
73
+ f'xmlns:ns{i + 1}="urn:sdmx:org.sdmx'
74
+ f".infomodel.{pre_urn}.{short_urn}"
75
+ f':ObsLevelDim:{dimension}" '
76
+ )
77
+
78
+ # Generating the initial tag with namespaces
79
+ outfile = create_namespaces(type_, ss_namespaces, prettyprint)
80
+ # Generating the header
81
+ outfile += __write_header(
82
+ header,
83
+ prettyprint,
84
+ add_namespace_structure,
85
+ data_message=True,
86
+ references_30=True,
87
+ )
88
+ # Writing the content
89
+ outfile += __write_data_structure_specific(
90
+ datasets=content,
91
+ dim_mapping=dim_mapping,
92
+ prettyprint=prettyprint,
93
+ references_30=True,
94
+ )
95
+
96
+ outfile += get_end_message(type_, prettyprint)
97
+
98
+ output_path = (
99
+ str(output_path) if isinstance(output_path, Path) else output_path
100
+ )
101
+
102
+ if output_path is None or output_path == "":
103
+ return outfile
104
+
105
+ with open(output_path, "w", encoding="UTF-8", errors="replace") as f:
106
+ f.write(outfile)
107
+
108
+ return None