sdmxlib 0.8.0__tar.gz
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-0.8.0/PKG-INFO +102 -0
- sdmxlib-0.8.0/README.md +76 -0
- sdmxlib-0.8.0/pyproject.toml +79 -0
- sdmxlib-0.8.0/src/sdmxlib/__init__.py +155 -0
- sdmxlib-0.8.0/src/sdmxlib/api/__init__.py +7 -0
- sdmxlib-0.8.0/src/sdmxlib/api/client.py +246 -0
- sdmxlib-0.8.0/src/sdmxlib/api/filters.py +67 -0
- sdmxlib-0.8.0/src/sdmxlib/api/providers.py +66 -0
- sdmxlib-0.8.0/src/sdmxlib/api/query.py +428 -0
- sdmxlib-0.8.0/src/sdmxlib/api/registry.py +666 -0
- sdmxlib-0.8.0/src/sdmxlib/api/session.py +124 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/__init__.py +166 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_csv/__init__.py +5 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_csv/reader.py +147 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_json/__init__.py +1 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_json/reader.py +897 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_json/writer.py +698 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml21/__init__.py +5 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml21/namespaces.py +12 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml21/reader.py +1147 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml21/writer.py +709 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml30/__init__.py +1 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml30/namespaces.py +12 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml30/reader.py +1138 -0
- sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml30/writer.py +730 -0
- sdmxlib-0.8.0/src/sdmxlib/local/__init__.py +6 -0
- sdmxlib-0.8.0/src/sdmxlib/local/_registry.py +368 -0
- sdmxlib-0.8.0/src/sdmxlib/model/__init__.py +122 -0
- sdmxlib-0.8.0/src/sdmxlib/model/annotations.py +103 -0
- sdmxlib-0.8.0/src/sdmxlib/model/base.py +61 -0
- sdmxlib-0.8.0/src/sdmxlib/model/category.py +88 -0
- sdmxlib-0.8.0/src/sdmxlib/model/codelist.py +91 -0
- sdmxlib-0.8.0/src/sdmxlib/model/collections.py +382 -0
- sdmxlib-0.8.0/src/sdmxlib/model/concept.py +121 -0
- sdmxlib-0.8.0/src/sdmxlib/model/constraint.py +92 -0
- sdmxlib-0.8.0/src/sdmxlib/model/convert.py +268 -0
- sdmxlib-0.8.0/src/sdmxlib/model/dataflow.py +49 -0
- sdmxlib-0.8.0/src/sdmxlib/model/dataset.py +337 -0
- sdmxlib-0.8.0/src/sdmxlib/model/datastructure.py +390 -0
- sdmxlib-0.8.0/src/sdmxlib/model/expr.py +120 -0
- sdmxlib-0.8.0/src/sdmxlib/model/hierarchy.py +111 -0
- sdmxlib-0.8.0/src/sdmxlib/model/istring.py +81 -0
- sdmxlib-0.8.0/src/sdmxlib/model/mapping.py +134 -0
- sdmxlib-0.8.0/src/sdmxlib/model/message.py +70 -0
- sdmxlib-0.8.0/src/sdmxlib/model/organisation.py +149 -0
- sdmxlib-0.8.0/src/sdmxlib/model/provision.py +45 -0
- sdmxlib-0.8.0/src/sdmxlib/model/ref.py +195 -0
- sdmxlib-0.8.0/src/sdmxlib/model/registry.py +191 -0
- sdmxlib-0.8.0/src/sdmxlib/model/representation.py +99 -0
- sdmxlib-0.8.0/src/sdmxlib/model/urn.py +130 -0
- sdmxlib-0.8.0/src/sdmxlib/model/validation.py +338 -0
- sdmxlib-0.8.0/src/sdmxlib/polars.py +392 -0
- sdmxlib-0.8.0/src/sdmxlib/py.typed +0 -0
- sdmxlib-0.8.0/src/sdmxlib/storage/__init__.py +28 -0
- sdmxlib-0.8.0/src/sdmxlib/storage/lazy.py +329 -0
- sdmxlib-0.8.0/src/sdmxlib/storage/readers.py +310 -0
- sdmxlib-0.8.0/src/sdmxlib/storage/schema.py +289 -0
- sdmxlib-0.8.0/src/sdmxlib/storage/writers.py +503 -0
sdmxlib-0.8.0/PKG-INFO
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: sdmxlib
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: SDMX structural metadata library for Python
|
|
5
|
+
Keywords: sdmx,statistics,metadata,datastructure
|
|
6
|
+
Author: gabrielgellner
|
|
7
|
+
Author-email: gabrielgellner <gabrielgellner@gmail.com>
|
|
8
|
+
License: Apache-2.0
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
15
|
+
Requires-Dist: attrs>=26.1.0
|
|
16
|
+
Requires-Dist: duckdb>=1.0.0
|
|
17
|
+
Requires-Dist: httpx>=0.28.1
|
|
18
|
+
Requires-Dist: lxml>=6.0.2
|
|
19
|
+
Requires-Dist: polars>=1.39.3
|
|
20
|
+
Requires-Python: >=3.12
|
|
21
|
+
Project-URL: Homepage, https://gitlab.com/gabrielgellner/sdmxlib
|
|
22
|
+
Project-URL: Repository, https://gitlab.com/gabrielgellner/sdmxlib
|
|
23
|
+
Project-URL: Documentation, https://gabrielgellner.gitlab.io/sdmxlib
|
|
24
|
+
Project-URL: Changelog, https://gitlab.com/gabrielgellner/sdmxlib/-/blob/main/CHANGELOG.md
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# sdmxlib
|
|
28
|
+
|
|
29
|
+
> **Note:** This library has been written extensively with AI assistance
|
|
30
|
+
> (Claude Code). Users who are not comfortable with AI-generated code should
|
|
31
|
+
> take that into account before adopting it.
|
|
32
|
+
|
|
33
|
+
SDMX structural metadata library for Python. Fetch and navigate Data Structure
|
|
34
|
+
Definitions, Codelists, Dataflows, and related artefacts from any SDMX REST
|
|
35
|
+
endpoint — with plain attribute access, no XML or URN chasing required.
|
|
36
|
+
|
|
37
|
+
## Quick start
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
import sdmxlib as sl
|
|
41
|
+
|
|
42
|
+
with sl.RestRegistry(sl.Provider.BIS) as reg:
|
|
43
|
+
dsd = reg.get(sl.DataStructure, agency="BIS", id="WS_CBPOL").resolve()
|
|
44
|
+
|
|
45
|
+
for dim in dsd.dimensions:
|
|
46
|
+
if dim.is_enumerated:
|
|
47
|
+
cl = dim.representation()
|
|
48
|
+
print(f"{dim.id}: {cl.id} ({len(cl.codes)} codes)")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
FREQ: CL_FREQ (7 codes)
|
|
53
|
+
REF_AREA: CL_BIS_IF_REF_AREA (44 codes)
|
|
54
|
+
INSTR_ASSET: CL_INSTR_ASSET (12 codes)
|
|
55
|
+
BORROWER_CTY: CL_BIS_IF_REF_AREA (44 codes)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Installation
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
uv add "sdmxlib @ git+https://gitlab.com/gabrielgellner/sdmxlib.git"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## What it does
|
|
65
|
+
|
|
66
|
+
- Connects to any SDMX 2.1 or 3.0 REST endpoint; built-in shortcuts for ECB,
|
|
67
|
+
BIS, ESTAT, IMF, OECD, and the SDMX Global Registry via `sl.Provider`
|
|
68
|
+
- Three resolution modes: `browse()` for catalogue scanning, `fetch()` for
|
|
69
|
+
unresolved artefacts, `resolve()` for the full reference graph (with optional
|
|
70
|
+
constraint-trimming via `partial=True/False`)
|
|
71
|
+
- Returns fully resolved domain objects with plain attribute access — no XML,
|
|
72
|
+
no URN chasing
|
|
73
|
+
- Filter collections with lambdas or the `item()` expression API
|
|
74
|
+
- Build SDMX artefacts locally and serialise to SDMX-ML or SDMX-JSON
|
|
75
|
+
- `LocalRegistry` -- a DuckDB-backed persistent store for offline access
|
|
76
|
+
with lazy codelist loading, automatic Ref resolution, and direct SQL
|
|
77
|
+
code maps; supports shared connections and configurable language columns
|
|
78
|
+
- Integrates with [Polars](https://pola.rs) for schema generation and data
|
|
79
|
+
validation
|
|
80
|
+
- Fully typed — IDE completion and static analysis work throughout
|
|
81
|
+
|
|
82
|
+
## Documentation
|
|
83
|
+
|
|
84
|
+
See the [docs](https://gabrielgellner.gitlab.io/sdmxlib/) for the full cookbook,
|
|
85
|
+
including:
|
|
86
|
+
|
|
87
|
+
- Connecting to registries and choosing a resolution mode
|
|
88
|
+
- Persisting artefacts in a local DuckDB store with `LocalRegistry`
|
|
89
|
+
- Fetching observation data with `sl.dim()` filters and pivoting to wide format
|
|
90
|
+
- Inspecting DSDs and building code maps for ETL pipelines
|
|
91
|
+
- Generating Polars schemas from SDMX Data Structure Definitions
|
|
92
|
+
- Browsing data catalogs and filtering dataflows
|
|
93
|
+
- Working with constraints, hierarchies, and serialisation
|
|
94
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
just check # lint + typecheck
|
|
99
|
+
just test # unit tests
|
|
100
|
+
just test-integration # live endpoint tests (requires network)
|
|
101
|
+
just ci # full pre-commit gate
|
|
102
|
+
```
|
sdmxlib-0.8.0/README.md
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# sdmxlib
|
|
2
|
+
|
|
3
|
+
> **Note:** This library has been written extensively with AI assistance
|
|
4
|
+
> (Claude Code). Users who are not comfortable with AI-generated code should
|
|
5
|
+
> take that into account before adopting it.
|
|
6
|
+
|
|
7
|
+
SDMX structural metadata library for Python. Fetch and navigate Data Structure
|
|
8
|
+
Definitions, Codelists, Dataflows, and related artefacts from any SDMX REST
|
|
9
|
+
endpoint — with plain attribute access, no XML or URN chasing required.
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
import sdmxlib as sl
|
|
15
|
+
|
|
16
|
+
with sl.RestRegistry(sl.Provider.BIS) as reg:
|
|
17
|
+
dsd = reg.get(sl.DataStructure, agency="BIS", id="WS_CBPOL").resolve()
|
|
18
|
+
|
|
19
|
+
for dim in dsd.dimensions:
|
|
20
|
+
if dim.is_enumerated:
|
|
21
|
+
cl = dim.representation()
|
|
22
|
+
print(f"{dim.id}: {cl.id} ({len(cl.codes)} codes)")
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
FREQ: CL_FREQ (7 codes)
|
|
27
|
+
REF_AREA: CL_BIS_IF_REF_AREA (44 codes)
|
|
28
|
+
INSTR_ASSET: CL_INSTR_ASSET (12 codes)
|
|
29
|
+
BORROWER_CTY: CL_BIS_IF_REF_AREA (44 codes)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
uv add "sdmxlib @ git+https://gitlab.com/gabrielgellner/sdmxlib.git"
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## What it does
|
|
39
|
+
|
|
40
|
+
- Connects to any SDMX 2.1 or 3.0 REST endpoint; built-in shortcuts for ECB,
|
|
41
|
+
BIS, ESTAT, IMF, OECD, and the SDMX Global Registry via `sl.Provider`
|
|
42
|
+
- Three resolution modes: `browse()` for catalogue scanning, `fetch()` for
|
|
43
|
+
unresolved artefacts, `resolve()` for the full reference graph (with optional
|
|
44
|
+
constraint-trimming via `partial=True/False`)
|
|
45
|
+
- Returns fully resolved domain objects with plain attribute access — no XML,
|
|
46
|
+
no URN chasing
|
|
47
|
+
- Filter collections with lambdas or the `item()` expression API
|
|
48
|
+
- Build SDMX artefacts locally and serialise to SDMX-ML or SDMX-JSON
|
|
49
|
+
- `LocalRegistry` -- a DuckDB-backed persistent store for offline access
|
|
50
|
+
with lazy codelist loading, automatic Ref resolution, and direct SQL
|
|
51
|
+
code maps; supports shared connections and configurable language columns
|
|
52
|
+
- Integrates with [Polars](https://pola.rs) for schema generation and data
|
|
53
|
+
validation
|
|
54
|
+
- Fully typed — IDE completion and static analysis work throughout
|
|
55
|
+
|
|
56
|
+
## Documentation
|
|
57
|
+
|
|
58
|
+
See the [docs](https://gabrielgellner.gitlab.io/sdmxlib/) for the full cookbook,
|
|
59
|
+
including:
|
|
60
|
+
|
|
61
|
+
- Connecting to registries and choosing a resolution mode
|
|
62
|
+
- Persisting artefacts in a local DuckDB store with `LocalRegistry`
|
|
63
|
+
- Fetching observation data with `sl.dim()` filters and pivoting to wide format
|
|
64
|
+
- Inspecting DSDs and building code maps for ETL pipelines
|
|
65
|
+
- Generating Polars schemas from SDMX Data Structure Definitions
|
|
66
|
+
- Browsing data catalogs and filtering dataflows
|
|
67
|
+
- Working with constraints, hierarchies, and serialisation
|
|
68
|
+
|
|
69
|
+
## Development
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
just check # lint + typecheck
|
|
73
|
+
just test # unit tests
|
|
74
|
+
just test-integration # live endpoint tests (requires network)
|
|
75
|
+
just ci # full pre-commit gate
|
|
76
|
+
```
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "sdmxlib"
|
|
3
|
+
version = "0.8.0"
|
|
4
|
+
description = "SDMX structural metadata library for Python"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = { text = "Apache-2.0" }
|
|
7
|
+
authors = [{ name = "gabrielgellner", email = "gabrielgellner@gmail.com" }]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
keywords = ["sdmx", "statistics", "metadata", "datastructure"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Intended Audience :: Developers",
|
|
13
|
+
"Intended Audience :: Science/Research",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
17
|
+
]
|
|
18
|
+
dependencies = [
|
|
19
|
+
"attrs>=26.1.0",
|
|
20
|
+
"duckdb>=1.0.0",
|
|
21
|
+
"httpx>=0.28.1",
|
|
22
|
+
"lxml>=6.0.2",
|
|
23
|
+
"polars>=1.39.3",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Homepage = "https://gitlab.com/gabrielgellner/sdmxlib"
|
|
28
|
+
Repository = "https://gitlab.com/gabrielgellner/sdmxlib"
|
|
29
|
+
Documentation = "https://gabrielgellner.gitlab.io/sdmxlib"
|
|
30
|
+
Changelog = "https://gitlab.com/gabrielgellner/sdmxlib/-/blob/main/CHANGELOG.md"
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.11.2,<0.12.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[tool.pytest.ini_options]
|
|
37
|
+
testpaths = ["tests"]
|
|
38
|
+
markers = [
|
|
39
|
+
"unit: fast unit tests (no I/O)",
|
|
40
|
+
"docs: tests that verify documentation examples",
|
|
41
|
+
"integration: live endpoint tests (require network)",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
[tool.coverage.run]
|
|
45
|
+
source = ["src/sdmxlib"]
|
|
46
|
+
omit = ["src/sdmxlib/contrib/*"]
|
|
47
|
+
|
|
48
|
+
[tool.coverage.report]
|
|
49
|
+
show_missing = true
|
|
50
|
+
skip_covered = false
|
|
51
|
+
fail_under = 80
|
|
52
|
+
exclude_lines = [
|
|
53
|
+
"pragma: no cover",
|
|
54
|
+
"if TYPE_CHECKING:",
|
|
55
|
+
"raise NotImplementedError",
|
|
56
|
+
"@overload",
|
|
57
|
+
]
|
|
58
|
+
|
|
59
|
+
[tool.basedpyright]
|
|
60
|
+
pythonVersion = "3.12"
|
|
61
|
+
pythonPlatform = "Linux"
|
|
62
|
+
venvPath = "."
|
|
63
|
+
venv = ".venv"
|
|
64
|
+
typeCheckingMode = "standard"
|
|
65
|
+
reportUnusedCallResult = false
|
|
66
|
+
|
|
67
|
+
[dependency-groups]
|
|
68
|
+
dev = [
|
|
69
|
+
"basedpyright>=1.38.4",
|
|
70
|
+
"catppuccin[pygments]>=2.5.0",
|
|
71
|
+
"lxml-stubs>=0.5.1",
|
|
72
|
+
"mkdocs-material>=9.7.6",
|
|
73
|
+
"mkdocstrings[python]>=1.0.3",
|
|
74
|
+
"polars>=1.39.3",
|
|
75
|
+
"pytest>=9.0.2",
|
|
76
|
+
"pytest-cov>=7.1.0",
|
|
77
|
+
"pytest-recording>=0.13.4",
|
|
78
|
+
"ruff>=0.15.8",
|
|
79
|
+
]
|
|
@@ -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
|
+
]
|
|
@@ -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"]
|
|
@@ -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)
|