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/__extras_check.py CHANGED
@@ -50,7 +50,8 @@ def __check_vtl_extra() -> None:
50
50
  except ImportError:
51
51
  raise ImportError(
52
52
  ERROR_MESSAGE.format(
53
- extra_name="vtlengine",
54
- extra_desc="validations for VTL engine",
53
+ extra_name="vtl",
54
+ extra_desc="VTL Scripts, SDMX-VTL model validations"
55
+ " and prettify",
55
56
  )
56
57
  ) from None
pysdmx/__init__.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Your opinionated Python SDMX library."""
2
2
 
3
- __version__ = "1.3.0"
3
+ __version__ = "1.4.0rc1"
@@ -642,7 +642,7 @@ class RegistryClient(__BaseRegistryClient):
642
642
  """
643
643
  query = super()._report_q(provider, id, version)
644
644
  out = self.__fetch(query)
645
- return super()._out(out, self.deser.report)[0]
645
+ return super()._out(out, self.deser.report).reports[0]
646
646
 
647
647
  def get_reports(
648
648
  self,
@@ -667,7 +667,7 @@ class RegistryClient(__BaseRegistryClient):
667
667
  """
668
668
  query = super()._reports_q(artefact_type, agency, id, version)
669
669
  out = self.__fetch(query)
670
- return super()._out(out, self.deser.report, True)
670
+ return super()._out(out, self.deser.report).reports
671
671
 
672
672
  def get_mapping(
673
673
  self,
@@ -1104,7 +1104,7 @@ class AsyncRegistryClient(__BaseRegistryClient):
1104
1104
  """
1105
1105
  query = super()._report_q(provider, id, version)
1106
1106
  out = await self.__fetch(query)
1107
- return super()._out(out, self.deser.report)[0]
1107
+ return super()._out(out, self.deser.report).reports[0]
1108
1108
 
1109
1109
  async def get_reports(
1110
1110
  self,
@@ -1129,7 +1129,7 @@ class AsyncRegistryClient(__BaseRegistryClient):
1129
1129
  """
1130
1130
  query = super()._reports_q(artefact_type, agency, id, version)
1131
1131
  out = await self.__fetch(query)
1132
- return super()._out(out, self.deser.report, True)
1132
+ return super()._out(out, self.deser.report).reports
1133
1133
 
1134
1134
  async def get_mapping(
1135
1135
  self,
@@ -0,0 +1,328 @@
1
+ """API client for interacting with the GDS (Global Discovery Service) service.
2
+
3
+ This module provides classes and utilities to interact with the GDS service,
4
+ allowing retrieval of metadata such as agency information in SDMX-JSON format.
5
+
6
+ Exports: GdsClient: A synchronous client for retrieving metadata from
7
+ the GDS.
8
+ """
9
+
10
+ from typing import Any, Literal, Optional, Sequence
11
+
12
+ from msgspec.json import decode
13
+
14
+ from pysdmx.api.qb.gds import GdsQuery, GdsType
15
+ from pysdmx.api.qb.service import GdsAsyncRestService, GdsRestService
16
+ from pysdmx.api.qb.util import REST_ALL
17
+ from pysdmx.io.json.gds.reader import deserializers as gds_readers
18
+ from pysdmx.io.serde import Deserializer
19
+ from pysdmx.model import Agency
20
+ from pysdmx.model.gds import (
21
+ GdsCatalog,
22
+ GdsSdmxApi,
23
+ GdsService,
24
+ GdsUrnResolver,
25
+ )
26
+
27
+ GDS_BASE_ENDPOINT = "https://gds.sdmx.io/"
28
+
29
+ READERS = gds_readers
30
+
31
+
32
+ class __BaseGdsClient:
33
+ def __init__(
34
+ self,
35
+ api_endpoint: str = GDS_BASE_ENDPOINT,
36
+ pem: Optional[str] = None,
37
+ ):
38
+ """Instantiate a new client against the target endpoint."""
39
+ if api_endpoint.endswith("/"):
40
+ api_endpoint = api_endpoint[0:-1]
41
+ self.api_endpoint = api_endpoint
42
+ self.reader = READERS
43
+
44
+ def _out(self, response: bytes, typ: Deserializer, *params: Any) -> Any:
45
+ return decode(response, type=typ).to_model(*params)
46
+
47
+ def _agencies_q(self, agency: str) -> GdsQuery:
48
+ return GdsQuery(artefact_type=GdsType.GDS_AGENCY, resource_id=agency)
49
+
50
+ def _catalogs_q(
51
+ self,
52
+ agency: str,
53
+ resource: str = REST_ALL,
54
+ version: str = REST_ALL,
55
+ resource_type: Optional[Literal["data", "metadata"]] = None,
56
+ message_format: Optional[Literal["json", "csv", "xml"]] = None,
57
+ api_version: Optional[str] = None,
58
+ detail: Optional[Literal["full", "raw"]] = None,
59
+ references: Optional[Literal["none", "children"]] = None,
60
+ ) -> GdsQuery:
61
+ return GdsQuery(
62
+ artefact_type=GdsType.GDS_CATALOG,
63
+ agency=agency,
64
+ resource_id=resource,
65
+ version=version,
66
+ resource_type=resource_type,
67
+ message_format=message_format,
68
+ api_version=api_version,
69
+ detail=detail,
70
+ references=references,
71
+ )
72
+
73
+ def _sdmx_api_q(self, id: str = REST_ALL) -> GdsQuery:
74
+ return GdsQuery(artefact_type=GdsType.GDS_SDMX_API, resource_id=id)
75
+
76
+ def _services_q(
77
+ self, agency: str, resource: str = REST_ALL, version: str = REST_ALL
78
+ ) -> GdsQuery:
79
+ return GdsQuery(
80
+ artefact_type=GdsType.GDS_SERVICE,
81
+ agency=agency,
82
+ resource_id=resource,
83
+ version=version,
84
+ )
85
+
86
+ def _urn_resolver_q(self, urn: str) -> GdsQuery:
87
+ return GdsQuery(
88
+ artefact_type=GdsType.GDS_URN_RESOLVER, resource_id=urn
89
+ )
90
+
91
+
92
+ class GdsClient(__BaseGdsClient):
93
+ """A client to be used to retrieve metadata from the GDS.
94
+
95
+ With this client, metadata will be retrieved in a synchronous fashion.
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ api_endpoint: str = GDS_BASE_ENDPOINT,
101
+ pem: Optional[str] = None,
102
+ ) -> None:
103
+ """Instantiate a new client against the target endpoint.
104
+
105
+ Args:
106
+ api_endpoint: The endpoint of the targeted service.
107
+ pem: In case the service exposed a certificate created
108
+ by an unknown certificate authority, you can pass
109
+ a pem file for this authority using this parameter.
110
+ """
111
+ super().__init__(api_endpoint, pem)
112
+ self.__service = GdsRestService(
113
+ self.api_endpoint,
114
+ pem=pem,
115
+ timeout=10.0,
116
+ )
117
+
118
+ def __fetch(
119
+ self,
120
+ query: GdsQuery,
121
+ ) -> bytes:
122
+ """Fetch the requested metadata from the GDS service."""
123
+ return self.__service.gds(query)
124
+
125
+ def get_agencies(self, agency: str) -> Sequence[Agency]:
126
+ """Get the list of agencies for the supplied name.
127
+
128
+ Args:
129
+ agency: The agency maintaining the agency scheme from
130
+ which sub-agencies must be returned.
131
+
132
+ Returns:
133
+ The requested list of agencies.
134
+ """
135
+ query = super()._agencies_q(agency)
136
+ out = self.__fetch(query)
137
+ agencies = super()._out(out, self.reader.agencies)
138
+ return agencies
139
+
140
+ def get_catalogs(
141
+ self,
142
+ catalog: str,
143
+ resource: str = REST_ALL,
144
+ version: str = REST_ALL,
145
+ resource_type: Optional[Literal["data", "metadata"]] = None,
146
+ message_format: Optional[Literal["json", "csv", "xml"]] = None,
147
+ api_version: Optional[str] = None,
148
+ detail: Optional[Literal["full", "raw"]] = None,
149
+ references: Optional[Literal["none", "children"]] = None,
150
+ ) -> Sequence[GdsCatalog]:
151
+ """Get the list of catalogs for the supplied parameters.
152
+
153
+ Args:
154
+ catalog: The agency maintaining the catalog.
155
+ resource: The resource ID(s) to query. Defaults to '*'.
156
+ version: The version(s) of the resource. Defaults to '*'.
157
+ resource_type: Filters the endpoints that support
158
+ the requested resource type (eg, 'data', 'metadata')
159
+ message_format: Filters the endpoints that support any
160
+ of the requested message formats.
161
+ api_version: Filters the endpoints that is in a
162
+ specific SDMX API version.
163
+ Multiple values separated by commas are possible.
164
+ By default (if nothing is sent) it returns everything.
165
+ detail: The amount of information to be returned.
166
+ If detail=full: All available information for all artefacts
167
+ should be returned.
168
+ If detail=raw: Any nested service will be referenced.
169
+ references: Instructs the web service to return
170
+ (or not) the artefacts referenced by the
171
+ artefact to be returned.
172
+ If references=none: No referenced artefacts will be returned.
173
+ If references=children: Returns the artefacts
174
+ referenced by the artefact to be returned.
175
+
176
+ Returns:
177
+ A list of GdsCatalog objects.
178
+ """
179
+ query = super()._catalogs_q(
180
+ catalog,
181
+ resource,
182
+ version,
183
+ resource_type,
184
+ message_format,
185
+ api_version,
186
+ detail,
187
+ references,
188
+ )
189
+ response = self.__fetch(query)
190
+ catalogs = super()._out(response, self.reader.catalogs)
191
+ return catalogs
192
+
193
+ def get_sdmx_apis(
194
+ self, api_version: str = REST_ALL
195
+ ) -> Sequence[GdsSdmxApi]:
196
+ """Get the list of SDMX API versions.
197
+
198
+ Args:
199
+ api_version: The version of the SDMX API to be returned.
200
+ Defaults to '*'.
201
+ """
202
+ query = super()._sdmx_api_q(api_version)
203
+ response = self.__fetch(query)
204
+ sdmx_api = super()._out(response, self.reader.sdmx_api)
205
+ return sdmx_api
206
+
207
+ def get_services(
208
+ self, service: str, resource: str = REST_ALL, version: str = REST_ALL
209
+ ) -> Sequence[GdsService]:
210
+ """Get the list of services for the supplied parameters.
211
+
212
+ Args:
213
+ service: The agency maintaining the service.
214
+ resource: The resource ID(s) to query. Defaults to '*'.
215
+ version: The version(s) of the resource. Defaults to '*'.
216
+
217
+ Returns:
218
+ A list of GdsService objects.
219
+ """
220
+ query = super()._services_q(service, resource, version)
221
+ response = self.__fetch(query)
222
+ services = super()._out(response, self.reader.services)
223
+ return services
224
+
225
+ def get_urn_resolver(self, urn: str) -> GdsUrnResolver:
226
+ """Resolve a URN to its corresponding resource.
227
+
228
+ Args:
229
+ urn: The URN to resolve.
230
+
231
+ Returns:
232
+ A GdsUrnResolver object with the resolved information.
233
+ """
234
+ query = super()._urn_resolver_q(urn)
235
+ response = self.__fetch(query)
236
+ urn_resolution = super()._out(response, self.reader.urn_resolver)
237
+ return urn_resolution
238
+
239
+
240
+ class AsyncGdsClient(__BaseGdsClient):
241
+ """A client to be used to retrieve metadata from the GDS asynchronously."""
242
+
243
+ def __init__(
244
+ self,
245
+ api_endpoint: str = GDS_BASE_ENDPOINT,
246
+ pem: Optional[str] = None,
247
+ ) -> None:
248
+ """Instantiate a new client against the target endpoint.
249
+
250
+ Args:
251
+ api_endpoint: The endpoint of the targeted service.
252
+ pem: PEM file for unknown certificate authorities.
253
+ """
254
+ super().__init__(api_endpoint, pem)
255
+ self.__service = GdsAsyncRestService(
256
+ self.api_endpoint,
257
+ pem=pem,
258
+ timeout=10.0,
259
+ )
260
+
261
+ async def __fetch(self, query: GdsQuery) -> bytes:
262
+ """Fetch the requested metadata from the GDS service asynchronously."""
263
+ return await self.__service.gds(query)
264
+
265
+ async def get_agencies(self, agency: str) -> Sequence[Agency]:
266
+ """Get the list of agencies for the supplied name asynchronously.
267
+
268
+ Args:
269
+ agency: The agency maintaining the agency scheme.
270
+
271
+ Returns:
272
+ The requested list of agencies.
273
+ """
274
+ query = super(AsyncGdsClient, self)._agencies_q(agency)
275
+ out = await self.__fetch(query)
276
+ agencies = super(AsyncGdsClient, self)._out(out, self.reader.agencies)
277
+ return agencies
278
+
279
+ async def get_catalogs(
280
+ self,
281
+ catalog: str,
282
+ resource: str = REST_ALL,
283
+ version: str = REST_ALL,
284
+ resource_type: Optional[Literal["data", "metadata"]] = None,
285
+ message_format: Optional[Literal["json", "csv", "xml"]] = None,
286
+ api_version: Optional[str] = None,
287
+ detail: Optional[Literal["full", "raw"]] = None,
288
+ references: Optional[Literal["none", "children"]] = None,
289
+ ) -> Sequence[GdsCatalog]:
290
+ """Get the list of catalogs for the supplied params asynchronously."""
291
+ query = super()._catalogs_q(
292
+ catalog,
293
+ resource,
294
+ version,
295
+ resource_type,
296
+ message_format,
297
+ api_version,
298
+ detail,
299
+ references,
300
+ )
301
+ response = await self.__fetch(query)
302
+ catalogs = super()._out(response, self.reader.catalogs)
303
+ return catalogs
304
+
305
+ async def get_sdmx_apis(
306
+ self, api_version: str = REST_ALL
307
+ ) -> Sequence[GdsSdmxApi]:
308
+ """Get the list of SDMX API versions asynchronously."""
309
+ query = super()._sdmx_api_q(api_version)
310
+ response = await self.__fetch(query)
311
+ sdmx_api = super()._out(response, self.reader.sdmx_api)
312
+ return sdmx_api
313
+
314
+ async def get_services(
315
+ self, service: str, resource: str = REST_ALL, version: str = REST_ALL
316
+ ) -> Sequence[GdsService]:
317
+ """Get a list of services for the supplied params asynchronously."""
318
+ query = super()._services_q(service, resource, version)
319
+ response = await self.__fetch(query)
320
+ services = super()._out(response, self.reader.services)
321
+ return services
322
+
323
+ async def get_urn_resolver(self, urn: str) -> GdsUrnResolver:
324
+ """Resolve a URN to its corresponding resource asynchronously."""
325
+ query = super()._urn_resolver_q(urn)
326
+ response = await self.__fetch(query)
327
+ urn_resolution = super()._out(response, self.reader.urn_resolver)
328
+ return urn_resolution
pysdmx/api/qb/gds.py ADDED
@@ -0,0 +1,153 @@
1
+ """Build GDS-REST structure queries."""
2
+
3
+ from enum import Enum
4
+ from typing import Literal, Optional, Sequence, Union
5
+
6
+ import msgspec
7
+
8
+ from pysdmx.api.qb.util import (
9
+ REST_ALL,
10
+ REST_LATEST,
11
+ )
12
+ from pysdmx.errors import Invalid
13
+
14
+
15
+ class GdsType(Enum):
16
+ """The type of GDS metadata to be returned."""
17
+
18
+ GDS_AGENCY = "agency"
19
+ GDS_CATALOG = "catalog"
20
+ GDS_SDMX_API = "sdmxapi"
21
+ GDS_SERVICE = "service"
22
+ GDS_URN_RESOLVER = "urn_resolver"
23
+ ALL = REST_ALL
24
+ LATEST = REST_LATEST
25
+
26
+
27
+ _RESOURCES = {
28
+ GdsType.GDS_AGENCY,
29
+ GdsType.GDS_CATALOG,
30
+ GdsType.GDS_SDMX_API,
31
+ GdsType.GDS_SERVICE,
32
+ GdsType.GDS_URN_RESOLVER,
33
+ }
34
+
35
+
36
+ class GdsQuery(msgspec.Struct, frozen=True, omit_defaults=True):
37
+ """A query for base GDS metadata.
38
+
39
+ Attributes:
40
+ artefact_type: The type of GDS metadata to be returned.
41
+ agency: The agency (or agencies) maintaining the artefact(s)
42
+ to be returned.
43
+ resource_id: The resource ID(s) to query. Defaults to '*'.
44
+ version: The version(s) of the resource. Defaults to '*'.
45
+ resource_type: Filters the endpoints that support
46
+ the requested resource type (eg, 'data', 'metadata').
47
+ message_format: Filters the endpoints that support any
48
+ of the requested message formats.
49
+
50
+ api_version: Filters the endpoints that is in a
51
+ specific SDMX API version.
52
+ Multiple values separated by commas are possible.
53
+ By default (if nothing is sent) it returns everything.
54
+
55
+ detail: The amount of information to be returned.
56
+ Option full: All available information for all artefacts
57
+ should be returned.
58
+ Option raw: Any nested service will be referenced.
59
+
60
+ references: Instructs the web service to return
61
+ (or not) the artefacts referenced by the
62
+ artefact to be returned.
63
+
64
+ Option none: No referenced artefacts will be returned.
65
+ Option children: Returns the artefacts
66
+ referenced by the artefact to be returned.
67
+ """
68
+
69
+ artefact_type: GdsType
70
+ agency: Union[str, Sequence[str]] = REST_ALL
71
+ resource_id: Union[str, Sequence[str]] = REST_ALL
72
+ version: Optional[str] = None
73
+ resource_type: Optional[Literal["data", "metadata"]] = None
74
+ message_format: Optional[Literal["json", "csv", "xml"]] = None
75
+ api_version: Optional[str] = None
76
+ detail: Optional[Literal["full", "raw"]] = None
77
+ references: Optional[Literal["none", "children"]] = None
78
+
79
+ def validate(self) -> None:
80
+ """Validate the query."""
81
+ try:
82
+ decoder.decode(encoder.encode(self))
83
+ except msgspec.DecodeError as err:
84
+ raise Invalid("Invalid Structure Query", str(err)) from err
85
+
86
+ def _get_base_url(self) -> str:
87
+ """The URL for the query in the GDS-REST API."""
88
+ self.__validate_query()
89
+ return self.__create_query()
90
+
91
+ def get_url(self) -> str:
92
+ """The URL for the query in the GDS-REST API."""
93
+ base_url = self._get_base_url()
94
+ params = []
95
+ if self.resource_type:
96
+ params.append(f"resource_type={self.resource_type}")
97
+ if self.message_format:
98
+ params.append(f"message_format={self.message_format}")
99
+ if self.api_version:
100
+ params.append(f"api_version={self.api_version}")
101
+ if self.detail:
102
+ params.append(f"detail={self.detail}")
103
+ if self.references:
104
+ params.append(f"references={self.references}")
105
+ query_string = f"/?{'&'.join(params)}" if params else ""
106
+ return f"{base_url}{query_string}"
107
+
108
+ def __validate_query(self) -> None:
109
+ self.validate()
110
+ self.__check_artefact_type(self.artefact_type)
111
+
112
+ def __check_artefact_type(self, atyp: GdsType) -> None:
113
+ if atyp not in _RESOURCES:
114
+ raise Invalid(
115
+ "Validation Error",
116
+ f"{atyp} is not valid for GDS-REST.",
117
+ )
118
+
119
+ def __to_type_kw(self, val: GdsType) -> str:
120
+ return val.value
121
+
122
+ def __to_kw(self, val: str) -> str:
123
+ return val
124
+
125
+ def __to_kws(self, vals: Union[str, Sequence[str]]) -> str:
126
+ vals = [vals] if isinstance(vals, str) else vals
127
+ mapped = [self.__to_kw(v) for v in vals]
128
+ sep = ","
129
+ return sep.join(mapped)
130
+
131
+ def __create_query(self) -> str:
132
+ t = self.__to_type_kw(self.artefact_type)
133
+ a = self.__to_kws(self.agency)
134
+ r = self.__to_kws(self.resource_id)
135
+ v = self.__to_kws(self.version) if self.version else REST_ALL
136
+
137
+ if t in ("sdmxapi", "agency", "urn_resolver"):
138
+ return f"/{t}/{r}"
139
+
140
+ vu = f"/{v}" if v != REST_ALL else ""
141
+ ru = f"/{r}{vu}" if vu or self.resource_id != REST_ALL else ""
142
+ au = f"/{a}{ru}" if ru or self.agency != REST_ALL else ""
143
+
144
+ return f"/{t}{au}"
145
+
146
+
147
+ decoder = msgspec.json.Decoder(GdsQuery)
148
+ encoder = msgspec.json.Encoder()
149
+
150
+
151
+ __all__ = [
152
+ "GdsQuery",
153
+ ]
pysdmx/api/qb/service.py CHANGED
@@ -1,4 +1,4 @@
1
- """Connector to SDMX-REST services."""
1
+ """Connector to SDMX-REST and GDS-REST services."""
2
2
 
3
3
  from typing import NoReturn, Optional, Union
4
4
 
@@ -7,6 +7,7 @@ import httpx
7
7
  from pysdmx import errors
8
8
  from pysdmx.api.qb.availability import AvailabilityFormat, AvailabilityQuery
9
9
  from pysdmx.api.qb.data import DataFormat, DataQuery
10
+ from pysdmx.api.qb.gds import GdsQuery
10
11
  from pysdmx.api.qb.refmeta import (
11
12
  RefMetaByMetadataflowQuery,
12
13
  RefMetaByMetadatasetQuery,
@@ -22,6 +23,7 @@ from pysdmx.api.qb.registration import (
22
23
  from pysdmx.api.qb.schema import SchemaFormat, SchemaQuery
23
24
  from pysdmx.api.qb.structure import StructureFormat, StructureQuery
24
25
  from pysdmx.api.qb.util import ApiVersion
26
+ from pysdmx.io.format import GDS_FORMAT
25
27
 
26
28
 
27
29
  class _CoreRestService:
@@ -100,7 +102,7 @@ class _CoreRestService:
100
102
  class RestService(_CoreRestService):
101
103
  """Synchronous connector to SDMX-REST services.
102
104
 
103
- Attributes:
105
+ Args:
104
106
  api_endpoint: The entry point (URL) of the SDMX-REST service.
105
107
  api_version: The most recent version of the SDMX-REST specification
106
108
  supported by the service.
@@ -210,7 +212,7 @@ class RestService(_CoreRestService):
210
212
  class AsyncRestService(_CoreRestService):
211
213
  """Asynchronous connector to SDMX-REST services.
212
214
 
213
- Attributes:
215
+ Args:
214
216
  api_endpoint: The entry point (URL) of the SDMX-REST service.
215
217
  api_version: The most recent version of the SDMX-REST specification
216
218
  supported by the service.
@@ -321,3 +323,89 @@ class AsyncRestService(_CoreRestService):
321
323
  return r.content
322
324
  except (httpx.RequestError, httpx.HTTPStatusError) as e:
323
325
  self._map_error(e)
326
+
327
+
328
+ class _CoreGdsRestService:
329
+ """Base class for GDS-REST services."""
330
+
331
+ def __init__(
332
+ self,
333
+ api_endpoint: str,
334
+ pem: Optional[str] = None,
335
+ timeout: float = 5.0,
336
+ ):
337
+ self._api_endpoint = api_endpoint.rstrip("/")
338
+ self._ssl_context = (
339
+ httpx.create_ssl_context(verify=pem)
340
+ if pem
341
+ else httpx.create_ssl_context()
342
+ )
343
+ self._headers = {"Accept-Encoding": "gzip, deflate"}
344
+ self._timeout = timeout
345
+
346
+ def _map_error(
347
+ self, e: Union[httpx.RequestError, httpx.HTTPStatusError]
348
+ ) -> NoReturn:
349
+ q = e.request.url
350
+ if isinstance(e, httpx.HTTPStatusError):
351
+ s = e.response.status_code
352
+ t = e.response.text
353
+ if s == 404:
354
+ msg = (
355
+ f"The requested resource(s) could "
356
+ f"not be found. Query: `{q}`"
357
+ )
358
+ raise errors.NotFound("Not found", msg) from e
359
+ elif s < 500:
360
+ msg = f"Client error {s}. Query: `{q}`. Error: `{t}`."
361
+ raise errors.Invalid(f"Client error {s}", msg) from e
362
+ else:
363
+ msg = f"Service error {s}. Query: `{q}`. Error: `{t}`."
364
+ raise errors.InternalError(f"Service error {s}", msg) from e
365
+ else:
366
+ msg = f"Connection error. Query: `{q}`. Error: `{e}`."
367
+ raise errors.Unavailable("Connection error", msg) from e
368
+
369
+
370
+ class GdsRestService(_CoreGdsRestService):
371
+ """Synchronous GDS-REST service."""
372
+
373
+ def _fetch(self, query: str, format_: str) -> bytes:
374
+ with httpx.Client(verify=self._ssl_context) as client:
375
+ try:
376
+ url = f"{self._api_endpoint}{query}"
377
+ headers = {**self._headers, "Accept": format_}
378
+ response = client.get(
379
+ url, headers=headers, timeout=self._timeout
380
+ )
381
+ response.raise_for_status()
382
+ return response.content
383
+ except (httpx.RequestError, httpx.HTTPStatusError) as e:
384
+ self._map_error(e)
385
+
386
+ def gds(self, query: GdsQuery) -> bytes:
387
+ """Execute a GDS query against the service."""
388
+ q = query.get_url()
389
+ return self._fetch(q, GDS_FORMAT)
390
+
391
+
392
+ class GdsAsyncRestService(_CoreGdsRestService):
393
+ """Asynchronous GDS-REST service."""
394
+
395
+ async def _fetch(self, query: str, format_: str) -> bytes:
396
+ async with httpx.AsyncClient(verify=self._ssl_context) as client:
397
+ try:
398
+ url = f"{self._api_endpoint}{query}"
399
+ headers = {**self._headers, "Accept": format_}
400
+ response = await client.get(
401
+ url, headers=headers, timeout=self._timeout
402
+ )
403
+ response.raise_for_status()
404
+ return response.content
405
+ except (httpx.RequestError, httpx.HTTPStatusError) as e:
406
+ self._map_error(e)
407
+
408
+ async def gds(self, query: GdsQuery) -> bytes:
409
+ """Execute a GDS query against the service."""
410
+ q = query.get_url()
411
+ return await self._fetch(q, GDS_FORMAT)
@@ -272,6 +272,7 @@ _API_RESOURCES = {
272
272
  "V2.0.0": _V2_0_RESOURCES,
273
273
  "V2.1.0": _V2_0_RESOURCES,
274
274
  "V2.2.0": _V2_0_RESOURCES,
275
+ "V2.2.1": _V2_0_RESOURCES,
275
276
  "LATEST": _V2_0_RESOURCES,
276
277
  }
277
278
 
pysdmx/api/qb/util.py CHANGED
@@ -24,6 +24,7 @@ class ApiVersion(IntEnum):
24
24
  V2_0_0 = 8
25
25
  V2_1_0 = 9
26
26
  V2_2_0 = 10
27
+ V2_2_1 = 11
27
28
 
28
29
 
29
30
  MULT_SEP = re.compile(r"\+")
pysdmx/io/__init__.py CHANGED
@@ -1,5 +1,6 @@
1
1
  """IO module for SDMX data."""
2
2
 
3
3
  from pysdmx.io.reader import get_datasets, read_sdmx
4
+ from pysdmx.io.writer import write_sdmx
4
5
 
5
- __all__ = ["read_sdmx", "get_datasets"]
6
+ __all__ = ["read_sdmx", "get_datasets", "write_sdmx"]