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.
- pysdmx/__extras_check.py +3 -2
- pysdmx/__init__.py +1 -1
- pysdmx/api/fmr/__init__.py +4 -4
- pysdmx/api/gds/__init__.py +328 -0
- pysdmx/api/qb/gds.py +153 -0
- pysdmx/api/qb/service.py +91 -3
- pysdmx/api/qb/structure.py +1 -0
- pysdmx/api/qb/util.py +1 -0
- pysdmx/io/__init__.py +2 -1
- pysdmx/io/csv/sdmx10/reader/__init__.py +4 -2
- pysdmx/io/csv/sdmx10/writer/__init__.py +15 -2
- pysdmx/io/csv/sdmx20/reader/__init__.py +5 -2
- pysdmx/io/csv/sdmx20/writer/__init__.py +13 -2
- pysdmx/io/format.py +4 -0
- pysdmx/io/input_processor.py +12 -3
- pysdmx/io/json/fusion/messages/core.py +2 -0
- pysdmx/io/json/fusion/messages/report.py +13 -7
- pysdmx/io/json/gds/messages/__init__.py +35 -0
- pysdmx/io/json/gds/messages/agencies.py +41 -0
- pysdmx/io/json/gds/messages/catalog.py +79 -0
- pysdmx/io/json/gds/messages/sdmx_api.py +23 -0
- pysdmx/io/json/gds/messages/services.py +49 -0
- pysdmx/io/json/gds/messages/urn_resolver.py +43 -0
- pysdmx/io/json/gds/reader/__init__.py +12 -0
- pysdmx/io/json/sdmxjson2/messages/__init__.py +12 -4
- pysdmx/io/json/sdmxjson2/messages/agency.py +72 -0
- pysdmx/io/json/sdmxjson2/messages/category.py +22 -29
- pysdmx/io/json/sdmxjson2/messages/code.py +68 -64
- pysdmx/io/json/sdmxjson2/messages/concept.py +9 -18
- pysdmx/io/json/sdmxjson2/messages/constraint.py +2 -13
- pysdmx/io/json/sdmxjson2/messages/core.py +113 -21
- pysdmx/io/json/sdmxjson2/messages/dataflow.py +51 -21
- pysdmx/io/json/sdmxjson2/messages/dsd.py +110 -36
- pysdmx/io/json/sdmxjson2/messages/map.py +61 -49
- pysdmx/io/json/sdmxjson2/messages/pa.py +9 -17
- pysdmx/io/json/sdmxjson2/messages/provider.py +88 -0
- pysdmx/io/json/sdmxjson2/messages/report.py +84 -14
- pysdmx/io/json/sdmxjson2/messages/schema.py +14 -5
- pysdmx/io/json/sdmxjson2/messages/structure.py +105 -36
- pysdmx/io/json/sdmxjson2/messages/vtl.py +42 -96
- pysdmx/io/pd.py +2 -9
- pysdmx/io/reader.py +72 -27
- pysdmx/io/serde.py +11 -0
- pysdmx/io/writer.py +134 -0
- pysdmx/io/xml/{sdmx21/reader/__data_aux.py → __data_aux.py} +9 -2
- pysdmx/io/xml/{sdmx21/reader/__parse_xml.py → __parse_xml.py} +30 -6
- pysdmx/io/xml/__ss_aux_reader.py +96 -0
- pysdmx/io/xml/__structure_aux_reader.py +1174 -0
- pysdmx/io/xml/__structure_aux_writer.py +1233 -0
- pysdmx/io/xml/{sdmx21/__tokens.py → __tokens.py} +33 -1
- pysdmx/io/xml/{sdmx21/writer/__write_aux.py → __write_aux.py} +129 -37
- pysdmx/io/xml/{sdmx21/writer/__write_data_aux.py → __write_data_aux.py} +1 -1
- pysdmx/io/xml/__write_structure_specific_aux.py +254 -0
- pysdmx/io/xml/{sdmx21/reader/doc_validation.py → doc_validation.py} +10 -2
- pysdmx/io/xml/{sdmx21/reader/header.py → header.py} +11 -3
- pysdmx/io/xml/sdmx21/reader/error.py +2 -2
- pysdmx/io/xml/sdmx21/reader/generic.py +12 -8
- pysdmx/io/xml/sdmx21/reader/structure.py +5 -840
- pysdmx/io/xml/sdmx21/reader/structure_specific.py +13 -97
- pysdmx/io/xml/sdmx21/reader/submission.py +2 -2
- pysdmx/io/xml/sdmx21/writer/error.py +1 -1
- pysdmx/io/xml/sdmx21/writer/generic.py +13 -7
- pysdmx/io/xml/sdmx21/writer/structure.py +16 -828
- pysdmx/io/xml/sdmx21/writer/structure_specific.py +13 -238
- pysdmx/io/xml/sdmx30/__init__.py +1 -0
- pysdmx/io/xml/sdmx30/reader/__init__.py +1 -0
- pysdmx/io/xml/sdmx30/reader/structure.py +39 -0
- pysdmx/io/xml/sdmx30/reader/structure_specific.py +39 -0
- pysdmx/io/xml/sdmx30/writer/__init__.py +1 -0
- pysdmx/io/xml/sdmx30/writer/structure.py +67 -0
- pysdmx/io/xml/sdmx30/writer/structure_specific.py +108 -0
- pysdmx/model/__base.py +99 -34
- pysdmx/model/__init__.py +4 -0
- pysdmx/model/category.py +20 -0
- pysdmx/model/code.py +29 -8
- pysdmx/model/concept.py +52 -11
- pysdmx/model/dataflow.py +117 -33
- pysdmx/model/dataset.py +66 -14
- pysdmx/model/gds.py +161 -0
- pysdmx/model/map.py +51 -8
- pysdmx/model/message.py +235 -55
- pysdmx/model/metadata.py +79 -16
- pysdmx/model/submission.py +12 -7
- pysdmx/model/vtl.py +30 -13
- pysdmx/toolkit/__init__.py +1 -1
- pysdmx/toolkit/pd/__init__.py +85 -0
- pysdmx/toolkit/vtl/__init__.py +2 -1
- pysdmx/toolkit/vtl/_validations.py +1 -1
- pysdmx/toolkit/vtl/{generate_vtl_script.py → script_generation.py} +30 -4
- pysdmx/toolkit/vtl/validation.py +119 -0
- pysdmx/util/_model_utils.py +1 -1
- pysdmx-1.4.0rc1.dist-info/METADATA +119 -0
- pysdmx-1.4.0rc1.dist-info/RECORD +140 -0
- pysdmx/io/json/sdmxjson2/messages/org.py +0 -140
- pysdmx/toolkit/vtl/model_validations.py +0 -50
- pysdmx-1.3.0.dist-info/METADATA +0 -76
- pysdmx-1.3.0.dist-info/RECORD +0 -116
- /pysdmx/io/xml/{sdmx21/writer/config.py → config.py} +0 -0
- {pysdmx-1.3.0.dist-info → pysdmx-1.4.0rc1.dist-info}/LICENSE +0 -0
- {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="
|
|
54
|
-
extra_desc="
|
|
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
pysdmx/api/fmr/__init__.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
pysdmx/api/qb/structure.py
CHANGED
pysdmx/api/qb/util.py
CHANGED