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.
Files changed (83) hide show
  1. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/PKG-INFO +1 -1
  2. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/pyproject.toml +1 -1
  3. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/__init__.py +2 -4
  4. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/federated.py +14 -0
  5. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/registry.py +13 -0
  6. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/local/registry.py +40 -1
  7. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/__init__.py +3 -4
  8. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/binding.py +8 -18
  9. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/code_query.py +59 -15
  10. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/hierarchy.py +78 -29
  11. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/in_memory_registry.py +54 -1
  12. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/README.md +0 -0
  13. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/_duckdb.py +0 -0
  14. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/__init__.py +0 -0
  15. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/client.py +0 -0
  16. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/filters.py +0 -0
  17. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/policy.py +0 -0
  18. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/providers.py +0 -0
  19. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/query.py +0 -0
  20. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/api/session.py +0 -0
  21. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/catalog.py +0 -0
  22. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/data_store.py +0 -0
  23. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/__init__.py +0 -0
  24. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
  25. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_csv/reader.py +0 -0
  26. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_csv/writer.py +0 -0
  27. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
  28. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_json/metadata.py +0 -0
  29. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_json/reader.py +0 -0
  30. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_json/writer.py +0 -0
  31. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml/__init__.py +0 -0
  32. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml/_common.py +0 -0
  33. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
  34. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
  35. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml21/reader.py +0 -0
  36. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml21/writer.py +0 -0
  37. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
  38. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/metadata.py +0 -0
  39. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +0 -0
  40. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/reader.py +0 -0
  41. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml30/writer.py +0 -0
  42. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml31/__init__.py +0 -0
  43. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml31/namespaces.py +0 -0
  44. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml31/reader.py +0 -0
  45. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/formats/sdmx_ml31/writer.py +0 -0
  46. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/local/__init__.py +0 -0
  47. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/local/data_store.py +0 -0
  48. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/annotations.py +0 -0
  49. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/base.py +0 -0
  50. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/category.py +0 -0
  51. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/codelist.py +0 -0
  52. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/collections.py +0 -0
  53. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/concept.py +0 -0
  54. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/constraint.py +0 -0
  55. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/convert.py +0 -0
  56. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/dataflow.py +0 -0
  57. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/dataset.py +0 -0
  58. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/datastructure.py +0 -0
  59. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/expr.py +0 -0
  60. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/hierarchy_query.py +0 -0
  61. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/istring.py +0 -0
  62. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/mapping.py +0 -0
  63. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/message.py +0 -0
  64. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/metadataflow.py +0 -0
  65. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/metadataset.py +0 -0
  66. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/metadatastructure.py +0 -0
  67. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/organisation.py +0 -0
  68. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/provision.py +0 -0
  69. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/ref.py +0 -0
  70. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/registry.py +0 -0
  71. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/representation.py +0 -0
  72. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/urn.py +0 -0
  73. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/model/validation.py +0 -0
  74. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/polars.py +0 -0
  75. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/py.typed +0 -0
  76. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/rest.py +0 -0
  77. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/sql.py +0 -0
  78. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/__init__.py +0 -0
  79. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/_kinds.py +0 -0
  80. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/lazy.py +0 -0
  81. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/readers.py +0 -0
  82. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/schema.py +0 -0
  83. {sdmxlib-0.32.2 → sdmxlib-0.34.0}/src/sdmxlib/storage/writers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sdmxlib
3
- Version: 0.32.2
3
+ Version: 0.34.0
4
4
  Summary: SDMX structural metadata library for Python
5
5
  Keywords: sdmx,statistics,metadata,datastructure
6
6
  Author: gabrielgellner
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sdmxlib"
3
- version = "0.32.2"
3
+ version = "0.34.0"
4
4
  description = "SDMX structural metadata library for Python"
5
5
  readme = "README.md"
6
6
  license = { text = "Apache-2.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, validate_binding
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 validate_binding(binding: DataflowBinding, registry: "RegistryReader") -> list[DriftIssue]:
257
- """Check a binding against a structural registry for drift.
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
- v1 reports three drift kinds (see `DriftIssue`). Detailed
260
- codelist content diffs (added codes / removed codes / renamed
261
- labels) are intentionally deferredthe v1 ``structural_version``
262
- pin is enough to surface the case operationally.
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
- """Marker base for predicates the query compiler understands."""
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
- `Dataflow.default_hierarchy(dim_id)` for the "use whatever the
438
- dataflow declared" case.
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
- join_sqls: list[str] = []
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 = _emit_predicate(
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
- from sdmxlib.model.in_memory_registry import InMemoryRegistry
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 default_hierarchy(
403
- registry: "InMemoryRegistry",
425
+ def _partition_associations(
426
+ registry: object,
404
427
  dataflow: "Dataflow",
405
428
  dim_id: str,
406
- ) -> Ref[Hierarchy] | None:
407
- """Resolve the declared default Hierarchy for a Dataflow's dimension.
408
-
409
- Scans `HierarchyAssociation` artefacts in ``registry`` for those
410
- whose ``linked_object`` is the named dimension on ``dataflow``'s DSD.
411
- Dataflow-scoped associations (``context_object`` equals this
412
- dataflow's URN) win over unscoped ones; an association whose
413
- ``context_object`` names a *different* dataflow is ignored.
414
-
415
- Returns the linked Hierarchy ref, or ``None`` if no association
416
- applies. Returning ``None`` is the cue to fall back to the
417
- codelist's intrinsic ``parent`` chain — see
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 None
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
- # else: bound to a different dataflow context, ignore
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
- kind = "dataflow-scoped" if scoped else "unscoped"
459
- urns = ", ".join(str(artefact_urn(ha)) for ha in chosen)
460
- msg = f"Multiple {kind} HierarchyAssociations bind {dim_urn_str} for dataflow {df_urn_str}: {urns}"
461
- raise ValueError(msg)
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