pysdmx 1.3.0__py3-none-any.whl → 1.4.0rc1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. pysdmx/__extras_check.py +3 -2
  2. pysdmx/__init__.py +1 -1
  3. pysdmx/api/fmr/__init__.py +4 -4
  4. pysdmx/api/gds/__init__.py +328 -0
  5. pysdmx/api/qb/gds.py +153 -0
  6. pysdmx/api/qb/service.py +91 -3
  7. pysdmx/api/qb/structure.py +1 -0
  8. pysdmx/api/qb/util.py +1 -0
  9. pysdmx/io/__init__.py +2 -1
  10. pysdmx/io/csv/sdmx10/reader/__init__.py +4 -2
  11. pysdmx/io/csv/sdmx10/writer/__init__.py +15 -2
  12. pysdmx/io/csv/sdmx20/reader/__init__.py +5 -2
  13. pysdmx/io/csv/sdmx20/writer/__init__.py +13 -2
  14. pysdmx/io/format.py +4 -0
  15. pysdmx/io/input_processor.py +12 -3
  16. pysdmx/io/json/fusion/messages/core.py +2 -0
  17. pysdmx/io/json/fusion/messages/report.py +13 -7
  18. pysdmx/io/json/gds/messages/__init__.py +35 -0
  19. pysdmx/io/json/gds/messages/agencies.py +41 -0
  20. pysdmx/io/json/gds/messages/catalog.py +79 -0
  21. pysdmx/io/json/gds/messages/sdmx_api.py +23 -0
  22. pysdmx/io/json/gds/messages/services.py +49 -0
  23. pysdmx/io/json/gds/messages/urn_resolver.py +43 -0
  24. pysdmx/io/json/gds/reader/__init__.py +12 -0
  25. pysdmx/io/json/sdmxjson2/messages/__init__.py +12 -4
  26. pysdmx/io/json/sdmxjson2/messages/agency.py +72 -0
  27. pysdmx/io/json/sdmxjson2/messages/category.py +22 -29
  28. pysdmx/io/json/sdmxjson2/messages/code.py +68 -64
  29. pysdmx/io/json/sdmxjson2/messages/concept.py +9 -18
  30. pysdmx/io/json/sdmxjson2/messages/constraint.py +2 -13
  31. pysdmx/io/json/sdmxjson2/messages/core.py +113 -21
  32. pysdmx/io/json/sdmxjson2/messages/dataflow.py +51 -21
  33. pysdmx/io/json/sdmxjson2/messages/dsd.py +110 -36
  34. pysdmx/io/json/sdmxjson2/messages/map.py +61 -49
  35. pysdmx/io/json/sdmxjson2/messages/pa.py +9 -17
  36. pysdmx/io/json/sdmxjson2/messages/provider.py +88 -0
  37. pysdmx/io/json/sdmxjson2/messages/report.py +84 -14
  38. pysdmx/io/json/sdmxjson2/messages/schema.py +14 -5
  39. pysdmx/io/json/sdmxjson2/messages/structure.py +105 -36
  40. pysdmx/io/json/sdmxjson2/messages/vtl.py +42 -96
  41. pysdmx/io/pd.py +2 -9
  42. pysdmx/io/reader.py +72 -27
  43. pysdmx/io/serde.py +11 -0
  44. pysdmx/io/writer.py +134 -0
  45. pysdmx/io/xml/{sdmx21/reader/__data_aux.py → __data_aux.py} +9 -2
  46. pysdmx/io/xml/{sdmx21/reader/__parse_xml.py → __parse_xml.py} +30 -6
  47. pysdmx/io/xml/__ss_aux_reader.py +96 -0
  48. pysdmx/io/xml/__structure_aux_reader.py +1174 -0
  49. pysdmx/io/xml/__structure_aux_writer.py +1233 -0
  50. pysdmx/io/xml/{sdmx21/__tokens.py → __tokens.py} +33 -1
  51. pysdmx/io/xml/{sdmx21/writer/__write_aux.py → __write_aux.py} +129 -37
  52. pysdmx/io/xml/{sdmx21/writer/__write_data_aux.py → __write_data_aux.py} +1 -1
  53. pysdmx/io/xml/__write_structure_specific_aux.py +254 -0
  54. pysdmx/io/xml/{sdmx21/reader/doc_validation.py → doc_validation.py} +10 -2
  55. pysdmx/io/xml/{sdmx21/reader/header.py → header.py} +11 -3
  56. pysdmx/io/xml/sdmx21/reader/error.py +2 -2
  57. pysdmx/io/xml/sdmx21/reader/generic.py +12 -8
  58. pysdmx/io/xml/sdmx21/reader/structure.py +5 -840
  59. pysdmx/io/xml/sdmx21/reader/structure_specific.py +13 -97
  60. pysdmx/io/xml/sdmx21/reader/submission.py +2 -2
  61. pysdmx/io/xml/sdmx21/writer/error.py +1 -1
  62. pysdmx/io/xml/sdmx21/writer/generic.py +13 -7
  63. pysdmx/io/xml/sdmx21/writer/structure.py +16 -828
  64. pysdmx/io/xml/sdmx21/writer/structure_specific.py +13 -238
  65. pysdmx/io/xml/sdmx30/__init__.py +1 -0
  66. pysdmx/io/xml/sdmx30/reader/__init__.py +1 -0
  67. pysdmx/io/xml/sdmx30/reader/structure.py +39 -0
  68. pysdmx/io/xml/sdmx30/reader/structure_specific.py +39 -0
  69. pysdmx/io/xml/sdmx30/writer/__init__.py +1 -0
  70. pysdmx/io/xml/sdmx30/writer/structure.py +67 -0
  71. pysdmx/io/xml/sdmx30/writer/structure_specific.py +108 -0
  72. pysdmx/model/__base.py +99 -34
  73. pysdmx/model/__init__.py +4 -0
  74. pysdmx/model/category.py +20 -0
  75. pysdmx/model/code.py +29 -8
  76. pysdmx/model/concept.py +52 -11
  77. pysdmx/model/dataflow.py +117 -33
  78. pysdmx/model/dataset.py +66 -14
  79. pysdmx/model/gds.py +161 -0
  80. pysdmx/model/map.py +51 -8
  81. pysdmx/model/message.py +235 -55
  82. pysdmx/model/metadata.py +79 -16
  83. pysdmx/model/submission.py +12 -7
  84. pysdmx/model/vtl.py +30 -13
  85. pysdmx/toolkit/__init__.py +1 -1
  86. pysdmx/toolkit/pd/__init__.py +85 -0
  87. pysdmx/toolkit/vtl/__init__.py +2 -1
  88. pysdmx/toolkit/vtl/_validations.py +1 -1
  89. pysdmx/toolkit/vtl/{generate_vtl_script.py → script_generation.py} +30 -4
  90. pysdmx/toolkit/vtl/validation.py +119 -0
  91. pysdmx/util/_model_utils.py +1 -1
  92. pysdmx-1.4.0rc1.dist-info/METADATA +119 -0
  93. pysdmx-1.4.0rc1.dist-info/RECORD +140 -0
  94. pysdmx/io/json/sdmxjson2/messages/org.py +0 -140
  95. pysdmx/toolkit/vtl/model_validations.py +0 -50
  96. pysdmx-1.3.0.dist-info/METADATA +0 -76
  97. pysdmx-1.3.0.dist-info/RECORD +0 -116
  98. /pysdmx/io/xml/{sdmx21/writer/config.py → config.py} +0 -0
  99. {pysdmx-1.3.0.dist-info → pysdmx-1.4.0rc1.dist-info}/LICENSE +0 -0
  100. {pysdmx-1.3.0.dist-info → pysdmx-1.4.0rc1.dist-info}/WHEEL +0 -0
pysdmx/model/message.py CHANGED
@@ -19,22 +19,60 @@ from typing import Any, Dict, List, Optional, Sequence, Type, Union
19
19
  from msgspec import Struct
20
20
 
21
21
  from pysdmx.errors import Invalid, NotFound
22
- from pysdmx.model import (
23
- AgencyScheme,
22
+ from pysdmx.model.__base import ItemScheme, MaintainableArtefact, Organisation
23
+ from pysdmx.model.category import Categorisation, CategoryScheme
24
+ from pysdmx.model.code import Codelist, Hierarchy, HierarchyAssociation
25
+ from pysdmx.model.concept import ConceptScheme
26
+ from pysdmx.model.dataflow import (
27
+ Dataflow,
28
+ DataStructureDefinition,
29
+ ProvisionAgreement,
30
+ )
31
+ from pysdmx.model.dataset import ActionType, Dataset
32
+ from pysdmx.model.map import (
33
+ MultiRepresentationMap,
34
+ RepresentationMap,
35
+ StructureMap,
36
+ )
37
+ from pysdmx.model.metadata import MetadataReport
38
+ from pysdmx.model.organisation import AgencyScheme, DataProviderScheme
39
+ from pysdmx.model.submission import SubmissionResult
40
+ from pysdmx.model.vtl import (
41
+ CustomTypeScheme,
42
+ NamePersonalisationScheme,
24
43
  RulesetScheme,
25
44
  TransformationScheme,
26
45
  UserDefinedOperatorScheme,
46
+ VtlMappingScheme,
27
47
  )
28
- from pysdmx.model.__base import ItemScheme, Organisation
29
- from pysdmx.model.code import Codelist
30
- from pysdmx.model.concept import ConceptScheme
31
- from pysdmx.model.dataflow import Dataflow, DataStructureDefinition
32
- from pysdmx.model.dataset import ActionType, Dataset
33
- from pysdmx.model.submission import SubmissionResult
34
48
 
35
49
 
36
- class Header(Struct, kw_only=True):
37
- """Header for the SDMX messages."""
50
+ class Header(Struct, repr_omit_defaults=True, kw_only=True):
51
+ """Header for the SDMX messages.
52
+
53
+ Represents the Header of an SDMX message, containing metadata about the
54
+ message such as the sender, receiver, and other relevant information.
55
+
56
+ Attributes:
57
+ id: Unique identifier for the message. (default: generated UUID)
58
+ test: Indicates if the message is a test message. (default: False)
59
+ prepared: Timestamp when the message was prepared.
60
+ (default: current UTC time)
61
+ sender: Organisation that sent the message.
62
+ (default: Organisation with id "ZZZ")
63
+ receiver: Optional Organisation that received the message.
64
+ (default: None)
65
+ source: Optional source of the message. (default: None)
66
+ dataset_action: Optional action for the dataset
67
+ (only for SDMX Data messages). (default: None)
68
+ structure: Dimension at observation mapping
69
+ (dict with short URN as key and Dimension ID as value)
70
+ (only for SDMX Data Messages)
71
+ (Overridden by dimension_at_observation argument in writers).
72
+ (default: None)
73
+ dataset_id: DatasetID defined at SDMX-ML
74
+ (only for SDMX-ML Data messages). (default: None)
75
+ """
38
76
 
39
77
  id: str = str(uuid.uuid4())
40
78
  test: bool = False
@@ -46,53 +84,84 @@ class Header(Struct, kw_only=True):
46
84
  structure: Optional[Dict[str, str]] = None
47
85
  dataset_id: Optional[str] = None
48
86
 
87
+ def __post_init__(self) -> None:
88
+ """Header post-initialization."""
89
+ if isinstance(self.sender, str):
90
+ self.sender = Organisation(id=self.sender)
49
91
 
50
- class Message(Struct, frozen=True):
51
- """Message class holds the content of SDMX Message.
92
+ if isinstance(self.receiver, str):
93
+ self.receiver = Organisation(id=self.receiver)
94
+
95
+ def __str__(self) -> str:
96
+ """Custom string representation without the class name."""
97
+ processed_output = []
98
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
99
+ processed_output.append(f"{attr}: {value}")
100
+ return f"{', '.join(processed_output)}"
101
+
102
+
103
+ class StructureMessage(Struct, repr_omit_defaults=True, frozen=True):
104
+ """Message class holds the content of an SDMX Structure Message.
52
105
 
53
106
  Attributes:
54
- structures: Sequence of structure objects (ItemScheme, Schema).
55
- They represent the contents of a Structure Message.
56
- data: Sequence of Dataset objects. They represent the contents of a
57
- SDMX Data Message in any format.
58
- submission: Sequence of SubmissionResult objects. They represent the
59
- contents of a SDMX Submission Message.
107
+ header: The header of the SDMX message.
108
+ structures: Sequence of MaintainableArtefact objects.
109
+ They represent the contents of a Structure Message.
60
110
  """
61
111
 
62
112
  header: Optional[Header] = None
63
- structures: Optional[
64
- Sequence[
65
- Union[
66
- ItemScheme,
67
- DataStructureDefinition,
68
- Dataflow,
69
- ]
70
- ]
71
- ] = None
72
- data: Optional[Sequence[Dataset]] = None
73
- submission: Optional[Sequence[SubmissionResult]] = None
113
+ structures: Optional[Sequence[MaintainableArtefact]] = None
74
114
 
75
115
  def __post_init__(self) -> None:
76
116
  """Checks if the content is valid."""
77
117
  if self.structures is not None:
78
118
  for obj_ in self.structures:
79
- if not isinstance(
80
- obj_, (ItemScheme, DataStructureDefinition, Dataflow)
81
- ):
119
+ if not isinstance(obj_, (MaintainableArtefact)):
82
120
  raise Invalid(
83
121
  f"Invalid structure: {type(obj_).__name__} ",
84
122
  "Check the docs on structures.",
85
123
  )
86
- if self.data is not None:
87
- for data_value in self.data:
88
- if not isinstance(data_value, Dataset):
89
- raise Invalid(
90
- f"Invalid data type: "
91
- f"{type(data_value).__name__} "
92
- f"for Data Message, requires a Dataset object.",
93
- "Check the docs for the proper structure on data.",
124
+
125
+ def __str__(self) -> str:
126
+ """Custom string representation with detailed structure counts."""
127
+ processed_output = []
128
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
129
+ if attr in ["data", "structures"] and value:
130
+ # Count occurrences of each class in structures
131
+ class_counts: Dict[str, int] = {}
132
+ for obj in value:
133
+ class_name = obj.__class__.__name__
134
+ class_counts[class_name] = (
135
+ class_counts.get(class_name, 0) + 1
94
136
  )
95
137
 
138
+ # Format the counts
139
+ value = ", ".join(
140
+ f"{count} {class_name.lower()}"
141
+ for class_name, count in class_counts.items()
142
+ )
143
+
144
+ # Handle sequences and omit empty ones
145
+ if (
146
+ isinstance(value, Sequence)
147
+ and not isinstance(value, str)
148
+ and not value
149
+ ):
150
+ continue
151
+
152
+ processed_output.append(f"{attr}: {value}")
153
+ return f"{', '.join(processed_output)}"
154
+
155
+ def __repr__(self) -> str:
156
+ """Custom __repr__ that omits empty sequences."""
157
+ attrs = []
158
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
159
+ # Omit empty sequences
160
+ if isinstance(value, (list, tuple, set)) and not value:
161
+ continue
162
+ attrs.append(f"{attr}={repr(value)}")
163
+ return f"{self.__class__.__name__}({', '.join(attrs)})"
164
+
96
165
  def __get_elements(self, type_: Type[Any]) -> List[Any]:
97
166
  """Returns a list of elements of a specific type."""
98
167
  if self.structures is None:
@@ -105,6 +174,14 @@ class Message(Struct, frozen=True):
105
174
  structures.append(element)
106
175
  return structures
107
176
 
177
+ def __get_enumerations(
178
+ self, type_: Type[Any], is_vl: bool = False
179
+ ) -> List[Any]:
180
+ """Returns a list of elements of a specific type."""
181
+ enums = self.__get_elements(type_)
182
+ t = "valuelist" if is_vl else "codelist"
183
+ return [e for e in enums if e.sdmx_type == t]
184
+
108
185
  def __get_single_structure(
109
186
  self,
110
187
  type_: Type[Union[ItemScheme, DataStructureDefinition, Dataflow]],
@@ -131,7 +208,7 @@ class Message(Struct, frozen=True):
131
208
 
132
209
  def get_codelists(self) -> List[Codelist]:
133
210
  """Returns the Codelists."""
134
- return self.__get_elements(Codelist)
211
+ return self.__get_enumerations(Codelist, False)
135
212
 
136
213
  def get_concept_schemes(self) -> List[ConceptScheme]:
137
214
  """Returns the Concept Schemes."""
@@ -169,6 +246,123 @@ class Message(Struct, frozen=True):
169
246
  """Returns a specific Dataflow."""
170
247
  return self.__get_single_structure(Dataflow, short_urn)
171
248
 
249
+ def get_transformation_schemes(self) -> List[TransformationScheme]:
250
+ """Returns the TransformationSchemes."""
251
+ return self.__get_elements(TransformationScheme)
252
+
253
+ def get_user_defined_operator_schemes(
254
+ self,
255
+ ) -> List[UserDefinedOperatorScheme]:
256
+ """Returns the UserDefinedOperatorSchemes."""
257
+ return self.__get_elements(UserDefinedOperatorScheme)
258
+
259
+ def get_ruleset_schemes(self) -> List[RulesetScheme]:
260
+ """Returns the RulesetSchemes."""
261
+ return self.__get_elements(RulesetScheme)
262
+
263
+ def get_category_schemes(self) -> List[CategoryScheme]:
264
+ """Returns the CategorySchemes."""
265
+ return self.__get_elements(CategoryScheme)
266
+
267
+ def get_value_lists(self) -> List[Codelist]:
268
+ """Returns the Codelists."""
269
+ return self.__get_enumerations(Codelist, True)
270
+
271
+ def get_hierarchies(self) -> List[Hierarchy]:
272
+ """Returns the HierarchyCodelists."""
273
+ return self.__get_elements(Hierarchy)
274
+
275
+ def get_hierarchy_associations(self) -> List[HierarchyAssociation]:
276
+ """Returns the HierarchyAssociations."""
277
+ return self.__get_elements(HierarchyAssociation)
278
+
279
+ def get_data_provider_schemes(self) -> List[DataProviderScheme]:
280
+ """Returns the DataProviderSchemes."""
281
+ return self.__get_elements(DataProviderScheme)
282
+
283
+ def get_provision_agreements(self) -> List[ProvisionAgreement]:
284
+ """Returns the ProvisionAgreements."""
285
+ return self.__get_elements(ProvisionAgreement)
286
+
287
+ def get_structure_maps(self) -> List[StructureMap]:
288
+ """Returns the StructureMaps."""
289
+ return self.__get_elements(StructureMap)
290
+
291
+ def get_representation_maps(
292
+ self,
293
+ ) -> List[Union[MultiRepresentationMap, RepresentationMap]]:
294
+ """Returns the RepresentationMaps."""
295
+ out = []
296
+ out.extend(self.__get_elements(RepresentationMap))
297
+ out.extend(self.__get_elements(MultiRepresentationMap))
298
+ return out
299
+
300
+ def get_categorisations(self) -> List[Categorisation]:
301
+ """Returns the Categorisations."""
302
+ return self.__get_elements(Categorisation)
303
+
304
+ def get_custom_type_schemes(self) -> List[CustomTypeScheme]:
305
+ """Returns the CustomType Schemes."""
306
+ return self.__get_elements(CustomTypeScheme)
307
+
308
+ def get_vtl_mapping_schemes(self) -> List[VtlMappingScheme]:
309
+ """Returns the VTL Mapping Schemes."""
310
+ return self.__get_elements(VtlMappingScheme)
311
+
312
+ def get_name_personalisation_schemes(
313
+ self,
314
+ ) -> List[NamePersonalisationScheme]:
315
+ """Returns the NamePersonalisationSchemes."""
316
+ return self.__get_elements(NamePersonalisationScheme)
317
+
318
+
319
+ class MetadataMessage(Struct, frozen=True):
320
+ """Message class holds the content of an SDMX Reference Metadata Message.
321
+
322
+ Attributes:
323
+ header: The header of the SDMX message.
324
+ reports: Sequence of metadata reports.
325
+ """
326
+
327
+ header: Optional[Header] = None
328
+ reports: Optional[Sequence[MetadataReport]] = None
329
+
330
+ def get_reports(self) -> Sequence[MetadataReport]:
331
+ """Returns the metadata reports."""
332
+ if self.reports:
333
+ return self.reports
334
+ else:
335
+ raise NotFound("No metadata reports werefound in the message.")
336
+
337
+
338
+ class Message(StructureMessage, frozen=True):
339
+ """Message class holds the content of SDMX Message.
340
+
341
+ Attributes:
342
+ header: The header of the SDMX message.
343
+ structures: Sequence of MaintainableArtefact objects.
344
+ data: Sequence of Dataset objects. They represent the contents of a
345
+ SDMX Data Message in any format.
346
+ submission: Sequence of SubmissionResult objects. They represent the
347
+ contents of a SDMX Submission Message.
348
+ """
349
+
350
+ data: Optional[Sequence[Dataset]] = None
351
+ submission: Optional[Sequence[SubmissionResult]] = None
352
+
353
+ def __post_init__(self) -> None:
354
+ """Checks if the content is valid."""
355
+ super().__post_init__()
356
+ if self.data is not None:
357
+ for data_value in self.data:
358
+ if not isinstance(data_value, Dataset):
359
+ raise Invalid(
360
+ f"Invalid data type: "
361
+ f"{type(data_value).__name__} "
362
+ f"for Data Message, requires a Dataset object.",
363
+ "Check the docs for the proper structure on data.",
364
+ )
365
+
172
366
  def get_datasets(self) -> Sequence[Dataset]:
173
367
  """Returns the Datasets."""
174
368
  if self.data is not None:
@@ -188,17 +382,3 @@ class Message(Struct, frozen=True):
188
382
  f"No Dataset with Short URN {short_urn} found in data.",
189
383
  "Could not find the requested Dataset.",
190
384
  )
191
-
192
- def get_transformation_schemes(self) -> List[TransformationScheme]:
193
- """Returns the TransformationSchemes."""
194
- return self.__get_elements(TransformationScheme)
195
-
196
- def get_user_defined_operator_schemes(
197
- self,
198
- ) -> List[UserDefinedOperatorScheme]:
199
- """Returns the UserDefinedOperatorSchemes."""
200
- return self.__get_elements(UserDefinedOperatorScheme)
201
-
202
- def get_ruleset_schemes(self) -> List[RulesetScheme]:
203
- """Returns the RulesetSchemes."""
204
- return self.__get_elements(RulesetScheme)
pysdmx/model/metadata.py CHANGED
@@ -13,8 +13,14 @@ from typing import Any, Dict, Iterator, List, Optional, Sequence
13
13
 
14
14
  from msgspec import Struct
15
15
 
16
+ from pysdmx.model.__base import Annotation, MaintainableArtefact
17
+ from pysdmx.model.concept import Facets
18
+ from pysdmx.model.dataset import ActionType
16
19
 
17
- class MetadataAttribute(Struct, frozen=True, omit_defaults=True):
20
+
21
+ class MetadataAttribute(
22
+ Struct, frozen=True, omit_defaults=True, repr_omit_defaults=True
23
+ ):
18
24
  """An entry in a metadata report.
19
25
 
20
26
  An attribute is iterable, as it may contain other attributes.
@@ -29,40 +35,70 @@ class MetadataAttribute(Struct, frozen=True, omit_defaults=True):
29
35
  id: str
30
36
  value: Optional[Any] = None
31
37
  attributes: Sequence["MetadataAttribute"] = ()
38
+ annotations: Sequence[Annotation] = ()
39
+ format: Optional[Facets] = None
32
40
 
33
41
  def __iter__(self) -> Iterator["MetadataAttribute"]:
34
42
  """Return an iterator over the list of attributes."""
35
43
  yield from self.attributes
36
44
 
37
45
  def __str__(self) -> str:
38
- """Returns a human-friendly description."""
39
- return f"{self.id}: {self.value}"
40
-
41
-
42
- class MetadataReport(Struct, frozen=True, omit_defaults=True):
46
+ """Custom string representation without the class name."""
47
+ processed_output = []
48
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
49
+ # str is taken as a Sequence, so we need to check it's not a str
50
+ if isinstance(value, Sequence) and not isinstance(value, str):
51
+ # Handle non-empty lists
52
+ if not value:
53
+ continue
54
+ class_name = value[0].__class__.__name__
55
+ value = f"{len(value)} {class_name.lower()}s"
56
+
57
+ processed_output.append(f"{attr}: {value}")
58
+ return f"{', '.join(processed_output)}"
59
+
60
+ def __repr__(self) -> str:
61
+ """Custom __repr__ that omits empty sequences."""
62
+ attrs = []
63
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
64
+ # Omit empty sequences
65
+ if isinstance(value, (list, tuple, set)) and not value:
66
+ continue
67
+ attrs.append(f"{attr}={repr(value)}")
68
+ return f"{self.__class__.__name__}({', '.join(attrs)})"
69
+
70
+
71
+ class MetadataReport(MaintainableArtefact, frozen=True, omit_defaults=True):
43
72
  """An organized collection of metadata.
44
73
 
45
74
  A metadata report is iterable and it is also possible to directly
46
75
  retrieve an attribute using its ID.
47
76
 
48
77
  Attributes:
49
- id: The identifier of the report (e.g. DTI_MACRO).
50
- name: The name of the report (e.g. "Configuration metadata for
51
- the MACRO dataflow").
52
78
  metadataflow: The URN of the dataflow for which the report
53
79
  belongs.
54
80
  targets: The URN(s) of SDMX artefact(s) to which the report relates.
55
81
  attributes: The list of metadata attributes included in the report.
56
82
  Attributes may contain other attributes.
57
- version: The version of the metadata report.
83
+ metadataProvisionAgreement: reference to a metadata provision
84
+ agreement
85
+ publicationPeriod: The reporting period to which the metadata report
86
+ relates
87
+ publicationYear: The year when the report was published
88
+ reportingBegin: The oldest period to which the report relates.
89
+ reportingEnd: The most recent period to which the report relates.
90
+ action: The action to be performed by the receiver.
58
91
  """
59
92
 
60
- id: str
61
- name: str
62
- metadataflow: str
63
- targets: Sequence[str]
64
- attributes: Sequence[MetadataAttribute]
65
- version: str = "1.0"
93
+ metadataflow: str = ""
94
+ targets: Sequence[str] = ()
95
+ attributes: Sequence[MetadataAttribute] = ()
96
+ metadataProvisionAgreement: Optional[str] = None
97
+ publicationPeriod: Optional[str] = None
98
+ publicationYear: Optional[str] = None
99
+ reportingBegin: Optional[str] = None
100
+ reportingEnd: Optional[str] = None
101
+ action: Optional[ActionType] = None
66
102
 
67
103
  def __iter__(self) -> Iterator[MetadataAttribute]:
68
104
  """Return an iterator over the list of report attributes."""
@@ -99,6 +135,33 @@ class MetadataReport(Struct, frozen=True, omit_defaults=True):
99
135
  return out[0]
100
136
  return None
101
137
 
138
+ def __str__(self) -> str:
139
+ """Custom string representation without the class name."""
140
+ processed_output = []
141
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
142
+ # str is taken as a Sequence, so we need to check it's not a str
143
+ if isinstance(value, Sequence) and not isinstance(value, str):
144
+ # Handle non-empty lists
145
+ if not value:
146
+ continue
147
+ class_name = value[0].__class__.__name__
148
+ if class_name == "MetadataAttribute":
149
+ class_name = "Metadata Attribute"
150
+ value = f"{len(value)} {class_name.lower()}s"
151
+
152
+ processed_output.append(f"{attr}: {value}")
153
+ return f"{', '.join(processed_output)}"
154
+
155
+ def __repr__(self) -> str:
156
+ """Custom __repr__ that omits empty sequences."""
157
+ attrs = []
158
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
159
+ # Omit empty sequences
160
+ if isinstance(value, (list, tuple, set)) and not value:
161
+ continue
162
+ attrs.append(f"{attr}={repr(value)}")
163
+ return f"{self.__class__.__name__}({', '.join(attrs)})"
164
+
102
165
 
103
166
  def merge_attributes(
104
167
  attrs: Sequence[MetadataAttribute],
@@ -11,10 +11,15 @@ class SubmissionResult(Struct, frozen=True):
11
11
  status: str
12
12
 
13
13
  def __str__(self) -> str:
14
- """Return a string representation of the SubmissionResult."""
15
- return (
16
- f"<Submission Result - "
17
- f"Action: {self.action} - "
18
- f"Short URN: {self.short_urn} - "
19
- f"Status: {self.status}>"
20
- )
14
+ """Custom string representation without the class name."""
15
+ processed_output = []
16
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
17
+ processed_output.append(f"{attr}: {value}")
18
+ return f"{', '.join(processed_output)}"
19
+
20
+ def __repr__(self) -> str:
21
+ """Custom __repr__ that omits empty sequences."""
22
+ attrs = []
23
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
24
+ attrs.append(f"{attr}={repr(value)}")
25
+ return f"{self.__class__.__name__}({', '.join(attrs)})"
pysdmx/model/vtl.py CHANGED
@@ -6,7 +6,14 @@ from typing import Literal, Optional, Sequence, Union
6
6
  from msgspec import Struct
7
7
 
8
8
  from pysdmx.errors import Invalid
9
- from pysdmx.model.__base import DataflowRef, Item, ItemScheme, Reference
9
+ from pysdmx.model import Codelist, Concept
10
+ from pysdmx.model.__base import (
11
+ DataflowRef,
12
+ Item,
13
+ ItemReference,
14
+ ItemScheme,
15
+ Reference,
16
+ )
10
17
  from pysdmx.model.dataflow import Dataflow
11
18
 
12
19
 
@@ -92,14 +99,14 @@ class VtlDataflowMapping(
92
99
  class VtlCodelistMapping(VtlMapping, frozen=True, omit_defaults=True):
93
100
  """Single mapping with a codelist."""
94
101
 
95
- codelist: str = ""
102
+ codelist: Union[str, Codelist, Reference] = ""
96
103
  codelist_alias: str = ""
97
104
 
98
105
 
99
106
  class VtlConceptMapping(VtlMapping, frozen=True, omit_defaults=True):
100
107
  """Single mapping with a concept."""
101
108
 
102
- concept: str = ""
109
+ concept: Union[str, Concept, ItemReference] = ""
103
110
  concept_alias: str = ""
104
111
 
105
112
 
@@ -141,30 +148,38 @@ class VtlScheme(ItemScheme, frozen=True, omit_defaults=True):
141
148
  class CustomTypeScheme(VtlScheme, frozen=True, omit_defaults=True):
142
149
  """A collection of custom specifications for VTL basic scalar types."""
143
150
 
151
+ items: Sequence[CustomType] = ()
152
+
144
153
 
145
154
  class NamePersonalisationScheme(VtlScheme, frozen=True, omit_defaults=True):
146
155
  """A collection of name personalisations."""
147
156
 
157
+ items: Sequence[NamePersonalisation] = ()
158
+
159
+
160
+ class VtlMappingScheme(ItemScheme, frozen=True, omit_defaults=True):
161
+ """A collection of VTL mappings."""
162
+
148
163
 
149
164
  class RulesetScheme(VtlScheme, frozen=True, omit_defaults=True):
150
165
  """A collection of rulesets."""
151
166
 
152
- vtl_mapping_scheme: Optional[str] = None
167
+ vtl_mapping_scheme: Optional[Union[str, VtlMappingScheme, Reference]] = (
168
+ None
169
+ )
153
170
  items: Sequence[Ruleset] = ()
154
171
 
155
172
 
156
173
  class UserDefinedOperatorScheme(VtlScheme, frozen=True, omit_defaults=True):
157
174
  """A collection of user-defined operators."""
158
175
 
159
- vtl_mapping_scheme: Optional[str] = None
160
- ruleset_schemes: Sequence[Union[str, Reference]] = ()
176
+ vtl_mapping_scheme: Optional[Union[str, VtlMappingScheme, Reference]] = (
177
+ None
178
+ )
179
+ ruleset_schemes: Sequence[Union[str, RulesetScheme, Reference]] = ()
161
180
  items: Sequence[UserDefinedOperator] = ()
162
181
 
163
182
 
164
- class VtlMappingScheme(ItemScheme, frozen=True, omit_defaults=True):
165
- """A collection of VTL mappings."""
166
-
167
-
168
183
  class TransformationScheme(VtlScheme, frozen=True, omit_defaults=True):
169
184
  """VTL Transformation Scheme class.
170
185
 
@@ -195,9 +210,11 @@ class TransformationScheme(VtlScheme, frozen=True, omit_defaults=True):
195
210
  The user-defined operator schemes.
196
211
  """
197
212
 
198
- vtl_mapping_scheme: Optional[VtlMappingScheme] = None
199
- name_personalisation_scheme: Optional[NamePersonalisationScheme] = None
200
- custom_type_scheme: Optional[CustomTypeScheme] = None
213
+ vtl_mapping_scheme: Optional[Union[VtlMappingScheme, Reference]] = None
214
+ name_personalisation_scheme: Optional[
215
+ Union[NamePersonalisationScheme, Reference]
216
+ ] = None
217
+ custom_type_scheme: Optional[Union[CustomTypeScheme, Reference]] = None
201
218
  ruleset_schemes: Sequence[Union[RulesetScheme, Reference]] = ()
202
219
  user_defined_operator_schemes: Sequence[
203
220
  Union[UserDefinedOperatorScheme, Reference]
@@ -1 +1 @@
1
- """VTL toolkit for PySDMX."""
1
+ """Toolkit for PySDMX."""