pysdmx 1.5.1__py3-none-any.whl → 1.6.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 (60) hide show
  1. pysdmx/__init__.py +1 -1
  2. pysdmx/api/fmr/__init__.py +8 -3
  3. pysdmx/api/fmr/maintenance.py +158 -0
  4. pysdmx/api/qb/structure.py +1 -0
  5. pysdmx/api/qb/util.py +1 -0
  6. pysdmx/io/csv/__csv_aux_reader.py +99 -0
  7. pysdmx/io/csv/__csv_aux_writer.py +118 -0
  8. pysdmx/io/csv/sdmx10/reader/__init__.py +9 -14
  9. pysdmx/io/csv/sdmx10/writer/__init__.py +28 -2
  10. pysdmx/io/csv/sdmx20/__init__.py +0 -9
  11. pysdmx/io/csv/sdmx20/reader/__init__.py +8 -61
  12. pysdmx/io/csv/sdmx20/writer/__init__.py +32 -25
  13. pysdmx/io/csv/sdmx21/__init__.py +1 -0
  14. pysdmx/io/csv/sdmx21/reader/__init__.py +86 -0
  15. pysdmx/io/csv/sdmx21/writer/__init__.py +70 -0
  16. pysdmx/io/format.py +8 -0
  17. pysdmx/io/input_processor.py +16 -2
  18. pysdmx/io/json/fusion/messages/code.py +21 -4
  19. pysdmx/io/json/fusion/messages/concept.py +16 -8
  20. pysdmx/io/json/fusion/messages/dataflow.py +8 -1
  21. pysdmx/io/json/fusion/messages/dsd.py +15 -0
  22. pysdmx/io/json/fusion/messages/schema.py +8 -1
  23. pysdmx/io/json/sdmxjson2/messages/agency.py +43 -7
  24. pysdmx/io/json/sdmxjson2/messages/category.py +92 -7
  25. pysdmx/io/json/sdmxjson2/messages/code.py +239 -18
  26. pysdmx/io/json/sdmxjson2/messages/concept.py +78 -13
  27. pysdmx/io/json/sdmxjson2/messages/constraint.py +5 -5
  28. pysdmx/io/json/sdmxjson2/messages/core.py +121 -14
  29. pysdmx/io/json/sdmxjson2/messages/dataflow.py +63 -8
  30. pysdmx/io/json/sdmxjson2/messages/dsd.py +215 -20
  31. pysdmx/io/json/sdmxjson2/messages/map.py +200 -24
  32. pysdmx/io/json/sdmxjson2/messages/pa.py +36 -5
  33. pysdmx/io/json/sdmxjson2/messages/provider.py +35 -7
  34. pysdmx/io/json/sdmxjson2/messages/report.py +85 -7
  35. pysdmx/io/json/sdmxjson2/messages/schema.py +11 -12
  36. pysdmx/io/json/sdmxjson2/messages/structure.py +150 -2
  37. pysdmx/io/json/sdmxjson2/messages/vtl.py +547 -17
  38. pysdmx/io/json/sdmxjson2/reader/metadata.py +32 -0
  39. pysdmx/io/json/sdmxjson2/reader/structure.py +32 -0
  40. pysdmx/io/json/sdmxjson2/writer/__init__.py +9 -0
  41. pysdmx/io/json/sdmxjson2/writer/metadata.py +60 -0
  42. pysdmx/io/json/sdmxjson2/writer/structure.py +61 -0
  43. pysdmx/io/reader.py +28 -9
  44. pysdmx/io/serde.py +17 -0
  45. pysdmx/io/writer.py +45 -9
  46. pysdmx/io/xml/__structure_aux_reader.py +2 -2
  47. pysdmx/io/xml/__structure_aux_writer.py +5 -5
  48. pysdmx/io/xml/__write_data_aux.py +1 -54
  49. pysdmx/io/xml/__write_structure_specific_aux.py +1 -1
  50. pysdmx/io/xml/sdmx21/writer/generic.py +1 -1
  51. pysdmx/model/code.py +11 -1
  52. pysdmx/model/dataflow.py +26 -3
  53. pysdmx/model/map.py +12 -4
  54. pysdmx/model/message.py +9 -1
  55. pysdmx/toolkit/pd/_data_utils.py +100 -0
  56. pysdmx/toolkit/vtl/_validations.py +2 -3
  57. {pysdmx-1.5.1.dist-info → pysdmx-1.6.0.dist-info}/METADATA +3 -2
  58. {pysdmx-1.5.1.dist-info → pysdmx-1.6.0.dist-info}/RECORD +60 -48
  59. {pysdmx-1.5.1.dist-info → pysdmx-1.6.0.dist-info}/WHEEL +1 -1
  60. {pysdmx-1.5.1.dist-info → pysdmx-1.6.0.dist-info/licenses}/LICENSE +0 -0
@@ -6,11 +6,13 @@ from typing import Any, Dict, Literal, Optional, Sequence, Union
6
6
 
7
7
  from msgspec import Struct
8
8
 
9
+ from pysdmx import errors
9
10
  from pysdmx.io.json.sdmxjson2.messages.core import (
10
11
  JsonAnnotation,
11
12
  MaintainableType,
12
13
  )
13
14
  from pysdmx.model import (
15
+ Agency,
14
16
  ComponentMap,
15
17
  DataType,
16
18
  DatePatternMap,
@@ -26,7 +28,7 @@ from pysdmx.model import (
26
28
  from pysdmx.util import find_by_urn
27
29
 
28
30
 
29
- class JsonSourceValue(Struct, frozen=True):
31
+ class JsonSourceValue(Struct, frozen=True, omit_defaults=True):
30
32
  """SDMX-JSON payload for a source value."""
31
33
 
32
34
  value: str
@@ -39,8 +41,16 @@ class JsonSourceValue(Struct, frozen=True):
39
41
  else:
40
42
  return self.value
41
43
 
44
+ @classmethod
45
+ def from_model(self, value: str) -> "JsonSourceValue":
46
+ """Converts a pysdmx source string value to an SDMX-JSON one."""
47
+ if value.startswith("regex:"):
48
+ return JsonSourceValue(value.replace("regex:", ""), True)
49
+ else:
50
+ return JsonSourceValue(value)
51
+
42
52
 
43
- class JsonRepresentationMapping(Struct, frozen=True):
53
+ class JsonRepresentationMapping(Struct, frozen=True, omit_defaults=True):
44
54
  """SDMX-JSON payload for a representation mapping."""
45
55
 
46
56
  sourceValues: Sequence[JsonSourceValue]
@@ -72,8 +82,28 @@ class JsonRepresentationMapping(Struct, frozen=True):
72
82
  valid_to=self.__get_dt(self.validTo) if self.validTo else None,
73
83
  )
74
84
 
85
+ @classmethod
86
+ def from_model(
87
+ self, vm: Union[MultiValueMap, ValueMap]
88
+ ) -> "JsonRepresentationMapping":
89
+ """Converts a value map to an SDMX-JSON JsonRepresentationMapping."""
90
+ if isinstance(vm, ValueMap):
91
+ return JsonRepresentationMapping(
92
+ [JsonSourceValue.from_model(vm.source)],
93
+ [vm.target],
94
+ vm.valid_from.strftime("%Y-%m-%d") if vm.valid_from else None,
95
+ vm.valid_to.strftime("%Y-%m-%d") if vm.valid_to else None,
96
+ )
97
+ else:
98
+ return JsonRepresentationMapping(
99
+ [JsonSourceValue.from_model(s) for s in vm.source],
100
+ vm.target,
101
+ vm.valid_from.strftime("%Y-%m-%d") if vm.valid_from else None,
102
+ vm.valid_to.strftime("%Y-%m-%d") if vm.valid_to else None,
103
+ )
104
+
75
105
 
76
- class JsonRepresentationMap(MaintainableType, frozen=True):
106
+ class JsonRepresentationMap(MaintainableType, frozen=True, omit_defaults=True):
77
107
  """SDMX-JSON payload for a representation map."""
78
108
 
79
109
  source: Sequence[Dict[str, str]] = ()
@@ -83,8 +113,6 @@ class JsonRepresentationMap(MaintainableType, frozen=True):
83
113
  def __parse_st(self, item: Dict[str, str]) -> Union[DataType, str]:
84
114
  if "dataType" in item:
85
115
  return DataType(item["dataType"])
86
- elif "valuelist" in item:
87
- return item["valuelist"]
88
116
  else:
89
117
  return item["codelist"]
90
118
 
@@ -118,8 +146,54 @@ class JsonRepresentationMap(MaintainableType, frozen=True):
118
146
  version=self.version,
119
147
  )
120
148
 
149
+ @classmethod
150
+ def from_model(
151
+ self, rm: Union[MultiRepresentationMap, RepresentationMap]
152
+ ) -> "JsonRepresentationMap":
153
+ """Converts a pysdmx representation map to an SDMX-JSON one."""
154
+
155
+ def __convert_st(st: str) -> Dict[str, str]:
156
+ if "Codelist" in st or "ValueList" in st:
157
+ return {"codelist": st}
158
+ else:
159
+ return {"dataType": st}
160
+
161
+ if not rm.name:
162
+ raise errors.Invalid(
163
+ "Invalid input",
164
+ "SDMX-JSON representation maps must have a name",
165
+ {"representation_map": rm.id},
166
+ )
167
+
168
+ if isinstance(rm, RepresentationMap):
169
+ source = [__convert_st(rm.source)] if rm.source else []
170
+ target = [__convert_st(rm.target)] if rm.target else []
171
+ else:
172
+ source = [__convert_st(s) for s in rm.source]
173
+ target = [__convert_st(t) for t in rm.target]
174
+ return JsonRepresentationMap(
175
+ agency=(
176
+ rm.agency.id if isinstance(rm.agency, Agency) else rm.agency
177
+ ),
178
+ id=rm.id,
179
+ name=rm.name,
180
+ version=rm.version,
181
+ isExternalReference=rm.is_external_reference,
182
+ validFrom=rm.valid_from,
183
+ validTo=rm.valid_to,
184
+ description=rm.description,
185
+ annotations=tuple(
186
+ [JsonAnnotation.from_model(a) for a in rm.annotations]
187
+ ),
188
+ source=tuple(source),
189
+ target=tuple(target),
190
+ representationMappings=tuple(
191
+ [JsonRepresentationMapping.from_model(m) for m in rm.maps]
192
+ ),
193
+ )
194
+
121
195
 
122
- class JsonFixedValueMap(Struct, frozen=True):
196
+ class JsonFixedValueMap(Struct, frozen=True, omit_defaults=True):
123
197
  """SDMX-JSON payload for a fixed value map."""
124
198
 
125
199
  values: Sequence[Any]
@@ -136,8 +210,17 @@ class JsonFixedValueMap(Struct, frozen=True):
136
210
  located_in, # type: ignore[arg-type]
137
211
  )
138
212
 
213
+ @classmethod
214
+ def from_model(self, fvm: FixedValueMap) -> "JsonFixedValueMap":
215
+ """Converts a pysdmx fixed value map to an SDMX-JSON one."""
216
+ return JsonFixedValueMap(
217
+ values=[fvm.value],
218
+ source=fvm.target if fvm.located_in == "source" else None,
219
+ target=fvm.target if fvm.located_in == "target" else None,
220
+ )
221
+
139
222
 
140
- class JsonComponentMap(Struct, frozen=True):
223
+ class JsonComponentMap(Struct, frozen=True, omit_defaults=True):
141
224
  """SDMX-JSON payload for a component map."""
142
225
 
143
226
  source: Sequence[str]
@@ -162,15 +245,43 @@ class JsonComponentMap(Struct, frozen=True):
162
245
  else:
163
246
  return ImplicitComponentMap(self.source[0], self.target[0])
164
247
 
248
+ @classmethod
249
+ def from_model(
250
+ self, cm: Union[ComponentMap, MultiComponentMap, ImplicitComponentMap]
251
+ ) -> "JsonComponentMap":
252
+ """Converts a pysdmx component map to an SDMX-JSON one."""
253
+ if isinstance(cm, ImplicitComponentMap):
254
+ return JsonComponentMap([cm.source], [cm.target])
255
+ elif isinstance(cm, ComponentMap):
256
+ rm = (
257
+ (
258
+ "urn:sdmx:org.sdmx.infomodel.structuremapping."
259
+ f"{cm.values.short_urn}"
260
+ )
261
+ if isinstance(cm.values, RepresentationMap)
262
+ else cm.values
263
+ )
264
+ return JsonComponentMap([cm.source], [cm.target], rm)
265
+ else:
266
+ rm = (
267
+ (
268
+ "urn:sdmx:org.sdmx.infomodel.structuremapping."
269
+ f"{cm.values.short_urn}"
270
+ )
271
+ if isinstance(cm.values, MultiRepresentationMap)
272
+ else cm.values
273
+ )
274
+ return JsonComponentMap(cm.source, cm.target, rm)
275
+
165
276
 
166
- class JsonMappedPair(Struct, frozen=True):
277
+ class JsonMappedPair(Struct, frozen=True, omit_defaults=True):
167
278
  """SDMX-JSON payload for a pair of mapped components."""
168
279
 
169
280
  source: str
170
281
  target: str
171
282
 
172
283
 
173
- class JsonDatePatternMap(Struct, frozen=True):
284
+ class JsonDatePatternMap(Struct, frozen=True, omit_defaults=True):
174
285
  """SDMX-JSON payload for a date pattern map."""
175
286
 
176
287
  sourcePattern: str
@@ -194,18 +305,38 @@ class JsonDatePatternMap(Struct, frozen=True):
194
305
  )
195
306
  typ = "fixed" if self.targetFrequencyID else "variable"
196
307
  return DatePatternMap(
197
- self.mappedComponents[0].source,
198
- self.mappedComponents[0].target,
199
- self.sourcePattern,
200
- freq, # type: ignore[arg-type]
201
- self.id,
202
- self.locale,
203
- typ, # type: ignore[arg-type]
204
- self.resolvePeriod,
308
+ source=self.mappedComponents[0].source,
309
+ target=self.mappedComponents[0].target,
310
+ pattern=self.sourcePattern,
311
+ frequency=freq, # type: ignore[arg-type]
312
+ id=self.id,
313
+ locale=self.locale,
314
+ pattern_type=typ, # type: ignore[arg-type]
315
+ resolve_period=self.resolvePeriod,
316
+ )
317
+
318
+ @classmethod
319
+ def from_model(self, dpm: DatePatternMap) -> "JsonDatePatternMap":
320
+ """Converts a pysdmx date pattern map to an SDMX-JSON one."""
321
+ if dpm.pattern_type == "fixed":
322
+ tf = dpm.frequency
323
+ fd = None
324
+ else:
325
+ tf = None
326
+ fd = dpm.frequency
327
+
328
+ return JsonDatePatternMap(
329
+ sourcePattern=dpm.pattern,
330
+ mappedComponents=[JsonMappedPair(dpm.source, dpm.target)],
331
+ locale=dpm.locale,
332
+ id=dpm.id,
333
+ resolvePeriod=dpm.resolve_period,
334
+ targetFrequencyID=tf,
335
+ frequencyDimension=fd,
205
336
  )
206
337
 
207
338
 
208
- class JsonStructureMap(MaintainableType, frozen=True):
339
+ class JsonStructureMap(MaintainableType, frozen=True, omit_defaults=True):
209
340
  """SDMX-JSON payload for a structure map."""
210
341
 
211
342
  source: str = ""
@@ -237,8 +368,53 @@ class JsonStructureMap(MaintainableType, frozen=True):
237
368
  valid_to=self.validTo,
238
369
  )
239
370
 
371
+ @classmethod
372
+ def from_model(self, sm: StructureMap) -> "JsonStructureMap":
373
+ """Converts a pysdmx structure map to an SDMX-JSON one."""
374
+ cms = list(sm.component_maps)
375
+ cms.extend(list(sm.implicit_component_maps)) # type: ignore[arg-type]
376
+ cms.extend(list(sm.multi_component_maps)) # type: ignore[arg-type]
377
+ if not sm.name:
378
+ raise errors.Invalid(
379
+ "Invalid input",
380
+ "SDMX-JSON structure maps must have a name",
381
+ {"structure_map": sm.id},
382
+ )
383
+ return JsonStructureMap(
384
+ agency=(
385
+ sm.agency.id if isinstance(sm.agency, Agency) else sm.agency
386
+ ),
387
+ id=sm.id,
388
+ name=sm.name,
389
+ version=sm.version,
390
+ isExternalReference=sm.is_external_reference,
391
+ validFrom=sm.valid_from,
392
+ validTo=sm.valid_to,
393
+ description=sm.description,
394
+ annotations=tuple(
395
+ [JsonAnnotation.from_model(a) for a in sm.annotations]
396
+ ),
397
+ source=sm.source,
398
+ target=sm.target,
399
+ datePatternMaps=tuple(
400
+ [
401
+ JsonDatePatternMap.from_model(dpm)
402
+ for dpm in sm.date_pattern_maps
403
+ ]
404
+ ),
405
+ componentMaps=tuple(
406
+ [JsonComponentMap.from_model(cm) for cm in cms]
407
+ ),
408
+ fixedValueMaps=tuple(
409
+ [
410
+ JsonFixedValueMap.from_model(fvm)
411
+ for fvm in sm.fixed_value_maps
412
+ ]
413
+ ),
414
+ )
415
+
240
416
 
241
- class JsonStructureMaps(Struct, frozen=True):
417
+ class JsonStructureMaps(Struct, frozen=True, omit_defaults=True):
242
418
  """SDMX-JSON payload for structure maps."""
243
419
 
244
420
  structureMaps: Sequence[JsonStructureMap]
@@ -251,7 +427,7 @@ class JsonStructureMaps(Struct, frozen=True):
251
427
  ]
252
428
 
253
429
 
254
- class JsonMappingMessage(Struct, frozen=True):
430
+ class JsonMappingMessage(Struct, frozen=True, omit_defaults=True):
255
431
  """SDMX-JSON payload for /structuremap queries."""
256
432
 
257
433
  data: JsonStructureMaps
@@ -261,7 +437,7 @@ class JsonMappingMessage(Struct, frozen=True):
261
437
  return self.data.to_model()[0]
262
438
 
263
439
 
264
- class JsonStructureMapsMessage(Struct, frozen=True):
440
+ class JsonStructureMapsMessage(Struct, frozen=True, omit_defaults=True):
265
441
  """SDMX-JSON payload for generic /structuremap queries."""
266
442
 
267
443
  data: JsonStructureMaps
@@ -271,7 +447,7 @@ class JsonStructureMapsMessage(Struct, frozen=True):
271
447
  return self.data.to_model()
272
448
 
273
449
 
274
- class JsonRepresentationMaps(Struct, frozen=True):
450
+ class JsonRepresentationMaps(Struct, frozen=True, omit_defaults=True):
275
451
  """SDMX-JSON payload for representation maps."""
276
452
 
277
453
  representationMaps: Sequence[JsonRepresentationMap]
@@ -287,7 +463,7 @@ class JsonRepresentationMaps(Struct, frozen=True):
287
463
  return maps
288
464
 
289
465
 
290
- class JsonRepresentationMapMessage(Struct, frozen=True):
466
+ class JsonRepresentationMapMessage(Struct, frozen=True, omit_defaults=True):
291
467
  """SDMX-JSON payload for /representationmap queries."""
292
468
 
293
469
  data: JsonRepresentationMaps
@@ -297,7 +473,7 @@ class JsonRepresentationMapMessage(Struct, frozen=True):
297
473
  return self.data.to_model()[0]
298
474
 
299
475
 
300
- class JsonRepresentationMapsMessage(Struct, frozen=True):
476
+ class JsonRepresentationMapsMessage(Struct, frozen=True, omit_defaults=True):
301
477
  """SDMX-JSON payload for /representationmap queries."""
302
478
 
303
479
  data: JsonRepresentationMaps
@@ -4,13 +4,17 @@ from typing import Sequence
4
4
 
5
5
  from msgspec import Struct
6
6
 
7
+ from pysdmx import errors
7
8
  from pysdmx.io.json.sdmxjson2.messages.core import (
9
+ JsonAnnotation,
8
10
  MaintainableType,
9
11
  )
10
- from pysdmx.model import ProvisionAgreement
12
+ from pysdmx.model import Agency, ProvisionAgreement
11
13
 
12
14
 
13
- class JsonProvisionAgreement(MaintainableType, frozen=True):
15
+ class JsonProvisionAgreement(
16
+ MaintainableType, frozen=True, omit_defaults=True
17
+ ):
14
18
  """SDMX-JSON payload for a provision agreement."""
15
19
 
16
20
  dataflow: str = ""
@@ -28,12 +32,39 @@ class JsonProvisionAgreement(MaintainableType, frozen=True):
28
32
  valid_to=self.validTo,
29
33
  dataflow=self.dataflow,
30
34
  provider=self.dataProvider,
31
- annotations=[a.to_model() for a in self.annotations],
35
+ annotations=tuple([a.to_model() for a in self.annotations]),
32
36
  is_external_reference=self.isExternalReference,
33
37
  )
34
38
 
39
+ @classmethod
40
+ def from_model(self, pa: ProvisionAgreement) -> "JsonProvisionAgreement":
41
+ """Converts a pysdmx provision agreement to an SDMX-JSON one."""
42
+ if not pa.name:
43
+ raise errors.Invalid(
44
+ "Invalid input",
45
+ "SDMX-JSON provision agreements must have a name",
46
+ {"provision_agreement": pa.id},
47
+ )
48
+ return JsonProvisionAgreement(
49
+ agency=(
50
+ pa.agency.id if isinstance(pa.agency, Agency) else pa.agency
51
+ ),
52
+ id=pa.id,
53
+ name=pa.name,
54
+ version=pa.version,
55
+ isExternalReference=pa.is_external_reference,
56
+ validFrom=pa.valid_from,
57
+ validTo=pa.valid_to,
58
+ description=pa.description,
59
+ annotations=tuple(
60
+ [JsonAnnotation.from_model(a) for a in pa.annotations]
61
+ ),
62
+ dataflow=pa.dataflow,
63
+ dataProvider=pa.provider,
64
+ )
65
+
35
66
 
36
- class JsonProvisionAgreements(Struct, frozen=True):
67
+ class JsonProvisionAgreements(Struct, frozen=True, omit_defaults=True):
37
68
  """SDMX-JSON payload for provision agreements."""
38
69
 
39
70
  provisionAgreements: Sequence[JsonProvisionAgreement]
@@ -43,7 +74,7 @@ class JsonProvisionAgreements(Struct, frozen=True):
43
74
  return [pa.to_model() for pa in self.provisionAgreements]
44
75
 
45
76
 
46
- class JsonProvisionAgreementsMessage(Struct, frozen=True):
77
+ class JsonProvisionAgreementsMessage(Struct, frozen=True, omit_defaults=True):
47
78
  """SDMX-JSON payload for /provisionagreement queries."""
48
79
 
49
80
  data: JsonProvisionAgreements
@@ -5,13 +5,16 @@ from typing import Dict, Sequence, Set
5
5
 
6
6
  from msgspec import Struct
7
7
 
8
- from pysdmx.io.json.sdmxjson2.messages.core import ItemSchemeType
8
+ from pysdmx.io.json.sdmxjson2.messages.core import (
9
+ ItemSchemeType,
10
+ JsonAnnotation,
11
+ )
9
12
  from pysdmx.io.json.sdmxjson2.messages.pa import JsonProvisionAgreement
10
- from pysdmx.model import DataflowRef, DataProvider, DataProviderScheme
13
+ from pysdmx.model import Agency, DataflowRef, DataProvider, DataProviderScheme
11
14
  from pysdmx.util import parse_item_urn, parse_urn
12
15
 
13
16
 
14
- class JsonDataProviderScheme(ItemSchemeType, frozen=True):
17
+ class JsonDataProviderScheme(ItemSchemeType, frozen=True, omit_defaults=True):
15
18
  """SDMX-JSON payload for a data provider scheme."""
16
19
 
17
20
  dataProviders: Sequence[DataProvider] = ()
@@ -37,7 +40,9 @@ class JsonDataProviderScheme(ItemSchemeType, frozen=True):
37
40
  description=p.description,
38
41
  contacts=p.contacts,
39
42
  dataflows=list(paprs[f"{self.agency}:{p.id}"]),
40
- annotations=[a.to_model() for a in self.annotations],
43
+ annotations=tuple(
44
+ [a.to_model() for a in self.annotations]
45
+ ),
41
46
  )
42
47
  for p in self.dataProviders
43
48
  ]
@@ -48,7 +53,9 @@ class JsonDataProviderScheme(ItemSchemeType, frozen=True):
48
53
  name=p.name,
49
54
  description=p.description,
50
55
  contacts=p.contacts,
51
- annotations=[a.to_model() for a in self.annotations],
56
+ annotations=tuple(
57
+ [a.to_model() for a in self.annotations]
58
+ ),
52
59
  )
53
60
  for p in self.dataProviders
54
61
  ]
@@ -63,8 +70,29 @@ class JsonDataProviderScheme(ItemSchemeType, frozen=True):
63
70
  valid_to=self.validTo,
64
71
  )
65
72
 
73
+ @classmethod
74
+ def from_model(self, dps: DataProviderScheme) -> "JsonDataProviderScheme":
75
+ """Converts a pysdmx data provider scheme to an SDMX-JSON one."""
76
+ return JsonDataProviderScheme(
77
+ id="DATA_PROVIDERS",
78
+ name="DATA_PROVIDERS",
79
+ agency=(
80
+ dps.agency.id if isinstance(dps.agency, Agency) else dps.agency
81
+ ),
82
+ description=dps.description,
83
+ version="1.0",
84
+ dataProviders=dps.items,
85
+ annotations=tuple(
86
+ [JsonAnnotation.from_model(a) for a in dps.annotations]
87
+ ),
88
+ isExternalReference=dps.is_external_reference,
89
+ isPartial=dps.is_partial,
90
+ validFrom=dps.valid_from,
91
+ validTo=dps.valid_to,
92
+ )
93
+
66
94
 
67
- class JsonDataProviderSchemes(Struct, frozen=True):
95
+ class JsonDataProviderSchemes(Struct, frozen=True, omit_defaults=True):
68
96
  """SDMX-JSON payload for the list of data provider schemes."""
69
97
 
70
98
  dataProviderSchemes: Sequence[JsonDataProviderScheme]
@@ -78,7 +106,7 @@ class JsonDataProviderSchemes(Struct, frozen=True):
78
106
  ]
79
107
 
80
108
 
81
- class JsonProviderMessage(Struct, frozen=True):
109
+ class JsonProviderMessage(Struct, frozen=True, omit_defaults=True):
82
110
  """SDMX-JSON payload for /dataproviderscheme queries."""
83
111
 
84
112
  data: JsonDataProviderSchemes
@@ -4,13 +4,16 @@ from typing import Any, Optional, Sequence
4
4
 
5
5
  from msgspec import Struct
6
6
 
7
+ from pysdmx import errors
7
8
  from pysdmx.io.json.sdmxjson2.messages.core import (
8
9
  IdentifiableType,
9
10
  ItemSchemeType,
11
+ JsonAnnotation,
10
12
  JsonHeader,
11
13
  JsonTextFormat,
12
14
  get_facets,
13
15
  )
16
+ from pysdmx.model import Agency
14
17
  from pysdmx.model.dataset import ActionType
15
18
  from pysdmx.model.message import MetadataMessage
16
19
  from pysdmx.model.metadata import (
@@ -35,12 +38,27 @@ class JsonMetadataAttribute(IdentifiableType, frozen=True, omit_defaults=True):
35
38
  id=self.id,
36
39
  value=self.value,
37
40
  attributes=attrs,
38
- annotations=[a.to_model() for a in self.annotations],
41
+ annotations=tuple([a.to_model() for a in self.annotations]),
39
42
  format=get_facets(self.format) if self.format else None,
40
43
  )
41
44
 
45
+ @classmethod
46
+ def from_model(self, attr: MetadataAttribute) -> "JsonMetadataAttribute":
47
+ """Converts a pysdmx metadata attribute to an SDMX-JSON one."""
48
+ return JsonMetadataAttribute(
49
+ id=attr.id,
50
+ annotations=tuple(
51
+ [JsonAnnotation.from_model(a) for a in attr.annotations]
52
+ ),
53
+ value=attr.value,
54
+ attributes=tuple(
55
+ [JsonMetadataAttribute.from_model(a) for a in attr.attributes]
56
+ ),
57
+ format=JsonTextFormat.from_model(None, attr.format),
58
+ )
59
+
42
60
 
43
- class JsonMetadataReport(ItemSchemeType, frozen=True):
61
+ class JsonMetadataReport(ItemSchemeType, frozen=True, omit_defaults=True):
44
62
  """SDMX-JSON payload for a metadata report."""
45
63
 
46
64
  metadataflow: str = ""
@@ -58,7 +76,7 @@ class JsonMetadataReport(ItemSchemeType, frozen=True):
58
76
  attrs = [a.to_model() for a in self.attributes]
59
77
  attrs = merge_attributes(attrs) # type: ignore[assignment]
60
78
  return MetadataReport(
61
- annotations=[a.to_model() for a in self.annotations],
79
+ annotations=tuple([a.to_model() for a in self.annotations]),
62
80
  id=self.id,
63
81
  name=self.name,
64
82
  description=self.description,
@@ -68,8 +86,8 @@ class JsonMetadataReport(ItemSchemeType, frozen=True):
68
86
  agency=self.agency,
69
87
  is_external_reference=self.isExternalReference,
70
88
  metadataflow=self.metadataflow,
71
- targets=self.targets,
72
- attributes=attrs,
89
+ targets=tuple(self.targets),
90
+ attributes=tuple(attrs),
73
91
  metadataProvisionAgreement=self.metadataProvisionAgreement,
74
92
  publicationPeriod=self.publicationPeriod,
75
93
  publicationYear=self.publicationYear,
@@ -78,8 +96,50 @@ class JsonMetadataReport(ItemSchemeType, frozen=True):
78
96
  action=ActionType(self.action) if self.action else None,
79
97
  )
80
98
 
99
+ @classmethod
100
+ def from_model(cls, report: MetadataReport) -> "JsonMetadataReport":
101
+ """Converts a pysdmx metadata report to an SDMX-JSON one."""
102
+ if not report.name:
103
+ raise errors.Invalid(
104
+ "Invalid input",
105
+ "SDMX-JSON metadata reports must have a name",
106
+ {"metadata_report": report.id},
107
+ )
108
+
109
+ return JsonMetadataReport(
110
+ agency=(
111
+ report.agency.id
112
+ if isinstance(report.agency, Agency)
113
+ else report.agency
114
+ ),
115
+ id=report.id,
116
+ name=report.name,
117
+ version=report.version,
118
+ isExternalReference=report.is_external_reference,
119
+ validFrom=report.valid_from,
120
+ validTo=report.valid_to,
121
+ description=report.description,
122
+ annotations=tuple(
123
+ [JsonAnnotation.from_model(a) for a in report.annotations]
124
+ ),
125
+ metadataflow=report.metadataflow,
126
+ targets=report.targets,
127
+ attributes=tuple(
128
+ [
129
+ JsonMetadataAttribute.from_model(a)
130
+ for a in report.attributes
131
+ ]
132
+ ),
133
+ metadataProvisionAgreement=report.metadataProvisionAgreement,
134
+ publicationPeriod=report.publicationPeriod,
135
+ publicationYear=report.publicationYear,
136
+ reportingBegin=report.reportingBegin,
137
+ reportingEnd=report.reportingEnd,
138
+ action=report.action.value if report.action else None,
139
+ )
140
+
81
141
 
82
- class JsonMetadataSets(Struct, frozen=True):
142
+ class JsonMetadataSets(Struct, frozen=True, omit_defaults=True):
83
143
  """SDMX-JSON payload for the list of metadata sets."""
84
144
 
85
145
  metadataSets: Sequence[JsonMetadataReport]
@@ -89,7 +149,7 @@ class JsonMetadataSets(Struct, frozen=True):
89
149
  return [r.to_model() for r in self.metadataSets]
90
150
 
91
151
 
92
- class JsonMetadataMessage(Struct, frozen=True):
152
+ class JsonMetadataMessage(Struct, frozen=True, omit_defaults=True):
93
153
  """SDMX-JSON payload for /metadata queries."""
94
154
 
95
155
  meta: JsonHeader
@@ -100,3 +160,21 @@ class JsonMetadataMessage(Struct, frozen=True):
100
160
  header = self.meta.to_model()
101
161
  reports = self.data.to_model()
102
162
  return MetadataMessage(header, reports)
163
+
164
+ @classmethod
165
+ def from_model(self, msg: MetadataMessage) -> "JsonMetadataMessage":
166
+ """Converts a pysdmx metadata message to an SDMX-JSON one."""
167
+ if not msg.header:
168
+ raise errors.Invalid(
169
+ "Invalid input",
170
+ "SDMX-JSON metadata messages must have a header.",
171
+ )
172
+ if not msg.reports:
173
+ raise errors.Invalid(
174
+ "Invalid input",
175
+ "SDMX-JSON metadata messages must have metadata reports.",
176
+ )
177
+
178
+ header = JsonHeader.from_model(msg.header, "metadata")
179
+ reports = [JsonMetadataReport.from_model(r) for r in msg.reports]
180
+ return JsonMetadataMessage(header, JsonMetadataSets(reports))