sdmxlib 0.70.0__tar.gz → 0.71.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.70.0 → sdmxlib-0.71.0}/PKG-INFO +1 -1
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/pyproject.toml +1 -1
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/pyproject.toml.orig +1 -1
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/__init__.py +2 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/api/client.py +40 -3
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/api/federated.py +3 -1
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/api/providers.py +13 -1
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/api/registry.py +24 -8
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/local/registry.py +57 -12
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_label_match.py +1 -1
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_common.py +1 -1
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/observations.py +6 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/raw.py +1 -1
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/sql.py +7 -7
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/schema.py +35 -6
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/README.md +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/_duckdb.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/_singleflight.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/_time.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/api/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/api/_freshness.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/api/policy.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/api/session.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/builders/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/builders/contentconstraint.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/builders/metadata_annotation.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/data_store.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/_arrow.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/_attachment.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/_coerce.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/_index.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/_positions.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/_refuse.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/data_format.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/gaps.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_csv/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_csv/reader.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_csv/writer.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/_data_common.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/_dto.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/_header.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/_losses.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/_periods.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/_resolve.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/_structure_dto.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/_structure_ingest.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/data.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/data_v1.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/data_v2.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/metadata.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/reader.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/structure_values.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json/writer.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json_1_0/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_json_1_0/writer.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml/_allowed_children.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml/_closed_world.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml/_common.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml/_header.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml/_losses.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml/_v3_reader.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml/_v3_writer.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml21/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml21/_providers.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml21/_synthetic.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml21/namespaces.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml21/reader.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml21/writer.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml30/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml30/metadata.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml30/namespaces.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml30/reader.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml30/writer.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml31/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml31/namespaces.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml31/reader.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/formats/sdmx_ml31/writer.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/local/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/local/_scan.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/local/data_store.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/local/sql.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_binding.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_container.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_invariants.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_items.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_rebind.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_referenceable.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_refshape.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_resolvers.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_rules.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/_tree.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/annotations.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/base.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/binding.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/category.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/codelist.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/collections.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/concept.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/constraint.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/convert.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/dataflow.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/datastructure.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/errors.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/expr.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/header.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/hierarchy.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/in_memory_registry.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/istring.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/link.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/mapping.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/message.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/metadata_provision.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/metadataflow.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/metadataset.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/metadatastructure.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/organisation.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/provision.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/ref.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/registry.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/representation.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/rest_api.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/rollup.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/urn.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/validation.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/valuelist.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/version.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/model/version_rewrite.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/polars.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/py.typed +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_closure.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_compile.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_edges.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_execute.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_expr.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_joins.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_plan.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_pushdown.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_rebase.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/_rest.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/accessors.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/query.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/query/schema.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/rest.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/session.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/__init__.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/_checked.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/_kinds.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/_parsed.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/_projections.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/lazy.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/readers.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/resolve.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/storage/writers.py +0 -0
- {sdmxlib-0.70.0 → sdmxlib-0.71.0}/src/sdmxlib/versions.py +0 -0
|
@@ -49,6 +49,7 @@ from sdmxlib.rest import ComponentFilter, parse_components, widen_key
|
|
|
49
49
|
from sdmxlib.query._common import Descending
|
|
50
50
|
from sdmxlib.model.convert import from_dict, to_dict
|
|
51
51
|
from sdmxlib.model.errors import ImRuleWarning, ReadAssumption, ReadGap, SdmxWarning
|
|
52
|
+
from sdmxlib.storage.schema import StoreSchemaError
|
|
52
53
|
from sdmxlib.model.validation import (
|
|
53
54
|
LossyReadError,
|
|
54
55
|
ReferenceIssue,
|
|
@@ -395,6 +396,7 @@ __all__ = [
|
|
|
395
396
|
"SliceError",
|
|
396
397
|
"SourceValue",
|
|
397
398
|
"SqlRelation",
|
|
399
|
+
"StoreSchemaError",
|
|
398
400
|
"Str",
|
|
399
401
|
"StructureMap",
|
|
400
402
|
"StructureMessage",
|
|
@@ -216,6 +216,33 @@ def _check_content_type(response: httpx.Response, fmt: DataFormat, *, url: str)
|
|
|
216
216
|
raise ValueError(msg)
|
|
217
217
|
|
|
218
218
|
|
|
219
|
+
def _require_csv(response: httpx.Response, *, accept: str, url: str) -> None:
|
|
220
|
+
"""Refuse an observation fetch's response that is not CSV (GH-50).
|
|
221
|
+
|
|
222
|
+
The data plane hands the body to DuckDB's CSV reader, which reads an
|
|
223
|
+
SDMX-ML document as a CSV of one malformed column. BIS answers
|
|
224
|
+
``text/csv`` with SDMX-ML and a 200, and ECB and ESTAT answer an
|
|
225
|
+
``Accept`` listing two types the same way. Either SDMX-CSV version, or
|
|
226
|
+
plain ``text/csv``, is fine: the reader takes both envelopes. As in
|
|
227
|
+
`_check_content_type`, a response with no ``Content-Type`` passes.
|
|
228
|
+
|
|
229
|
+
Raises:
|
|
230
|
+
ValueError: If the response says it is something other than CSV.
|
|
231
|
+
"""
|
|
232
|
+
header = response.headers.get("content-type")
|
|
233
|
+
if not header:
|
|
234
|
+
return
|
|
235
|
+
media_type = header.partition(";")[0].strip().lower()
|
|
236
|
+
if media_type == "text/csv" or media_type.endswith("+csv"):
|
|
237
|
+
return
|
|
238
|
+
msg = (
|
|
239
|
+
f"Asked for {accept!r} but the response is {header!r}, not CSV, so it was not read. "
|
|
240
|
+
"Pass the media type this provider serves CSV under as `csv_media_type=` "
|
|
241
|
+
f"(see `sl.Provider` for the ones checked). URL: {url}"
|
|
242
|
+
)
|
|
243
|
+
raise ValueError(msg)
|
|
244
|
+
|
|
245
|
+
|
|
219
246
|
def _raise_for_status(response: httpx.Response, *, url: str) -> None:
|
|
220
247
|
"""`raise_for_status`, with the two size refusals named.
|
|
221
248
|
|
|
@@ -277,6 +304,8 @@ class SdmxClient:
|
|
|
277
304
|
obey the provider exactly.
|
|
278
305
|
freshness_size: How many URLs to remember freshness for. Bounded,
|
|
279
306
|
per `docs/design/caching.md`; eviction only costs a request.
|
|
307
|
+
csv_media_type: The ``Accept`` an observation fetch sends. Defaults to
|
|
308
|
+
the SDMX-CSV the API version defines: 1.0 for 2.1, 2.0 for 3.0.
|
|
280
309
|
"""
|
|
281
310
|
|
|
282
311
|
def __init__(
|
|
@@ -288,6 +317,7 @@ class SdmxClient:
|
|
|
288
317
|
headers: dict[str, str] | None = None,
|
|
289
318
|
latest_ttl: int = 0,
|
|
290
319
|
freshness_size: int = 256,
|
|
320
|
+
csv_media_type: str | None = None,
|
|
291
321
|
**httpx_kwargs: Unpack[HttpxOptions],
|
|
292
322
|
) -> None:
|
|
293
323
|
self._endpoint = endpoint.rstrip("/")
|
|
@@ -300,6 +330,11 @@ class SdmxClient:
|
|
|
300
330
|
self._freshness_lock = threading.Lock()
|
|
301
331
|
self._api = api
|
|
302
332
|
self._v3 = api.startswith(("3", "2.0"))
|
|
333
|
+
#: What an observation fetch asks for (GH-50): the provider's, else
|
|
334
|
+
#: the SDMX-CSV the API version defines.
|
|
335
|
+
self._csv_media_type = (
|
|
336
|
+
csv_media_type or (DataFormat.SDMX_CSV_2_0_0 if self._v3 else DataFormat.SDMX_CSV_1_0_0).media_type
|
|
337
|
+
)
|
|
303
338
|
default_headers: dict[str, str]
|
|
304
339
|
if self._v3:
|
|
305
340
|
default_headers = {"Accept": "application/vnd.sdmx.structure+xml;version=3.0"}
|
|
@@ -566,8 +601,9 @@ class SdmxClient:
|
|
|
566
601
|
"""
|
|
567
602
|
url = f"{self._endpoint}/{path}"
|
|
568
603
|
with self._reaching(url):
|
|
569
|
-
response = self._client.get(url, params=dict(params), headers={"Accept":
|
|
604
|
+
response = self._client.get(url, params=dict(params), headers={"Accept": self._csv_media_type})
|
|
570
605
|
_raise_for_status(response, url=url)
|
|
606
|
+
_require_csv(response, accept=self._csv_media_type, url=url)
|
|
571
607
|
return response.content
|
|
572
608
|
|
|
573
609
|
def stream_data_at(self, path: str, params: Mapping[str, str], *, to: Path) -> None:
|
|
@@ -587,9 +623,10 @@ class SdmxClient:
|
|
|
587
623
|
url = f"{self._endpoint}/{path}"
|
|
588
624
|
with (
|
|
589
625
|
self._reaching(url),
|
|
590
|
-
self._client.stream("GET", url, params=dict(params), headers={"Accept":
|
|
626
|
+
self._client.stream("GET", url, params=dict(params), headers={"Accept": self._csv_media_type}) as response,
|
|
591
627
|
):
|
|
592
628
|
_raise_for_status(response, url=url)
|
|
629
|
+
_require_csv(response, accept=self._csv_media_type, url=url)
|
|
593
630
|
with to.open("wb") as handle:
|
|
594
631
|
for chunk in response.iter_bytes():
|
|
595
632
|
handle.write(chunk)
|
|
@@ -609,7 +646,7 @@ class SdmxClient:
|
|
|
609
646
|
"""Fetch a dataflow's data in `fmt`, unparsed.
|
|
610
647
|
|
|
611
648
|
The byte-faithful counterpart to `get_data_at` / `stream_data_at`, which
|
|
612
|
-
|
|
649
|
+
ask for CSV (`csv_media_type`) because their caller is about to hand the
|
|
613
650
|
body to DuckDB's CSV reader. Here the caller wants what the provider
|
|
614
651
|
sent, so the format is the caller's to choose and nothing inspects the
|
|
615
652
|
body afterwards.
|
|
@@ -10,7 +10,9 @@ sources cooperating to answer a single URN) is intentionally out of
|
|
|
10
10
|
scope; see #46 for that conversation.
|
|
11
11
|
|
|
12
12
|
Example:
|
|
13
|
-
|
|
13
|
+
# rebuild=True: a cache can be fetched again, so a store schema change may
|
|
14
|
+
# empty it rather than refuse to open (GH-57).
|
|
15
|
+
local = sl.LocalRegistry("cache.duckdb", source_registry="local", rebuild=True).open()
|
|
14
16
|
estat = sl.RestRegistry(sl.Provider.ESTAT, store=local)
|
|
15
17
|
sdmx_global = sl.RestRegistry(sl.Provider.SDMX_GLOBAL, store=local)
|
|
16
18
|
|
|
@@ -22,11 +22,18 @@ class ProviderInfo:
|
|
|
22
22
|
tag when a `RestRegistry` materialises through to a
|
|
23
23
|
local store. Required for federation; ``None`` for ad-hoc
|
|
24
24
|
URLs without a known label.
|
|
25
|
+
csv_media_type: The ``Accept`` an observation fetch sends, when the
|
|
26
|
+
API version's SDMX-CSV media type is not what this provider
|
|
27
|
+
answers (GH-50). None means that default: SDMX-CSV 1.0 for a
|
|
28
|
+
2.1 API, 2.0 for a 3.0 one. Set per provider because two 2.1
|
|
29
|
+
providers want opposite headers, and a header listing both gets
|
|
30
|
+
SDMX-ML back from each.
|
|
25
31
|
"""
|
|
26
32
|
|
|
27
33
|
url: str
|
|
28
34
|
api: str = "2.1"
|
|
29
35
|
id: str | None = None
|
|
36
|
+
csv_media_type: str | None = None
|
|
30
37
|
|
|
31
38
|
|
|
32
39
|
class Provider:
|
|
@@ -48,8 +55,13 @@ class Provider:
|
|
|
48
55
|
sl.Provider.ECB.api # "2.1"
|
|
49
56
|
"""
|
|
50
57
|
|
|
58
|
+
# The data `Accept` each answers, checked live 2026-10-22 (GH-50): ECB
|
|
59
|
+
# serves CSV for `text/csv` alone (406 for either SDMX-CSV type); ESTAT
|
|
60
|
+
# the reverse (406 for `text/csv`); BIS answers `text/csv` with SDMX-ML;
|
|
61
|
+
# OECD serves all three. IMF's is unchecked, so it takes the default.
|
|
62
|
+
|
|
51
63
|
#: European Central Bank
|
|
52
|
-
ECB = ProviderInfo("https://data-api.ecb.europa.eu/service", api="2.1", id="ECB")
|
|
64
|
+
ECB = ProviderInfo("https://data-api.ecb.europa.eu/service", api="2.1", id="ECB", csv_media_type="text/csv")
|
|
53
65
|
#: Bank for International Settlements
|
|
54
66
|
BIS = ProviderInfo("https://stats.bis.org/api/v1", api="2.1", id="BIS")
|
|
55
67
|
#: Eurostat
|
|
@@ -178,6 +178,11 @@ class RestRegistry:
|
|
|
178
178
|
whose `no-store` is appliance boilerplate, and cannot shorten a
|
|
179
179
|
generous one. Defaults to 0 — obey the provider exactly. Wanting
|
|
180
180
|
*less* is spelled with a pinned URN, which is not a pointer.
|
|
181
|
+
csv_media_type: The ``Accept`` `obs` fetches observations with
|
|
182
|
+
(GH-50). Defaults to the `ProviderInfo`'s, else the SDMX-CSV the
|
|
183
|
+
API version defines: 1.0 for 2.1, 2.0 for 3.0. Providers differ —
|
|
184
|
+
ECB serves CSV only for ``text/csv``, Eurostat only for SDMX-CSV
|
|
185
|
+
1.0 — so pass one when a bare URL's provider refuses the default.
|
|
181
186
|
|
|
182
187
|
**Thread safety: concurrent reads are safe and do not duplicate work.**
|
|
183
188
|
|
|
@@ -202,9 +207,13 @@ class RestRegistry:
|
|
|
202
207
|
store — inherit the local `LocalRegistry`'s rule and remain single-threaded.
|
|
203
208
|
|
|
204
209
|
Example:
|
|
205
|
-
# Cache-mode federation: structural reads cached locally.
|
|
206
|
-
|
|
207
|
-
|
|
210
|
+
# Cache-mode federation: structural reads cached locally. A cache can
|
|
211
|
+
# be fetched again, so it may be rebuilt when sdmxlib's store schema
|
|
212
|
+
# moves; any other store on an old schema is refused instead (GH-57).
|
|
213
|
+
with (
|
|
214
|
+
sl.LocalRegistry("cache.duckdb", rebuild=True) as local,
|
|
215
|
+
sl.RestRegistry(sl.Provider.ECB, store=local) as reg,
|
|
216
|
+
):
|
|
208
217
|
dsd = reg.get(sl.DataStructure.urn_for(agency="ECB", id="ECB_EXR1"), resolve=True)
|
|
209
218
|
|
|
210
219
|
# Single-source ad-hoc.
|
|
@@ -226,6 +235,7 @@ class RestRegistry:
|
|
|
226
235
|
store: LocalRegistry | None = None,
|
|
227
236
|
fetch_cache_size: int = 256,
|
|
228
237
|
latest_ttl: int = 0,
|
|
238
|
+
csv_media_type: str | None = None,
|
|
229
239
|
**httpx_kwargs: Unpack[HttpxOptions],
|
|
230
240
|
) -> None:
|
|
231
241
|
if isinstance(endpoint, ProviderInfo):
|
|
@@ -233,6 +243,8 @@ class RestRegistry:
|
|
|
233
243
|
api = endpoint.api
|
|
234
244
|
if id is None:
|
|
235
245
|
id = endpoint.id
|
|
246
|
+
if csv_media_type is None:
|
|
247
|
+
csv_media_type = endpoint.csv_media_type
|
|
236
248
|
else:
|
|
237
249
|
url = endpoint
|
|
238
250
|
|
|
@@ -278,6 +290,7 @@ class RestRegistry:
|
|
|
278
290
|
timeout=timeout,
|
|
279
291
|
headers=headers,
|
|
280
292
|
latest_ttl=latest_ttl,
|
|
293
|
+
csv_media_type=csv_media_type,
|
|
281
294
|
**httpx_kwargs,
|
|
282
295
|
)
|
|
283
296
|
self._session = Session()
|
|
@@ -1001,8 +1014,11 @@ class RestRegistry:
|
|
|
1001
1014
|
without a DSD there is no key order, and a key built on a guess
|
|
1002
1015
|
queries a different slice silently.
|
|
1003
1016
|
"""
|
|
1004
|
-
|
|
1005
|
-
|
|
1017
|
+
# Bound to the dataflow as fetched, not as asked for: the data URL names
|
|
1018
|
+
# its version, and BIS and OECD refuse `latest` there with a 400 that
|
|
1019
|
+
# ECB and Eurostat let pass (GH-51).
|
|
1020
|
+
resolved, dsd = self._resolve_data_structure(_flow_urn(flow))
|
|
1021
|
+
urn = resolved.urn
|
|
1006
1022
|
store = DuckDBDataStore.from_registry(self._query_store())
|
|
1007
1023
|
binding = DataflowBinding.from_dsd(
|
|
1008
1024
|
dsd,
|
|
@@ -1036,8 +1052,8 @@ class RestRegistry:
|
|
|
1036
1052
|
raise RuntimeError(msg)
|
|
1037
1053
|
return str(rows[0][0])
|
|
1038
1054
|
|
|
1039
|
-
def _resolve_data_structure(self, urn: SdmxUrn[Dataflow]) -> DataStructure:
|
|
1040
|
-
"""The dataflow
|
|
1055
|
+
def _resolve_data_structure(self, urn: SdmxUrn[Dataflow]) -> tuple[Dataflow, DataStructure]:
|
|
1056
|
+
"""The dataflow as fetched, and its structure resolved, or a refusal naming what is missing."""
|
|
1041
1057
|
flow = self._fetch_one(
|
|
1042
1058
|
Dataflow, urn.agency, urn.id, urn.version, detail="referencepartial", references="descendants"
|
|
1043
1059
|
)
|
|
@@ -1048,7 +1064,7 @@ class RestRegistry:
|
|
|
1048
1064
|
if not isinstance(dsd, DataStructure):
|
|
1049
1065
|
msg = f"{flow.structure.urn} did not resolve to a DataStructure."
|
|
1050
1066
|
raise TypeError(msg)
|
|
1051
|
-
return dsd
|
|
1067
|
+
return flow, dsd
|
|
1052
1068
|
|
|
1053
1069
|
def _fetch_data_for[R: "RowKind"](
|
|
1054
1070
|
self, plan: Plan[R], _projection: frozenset[tuple[str, str]] | None = None
|
|
@@ -20,7 +20,7 @@ stashes the URI; the DuckDB connection is opened by ``open()`` /
|
|
|
20
20
|
dsd = local.get(dsd_urn)
|
|
21
21
|
cm = local.code_map(codelist_urn, lang="en")
|
|
22
22
|
|
|
23
|
-
# Share an existing DuckDB connection (e.g. with
|
|
23
|
+
# Share an existing DuckDB connection (e.g. with statcatlib): use the
|
|
24
24
|
# ``from_connection`` classmethod, which doesn't take ownership of the
|
|
25
25
|
# connection's lifecycle.
|
|
26
26
|
import duckdb
|
|
@@ -37,6 +37,7 @@ import threading
|
|
|
37
37
|
import time
|
|
38
38
|
from collections.abc import Callable, Generator, Iterable, Mapping, Sequence
|
|
39
39
|
from contextlib import contextmanager
|
|
40
|
+
from importlib import metadata
|
|
40
41
|
from pathlib import Path
|
|
41
42
|
from typing import Final, Literal, Self, cast, overload, override
|
|
42
43
|
|
|
@@ -94,9 +95,11 @@ from sdmxlib.storage.readers import make_lazy_codelist
|
|
|
94
95
|
from sdmxlib.storage.schema import (
|
|
95
96
|
SCHEMA_VERSION,
|
|
96
97
|
ReadOnlySchemaError,
|
|
98
|
+
StoreSchemaError,
|
|
97
99
|
check_scan_integrity,
|
|
98
100
|
connection_is_read_only,
|
|
99
101
|
get_schema_version,
|
|
102
|
+
holds_rows,
|
|
100
103
|
migrate,
|
|
101
104
|
schema_exists,
|
|
102
105
|
)
|
|
@@ -104,6 +107,14 @@ from sdmxlib.storage.schema import (
|
|
|
104
107
|
_SIBLING_INDEX = re.compile(r"\[\d+\]")
|
|
105
108
|
|
|
106
109
|
|
|
110
|
+
def _sdmxlib_version() -> str:
|
|
111
|
+
"""This sdmxlib's version, for a message; ``"unknown"`` when not installed as a package."""
|
|
112
|
+
try:
|
|
113
|
+
return metadata.version("sdmxlib")
|
|
114
|
+
except metadata.PackageNotFoundError:
|
|
115
|
+
return "unknown"
|
|
116
|
+
|
|
117
|
+
|
|
107
118
|
def _declared_path(attribute_path: str) -> str:
|
|
108
119
|
"""The MSD path a reported ``attribute_path`` instantiates.
|
|
109
120
|
|
|
@@ -573,9 +584,17 @@ class LocalRegistry(SessionFactory):
|
|
|
573
584
|
source_registry: str | None = None,
|
|
574
585
|
resolve_cache_items: int = DEFAULT_RESOLVE_CACHE_ITEMS,
|
|
575
586
|
target: str | None = None,
|
|
587
|
+
rebuild: bool = False,
|
|
576
588
|
) -> None:
|
|
577
589
|
"""Open or wrap a DuckDB-backed registry.
|
|
578
590
|
|
|
591
|
+
``rebuild`` lets opening drop and rebuild a store that holds artefacts
|
|
592
|
+
on another schema version (GH-57). Without it such a store is refused
|
|
593
|
+
with `StoreSchemaError`, because the rebuild empties it: right for a
|
|
594
|
+
cache that `RestRegistry(store=...)` can fetch again, and wrong for a
|
|
595
|
+
bundle or a store filled with ``add``. An empty store, or a new file,
|
|
596
|
+
is created or migrated either way.
|
|
597
|
+
|
|
579
598
|
``target`` declares which SDMX-ML version this store's contents are
|
|
580
599
|
meant to be written as — ``"2.1"``, ``"3.0"`` or ``"3.1"``. Adding an
|
|
581
600
|
artefact carrying something that format cannot represent then **raises**
|
|
@@ -602,6 +621,8 @@ class LocalRegistry(SessionFactory):
|
|
|
602
621
|
self._uri = store_path(uri, owner="LocalRegistry", wrap="sl.LocalRegistry.from_connection(conn)")
|
|
603
622
|
self._source_registry = source_registry
|
|
604
623
|
self._target = target
|
|
624
|
+
#: Whether opening may drop a populated store on another schema (GH-57).
|
|
625
|
+
self._rebuild = rebuild
|
|
605
626
|
self._conn: duckdb.DuckDBPyConnection | None = None
|
|
606
627
|
self._cursor: duckdb.DuckDBPyConnection | None = None
|
|
607
628
|
self._owns_connection = True
|
|
@@ -697,6 +718,7 @@ class LocalRegistry(SessionFactory):
|
|
|
697
718
|
source_registry: str | None = None,
|
|
698
719
|
resolve_cache_items: int = DEFAULT_RESOLVE_CACHE_ITEMS,
|
|
699
720
|
target: str | None = None,
|
|
721
|
+
rebuild: bool = False,
|
|
700
722
|
) -> Self:
|
|
701
723
|
"""Wrap an existing DuckDB connection.
|
|
702
724
|
|
|
@@ -705,9 +727,10 @@ class LocalRegistry(SessionFactory):
|
|
|
705
727
|
is shared with another component (e.g. a co-located DuckLake catalog)
|
|
706
728
|
that owns its lifecycle.
|
|
707
729
|
|
|
708
|
-
``resolve_cache_items`` configures the resolver cache (FIFO),
|
|
709
|
-
declares an SDMX-ML version to hold the contents to
|
|
710
|
-
|
|
730
|
+
``resolve_cache_items`` configures the resolver cache (FIFO), ``target``
|
|
731
|
+
declares an SDMX-ML version to hold the contents to, and ``rebuild``
|
|
732
|
+
lets a populated store on another schema be dropped; all are described
|
|
733
|
+
on `__init__`.
|
|
711
734
|
"""
|
|
712
735
|
# `Self`-annotated because `cls.__new__(cls)` is otherwise untyped: the
|
|
713
736
|
# local would be `Self@__new__` with no field information. (It also
|
|
@@ -727,6 +750,7 @@ class LocalRegistry(SessionFactory):
|
|
|
727
750
|
# raise `AttributeError` on any registry built this way. A comment was
|
|
728
751
|
# not enough; `test_constructors_agree_on_fields` is the guard.
|
|
729
752
|
instance._target = target
|
|
753
|
+
instance._rebuild = rebuild
|
|
730
754
|
instance._conn = conn
|
|
731
755
|
instance._cursor = None
|
|
732
756
|
instance._owns_connection = False
|
|
@@ -744,19 +768,33 @@ class LocalRegistry(SessionFactory):
|
|
|
744
768
|
return instance
|
|
745
769
|
|
|
746
770
|
def _initialise_schema(self) -> None:
|
|
747
|
-
"""Migrate or open a cursor on the ``sdmx`` schema.
|
|
771
|
+
"""Migrate or open a cursor on the ``sdmx`` schema.
|
|
772
|
+
|
|
773
|
+
Raises:
|
|
774
|
+
StoreSchemaError: If the store holds artefacts on another schema
|
|
775
|
+
version and ``rebuild`` was not asked for (GH-57).
|
|
776
|
+
ReadOnlySchemaError: If the schema must be created or rebuilt and
|
|
777
|
+
the connection is read-only.
|
|
778
|
+
"""
|
|
748
779
|
if self._conn is None:
|
|
749
780
|
return
|
|
750
781
|
version = get_schema_version(self._conn)
|
|
751
|
-
|
|
782
|
+
exists = schema_exists(self._conn)
|
|
783
|
+
if not exists or version != SCHEMA_VERSION:
|
|
784
|
+
found = f"schema {version}" if exists else "no sdmx schema"
|
|
785
|
+
if exists and not self._rebuild and holds_rows(self._conn):
|
|
786
|
+
msg = (
|
|
787
|
+
f"{self._uri} holds artefacts on store {found}, and this sdmxlib "
|
|
788
|
+
f"({_sdmxlib_version()}) reads schema {SCHEMA_VERSION}, so it was not opened: "
|
|
789
|
+
f"migrating means dropping them. A bundle: rebuild it from its source with this "
|
|
790
|
+
f"sdmxlib, or read it with the one that built it. A cache that can be fetched "
|
|
791
|
+
f"again: open it with rebuild=True."
|
|
792
|
+
)
|
|
793
|
+
raise StoreSchemaError(msg)
|
|
752
794
|
if connection_is_read_only(self._conn):
|
|
753
|
-
found = version if schema_exists(self._conn) else "no sdmx schema"
|
|
754
795
|
msg = (
|
|
755
|
-
f"this store
|
|
756
|
-
f"
|
|
757
|
-
f"is drop-and-rebuild, which a read-only connection cannot do "
|
|
758
|
-
f"— reopen it read-write to rebuild in place, or rebuild the "
|
|
759
|
-
f"bundle from its source."
|
|
796
|
+
f"this store has {found}, and this build of sdmxlib uses schema {SCHEMA_VERSION}. "
|
|
797
|
+
f"Creating or rebuilding it needs a read-write connection."
|
|
760
798
|
)
|
|
761
799
|
raise ReadOnlySchemaError(msg)
|
|
762
800
|
self._cursor = migrate(self._conn)
|
|
@@ -1540,6 +1578,10 @@ class LocalRegistry(SessionFactory):
|
|
|
1540
1578
|
set — §9's rule. The lake is ATTACHed lazily, when something first asks
|
|
1541
1579
|
for observations, so opening a bundle to read a codelist costs nothing.
|
|
1542
1580
|
|
|
1581
|
+
**A bundle is never rebuilt.** Its ``meta.duckdb`` is the published
|
|
1582
|
+
record, so one built on another schema version is refused with
|
|
1583
|
+
`StoreSchemaError` rather than emptied (GH-57).
|
|
1584
|
+
|
|
1543
1585
|
Args:
|
|
1544
1586
|
path: The bundle directory.
|
|
1545
1587
|
source_registry: Provenance tag for artefacts added later.
|
|
@@ -1547,6 +1589,9 @@ class LocalRegistry(SessionFactory):
|
|
|
1547
1589
|
|
|
1548
1590
|
Returns:
|
|
1549
1591
|
An open registry over the bundle.
|
|
1592
|
+
|
|
1593
|
+
Raises:
|
|
1594
|
+
StoreSchemaError: If the bundle was built on another schema version.
|
|
1550
1595
|
"""
|
|
1551
1596
|
root = Path(path) if isinstance(path, str) else path
|
|
1552
1597
|
registry = cls(
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Package-internal: plain names in a private module, per CLAUDE.md rule 4. It is
|
|
4
4
|
deliberately *not* in `sdmxlib.sql`, which is the public cross-library seam —
|
|
5
5
|
these fragments are an implementation detail of `code_query` and
|
|
6
|
-
`hierarchy_query`, not part of the contract
|
|
6
|
+
`hierarchy_query`, not part of the contract statcatlib composes against.
|
|
7
7
|
|
|
8
8
|
Four matching strengths, exposed on the DSLs as the plain string ops plus
|
|
9
9
|
three namespaces:
|
|
@@ -229,7 +229,7 @@ def canonical_key(value: object) -> tuple[object, ...]:
|
|
|
229
229
|
),
|
|
230
230
|
)
|
|
231
231
|
if isinstance(value, SqlRelation):
|
|
232
|
-
# A foreign relation —
|
|
232
|
+
# A foreign relation — statcatlib's, or anything else implementing the
|
|
233
233
|
# protocol without being attrs. Its compiled SQL and binds are the only
|
|
234
234
|
# stable description available, and are what a cache would key on.
|
|
235
235
|
sql, params = value.to_sql()
|
|
@@ -692,6 +692,12 @@ class ObsQuery(Query[ObsRow]):
|
|
|
692
692
|
forms the index. One measure fills the cells; several give a column
|
|
693
693
|
per measure per value. Attributes are not carried.
|
|
694
694
|
|
|
695
|
+
A `pl.DataFrame`, not a `LazyFrame` (#516), as every other terminal's
|
|
696
|
+
result is: the query has run by the time a pivot can start, so
|
|
697
|
+
laziness after it would push nothing back into the fetch, and when
|
|
698
|
+
the axis's enumeration is unresolved its columns can only be read
|
|
699
|
+
from the rows. ``.lazy()`` on the result costs nothing.
|
|
700
|
+
|
|
695
701
|
Args:
|
|
696
702
|
on: The dimension to spread into columns. Needed when the DSD
|
|
697
703
|
declares no measure dimension.
|
|
@@ -25,7 +25,7 @@ back across.
|
|
|
25
25
|
|
|
26
26
|
The protocol itself is untouched, because it is not the raw-SQL seam: `Query`
|
|
27
27
|
satisfies it, which is how one structural query filters another, and external
|
|
28
|
-
producers satisfy it, which is how
|
|
28
|
+
producers satisfy it, which is how statcatlib composes. Both are compiled
|
|
29
29
|
relations the planner can still reason about. Raw text is not.
|
|
30
30
|
"""
|
|
31
31
|
|
|
@@ -16,12 +16,12 @@ there is nothing for a foreign predicate to be assignable to.
|
|
|
16
16
|
That is a real narrowing and worth naming rather than burying: a downstream
|
|
17
17
|
library can no longer inject a *predicate*. It can still inject a *relation*,
|
|
18
18
|
which is the composition that was actually used — `SqlRelation` has consumers
|
|
19
|
-
in `rest.py`, `is_in`, and
|
|
19
|
+
in `rest.py`, `is_in`, and statcatlib — and which works precisely because a
|
|
20
20
|
single-column subquery carries no row kind to disagree about.
|
|
21
21
|
|
|
22
22
|
A bundle laid out as a single DuckDB file co-locating ``sdmx.*``,
|
|
23
|
-
``
|
|
24
|
-
composition routinely useful across sdmxlib +
|
|
23
|
+
``statcat.*``, and the DuckLake catalog tables is what makes `SqlRelation`
|
|
24
|
+
composition routinely useful across sdmxlib + statcatlib: subquery tables are
|
|
25
25
|
always visible on the same connection.
|
|
26
26
|
"""
|
|
27
27
|
|
|
@@ -34,7 +34,7 @@ __all__ = ["HasSource", "SqlRelation"]
|
|
|
34
34
|
class SqlRelation(Protocol):
|
|
35
35
|
"""A lazy relation usable inside ``WHERE col IN (...)`` or as a join input.
|
|
36
36
|
|
|
37
|
-
Producers — `sdmxlib.query.query.Select`, `AncestorMap`, and the
|
|
37
|
+
Producers — `sdmxlib.query.query.Select`, `AncestorMap`, and the statcatlib
|
|
38
38
|
query builders — expose their compiled SELECT as a ``(sql, params)`` pair.
|
|
39
39
|
The consumer splices the SQL inline as a subquery and concatenates the
|
|
40
40
|
params with the outer query's binds, so an N-row traversal becomes one
|
|
@@ -43,8 +43,8 @@ class SqlRelation(Protocol):
|
|
|
43
43
|
The subquery's tables must be visible on the consumer's connection. For
|
|
44
44
|
`DuckDBDataStore` (which shares its connection with `Registry`), codelist
|
|
45
45
|
relations compose automatically because both sides see the same
|
|
46
|
-
``sdmx.code`` table. For
|
|
47
|
-
bundle uses Topology B (one DuckDB file holding sdmx.*,
|
|
46
|
+
``sdmx.code`` table. For statcatlib's ``Catalog``, the same applies when the
|
|
47
|
+
bundle uses Topology B (one DuckDB file holding sdmx.*, statcat.*, and the
|
|
48
48
|
DuckLake catalog).
|
|
49
49
|
"""
|
|
50
50
|
|
|
@@ -79,7 +79,7 @@ class HasSource(Protocol):
|
|
|
79
79
|
Optional by design, and deliberately not folded into `SqlRelation`. Making
|
|
80
80
|
it required would break every existing producer, and a foreign relation
|
|
81
81
|
that cannot answer is not necessarily wrong — Topology B puts ``sdmx.*``
|
|
82
|
-
and ``
|
|
82
|
+
and ``statcat.*`` on one connection precisely so cross-library composition is
|
|
83
83
|
legitimate. So: a producer that *can* identify its source gets checked, and
|
|
84
84
|
one that cannot is trusted, which is exactly the state before this existed.
|
|
85
85
|
"""
|
|
@@ -1571,12 +1571,25 @@ def check_scan_integrity(conn: duckdb.DuckDBPyConnection) -> None:
|
|
|
1571
1571
|
raise ScanIntegrityError(msg)
|
|
1572
1572
|
|
|
1573
1573
|
|
|
1574
|
-
class
|
|
1574
|
+
class StoreSchemaError(Exception):
|
|
1575
|
+
"""A store holds artefacts on a schema version this build does not read (GH-57).
|
|
1576
|
+
|
|
1577
|
+
Migration is *drop and rebuild*, which is only right for a cache that can
|
|
1578
|
+
be fetched again. A bundle's ``meta.duckdb`` is the published record, and a
|
|
1579
|
+
store filled with ``add`` may have no source to fetch from, so a populated
|
|
1580
|
+
store on another version is refused rather than emptied: sdmxlib 0.68 to
|
|
1581
|
+
0.70 opened a 0.67 bundle read-write and deleted every artefact in it,
|
|
1582
|
+
silently, for the reader and for the older sdmxlib that built it.
|
|
1583
|
+
``LocalRegistry(path, rebuild=True)`` asks for the rebuild.
|
|
1584
|
+
"""
|
|
1585
|
+
|
|
1586
|
+
|
|
1587
|
+
class ReadOnlySchemaError(StoreSchemaError):
|
|
1575
1588
|
"""A read-only store is not on this build's schema version.
|
|
1576
1589
|
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
``Cannot execute statement of type "DROP" on database … read-only``,
|
|
1590
|
+
Raised when a rebuild would otherwise run, but the connection is read-only:
|
|
1591
|
+
an empty store to create, or ``rebuild=True``. Raised in place of DuckDB's
|
|
1592
|
+
own ``Cannot execute statement of type "DROP" on database … read-only``,
|
|
1580
1593
|
which names neither version and reads as a bug in the caller.
|
|
1581
1594
|
|
|
1582
1595
|
First hit when 2.19 landed (#145): the bundle-assertion tier opens
|
|
@@ -1591,11 +1604,27 @@ def connection_is_read_only(conn: duckdb.DuckDBPyConnection) -> bool:
|
|
|
1591
1604
|
return bool(row[0]) if row else False
|
|
1592
1605
|
|
|
1593
1606
|
|
|
1607
|
+
def holds_rows(conn: duckdb.DuckDBPyConnection) -> bool:
|
|
1608
|
+
"""Whether any ``sdmx`` table but ``_meta`` has a row — what a rebuild would lose.
|
|
1609
|
+
|
|
1610
|
+
Asks every table rather than ``artefact`` alone, because the store being
|
|
1611
|
+
asked about is on *another* schema version, whose tables this build does
|
|
1612
|
+
not know.
|
|
1613
|
+
"""
|
|
1614
|
+
tables = conn.execute(
|
|
1615
|
+
"""
|
|
1616
|
+
SELECT table_name FROM duckdb_tables()
|
|
1617
|
+
WHERE database_name = current_database() AND schema_name = 'sdmx' AND table_name <> '_meta'
|
|
1618
|
+
"""
|
|
1619
|
+
).fetchall()
|
|
1620
|
+
return any(conn.execute(f'SELECT EXISTS (SELECT 1 FROM sdmx."{name}")').fetchone() == (True,) for (name,) in tables)
|
|
1621
|
+
|
|
1622
|
+
|
|
1594
1623
|
def migrate(conn: duckdb.DuckDBPyConnection) -> duckdb.DuckDBPyConnection:
|
|
1595
1624
|
"""Drop and rebuild the ``sdmx`` schema.
|
|
1596
1625
|
|
|
1597
|
-
|
|
1598
|
-
|
|
1626
|
+
Everything in it is lost. `LocalRegistry` calls this only for a store with
|
|
1627
|
+
nothing in it, or when opened with ``rebuild=True`` (GH-57).
|
|
1599
1628
|
|
|
1600
1629
|
Returns a fresh cursor with ``search_path = 'sdmx'``.
|
|
1601
1630
|
"""
|
|
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
|
|
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
|