pysdmx 1.6.0__py3-none-any.whl → 1.8.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 (44) hide show
  1. pysdmx/__init__.py +1 -1
  2. pysdmx/api/fmr/__init__.py +266 -0
  3. pysdmx/api/qb/refmeta.py +1 -1
  4. pysdmx/api/qb/schema.py +4 -10
  5. pysdmx/api/qb/service.py +26 -4
  6. pysdmx/api/qb/structure.py +2 -2
  7. pysdmx/io/input_processor.py +4 -4
  8. pysdmx/io/json/fusion/messages/__init__.py +14 -0
  9. pysdmx/io/json/fusion/messages/concept.py +2 -8
  10. pysdmx/io/json/fusion/messages/dsd.py +52 -1
  11. pysdmx/io/json/fusion/messages/metadataflow.py +44 -0
  12. pysdmx/io/json/fusion/messages/mpa.py +45 -0
  13. pysdmx/io/json/fusion/messages/msd.py +121 -0
  14. pysdmx/io/json/fusion/messages/org.py +90 -0
  15. pysdmx/io/json/fusion/reader/__init__.py +5 -0
  16. pysdmx/io/json/sdmxjson2/messages/__init__.py +15 -1
  17. pysdmx/io/json/sdmxjson2/messages/code.py +35 -13
  18. pysdmx/io/json/sdmxjson2/messages/concept.py +5 -8
  19. pysdmx/io/json/sdmxjson2/messages/dsd.py +9 -6
  20. pysdmx/io/json/sdmxjson2/messages/metadataflow.py +88 -0
  21. pysdmx/io/json/sdmxjson2/messages/mpa.py +88 -0
  22. pysdmx/io/json/sdmxjson2/messages/msd.py +241 -0
  23. pysdmx/io/json/sdmxjson2/messages/provider.py +117 -1
  24. pysdmx/io/json/sdmxjson2/messages/structure.py +25 -1
  25. pysdmx/io/json/sdmxjson2/reader/__init__.py +5 -0
  26. pysdmx/io/serde.py +5 -0
  27. pysdmx/io/writer.py +2 -4
  28. pysdmx/io/xml/__ss_aux_reader.py +1 -2
  29. pysdmx/io/xml/__structure_aux_reader.py +15 -10
  30. pysdmx/io/xml/__structure_aux_writer.py +6 -4
  31. pysdmx/io/xml/__write_data_aux.py +6 -5
  32. pysdmx/io/xml/__write_structure_specific_aux.py +6 -2
  33. pysdmx/io/xml/doc_validation.py +1 -3
  34. pysdmx/io/xml/sdmx21/writer/generic.py +5 -3
  35. pysdmx/model/__init__.py +13 -4
  36. pysdmx/model/dataflow.py +1 -0
  37. pysdmx/model/map.py +6 -6
  38. pysdmx/model/message.py +30 -6
  39. pysdmx/model/metadata.py +255 -4
  40. pysdmx/toolkit/pd/_data_utils.py +3 -4
  41. {pysdmx-1.6.0.dist-info → pysdmx-1.8.0.dist-info}/METADATA +2 -2
  42. {pysdmx-1.6.0.dist-info → pysdmx-1.8.0.dist-info}/RECORD +44 -38
  43. {pysdmx-1.6.0.dist-info → pysdmx-1.8.0.dist-info}/WHEEL +0 -0
  44. {pysdmx-1.6.0.dist-info → pysdmx-1.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -39,9 +39,7 @@ def validate_doc(input_str: str) -> None:
39
39
  doc = etree.parse(bytes_infile, parser=parser)
40
40
  if not xmlschema.validate(doc):
41
41
  log_errors = list(xmlschema.error_log) # type: ignore[call-overload]
42
- unhandled_errors = []
43
- for e in log_errors:
44
- unhandled_errors.append(e.message)
42
+ unhandled_errors = [e.message for e in log_errors]
45
43
  severe_errors = unhandled_errors.copy()
46
44
  for e in unhandled_errors:
47
45
  for allowed_error in ALLOWED_ERRORS_CONTENT:
@@ -327,10 +327,12 @@ def __group_processing(
327
327
  .to_dict(orient="records")
328
328
  )
329
329
 
330
- for record in grouped_data:
331
- out_list.append(
330
+ out_list.extend(
331
+ [
332
332
  __format_group_str(record, group_id, dimensions, attribute)
333
- )
333
+ for record in grouped_data
334
+ ]
335
+ )
334
336
 
335
337
  return "".join(out_list)
336
338
 
pysdmx/model/__init__.py CHANGED
@@ -53,7 +53,14 @@ from pysdmx.model.map import (
53
53
  StructureMap,
54
54
  ValueMap,
55
55
  )
56
- from pysdmx.model.metadata import MetadataAttribute, MetadataReport
56
+ from pysdmx.model.metadata import (
57
+ MetadataAttribute,
58
+ MetadataComponent,
59
+ Metadataflow,
60
+ MetadataProvisionAgreement,
61
+ MetadataReport,
62
+ MetadataStructure,
63
+ )
57
64
  from pysdmx.model.organisation import (
58
65
  AgencyScheme,
59
66
  DataConsumerScheme,
@@ -131,9 +138,7 @@ def decoders(type: Type, obj: Any) -> Any: # type: ignore[type-arg]
131
138
  target types
132
139
  """
133
140
  if type is Components:
134
- comps = []
135
- for item in obj:
136
- comps.append(msgspec.convert(item, Component))
141
+ comps = [msgspec.convert(item, Component) for item in obj]
137
142
  return Components(comps)
138
143
  else:
139
144
  raise NotImplementedError(f"Objects of type {type} are not supported")
@@ -176,9 +181,13 @@ __all__ = [
176
181
  "ImplicitComponentMap",
177
182
  "ItemReference",
178
183
  "MetadataAttribute",
184
+ "MetadataComponent",
185
+ "Metadataflow",
179
186
  "MetadataProvider",
180
187
  "MetadataProviderScheme",
188
+ "MetadataProvisionAgreement",
181
189
  "MetadataReport",
190
+ "MetadataStructure",
182
191
  "MultiComponentMap",
183
192
  "MultiRepresentationMap",
184
193
  "MultiValueMap",
pysdmx/model/dataflow.py CHANGED
@@ -116,6 +116,7 @@ class Component(
116
116
  id: A unique identifier for the component (e.g. FREQ).
117
117
  required: Whether the component must have a value.
118
118
  role: The role played by the component.
119
+ concept: The concept giving its identity to the component.
119
120
  local_dtype: The component's local data type (string, number, etc.).
120
121
  local_facets: Additional local details such as the component's minimum
121
122
  length.
pysdmx/model/map.py CHANGED
@@ -500,12 +500,12 @@ class StructureMap(MaintainableArtefact, frozen=True, omit_defaults=True):
500
500
  ]
501
501
  ]:
502
502
  """Return the mapping rules for the supplied component."""
503
- out = []
504
- for m in self.maps:
505
- if (
506
- hasattr(m, "source") and (m.source == id_ or id_ in m.source)
507
- ) or (isinstance(m, FixedValueMap) and m.target == id_):
508
- out.append(m)
503
+ out = [
504
+ m
505
+ for m in self.maps
506
+ if (hasattr(m, "source") and (m.source == id_ or id_ in m.source))
507
+ or (isinstance(m, FixedValueMap) and m.target == id_)
508
+ ]
509
509
  if len(out) == 0:
510
510
  return None
511
511
  else:
pysdmx/model/message.py CHANGED
@@ -34,8 +34,17 @@ from pysdmx.model.map import (
34
34
  RepresentationMap,
35
35
  StructureMap,
36
36
  )
37
- from pysdmx.model.metadata import MetadataReport
38
- from pysdmx.model.organisation import AgencyScheme, DataProviderScheme
37
+ from pysdmx.model.metadata import (
38
+ Metadataflow,
39
+ MetadataProvisionAgreement,
40
+ MetadataReport,
41
+ MetadataStructure,
42
+ )
43
+ from pysdmx.model.organisation import (
44
+ AgencyScheme,
45
+ DataProviderScheme,
46
+ MetadataProviderScheme,
47
+ )
39
48
  from pysdmx.model.submission import SubmissionResult
40
49
  from pysdmx.model.vtl import (
41
50
  CustomTypeScheme,
@@ -168,10 +177,7 @@ class StructureMessage(Struct, repr_omit_defaults=True, frozen=True):
168
177
  raise NotFound(
169
178
  f"No {type_.__name__} found in message.",
170
179
  )
171
- structures = []
172
- for element in self.structures:
173
- if isinstance(element, type_):
174
- structures.append(element)
180
+ structures = [e for e in self.structures if isinstance(e, type_)]
175
181
  return structures
176
182
 
177
183
  def __get_enumerations(
@@ -224,6 +230,10 @@ class StructureMessage(Struct, repr_omit_defaults=True, frozen=True):
224
230
  """Returns the Dataflows."""
225
231
  return self.__get_elements(Dataflow)
226
232
 
233
+ def get_metadataflows(self) -> List[Metadataflow]:
234
+ """Returns the MetadataProvisionAgreements."""
235
+ return self.__get_elements(Metadataflow)
236
+
227
237
  def get_organisation_scheme(self, short_urn: str) -> AgencyScheme:
228
238
  """Returns a specific OrganisationScheme."""
229
239
  return self.__get_single_structure(AgencyScheme, short_urn)
@@ -284,6 +294,20 @@ class StructureMessage(Struct, repr_omit_defaults=True, frozen=True):
284
294
  """Returns the ProvisionAgreements."""
285
295
  return self.__get_elements(ProvisionAgreement)
286
296
 
297
+ def get_metadata_provider_schemes(self) -> List[MetadataProviderScheme]:
298
+ """Returns the MetadataProviderSchemes."""
299
+ return self.__get_elements(MetadataProviderScheme)
300
+
301
+ def get_metadata_provision_agreements(
302
+ self,
303
+ ) -> List[MetadataProvisionAgreement]:
304
+ """Returns the MetadataProvisionAgreements."""
305
+ return self.__get_elements(MetadataProvisionAgreement)
306
+
307
+ def get_metadata_structures(self) -> List[MetadataStructure]:
308
+ """Returns the MetadataStructures."""
309
+ return self.__get_elements(MetadataStructure)
310
+
287
311
  def get_structure_maps(self) -> List[StructureMap]:
288
312
  """Returns the StructureMaps."""
289
313
  return self.__get_elements(StructureMap)
pysdmx/model/metadata.py CHANGED
@@ -9,21 +9,272 @@ example by providing configuration details in a metadata report.
9
9
  """
10
10
 
11
11
  from collections import defaultdict
12
- from typing import Any, Dict, Iterator, List, Optional, Sequence
12
+ from typing import Any, Dict, Iterator, List, Optional, Sequence, Union
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
16
+ from pysdmx.model.__base import (
17
+ Annotation,
18
+ IdentifiableArtefact,
19
+ ItemReference,
20
+ MaintainableArtefact,
21
+ Reference,
22
+ )
23
+ from pysdmx.model.code import Codelist, Hierarchy
24
+ from pysdmx.model.concept import Concept, DataType, Facets
25
+ from pysdmx.model.dataflow import ArrayBoundaries
18
26
  from pysdmx.model.dataset import ActionType
19
27
 
20
28
 
29
+ class MetadataComponent(
30
+ IdentifiableArtefact, frozen=True, omit_defaults=True, kw_only=True
31
+ ):
32
+ """A component defines the expected structure of a metadata attribute.
33
+
34
+ The metadata component takes its semantic, and in some cases it
35
+ representation, from its concept identity. A metadata component
36
+ may be coded (via the local representation), uncoded (via the text
37
+ format), or take no value. In addition to this value, the metadata
38
+ component may also specify subordinate metadata components.
39
+
40
+ If a metadata component only serves the purpose of containing
41
+ subordinate metadata components, then the is_presentational attribute
42
+ should be set to True. Otherwise, it is assumed to also take a value.
43
+
44
+ If the metadata component does take a value, and a representation is
45
+ not defined, it will be inherited from the concept it takes its
46
+ semantic from. The optional id on the metadata component uniquely
47
+ identifies it within the metadata structured definition.
48
+
49
+ If this id is not supplied, its value is assumed to be that of the
50
+ concept referenced from the concept identity. Note that a metadata
51
+ component (as identified by the id attribute) definition must be
52
+ unique across the entire metadata structure definition.
53
+
54
+ Attributes:
55
+ id: The identifier of the component.
56
+ is_presentational: Whether the component is for presentation
57
+ purposes only (e.g. a section header), or may contain a
58
+ value.
59
+ concept: The concept giving its identity to the component.
60
+ local_dtype: The component's local data type (string, number, etc.).
61
+ local_facets: Additional local details such as the component's minimum
62
+ length.
63
+ local_codes: The expected local values for the component (e.g. currency
64
+ codes).
65
+ array_def: Any additional constraints for array types.
66
+ local_enum_ref: The URN of the enumeration (codelist or valuelist) from
67
+ which the local codes are taken.
68
+ """
69
+
70
+ is_presentational: bool = False
71
+ concept: Union[Concept, ItemReference]
72
+ local_dtype: Optional[DataType] = None
73
+ local_facets: Optional[Facets] = None
74
+ local_codes: Union[Codelist, Hierarchy, None] = None
75
+ array_def: Optional[ArrayBoundaries] = None
76
+ local_enum_ref: Optional[str] = None
77
+ components: Sequence["MetadataComponent"] = ()
78
+
79
+ @property
80
+ def dtype(self) -> DataType:
81
+ """Returns the component data type.
82
+
83
+ This will return the local data type (if any) or
84
+ the data type of the referenced concept (if any).
85
+ In case neither are set, the data type will default
86
+ to string.
87
+
88
+ Returns:
89
+ The component data type (local, core or default).
90
+ """
91
+ if self.local_dtype:
92
+ return self.local_dtype
93
+ elif isinstance(self.concept, Concept) and self.concept.dtype:
94
+ return self.concept.dtype
95
+ else:
96
+ return DataType.STRING
97
+
98
+ @property
99
+ def facets(self) -> Optional[Facets]:
100
+ """Returns the component facets.
101
+
102
+ This will return the local facets (if any) or
103
+ the facets of the referenced concept (if any), or
104
+ None in case neither are set.
105
+
106
+ Returns:
107
+ The component facets (local or core).
108
+ """
109
+ if self.local_facets:
110
+ return self.local_facets
111
+ elif isinstance(self.concept, Concept) and self.concept.facets:
112
+ return self.concept.facets
113
+ else:
114
+ return None
115
+
116
+ @property
117
+ def enumeration(self) -> Union[Codelist, Hierarchy, None]:
118
+ """Returns the list of valid codes for the component.
119
+
120
+ This will return the local codes (if any) or
121
+ the codes of the referenced concept (if any), or
122
+ None in case neither are set.
123
+
124
+ Returns:
125
+ The component codes (local or core).
126
+ """
127
+ if self.local_codes:
128
+ return self.local_codes
129
+ elif isinstance(self.concept, Concept) and self.concept.codes:
130
+ return self.concept.codes
131
+ else:
132
+ return None
133
+
134
+ @property
135
+ def enum_ref(self) -> Optional[str]:
136
+ """Returns the URN of the enumeration from which the codes are taken.
137
+
138
+ Returns:
139
+ The URN of the enumeration from which the codes are taken.
140
+ """
141
+ if self.local_enum_ref:
142
+ return self.local_enum_ref
143
+ elif isinstance(self.concept, Concept) and self.concept.enum_ref:
144
+ return self.concept.enum_ref
145
+ else:
146
+ return None
147
+
148
+ def __str__(self) -> str:
149
+ """Custom string representation without the class name."""
150
+ processed_output = []
151
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
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
+ attrs.append(f"{attr}={repr(value)}")
160
+ return f"{self.__class__.__name__}({', '.join(attrs)})"
161
+
162
+
163
+ class MetadataStructure(
164
+ MaintainableArtefact, frozen=True, omit_defaults=True, kw_only=True
165
+ ):
166
+ """A metadata structure definition, i.e. a collection of metadata concepts.
167
+
168
+ Attributes:
169
+ id: The identifier for the MSD.
170
+ name: The MSD name (e.g. "Frequency codelist").
171
+ agency: The maintainer of the MSD (e.g. SDMX).
172
+ description: Additional descriptive information about the MSD.
173
+ version: The MSD version (e.g. 1.0.1)
174
+ components: The MSD components, i.e. the collection of metadata
175
+ concepts.
176
+ """
177
+
178
+ components: Sequence[MetadataComponent] = ()
179
+
180
+ def __iter__(self) -> Iterator[MetadataComponent]:
181
+ """Return an iterator over the list of components."""
182
+ yield from self.components
183
+
184
+ def __len__(self) -> int:
185
+ """Return the number of components in the MSD."""
186
+ return self.__get_count(self.components)
187
+
188
+ def __getitem__(self, id_: str) -> Optional[MetadataComponent]:
189
+ """Return the component identified by the given ID."""
190
+ return self.__extract_cat(self.components, id_)
191
+
192
+ def __contains__(self, id_: str) -> bool:
193
+ """Whether there is a component with the supplied ID in the MSD."""
194
+ return bool(self.__getitem__(id_))
195
+
196
+ def __get_count(self, comps: Sequence[MetadataComponent]) -> int:
197
+ """Return the number of components at any levels."""
198
+ count = len(comps)
199
+ for comp in comps:
200
+ if comp.components:
201
+ count += self.__get_count(comp.components)
202
+ return count
203
+
204
+ def __extract_cat(
205
+ self, comps: Sequence[MetadataComponent], id_: str
206
+ ) -> Optional[MetadataComponent]:
207
+ if "." in id_:
208
+ ids = id_.split(".")
209
+ out = list(filter(lambda cat: cat.id == ids[0], comps))
210
+ if out:
211
+ pkey = ".".join(ids[1:])
212
+ return self.__extract_cat(out[0].components, pkey)
213
+ else:
214
+ out = list(filter(lambda cat: cat.id == id_, comps))
215
+ if out:
216
+ return out[0]
217
+ return None
218
+
219
+ def __str__(self) -> str:
220
+ """Custom string representation without the class name."""
221
+ processed_output = []
222
+ for attr, value, *_ in self.__rich_repr__(): # type: ignore[misc]
223
+ # str is taken as a Sequence, so we need to check it's not a str
224
+ if isinstance(value, Sequence) and not isinstance(value, str):
225
+ # Handle empty lists
226
+ if not value:
227
+ continue
228
+ class_name = value[0].__class__.__name__
229
+ class_name = (
230
+ class_name.lower() + "s"
231
+ if attr != "components"
232
+ else "components"
233
+ )
234
+ value = f"{len(value)} {class_name}"
235
+
236
+ processed_output.append(f"{attr}: {value}")
237
+ return f"{', '.join(processed_output)}"
238
+
239
+
240
+ class Metadataflow(
241
+ MaintainableArtefact,
242
+ frozen=True,
243
+ omit_defaults=True,
244
+ tag=True,
245
+ kw_only=True,
246
+ ):
247
+ """A flow of reference metadata that metadata providers will provide.
248
+
249
+ Attributes:
250
+ structure: The MSD describing the structure of all reference
251
+ metadata reports for this metadataflow.
252
+ targets: Identifiable structures to which the reference metadata
253
+ reports described by the referenced MSD should be restricted to.
254
+ For example, to indicate that the reports can be related to
255
+ dataflows only, the following can be used:
256
+ urn:sdmx:org.sdmx.infomodel.datastructure.Dataflow=*:*(*)
257
+ """
258
+
259
+ structure: Optional[Union[MetadataStructure, str]]
260
+ targets: Union[Sequence[str], Sequence[Reference]]
261
+
262
+
263
+ class MetadataProvisionAgreement(
264
+ MaintainableArtefact, frozen=True, omit_defaults=True, kw_only=True
265
+ ):
266
+ """Link between a metadata provider and metadataflow."""
267
+
268
+ metadataflow: str
269
+ metadata_provider: str
270
+
271
+
21
272
  class MetadataAttribute(
22
273
  Struct, frozen=True, omit_defaults=True, repr_omit_defaults=True
23
274
  ):
24
275
  """An entry in a metadata report.
25
276
 
26
- An attribute is iterable, as it may contain other attributes.
277
+ An component is iterable, as it may contain other attributes.
27
278
 
28
279
  Attributes:
29
280
  id: The identifier of the attribute (e.g. "License").
@@ -53,15 +53,14 @@ def get_codes(
53
53
  dimension_code: str, structure: Schema, data: pd.DataFrame
54
54
  ) -> Tuple[List[str], List[str], List[Dict[str, Any]]]:
55
55
  """This function divides the components in Series and Obs."""
56
- series_codes = []
57
56
  groups = structure.groups
58
57
  group_codes = []
59
58
  obs_codes = [dimension_code, structure.components.measures[0].id]
60
59
 
61
60
  # Getting the series and obs codes
62
- for dim in structure.components.dimensions:
63
- if dim.id != dimension_code:
64
- series_codes.append(dim.id)
61
+ series_codes = [
62
+ d.id for d in structure.components.dimensions if d.id != dimension_code
63
+ ]
65
64
 
66
65
  # Adding the attributes based on the attachment level
67
66
  for att in structure.components.attributes:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysdmx
3
- Version: 1.6.0
3
+ Version: 1.8.0
4
4
  Summary: Your opinionated Python SDMX library
5
5
  License: Apache-2.0
6
6
  License-File: LICENSE
@@ -18,7 +18,7 @@ Provides-Extra: data
18
18
  Provides-Extra: dc
19
19
  Provides-Extra: vtl
20
20
  Provides-Extra: xml
21
- Requires-Dist: httpx (>=0)
21
+ Requires-Dist: httpx[http2] (>=0)
22
22
  Requires-Dist: lxml (>=5.2) ; extra == "all"
23
23
  Requires-Dist: lxml (>=5.2) ; extra == "xml"
24
24
  Requires-Dist: msgspec (>=0)