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.
Files changed (47) hide show
  1. {statcatlib-0.12.0 → statcatlib-0.12.1}/PKG-INFO +2 -2
  2. {statcatlib-0.12.0 → statcatlib-0.12.1}/pyproject.toml +2 -2
  3. {statcatlib-0.12.0 → statcatlib-0.12.1}/pyproject.toml.orig +3 -3
  4. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/EGM.md +6 -4
  5. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/catalog.py +17 -17
  6. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/EGM.md +9 -7
  7. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/inventory.py +10 -28
  8. {statcatlib-0.12.0 → statcatlib-0.12.1}/LICENSE +0 -0
  9. {statcatlib-0.12.0 → statcatlib-0.12.1}/README.md +0 -0
  10. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/__init__.py +0 -0
  11. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/admin.py +0 -0
  12. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/bundle.py +0 -0
  13. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/checks.py +0 -0
  14. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/cli/EGM.md +0 -0
  15. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/cli/__init__.py +0 -0
  16. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/diagnostics.py +0 -0
  17. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/expr.py +0 -0
  18. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/EGM.md +0 -0
  19. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/__init__.py +0 -0
  20. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/agents.py +0 -0
  21. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/catalog.py +0 -0
  22. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/classification.py +0 -0
  23. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/dataset.py +0 -0
  24. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/distribution.py +0 -0
  25. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/licence.py +0 -0
  26. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/quality.py +0 -0
  27. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/skos.py +0 -0
  28. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/spatial.py +0 -0
  29. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/temporal.py +0 -0
  30. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/model/types.py +0 -0
  31. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/py.typed +0 -0
  32. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/query.py +0 -0
  33. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/sources/EGM.md +0 -0
  34. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/sources/__init__.py +0 -0
  35. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/sources/ckan.py +0 -0
  36. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/sources/sdmx.py +0 -0
  37. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/__init__.py +0 -0
  38. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/lance.py +0 -0
  39. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/row.py +0 -0
  40. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/schema.py +0 -0
  41. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/scopes.py +0 -0
  42. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/sentinel.py +0 -0
  43. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/store.py +0 -0
  44. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/working_copy.py +0 -0
  45. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/store/xhtml.py +0 -0
  46. {statcatlib-0.12.0 → statcatlib-0.12.1}/src/statcatlib/testing.py +0 -0
  47. {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.0
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.81.0,<0.82
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.0"
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.81.0,<0.82",
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.0"
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.82: every suite library is pre-1.0, so a minor bump may break
30
+ # <0.83: every suite library is pre-1.0, so a minor bump may break
31
31
  # (GH-67).
32
- "sdmxlib>=0.81.0,<0.82",
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` hashes the live edition's `manifest.json` and raises
279
- `IncompatibleCatalogError` when it differs from the `manifest_sha256` the
280
- Inventory records. Every view a `Catalog` returns shares one
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, manifest_sha256
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 checks the edition's ``manifest.json`` against the SHA-256 the
622
- Inventory records, then attaches the edition to the catalog's engine
623
- (``LocalRegistry.edition``), which checks the manifest's store schema.
624
- The view stays on this edition after a refresh or a rollback.
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 not
637
- the one the Inventory recorded. Its ``statcat`` schema at
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
- found = manifest_sha256(edition_dir)
646
- except FileNotFoundError:
647
- found = "missing"
648
- if found != live.manifest_sha256:
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} has a manifest.json ({found}) that is not "
651
- f"the one the Inventory recorded ({live.manifest_sha256}); run `statcat verify-catalog` on "
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 in five steps (staged
242
- file, fsync, `os.link` to the name, unlink the staged file, fsync the
243
- directory), so a name never holds half a file and of two writers exactly
244
- one creates a root; a leaf that exists already counts as written, since
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/`, and `CURRENT`, each through a staged
281
- `.<name>.<pid>.<uuid>.tmp` file, and fsyncs files and directories.
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**: a file is staged, fsynced, hard-linked to its name and
9
- the directory fsynced, so a name never holds half a file, and two writers
10
- never both create one root;
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._rules import Rule
61
- from sdmxlib.storage._manifest import JsonValue
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
- self._create_exclusive(path, data)
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
- self._create_exclusive(self._root / CATALOG_NAME, encode(document))
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
- self._create_exclusive(self._root / root_path(root.seq), data)
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