sdmxlib 0.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.
- sdmxlib/__init__.py +155 -0
- sdmxlib/api/__init__.py +7 -0
- sdmxlib/api/client.py +246 -0
- sdmxlib/api/filters.py +67 -0
- sdmxlib/api/providers.py +66 -0
- sdmxlib/api/query.py +428 -0
- sdmxlib/api/registry.py +666 -0
- sdmxlib/api/session.py +124 -0
- sdmxlib/formats/__init__.py +166 -0
- sdmxlib/formats/sdmx_csv/__init__.py +5 -0
- sdmxlib/formats/sdmx_csv/reader.py +147 -0
- sdmxlib/formats/sdmx_json/__init__.py +1 -0
- sdmxlib/formats/sdmx_json/reader.py +897 -0
- sdmxlib/formats/sdmx_json/writer.py +698 -0
- sdmxlib/formats/sdmx_ml21/__init__.py +5 -0
- sdmxlib/formats/sdmx_ml21/namespaces.py +12 -0
- sdmxlib/formats/sdmx_ml21/reader.py +1147 -0
- sdmxlib/formats/sdmx_ml21/writer.py +709 -0
- sdmxlib/formats/sdmx_ml30/__init__.py +1 -0
- sdmxlib/formats/sdmx_ml30/namespaces.py +12 -0
- sdmxlib/formats/sdmx_ml30/reader.py +1138 -0
- sdmxlib/formats/sdmx_ml30/writer.py +730 -0
- sdmxlib/local/__init__.py +6 -0
- sdmxlib/local/_registry.py +368 -0
- sdmxlib/model/__init__.py +122 -0
- sdmxlib/model/annotations.py +103 -0
- sdmxlib/model/base.py +61 -0
- sdmxlib/model/category.py +88 -0
- sdmxlib/model/codelist.py +91 -0
- sdmxlib/model/collections.py +382 -0
- sdmxlib/model/concept.py +121 -0
- sdmxlib/model/constraint.py +92 -0
- sdmxlib/model/convert.py +268 -0
- sdmxlib/model/dataflow.py +49 -0
- sdmxlib/model/dataset.py +337 -0
- sdmxlib/model/datastructure.py +390 -0
- sdmxlib/model/expr.py +120 -0
- sdmxlib/model/hierarchy.py +111 -0
- sdmxlib/model/istring.py +81 -0
- sdmxlib/model/mapping.py +134 -0
- sdmxlib/model/message.py +70 -0
- sdmxlib/model/organisation.py +149 -0
- sdmxlib/model/provision.py +45 -0
- sdmxlib/model/ref.py +195 -0
- sdmxlib/model/registry.py +191 -0
- sdmxlib/model/representation.py +99 -0
- sdmxlib/model/urn.py +130 -0
- sdmxlib/model/validation.py +338 -0
- sdmxlib/polars.py +392 -0
- sdmxlib/py.typed +0 -0
- sdmxlib/storage/__init__.py +28 -0
- sdmxlib/storage/lazy.py +329 -0
- sdmxlib/storage/readers.py +310 -0
- sdmxlib/storage/schema.py +289 -0
- sdmxlib/storage/writers.py +503 -0
- sdmxlib-0.8.0.dist-info/METADATA +102 -0
- sdmxlib-0.8.0.dist-info/RECORD +58 -0
- sdmxlib-0.8.0.dist-info/WHEEL +4 -0
sdmxlib/__init__.py
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""sdmxlib — SDMX structural metadata library for Python."""
|
|
2
|
+
|
|
3
|
+
from sdmxlib.api.filters import DimFilter, dim
|
|
4
|
+
from sdmxlib.api.providers import Provider
|
|
5
|
+
from sdmxlib.api.query import DataQuery
|
|
6
|
+
from sdmxlib.api.registry import BearerToken, FmrRegistry, RestRegistry
|
|
7
|
+
from sdmxlib.formats import parse, read, serialize, to_json, to_xml
|
|
8
|
+
from sdmxlib.local import LazyCodelist, LocalRegistry
|
|
9
|
+
from sdmxlib.model.convert import from_dict, to_dict
|
|
10
|
+
from sdmxlib.model.validation import ReferenceIssue, ValidationIssue, validate, validate_references
|
|
11
|
+
from sdmxlib.model import (
|
|
12
|
+
Agency,
|
|
13
|
+
AgencyScheme,
|
|
14
|
+
Annotation,
|
|
15
|
+
Annotations,
|
|
16
|
+
ArtefactList,
|
|
17
|
+
AttachmentLevel,
|
|
18
|
+
Attribute,
|
|
19
|
+
Categorisation,
|
|
20
|
+
Category,
|
|
21
|
+
CategoryScheme,
|
|
22
|
+
Code,
|
|
23
|
+
Codelist,
|
|
24
|
+
Concept,
|
|
25
|
+
ConceptScheme,
|
|
26
|
+
CategoryMap,
|
|
27
|
+
CategorySchemeMap,
|
|
28
|
+
CodeMap,
|
|
29
|
+
CodelistMap,
|
|
30
|
+
ComponentMap,
|
|
31
|
+
ConceptMap,
|
|
32
|
+
ConceptSchemeMap,
|
|
33
|
+
ConstraintAttachment,
|
|
34
|
+
Contact,
|
|
35
|
+
ContentConstraint,
|
|
36
|
+
CubeRegion,
|
|
37
|
+
ReferencePeriod,
|
|
38
|
+
ReleaseCalendar,
|
|
39
|
+
DataProvider,
|
|
40
|
+
DataProviderScheme,
|
|
41
|
+
DataStructure,
|
|
42
|
+
Dataset,
|
|
43
|
+
DataStructureMap,
|
|
44
|
+
Dataflow,
|
|
45
|
+
DataflowMap,
|
|
46
|
+
Dimension,
|
|
47
|
+
Hierarchy,
|
|
48
|
+
HierarchicalCode,
|
|
49
|
+
Group,
|
|
50
|
+
InternationalString,
|
|
51
|
+
ItemExpr,
|
|
52
|
+
ItemList,
|
|
53
|
+
ItemPredicate,
|
|
54
|
+
KeyValue,
|
|
55
|
+
item,
|
|
56
|
+
Maintainable,
|
|
57
|
+
MaintainableArtefact,
|
|
58
|
+
Measure,
|
|
59
|
+
MeasureDimension,
|
|
60
|
+
ProvisionAgreement,
|
|
61
|
+
DataType,
|
|
62
|
+
Facet,
|
|
63
|
+
Ref,
|
|
64
|
+
Registry,
|
|
65
|
+
Representation,
|
|
66
|
+
SdmxRef,
|
|
67
|
+
SdmxUrn,
|
|
68
|
+
StructureMessage,
|
|
69
|
+
StructureSet,
|
|
70
|
+
TimeDimension,
|
|
71
|
+
UnresolvedRef,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"Agency",
|
|
76
|
+
"AgencyScheme",
|
|
77
|
+
"Annotation",
|
|
78
|
+
"Annotations",
|
|
79
|
+
"ArtefactList",
|
|
80
|
+
"AttachmentLevel",
|
|
81
|
+
"Attribute",
|
|
82
|
+
"BearerToken",
|
|
83
|
+
"Categorisation",
|
|
84
|
+
"Category",
|
|
85
|
+
"CategoryMap",
|
|
86
|
+
"CategoryScheme",
|
|
87
|
+
"CategorySchemeMap",
|
|
88
|
+
"Code",
|
|
89
|
+
"CodeMap",
|
|
90
|
+
"Codelist",
|
|
91
|
+
"CodelistMap",
|
|
92
|
+
"ComponentMap",
|
|
93
|
+
"Concept",
|
|
94
|
+
"ConceptMap",
|
|
95
|
+
"ConceptScheme",
|
|
96
|
+
"ConceptSchemeMap",
|
|
97
|
+
"ConstraintAttachment",
|
|
98
|
+
"Contact",
|
|
99
|
+
"ContentConstraint",
|
|
100
|
+
"CubeRegion",
|
|
101
|
+
"DataProvider",
|
|
102
|
+
"DataProviderScheme",
|
|
103
|
+
"DataQuery",
|
|
104
|
+
"DataStructure",
|
|
105
|
+
"DataStructureMap",
|
|
106
|
+
"DataType",
|
|
107
|
+
"Dataflow",
|
|
108
|
+
"DataflowMap",
|
|
109
|
+
"Dataset",
|
|
110
|
+
"DimFilter",
|
|
111
|
+
"Dimension",
|
|
112
|
+
"Facet",
|
|
113
|
+
"FmrRegistry",
|
|
114
|
+
"Group",
|
|
115
|
+
"HierarchicalCode",
|
|
116
|
+
"Hierarchy",
|
|
117
|
+
"InternationalString",
|
|
118
|
+
"ItemExpr",
|
|
119
|
+
"ItemList",
|
|
120
|
+
"ItemPredicate",
|
|
121
|
+
"KeyValue",
|
|
122
|
+
"LazyCodelist",
|
|
123
|
+
"LocalRegistry",
|
|
124
|
+
"Maintainable",
|
|
125
|
+
"MaintainableArtefact",
|
|
126
|
+
"Measure",
|
|
127
|
+
"MeasureDimension",
|
|
128
|
+
"Provider",
|
|
129
|
+
"ProvisionAgreement",
|
|
130
|
+
"Ref",
|
|
131
|
+
"ReferenceIssue",
|
|
132
|
+
"ReferencePeriod",
|
|
133
|
+
"Registry",
|
|
134
|
+
"ReleaseCalendar",
|
|
135
|
+
"Representation",
|
|
136
|
+
"RestRegistry",
|
|
137
|
+
"SdmxRef",
|
|
138
|
+
"SdmxUrn",
|
|
139
|
+
"StructureMessage",
|
|
140
|
+
"StructureSet",
|
|
141
|
+
"TimeDimension",
|
|
142
|
+
"UnresolvedRef",
|
|
143
|
+
"ValidationIssue",
|
|
144
|
+
"dim",
|
|
145
|
+
"from_dict",
|
|
146
|
+
"item",
|
|
147
|
+
"parse",
|
|
148
|
+
"read",
|
|
149
|
+
"serialize",
|
|
150
|
+
"to_dict",
|
|
151
|
+
"to_json",
|
|
152
|
+
"to_xml",
|
|
153
|
+
"validate",
|
|
154
|
+
"validate_references",
|
|
155
|
+
]
|
sdmxlib/api/__init__.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"""sdmxlib.api — HTTP client and registry."""
|
|
2
|
+
|
|
3
|
+
from sdmxlib.api.providers import Provider
|
|
4
|
+
from sdmxlib.api.query import GetQuery, Query
|
|
5
|
+
from sdmxlib.api.registry import BearerToken, FmrRegistry, RestRegistry
|
|
6
|
+
|
|
7
|
+
__all__ = ["BearerToken", "FmrRegistry", "GetQuery", "Provider", "Query", "RestRegistry"]
|
sdmxlib/api/client.py
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""SdmxClient — thin httpx wrapper around the SDMX REST API."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from sdmxlib.formats import parse
|
|
9
|
+
from sdmxlib.model.message import StructureMessage
|
|
10
|
+
|
|
11
|
+
# SDMX REST resource path segments (lowercase endpoint name)
|
|
12
|
+
_RESOURCE_PATH: dict[str, str] = {
|
|
13
|
+
"DataStructure": "datastructure",
|
|
14
|
+
"Codelist": "codelist",
|
|
15
|
+
"Dataflow": "dataflow",
|
|
16
|
+
"ConceptScheme": "conceptscheme",
|
|
17
|
+
"ProvisionAgreement": "provisionagreement",
|
|
18
|
+
"CategoryScheme": "categoryscheme",
|
|
19
|
+
"Categorisation": "categorisation",
|
|
20
|
+
"ContentConstraint": "contentconstraint",
|
|
21
|
+
"AgencyScheme": "agencyscheme",
|
|
22
|
+
"DataProviderScheme": "dataproviderorganisationscheme",
|
|
23
|
+
"Hierarchy": "hierarchy",
|
|
24
|
+
"StructureSet": "structureset",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# Wire parameter presets — (detail, references)
|
|
28
|
+
_BROWSE_PARAMS = {"detail": "allstubs", "references": "none"}
|
|
29
|
+
_FETCH_PARAMS = {"detail": "full", "references": "none"}
|
|
30
|
+
_RESOLVE_PARAMS = {"detail": "referencepartial", "references": "descendants"}
|
|
31
|
+
_RESOLVE_FULL = {"detail": "full", "references": "descendants"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SdmxClient:
|
|
35
|
+
"""Thin httpx wrapper that fetches SDMX REST resources and parses them.
|
|
36
|
+
|
|
37
|
+
Handles URL construction, Accept headers, and response parsing.
|
|
38
|
+
Does not cache — caching is the Session's responsibility.
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
endpoint: Base URL of the SDMX REST endpoint.
|
|
42
|
+
api: SDMX REST API version — ``"2.1"`` (default) or ``"3.0"``.
|
|
43
|
+
Controls URL scheme and response format.
|
|
44
|
+
timeout: HTTP request timeout in seconds.
|
|
45
|
+
headers: Extra HTTP headers merged with defaults.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
def __init__(
|
|
49
|
+
self,
|
|
50
|
+
endpoint: str,
|
|
51
|
+
*,
|
|
52
|
+
api: str = "2.1",
|
|
53
|
+
timeout: float = 30.0,
|
|
54
|
+
headers: dict[str, str] | None = None,
|
|
55
|
+
**httpx_kwargs: object,
|
|
56
|
+
) -> None:
|
|
57
|
+
self._endpoint = endpoint.rstrip("/")
|
|
58
|
+
self._api = api
|
|
59
|
+
self._v3 = api.startswith(("3", "2.0"))
|
|
60
|
+
default_headers: dict[str, str]
|
|
61
|
+
if self._v3:
|
|
62
|
+
default_headers = {"Accept": "application/vnd.sdmx.structure+xml;version=3.0"}
|
|
63
|
+
else:
|
|
64
|
+
default_headers = {"Accept": "application/xml"}
|
|
65
|
+
if headers:
|
|
66
|
+
default_headers.update(headers)
|
|
67
|
+
self._client = httpx.Client(
|
|
68
|
+
headers=default_headers,
|
|
69
|
+
timeout=timeout,
|
|
70
|
+
**httpx_kwargs, # type: ignore[arg-type]
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def close(self) -> None:
|
|
74
|
+
"""Close the underlying HTTP client."""
|
|
75
|
+
self._client.close()
|
|
76
|
+
|
|
77
|
+
def __enter__(self) -> Self:
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, *args: object) -> None:
|
|
81
|
+
self.close()
|
|
82
|
+
|
|
83
|
+
# ── URL building ──────────────────────────────────────────────────────────
|
|
84
|
+
|
|
85
|
+
def _resource_url(
|
|
86
|
+
self,
|
|
87
|
+
resource: str,
|
|
88
|
+
agency: str,
|
|
89
|
+
id: str, # noqa: A002
|
|
90
|
+
version: str,
|
|
91
|
+
) -> str:
|
|
92
|
+
"""Build the resource URL for the given API version."""
|
|
93
|
+
path = _RESOURCE_PATH.get(resource, resource.lower())
|
|
94
|
+
if self._v3:
|
|
95
|
+
# v2.x: /structure/{resource}/{agency}/{id}/{version}
|
|
96
|
+
# "latest" sentinel becomes "~" on the wire
|
|
97
|
+
ver = "~" if version == "latest" else version
|
|
98
|
+
return f"{self._endpoint}/structure/{path}/{agency}/{id}/{ver}"
|
|
99
|
+
# v1.5: /{resource}/{agency}/{id}/{version}
|
|
100
|
+
return f"{self._endpoint}/{path}/{agency}/{id}/{version}"
|
|
101
|
+
|
|
102
|
+
def _collection_url(self, resource: str, agency: str | None) -> str:
|
|
103
|
+
"""Build the collection URL (all artefacts of a type)."""
|
|
104
|
+
path = _RESOURCE_PATH.get(resource, resource.lower())
|
|
105
|
+
agency_seg = agency or ("*" if self._v3 else "all")
|
|
106
|
+
if self._v3:
|
|
107
|
+
return f"{self._endpoint}/structure/{path}/{agency_seg}"
|
|
108
|
+
return f"{self._endpoint}/{path}/{agency_seg}"
|
|
109
|
+
|
|
110
|
+
# ── Parse version ─────────────────────────────────────────────────────────
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def _parse_version(self) -> str:
|
|
114
|
+
"""Format version string passed to the parser."""
|
|
115
|
+
return "3.0" if self._v3 else "2.1"
|
|
116
|
+
|
|
117
|
+
# ── HTTP methods ──────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
def get_artefact(
|
|
120
|
+
self,
|
|
121
|
+
resource: str,
|
|
122
|
+
agency: str,
|
|
123
|
+
id: str, # noqa: A002
|
|
124
|
+
version: str = "latest",
|
|
125
|
+
*,
|
|
126
|
+
detail: str = "full",
|
|
127
|
+
references: str = "none",
|
|
128
|
+
) -> StructureMessage:
|
|
129
|
+
"""Fetch a single maintainable artefact."""
|
|
130
|
+
url = self._resource_url(resource, agency, id, version)
|
|
131
|
+
params: dict[str, str] = {"detail": detail}
|
|
132
|
+
if references != "none":
|
|
133
|
+
params["references"] = references
|
|
134
|
+
response = self._client.get(url, params=params)
|
|
135
|
+
response.raise_for_status()
|
|
136
|
+
return parse(response.content, version=self._parse_version)
|
|
137
|
+
|
|
138
|
+
def get_all(
|
|
139
|
+
self,
|
|
140
|
+
resource: str,
|
|
141
|
+
agency: str | None = None,
|
|
142
|
+
*,
|
|
143
|
+
detail: str = "full",
|
|
144
|
+
references: str = "none",
|
|
145
|
+
) -> StructureMessage:
|
|
146
|
+
"""Fetch all artefacts of a type, optionally filtered by agency."""
|
|
147
|
+
url = self._collection_url(resource, agency)
|
|
148
|
+
params: dict[str, str] = {"detail": detail}
|
|
149
|
+
if references != "none":
|
|
150
|
+
params["references"] = references
|
|
151
|
+
response = self._client.get(url, params=params)
|
|
152
|
+
response.raise_for_status()
|
|
153
|
+
return parse(response.content, version=self._parse_version)
|
|
154
|
+
|
|
155
|
+
def _data_url(
|
|
156
|
+
self,
|
|
157
|
+
agency: str,
|
|
158
|
+
id: str, # noqa: A002
|
|
159
|
+
version: str,
|
|
160
|
+
key: str,
|
|
161
|
+
) -> str:
|
|
162
|
+
"""Build the data URL for the given API version."""
|
|
163
|
+
if self._v3:
|
|
164
|
+
ver = "~" if version == "latest" else version
|
|
165
|
+
return f"{self._endpoint}/data/dataflow/{agency}/{id}/{ver}/{key}"
|
|
166
|
+
return f"{self._endpoint}/data/{agency},{id},{version}/{key}"
|
|
167
|
+
|
|
168
|
+
def get_data(
|
|
169
|
+
self,
|
|
170
|
+
agency: str,
|
|
171
|
+
id: str, # noqa: A002
|
|
172
|
+
version: str = "latest",
|
|
173
|
+
*,
|
|
174
|
+
key: str = "all",
|
|
175
|
+
start_period: str | None = None,
|
|
176
|
+
end_period: str | None = None,
|
|
177
|
+
) -> bytes:
|
|
178
|
+
"""Fetch observation data as SDMX-CSV bytes.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
agency: Dataflow maintainer agency id.
|
|
182
|
+
id: Dataflow id.
|
|
183
|
+
version: Dataflow version. Defaults to ``"latest"``.
|
|
184
|
+
key: SDMX key filter string, e.g. ``"M.AT+DE.INDPRO"``.
|
|
185
|
+
Defaults to ``"all"`` (all observations).
|
|
186
|
+
start_period: ISO period start, e.g. ``"2020-01"``.
|
|
187
|
+
end_period: ISO period end.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Raw SDMX-CSV bytes.
|
|
191
|
+
"""
|
|
192
|
+
url = self._data_url(agency, id, version, key)
|
|
193
|
+
params: dict[str, str] = {}
|
|
194
|
+
if start_period is not None:
|
|
195
|
+
params["startPeriod"] = start_period
|
|
196
|
+
if end_period is not None:
|
|
197
|
+
params["endPeriod"] = end_period
|
|
198
|
+
response = self._client.get(
|
|
199
|
+
url,
|
|
200
|
+
params=params,
|
|
201
|
+
headers={"Accept": "text/csv"},
|
|
202
|
+
)
|
|
203
|
+
response.raise_for_status()
|
|
204
|
+
return response.content
|
|
205
|
+
|
|
206
|
+
def stream_data_to(
|
|
207
|
+
self,
|
|
208
|
+
agency: str,
|
|
209
|
+
id: str, # noqa: A002
|
|
210
|
+
version: str = "latest",
|
|
211
|
+
*,
|
|
212
|
+
path: Path,
|
|
213
|
+
key: str = "all",
|
|
214
|
+
start_period: str | None = None,
|
|
215
|
+
end_period: str | None = None,
|
|
216
|
+
) -> None:
|
|
217
|
+
"""Stream observation data directly to a file.
|
|
218
|
+
|
|
219
|
+
Uses httpx chunked streaming — the response is never fully loaded into
|
|
220
|
+
memory. Suitable for multi-GB datasets.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
agency: Dataflow maintainer agency id.
|
|
224
|
+
id: Dataflow id.
|
|
225
|
+
version: Dataflow version.
|
|
226
|
+
path: Destination file path.
|
|
227
|
+
key: SDMX key filter string.
|
|
228
|
+
start_period: ISO period start.
|
|
229
|
+
end_period: ISO period end.
|
|
230
|
+
"""
|
|
231
|
+
url = self._data_url(agency, id, version, key)
|
|
232
|
+
params: dict[str, str] = {}
|
|
233
|
+
if start_period is not None:
|
|
234
|
+
params["startPeriod"] = start_period
|
|
235
|
+
if end_period is not None:
|
|
236
|
+
params["endPeriod"] = end_period
|
|
237
|
+
with self._client.stream(
|
|
238
|
+
"GET",
|
|
239
|
+
url,
|
|
240
|
+
params=params,
|
|
241
|
+
headers={"Accept": "text/csv"},
|
|
242
|
+
) as response:
|
|
243
|
+
response.raise_for_status()
|
|
244
|
+
with path.open("wb") as f:
|
|
245
|
+
for chunk in response.iter_bytes():
|
|
246
|
+
f.write(chunk)
|
sdmxlib/api/filters.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Server-side dimension filter expressions for DataQuery.slice()."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from attrs import frozen
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@frozen
|
|
9
|
+
class DimFilter:
|
|
10
|
+
"""A resolved server-side filter on a single dimension.
|
|
11
|
+
|
|
12
|
+
Produced by :func:`dim` expressions — not constructed directly.
|
|
13
|
+
|
|
14
|
+
Attributes:
|
|
15
|
+
id: DSD dimension id.
|
|
16
|
+
codes: One or more codes to match (OR semantics, ``+``-joined on the wire).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
id: str
|
|
20
|
+
codes: list[str]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class _DimExpr:
|
|
24
|
+
"""Builder returned by :func:`dim`. Produces a :class:`DimFilter` via ``==`` or ``.isin()``."""
|
|
25
|
+
|
|
26
|
+
def __init__(self, id: str) -> None: # noqa: A002
|
|
27
|
+
self._id = id
|
|
28
|
+
|
|
29
|
+
def __eq__(self, other: object) -> DimFilter: # type: ignore[override]
|
|
30
|
+
if not isinstance(other, str):
|
|
31
|
+
msg = f"Expected a code string, got {type(other).__name__!r}"
|
|
32
|
+
raise TypeError(msg)
|
|
33
|
+
return DimFilter(id=self._id, codes=[other])
|
|
34
|
+
|
|
35
|
+
def isin(self, codes: list[str]) -> DimFilter:
|
|
36
|
+
"""Match any of the given codes (server-side OR — ``+``-joined in the key).
|
|
37
|
+
|
|
38
|
+
Codes are sorted for stable, deterministic URLs.
|
|
39
|
+
|
|
40
|
+
Example:
|
|
41
|
+
sl.dim("REF_AREA").isin(["AT", "DE", "FR"])
|
|
42
|
+
"""
|
|
43
|
+
return DimFilter(id=self._id, codes=sorted(codes))
|
|
44
|
+
|
|
45
|
+
def __hash__(self) -> int:
|
|
46
|
+
return hash(self._id)
|
|
47
|
+
|
|
48
|
+
def __repr__(self) -> str:
|
|
49
|
+
return f"dim({self._id!r})"
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def dim(id: str) -> _DimExpr: # noqa: A002
|
|
53
|
+
"""Create a server-side dimension filter expression for :meth:`DataQuery.slice`.
|
|
54
|
+
|
|
55
|
+
Use ``==`` for a single code or ``.isin()`` for multiple codes (OR semantics).
|
|
56
|
+
Dimensions not mentioned in any filter become wildcards in the SDMX REST key.
|
|
57
|
+
|
|
58
|
+
Example:
|
|
59
|
+
sl.dim("FREQ") == "M"
|
|
60
|
+
sl.dim("REF_AREA").isin(["AT", "DE", "FR"])
|
|
61
|
+
|
|
62
|
+
reg.data(flow).slice(
|
|
63
|
+
sl.dim("FREQ") == "M",
|
|
64
|
+
sl.dim("REF_AREA").isin(["AT", "DE"]),
|
|
65
|
+
).fetch()
|
|
66
|
+
"""
|
|
67
|
+
return _DimExpr(id)
|
sdmxlib/api/providers.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Known SDMX REST endpoint URLs and API versions.
|
|
2
|
+
|
|
3
|
+
Use as a shortcut when constructing a RestRegistry::
|
|
4
|
+
|
|
5
|
+
with sl.RestRegistry(sl.Provider.ECB) as reg:
|
|
6
|
+
...
|
|
7
|
+
with sl.RestRegistry(sl.Provider["ECB"]) as reg:
|
|
8
|
+
...
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from attrs import frozen
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@frozen
|
|
15
|
+
class ProviderInfo:
|
|
16
|
+
"""URL and REST API version for a known SDMX endpoint."""
|
|
17
|
+
|
|
18
|
+
url: str
|
|
19
|
+
api: str = "2.1"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Provider:
|
|
23
|
+
"""Built-in SDMX REST endpoint URLs.
|
|
24
|
+
|
|
25
|
+
Each entry is a :class:`ProviderInfo` carrying the endpoint URL and its
|
|
26
|
+
REST API version. Pass directly to ``RestRegistry``::
|
|
27
|
+
|
|
28
|
+
with sl.RestRegistry(sl.Provider.ECB) as reg:
|
|
29
|
+
...
|
|
30
|
+
with sl.RestRegistry(sl.Provider.BIS) as reg:
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
Supports both attribute and subscript access::
|
|
34
|
+
|
|
35
|
+
sl.Provider.ECB # ProviderInfo(url="...", api="2.1")
|
|
36
|
+
sl.Provider["ECB"] # same
|
|
37
|
+
sl.Provider.ECB.url # "https://data-api.ecb.europa.eu/service"
|
|
38
|
+
sl.Provider.ECB.api # "2.1"
|
|
39
|
+
"""
|
|
40
|
+
|
|
41
|
+
#: European Central Bank
|
|
42
|
+
ECB = ProviderInfo("https://data-api.ecb.europa.eu/service", api="2.1")
|
|
43
|
+
#: Bank for International Settlements
|
|
44
|
+
BIS = ProviderInfo("https://stats.bis.org/api/v1", api="2.1")
|
|
45
|
+
#: Eurostat
|
|
46
|
+
ESTAT = ProviderInfo("https://ec.europa.eu/eurostat/api/dissemination/sdmx/2.1", api="2.1")
|
|
47
|
+
#: International Monetary Fund
|
|
48
|
+
IMF = ProviderInfo("https://sdmxcentral.imf.org/ws/public/sdmxapi/rest", api="2.1")
|
|
49
|
+
#: OECD
|
|
50
|
+
OECD = ProviderInfo("https://sdmx.oecd.org/public/rest", api="2.1")
|
|
51
|
+
#: SDMX Global Registry (FMR — SDMX 3.0 endpoint)
|
|
52
|
+
SDMX_GLOBAL = ProviderInfo("https://registry.sdmx.org/sdmx/v2", api="3.0")
|
|
53
|
+
|
|
54
|
+
def __class_getitem__(cls, key: str) -> ProviderInfo:
|
|
55
|
+
val = getattr(cls, key, None)
|
|
56
|
+
if not isinstance(val, ProviderInfo):
|
|
57
|
+
available = ", ".join(
|
|
58
|
+
k for k in vars(cls) if not k.startswith("_") and isinstance(getattr(cls, k), ProviderInfo)
|
|
59
|
+
)
|
|
60
|
+
msg = f"{key!r} — available: {available}"
|
|
61
|
+
raise KeyError(msg)
|
|
62
|
+
return val
|
|
63
|
+
|
|
64
|
+
def __init_subclass__(cls, **kwargs: object) -> None:
|
|
65
|
+
msg = "Provider cannot be subclassed"
|
|
66
|
+
raise TypeError(msg)
|