statcatlib 0.8.0__tar.gz → 0.9.1__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.
- {statcatlib-0.8.0 → statcatlib-0.9.1}/PKG-INFO +6 -6
- {statcatlib-0.8.0 → statcatlib-0.9.1}/README.md +1 -1
- {statcatlib-0.8.0 → statcatlib-0.9.1}/pyproject.toml +6 -6
- {statcatlib-0.8.0 → statcatlib-0.9.1}/pyproject.toml.orig +13 -10
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/__init__.py +2 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/sources/ckan.py +6 -6
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/sources/sdmx.py +11 -15
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/store/__init__.py +1 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/store/lance.py +1 -1
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/store/store.py +108 -2
- {statcatlib-0.8.0 → statcatlib-0.9.1}/LICENSE +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/bundle.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/cli/__init__.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/expr.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/__init__.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/agents.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/catalog.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/classification.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/dataset.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/distribution.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/licence.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/quality.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/skos.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/spatial.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/temporal.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/model/types.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/py.typed +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/query.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/sources/__init__.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/store/row.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/store/schema.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/store/scopes.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/store/sentinel.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/store/xhtml.py +0 -0
- {statcatlib-0.8.0 → statcatlib-0.9.1}/src/statcatlib/urn.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: statcatlib
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.1
|
|
4
4
|
Summary: Queryable open data catalog engine for DCAT-AP, StatDCAT-AP, CKAN, and SDMX
|
|
5
5
|
Keywords: sdmx,dcat,open-data,catalog,duckdb,statistical-data,ckan
|
|
6
6
|
Author: gabrielgellner
|
|
@@ -11,19 +11,19 @@ Classifier: Development Status :: 3 - Alpha
|
|
|
11
11
|
Classifier: Intended Audience :: Developers
|
|
12
12
|
Classifier: Intended Audience :: Science/Research
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Programming Language :: Python :: 3.
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
15
|
Classifier: Topic :: Database
|
|
16
16
|
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
17
17
|
Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
18
18
|
Requires-Dist: attrs>=26.1.0
|
|
19
|
-
Requires-Dist: duckdb>=
|
|
19
|
+
Requires-Dist: duckdb>=2.0.0
|
|
20
20
|
Requires-Dist: httpx>=0.28
|
|
21
|
-
Requires-Dist: sdmxlib>=0.
|
|
21
|
+
Requires-Dist: sdmxlib>=0.70.1
|
|
22
22
|
Requires-Dist: lancedb>=0.39 ; extra == 'search'
|
|
23
23
|
Requires-Dist: sentence-transformers>=3.0 ; extra == 'search'
|
|
24
24
|
Requires-Dist: typer>=0.15 ; extra == 'search'
|
|
25
25
|
Requires-Dist: rich>=15.0 ; extra == 'search'
|
|
26
|
-
Requires-Python: >=3.
|
|
26
|
+
Requires-Python: >=3.14
|
|
27
27
|
Project-URL: Homepage, https://github.com/StatCat-Suite/statcatlib
|
|
28
28
|
Project-URL: Repository, https://github.com/StatCat-Suite/statcatlib
|
|
29
29
|
Project-URL: Documentation, https://statcat-suite.github.io/statcatlib
|
|
@@ -65,7 +65,7 @@ SQL JOINs are the graph traversal. No network round-trips at query time.
|
|
|
65
65
|
|
|
66
66
|
## Installation
|
|
67
67
|
|
|
68
|
-
Requires Python 3.
|
|
68
|
+
Requires Python 3.14+. Managed with [uv](https://docs.astral.sh/uv/).
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
71
|
uv add statcatlib
|
|
@@ -31,7 +31,7 @@ SQL JOINs are the graph traversal. No network round-trips at query time.
|
|
|
31
31
|
|
|
32
32
|
## Installation
|
|
33
33
|
|
|
34
|
-
Requires Python 3.
|
|
34
|
+
Requires Python 3.14+. Managed with [uv](https://docs.astral.sh/uv/).
|
|
35
35
|
|
|
36
36
|
```bash
|
|
37
37
|
uv add statcatlib
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "statcatlib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Queryable open data catalog engine for DCAT-AP, StatDCAT-AP, CKAN, and SDMX"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
license-files = ["LICENSE"]
|
|
7
7
|
readme = "README.md"
|
|
8
|
-
requires-python = ">=3.
|
|
8
|
+
requires-python = ">=3.14"
|
|
9
9
|
keywords = [
|
|
10
10
|
"sdmx",
|
|
11
11
|
"dcat",
|
|
@@ -20,16 +20,16 @@ classifiers = [
|
|
|
20
20
|
"Intended Audience :: Developers",
|
|
21
21
|
"Intended Audience :: Science/Research",
|
|
22
22
|
"Programming Language :: Python :: 3",
|
|
23
|
-
"Programming Language :: Python :: 3.
|
|
23
|
+
"Programming Language :: Python :: 3.14",
|
|
24
24
|
"Topic :: Database",
|
|
25
25
|
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
26
26
|
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
|
27
27
|
]
|
|
28
28
|
dependencies = [
|
|
29
29
|
"attrs>=26.1.0",
|
|
30
|
-
"duckdb>=
|
|
30
|
+
"duckdb>=2.0.0",
|
|
31
31
|
"httpx>=0.28",
|
|
32
|
-
"sdmxlib>=0.
|
|
32
|
+
"sdmxlib>=0.70.1",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[[project.authors]]
|
|
@@ -79,7 +79,7 @@ markers = [
|
|
|
79
79
|
]
|
|
80
80
|
|
|
81
81
|
[tool.basedpyright]
|
|
82
|
-
pythonVersion = "3.
|
|
82
|
+
pythonVersion = "3.14"
|
|
83
83
|
typeCheckingMode = "standard"
|
|
84
84
|
venvPath = "."
|
|
85
85
|
venv = ".venv"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "statcatlib"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.9.1"
|
|
4
4
|
description = "Queryable open data catalog engine for DCAT-AP, StatDCAT-AP, CKAN, and SDMX"
|
|
5
5
|
license = "Apache-2.0"
|
|
6
6
|
license-files = ["LICENSE"]
|
|
@@ -8,27 +8,30 @@ readme = "README.md"
|
|
|
8
8
|
authors = [
|
|
9
9
|
{ name = "gabrielgellner", email = "gabrielgellner@gmail.com" }
|
|
10
10
|
]
|
|
11
|
-
requires-python = ">=3.
|
|
11
|
+
requires-python = ">=3.14"
|
|
12
12
|
keywords = ["sdmx", "dcat", "open-data", "catalog", "duckdb", "statistical-data", "ckan"]
|
|
13
13
|
classifiers = [
|
|
14
14
|
"Development Status :: 3 - Alpha",
|
|
15
15
|
"Intended Audience :: Developers",
|
|
16
16
|
"Intended Audience :: Science/Research",
|
|
17
17
|
"Programming Language :: Python :: 3",
|
|
18
|
-
"Programming Language :: Python :: 3.
|
|
18
|
+
"Programming Language :: Python :: 3.14",
|
|
19
19
|
"Topic :: Database",
|
|
20
20
|
"Topic :: Scientific/Engineering :: Information Analysis",
|
|
21
21
|
"Topic :: Internet :: WWW/HTTP :: Indexing/Search",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
24
|
"attrs>=26.1.0",
|
|
25
|
-
#
|
|
26
|
-
#
|
|
27
|
-
#
|
|
28
|
-
|
|
29
|
-
"duckdb>=1.1,<2",
|
|
25
|
+
# >=2.0: sdmxlib 0.70 moved its store to DuckDB 2.0, and the catalog
|
|
26
|
+
# shares that store's connection. A catalog written here cannot be opened
|
|
27
|
+
# by DuckDB 1.x, so by statcatlib 0.8 or earlier.
|
|
28
|
+
"duckdb>=2.0.0",
|
|
30
29
|
"httpx>=0.28",
|
|
31
|
-
|
|
30
|
+
# >=0.70: needs Python 3.14 and DuckDB 2.0; `reg.data()` is gone, and a
|
|
31
|
+
# component without a concept is refused as it is built. >=0.70.1:
|
|
32
|
+
# `reg.obs()` asks Eurostat for CSV it serves, so `ingest_data()` can
|
|
33
|
+
# fetch from it (sdmxlib GH-50).
|
|
34
|
+
"sdmxlib>=0.70.1",
|
|
32
35
|
]
|
|
33
36
|
|
|
34
37
|
[project.optional-dependencies]
|
|
@@ -95,7 +98,7 @@ dev = [
|
|
|
95
98
|
]
|
|
96
99
|
|
|
97
100
|
[tool.basedpyright]
|
|
98
|
-
pythonVersion = "3.
|
|
101
|
+
pythonVersion = "3.14"
|
|
99
102
|
typeCheckingMode = "standard"
|
|
100
103
|
venvPath = "."
|
|
101
104
|
venv = ".venv"
|
|
@@ -83,6 +83,7 @@ from statcatlib.store import (
|
|
|
83
83
|
Hit,
|
|
84
84
|
SEARCH_CONTEXT_ANNOTATION_TYPE,
|
|
85
85
|
SEARCH_EXCLUDE_ANNOTATION_TYPE,
|
|
86
|
+
IncompatibleCatalogError,
|
|
86
87
|
IncompatibleIndexError,
|
|
87
88
|
QueryBuilder,
|
|
88
89
|
ReferenceMetadataBlend,
|
|
@@ -145,6 +146,7 @@ __all__ = [
|
|
|
145
146
|
"Frequency",
|
|
146
147
|
"GeospatialDataset",
|
|
147
148
|
"Hit",
|
|
149
|
+
"IncompatibleCatalogError",
|
|
148
150
|
"IncompatibleIndexError",
|
|
149
151
|
"InternationalString",
|
|
150
152
|
"LicenceDocument",
|
|
@@ -206,7 +206,7 @@ def _parse_istring(
|
|
|
206
206
|
if fr_val2 := parsed.get(fr_key):
|
|
207
207
|
kw2["fr"] = fr_val2
|
|
208
208
|
return InternationalString(**kw2)
|
|
209
|
-
except
|
|
209
|
+
except json.JSONDecodeError, ValueError:
|
|
210
210
|
pass
|
|
211
211
|
return InternationalString(en=raw or fallback_en)
|
|
212
212
|
return InternationalString(en=fallback_en)
|
|
@@ -231,7 +231,7 @@ def _parse_keywords(raw: object) -> list[InternationalString]:
|
|
|
231
231
|
if isinstance(raw, str):
|
|
232
232
|
try:
|
|
233
233
|
raw = json.loads(raw)
|
|
234
|
-
except
|
|
234
|
+
except json.JSONDecodeError, ValueError:
|
|
235
235
|
return []
|
|
236
236
|
if not isinstance(raw, dict):
|
|
237
237
|
return []
|
|
@@ -255,7 +255,7 @@ def _parse_frequency(raw: object) -> Frequency | None:
|
|
|
255
255
|
parsed = json.loads(raw)
|
|
256
256
|
if isinstance(parsed, dict):
|
|
257
257
|
raw = parsed
|
|
258
|
-
except
|
|
258
|
+
except json.JSONDecodeError, ValueError:
|
|
259
259
|
pass
|
|
260
260
|
if isinstance(raw, dict):
|
|
261
261
|
en_label = (raw.get("en") or "").strip()
|
|
@@ -285,7 +285,7 @@ def _parse_subjects(raw: object) -> list[ConceptRef]:
|
|
|
285
285
|
if isinstance(raw, str):
|
|
286
286
|
try:
|
|
287
287
|
raw = json.loads(raw)
|
|
288
|
-
except
|
|
288
|
+
except json.JSONDecodeError, ValueError:
|
|
289
289
|
raw = [raw]
|
|
290
290
|
if not isinstance(raw, list):
|
|
291
291
|
return []
|
|
@@ -302,7 +302,7 @@ def _parse_spatial(raw: object) -> list[SpatialCoverage]:
|
|
|
302
302
|
if isinstance(raw, str):
|
|
303
303
|
try:
|
|
304
304
|
raw = json.loads(raw)
|
|
305
|
-
except
|
|
305
|
+
except json.JSONDecodeError, ValueError:
|
|
306
306
|
raw = [raw]
|
|
307
307
|
if not isinstance(raw, list):
|
|
308
308
|
return []
|
|
@@ -427,7 +427,7 @@ def _ckan_package_to_dataset(raw: dict[str, object]) -> OpenDataset:
|
|
|
427
427
|
if isinstance(lang_raw, str):
|
|
428
428
|
try:
|
|
429
429
|
lang_raw = json.loads(lang_raw)
|
|
430
|
-
except
|
|
430
|
+
except json.JSONDecodeError, ValueError:
|
|
431
431
|
lang_raw = [lang_raw]
|
|
432
432
|
language: list[str] = [str(lang) for lang in _seq(lang_raw) if lang]
|
|
433
433
|
|
|
@@ -240,8 +240,8 @@ def ingest_data(
|
|
|
240
240
|
) -> str:
|
|
241
241
|
"""Materialise observation data for one dataflow into the bundle's lake.
|
|
242
242
|
|
|
243
|
-
Fetches the
|
|
244
|
-
|
|
243
|
+
Fetches the observations wide via ``reg.obs(...).pivot()``, writes them
|
|
244
|
+
into ``lake.<agency>.df_<id>_<version>`` through the bundle's
|
|
245
245
|
DuckLake-backed data store, and registers a
|
|
246
246
|
`sdmxlib.DataflowBinding` so the dataflow is queryable through
|
|
247
247
|
``cat.observations(...)``.
|
|
@@ -257,7 +257,7 @@ def ingest_data(
|
|
|
257
257
|
identifier: Catalog identifier in ``AGENCY:ID(VERSION)`` form.
|
|
258
258
|
``(latest)`` resolves to the highest stored version.
|
|
259
259
|
measure_dim: Dimension to pivot into measure columns. ``None``
|
|
260
|
-
(default)
|
|
260
|
+
(default) pivots on the DSD's ``MeasureDimension``.
|
|
261
261
|
|
|
262
262
|
Returns:
|
|
263
263
|
The fully-qualified table location (``<schema>.<table>``).
|
|
@@ -266,8 +266,9 @@ def ingest_data(
|
|
|
266
266
|
KeyError: No `AggregateDataset` is stored under *identifier* — run
|
|
267
267
|
``ingest_sdmx`` first.
|
|
268
268
|
ValueError: The dataset carries no ``sdmx_dataflow_urn``, so there is
|
|
269
|
-
nothing to fetch observations for;
|
|
270
|
-
``AGENCY:ID(VERSION)`` form
|
|
269
|
+
nothing to fetch observations for; *identifier* is not in
|
|
270
|
+
``AGENCY:ID(VERSION)`` form; or *measure_dim* is ``None`` and the
|
|
271
|
+
DSD declares no ``MeasureDimension`` to pivot on.
|
|
271
272
|
"""
|
|
272
273
|
identifier = _resolve_latest(store, identifier)
|
|
273
274
|
|
|
@@ -289,21 +290,16 @@ def ingest_data(
|
|
|
289
290
|
table = f"df_{flow_id}_{version.replace('.', '_')}".lower()
|
|
290
291
|
|
|
291
292
|
# Pull the wide dataframe and write through the bundle's lake. ``pivot()``
|
|
292
|
-
# returns
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
version=version,
|
|
298
|
-
measure_dim=measure_dim,
|
|
299
|
-
).pivot()
|
|
300
|
-
store.data.write_observations(schema=schema, table=table, source=lf)
|
|
293
|
+
# returns an eager DataFrame; with ``on=None`` it pivots on the DSD's
|
|
294
|
+
# MeasureDimension, as ``reg.data(...).pivot()`` did.
|
|
295
|
+
flow_urn = Dataflow.parse_urn(ds.sdmx_dataflow_urn)
|
|
296
|
+
wide = reg.obs(flow_urn).pivot(on=measure_dim)
|
|
297
|
+
store.data.write_observations(schema=schema, table=table, source=wide)
|
|
301
298
|
|
|
302
299
|
# Register a DataflowBinding so ``cat.observations(flow_urn)`` can find
|
|
303
300
|
# the table. Derive identity column maps from the DSD when resolvable.
|
|
304
301
|
import sdmxlib as sl # noqa: PLC0415
|
|
305
302
|
|
|
306
|
-
flow_urn = Dataflow.parse_urn(ds.sdmx_dataflow_urn)
|
|
307
303
|
flow = store.registry.get(flow_urn)
|
|
308
304
|
if isinstance(flow, Dataflow) and isinstance(flow.structure, sl.Ref):
|
|
309
305
|
dsd = store.registry.get(flow.structure.urn)
|
|
@@ -1043,7 +1043,7 @@ class LanceIndex:
|
|
|
1043
1043
|
"""
|
|
1044
1044
|
try:
|
|
1045
1045
|
data = json.loads((self._path / _MANIFEST_FILE).read_text())
|
|
1046
|
-
except
|
|
1046
|
+
except OSError, ValueError:
|
|
1047
1047
|
return None
|
|
1048
1048
|
return data if isinstance(data, dict) else None
|
|
1049
1049
|
|
|
@@ -27,6 +27,7 @@ import sdmxlib.model.datastructure
|
|
|
27
27
|
from attrs import Factory, define, field
|
|
28
28
|
from sdmxlib import InternationalString
|
|
29
29
|
from sdmxlib.model.registry import artefact_urn
|
|
30
|
+
from sdmxlib.storage.schema import SCHEMA_VERSION as _SDMX_SCHEMA_VERSION
|
|
30
31
|
|
|
31
32
|
from statcatlib.bundle import Embedded
|
|
32
33
|
from statcatlib.model.agents import Agent, ContactPoint
|
|
@@ -143,6 +144,60 @@ class IncompatibleIndexError(ValueError):
|
|
|
143
144
|
"""
|
|
144
145
|
|
|
145
146
|
|
|
147
|
+
class IncompatibleCatalogError(RuntimeError):
|
|
148
|
+
"""Raised on open when the catalog's ``sdmx`` schema is another sdmxlib's.
|
|
149
|
+
|
|
150
|
+
sdmxlib treats its store as a cache: on a schema-version mismatch,
|
|
151
|
+
`LocalRegistry` drops the ``sdmx`` schema and rebuilds it empty. Here
|
|
152
|
+
that schema is not a cache. It is the structural half of the catalog,
|
|
153
|
+
which ``statcat.dataset`` links into by URN, so the rebuild would leave
|
|
154
|
+
every dataset pointing at nothing, and structural queries answering
|
|
155
|
+
empty rather than failing.
|
|
156
|
+
"""
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _refuse_sdmx_schema_rebuild(conn: duckdb.DuckDBPyConnection) -> None:
|
|
160
|
+
"""Refuse to open a catalog whose SDMX structure sdmxlib would drop.
|
|
161
|
+
|
|
162
|
+
Runs before `LocalRegistry.from_connection`, which is where the drop
|
|
163
|
+
happens. A missing ``sdmx`` schema, or one at another version that holds
|
|
164
|
+
no artefacts, has nothing to lose, so sdmxlib is left to create or
|
|
165
|
+
rebuild it.
|
|
166
|
+
|
|
167
|
+
Read-only opens are refused here too. sdmxlib would refuse them itself,
|
|
168
|
+
but its message says to reopen read-write and rebuild in place, which
|
|
169
|
+
for a catalog is the data loss this guard exists to prevent.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
conn: The catalog's connection, before sdmxlib has touched it.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
IncompatibleCatalogError: The stored ``sdmx`` schema is at another
|
|
176
|
+
version and holds artefacts.
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
row = conn.execute("""
|
|
180
|
+
SELECT value FROM sdmx._meta WHERE key = 'schema_version'
|
|
181
|
+
""").fetchone()
|
|
182
|
+
stored = row[0] if row else None
|
|
183
|
+
artefacts = conn.execute("""
|
|
184
|
+
SELECT count(*) FROM sdmx.artefact
|
|
185
|
+
""").fetchone()
|
|
186
|
+
except duckdb.CatalogException:
|
|
187
|
+
return
|
|
188
|
+
n_artefacts = artefacts[0] if artefacts else 0
|
|
189
|
+
if stored == _SDMX_SCHEMA_VERSION or n_artefacts == 0:
|
|
190
|
+
return
|
|
191
|
+
msg = (
|
|
192
|
+
f"this catalog's SDMX structure is stored in sdmxlib schema {stored}, and this "
|
|
193
|
+
f"build of sdmxlib uses schema {_SDMX_SCHEMA_VERSION}. sdmxlib migrates by dropping "
|
|
194
|
+
f"the sdmx schema, which would delete the {n_artefacts} artefacts this catalog's "
|
|
195
|
+
f"datasets link to. Rebuild the catalog from its sources with this version, or "
|
|
196
|
+
f"open it with the statcatlib release that built it."
|
|
197
|
+
)
|
|
198
|
+
raise IncompatibleCatalogError(msg)
|
|
199
|
+
|
|
200
|
+
|
|
146
201
|
@define(frozen=True)
|
|
147
202
|
class EmbeddingMetadata:
|
|
148
203
|
"""Read-side view of a bundle's ``search/embeddings.json`` sidecar.
|
|
@@ -237,7 +292,7 @@ def _read_embeddings_meta(search_dir: Path) -> dict[str, object] | None:
|
|
|
237
292
|
return None
|
|
238
293
|
try:
|
|
239
294
|
data = json.loads(meta_path.read_text(encoding="utf-8"))
|
|
240
|
-
except
|
|
295
|
+
except OSError, json.JSONDecodeError:
|
|
241
296
|
return None
|
|
242
297
|
if not isinstance(data, dict):
|
|
243
298
|
return None
|
|
@@ -2677,6 +2732,16 @@ class Catalog:
|
|
|
2677
2732
|
|
|
2678
2733
|
with Catalog("my_catalog").open() as cat:
|
|
2679
2734
|
...
|
|
2735
|
+
|
|
2736
|
+
Raises `IncompatibleCatalogError` when the catalog's SDMX structure
|
|
2737
|
+
was stored by an sdmxlib with another store schema, which this one
|
|
2738
|
+
would drop on open. The connection is closed again first.
|
|
2739
|
+
|
|
2740
|
+
Returns:
|
|
2741
|
+
This catalog, open.
|
|
2742
|
+
|
|
2743
|
+
Raises:
|
|
2744
|
+
ValueError: ``embedding_model`` was given for an in-memory catalog.
|
|
2680
2745
|
"""
|
|
2681
2746
|
if self._conn is not None:
|
|
2682
2747
|
return self
|
|
@@ -2697,7 +2762,7 @@ class Catalog:
|
|
|
2697
2762
|
# schema and the sdmx schema co-exist in one DuckDB file, so cross-
|
|
2698
2763
|
# schema queries (e.g. join statcat.dataset to sdmx.codelist) work
|
|
2699
2764
|
# natively without ATTACH plumbing.
|
|
2700
|
-
self._registry =
|
|
2765
|
+
self._registry = self._open_registry(self._conn)
|
|
2701
2766
|
# ATTACH the DuckLake sidecar (folder mode only — in-memory has no
|
|
2702
2767
|
# lake). Sidecar layout because DuckDB rejects self-ATTACH; the
|
|
2703
2768
|
# ducklake catalog file sits next to meta.duckdb so the bundle is
|
|
@@ -2737,6 +2802,28 @@ class Catalog:
|
|
|
2737
2802
|
self._query_prefix = meta.query_prefix
|
|
2738
2803
|
return self
|
|
2739
2804
|
|
|
2805
|
+
def _open_registry(self, conn: duckdb.DuckDBPyConnection) -> sdmxlib.LocalRegistry:
|
|
2806
|
+
"""Compose the sdmxlib `LocalRegistry` on *conn*, once the guard allows it.
|
|
2807
|
+
|
|
2808
|
+
Args:
|
|
2809
|
+
conn: The connection `open` just made, which this catalog owns.
|
|
2810
|
+
|
|
2811
|
+
Returns:
|
|
2812
|
+
The registry sharing *conn*.
|
|
2813
|
+
|
|
2814
|
+
Raises:
|
|
2815
|
+
IncompatibleCatalogError: sdmxlib would drop this catalog's SDMX
|
|
2816
|
+
structure. *conn* is closed first, releasing the file so the
|
|
2817
|
+
release that built it can reopen it from this process.
|
|
2818
|
+
"""
|
|
2819
|
+
try:
|
|
2820
|
+
_refuse_sdmx_schema_rebuild(conn)
|
|
2821
|
+
except IncompatibleCatalogError:
|
|
2822
|
+
conn.close()
|
|
2823
|
+
self._conn = None
|
|
2824
|
+
raise
|
|
2825
|
+
return sdmxlib.LocalRegistry.from_connection(conn)
|
|
2826
|
+
|
|
2740
2827
|
def _open_connection(self) -> tuple[duckdb.DuckDBPyConnection, Path | None]:
|
|
2741
2828
|
"""Open the DuckDB connection and (for folder mode) prepare the bundle dir.
|
|
2742
2829
|
|
|
@@ -2825,6 +2912,24 @@ class Catalog:
|
|
|
2825
2912
|
|
|
2826
2913
|
``embedding_model`` is rejected here because shared-connection mode
|
|
2827
2914
|
has no on-disk ``data/`` directory to host the Lance index.
|
|
2915
|
+
|
|
2916
|
+
As `open` does, this raises `IncompatibleCatalogError` when the
|
|
2917
|
+
connection's SDMX structure was stored by an sdmxlib with another
|
|
2918
|
+
store schema, which this one would drop. *conn* is left open then:
|
|
2919
|
+
the caller owns it.
|
|
2920
|
+
|
|
2921
|
+
Args:
|
|
2922
|
+
conn: The connection to wrap. Left open by `close`.
|
|
2923
|
+
embedding_model: Must be ``None``; see above.
|
|
2924
|
+
document_prefix: Prefix for documents embedded into the index.
|
|
2925
|
+
query_prefix: Prefix for queries embedded against the index.
|
|
2926
|
+
read_only: Open the catalog schema without creating it.
|
|
2927
|
+
|
|
2928
|
+
Returns:
|
|
2929
|
+
A catalog over *conn*, already open.
|
|
2930
|
+
|
|
2931
|
+
Raises:
|
|
2932
|
+
ValueError: ``embedding_model`` was given.
|
|
2828
2933
|
"""
|
|
2829
2934
|
if embedding_model is not None:
|
|
2830
2935
|
msg = "from_connection() does not support embedding_model — Lance needs a folder-mode catalog"
|
|
@@ -2848,6 +2953,7 @@ class Catalog:
|
|
|
2848
2953
|
_ = _catalog_schema.open_schema_readonly(conn)
|
|
2849
2954
|
else:
|
|
2850
2955
|
_ = _catalog_schema.create_schema(conn)
|
|
2956
|
+
_refuse_sdmx_schema_rebuild(conn)
|
|
2851
2957
|
instance._registry = sdmxlib.LocalRegistry.from_connection(conn) # noqa: SLF001
|
|
2852
2958
|
# Shared-connection mode: the caller owns any ATTACH plumbing. The
|
|
2853
2959
|
# DataStore wraps the same connection without applying its own
|
|
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
|