sdmxlib 0.8.0__tar.gz → 0.16.2__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 (78) hide show
  1. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/PKG-INFO +15 -8
  2. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/README.md +8 -1
  3. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/pyproject.toml +10 -9
  4. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/__init__.py +44 -2
  5. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/api/client.py +7 -3
  6. sdmxlib-0.16.2/src/sdmxlib/api/federated.py +166 -0
  7. sdmxlib-0.16.2/src/sdmxlib/api/policy.py +39 -0
  8. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/api/providers.py +17 -7
  9. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/api/query.py +2 -2
  10. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/api/registry.py +181 -31
  11. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/api/session.py +1 -1
  12. sdmxlib-0.16.2/src/sdmxlib/catalog.py +299 -0
  13. sdmxlib-0.16.2/src/sdmxlib/data_store.py +64 -0
  14. sdmxlib-0.16.2/src/sdmxlib/formats/__init__.py +211 -0
  15. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_csv/reader.py +1 -1
  16. sdmxlib-0.16.2/src/sdmxlib/formats/sdmx_json/metadata.py +192 -0
  17. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_json/reader.py +119 -11
  18. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_json/writer.py +92 -4
  19. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_ml21/reader.py +6 -6
  20. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_ml21/writer.py +3 -3
  21. sdmxlib-0.16.2/src/sdmxlib/formats/sdmx_ml30/metadata.py +320 -0
  22. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +3 -1
  23. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_ml30/reader.py +158 -10
  24. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_ml30/writer.py +76 -3
  25. sdmxlib-0.16.2/src/sdmxlib/local/__init__.py +6 -0
  26. sdmxlib-0.16.2/src/sdmxlib/local/_data_store.py +168 -0
  27. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/local/_registry.py +104 -128
  28. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/__init__.py +29 -5
  29. sdmxlib-0.16.2/src/sdmxlib/model/binding.py +234 -0
  30. sdmxlib-0.16.2/src/sdmxlib/model/code_query.py +893 -0
  31. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/codelist.py +74 -6
  32. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/constraint.py +4 -4
  33. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/dataset.py +24 -15
  34. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/datastructure.py +37 -74
  35. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/hierarchy.py +32 -1
  36. sdmxlib-0.8.0/src/sdmxlib/model/registry.py → sdmxlib-0.16.2/src/sdmxlib/model/in_memory_registry.py +24 -11
  37. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/message.py +47 -7
  38. sdmxlib-0.16.2/src/sdmxlib/model/metadataflow.py +55 -0
  39. sdmxlib-0.16.2/src/sdmxlib/model/metadataset.py +165 -0
  40. sdmxlib-0.16.2/src/sdmxlib/model/metadatastructure.py +126 -0
  41. sdmxlib-0.16.2/src/sdmxlib/model/registry.py +59 -0
  42. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/polars.py +99 -18
  43. sdmxlib-0.16.2/src/sdmxlib/storage/__init__.py +28 -0
  44. sdmxlib-0.16.2/src/sdmxlib/storage/lazy.py +220 -0
  45. sdmxlib-0.16.2/src/sdmxlib/storage/readers.py +1366 -0
  46. sdmxlib-0.16.2/src/sdmxlib/storage/schema.py +561 -0
  47. sdmxlib-0.16.2/src/sdmxlib/storage/writers.py +1408 -0
  48. sdmxlib-0.8.0/src/sdmxlib/formats/__init__.py +0 -166
  49. sdmxlib-0.8.0/src/sdmxlib/local/__init__.py +0 -6
  50. sdmxlib-0.8.0/src/sdmxlib/storage/__init__.py +0 -28
  51. sdmxlib-0.8.0/src/sdmxlib/storage/lazy.py +0 -329
  52. sdmxlib-0.8.0/src/sdmxlib/storage/readers.py +0 -310
  53. sdmxlib-0.8.0/src/sdmxlib/storage/schema.py +0 -289
  54. sdmxlib-0.8.0/src/sdmxlib/storage/writers.py +0 -503
  55. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/api/__init__.py +0 -0
  56. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/api/filters.py +0 -0
  57. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
  58. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
  59. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
  60. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
  61. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
  62. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/annotations.py +0 -0
  63. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/base.py +0 -0
  64. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/category.py +0 -0
  65. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/collections.py +0 -0
  66. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/concept.py +0 -0
  67. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/convert.py +0 -0
  68. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/dataflow.py +0 -0
  69. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/expr.py +0 -0
  70. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/istring.py +0 -0
  71. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/mapping.py +0 -0
  72. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/organisation.py +0 -0
  73. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/provision.py +0 -0
  74. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/ref.py +0 -0
  75. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/representation.py +0 -0
  76. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/urn.py +0 -0
  77. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/model/validation.py +0 -0
  78. {sdmxlib-0.8.0 → sdmxlib-0.16.2}/src/sdmxlib/py.typed +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sdmxlib
3
- Version: 0.8.0
3
+ Version: 0.16.2
4
4
  Summary: SDMX structural metadata library for Python
5
5
  Keywords: sdmx,statistics,metadata,datastructure
6
6
  Author: gabrielgellner
@@ -10,18 +10,18 @@ Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Intended Audience :: Developers
11
11
  Classifier: Intended Audience :: Science/Research
12
12
  Classifier: Programming Language :: Python :: 3
13
- Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
14
  Classifier: Topic :: Scientific/Engineering :: Information Analysis
15
15
  Requires-Dist: attrs>=26.1.0
16
16
  Requires-Dist: duckdb>=1.0.0
17
17
  Requires-Dist: httpx>=0.28.1
18
18
  Requires-Dist: lxml>=6.0.2
19
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
20
+ Requires-Python: >=3.13
21
+ Project-URL: Homepage, https://gitlab.com/pinax-suite/sdmxlib
22
+ Project-URL: Repository, https://gitlab.com/pinax-suite/sdmxlib
23
+ Project-URL: Documentation, https://pinax-suite.gitlab.io/sdmxlib
24
+ Project-URL: Changelog, https://gitlab.com/pinax-suite/sdmxlib/-/blob/main/CHANGELOG.md
25
25
  Description-Content-Type: text/markdown
26
26
 
27
27
  # sdmxlib
@@ -74,7 +74,14 @@ uv add "sdmxlib @ git+https://gitlab.com/gabrielgellner/sdmxlib.git"
74
74
  - Build SDMX artefacts locally and serialise to SDMX-ML or SDMX-JSON
75
75
  - `LocalRegistry` -- a DuckDB-backed persistent store for offline access
76
76
  with lazy codelist loading, automatic Ref resolution, and direct SQL
77
- code maps; supports shared connections and configurable language columns
77
+ code maps; supports shared connections and any-language queries
78
+ - Reference metadata via `MetadataStructure` (MSD), `Metadataflow`, and
79
+ `MetadataSet` — attach typed footnotes, methodology notes, or quality
80
+ reports to any identifiable artefact. Definitions ride in
81
+ `StructureMessage`; populated payloads ride in `MetadataMessage`
82
+ (`<mes:GenericMetadata>` in SDMX-ML 3.0; metadata-schema envelope in
83
+ SDMX-JSON 2.0). Both message kinds round-trip through
84
+ `LocalRegistry`, JSON, and XML.
78
85
  - Integrates with [Polars](https://pola.rs) for schema generation and data
79
86
  validation
80
87
  - Fully typed — IDE completion and static analysis work throughout
@@ -48,7 +48,14 @@ uv add "sdmxlib @ git+https://gitlab.com/gabrielgellner/sdmxlib.git"
48
48
  - Build SDMX artefacts locally and serialise to SDMX-ML or SDMX-JSON
49
49
  - `LocalRegistry` -- a DuckDB-backed persistent store for offline access
50
50
  with lazy codelist loading, automatic Ref resolution, and direct SQL
51
- code maps; supports shared connections and configurable language columns
51
+ code maps; supports shared connections and any-language queries
52
+ - Reference metadata via `MetadataStructure` (MSD), `Metadataflow`, and
53
+ `MetadataSet` — attach typed footnotes, methodology notes, or quality
54
+ reports to any identifiable artefact. Definitions ride in
55
+ `StructureMessage`; populated payloads ride in `MetadataMessage`
56
+ (`<mes:GenericMetadata>` in SDMX-ML 3.0; metadata-schema envelope in
57
+ SDMX-JSON 2.0). Both message kinds round-trip through
58
+ `LocalRegistry`, JSON, and XML.
52
59
  - Integrates with [Polars](https://pola.rs) for schema generation and data
53
60
  validation
54
61
  - Fully typed — IDE completion and static analysis work throughout
@@ -1,18 +1,18 @@
1
1
  [project]
2
2
  name = "sdmxlib"
3
- version = "0.8.0"
3
+ version = "0.16.2"
4
4
  description = "SDMX structural metadata library for Python"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.0" }
7
7
  authors = [{ name = "gabrielgellner", email = "gabrielgellner@gmail.com" }]
8
- requires-python = ">=3.12"
8
+ requires-python = ">=3.13"
9
9
  keywords = ["sdmx", "statistics", "metadata", "datastructure"]
10
10
  classifiers = [
11
11
  "Development Status :: 4 - Beta",
12
12
  "Intended Audience :: Developers",
13
13
  "Intended Audience :: Science/Research",
14
14
  "Programming Language :: Python :: 3",
15
- "Programming Language :: Python :: 3.12",
15
+ "Programming Language :: Python :: 3.13",
16
16
  "Topic :: Scientific/Engineering :: Information Analysis",
17
17
  ]
18
18
  dependencies = [
@@ -24,10 +24,10 @@ dependencies = [
24
24
  ]
25
25
 
26
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"
27
+ Homepage = "https://gitlab.com/pinax-suite/sdmxlib"
28
+ Repository = "https://gitlab.com/pinax-suite/sdmxlib"
29
+ Documentation = "https://pinax-suite.gitlab.io/sdmxlib"
30
+ Changelog = "https://gitlab.com/pinax-suite/sdmxlib/-/blob/main/CHANGELOG.md"
31
31
 
32
32
  [build-system]
33
33
  requires = ["uv_build>=0.11.2,<0.12.0"]
@@ -38,7 +38,8 @@ testpaths = ["tests"]
38
38
  markers = [
39
39
  "unit: fast unit tests (no I/O)",
40
40
  "docs: tests that verify documentation examples",
41
- "integration: live endpoint tests (require network)",
41
+ "integration: tests that use real DuckDB and sdmxlib but no network",
42
+ "live: tests that make real HTTP calls to external SDMX endpoints",
42
43
  ]
43
44
 
44
45
  [tool.coverage.run]
@@ -57,7 +58,7 @@ exclude_lines = [
57
58
  ]
58
59
 
59
60
  [tool.basedpyright]
60
- pythonVersion = "3.12"
61
+ pythonVersion = "3.13"
61
62
  pythonPlatform = "Linux"
62
63
  venvPath = "."
63
64
  venv = ".venv"
@@ -1,11 +1,16 @@
1
1
  """sdmxlib — SDMX structural metadata library for Python."""
2
2
 
3
+ from sdmxlib.api.federated import FederatedRegistry
3
4
  from sdmxlib.api.filters import DimFilter, dim
5
+ from sdmxlib.api.policy import FailurePolicy, RegistryUnavailable
4
6
  from sdmxlib.api.providers import Provider
5
7
  from sdmxlib.api.query import DataQuery
6
8
  from sdmxlib.api.registry import BearerToken, FmrRegistry, RestRegistry
9
+ from sdmxlib.catalog import DataflowEntry, FederatedCatalog
10
+ from sdmxlib.data_store import DataStore
7
11
  from sdmxlib.formats import parse, read, serialize, to_json, to_xml
8
12
  from sdmxlib.local import LazyCodelist, LocalRegistry
13
+ from sdmxlib.local._data_store import DuckDBDataStore
9
14
  from sdmxlib.model.convert import from_dict, to_dict
10
15
  from sdmxlib.model.validation import ReferenceIssue, ValidationIssue, validate, validate_references
11
16
  from sdmxlib.model import (
@@ -20,6 +25,7 @@ from sdmxlib.model import (
20
25
  Category,
21
26
  CategoryScheme,
22
27
  Code,
28
+ CodeQuery,
23
29
  Codelist,
24
30
  Concept,
25
31
  ConceptScheme,
@@ -32,7 +38,7 @@ from sdmxlib.model import (
32
38
  ConceptSchemeMap,
33
39
  ConstraintAttachment,
34
40
  Contact,
35
- ContentConstraint,
41
+ DataConstraint,
36
42
  CubeRegion,
37
43
  ReferencePeriod,
38
44
  ReleaseCalendar,
@@ -42,22 +48,33 @@ from sdmxlib.model import (
42
48
  Dataset,
43
49
  DataStructureMap,
44
50
  Dataflow,
51
+ DataflowBinding,
45
52
  DataflowMap,
46
53
  Dimension,
54
+ DriftIssue,
47
55
  Hierarchy,
48
56
  HierarchicalCode,
49
57
  Group,
58
+ InMemoryRegistry,
50
59
  InternationalString,
51
60
  ItemExpr,
52
61
  ItemList,
53
62
  ItemPredicate,
54
63
  KeyValue,
64
+ Level,
55
65
  item,
56
66
  Maintainable,
57
67
  MaintainableArtefact,
58
68
  Measure,
59
69
  MeasureDimension,
70
+ MetadataAttribute,
71
+ MetadataMessage,
72
+ MetadataSet,
73
+ MetadataStructure,
74
+ Metadataflow,
60
75
  ProvisionAgreement,
76
+ ReportedAttribute,
77
+ ReportedAttributeValue,
61
78
  DataType,
62
79
  Facet,
63
80
  Ref,
@@ -69,6 +86,9 @@ from sdmxlib.model import (
69
86
  StructureSet,
70
87
  TimeDimension,
71
88
  UnresolvedRef,
89
+ artefact_urn,
90
+ code,
91
+ validate_binding,
72
92
  )
73
93
 
74
94
  __all__ = [
@@ -87,6 +107,7 @@ __all__ = [
87
107
  "CategorySchemeMap",
88
108
  "Code",
89
109
  "CodeMap",
110
+ "CodeQuery",
90
111
  "Codelist",
91
112
  "CodelistMap",
92
113
  "ComponentMap",
@@ -96,42 +117,60 @@ __all__ = [
96
117
  "ConceptSchemeMap",
97
118
  "ConstraintAttachment",
98
119
  "Contact",
99
- "ContentConstraint",
100
120
  "CubeRegion",
121
+ "DataConstraint",
101
122
  "DataProvider",
102
123
  "DataProviderScheme",
103
124
  "DataQuery",
125
+ "DataStore",
104
126
  "DataStructure",
105
127
  "DataStructureMap",
106
128
  "DataType",
107
129
  "Dataflow",
130
+ "DataflowBinding",
131
+ "DataflowEntry",
108
132
  "DataflowMap",
109
133
  "Dataset",
110
134
  "DimFilter",
111
135
  "Dimension",
136
+ "DriftIssue",
137
+ "DuckDBDataStore",
112
138
  "Facet",
139
+ "FailurePolicy",
140
+ "FederatedCatalog",
141
+ "FederatedRegistry",
113
142
  "FmrRegistry",
114
143
  "Group",
115
144
  "HierarchicalCode",
116
145
  "Hierarchy",
146
+ "InMemoryRegistry",
117
147
  "InternationalString",
118
148
  "ItemExpr",
119
149
  "ItemList",
120
150
  "ItemPredicate",
121
151
  "KeyValue",
122
152
  "LazyCodelist",
153
+ "Level",
123
154
  "LocalRegistry",
124
155
  "Maintainable",
125
156
  "MaintainableArtefact",
126
157
  "Measure",
127
158
  "MeasureDimension",
159
+ "MetadataAttribute",
160
+ "MetadataMessage",
161
+ "MetadataSet",
162
+ "MetadataStructure",
163
+ "Metadataflow",
128
164
  "Provider",
129
165
  "ProvisionAgreement",
130
166
  "Ref",
131
167
  "ReferenceIssue",
132
168
  "ReferencePeriod",
133
169
  "Registry",
170
+ "RegistryUnavailable",
134
171
  "ReleaseCalendar",
172
+ "ReportedAttribute",
173
+ "ReportedAttributeValue",
135
174
  "Representation",
136
175
  "RestRegistry",
137
176
  "SdmxRef",
@@ -141,6 +180,8 @@ __all__ = [
141
180
  "TimeDimension",
142
181
  "UnresolvedRef",
143
182
  "ValidationIssue",
183
+ "artefact_urn",
184
+ "code",
144
185
  "dim",
145
186
  "from_dict",
146
187
  "item",
@@ -151,5 +192,6 @@ __all__ = [
151
192
  "to_json",
152
193
  "to_xml",
153
194
  "validate",
195
+ "validate_binding",
154
196
  "validate_references",
155
197
  ]
@@ -17,7 +17,7 @@ _RESOURCE_PATH: dict[str, str] = {
17
17
  "ProvisionAgreement": "provisionagreement",
18
18
  "CategoryScheme": "categoryscheme",
19
19
  "Categorisation": "categorisation",
20
- "ContentConstraint": "contentconstraint",
20
+ "DataConstraint": "dataconstraint",
21
21
  "AgencyScheme": "agencyscheme",
22
22
  "DataProviderScheme": "dataproviderorganisationscheme",
23
23
  "Hierarchy": "hierarchy",
@@ -133,7 +133,9 @@ class SdmxClient:
133
133
  params["references"] = references
134
134
  response = self._client.get(url, params=params)
135
135
  response.raise_for_status()
136
- return parse(response.content, version=self._parse_version)
136
+ msg = parse(response.content, version=self._parse_version)
137
+ assert isinstance(msg, StructureMessage) # noqa: S101 # structure endpoint only
138
+ return msg
137
139
 
138
140
  def get_all(
139
141
  self,
@@ -150,7 +152,9 @@ class SdmxClient:
150
152
  params["references"] = references
151
153
  response = self._client.get(url, params=params)
152
154
  response.raise_for_status()
153
- return parse(response.content, version=self._parse_version)
155
+ msg = parse(response.content, version=self._parse_version)
156
+ assert isinstance(msg, StructureMessage) # noqa: S101 # structure endpoint only
157
+ return msg
154
158
 
155
159
  def _data_url(
156
160
  self,
@@ -0,0 +1,166 @@
1
+ """FederatedRegistry — agency-routed dispatch across multiple sources.
2
+
3
+ The headline of #46. Composes any number of :class:`~sdmxlib.model.registry.Registry`
4
+ instances (in-memory, local DuckDB, REST) and dispatches each ``get(urn)``
5
+ call to the source responsible for the URN's maintenance agency. The user
6
+ sees a single registry that resolves URNs from many backends.
7
+
8
+ Cache-mode v1 — each agency has exactly one source. Peer-mode (multiple
9
+ sources cooperating to answer a single URN) is intentionally out of
10
+ scope; see #46 for that conversation.
11
+
12
+ Example:
13
+ local = sl.LocalRegistry("cache.duckdb", source_registry="local")
14
+ estat = sl.RestRegistry(sl.Provider.ESTAT, materialise_to=local)
15
+ sdmx_global = sl.RestRegistry(sl.Provider.SDMX_GLOBAL, materialise_to=local)
16
+
17
+ fed = sl.FederatedRegistry(
18
+ routing={"ESTAT": estat, "SDMX": sdmx_global},
19
+ default=local, # locally-authored agencies dispatch here
20
+ )
21
+
22
+ # Routes by maintenance agency:
23
+ dsd = fed.get(sl.DataStructure.urn(agency="ESTAT", id="X", version="1.0"))
24
+ # → estat → HTTP fetch → materialise to local
25
+
26
+ cl = fed.get(sl.Codelist.urn(agency="SDMX", id="CL_FREQ", version="2.1"))
27
+ # → sdmx_global → HTTP fetch → materialise to local
28
+
29
+ own = fed.get(sl.Codelist.urn(agency="MY_ORG", id="CL_X", version="1.0"))
30
+ # → falls through to local (no routing entry)
31
+ """
32
+
33
+ from typing import TYPE_CHECKING
34
+
35
+ import httpx
36
+
37
+ from sdmxlib.api.policy import FailurePolicy, RegistryUnavailable
38
+
39
+ if TYPE_CHECKING:
40
+ from sdmxlib.model.registry import Registry
41
+ from sdmxlib.model.urn import SdmxUrn
42
+
43
+
44
+ class FederatedRegistry:
45
+ """Read-side dispatcher across multiple Registry sources.
46
+
47
+ Routes each URN-keyed lookup by maintenance agency. Configure with a
48
+ ``routing`` map from agency id to a :class:`Registry` source, plus
49
+ an optional ``default`` source for agencies absent from the map.
50
+
51
+ Satisfies the :class:`~sdmxlib.model.registry.Registry` Protocol —
52
+ a ``FederatedRegistry`` can itself be a source in another
53
+ ``FederatedRegistry`` (composition).
54
+
55
+ Args:
56
+ routing: Mapping of agency id (e.g. ``"ESTAT"``, ``"SDMX"``) to a
57
+ :class:`Registry` source. Lookup keys are matched against
58
+ ``SdmxUrn.agency``.
59
+ default: Optional fallback source for URNs whose agency is not
60
+ in ``routing``. ``None`` (the default) means "no fallback":
61
+ unrouted URNs raise :class:`KeyError`.
62
+ failure: How to treat exceptions raised by a routed source.
63
+ Defaults to :attr:`FailurePolicy.HARD_FAIL` — propagate the
64
+ underlying error wrapped in :class:`RegistryUnavailable`.
65
+
66
+ Note:
67
+ FederatedRegistry is a *router*, not a *store*. It has no
68
+ ``add()`` method — write to specific concrete registries
69
+ (typically the local one in your routing chain).
70
+ """
71
+
72
+ def __init__(
73
+ self,
74
+ *,
75
+ routing: "dict[str, Registry]",
76
+ default: "Registry | None" = None,
77
+ failure: FailurePolicy = FailurePolicy.HARD_FAIL,
78
+ ) -> None:
79
+ self._routing = dict(routing)
80
+ self._default = default
81
+ self._failure = failure
82
+
83
+ @property
84
+ def routing(self) -> "dict[str, Registry]":
85
+ """Read-only view of the agency → source map."""
86
+ return dict(self._routing)
87
+
88
+ @property
89
+ def default(self) -> "Registry | None":
90
+ """The fallback source used when no routing entry matches."""
91
+ return self._default
92
+
93
+ @property
94
+ def failure(self) -> FailurePolicy:
95
+ """The configured failure policy."""
96
+ return self._failure
97
+
98
+ # ── Routing ──────────────────────────────────────────────────────────
99
+
100
+ def _route(self, urn: "SdmxUrn[object]") -> "Registry | None":
101
+ """Pick the source responsible for ``urn``'s agency."""
102
+ return self._routing.get(urn.agency, self._default)
103
+
104
+ def source_for[T](self, urn: "SdmxUrn[T]") -> "Registry | None":
105
+ """Public version of the routing decision — useful for diagnostics."""
106
+ return self._route(urn)
107
+
108
+ # ── Registry Protocol ────────────────────────────────────────────────
109
+
110
+ def get[T](self, urn: "SdmxUrn[T]") -> "T | None":
111
+ """Resolve ``urn`` via the routed source.
112
+
113
+ Routing rules:
114
+
115
+ - URN agency in ``routing`` → dispatch to that source.
116
+ - URN agency absent and ``default`` is set → dispatch to default.
117
+ - URN agency absent and no default → raise ``KeyError``.
118
+
119
+ Errors from the routed source are wrapped in
120
+ :class:`RegistryUnavailable` when ``failure`` is
121
+ :attr:`FailurePolicy.HARD_FAIL` (default), or swallowed (returning
122
+ ``None``) when ``failure`` is :attr:`FailurePolicy.RETURN_NONE`.
123
+ """
124
+ source = self._route(urn)
125
+ if source is None:
126
+ err = (
127
+ f"No source registered for agency {urn.agency!r} and no default — "
128
+ f"add an entry to routing or supply default=..."
129
+ )
130
+ raise KeyError(err)
131
+
132
+ try:
133
+ return source.get(urn) # type: ignore[return-value]
134
+ except (httpx.HTTPError, OSError, RegistryUnavailable) as exc:
135
+ return self._on_failure(source, exc)
136
+
137
+ def __contains__(self, urn: object) -> bool:
138
+ """True if the routed source already has ``urn`` cached.
139
+
140
+ Like the underlying Registry membership: never triggers a network
141
+ round-trip. ``False`` if no source is routed for the URN's agency.
142
+ """
143
+ from sdmxlib.model.urn import SdmxUrn # noqa: PLC0415
144
+
145
+ if not isinstance(urn, SdmxUrn):
146
+ return False
147
+ source = self._route(urn)
148
+ return source is not None and urn in source
149
+
150
+ # ── Failure handling ─────────────────────────────────────────────────
151
+
152
+ def _on_failure[T](self, source: "Registry", exc: Exception) -> "T | None":
153
+ """Apply the configured ``FailurePolicy`` to an upstream error."""
154
+ source_id = getattr(source, "id", None)
155
+ if self._failure is FailurePolicy.RETURN_NONE:
156
+ return None
157
+ # HARD_FAIL — wrap and re-raise
158
+ raise RegistryUnavailable(source_id, exc) from exc
159
+
160
+ def __repr__(self) -> str:
161
+ agencies = ", ".join(sorted(self._routing))
162
+ default = " +default" if self._default is not None else ""
163
+ return f"FederatedRegistry(routes=[{agencies}]{default}, failure={self._failure.name})"
164
+
165
+
166
+ __all__ = ["FederatedRegistry"]
@@ -0,0 +1,39 @@
1
+ """FailurePolicy — how a federated read should behave when a source errors.
2
+
3
+ Per #46's pitfall list ("Partial failure handling"), there are several
4
+ valid behaviours when a routed source is unreachable: hard-fail, return
5
+ ``None`` (treat as not-found), or fall through to a stale local cache.
6
+ The policy is configurable per source or globally on
7
+ :class:`~sdmxlib.api.federated.FederatedRegistry`.
8
+ """
9
+
10
+ from enum import Enum
11
+
12
+
13
+ class FailurePolicy(Enum):
14
+ """How a federated registry handles errors from a routed source.
15
+
16
+ Attributes:
17
+ HARD_FAIL: Re-raise the underlying error (default). The caller
18
+ sees the network/HTTP exception unchanged.
19
+ RETURN_NONE: Catch errors that look like "source unreachable"
20
+ and return ``None``. The caller cannot distinguish "agency
21
+ doesn't have this artefact" from "source was down."
22
+ """
23
+
24
+ HARD_FAIL = "hard_fail"
25
+ RETURN_NONE = "return_none"
26
+
27
+
28
+ class RegistryUnavailable(Exception): # noqa: N818 # name matches #46 vocabulary
29
+ """Raised when a routed source cannot be reached and the policy is HARD_FAIL.
30
+
31
+ Wraps the underlying httpx / connection error so callers can catch a
32
+ single exception type without depending on the HTTP layer.
33
+ """
34
+
35
+ def __init__(self, source_id: str | None, cause: Exception) -> None:
36
+ self.source_id = source_id
37
+ self.cause = cause
38
+ sid = f" (source={source_id!r})" if source_id else ""
39
+ super().__init__(f"Registry unavailable{sid}: {cause}")
@@ -13,10 +13,20 @@ from attrs import frozen
13
13
 
14
14
  @frozen
15
15
  class ProviderInfo:
16
- """URL and REST API version for a known SDMX endpoint."""
16
+ """URL and REST API version for a known SDMX endpoint.
17
+
18
+ Attributes:
19
+ url: Base REST endpoint URL.
20
+ api: SDMX REST API version (``"2.1"`` or ``"3.0"``).
21
+ id: Stable identifier used as the ``source_registry`` provenance
22
+ tag when a :class:`RestRegistry` materialises through to a
23
+ local store. Required for federation; ``None`` for ad-hoc
24
+ URLs without a known label.
25
+ """
17
26
 
18
27
  url: str
19
28
  api: str = "2.1"
29
+ id: str | None = None
20
30
 
21
31
 
22
32
  class Provider:
@@ -39,17 +49,17 @@ class Provider:
39
49
  """
40
50
 
41
51
  #: European Central Bank
42
- ECB = ProviderInfo("https://data-api.ecb.europa.eu/service", api="2.1")
52
+ ECB = ProviderInfo("https://data-api.ecb.europa.eu/service", api="2.1", id="ECB")
43
53
  #: Bank for International Settlements
44
- BIS = ProviderInfo("https://stats.bis.org/api/v1", api="2.1")
54
+ BIS = ProviderInfo("https://stats.bis.org/api/v1", api="2.1", id="BIS")
45
55
  #: Eurostat
46
- ESTAT = ProviderInfo("https://ec.europa.eu/eurostat/api/dissemination/sdmx/2.1", api="2.1")
56
+ ESTAT = ProviderInfo("https://ec.europa.eu/eurostat/api/dissemination/sdmx/2.1", api="2.1", id="ESTAT")
47
57
  #: International Monetary Fund
48
- IMF = ProviderInfo("https://sdmxcentral.imf.org/ws/public/sdmxapi/rest", api="2.1")
58
+ IMF = ProviderInfo("https://sdmxcentral.imf.org/ws/public/sdmxapi/rest", api="2.1", id="IMF")
49
59
  #: OECD
50
- OECD = ProviderInfo("https://sdmx.oecd.org/public/rest", api="2.1")
60
+ OECD = ProviderInfo("https://sdmx.oecd.org/public/rest", api="2.1", id="OECD")
51
61
  #: SDMX Global Registry (FMR — SDMX 3.0 endpoint)
52
- SDMX_GLOBAL = ProviderInfo("https://registry.sdmx.org/sdmx/v2", api="3.0")
62
+ SDMX_GLOBAL = ProviderInfo("https://registry.sdmx.org/sdmx/v2", api="3.0", id="SDMX_GLOBAL")
53
63
 
54
64
  def __class_getitem__(cls, key: str) -> ProviderInfo:
55
65
  val = getattr(cls, key, None)
@@ -66,7 +66,7 @@ class GetQuery[T]:
66
66
  reference_type: If given, resolve only Refs of this type.
67
67
  If ``None`` (default), resolve all references.
68
68
  partial: If ``True`` (default), referenced ItemSchemes are trimmed
69
- to items allowed by ContentConstraints
69
+ to items allowed by DataConstraints
70
70
  (``detail=referencepartial``). Produces smaller, more
71
71
  useful responses for the common case. Set to ``False``
72
72
  for full metadata audits (``detail=full``).
@@ -204,7 +204,7 @@ class Query[T]:
204
204
  Args:
205
205
  reference_type: If given, resolve only Refs of this type.
206
206
  partial: Controls whether referenced ItemSchemes are trimmed by
207
- ContentConstraints. ``True`` (default) is faster and
207
+ DataConstraints. ``True`` (default) is faster and
208
208
  more useful for data queries; ``False`` for full audits.
209
209
  """
210
210
  ref_str = "descendants" if reference_type is None else _to_references(reference_type)