pysdmx 1.10.1__py3-none-any.whl → 1.11.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 (35) hide show
  1. pysdmx/__init__.py +1 -1
  2. pysdmx/api/fmr/maintenance.py +10 -5
  3. pysdmx/io/input_processor.py +4 -0
  4. pysdmx/io/json/fusion/messages/dsd.py +19 -14
  5. pysdmx/io/json/fusion/messages/msd.py +6 -9
  6. pysdmx/io/json/sdmxjson2/messages/core.py +12 -5
  7. pysdmx/io/json/sdmxjson2/messages/dsd.py +11 -17
  8. pysdmx/io/json/sdmxjson2/messages/msd.py +2 -5
  9. pysdmx/io/json/sdmxjson2/messages/report.py +7 -3
  10. pysdmx/io/json/sdmxjson2/messages/structure.py +7 -3
  11. pysdmx/io/json/sdmxjson2/reader/metadata.py +3 -3
  12. pysdmx/io/json/sdmxjson2/reader/structure.py +3 -3
  13. pysdmx/io/json/sdmxjson2/writer/_helper.py +118 -0
  14. pysdmx/io/json/sdmxjson2/writer/v2_0/__init__.py +1 -0
  15. pysdmx/io/json/sdmxjson2/writer/v2_0/metadata.py +33 -0
  16. pysdmx/io/json/sdmxjson2/writer/v2_0/structure.py +33 -0
  17. pysdmx/io/json/sdmxjson2/writer/v2_1/__init__.py +1 -0
  18. pysdmx/io/json/sdmxjson2/writer/v2_1/metadata.py +31 -0
  19. pysdmx/io/json/sdmxjson2/writer/v2_1/structure.py +33 -0
  20. pysdmx/io/reader.py +12 -3
  21. pysdmx/io/writer.py +13 -3
  22. pysdmx/io/xml/__ss_aux_reader.py +39 -17
  23. pysdmx/io/xml/__structure_aux_reader.py +221 -33
  24. pysdmx/io/xml/__structure_aux_writer.py +304 -5
  25. pysdmx/io/xml/__tokens.py +12 -0
  26. pysdmx/io/xml/__write_aux.py +9 -0
  27. pysdmx/io/xml/sdmx21/writer/generic.py +2 -2
  28. pysdmx/model/dataflow.py +2 -2
  29. pysdmx/toolkit/pd/_data_utils.py +1 -1
  30. {pysdmx-1.10.1.dist-info → pysdmx-1.11.0.dist-info}/METADATA +7 -1
  31. {pysdmx-1.10.1.dist-info → pysdmx-1.11.0.dist-info}/RECORD +33 -28
  32. {pysdmx-1.10.1.dist-info → pysdmx-1.11.0.dist-info}/WHEEL +1 -1
  33. pysdmx/io/json/sdmxjson2/writer/metadata.py +0 -60
  34. pysdmx/io/json/sdmxjson2/writer/structure.py +0 -61
  35. {pysdmx-1.10.1.dist-info → pysdmx-1.11.0.dist-info}/licenses/LICENSE +0 -0
pysdmx/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Your opinionated Python SDMX library."""
2
2
 
3
- __version__ = "1.10.1"
3
+ __version__ = "1.11.0"
@@ -64,9 +64,7 @@ class RegistryMaintenanceClient:
64
64
  timeout: The maximum number of seconds to wait before considering
65
65
  that a request timed out. Defaults to 10 seconds.
66
66
  """
67
- if api_endpoint.endswith("/"):
68
- api_endpoint = api_endpoint[0:-1]
69
- self._api_endpoint = f"{api_endpoint}"
67
+ self._api_endpoint = self.__sanitize_endpoint(api_endpoint)
70
68
  self._user = user
71
69
  self._password = password
72
70
  self._timeout = timeout
@@ -87,7 +85,6 @@ class RegistryMaintenanceClient:
87
85
  ) -> None:
88
86
  with httpx.Client(verify=self._ssl_context) as client:
89
87
  try:
90
- url = f"{endpoint}"
91
88
  auth = httpx.BasicAuth(self._user, self._password)
92
89
  headers = {
93
90
  "Content-Type": "application/text",
@@ -99,7 +96,7 @@ class RegistryMaintenanceClient:
99
96
  serializer = serializers.structure_message
100
97
  bodyjs = self._encoder.encode(serializer.from_model(message))
101
98
  r = client.post(
102
- url,
99
+ endpoint,
103
100
  headers=headers,
104
101
  content=bodyjs,
105
102
  timeout=self._timeout,
@@ -156,3 +153,11 @@ class RegistryMaintenanceClient:
156
153
  message = MetadataMessage(header=header, reports=reports)
157
154
  endpoint = f"{self._api_endpoint}/ws/secure/sdmx/v2/metadata"
158
155
  return self.__post(message, action, endpoint)
156
+
157
+ def __sanitize_endpoint(self, endpoint: str) -> str:
158
+ if endpoint.endswith("/"):
159
+ endpoint = endpoint[0:-1]
160
+ endpoint = endpoint.replace("/ws/secure/sdmx/v2/metadata", "")
161
+ endpoint = endpoint.replace("/sdmx/v2", "")
162
+ endpoint = endpoint.replace("/ws/secure/sdmxapi/rest", "")
163
+ return endpoint
@@ -97,6 +97,10 @@ def __get_sdmx_json_flavour(input_str: str) -> Tuple[str, Format]:
97
97
  return input_str, Format.STRUCTURE_SDMX_JSON_2_0_0
98
98
  elif "2.0.0/sdmx-json-metadata-schema.json" in flavour_check:
99
99
  return input_str, Format.REFMETA_SDMX_JSON_2_0_0
100
+ elif "2.1/sdmx-json-structure-schema.json" in flavour_check:
101
+ return input_str, Format.STRUCTURE_SDMX_JSON_2_1_0
102
+ elif "2.1/sdmx-json-metadata-schema.json" in flavour_check:
103
+ return input_str, Format.REFMETA_SDMX_JSON_2_1_0
100
104
  elif "sdmx-json" in flavour_check:
101
105
  raise NotImplemented(
102
106
  "Unsupported format", "This flavour of SDMX-JSON is not supported."
@@ -20,6 +20,7 @@ from pysdmx.model import (
20
20
  Codelist,
21
21
  Component,
22
22
  Components,
23
+ Concept,
23
24
  DataStructureDefinition,
24
25
  DataType,
25
26
  Facets,
@@ -32,14 +33,15 @@ from pysdmx.util import parse_item_urn
32
33
  def _find_concept(
33
34
  cs: Sequence[FusionConceptScheme],
34
35
  urn: str,
35
- ) -> FusionConcept:
36
+ ) -> Optional[FusionConcept]:
36
37
  r = parse_item_urn(urn)
37
38
  f = [
38
39
  m
39
40
  for m in cs
40
41
  if (m.agency == r.agency and m.id == r.id and m.version == r.version)
41
42
  ]
42
- return [c for c in f[0].items if c.id == r.item_id][0]
43
+ m = [c for c in f[0].items if c.id == r.item_id]
44
+ return m[0] if m else None
43
45
 
44
46
 
45
47
  def _get_representation(
@@ -112,12 +114,13 @@ class FusionAttribute(Struct, frozen=True):
112
114
  groups: Sequence[FusionGroup],
113
115
  ) -> Component:
114
116
  """Returns an attribute."""
115
- c = _find_concept(cs, self.concept)
117
+ m = _find_concept(cs, self.concept) if cs else None
118
+ c = m.to_model(cls) if m else parse_item_urn(self.concept)
116
119
  dt, facets, codes, ab = _get_representation(
117
120
  self.id, self.representation, cls, cons
118
121
  )
119
122
  lvl = self.__derive_level(groups)
120
- desc = c.descriptions[0].value if c.descriptions else None
123
+ desc = c.description if isinstance(c, Concept) else None
121
124
  if self.representation and self.representation.representation:
122
125
  local_enum_ref = self.representation.representation
123
126
  else:
@@ -126,10 +129,10 @@ class FusionAttribute(Struct, frozen=True):
126
129
  id=self.id,
127
130
  required=self.mandatory,
128
131
  role=Role.ATTRIBUTE,
129
- concept=c.to_model(cls),
132
+ concept=c,
130
133
  local_dtype=dt,
131
134
  local_facets=facets,
132
- name=c.names[0].value,
135
+ name=c.name if isinstance(c, Concept) else None,
133
136
  description=desc,
134
137
  local_codes=codes,
135
138
  attachment_level=lvl,
@@ -168,11 +171,12 @@ class FusionDimension(Struct, frozen=True):
168
171
  cons: Dict[str, Sequence[str]],
169
172
  ) -> Component:
170
173
  """Returns a dimension."""
171
- c = _find_concept(cs, self.concept)
174
+ m = _find_concept(cs, self.concept) if cs else None
175
+ c = m.to_model(cls) if m else parse_item_urn(self.concept)
172
176
  dt, facets, codes, ab = _get_representation(
173
177
  self.id, self.representation, cls, cons
174
178
  )
175
- desc = c.descriptions[0].value if c.descriptions else None
179
+ desc = c.description if isinstance(c, Concept) else None
176
180
  if self.representation and self.representation.representation:
177
181
  local_enum_ref = self.representation.representation
178
182
  else:
@@ -181,10 +185,10 @@ class FusionDimension(Struct, frozen=True):
181
185
  id=self.id,
182
186
  required=True,
183
187
  role=Role.DIMENSION,
184
- concept=c.to_model(cls),
188
+ concept=c,
185
189
  local_dtype=dt,
186
190
  local_facets=facets,
187
- name=c.names[0].value,
191
+ name=c.name if isinstance(c, Concept) else None,
188
192
  description=desc,
189
193
  local_codes=codes,
190
194
  array_def=ab,
@@ -222,11 +226,12 @@ class FusionMeasure(Struct, frozen=True):
222
226
  cons: Dict[str, Sequence[str]],
223
227
  ) -> Component:
224
228
  """Returns a measure."""
225
- c = _find_concept(cs, self.concept)
229
+ m = _find_concept(cs, self.concept) if cs else None
230
+ c = m.to_model(cls) if m else parse_item_urn(self.concept)
226
231
  dt, facets, codes, ab = _get_representation(
227
232
  self.id, self.representation, cls, cons
228
233
  )
229
- desc = c.descriptions[0].value if c.descriptions else None
234
+ desc = c.description if isinstance(c, Concept) else None
230
235
  if self.representation and self.representation.representation:
231
236
  local_enum_ref = self.representation.representation
232
237
  else:
@@ -235,10 +240,10 @@ class FusionMeasure(Struct, frozen=True):
235
240
  id=self.id,
236
241
  required=self.mandatory,
237
242
  role=Role.MEASURE,
238
- concept=c.to_model(cls),
243
+ concept=c,
239
244
  local_dtype=dt,
240
245
  local_facets=facets,
241
- name=c.names[0].value,
246
+ name=c.name if isinstance(c, Concept) else None,
242
247
  description=desc,
243
248
  local_codes=codes,
244
249
  array_def=ab,
@@ -14,13 +14,9 @@ from pysdmx.io.json.fusion.messages.dsd import (
14
14
  _find_concept,
15
15
  _get_representation,
16
16
  )
17
- from pysdmx.model import (
18
- ArrayBoundaries,
19
- MetadataComponent,
20
- )
21
- from pysdmx.model import (
22
- MetadataStructure as MSD,
23
- )
17
+ from pysdmx.model import ArrayBoundaries, MetadataComponent
18
+ from pysdmx.model import MetadataStructure as MSD
19
+ from pysdmx.util import parse_item_urn
24
20
 
25
21
 
26
22
  class FusionMetadataAttribute(Struct, frozen=True):
@@ -40,7 +36,8 @@ class FusionMetadataAttribute(Struct, frozen=True):
40
36
  cls: Sequence[FusionCodelist],
41
37
  ) -> MetadataComponent:
42
38
  """Returns an attribute."""
43
- c = _find_concept(cs, self.concept)
39
+ m = _find_concept(cs, self.concept) if cs else None
40
+ c = m.to_model(cls) if m else parse_item_urn(self.concept)
44
41
  dt, facets, codes, _ = _get_representation(
45
42
  self.id, self.representation, cls, {}
46
43
  )
@@ -60,7 +57,7 @@ class FusionMetadataAttribute(Struct, frozen=True):
60
57
  return MetadataComponent(
61
58
  self.id,
62
59
  is_presentational=self.presentational, # type: ignore[arg-type]
63
- concept=c.to_model(cls),
60
+ concept=c,
64
61
  local_dtype=dt,
65
62
  local_facets=facets,
66
63
  local_codes=codes,
@@ -307,17 +307,24 @@ class JsonHeader(msgspec.Struct, frozen=True, omit_defaults=True):
307
307
  self,
308
308
  header: Header,
309
309
  msg_type: Literal["structure", "metadata"] = "structure",
310
+ msg_version: Literal["2.0.0", "2.1"] = "2.0.0",
310
311
  ) -> "JsonHeader":
311
312
  """Create an SDMX-JSON header from a pysdmx Header."""
313
+ if msg_version == "2.0.0":
314
+ schema = (
315
+ "https://raw.githubusercontent.com/sdmx-twg/sdmx-json/"
316
+ f"develop/structure-message/tools/schemas/{msg_version}/"
317
+ f"sdmx-json-{msg_type}-schema.json"
318
+ )
319
+ else:
320
+ schema = (
321
+ f"https://json.sdmx.org/2.1/sdmx-json-{msg_type}-schema.json"
322
+ )
312
323
  return JsonHeader(
313
324
  header.id,
314
325
  header.prepared,
315
326
  header.sender,
316
327
  header.test,
317
328
  receivers=header.receiver if header.receiver else None,
318
- schema=(
319
- "https://raw.githubusercontent.com/sdmx-twg/sdmx-json/"
320
- "develop/structure-message/tools/schemas/2.0.0/"
321
- f"sdmx-json-{msg_type}-schema.json"
322
- ),
329
+ schema=schema,
323
330
  )
@@ -34,14 +34,17 @@ from pysdmx.model.dataflow import Group
34
34
  from pysdmx.util import parse_item_urn
35
35
 
36
36
 
37
- def _find_concept(cs: Sequence[JsonConceptScheme], urn: str) -> JsonConcept:
37
+ def _find_concept(
38
+ cs: Sequence[JsonConceptScheme], urn: str
39
+ ) -> Optional[JsonConcept]:
38
40
  r = parse_item_urn(urn)
39
41
  f = [
40
42
  m
41
43
  for m in cs
42
44
  if (m.agency == r.agency and m.id == r.id and m.version == r.version)
43
45
  ]
44
- return [c for c in f[0].concepts if c.id == r.item_id][0]
46
+ m = [c for c in f[0].concepts if c.id == r.item_id]
47
+ return m[0] if m else None
45
48
 
46
49
 
47
50
  def _get_type(repr_: JsonRepresentation) -> Optional[str]:
@@ -168,11 +171,8 @@ class JsonDimension(Struct, frozen=True, omit_defaults=True):
168
171
  cons: Dict[str, Sequence[str]],
169
172
  ) -> Component:
170
173
  """Returns a component."""
171
- c = (
172
- _find_concept(cs, self.conceptIdentity).to_model(cls)
173
- if cs
174
- else parse_item_urn(self.conceptIdentity)
175
- )
174
+ m = _find_concept(cs, self.conceptIdentity) if cs else None
175
+ c = m.to_model(cls) if m else parse_item_urn(self.conceptIdentity)
176
176
  name = c.name if isinstance(c, Concept) else None
177
177
  desc = c.description if isinstance(c, Concept) else None
178
178
  dt, facets, codes, ab = _get_representation(
@@ -227,11 +227,8 @@ class JsonAttribute(Struct, frozen=True, omit_defaults=True):
227
227
  groups: Sequence[JsonGroup],
228
228
  ) -> Component:
229
229
  """Returns a component."""
230
- c = (
231
- _find_concept(cs, self.conceptIdentity).to_model(cls)
232
- if cs
233
- else parse_item_urn(self.conceptIdentity)
234
- )
230
+ m = _find_concept(cs, self.conceptIdentity) if cs else None
231
+ c = m.to_model(cls) if m else parse_item_urn(self.conceptIdentity)
235
232
  name = c.name if isinstance(c, Concept) else None
236
233
  desc = c.description if isinstance(c, Concept) else None
237
234
  dt, facets, codes, ab = _get_representation(
@@ -312,11 +309,8 @@ class JsonMeasure(Struct, frozen=True, omit_defaults=True):
312
309
  cons: Dict[str, Sequence[str]],
313
310
  ) -> Component:
314
311
  """Returns a component."""
315
- c = (
316
- _find_concept(cs, self.conceptIdentity).to_model(cls)
317
- if cs
318
- else parse_item_urn(self.conceptIdentity)
319
- )
312
+ m = _find_concept(cs, self.conceptIdentity) if cs else None
313
+ c = m.to_model(cls) if m else parse_item_urn(self.conceptIdentity)
320
314
  name = c.name if isinstance(c, Concept) else None
321
315
  desc = c.description if isinstance(c, Concept) else None
322
316
  dt, facets, codes, ab = _get_representation(
@@ -49,11 +49,8 @@ class JsonMetadataAttribute(Struct, frozen=True, omit_defaults=True):
49
49
  self, cs: Sequence[JsonConceptScheme], cls: Sequence[Codelist]
50
50
  ) -> MetadataComponent:
51
51
  """Returns a metadata component."""
52
- c = (
53
- _find_concept(cs, self.conceptIdentity).to_model(cls)
54
- if cs
55
- else parse_item_urn(self.conceptIdentity)
56
- )
52
+ m = _find_concept(cs, self.conceptIdentity) if cs else None
53
+ c = m.to_model(cls) if m else parse_item_urn(self.conceptIdentity)
57
54
  dt, facets, codes, _ = _get_representation(
58
55
  self.id, self.localRepresentation, cls, {}
59
56
  )
@@ -1,6 +1,6 @@
1
1
  """Collection of SDMX-JSON schemas for reference metadata queries."""
2
2
 
3
- from typing import Any, Optional, Sequence
3
+ from typing import Any, Literal, Optional, Sequence
4
4
 
5
5
  from msgspec import Struct
6
6
 
@@ -162,7 +162,11 @@ class JsonMetadataMessage(Struct, frozen=True, omit_defaults=True):
162
162
  return MetadataMessage(header, reports)
163
163
 
164
164
  @classmethod
165
- def from_model(self, msg: MetadataMessage) -> "JsonMetadataMessage":
165
+ def from_model(
166
+ self,
167
+ msg: MetadataMessage,
168
+ msg_version: Literal["2.0.0", "2.1"] = "2.0.0",
169
+ ) -> "JsonMetadataMessage":
166
170
  """Converts a pysdmx metadata message to an SDMX-JSON one."""
167
171
  if not msg.header:
168
172
  raise errors.Invalid(
@@ -175,6 +179,6 @@ class JsonMetadataMessage(Struct, frozen=True, omit_defaults=True):
175
179
  "SDMX-JSON metadata messages must have metadata reports.",
176
180
  )
177
181
 
178
- header = JsonHeader.from_model(msg.header, "metadata")
182
+ header = JsonHeader.from_model(msg.header, "metadata", msg_version)
179
183
  reports = [JsonMetadataReport.from_model(r) for r in msg.reports]
180
184
  return JsonMetadataMessage(header, JsonMetadataSets(reports))
@@ -1,6 +1,6 @@
1
1
  """Collection of SDMX-JSON schemas for generic structure messages."""
2
2
 
3
- from typing import Sequence
3
+ from typing import Literal, Sequence
4
4
 
5
5
  from msgspec import Struct
6
6
 
@@ -338,12 +338,16 @@ class JsonStructureMessage(Struct, frozen=True, omit_defaults=True):
338
338
  return StructureMessage(header, structures)
339
339
 
340
340
  @classmethod
341
- def from_model(cls, message: StructureMessage) -> "JsonStructureMessage":
341
+ def from_model(
342
+ cls,
343
+ message: StructureMessage,
344
+ msg_version: Literal["2.0.0", "2.1"] = "2.0.0",
345
+ ) -> "JsonStructureMessage":
342
346
  """Creates an SDMX-JSON payload from a pysdmx StructureMessage."""
343
347
  if not message.header:
344
348
  raise errors.Invalid(
345
349
  "Invalid input", "SDMX-JSON messages must have a header."
346
350
  )
347
- header = JsonHeader.from_model(message.header)
351
+ header = JsonHeader.from_model(message.header, msg_version=msg_version)
348
352
  structs = JsonStructures.from_model(message)
349
353
  return JsonStructureMessage(header, structs)
@@ -1,4 +1,4 @@
1
- """Writer interface for SDMX-JSON 2.0.0 Reference Metadata messages."""
1
+ """Reader interface for SDMX-JSON 2.0.0 and 2.1.0 Reference Metadata."""
2
2
 
3
3
  import msgspec
4
4
 
@@ -11,7 +11,7 @@ from pysdmx.model.message import MetadataMessage
11
11
 
12
12
 
13
13
  def read(input_str: str, validate: bool = True) -> MetadataMessage:
14
- """Read an SDMX-JSON 2.0.0 Metadata Message.
14
+ """Read SDMX-JSON 2.0.0 and 2.1.0 Metadata messages.
15
15
 
16
16
  Args:
17
17
  input_str: SDMX-JSON reference metadata message to read.
@@ -34,6 +34,6 @@ def read(input_str: str, validate: bool = True) -> MetadataMessage:
34
34
  "Invalid message",
35
35
  (
36
36
  "The supplied file could not be read as SDMX-JSON 2.0.0 "
37
- "reference metadata message."
37
+ "or 2.1.0 reference metadata message."
38
38
  ),
39
39
  ) from de
@@ -1,4 +1,4 @@
1
- """Reader interface for SDMX-JSON 2.0.0 Structure messages."""
1
+ """Reader interface for SDMX-JSON 2.0.0 and 2.1.0 Structure messages."""
2
2
 
3
3
  import msgspec
4
4
 
@@ -11,7 +11,7 @@ from pysdmx.model.message import StructureMessage
11
11
 
12
12
 
13
13
  def read(input_str: str, validate: bool = True) -> StructureMessage:
14
- """Read an SDMX-JSON 2.0.0 Structure Message.
14
+ """Read SDMX-JSON 2.0.0 and 2.1.0 Structure messages.
15
15
 
16
16
  Args:
17
17
  input_str: SDMX-JSON structure message to read.
@@ -34,6 +34,6 @@ def read(input_str: str, validate: bool = True) -> StructureMessage:
34
34
  "Invalid message",
35
35
  (
36
36
  "The supplied file could not be read as SDMX-JSON 2.0.0 "
37
- "structure message."
37
+ "or 2.1.0 structure message."
38
38
  ),
39
39
  ) from de
@@ -0,0 +1,118 @@
1
+ """Writer interface for SDMX-JSON 2.0.0 Reference Metadata messages."""
2
+
3
+ from pathlib import Path
4
+ from typing import Literal, Optional, Sequence, Union
5
+
6
+ import msgspec
7
+
8
+ from pysdmx.io.json.sdmxjson2.messages import (
9
+ JsonMetadataMessage,
10
+ JsonStructureMessage,
11
+ )
12
+ from pysdmx.model import MetadataReport, encoders
13
+ from pysdmx.model.__base import MaintainableArtefact
14
+ from pysdmx.model.message import Header, MetadataMessage, StructureMessage
15
+
16
+
17
+ def write_metadata_msg(
18
+ reports: Sequence[MetadataReport],
19
+ output_path: Optional[Union[str, Path]] = None,
20
+ prettyprint: bool = True,
21
+ header: Optional[Header] = None,
22
+ msg_version: Literal["2.0.0", "2.1"] = "2.0.0",
23
+ ) -> Optional[str]:
24
+ """Write metadata reports in requested SDMX-JSON version.
25
+
26
+ Args:
27
+ reports: The reference metadata reports to be serialized.
28
+ output_path: The path to save the JSON file. If None or empty, the
29
+ serialized content is returned as a string instead.
30
+ prettyprint: Whether to format the JSON output with indentation (True)
31
+ or output compact JSON without extra whitespace (False).
32
+ header: The header to be used in the SDMX-JSON message
33
+ (will be generated if no header is supplied).
34
+ msg_version: The desired version of SDMX-JSON. Defaults to 2.0.0.
35
+
36
+ Returns:
37
+ The JSON string if output_path is None or empty, None otherwise.
38
+ """
39
+ if not header:
40
+ header = Header()
41
+ sm = MetadataMessage(header, reports)
42
+ jsm = JsonMetadataMessage.from_model(sm, msg_version)
43
+
44
+ encoder = msgspec.json.Encoder(enc_hook=encoders)
45
+ serialized_data = encoder.encode(jsm)
46
+
47
+ # Apply pretty-printing if requested
48
+ if prettyprint:
49
+ serialized_data = msgspec.json.format(serialized_data, indent=4)
50
+
51
+ # If output_path is provided, write to file
52
+ if output_path:
53
+ # Convert to Path object if string
54
+ if isinstance(output_path, str):
55
+ output_path = Path(output_path)
56
+
57
+ # Create parent directories if they don't exist
58
+ output_path.parent.mkdir(parents=True, exist_ok=True)
59
+
60
+ # Write to file
61
+ with open(output_path, "wb") as f:
62
+ f.write(serialized_data)
63
+ return None
64
+ else:
65
+ # Return as string
66
+ return serialized_data.decode("utf-8")
67
+
68
+
69
+ def write_structure_msg(
70
+ structures: Sequence[MaintainableArtefact],
71
+ output_path: Optional[Union[str, Path]] = None,
72
+ prettyprint: bool = True,
73
+ header: Optional[Header] = None,
74
+ msg_version: Literal["2.0.0", "2.1"] = "2.0.0",
75
+ ) -> Optional[str]:
76
+ """Write maintainable SDMX artefacts in SDMX-JSON 2.0.0.
77
+
78
+ Args:
79
+ structures: The maintainable SDMX artefacts to be serialized.
80
+ output_path: The path to save the JSON file. If None or empty, the
81
+ serialized content is returned as a string instead.
82
+ prettyprint: Whether to format the JSON output with indentation (True)
83
+ or output compact JSON without extra whitespace (False).
84
+ header: The header to be used in the SDMX-JSON message
85
+ (will be generated if no header is supplied).
86
+ msg_version: The desired version of SDMX-JSON. Defaults to 2.0.0.
87
+
88
+ Returns:
89
+ The JSON string if output_path is None or empty, None otherwise.
90
+ """
91
+ if not header:
92
+ header = Header()
93
+ sm = StructureMessage(header, structures)
94
+ jsm = JsonStructureMessage.from_model(sm, msg_version)
95
+
96
+ encoder = msgspec.json.Encoder(enc_hook=encoders)
97
+ serialized_data = encoder.encode(jsm)
98
+
99
+ # Apply pretty-printing if requested
100
+ if prettyprint:
101
+ serialized_data = msgspec.json.format(serialized_data, indent=4)
102
+
103
+ # If output_path is provided, write to file
104
+ if output_path:
105
+ # Convert to Path object if string
106
+ if isinstance(output_path, str):
107
+ output_path = Path(output_path)
108
+
109
+ # Create parent directories if they don't exist
110
+ output_path.parent.mkdir(parents=True, exist_ok=True)
111
+
112
+ # Write to file
113
+ with open(output_path, "wb") as f:
114
+ f.write(serialized_data)
115
+ return None
116
+ else:
117
+ # Return as string
118
+ return serialized_data.decode("utf-8")
@@ -0,0 +1 @@
1
+ """Collection of writers for SDMX-JSON 2.0.0 messages."""
@@ -0,0 +1,33 @@
1
+ """Writer interface for SDMX-JSON 2.0.0 Reference Metadata messages."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Sequence, Union
5
+
6
+ from pysdmx.io.json.sdmxjson2.writer._helper import write_metadata_msg
7
+ from pysdmx.model import MetadataReport
8
+ from pysdmx.model.message import Header
9
+
10
+
11
+ def write(
12
+ reports: Sequence[MetadataReport],
13
+ output_path: Optional[Union[str, Path]] = None,
14
+ prettyprint: bool = True,
15
+ header: Optional[Header] = None,
16
+ ) -> Optional[str]:
17
+ """Write metadata reports in SDMX-JSON 2.0.0.
18
+
19
+ Args:
20
+ reports: The reference metadata reports to be serialized.
21
+ output_path: The path to save the JSON file. If None or empty, the
22
+ serialized content is returned as a string instead.
23
+ prettyprint: Whether to format the JSON output with indentation (True)
24
+ or output compact JSON without extra whitespace (False).
25
+ header: The header to be used in the SDMX-JSON message
26
+ (will be generated if no header is supplied).
27
+
28
+ Returns:
29
+ The JSON string if output_path is None or empty, None otherwise.
30
+ """
31
+ return write_metadata_msg(
32
+ reports, output_path, prettyprint, header, "2.0.0"
33
+ )
@@ -0,0 +1,33 @@
1
+ """Writer interface for SDMX-JSON 2.0.0 Structure messages."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Sequence, Union
5
+
6
+ from pysdmx.io.json.sdmxjson2.writer._helper import write_structure_msg
7
+ from pysdmx.model.__base import MaintainableArtefact
8
+ from pysdmx.model.message import Header
9
+
10
+
11
+ def write(
12
+ structures: Sequence[MaintainableArtefact],
13
+ output_path: Optional[Union[str, Path]] = None,
14
+ prettyprint: bool = True,
15
+ header: Optional[Header] = None,
16
+ ) -> Optional[str]:
17
+ """Write maintainable SDMX artefacts in SDMX-JSON 2.0.0.
18
+
19
+ Args:
20
+ structures: The maintainable SDMX artefacts to be serialized.
21
+ output_path: The path to save the JSON file. If None or empty, the
22
+ serialized content is returned as a string instead.
23
+ prettyprint: Whether to format the JSON output with indentation (True)
24
+ or output compact JSON without extra whitespace (False).
25
+ header: The header to be used in the SDMX-JSON message
26
+ (will be generated if no header is supplied).
27
+
28
+ Returns:
29
+ The JSON string if output_path is None or empty, None otherwise.
30
+ """
31
+ return write_structure_msg(
32
+ structures, output_path, prettyprint, header, "2.0.0"
33
+ )
@@ -0,0 +1 @@
1
+ """Collection of writers for SDMX-JSON 2.1.0 messages."""
@@ -0,0 +1,31 @@
1
+ """Writer interface for SDMX-JSON 2.1.0 Reference Metadata messages."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional, Sequence, Union
5
+
6
+ from pysdmx.io.json.sdmxjson2.writer._helper import write_metadata_msg
7
+ from pysdmx.model import MetadataReport
8
+ from pysdmx.model.message import Header
9
+
10
+
11
+ def write(
12
+ reports: Sequence[MetadataReport],
13
+ output_path: Optional[Union[str, Path]] = None,
14
+ prettyprint: bool = True,
15
+ header: Optional[Header] = None,
16
+ ) -> Optional[str]:
17
+ """Write metadata reports in SDMX-JSON 2.1.0.
18
+
19
+ Args:
20
+ reports: The reference metadata reports to be serialized.
21
+ output_path: The path to save the JSON file. If None or empty, the
22
+ serialized content is returned as a string instead.
23
+ prettyprint: Whether to format the JSON output with indentation (True)
24
+ or output compact JSON without extra whitespace (False).
25
+ header: The header to be used in the SDMX-JSON message
26
+ (will be generated if no header is supplied).
27
+
28
+ Returns:
29
+ The JSON string if output_path is None or empty, None otherwise.
30
+ """
31
+ return write_metadata_msg(reports, output_path, prettyprint, header, "2.1")