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.
Files changed (58) hide show
  1. sdmxlib-0.8.0/PKG-INFO +102 -0
  2. sdmxlib-0.8.0/README.md +76 -0
  3. sdmxlib-0.8.0/pyproject.toml +79 -0
  4. sdmxlib-0.8.0/src/sdmxlib/__init__.py +155 -0
  5. sdmxlib-0.8.0/src/sdmxlib/api/__init__.py +7 -0
  6. sdmxlib-0.8.0/src/sdmxlib/api/client.py +246 -0
  7. sdmxlib-0.8.0/src/sdmxlib/api/filters.py +67 -0
  8. sdmxlib-0.8.0/src/sdmxlib/api/providers.py +66 -0
  9. sdmxlib-0.8.0/src/sdmxlib/api/query.py +428 -0
  10. sdmxlib-0.8.0/src/sdmxlib/api/registry.py +666 -0
  11. sdmxlib-0.8.0/src/sdmxlib/api/session.py +124 -0
  12. sdmxlib-0.8.0/src/sdmxlib/formats/__init__.py +166 -0
  13. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_csv/__init__.py +5 -0
  14. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_csv/reader.py +147 -0
  15. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_json/__init__.py +1 -0
  16. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_json/reader.py +897 -0
  17. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_json/writer.py +698 -0
  18. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml21/__init__.py +5 -0
  19. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml21/namespaces.py +12 -0
  20. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml21/reader.py +1147 -0
  21. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml21/writer.py +709 -0
  22. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml30/__init__.py +1 -0
  23. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml30/namespaces.py +12 -0
  24. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml30/reader.py +1138 -0
  25. sdmxlib-0.8.0/src/sdmxlib/formats/sdmx_ml30/writer.py +730 -0
  26. sdmxlib-0.8.0/src/sdmxlib/local/__init__.py +6 -0
  27. sdmxlib-0.8.0/src/sdmxlib/local/_registry.py +368 -0
  28. sdmxlib-0.8.0/src/sdmxlib/model/__init__.py +122 -0
  29. sdmxlib-0.8.0/src/sdmxlib/model/annotations.py +103 -0
  30. sdmxlib-0.8.0/src/sdmxlib/model/base.py +61 -0
  31. sdmxlib-0.8.0/src/sdmxlib/model/category.py +88 -0
  32. sdmxlib-0.8.0/src/sdmxlib/model/codelist.py +91 -0
  33. sdmxlib-0.8.0/src/sdmxlib/model/collections.py +382 -0
  34. sdmxlib-0.8.0/src/sdmxlib/model/concept.py +121 -0
  35. sdmxlib-0.8.0/src/sdmxlib/model/constraint.py +92 -0
  36. sdmxlib-0.8.0/src/sdmxlib/model/convert.py +268 -0
  37. sdmxlib-0.8.0/src/sdmxlib/model/dataflow.py +49 -0
  38. sdmxlib-0.8.0/src/sdmxlib/model/dataset.py +337 -0
  39. sdmxlib-0.8.0/src/sdmxlib/model/datastructure.py +390 -0
  40. sdmxlib-0.8.0/src/sdmxlib/model/expr.py +120 -0
  41. sdmxlib-0.8.0/src/sdmxlib/model/hierarchy.py +111 -0
  42. sdmxlib-0.8.0/src/sdmxlib/model/istring.py +81 -0
  43. sdmxlib-0.8.0/src/sdmxlib/model/mapping.py +134 -0
  44. sdmxlib-0.8.0/src/sdmxlib/model/message.py +70 -0
  45. sdmxlib-0.8.0/src/sdmxlib/model/organisation.py +149 -0
  46. sdmxlib-0.8.0/src/sdmxlib/model/provision.py +45 -0
  47. sdmxlib-0.8.0/src/sdmxlib/model/ref.py +195 -0
  48. sdmxlib-0.8.0/src/sdmxlib/model/registry.py +191 -0
  49. sdmxlib-0.8.0/src/sdmxlib/model/representation.py +99 -0
  50. sdmxlib-0.8.0/src/sdmxlib/model/urn.py +130 -0
  51. sdmxlib-0.8.0/src/sdmxlib/model/validation.py +338 -0
  52. sdmxlib-0.8.0/src/sdmxlib/polars.py +392 -0
  53. sdmxlib-0.8.0/src/sdmxlib/py.typed +0 -0
  54. sdmxlib-0.8.0/src/sdmxlib/storage/__init__.py +28 -0
  55. sdmxlib-0.8.0/src/sdmxlib/storage/lazy.py +329 -0
  56. sdmxlib-0.8.0/src/sdmxlib/storage/readers.py +310 -0
  57. sdmxlib-0.8.0/src/sdmxlib/storage/schema.py +289 -0
  58. 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
+ ```
@@ -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)