statcatlib 0.12.0__tar.gz → 0.12.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.12.0 → statcatlib-0.12.1}/PKG-INFO +2 -2
- {statcatlib-0.12.0 → statcatlib-0.12.1}/pyproject.toml +2 -2
- {statcatlib-0.12.0 → statcatlib-0.12.1}/pyproject.toml.orig +3 -3
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/EGM.md +6 -4
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/catalog.py +17 -17
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/EGM.md +9 -7
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/inventory.py +10 -28
- {statcatlib-0.12.0 → statcatlib-0.12.1}/LICENSE +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/README.md +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/__init__.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/admin.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/bundle.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/checks.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/cli/EGM.md +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/cli/__init__.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/diagnostics.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/expr.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/EGM.md +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/__init__.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/agents.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/catalog.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/classification.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/dataset.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/distribution.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/licence.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/quality.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/skos.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/spatial.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/temporal.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/types.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/py.typed +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/query.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/sources/EGM.md +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/sources/__init__.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/sources/ckan.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/sources/sdmx.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/__init__.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/lance.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/row.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/schema.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/scopes.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/sentinel.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/store.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/working_copy.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/xhtml.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/testing.py +0 -0
- {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/urn.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: statcatlib
|
|
3
|
-
Version: 0.12.
|
|
3
|
+
Version: 0.12.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
|
|
@@ -18,7 +18,7 @@ Classifier: Topic :: Internet :: WWW/HTTP :: Indexing/Search
|
|
|
18
18
|
Requires-Dist: attrs>=26.1.0
|
|
19
19
|
Requires-Dist: duckdb>=2.0.0
|
|
20
20
|
Requires-Dist: httpx>=0.28
|
|
21
|
-
Requires-Dist: sdmxlib>=0.
|
|
21
|
+
Requires-Dist: sdmxlib>=0.82.0,<0.83
|
|
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'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "statcatlib"
|
|
3
|
-
version = "0.12.
|
|
3
|
+
version = "0.12.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"]
|
|
@@ -29,7 +29,7 @@ dependencies = [
|
|
|
29
29
|
"attrs>=26.1.0",
|
|
30
30
|
"duckdb>=2.0.0",
|
|
31
31
|
"httpx>=0.28",
|
|
32
|
-
"sdmxlib>=0.
|
|
32
|
+
"sdmxlib>=0.82.0,<0.83",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[[project.authors]]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "statcatlib"
|
|
3
|
-
version = "0.12.
|
|
3
|
+
version = "0.12.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"]
|
|
@@ -27,9 +27,9 @@ dependencies = [
|
|
|
27
27
|
# by DuckDB 1.x, so by statcatlib 0.8 or earlier.
|
|
28
28
|
"duckdb>=2.0.0",
|
|
29
29
|
"httpx>=0.28",
|
|
30
|
-
# <0.
|
|
30
|
+
# <0.83: every suite library is pre-1.0, so a minor bump may break
|
|
31
31
|
# (GH-67).
|
|
32
|
-
"sdmxlib>=0.
|
|
32
|
+
"sdmxlib>=0.82.0,<0.83",
|
|
33
33
|
]
|
|
34
34
|
|
|
35
35
|
[project.optional-dependencies]
|
|
@@ -275,9 +275,11 @@ read.
|
|
|
275
275
|
lands on: a rollback whose `if_live` is no longer live, a release that is
|
|
276
276
|
no longer a fast-forward, and a seal record computed before the embargo was
|
|
277
277
|
turned on are each refused or recomputed.
|
|
278
|
-
- `Product.view`
|
|
279
|
-
`
|
|
280
|
-
|
|
278
|
+
- `Product.view` passes the Inventory's `manifest_sha256` to
|
|
279
|
+
`sdmxlib.LocalRegistry.edition`, which hashes the bytes of
|
|
280
|
+
`manifest.json` it parses. A missing, malformed or different manifest
|
|
281
|
+
raises sdmxlib's `ManifestError`, which `view` re-raises as
|
|
282
|
+
`IncompatibleCatalogError` naming `statcat verify-catalog`, chained. Every view a `Catalog` returns shares one
|
|
281
283
|
`sdmxlib.EditionEngine`, and `Catalog.close` closes them all.
|
|
282
284
|
- A seal check only reads: `seal` runs every check against `wc.view()`
|
|
283
285
|
before writing anything, and any finding raises `SealCheckError`.
|
|
@@ -300,7 +302,7 @@ read.
|
|
|
300
302
|
at INFO, and writes and removes a `.statcat-link-probe.*` file and its hard
|
|
301
303
|
link.
|
|
302
304
|
- `Product.view` attaches the edition to the catalog's engine
|
|
303
|
-
(`LocalRegistry.edition`). `Product.working_copy` writes as
|
|
305
|
+
(`LocalRegistry.edition(path, engine=, manifest_sha256=)`). `Product.working_copy` writes as
|
|
304
306
|
`store/EGM.md` describes.
|
|
305
307
|
- `synthetic_catalog` writes a whole catalog: `catalog.json`, sdmxlib's
|
|
306
308
|
synthetic edition, the catalog rows, the search index when lancedb is
|
|
@@ -39,7 +39,7 @@ from sdmxlib.inventory import CATALOG_NAME, LiveEdition, LiveInventory, decode_r
|
|
|
39
39
|
from statcatlib.bundle import description_from_root
|
|
40
40
|
from statcatlib.store.inventory import Change, InventoryContentionError, InventoryRuleError, InventoryWriter
|
|
41
41
|
from statcatlib.store.store import VIEW_ATTRIBUTES, EditionView, IncompatibleCatalogError, _Store
|
|
42
|
-
from statcatlib.store.working_copy import Edition, SealCheckError, WorkingCopy
|
|
42
|
+
from statcatlib.store.working_copy import Edition, SealCheckError, WorkingCopy
|
|
43
43
|
|
|
44
44
|
if TYPE_CHECKING:
|
|
45
45
|
from collections.abc import Callable, Iterable, Mapping
|
|
@@ -618,10 +618,11 @@ class Product:
|
|
|
618
618
|
def view(self, *, embedding_model: EmbeddingModel | None = None, query_prefix: str = "") -> EditionView:
|
|
619
619
|
"""A read-only view of the product's live edition, as of the catalog's last `Catalog.refresh`.
|
|
620
620
|
|
|
621
|
-
It
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
The view stays on this edition after a refresh or a
|
|
621
|
+
It attaches the edition to the catalog's engine with
|
|
622
|
+
``LocalRegistry.edition``, which reads ``manifest.json`` once, checks
|
|
623
|
+
its SHA-256 against the one the Inventory records, and checks its
|
|
624
|
+
store schema. The view stays on this edition after a refresh or a
|
|
625
|
+
rollback.
|
|
625
626
|
|
|
626
627
|
Args:
|
|
627
628
|
embedding_model: The encoder the index was built with, for vector
|
|
@@ -633,27 +634,26 @@ class Product:
|
|
|
633
634
|
The view.
|
|
634
635
|
|
|
635
636
|
Raises:
|
|
636
|
-
IncompatibleCatalogError: The edition's ``manifest.json`` is
|
|
637
|
-
the one the Inventory recorded
|
|
637
|
+
IncompatibleCatalogError: The edition's ``manifest.json`` is
|
|
638
|
+
missing, unreadable, or not the one the Inventory recorded
|
|
639
|
+
(sdmxlib's `ManifestError`, chained). Its ``statcat`` schema at
|
|
638
640
|
another version, or a search index this build does not read,
|
|
639
641
|
raise it too, and a product with no live edition raises
|
|
640
642
|
`NoLiveEditionError`.
|
|
641
643
|
"""
|
|
642
644
|
live = self._head()
|
|
643
645
|
edition_dir = self._dir / live.edition_id
|
|
646
|
+
cat = self._catalog
|
|
644
647
|
try:
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
648
|
+
registry = sdmxlib.LocalRegistry.edition(
|
|
649
|
+
edition_dir, engine=cat._engine_for_views(), manifest_sha256=live.manifest_sha256
|
|
650
|
+
)
|
|
651
|
+
except ManifestError as exc:
|
|
649
652
|
msg = (
|
|
650
|
-
f"{self._product_id}'s live edition {live.edition_id}
|
|
651
|
-
f"
|
|
652
|
-
f"{self._catalog._root}"
|
|
653
|
+
f"{self._product_id}'s live edition {live.edition_id} does not hold the manifest.json the Inventory "
|
|
654
|
+
f"recorded ({exc}); run `statcat verify-catalog` on {cat._root}"
|
|
653
655
|
)
|
|
654
|
-
raise IncompatibleCatalogError(msg)
|
|
655
|
-
cat = self._catalog
|
|
656
|
-
registry = sdmxlib.LocalRegistry.edition(edition_dir, engine=cat._engine_for_views())
|
|
656
|
+
raise IncompatibleCatalogError(msg) from exc
|
|
657
657
|
opened = False
|
|
658
658
|
try:
|
|
659
659
|
store = _Store._over(
|
|
@@ -238,10 +238,11 @@ constants, `IncompatibleCatalogError`, `IncompatibleIndexError`, `Row` and
|
|
|
238
238
|
`ManifestError` from `sdmxlib.edition.seal` comes before the journal entry;
|
|
239
239
|
the working copy stays closed, and `working_copy(resume=True)` reopens it.
|
|
240
240
|
- `InventoryWriter` is the only code that writes `catalog.json`,
|
|
241
|
-
`inventory/` and `CURRENT`. It creates each file
|
|
242
|
-
file, fsync, `os.link` to the
|
|
243
|
-
directory), so a name never holds
|
|
244
|
-
|
|
241
|
+
`inventory/` and `CURRENT`. It creates each file with sdmxlib's
|
|
242
|
+
`sdmxlib.storage.create_exclusive` (staged file, fsync, `os.link` to the
|
|
243
|
+
name, unlink the staged file, fsync the directory), so a name never holds
|
|
244
|
+
half a file and of two writers exactly one creates a root: the loser's
|
|
245
|
+
`FileExistsError` becomes `InventoryConflictError`; a leaf that exists already counts as written, since
|
|
245
246
|
its name is the hash of its bytes. It writes leaves, then the root, then
|
|
246
247
|
`CURRENT`, and replaces `CURRENT` only with a higher `seq` in the same
|
|
247
248
|
`restore_epoch`. `commit` re-reads the head and calls *change* again after
|
|
@@ -276,9 +277,10 @@ constants, `IncompatibleCatalogError`, `IncompatibleIndexError`, `Row` and
|
|
|
276
277
|
unsealed edition directory.
|
|
277
278
|
- `settle_journal` can commit an Inventory version and deletes journal
|
|
278
279
|
entries.
|
|
279
|
-
- `InventoryWriter` writes `catalog.json`, leaves under `inventory/leaves
|
|
280
|
-
roots under `inventory
|
|
281
|
-
`.<name>.<pid>.<uuid>.tmp` file
|
|
280
|
+
- `InventoryWriter` writes `catalog.json`, leaves under `inventory/leaves/`
|
|
281
|
+
and roots under `inventory/` through `sdmxlib.storage.create_exclusive`,
|
|
282
|
+
and `CURRENT` through its own staged `.<name>.<pid>.<uuid>.tmp` file and
|
|
283
|
+
rename; each file and directory is fsynced.
|
|
282
284
|
- `discard_orphans()` drops LanceDB tables the manifest does not name.
|
|
283
285
|
- Emits `warnings.warn` for a quantised (stale) vector index and for changed search annotations, and logs a WARNING for an edition the journal rule cannot record.
|
|
284
286
|
|
|
@@ -5,9 +5,10 @@ transition rules (`sdmxlib.inventory`). statcatlib writes them, and this module
|
|
|
5
5
|
is the only code that writes ``catalog.json``, ``inventory/`` and ``CURRENT``.
|
|
6
6
|
It adds three things to sdmxlib's helpers:
|
|
7
7
|
|
|
8
|
-
- **exclusive create**:
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
- **exclusive create**: each document is written by
|
|
9
|
+
`sdmxlib.storage.create_exclusive`, which stages the file, fsyncs it,
|
|
10
|
+
hard-links it to its name and fsyncs the directory, so a name never holds
|
|
11
|
+
half a file, and two writers never both create one root;
|
|
11
12
|
- **the ``CURRENT`` advance**: ``CURRENT`` is replaced only by a higher
|
|
12
13
|
``seq``, so a slow writer never moves it back;
|
|
13
14
|
- **the retry loop**: `InventoryWriter.commit` re-reads the head and re-applies
|
|
@@ -53,12 +54,13 @@ from sdmxlib.inventory import (
|
|
|
53
54
|
sha256_hex,
|
|
54
55
|
validate_root,
|
|
55
56
|
)
|
|
57
|
+
from sdmxlib.storage import create_exclusive
|
|
56
58
|
|
|
57
59
|
if TYPE_CHECKING:
|
|
58
60
|
from collections.abc import Callable, Mapping, Sequence
|
|
59
61
|
|
|
60
|
-
from sdmxlib.inventory
|
|
61
|
-
from sdmxlib.storage
|
|
62
|
+
from sdmxlib.inventory import Rule
|
|
63
|
+
from sdmxlib.storage import JsonValue
|
|
62
64
|
|
|
63
65
|
__all__ = [
|
|
64
66
|
"MAX_RACES",
|
|
@@ -237,32 +239,12 @@ class InventoryWriter:
|
|
|
237
239
|
|
|
238
240
|
# -- writing ---------------------------------------------------------
|
|
239
241
|
|
|
240
|
-
def _create_exclusive(self, path: Path, data: bytes) -> None:
|
|
241
|
-
"""Create *path* holding *data*, complete before it is visible.
|
|
242
|
-
|
|
243
|
-
Five steps: write a staged file named ``.<name>.<pid>.<uuid>.tmp``,
|
|
244
|
-
fsync it, ``os.link`` it to *path*, unlink the staged file, and fsync
|
|
245
|
-
the directory. The link raises `FileExistsError` when *path* exists,
|
|
246
|
-
so of two writers exactly one creates it.
|
|
247
|
-
|
|
248
|
-
Args:
|
|
249
|
-
path: The file to create.
|
|
250
|
-
data: Its bytes.
|
|
251
|
-
"""
|
|
252
|
-
staged = _stage(path, data)
|
|
253
|
-
try:
|
|
254
|
-
os.link(staged, path)
|
|
255
|
-
finally:
|
|
256
|
-
with contextlib.suppress(FileNotFoundError):
|
|
257
|
-
staged.unlink()
|
|
258
|
-
_fsync_dir(path.parent)
|
|
259
|
-
|
|
260
242
|
def _write_leaf(self, path: Path, data: bytes) -> None:
|
|
261
243
|
"""Create a leaf. One already there is the same leaf: its name is the hash of its bytes."""
|
|
262
244
|
if path.exists():
|
|
263
245
|
return
|
|
264
246
|
with contextlib.suppress(FileExistsError):
|
|
265
|
-
|
|
247
|
+
create_exclusive(path, data)
|
|
266
248
|
|
|
267
249
|
def _advance_current(self, root: Root, data: bytes) -> None:
|
|
268
250
|
"""Point ``CURRENT`` at *root*, unless it already names a version at least as new.
|
|
@@ -296,7 +278,7 @@ class InventoryWriter:
|
|
|
296
278
|
document = CatalogDocument(
|
|
297
279
|
format_version=CATALOG_FORMAT, catalog_id=catalog_id or str(uuid.uuid7()), restore_epoch=0
|
|
298
280
|
)
|
|
299
|
-
|
|
281
|
+
create_exclusive(self._root / CATALOG_NAME, encode(document))
|
|
300
282
|
return document
|
|
301
283
|
|
|
302
284
|
def try_commit(
|
|
@@ -350,7 +332,7 @@ class InventoryWriter:
|
|
|
350
332
|
validate_root(root)
|
|
351
333
|
data = encode(root)
|
|
352
334
|
try:
|
|
353
|
-
|
|
335
|
+
create_exclusive(self._root / root_path(root.seq), data)
|
|
354
336
|
except FileExistsError as exc:
|
|
355
337
|
msg = f"another writer created Inventory version {root.seq} first"
|
|
356
338
|
raise InventoryConflictError(msg) from exc
|
|
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
|