sdmxlib 0.32.2__tar.gz → 0.34.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.32.2 → sdmxlib-0.34.0}/PKG-INFO +1 -1
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/pyproject.toml +1 -1
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/__init__.py +2 -4
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/federated.py +14 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/registry.py +13 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/local/registry.py +40 -1
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/__init__.py +3 -4
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/binding.py +8 -18
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/code_query.py +59 -15
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/hierarchy.py +78 -29
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/in_memory_registry.py +54 -1
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/README.md +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/_duckdb.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/client.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/filters.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/policy.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/providers.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/query.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/session.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/catalog.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/data_store.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_csv/reader.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_csv/writer.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_json/metadata.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_json/reader.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_json/writer.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml/_common.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml21/reader.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml21/writer.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/metadata.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/reader.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/writer.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml31/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml31/namespaces.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml31/reader.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml31/writer.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/local/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/local/data_store.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/annotations.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/base.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/category.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/codelist.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/collections.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/concept.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/constraint.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/convert.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/dataflow.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/dataset.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/datastructure.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/expr.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/hierarchy_query.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/istring.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/mapping.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/message.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/metadataflow.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/metadataset.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/metadatastructure.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/organisation.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/provision.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/ref.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/registry.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/representation.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/urn.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/validation.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/polars.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/py.typed +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/rest.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/sql.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/__init__.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/_kinds.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/lazy.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/readers.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/schema.py +0 -0
- {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/writers.py +0 -0
|
@@ -101,17 +101,17 @@ from sdmxlib.model import (
|
|
|
101
101
|
TimeDimension,
|
|
102
102
|
UnresolvedRef,
|
|
103
103
|
ValueMap,
|
|
104
|
+
AmbiguousHierarchyError,
|
|
104
105
|
artefact_urn,
|
|
105
106
|
code,
|
|
106
|
-
default_hierarchy,
|
|
107
107
|
node,
|
|
108
|
-
validate_binding,
|
|
109
108
|
)
|
|
110
109
|
from sdmxlib.model.hierarchy_query import HierarchyNodeQuery
|
|
111
110
|
|
|
112
111
|
__all__ = [
|
|
113
112
|
"Agency",
|
|
114
113
|
"AgencyScheme",
|
|
114
|
+
"AmbiguousHierarchyError",
|
|
115
115
|
"Annotation",
|
|
116
116
|
"Annotations",
|
|
117
117
|
"AnyRef",
|
|
@@ -217,7 +217,6 @@ __all__ = [
|
|
|
217
217
|
"ValueMap",
|
|
218
218
|
"artefact_urn",
|
|
219
219
|
"code",
|
|
220
|
-
"default_hierarchy",
|
|
221
220
|
"dim",
|
|
222
221
|
"from_dict",
|
|
223
222
|
"item",
|
|
@@ -230,6 +229,5 @@ __all__ = [
|
|
|
230
229
|
"to_json",
|
|
231
230
|
"to_xml",
|
|
232
231
|
"validate",
|
|
233
|
-
"validate_binding",
|
|
234
232
|
"validate_references",
|
|
235
233
|
]
|
|
@@ -37,6 +37,7 @@ import httpx
|
|
|
37
37
|
from sdmxlib.api.policy import FailurePolicy, RegistryUnavailable
|
|
38
38
|
|
|
39
39
|
if TYPE_CHECKING:
|
|
40
|
+
from sdmxlib.model.binding import DataflowBinding, DriftIssue
|
|
40
41
|
from sdmxlib.model.registry import RegistryReader
|
|
41
42
|
from sdmxlib.model.urn import SdmxUrn
|
|
42
43
|
|
|
@@ -146,6 +147,19 @@ class FederatedRegistry:
|
|
|
146
147
|
source = self._route(urn)
|
|
147
148
|
return source is not None and urn in source
|
|
148
149
|
|
|
150
|
+
# ── Graph queries ────────────────────────────────────────────────────
|
|
151
|
+
|
|
152
|
+
def validate_binding(self, binding: "DataflowBinding") -> "list[DriftIssue]":
|
|
153
|
+
"""Check ``binding`` against this federated registry for structural drift.
|
|
154
|
+
|
|
155
|
+
Routes the bound Dataflow lookup through the configured sources;
|
|
156
|
+
only the routed source is touched. Returns a list of
|
|
157
|
+
`DriftIssue` — empty if no drift is detected.
|
|
158
|
+
"""
|
|
159
|
+
from sdmxlib.model.binding import _validate_binding # noqa: PLC0415
|
|
160
|
+
|
|
161
|
+
return _validate_binding(binding, self)
|
|
162
|
+
|
|
149
163
|
# ── Failure handling ─────────────────────────────────────────────────
|
|
150
164
|
|
|
151
165
|
def _on_failure[T](self, source: "RegistryReader", exc: Exception) -> "T | None":
|
|
@@ -21,6 +21,7 @@ from sdmxlib.model.urn import SdmxUrn
|
|
|
21
21
|
|
|
22
22
|
if TYPE_CHECKING:
|
|
23
23
|
from sdmxlib.local.registry import Registry
|
|
24
|
+
from sdmxlib.model.binding import DataflowBinding, DriftIssue
|
|
24
25
|
|
|
25
26
|
SDMX_GLOBAL_REGISTRY = "https://registry.sdmx.org/sdmx/v2"
|
|
26
27
|
|
|
@@ -744,6 +745,18 @@ class RestRegistry:
|
|
|
744
745
|
structure: DataStructure = flow.structure()
|
|
745
746
|
return scan_csv(source, structure, dataflow=Ref.to(flow))
|
|
746
747
|
|
|
748
|
+
# ── Graph queries ─────────────────────────────────────────────────────────
|
|
749
|
+
|
|
750
|
+
def validate_binding(self, binding: "DataflowBinding") -> "list[DriftIssue]":
|
|
751
|
+
"""Check ``binding`` against this registry for structural drift.
|
|
752
|
+
|
|
753
|
+
Performs at most one HTTP fetch (the bound Dataflow). Returns a
|
|
754
|
+
list of `DriftIssue` — empty if no drift is detected.
|
|
755
|
+
"""
|
|
756
|
+
from sdmxlib.model.binding import _validate_binding # noqa: PLC0415
|
|
757
|
+
|
|
758
|
+
return _validate_binding(binding, self)
|
|
759
|
+
|
|
747
760
|
|
|
748
761
|
# ── FmrRegistry ───────────────────────────────────────────────────────────────
|
|
749
762
|
|
|
@@ -33,7 +33,7 @@ stashes the URI; the DuckDB connection is opened by ``open()`` /
|
|
|
33
33
|
import threading
|
|
34
34
|
from collections.abc import Iterable
|
|
35
35
|
from pathlib import Path
|
|
36
|
-
from typing import Any, Self
|
|
36
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
37
37
|
|
|
38
38
|
import attrs
|
|
39
39
|
import duckdb
|
|
@@ -52,6 +52,11 @@ from sdmxlib.storage.schema import (
|
|
|
52
52
|
schema_exists,
|
|
53
53
|
)
|
|
54
54
|
|
|
55
|
+
if TYPE_CHECKING:
|
|
56
|
+
from sdmxlib.model.binding import DataflowBinding, DriftIssue
|
|
57
|
+
from sdmxlib.model.dataflow import Dataflow
|
|
58
|
+
from sdmxlib.model.hierarchy import Hierarchy
|
|
59
|
+
|
|
55
60
|
# Map item-level artefact class -> (parent scheme type name, items attribute name)
|
|
56
61
|
_ITEM_TO_SCHEME: dict[str, tuple[str, str]] = {
|
|
57
62
|
"Concept": ("ConceptScheme", "concepts"),
|
|
@@ -490,6 +495,40 @@ class Registry:
|
|
|
490
495
|
memo.promote()
|
|
491
496
|
return results # type: ignore[return-value]
|
|
492
497
|
|
|
498
|
+
# ── Graph queries ──────────────────────────────────────────────────
|
|
499
|
+
|
|
500
|
+
def applicable_hierarchies(
|
|
501
|
+
self,
|
|
502
|
+
dataflow: "Dataflow",
|
|
503
|
+
dim_id: str,
|
|
504
|
+
) -> "list[Ref[Hierarchy]]":
|
|
505
|
+
"""All Hierarchies applicable to ``dim_id`` on ``dataflow``.
|
|
506
|
+
|
|
507
|
+
See `InMemoryRegistry.applicable_hierarchies` for semantics.
|
|
508
|
+
"""
|
|
509
|
+
from sdmxlib.model.hierarchy import _applicable_hierarchies # noqa: PLC0415
|
|
510
|
+
|
|
511
|
+
return _applicable_hierarchies(self, dataflow, dim_id)
|
|
512
|
+
|
|
513
|
+
def unique_hierarchy(
|
|
514
|
+
self,
|
|
515
|
+
dataflow: "Dataflow",
|
|
516
|
+
dim_id: str,
|
|
517
|
+
) -> "Ref[Hierarchy] | None":
|
|
518
|
+
"""The single applicable Hierarchy, ``None`` if none, raises on >1.
|
|
519
|
+
|
|
520
|
+
See `InMemoryRegistry.unique_hierarchy` for semantics.
|
|
521
|
+
"""
|
|
522
|
+
from sdmxlib.model.hierarchy import _unique_hierarchy # noqa: PLC0415
|
|
523
|
+
|
|
524
|
+
return _unique_hierarchy(self, dataflow, dim_id)
|
|
525
|
+
|
|
526
|
+
def validate_binding(self, binding: "DataflowBinding") -> "list[DriftIssue]":
|
|
527
|
+
"""Check ``binding`` against this registry for structural drift."""
|
|
528
|
+
from sdmxlib.model.binding import _validate_binding # noqa: PLC0415
|
|
529
|
+
|
|
530
|
+
return _validate_binding(binding, self)
|
|
531
|
+
|
|
493
532
|
# ── Ref resolution ─────────────────────────────────────────────────
|
|
494
533
|
|
|
495
534
|
def _resolve_refs( # noqa: C901
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""sdmxlib.model — domain model, stdlib only."""
|
|
2
2
|
|
|
3
3
|
from sdmxlib.model.annotations import Annotation, Annotations
|
|
4
|
-
from sdmxlib.model.binding import DataflowBinding, DriftIssue
|
|
4
|
+
from sdmxlib.model.binding import DataflowBinding, DriftIssue
|
|
5
5
|
from sdmxlib.model.convert import from_dict, to_dict
|
|
6
6
|
from sdmxlib.model.validation import ReferenceIssue, ValidationIssue, validate, validate_references
|
|
7
7
|
from sdmxlib.model.base import MaintainableArtefact
|
|
@@ -48,11 +48,11 @@ from sdmxlib.model.concept import Concept, ConceptScheme
|
|
|
48
48
|
from sdmxlib.model.dataflow import Dataflow
|
|
49
49
|
from sdmxlib.model.dataset import Dataset
|
|
50
50
|
from sdmxlib.model.hierarchy import (
|
|
51
|
+
AmbiguousHierarchyError,
|
|
51
52
|
Hierarchy,
|
|
52
53
|
HierarchicalCode,
|
|
53
54
|
HierarchyAssociation,
|
|
54
55
|
Level,
|
|
55
|
-
default_hierarchy,
|
|
56
56
|
)
|
|
57
57
|
from sdmxlib.model.datastructure import (
|
|
58
58
|
AttachmentLevel,
|
|
@@ -84,6 +84,7 @@ from sdmxlib.model.urn import SdmxUrn
|
|
|
84
84
|
__all__ = [
|
|
85
85
|
"Agency",
|
|
86
86
|
"AgencyScheme",
|
|
87
|
+
"AmbiguousHierarchyError",
|
|
87
88
|
"Annotation",
|
|
88
89
|
"Annotations",
|
|
89
90
|
"AnyRef",
|
|
@@ -171,12 +172,10 @@ __all__ = [
|
|
|
171
172
|
"ValueMap",
|
|
172
173
|
"artefact_urn",
|
|
173
174
|
"code",
|
|
174
|
-
"default_hierarchy",
|
|
175
175
|
"from_dict",
|
|
176
176
|
"item",
|
|
177
177
|
"node",
|
|
178
178
|
"to_dict",
|
|
179
179
|
"validate",
|
|
180
|
-
"validate_binding",
|
|
181
180
|
"validate_references",
|
|
182
181
|
]
|
|
@@ -46,7 +46,6 @@ from sdmxlib.model.urn import SdmxUrn
|
|
|
46
46
|
|
|
47
47
|
if TYPE_CHECKING:
|
|
48
48
|
from sdmxlib.model.datastructure import DataStructure
|
|
49
|
-
from sdmxlib.model.registry import RegistryReader
|
|
50
49
|
|
|
51
50
|
|
|
52
51
|
def _require_aware(value: datetime) -> datetime:
|
|
@@ -253,23 +252,14 @@ class DriftIssue:
|
|
|
253
252
|
description: str
|
|
254
253
|
|
|
255
254
|
|
|
256
|
-
def
|
|
257
|
-
"""
|
|
255
|
+
def _validate_binding(binding: DataflowBinding, registry: object) -> list[DriftIssue]:
|
|
256
|
+
"""Internal helper: check a binding against a registry for structural drift.
|
|
258
257
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
Args:
|
|
265
|
-
binding: The `DataflowBinding` to validate.
|
|
266
|
-
registry: The structural source. Typically a
|
|
267
|
-
`FederatedRegistry` or a
|
|
268
|
-
`Registry`.
|
|
269
|
-
|
|
270
|
-
Returns:
|
|
271
|
-
A list of `DriftIssue` — empty if the pinned structural
|
|
272
|
-
state matches the current registry view.
|
|
258
|
+
Called by the public `validate_binding` method on registries
|
|
259
|
+
(e.g. `InMemoryRegistry.validate_binding`). ``registry`` is
|
|
260
|
+
duck-typed on ``get(urn) -> artefact | None`` — typed as ``object``
|
|
261
|
+
rather than the `RegistryReader` Protocol so the overloaded
|
|
262
|
+
``RestRegistry.get`` and similar registries satisfy it.
|
|
273
263
|
"""
|
|
274
264
|
binding_urn = str(
|
|
275
265
|
DataflowBinding.urn(
|
|
@@ -280,7 +270,7 @@ def validate_binding(binding: DataflowBinding, registry: "RegistryReader") -> li
|
|
|
280
270
|
)
|
|
281
271
|
issues: list[DriftIssue] = []
|
|
282
272
|
|
|
283
|
-
flow = registry.get(binding.dataflow.urn)
|
|
273
|
+
flow = registry.get(binding.dataflow.urn) # type: ignore[attr-defined]
|
|
284
274
|
if flow is None:
|
|
285
275
|
issues.append(
|
|
286
276
|
DriftIssue(
|
|
@@ -31,6 +31,7 @@ from sdmxlib._duckdb import scalar
|
|
|
31
31
|
from sdmxlib.model.annotations import Annotation, Annotations
|
|
32
32
|
from sdmxlib.model.codelist import Code
|
|
33
33
|
from sdmxlib.model.istring import InternationalString
|
|
34
|
+
from sdmxlib.sql import CompileContext
|
|
34
35
|
|
|
35
36
|
if TYPE_CHECKING:
|
|
36
37
|
import duckdb
|
|
@@ -109,7 +110,56 @@ class _AnnotationTextField(_FieldExpr):
|
|
|
109
110
|
|
|
110
111
|
|
|
111
112
|
class Predicate:
|
|
112
|
-
"""
|
|
113
|
+
"""Base for predicates the code-query compiler understands.
|
|
114
|
+
|
|
115
|
+
Conforms to the cross-library `sdmxlib.sql.Predicate`
|
|
116
|
+
Protocol via `Predicate.to_sql`: a downstream library that
|
|
117
|
+
composes its own predicates with sdmxlib's (e.g. ``pinaxlib``'s
|
|
118
|
+
catalog query builder) shares one `CompileContext`
|
|
119
|
+
across both, so joins dedup and aliases stay unique within a
|
|
120
|
+
single compilation pass.
|
|
121
|
+
|
|
122
|
+
Public hook:
|
|
123
|
+
|
|
124
|
+
pred = sl.code.descendants_of("CA", hierarchy=h)
|
|
125
|
+
ctx = sl.CompileContext(row_alias="c")
|
|
126
|
+
clause, params = pred.to_sql(ctx)
|
|
127
|
+
# ctx.join_sqls / ctx.join_params carry any accumulated LATERAL
|
|
128
|
+
# JOINs (only emitted by annotation predicates today).
|
|
129
|
+
|
|
130
|
+
The emitted SQL references columns on the relation aliased as
|
|
131
|
+
``ctx.row_alias``. The caller must have a ``sdmx.code``-shaped row
|
|
132
|
+
source under that alias, with these columns:
|
|
133
|
+
|
|
134
|
+
- ``code_id`` (TEXT) — code identifier
|
|
135
|
+
- ``parent_id`` (TEXT) — intrinsic parent code id, or NULL
|
|
136
|
+
- ``depth`` (SMALLINT) — depth in intrinsic parent chain (root = 0)
|
|
137
|
+
- ``name`` (MAP(VARCHAR, VARCHAR)) — localised label
|
|
138
|
+
- ``code_urn`` (TEXT) — full URN of the code
|
|
139
|
+
- ``codelist_urn`` (TEXT) — URN of the owning codelist
|
|
140
|
+
- ``ancestor_ids`` (TEXT[]) — flat ancestor chain (intrinsic parent)
|
|
141
|
+
|
|
142
|
+
Hierarchy-aware predicates (``descendants_of(..., hierarchy=h)``,
|
|
143
|
+
``hierarchy(h).level(...)``) additionally reference
|
|
144
|
+
``sdmx.hierarchy_node`` — that table must also be visible on the
|
|
145
|
+
consumer's connection. Schema names are looked up via
|
|
146
|
+
``ctx.schema('sdmx')`` so consumers can remap if needed.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
def to_sql(self, ctx: CompileContext) -> tuple[str, list[object]]:
|
|
150
|
+
"""Compile this predicate to a SQL WHERE-fragment + params.
|
|
151
|
+
|
|
152
|
+
Side-effect: appends to ``ctx.join_sqls`` / ``ctx.join_params``
|
|
153
|
+
if the predicate needs a LATERAL JOIN (annotation predicates).
|
|
154
|
+
Shared aliases are deduped through ``ctx.join_aliases``.
|
|
155
|
+
"""
|
|
156
|
+
return _emit_predicate(
|
|
157
|
+
self,
|
|
158
|
+
row_alias=ctx.row_alias,
|
|
159
|
+
join_sqls=ctx.join_sqls,
|
|
160
|
+
join_params=ctx.join_params,
|
|
161
|
+
annotation_join_aliases=ctx.join_aliases,
|
|
162
|
+
)
|
|
113
163
|
|
|
114
164
|
|
|
115
165
|
@attrs.frozen
|
|
@@ -434,8 +484,9 @@ class _CodeAccessor:
|
|
|
434
484
|
Accepts a URN string, a `Hierarchy` artefact, or a `Ref[Hierarchy]`.
|
|
435
485
|
The same code can sit in multiple Hierarchies with different parent
|
|
436
486
|
graphs — this picks the one you name. Pairs with
|
|
437
|
-
`
|
|
438
|
-
|
|
487
|
+
`InMemoryRegistry.unique_hierarchy(dataflow, dim_id)` (or
|
|
488
|
+
`applicable_hierarchies`) for the "use whatever the dataflow
|
|
489
|
+
declared" case.
|
|
439
490
|
"""
|
|
440
491
|
urn: str | None = None
|
|
441
492
|
if hierarchy is not None:
|
|
@@ -694,25 +745,18 @@ def _compile(
|
|
|
694
745
|
omit_order: bool = False,
|
|
695
746
|
) -> tuple[str, list[object]]:
|
|
696
747
|
"""Translate the predicate tree to a single ``sdmx.code`` query."""
|
|
697
|
-
|
|
698
|
-
join_params: list[object] = []
|
|
748
|
+
ctx = CompileContext(row_alias="c")
|
|
699
749
|
where_sqls: list[str] = ["c.codelist_urn = ?"]
|
|
700
750
|
where_params: list[object] = [query.codelist_urn]
|
|
701
|
-
annotation_join_aliases: dict[str, str] = {}
|
|
702
751
|
|
|
703
752
|
for pred in query.predicates:
|
|
704
|
-
clause, params =
|
|
705
|
-
pred,
|
|
706
|
-
join_sqls=join_sqls,
|
|
707
|
-
join_params=join_params,
|
|
708
|
-
annotation_join_aliases=annotation_join_aliases,
|
|
709
|
-
)
|
|
753
|
+
clause, params = pred.to_sql(ctx)
|
|
710
754
|
where_sqls.append(clause)
|
|
711
755
|
where_params.extend(params)
|
|
712
756
|
|
|
713
757
|
sql_parts = [f"SELECT {select_clause} FROM sdmx.code c"]
|
|
714
|
-
if join_sqls:
|
|
715
|
-
sql_parts.append(" ".join(join_sqls))
|
|
758
|
+
if ctx.join_sqls:
|
|
759
|
+
sql_parts.append(" ".join(ctx.join_sqls))
|
|
716
760
|
sql_parts.append("WHERE " + " AND ".join(where_sqls))
|
|
717
761
|
|
|
718
762
|
if not omit_order:
|
|
@@ -725,7 +769,7 @@ def _compile(
|
|
|
725
769
|
sql_parts.append(f"LIMIT {int(query.limit_n)}")
|
|
726
770
|
|
|
727
771
|
sql = " ".join(sql_parts)
|
|
728
|
-
return sql, [*join_params, *where_params, *order_params]
|
|
772
|
+
return sql, [*ctx.join_params, *where_params, *order_params]
|
|
729
773
|
|
|
730
774
|
|
|
731
775
|
def _build_order_by(sort_keys: tuple[_SortKey, ...]) -> tuple[list[str], list[object]]:
|
|
@@ -23,7 +23,30 @@ from sdmxlib.model.urn import SdmxUrn
|
|
|
23
23
|
|
|
24
24
|
if TYPE_CHECKING:
|
|
25
25
|
from sdmxlib.model.dataflow import Dataflow
|
|
26
|
-
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AmbiguousHierarchyError(ValueError):
|
|
29
|
+
"""Raised when `unique_hierarchy` finds more than one applicable Hierarchy.
|
|
30
|
+
|
|
31
|
+
Carries the list of conflicting `HierarchyAssociation` URNs. The
|
|
32
|
+
caller asserted "at most one Hierarchy applies" (via the
|
|
33
|
+
`unique_hierarchy` accessor); the IM permits many, so this signals
|
|
34
|
+
that the caller's assumption doesn't hold for this registry.
|
|
35
|
+
|
|
36
|
+
Use `applicable_hierarchies` instead when the IM-canonical many-case
|
|
37
|
+
is the intended question.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, dim_urn: str, dataflow_urn: str, ha_urns: list[str]) -> None:
|
|
41
|
+
self.dim_urn = dim_urn
|
|
42
|
+
self.dataflow_urn = dataflow_urn
|
|
43
|
+
self.ha_urns = ha_urns
|
|
44
|
+
joined = ", ".join(ha_urns)
|
|
45
|
+
super().__init__(
|
|
46
|
+
f"Multiple HierarchyAssociations bind {dim_urn} for dataflow "
|
|
47
|
+
f"{dataflow_urn}: {joined}. Use applicable_hierarchies() to "
|
|
48
|
+
"enumerate them.",
|
|
49
|
+
)
|
|
27
50
|
|
|
28
51
|
|
|
29
52
|
@define
|
|
@@ -399,34 +422,27 @@ class HierarchyAssociation(MaintainableArtefact):
|
|
|
399
422
|
context_object: AnyRef | None = None
|
|
400
423
|
|
|
401
424
|
|
|
402
|
-
def
|
|
403
|
-
registry:
|
|
425
|
+
def _partition_associations(
|
|
426
|
+
registry: object,
|
|
404
427
|
dataflow: "Dataflow",
|
|
405
428
|
dim_id: str,
|
|
406
|
-
) ->
|
|
407
|
-
"""
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
dataflow
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
`code._CodeAccessor.descendants_of` for the predicate that consumes
|
|
419
|
-
this value.
|
|
420
|
-
|
|
421
|
-
Raises:
|
|
422
|
-
ValueError: if multiple associations match at the same scope
|
|
423
|
-
(both dataflow-scoped or both unscoped). The SDMX spec
|
|
424
|
-
doesn't order them, so the caller must disambiguate.
|
|
429
|
+
) -> tuple[str, str, list["HierarchyAssociation"], list["HierarchyAssociation"]]:
|
|
430
|
+
"""Internal helper: enumerate HAs binding (dim_id, dataflow), split by scope.
|
|
431
|
+
|
|
432
|
+
Returns (dim_urn_str, dataflow_urn_str, scoped_HAs, unscoped_HAs) where
|
|
433
|
+
``scoped`` are HAs with ``context_object`` equal to ``dataflow``'s URN
|
|
434
|
+
and ``unscoped`` have ``context_object=None``. HAs naming a different
|
|
435
|
+
dataflow context are omitted.
|
|
436
|
+
|
|
437
|
+
``registry`` only needs a ``get_all(HierarchyAssociation) -> list``
|
|
438
|
+
method. The duck-typed ``object`` annotation keeps this helper
|
|
439
|
+
backend-agnostic; ``InMemoryRegistry`` and ``local.Registry`` both
|
|
440
|
+
satisfy it.
|
|
425
441
|
"""
|
|
426
442
|
from sdmxlib.model.datastructure import Dimension # noqa: PLC0415
|
|
427
443
|
|
|
428
444
|
if dataflow.structure is None:
|
|
429
|
-
return
|
|
445
|
+
return ("", "", [], [])
|
|
430
446
|
dsd_urn = dataflow.structure.urn
|
|
431
447
|
dim_urn = SdmxUrn.make(
|
|
432
448
|
Dimension,
|
|
@@ -442,21 +458,54 @@ def default_hierarchy(
|
|
|
442
458
|
|
|
443
459
|
scoped: list[HierarchyAssociation] = []
|
|
444
460
|
unscoped: list[HierarchyAssociation] = []
|
|
445
|
-
for ha in registry.get_all(HierarchyAssociation):
|
|
461
|
+
for ha in registry.get_all(HierarchyAssociation): # type: ignore[attr-defined]
|
|
446
462
|
if ha.linked_object is None or str(ha.linked_object.urn) != dim_urn_str:
|
|
447
463
|
continue
|
|
448
464
|
if ha.context_object is None:
|
|
449
465
|
unscoped.append(ha)
|
|
450
466
|
elif str(ha.context_object.urn) == df_urn_str:
|
|
451
467
|
scoped.append(ha)
|
|
452
|
-
|
|
468
|
+
return (dim_urn_str, df_urn_str, scoped, unscoped)
|
|
453
469
|
|
|
470
|
+
|
|
471
|
+
def _applicable_hierarchies(
|
|
472
|
+
registry: object,
|
|
473
|
+
dataflow: "Dataflow",
|
|
474
|
+
dim_id: str,
|
|
475
|
+
) -> list[Ref[Hierarchy]]:
|
|
476
|
+
"""Implementation of the `applicable_hierarchies` registry method.
|
|
477
|
+
|
|
478
|
+
See `InMemoryRegistry.applicable_hierarchies` for semantics.
|
|
479
|
+
"""
|
|
480
|
+
_dim, _df, scoped, unscoped = _partition_associations(registry, dataflow, dim_id)
|
|
481
|
+
scoped.sort(key=lambda ha: str(artefact_urn(ha)))
|
|
482
|
+
unscoped.sort(key=lambda ha: str(artefact_urn(ha)))
|
|
483
|
+
return [ha.linked_hierarchy for ha in (*scoped, *unscoped) if ha.linked_hierarchy is not None]
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
def _unique_hierarchy(
|
|
487
|
+
registry: object,
|
|
488
|
+
dataflow: "Dataflow",
|
|
489
|
+
dim_id: str,
|
|
490
|
+
) -> Ref[Hierarchy] | None:
|
|
491
|
+
"""Implementation of the `unique_hierarchy` registry method.
|
|
492
|
+
|
|
493
|
+
See `InMemoryRegistry.unique_hierarchy` for semantics.
|
|
494
|
+
"""
|
|
495
|
+
dim_urn_str, df_urn_str, scoped, unscoped = _partition_associations(
|
|
496
|
+
registry,
|
|
497
|
+
dataflow,
|
|
498
|
+
dim_id,
|
|
499
|
+
)
|
|
454
500
|
chosen = scoped or unscoped
|
|
455
501
|
if not chosen:
|
|
456
502
|
return None
|
|
457
503
|
if len(chosen) > 1:
|
|
458
|
-
|
|
459
|
-
urns
|
|
460
|
-
|
|
461
|
-
|
|
504
|
+
urns = [str(artefact_urn(ha)) for ha in chosen]
|
|
505
|
+
urns.sort()
|
|
506
|
+
raise AmbiguousHierarchyError(
|
|
507
|
+
dim_urn=dim_urn_str,
|
|
508
|
+
dataflow_urn=df_urn_str,
|
|
509
|
+
ha_urns=urns,
|
|
510
|
+
)
|
|
462
511
|
return chosen[0].linked_hierarchy
|
|
@@ -20,7 +20,7 @@ Example:
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
from collections.abc import Iterable
|
|
23
|
-
from typing import Any
|
|
23
|
+
from typing import TYPE_CHECKING, Any
|
|
24
24
|
|
|
25
25
|
import attrs
|
|
26
26
|
|
|
@@ -28,6 +28,11 @@ from sdmxlib.model.collections import ItemList
|
|
|
28
28
|
from sdmxlib.model.ref import AnyRef, HasUrn, Ref
|
|
29
29
|
from sdmxlib.model.urn import SdmxUrn
|
|
30
30
|
|
|
31
|
+
if TYPE_CHECKING:
|
|
32
|
+
from sdmxlib.model.binding import DataflowBinding, DriftIssue
|
|
33
|
+
from sdmxlib.model.dataflow import Dataflow
|
|
34
|
+
from sdmxlib.model.hierarchy import Hierarchy
|
|
35
|
+
|
|
31
36
|
|
|
32
37
|
class InMemoryRegistry:
|
|
33
38
|
"""In-memory SDMX artefact store with URN identity and Ref interning.
|
|
@@ -117,6 +122,54 @@ class InMemoryRegistry:
|
|
|
117
122
|
return str(urn) in self._store
|
|
118
123
|
return False
|
|
119
124
|
|
|
125
|
+
# ── Graph queries ────────────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
def applicable_hierarchies(
|
|
128
|
+
self,
|
|
129
|
+
dataflow: "Dataflow",
|
|
130
|
+
dim_id: str,
|
|
131
|
+
) -> "list[Ref[Hierarchy]]":
|
|
132
|
+
"""All Hierarchies whose HierarchyAssociations bind ``dim_id`` on ``dataflow``.
|
|
133
|
+
|
|
134
|
+
Returns dataflow-scoped HAs first (sorted lex by HA URN), then
|
|
135
|
+
unscoped HAs (also lex-sorted). HAs whose ``context_object``
|
|
136
|
+
names a different dataflow are skipped.
|
|
137
|
+
|
|
138
|
+
Per the SDMX 3.x IM the relation is many-to-many — this is the
|
|
139
|
+
canonical enumeration surface. Use `unique_hierarchy` when the
|
|
140
|
+
caller asserts at most one applies.
|
|
141
|
+
"""
|
|
142
|
+
from sdmxlib.model.hierarchy import _applicable_hierarchies # noqa: PLC0415
|
|
143
|
+
|
|
144
|
+
return _applicable_hierarchies(self, dataflow, dim_id)
|
|
145
|
+
|
|
146
|
+
def unique_hierarchy(
|
|
147
|
+
self,
|
|
148
|
+
dataflow: "Dataflow",
|
|
149
|
+
dim_id: str,
|
|
150
|
+
) -> "Ref[Hierarchy] | None":
|
|
151
|
+
"""The single Hierarchy applicable to ``dim_id`` on ``dataflow``, when exactly one applies.
|
|
152
|
+
|
|
153
|
+
Returns ``None`` if no HierarchyAssociation binds the dimension.
|
|
154
|
+
Returns the single applicable Hierarchy's `Ref` if exactly one
|
|
155
|
+
applies (dataflow-scoped wins over unscoped at the same dim).
|
|
156
|
+
Raises `AmbiguousHierarchyError` if more than one HA applies at
|
|
157
|
+
the chosen scope — call `applicable_hierarchies` to enumerate.
|
|
158
|
+
"""
|
|
159
|
+
from sdmxlib.model.hierarchy import _unique_hierarchy # noqa: PLC0415
|
|
160
|
+
|
|
161
|
+
return _unique_hierarchy(self, dataflow, dim_id)
|
|
162
|
+
|
|
163
|
+
def validate_binding(self, binding: "DataflowBinding") -> "list[DriftIssue]":
|
|
164
|
+
"""Check ``binding`` against this registry for structural drift.
|
|
165
|
+
|
|
166
|
+
Reports a `DriftIssue` per detected mismatch — empty list
|
|
167
|
+
means the pinned structural state matches the current registry.
|
|
168
|
+
"""
|
|
169
|
+
from sdmxlib.model.binding import _validate_binding # noqa: PLC0415
|
|
170
|
+
|
|
171
|
+
return _validate_binding(binding, self)
|
|
172
|
+
|
|
120
173
|
def __len__(self) -> int:
|
|
121
174
|
"""Number of artefacts stored."""
|
|
122
175
|
return len(self._store)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|