pysdmx 1.5.2__py3-none-any.whl → 1.7.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 (63) 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 +20 -6
  18. pysdmx/io/json/fusion/messages/code.py +21 -4
  19. pysdmx/io/json/fusion/messages/concept.py +10 -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 +265 -22
  26. pysdmx/io/json/sdmxjson2/messages/concept.py +75 -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/__ss_aux_reader.py +1 -2
  47. pysdmx/io/xml/__structure_aux_reader.py +15 -10
  48. pysdmx/io/xml/__structure_aux_writer.py +15 -13
  49. pysdmx/io/xml/__write_data_aux.py +6 -57
  50. pysdmx/io/xml/__write_structure_specific_aux.py +7 -3
  51. pysdmx/io/xml/doc_validation.py +1 -3
  52. pysdmx/io/xml/sdmx21/writer/generic.py +6 -4
  53. pysdmx/model/__init__.py +1 -3
  54. pysdmx/model/code.py +11 -1
  55. pysdmx/model/dataflow.py +23 -0
  56. pysdmx/model/map.py +19 -13
  57. pysdmx/model/message.py +10 -5
  58. pysdmx/toolkit/pd/_data_utils.py +99 -0
  59. pysdmx/toolkit/vtl/_validations.py +2 -3
  60. {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info}/METADATA +4 -3
  61. {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info}/RECORD +63 -51
  62. {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info}/WHEEL +1 -1
  63. {pysdmx-1.5.2.dist-info → pysdmx-1.7.0.dist-info/licenses}/LICENSE +0 -0
@@ -1,9 +1,10 @@
1
1
  """Collection of SDMX-JSON schemas for SDMX-REST schema queries."""
2
2
 
3
- from typing import Dict, List, Optional, Sequence, Tuple
3
+ from typing import Dict, List, Literal, Optional, Sequence, Tuple
4
4
 
5
5
  from msgspec import Struct
6
6
 
7
+ from pysdmx import errors
7
8
  from pysdmx.io.json.sdmxjson2.messages.code import JsonCodelist, JsonValuelist
8
9
  from pysdmx.io.json.sdmxjson2.messages.concept import (
9
10
  JsonConcept,
@@ -11,10 +12,12 @@ from pysdmx.io.json.sdmxjson2.messages.concept import (
11
12
  )
12
13
  from pysdmx.io.json.sdmxjson2.messages.constraint import JsonDataConstraint
13
14
  from pysdmx.io.json.sdmxjson2.messages.core import (
15
+ JsonAnnotation,
14
16
  JsonRepresentation,
15
17
  MaintainableType,
16
18
  )
17
19
  from pysdmx.model import (
20
+ Agency,
18
21
  ArrayBoundaries,
19
22
  Codelist,
20
23
  Component,
@@ -23,8 +26,10 @@ from pysdmx.model import (
23
26
  DataStructureDefinition,
24
27
  DataType,
25
28
  Facets,
29
+ ItemReference,
26
30
  Role,
27
31
  )
32
+ from pysdmx.model.dataflow import Group
28
33
  from pysdmx.util import parse_item_urn
29
34
 
30
35
 
@@ -38,12 +43,13 @@ def _find_concept(cs: Sequence[JsonConceptScheme], urn: str) -> JsonConcept:
38
43
  return [c for c in f[0].concepts if c.id == r.item_id][0]
39
44
 
40
45
 
41
- def __get_type(repr_: JsonRepresentation) -> str:
46
+ def __get_type(repr_: JsonRepresentation) -> Optional[str]:
47
+ t: Optional[str] = None
42
48
  if repr_.enumerationFormat:
43
49
  t = repr_.enumerationFormat.dataType
44
50
  elif repr_.format:
45
51
  t = repr_.format.dataType
46
- else:
52
+ if not t:
47
53
  t = "String"
48
54
  return t
49
55
 
@@ -67,14 +73,44 @@ def _get_representation(
67
73
  return (dt, facets, codes, ab)
68
74
 
69
75
 
70
- class JsonGroup(Struct, frozen=True):
76
+ def _get_concept_reference(component: Component) -> str:
77
+ if isinstance(component.concept, ItemReference):
78
+ concept = (
79
+ "urn:sdmx:org.sdmx.infomodel.conceptscheme."
80
+ f"{str(component.concept)}"
81
+ )
82
+ elif component.concept.urn:
83
+ concept = component.concept.urn
84
+ else:
85
+ raise errors.Invalid(
86
+ "Missing concept reference",
87
+ (
88
+ "The full reference to the concept must be available "
89
+ "but could not be found. To have the full reference, "
90
+ "either the concept must be an ItemReference or a "
91
+ "concept object with a urn."
92
+ ),
93
+ )
94
+ return concept
95
+
96
+
97
+ def _get_json_representation(
98
+ comp: Component,
99
+ ) -> Optional[JsonRepresentation]:
100
+ enum = comp.local_enum_ref if comp.local_enum_ref else None
101
+ return JsonRepresentation.from_model(
102
+ comp.local_dtype, enum, comp.local_facets, comp.array_def
103
+ )
104
+
105
+
106
+ class JsonGroup(Struct, frozen=True, omit_defaults=True):
71
107
  """SDMX-JSON payload for a group."""
72
108
 
73
109
  id: str
74
110
  groupDimensions: Sequence[str]
75
111
 
76
112
 
77
- class JsonAttributeRelationship(Struct, frozen=True):
113
+ class JsonAttributeRelationship(Struct, frozen=True, omit_defaults=True):
78
114
  """SDMX-JSON payload for an attribute relationship."""
79
115
 
80
116
  dataflow: Optional[Dict] = None # type: ignore[type-arg]
@@ -96,8 +132,19 @@ class JsonAttributeRelationship(Struct, frozen=True):
96
132
  else:
97
133
  return "D"
98
134
 
135
+ @classmethod
136
+ def from_model(self, rel: str) -> "JsonAttributeRelationship":
137
+ """Converts a pysdmx attribute relationship to an SDMX-JSON one."""
138
+ if rel == "D":
139
+ return JsonAttributeRelationship(dataflow={})
140
+ elif rel == "O":
141
+ return JsonAttributeRelationship(observation={})
142
+ else:
143
+ dims = rel.split(",")
144
+ return JsonAttributeRelationship(dimensions=dims)
145
+
99
146
 
100
- class JsonDimension(Struct, frozen=True):
147
+ class JsonDimension(Struct, frozen=True, omit_defaults=True):
101
148
  """SDMX-JSON payload for a component."""
102
149
 
103
150
  id: str
@@ -123,6 +170,10 @@ class JsonDimension(Struct, frozen=True):
123
170
  dt, facets, codes, ab = _get_representation(
124
171
  self.id, self.localRepresentation, cls, cons
125
172
  )
173
+ if self.localRepresentation and self.localRepresentation.enumeration:
174
+ local_enum_ref = self.localRepresentation.enumeration
175
+ else:
176
+ local_enum_ref = None
126
177
  return Component(
127
178
  id=self.id,
128
179
  required=True,
@@ -134,10 +185,22 @@ class JsonDimension(Struct, frozen=True):
134
185
  description=desc,
135
186
  local_codes=codes,
136
187
  array_def=ab,
188
+ local_enum_ref=local_enum_ref,
137
189
  )
138
190
 
191
+ @classmethod
192
+ def from_model(self, dimension: Component) -> "JsonDimension":
193
+ """Converts a pysdmx dimension to an SDMX-JSON one."""
194
+ concept = _get_concept_reference(dimension)
195
+ repr = _get_json_representation(dimension)
196
+ return JsonDimension(
197
+ id=dimension.id,
198
+ conceptIdentity=concept,
199
+ localRepresentation=repr,
200
+ )
139
201
 
140
- class JsonAttribute(Struct, frozen=True):
202
+
203
+ class JsonAttribute(Struct, frozen=True, omit_defaults=True):
141
204
  """SDMX-JSON payload for an attribute."""
142
205
 
143
206
  id: str
@@ -171,6 +234,10 @@ class JsonAttribute(Struct, frozen=True):
171
234
  groups,
172
235
  self.measureRelationship,
173
236
  )
237
+ if self.localRepresentation and self.localRepresentation.enumeration:
238
+ local_enum_ref = self.localRepresentation.enumeration
239
+ else:
240
+ local_enum_ref = None
174
241
  return Component(
175
242
  id=self.id,
176
243
  required=req,
@@ -183,10 +250,28 @@ class JsonAttribute(Struct, frozen=True):
183
250
  local_codes=codes,
184
251
  attachment_level=lvl,
185
252
  array_def=ab,
253
+ local_enum_ref=local_enum_ref,
186
254
  )
187
255
 
256
+ @classmethod
257
+ def from_model(self, attribute: Component) -> "JsonAttribute":
258
+ """Converts a pysdmx attribute to an SDMX-JSON one."""
259
+ concept = _get_concept_reference(attribute)
260
+ usage = "mandatory" if attribute.required else "optional"
261
+ level = JsonAttributeRelationship.from_model(
262
+ attribute.attachment_level # type: ignore[arg-type]
263
+ )
264
+ repr = _get_json_representation(attribute)
265
+ return JsonAttribute(
266
+ id=attribute.id,
267
+ conceptIdentity=concept,
268
+ attributeRelationship=level,
269
+ usage=usage,
270
+ localRepresentation=repr,
271
+ )
188
272
 
189
- class JsonMeasure(Struct, frozen=True):
273
+
274
+ class JsonMeasure(Struct, frozen=True, omit_defaults=True):
190
275
  """SDMX-JSON payload for a measure."""
191
276
 
192
277
  id: str
@@ -213,6 +298,10 @@ class JsonMeasure(Struct, frozen=True):
213
298
  self.id, self.localRepresentation, cls, cons
214
299
  )
215
300
  req = self.usage != "optional"
301
+ if self.localRepresentation and self.localRepresentation.enumeration:
302
+ local_enum_ref = self.localRepresentation.enumeration
303
+ else:
304
+ local_enum_ref = None
216
305
  return Component(
217
306
  id=self.id,
218
307
  required=req,
@@ -224,12 +313,27 @@ class JsonMeasure(Struct, frozen=True):
224
313
  description=desc,
225
314
  local_codes=codes,
226
315
  array_def=ab,
316
+ local_enum_ref=local_enum_ref,
317
+ )
318
+
319
+ @classmethod
320
+ def from_model(self, measure: Component) -> "JsonMeasure":
321
+ """Converts a pysdmx measure to an SDMX-JSON one."""
322
+ concept = _get_concept_reference(measure)
323
+ usage = "mandatory" if measure.required else "optional"
324
+ repr = _get_json_representation(measure)
325
+ return JsonMeasure(
326
+ id=measure.id,
327
+ conceptIdentity=concept,
328
+ usage=usage,
329
+ localRepresentation=repr,
227
330
  )
228
331
 
229
332
 
230
- class JsonAttributes(Struct, frozen=True):
333
+ class JsonAttributes(Struct, frozen=True, omit_defaults=True):
231
334
  """SDMX-JSON payload for the list of attributes."""
232
335
 
336
+ id: Literal["AttributeDescriptor"] = "AttributeDescriptor"
233
337
  attributes: Sequence[JsonAttribute] = ()
234
338
 
235
339
  def to_model(
@@ -242,11 +346,24 @@ class JsonAttributes(Struct, frozen=True):
242
346
  """Returns the list of attributes."""
243
347
  return [a.to_model(cs, cls, cons, groups) for a in self.attributes]
244
348
 
349
+ @classmethod
350
+ def from_model(
351
+ self, attributes: Sequence[Component]
352
+ ) -> Optional["JsonAttributes"]:
353
+ """Converts a pysdmx list of attributes to an SDMX-JSON one."""
354
+ if len(attributes) > 0:
355
+ return JsonAttributes(
356
+ attributes=[JsonAttribute.from_model(a) for a in attributes]
357
+ )
358
+ else:
359
+ return None
360
+
245
361
 
246
- class JsonDimensions(Struct, frozen=True):
362
+ class JsonDimensions(Struct, frozen=True, omit_defaults=True):
247
363
  """SDMX-JSON payload for the list of dimensions."""
248
364
 
249
- dimensions: Sequence[JsonDimension]
365
+ id: Literal["DimensionDescriptor"] = "DimensionDescriptor"
366
+ dimensions: Sequence[JsonDimension] = ()
250
367
  timeDimension: Optional[JsonDimension] = None
251
368
 
252
369
  def to_model(
@@ -262,11 +379,29 @@ class JsonDimensions(Struct, frozen=True):
262
379
  dims.append(self.timeDimension.to_model(cs, cls, cons))
263
380
  return dims
264
381
 
382
+ @classmethod
383
+ def from_model(
384
+ self,
385
+ dimensions: Sequence[Component],
386
+ ) -> "JsonDimensions":
387
+ """Converts a pysdmx list of dimensions to an SDMX-JSON one."""
388
+ td = [d for d in dimensions if d.id == "TIME_PERIOD"]
389
+ ftd = None if len(td) == 0 else JsonDimension.from_model(td[0])
390
+ return JsonDimensions(
391
+ dimensions=[
392
+ JsonDimension.from_model(d)
393
+ for d in dimensions
394
+ if d.id != "TIME_PERIOD"
395
+ ],
396
+ timeDimension=ftd,
397
+ )
265
398
 
266
- class JsonMeasures(Struct, frozen=True):
399
+
400
+ class JsonMeasures(Struct, frozen=True, omit_defaults=True):
267
401
  """SDMX-JSON payload for the list of measures."""
268
402
 
269
- measures: Sequence[JsonMeasure]
403
+ id: Literal["MeasureDescriptor"] = "MeasureDescriptor"
404
+ measures: Sequence[JsonMeasure] = ()
270
405
 
271
406
  def to_model(
272
407
  self,
@@ -277,8 +412,20 @@ class JsonMeasures(Struct, frozen=True):
277
412
  """Returns the list of measures."""
278
413
  return [m.to_model(cs, cls, cons) for m in self.measures]
279
414
 
415
+ @classmethod
416
+ def from_model(
417
+ self, measures: Sequence[Component]
418
+ ) -> Optional["JsonMeasures"]:
419
+ """Converts a pysdmx list of measures to an SDMX-JSON one."""
420
+ if len(measures) > 0:
421
+ return JsonMeasures(
422
+ measures=[JsonMeasure.from_model(m) for m in measures]
423
+ )
424
+ else:
425
+ return None
426
+
280
427
 
281
- class JsonComponents(Struct, frozen=True):
428
+ class JsonComponents(Struct, frozen=True, omit_defaults=True):
282
429
  """SDMX-JSON payload for the list of DSD components."""
283
430
 
284
431
  dimensionList: JsonDimensions
@@ -292,7 +439,7 @@ class JsonComponents(Struct, frozen=True):
292
439
  cls: Sequence[JsonCodelist],
293
440
  vls: Sequence[JsonValuelist],
294
441
  constraints: Sequence[JsonDataConstraint],
295
- ) -> Components:
442
+ ) -> Tuple[Components, Sequence[Group]]:
296
443
  """Returns the components for this DSD."""
297
444
  enums = [cl.to_model() for cl in cls]
298
445
  enums.extend([vl.to_model() for vl in vls])
@@ -313,10 +460,27 @@ class JsonComponents(Struct, frozen=True):
313
460
  self.groups,
314
461
  )
315
462
  )
316
- return Components(comps)
463
+ mapped_grps = [
464
+ Group(g.id, dimensions=g.groupDimensions) for g in self.groups
465
+ ]
466
+ return (Components(comps), mapped_grps)
467
+
468
+ @classmethod
469
+ def from_model(
470
+ self, components: Components, grps: Optional[Sequence[Group]]
471
+ ) -> "JsonComponents":
472
+ """Converts a pysdmx components list to an SDMX-JSON one."""
473
+ dimensions = JsonDimensions.from_model(components.dimensions)
474
+ attributes = JsonAttributes.from_model(components.attributes)
475
+ measures = JsonMeasures.from_model(components.measures)
476
+ if grps is None:
477
+ groups = []
478
+ else:
479
+ groups = [JsonGroup(g.id, g.dimensions) for g in grps]
480
+ return JsonComponents(dimensions, measures, attributes, groups)
317
481
 
318
482
 
319
- class JsonDataStructure(MaintainableType, frozen=True):
483
+ class JsonDataStructure(MaintainableType, frozen=True, omit_defaults=True):
320
484
  """SDMX-JSON payload for a DSD."""
321
485
 
322
486
  dataStructureComponents: Optional[JsonComponents] = None
@@ -330,7 +494,7 @@ class JsonDataStructure(MaintainableType, frozen=True):
330
494
  constraints: Sequence[JsonDataConstraint],
331
495
  ) -> DataStructureDefinition:
332
496
  """Map to pysdmx model class."""
333
- c = self.dataStructureComponents.to_model( # type: ignore[union-attr]
497
+ c, grps = self.dataStructureComponents.to_model( # type: ignore[union-attr]
334
498
  cs,
335
499
  cls,
336
500
  vls,
@@ -348,10 +512,41 @@ class JsonDataStructure(MaintainableType, frozen=True):
348
512
  valid_to=self.validTo,
349
513
  components=c,
350
514
  evolving_structure=self.evolvingStructure,
515
+ groups=grps,
516
+ )
517
+
518
+ @classmethod
519
+ def from_model(self, dsd: DataStructureDefinition) -> "JsonDataStructure":
520
+ """Converts a pysdmx dsd to an SDMX-JSON one."""
521
+ if not dsd.name:
522
+ raise errors.Invalid(
523
+ "Invalid input",
524
+ "SDMX-JSON data structures must have a name",
525
+ {"data_structure": dsd.id},
526
+ )
527
+
528
+ return JsonDataStructure(
529
+ agency=(
530
+ dsd.agency.id if isinstance(dsd.agency, Agency) else dsd.agency
531
+ ),
532
+ id=dsd.id,
533
+ name=dsd.name,
534
+ version=dsd.version,
535
+ isExternalReference=dsd.is_external_reference,
536
+ validFrom=dsd.valid_from,
537
+ validTo=dsd.valid_to,
538
+ description=dsd.description,
539
+ annotations=tuple(
540
+ [JsonAnnotation.from_model(a) for a in dsd.annotations]
541
+ ),
542
+ dataStructureComponents=JsonComponents.from_model(
543
+ dsd.components, dsd.groups
544
+ ),
545
+ evolvingStructure=dsd.evolving_structure,
351
546
  )
352
547
 
353
548
 
354
- class JsonDataStructures(Struct, frozen=True):
549
+ class JsonDataStructures(Struct, frozen=True, omit_defaults=True):
355
550
  """SDMX-JSON payload for data structures."""
356
551
 
357
552
  dataStructures: Sequence[JsonDataStructure]
@@ -373,7 +568,7 @@ class JsonDataStructures(Struct, frozen=True):
373
568
  ]
374
569
 
375
570
 
376
- class JsonDataStructuresMessage(Struct, frozen=True):
571
+ class JsonDataStructuresMessage(Struct, frozen=True, omit_defaults=True):
377
572
  """SDMX-JSON payload for /datastructure queries."""
378
573
 
379
574
  data: JsonDataStructures