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
@@ -4,12 +4,13 @@ from typing import Literal, 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
  ItemSchemeType,
9
10
  JsonAnnotation,
10
11
  NameableType,
11
12
  )
12
- from pysdmx.model.__base import DataflowRef
13
+ from pysdmx.model.__base import Agency, DataflowRef, ItemReference
13
14
  from pysdmx.model.vtl import (
14
15
  CustomType,
15
16
  CustomTypeScheme,
@@ -32,7 +33,7 @@ from pysdmx.model.vtl import (
32
33
  from pysdmx.util import parse_urn
33
34
 
34
35
 
35
- class JsonCustomType(NameableType, frozen=True):
36
+ class JsonCustomType(NameableType, frozen=True, omit_defaults=True):
36
37
  """SDMX-JSON payload for custom types."""
37
38
 
38
39
  vtlScalarType: str = ""
@@ -55,8 +56,31 @@ class JsonCustomType(NameableType, frozen=True):
55
56
  annotations=[a.to_model() for a in self.annotations],
56
57
  )
57
58
 
59
+ @classmethod
60
+ def from_model(self, ct: CustomType) -> "JsonCustomType":
61
+ """Converts a pysdmx custom type to an SDMX-JSON one."""
62
+ if not ct.name:
63
+ raise errors.Invalid(
64
+ "Invalid input",
65
+ "SDMX-JSON custom types must have a name",
66
+ {"custom_type": ct.id},
67
+ )
68
+ return JsonCustomType(
69
+ id=ct.id,
70
+ name=ct.name,
71
+ description=ct.description,
72
+ annotations=tuple(
73
+ [JsonAnnotation.from_model(a) for a in ct.annotations]
74
+ ),
75
+ vtlScalarType=ct.vtl_scalar_type,
76
+ dataType=ct.data_type,
77
+ vtlLiteralFormat=ct.vtl_literal_format,
78
+ outputFormat=ct.output_format,
79
+ nullValue=ct.null_value,
80
+ )
81
+
58
82
 
59
- class JsonCustomTypeScheme(ItemSchemeType, frozen=True):
83
+ class JsonCustomTypeScheme(ItemSchemeType, frozen=True, omit_defaults=True):
60
84
  """SDMX-JSON payload for custom type schemes."""
61
85
 
62
86
  vtlVersion: str = ""
@@ -80,8 +104,38 @@ class JsonCustomTypeScheme(ItemSchemeType, frozen=True):
80
104
  annotations=[a.to_model() for a in self.annotations],
81
105
  )
82
106
 
107
+ @classmethod
108
+ def from_model(self, cts: CustomTypeScheme) -> "JsonCustomTypeScheme":
109
+ """Converts a pysdmx custom type scheme to an SDMX-JSON one."""
110
+ if not cts.name:
111
+ raise errors.Invalid(
112
+ "Invalid input",
113
+ "SDMX-JSON custom type schemes must have a name",
114
+ {"custom_type_scheme": cts.id},
115
+ )
116
+ return JsonCustomTypeScheme(
117
+ agency=(
118
+ cts.agency.id if isinstance(cts.agency, Agency) else cts.agency
119
+ ),
120
+ id=cts.id,
121
+ name=cts.name,
122
+ version=cts.version,
123
+ isExternalReference=cts.is_external_reference,
124
+ validFrom=cts.valid_from,
125
+ validTo=cts.valid_to,
126
+ description=cts.description,
127
+ annotations=tuple(
128
+ [JsonAnnotation.from_model(a) for a in cts.annotations]
129
+ ),
130
+ isPartial=cts.is_partial,
131
+ vtlVersion=cts.vtl_version, # type: ignore[arg-type]
132
+ customTypes=tuple(
133
+ [JsonCustomType.from_model(i) for i in cts.items]
134
+ ),
135
+ )
83
136
 
84
- class JsonNamePersonalisation(NameableType, frozen=True):
137
+
138
+ class JsonNamePersonalisation(NameableType, frozen=True, omit_defaults=True):
85
139
  """SDMX-JSON payload for name personalisations."""
86
140
 
87
141
  vtlDefaultName: str = ""
@@ -100,8 +154,31 @@ class JsonNamePersonalisation(NameableType, frozen=True):
100
154
  annotations=[a.to_model() for a in self.annotations],
101
155
  )
102
156
 
157
+ @classmethod
158
+ def from_model(cls, np: NamePersonalisation) -> "JsonNamePersonalisation":
159
+ """Converts a pysdmx name personalisation to an SDMX-JSON one."""
160
+ if not np.name:
161
+ raise errors.Invalid(
162
+ "Invalid input",
163
+ "SDMX-JSON name personalisations must have a name",
164
+ {"name_personalisation": np.id},
165
+ )
166
+ return JsonNamePersonalisation(
167
+ id=np.id,
168
+ name=np.name,
169
+ description=np.description,
170
+ annotations=tuple(
171
+ [JsonAnnotation.from_model(a) for a in np.annotations]
172
+ ),
173
+ vtlDefaultName=np.vtl_default_name,
174
+ personalisedName=np.personalised_name,
175
+ vtlArtefact=np.vtl_artefact,
176
+ )
177
+
103
178
 
104
- class JsonNamePersonalisationScheme(ItemSchemeType, frozen=True):
179
+ class JsonNamePersonalisationScheme(
180
+ ItemSchemeType, frozen=True, omit_defaults=True
181
+ ):
105
182
  """SDMX-JSON payload for name personalisation schemes."""
106
183
 
107
184
  vtlVersion: str = ""
@@ -125,8 +202,40 @@ class JsonNamePersonalisationScheme(ItemSchemeType, frozen=True):
125
202
  annotations=[a.to_model() for a in self.annotations],
126
203
  )
127
204
 
205
+ @classmethod
206
+ def from_model(
207
+ cls, nps: NamePersonalisationScheme
208
+ ) -> "JsonNamePersonalisationScheme":
209
+ """Converts a pysdmx name personalisation scheme to SDMX-JSON."""
210
+ if not nps.name:
211
+ raise errors.Invalid(
212
+ "Invalid input",
213
+ "SDMX-JSON name personalisation schemes must have a name",
214
+ {"name_personalisation_scheme": nps.id},
215
+ )
216
+ return JsonNamePersonalisationScheme(
217
+ agency=(
218
+ nps.agency.id if isinstance(nps.agency, Agency) else nps.agency
219
+ ),
220
+ id=nps.id,
221
+ name=nps.name,
222
+ version=nps.version,
223
+ isExternalReference=nps.is_external_reference,
224
+ validFrom=nps.valid_from,
225
+ validTo=nps.valid_to,
226
+ description=nps.description,
227
+ annotations=tuple(
228
+ [JsonAnnotation.from_model(a) for a in nps.annotations]
229
+ ),
230
+ isPartial=nps.is_partial,
231
+ vtlVersion=nps.vtl_version, # type: ignore[arg-type]
232
+ namePersonalisations=tuple(
233
+ [JsonNamePersonalisation.from_model(i) for i in nps.items]
234
+ ),
235
+ )
128
236
 
129
- class JsonUserDefinedOperator(NameableType, frozen=True):
237
+
238
+ class JsonUserDefinedOperator(NameableType, frozen=True, omit_defaults=True):
130
239
  """SDMX-JSON payload for user defined operator."""
131
240
 
132
241
  operatorDefinition: str = ""
@@ -141,8 +250,29 @@ class JsonUserDefinedOperator(NameableType, frozen=True):
141
250
  annotations=[a.to_model() for a in self.annotations],
142
251
  )
143
252
 
253
+ @classmethod
254
+ def from_model(cls, udo: UserDefinedOperator) -> "JsonUserDefinedOperator":
255
+ """Converts a pysdmx user defined operator to an SDMX-JSON one."""
256
+ if not udo.name:
257
+ raise errors.Invalid(
258
+ "Invalid input",
259
+ "SDMX-JSON user defined operators must have a name",
260
+ {"user_defined_operator": udo.id},
261
+ )
262
+ return JsonUserDefinedOperator(
263
+ id=udo.id,
264
+ name=udo.name,
265
+ description=udo.description,
266
+ annotations=tuple(
267
+ [JsonAnnotation.from_model(a) for a in udo.annotations]
268
+ ),
269
+ operatorDefinition=udo.operator_definition,
270
+ )
271
+
144
272
 
145
- class JsonUserDefinedOperatorScheme(ItemSchemeType, frozen=True):
273
+ class JsonUserDefinedOperatorScheme(
274
+ ItemSchemeType, frozen=True, omit_defaults=True
275
+ ):
146
276
  """SDMX-JSON payload for user defined operator schemes."""
147
277
 
148
278
  vtlVersion: str = ""
@@ -170,8 +300,78 @@ class JsonUserDefinedOperatorScheme(ItemSchemeType, frozen=True):
170
300
  annotations=[a.to_model() for a in self.annotations],
171
301
  )
172
302
 
303
+ @classmethod
304
+ def from_model(
305
+ cls, udos: UserDefinedOperatorScheme
306
+ ) -> "JsonUserDefinedOperatorScheme":
307
+ """Converts a pysdmx user defined operator scheme to SDMX-JSON."""
308
+ if not udos.name:
309
+ raise errors.Invalid(
310
+ "Invalid input",
311
+ "SDMX-JSON user defined operator schemes must have a name",
312
+ {"user_defined_operator_scheme": udos.id},
313
+ )
173
314
 
174
- class JsonRuleset(Struct, frozen=True):
315
+ # Convert vtl_mapping_scheme to URN string
316
+ vtl_mapping_ref = None
317
+ if udos.vtl_mapping_scheme:
318
+ if isinstance(udos.vtl_mapping_scheme, str):
319
+ vtl_mapping_ref = udos.vtl_mapping_scheme
320
+ else:
321
+ # Handle both VtlMappingScheme objects and References
322
+ agency = (
323
+ udos.vtl_mapping_scheme.agency.id
324
+ if hasattr(udos.vtl_mapping_scheme.agency, "id")
325
+ else udos.vtl_mapping_scheme.agency
326
+ )
327
+ vtl_mapping_ref = (
328
+ f"urn:sdmx:org.sdmx.infomodel.transformation.VtlMappingScheme="
329
+ f"{agency}:{udos.vtl_mapping_scheme.id}"
330
+ f"({udos.vtl_mapping_scheme.version})"
331
+ )
332
+
333
+ # Convert ruleset_schemes to URN strings
334
+ ruleset_refs = []
335
+ for rs in udos.ruleset_schemes:
336
+ if isinstance(rs, str):
337
+ ruleset_refs.append(rs)
338
+ else:
339
+ # Handle both RulesetScheme objects and References
340
+ agency = (
341
+ rs.agency.id if hasattr(rs.agency, "id") else rs.agency
342
+ )
343
+ ruleset_refs.append(
344
+ f"urn:sdmx:org.sdmx.infomodel.transformation.RulesetScheme="
345
+ f"{agency}:{rs.id}({rs.version})"
346
+ )
347
+
348
+ return JsonUserDefinedOperatorScheme(
349
+ agency=(
350
+ udos.agency.id
351
+ if isinstance(udos.agency, Agency)
352
+ else udos.agency
353
+ ),
354
+ id=udos.id,
355
+ name=udos.name,
356
+ version=udos.version,
357
+ isExternalReference=udos.is_external_reference,
358
+ validFrom=udos.valid_from,
359
+ validTo=udos.valid_to,
360
+ description=udos.description,
361
+ annotations=tuple(
362
+ [JsonAnnotation.from_model(a) for a in udos.annotations]
363
+ ),
364
+ isPartial=udos.is_partial,
365
+ vtlVersion=udos.vtl_version, # type: ignore[arg-type]
366
+ vtlMappingScheme=vtl_mapping_ref,
367
+ rulesetSchemes=ruleset_refs,
368
+ userDefinedOperators=tuple(
369
+ [JsonUserDefinedOperator.from_model(i) for i in udos.items]
370
+ ),
371
+ )
372
+
373
+
374
+ class JsonRuleset(Struct, frozen=True, omit_defaults=True):
175
375
  """SDMX-JSON payload for rulesets."""
176
376
 
177
377
  id: str
@@ -194,8 +394,41 @@ class JsonRuleset(Struct, frozen=True):
194
394
  annotations=[a.to_model() for a in self.annotations],
195
395
  )
196
396
 
397
+ @classmethod
398
+ def from_model(cls, ruleset: Ruleset) -> "JsonRuleset":
399
+ """Converts a pysdmx ruleset to an SDMX-JSON one."""
400
+ if not ruleset.name:
401
+ raise errors.Invalid(
402
+ "Invalid input",
403
+ "SDMX-JSON rulesets must have a name",
404
+ {"ruleset": ruleset.id},
405
+ )
406
+ if not ruleset.ruleset_type:
407
+ raise errors.Invalid(
408
+ "Invalid input",
409
+ "SDMX-JSON rulesets must have a ruleset type",
410
+ {"ruleset": ruleset.id},
411
+ )
412
+ if not ruleset.ruleset_scope:
413
+ raise errors.Invalid(
414
+ "Invalid input",
415
+ "SDMX-JSON rulesets must have a ruleset scope",
416
+ {"ruleset": ruleset.id},
417
+ )
418
+ return JsonRuleset(
419
+ id=ruleset.id,
420
+ name=ruleset.name,
421
+ description=ruleset.description,
422
+ annotations=tuple(
423
+ [JsonAnnotation.from_model(a) for a in ruleset.annotations]
424
+ ),
425
+ rulesetDefinition=ruleset.ruleset_definition,
426
+ rulesetType=ruleset.ruleset_type,
427
+ rulesetScope=ruleset.ruleset_scope,
428
+ )
429
+
197
430
 
198
- class JsonRulesetScheme(ItemSchemeType, frozen=True):
431
+ class JsonRulesetScheme(ItemSchemeType, frozen=True, omit_defaults=True):
199
432
  """SDMX-JSON payload for ruleset schemes."""
200
433
 
201
434
  vtlVersion: str = ""
@@ -221,8 +454,56 @@ class JsonRulesetScheme(ItemSchemeType, frozen=True):
221
454
  annotations=[a.to_model() for a in self.annotations],
222
455
  )
223
456
 
457
+ @classmethod
458
+ def from_model(cls, rss: RulesetScheme) -> "JsonRulesetScheme":
459
+ """Converts a pysdmx ruleset scheme to an SDMX-JSON one."""
460
+ if not rss.name:
461
+ raise errors.Invalid(
462
+ "Invalid input",
463
+ "SDMX-JSON ruleset schemes must have a name",
464
+ {"ruleset_scheme": rss.id},
465
+ )
466
+
467
+ # Convert vtl_mapping_scheme to URN string
468
+ vtl_mapping_ref = None
469
+ if rss.vtl_mapping_scheme:
470
+ if isinstance(rss.vtl_mapping_scheme, str):
471
+ vtl_mapping_ref = rss.vtl_mapping_scheme
472
+ else:
473
+ # Handle both VtlMappingScheme objects and References
474
+ agency = (
475
+ rss.vtl_mapping_scheme.agency.id
476
+ if hasattr(rss.vtl_mapping_scheme.agency, "id")
477
+ else rss.vtl_mapping_scheme.agency
478
+ )
479
+ vtl_mapping_ref = (
480
+ f"urn:sdmx:org.sdmx.infomodel.transformation.VtlMappingScheme="
481
+ f"{agency}:{rss.vtl_mapping_scheme.id}"
482
+ f"({rss.vtl_mapping_scheme.version})"
483
+ )
484
+
485
+ return JsonRulesetScheme(
486
+ agency=(
487
+ rss.agency.id if isinstance(rss.agency, Agency) else rss.agency
488
+ ),
489
+ id=rss.id,
490
+ name=rss.name,
491
+ version=rss.version,
492
+ isExternalReference=rss.is_external_reference,
493
+ validFrom=rss.valid_from,
494
+ validTo=rss.valid_to,
495
+ description=rss.description,
496
+ annotations=tuple(
497
+ [JsonAnnotation.from_model(a) for a in rss.annotations]
498
+ ),
499
+ isPartial=rss.is_partial,
500
+ vtlVersion=rss.vtl_version, # type: ignore[arg-type]
501
+ vtlMappingScheme=vtl_mapping_ref,
502
+ rulesets=tuple([JsonRuleset.from_model(i) for i in rss.items]),
503
+ )
504
+
224
505
 
225
- class JsonToVtlMapping(Struct, frozen=True):
506
+ class JsonToVtlMapping(Struct, frozen=True, omit_defaults=True):
226
507
  """SDMX-JSON payload for To VTL mappings."""
227
508
 
228
509
  toVtlSubSpace: Sequence[str]
@@ -232,8 +513,13 @@ class JsonToVtlMapping(Struct, frozen=True):
232
513
  """Converts deserialized class to pysdmx model class."""
233
514
  return ToVtlMapping(self.toVtlSubSpace, self.type)
234
515
 
516
+ @classmethod
517
+ def from_model(cls, mapping: ToVtlMapping) -> "JsonToVtlMapping":
518
+ """Converts a pysdmx "to VTL" mapping to an SDMX-JSON one."""
519
+ return JsonToVtlMapping(mapping.to_vtl_sub_space, mapping.method)
520
+
235
521
 
236
- class JsonFromVtlMapping(Struct, frozen=True):
522
+ class JsonFromVtlMapping(Struct, frozen=True, omit_defaults=True):
237
523
  """SDMX-JSON payload for from VTL mappings."""
238
524
 
239
525
  fromVtlSuperSpace: Sequence[str]
@@ -243,8 +529,13 @@ class JsonFromVtlMapping(Struct, frozen=True):
243
529
  """Converts deserialized class to pysdmx model class."""
244
530
  return FromVtlMapping(self.fromVtlSuperSpace, self.type)
245
531
 
532
+ @classmethod
533
+ def from_model(cls, mapping: FromVtlMapping) -> "JsonFromVtlMapping":
534
+ """Converts a pysdmx "from VTL" mapping to an SDMX-JSON one."""
535
+ return JsonFromVtlMapping(mapping.from_vtl_sub_space, mapping.method)
246
536
 
247
- class JsonVtlMapping(NameableType, frozen=True):
537
+
538
+ class JsonVtlMapping(NameableType, frozen=True, omit_defaults=True):
248
539
  """SDMX-JSON payload for VTL mappings."""
249
540
 
250
541
  alias: str = ""
@@ -301,8 +592,107 @@ class JsonVtlMapping(NameableType, frozen=True):
301
592
  annotations=[a.to_model() for a in self.annotations],
302
593
  )
303
594
 
595
+ @classmethod
596
+ def from_model(cls, mapping: VtlMapping) -> "JsonVtlMapping":
597
+ """Converts a pysdmx VTL mapping to an SDMX-JSON one."""
598
+ if not mapping.name:
599
+ raise errors.Invalid(
600
+ "Invalid input",
601
+ "SDMX-JSON VTL mappings must have a name",
602
+ {"vtl_mapping": mapping.id},
603
+ )
304
604
 
305
- class JsonVtlMappingScheme(ItemSchemeType, frozen=True):
605
+ if isinstance(mapping, VtlCodelistMapping):
606
+ # Convert codelist to URN string
607
+ if not isinstance(mapping.codelist, str):
608
+ # Handle both Codelist objects and References
609
+ agency = (
610
+ mapping.codelist.agency.id
611
+ if hasattr(mapping.codelist.agency, "id")
612
+ else mapping.codelist.agency
613
+ )
614
+ codelist_ref = (
615
+ f"urn:sdmx:org.sdmx.infomodel.codelist.Codelist="
616
+ f"{agency}:{mapping.codelist.id}({mapping.codelist.version})"
617
+ )
618
+ else:
619
+ codelist_ref = mapping.codelist
620
+
621
+ return JsonVtlMapping(
622
+ id=mapping.id,
623
+ name=mapping.name,
624
+ description=mapping.description,
625
+ annotations=tuple(
626
+ [JsonAnnotation.from_model(a) for a in mapping.annotations]
627
+ ),
628
+ alias=mapping.codelist_alias,
629
+ codelist=codelist_ref,
630
+ )
631
+ elif isinstance(mapping, VtlConceptMapping):
632
+ # Convert concept to URN string
633
+ if isinstance(mapping.concept, str):
634
+ concept_ref = mapping.concept
635
+ elif isinstance(mapping.concept, ItemReference):
636
+ concept_ref = (
637
+ "urn:sdmx:org.sdmx.infomodel.conceptscheme."
638
+ f"{mapping.concept.sdmx_type}={mapping.concept.agency}:"
639
+ f"{mapping.concept.id}({mapping.concept.version})."
640
+ f"{mapping.concept.item_id}"
641
+ )
642
+ elif mapping.concept.urn:
643
+ concept_ref = mapping.concept.urn
644
+ else:
645
+ raise errors.Invalid(
646
+ "Missing concept reference",
647
+ (
648
+ "The full reference to the concept is missing but "
649
+ "this is mandatory in a VtlConceptMapping"
650
+ ),
651
+ {"referenced_concept": mapping.concept},
652
+ )
653
+
654
+ return JsonVtlMapping(
655
+ id=mapping.id,
656
+ name=mapping.name,
657
+ description=mapping.description,
658
+ annotations=tuple(
659
+ [JsonAnnotation.from_model(a) for a in mapping.annotations]
660
+ ),
661
+ alias=mapping.concept_alias,
662
+ concept=concept_ref,
663
+ )
664
+ elif isinstance(mapping, VtlDataflowMapping):
665
+ return JsonVtlMapping(
666
+ id=mapping.id,
667
+ name=mapping.name,
668
+ description=mapping.description,
669
+ annotations=tuple(
670
+ [JsonAnnotation.from_model(a) for a in mapping.annotations]
671
+ ),
672
+ alias=mapping.dataflow_alias,
673
+ dataflow=f"urn:sdmx:org.sdmx.infomodel.datastructure.Dataflow={mapping.dataflow.agency}:{mapping.dataflow.id}({mapping.dataflow.version})",
674
+ toVtlMapping=(
675
+ JsonToVtlMapping.from_model(mapping.to_vtl_mapping_method)
676
+ if mapping.to_vtl_mapping_method
677
+ else None
678
+ ),
679
+ fromVtlMapping=(
680
+ JsonFromVtlMapping.from_model(
681
+ mapping.from_vtl_mapping_method
682
+ )
683
+ if mapping.from_vtl_mapping_method
684
+ else None
685
+ ),
686
+ )
687
+ else:
688
+ raise errors.Invalid(
689
+ "Invalid input",
690
+ f"Unsupported VTL mapping type: {type(mapping)}",
691
+ {"vtl_mapping": mapping.id},
692
+ )
693
+
694
+
695
+ class JsonVtlMappingScheme(ItemSchemeType, frozen=True, omit_defaults=True):
306
696
  """SDMX-JSON payload for VTL mapping schemes."""
307
697
 
308
698
  vtlMappings: Sequence[JsonVtlMapping] = ()
@@ -325,8 +715,40 @@ class JsonVtlMappingScheme(ItemSchemeType, frozen=True):
325
715
  annotations=[a.to_model() for a in self.annotations],
326
716
  )
327
717
 
718
+ @classmethod
719
+ def from_model(cls, vms: VtlMappingScheme) -> "JsonVtlMappingScheme":
720
+ """Converts a pysdmx VTL mapping scheme to an SDMX-JSON one."""
721
+ if not vms.name:
722
+ raise errors.Invalid(
723
+ "Invalid input",
724
+ "SDMX-JSON VTL mapping schemes must have a name",
725
+ {"vtl_mapping_scheme": vms.id},
726
+ )
727
+ return JsonVtlMappingScheme(
728
+ agency=(
729
+ vms.agency.id if isinstance(vms.agency, Agency) else vms.agency
730
+ ),
731
+ id=vms.id,
732
+ name=vms.name,
733
+ version=vms.version,
734
+ isExternalReference=vms.is_external_reference,
735
+ validFrom=vms.valid_from,
736
+ validTo=vms.valid_to,
737
+ description=vms.description,
738
+ annotations=tuple(
739
+ [JsonAnnotation.from_model(a) for a in vms.annotations]
740
+ ),
741
+ isPartial=vms.is_partial,
742
+ vtlMappings=tuple(
743
+ [
744
+ JsonVtlMapping.from_model(i) # type: ignore[arg-type]
745
+ for i in vms.items
746
+ ]
747
+ ),
748
+ )
328
749
 
329
- class JsonTransformation(Struct, frozen=True):
750
+
751
+ class JsonTransformation(Struct, frozen=True, omit_defaults=True):
330
752
  """SDMX-JSON payload for VTL transformations."""
331
753
 
332
754
  id: str
@@ -349,8 +771,36 @@ class JsonTransformation(Struct, frozen=True):
349
771
  annotations=[a.to_model() for a in self.annotations],
350
772
  )
351
773
 
774
+ @classmethod
775
+ def from_model(
776
+ cls, transformation: Transformation
777
+ ) -> "JsonTransformation":
778
+ """Converts a pysdmx transformation to an SDMX-JSON one."""
779
+ if not transformation.name:
780
+ raise errors.Invalid(
781
+ "Invalid input",
782
+ "SDMX-JSON transformations must have a name",
783
+ {"transformation": transformation.id},
784
+ )
785
+ return JsonTransformation(
786
+ id=transformation.id,
787
+ name=transformation.name,
788
+ expression=transformation.expression,
789
+ result=transformation.result,
790
+ isPersistent=transformation.is_persistent,
791
+ description=transformation.description,
792
+ annotations=tuple(
793
+ [
794
+ JsonAnnotation.from_model(a)
795
+ for a in transformation.annotations
796
+ ]
797
+ ),
798
+ )
799
+
352
800
 
353
- class JsonTransformationScheme(ItemSchemeType, frozen=True):
801
+ class JsonTransformationScheme(
802
+ ItemSchemeType, frozen=True, omit_defaults=True
803
+ ):
354
804
  """SDMX-JSON payload for VTL transformation schemes."""
355
805
 
356
806
  vtlVersion: str = ""
@@ -396,8 +846,88 @@ class JsonTransformationScheme(ItemSchemeType, frozen=True):
396
846
  annotations=[a.to_model() for a in self.annotations],
397
847
  )
398
848
 
849
+ @classmethod
850
+ def from_model(
851
+ cls, ts: TransformationScheme
852
+ ) -> "JsonTransformationScheme":
853
+ """Converts a pysdmx transformation scheme to an SDMX-JSON one."""
854
+ if not ts.name:
855
+ raise errors.Invalid(
856
+ "Invalid input",
857
+ "SDMX-JSON transformation schemes must have a name",
858
+ {"transformation_scheme": ts.id},
859
+ )
860
+
861
+ # Convert scheme references to strings
862
+ mapping_ref = None
863
+ if ts.vtl_mapping_scheme:
864
+ mapping_ref = (
865
+ f"urn:sdmx:org.sdmx.infomodel.transformation.VtlMappingScheme="
866
+ f"{ts.vtl_mapping_scheme.agency}:{ts.vtl_mapping_scheme.id}"
867
+ f"({ts.vtl_mapping_scheme.version})"
868
+ )
869
+
870
+ np_ref = None
871
+ if ts.name_personalisation_scheme:
872
+ np_ref = (
873
+ f"urn:sdmx:org.sdmx.infomodel.transformation.NamePersonalisationScheme="
874
+ f"{ts.name_personalisation_scheme.agency}:"
875
+ f"{ts.name_personalisation_scheme.id}"
876
+ f"({ts.name_personalisation_scheme.version})"
877
+ )
878
+
879
+ ct_ref = None
880
+ if ts.custom_type_scheme:
881
+ ct_ref = (
882
+ f"urn:sdmx:org.sdmx.infomodel.transformation.CustomTypeScheme="
883
+ f"{ts.custom_type_scheme.agency}:{ts.custom_type_scheme.id}"
884
+ f"({ts.custom_type_scheme.version})"
885
+ )
886
+
887
+ rs_refs = tuple(
888
+ [
889
+ f"urn:sdmx:org.sdmx.infomodel.transformation.RulesetScheme="
890
+ f"{rs.agency}:{rs.id}({rs.version})"
891
+ for rs in ts.ruleset_schemes
892
+ ]
893
+ )
894
+
895
+ udo_refs = tuple(
896
+ [
897
+ f"urn:sdmx:org.sdmx.infomodel.transformation.UserDefinedOperatorScheme="
898
+ f"{udos.agency}:{udos.id}({udos.version})"
899
+ for udos in ts.user_defined_operator_schemes
900
+ ]
901
+ )
902
+
903
+ return JsonTransformationScheme(
904
+ agency=(
905
+ ts.agency.id if isinstance(ts.agency, Agency) else ts.agency
906
+ ),
907
+ id=ts.id,
908
+ name=ts.name,
909
+ version=ts.version,
910
+ isExternalReference=ts.is_external_reference,
911
+ validFrom=ts.valid_from,
912
+ validTo=ts.valid_to,
913
+ description=ts.description,
914
+ annotations=tuple(
915
+ [JsonAnnotation.from_model(a) for a in ts.annotations]
916
+ ),
917
+ isPartial=ts.is_partial,
918
+ vtlVersion=ts.vtl_version, # type: ignore[arg-type]
919
+ vtlMappingScheme=mapping_ref,
920
+ namePersonalisationScheme=np_ref,
921
+ customTypeScheme=ct_ref,
922
+ rulesetSchemes=rs_refs,
923
+ userDefinedOperatorSchemes=udo_refs,
924
+ transformations=tuple(
925
+ [JsonTransformation.from_model(i) for i in ts.items]
926
+ ),
927
+ )
928
+
399
929
 
400
- class JsonVtlTransformations(Struct, frozen=True):
930
+ class JsonVtlTransformations(Struct, frozen=True, omit_defaults=True):
401
931
  """SDMX-JSON payload for VTL transformation schemes."""
402
932
 
403
933
  transformationSchemes: Sequence[JsonTransformationScheme]
@@ -418,7 +948,7 @@ class JsonVtlTransformations(Struct, frozen=True):
418
948
  )
419
949
 
420
950
 
421
- class JsonVtlTransformationsMessage(Struct, frozen=True):
951
+ class JsonVtlTransformationsMessage(Struct, frozen=True, omit_defaults=True):
422
952
  """SDMX-JSON payload for /transformationscheme queries."""
423
953
 
424
954
  data: JsonVtlTransformations