statcatlib 0.14.0__tar.gz → 0.16.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. {statcatlib-0.14.0 → statcatlib-0.16.0}/PKG-INFO +9 -3
  2. {statcatlib-0.14.0 → statcatlib-0.16.0}/README.md +2 -1
  3. {statcatlib-0.14.0 → statcatlib-0.16.0}/pyproject.toml +11 -2
  4. {statcatlib-0.14.0 → statcatlib-0.16.0}/pyproject.toml.orig +7 -3
  5. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/EGM.md +58 -8
  6. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/__init__.py +7 -0
  7. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/admin.py +2 -2
  8. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/bundle.py +41 -3
  9. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/catalog.py +176 -43
  10. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/cli/EGM.md +18 -5
  11. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/cli/__init__.py +147 -9
  12. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/EGM.md +113 -7
  13. statcatlib-0.16.0/src/statcatlib/store/blob.py +850 -0
  14. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/inventory.py +213 -64
  15. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/working_copy.py +1 -1
  16. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/testing.py +127 -0
  17. statcatlib-0.16.0/src/statcatlib/upload.py +353 -0
  18. {statcatlib-0.14.0 → statcatlib-0.16.0}/LICENSE +0 -0
  19. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/checks.py +0 -0
  20. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/diagnostics.py +0 -0
  21. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/expr.py +0 -0
  22. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/EGM.md +0 -0
  23. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/__init__.py +0 -0
  24. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/agents.py +0 -0
  25. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/catalog.py +0 -0
  26. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/classification.py +0 -0
  27. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/dataset.py +0 -0
  28. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/distribution.py +0 -0
  29. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/licence.py +0 -0
  30. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/quality.py +0 -0
  31. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/skos.py +0 -0
  32. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/spatial.py +0 -0
  33. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/temporal.py +0 -0
  34. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/model/types.py +0 -0
  35. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/py.typed +0 -0
  36. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/query.py +0 -0
  37. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/sources/EGM.md +0 -0
  38. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/sources/__init__.py +0 -0
  39. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/sources/ckan.py +0 -0
  40. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/sources/sdmx.py +0 -0
  41. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/__init__.py +0 -0
  42. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/cross_product.py +0 -0
  43. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/lance.py +0 -0
  44. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/row.py +0 -0
  45. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/schema.py +0 -0
  46. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/scopes.py +0 -0
  47. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/sentinel.py +0 -0
  48. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/store.py +0 -0
  49. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/store/xhtml.py +0 -0
  50. {statcatlib-0.14.0 → statcatlib-0.16.0}/src/statcatlib/urn.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: statcatlib
3
- Version: 0.14.0
3
+ Version: 0.16.0
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,11 @@ 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.85,<0.86
21
+ Requires-Dist: sdmxlib>=0.87,<0.88
22
+ Requires-Dist: obstore>=0.12,<0.13 ; extra == 'blob'
23
+ Requires-Dist: azure-identity>=1 ; extra == 'blob'
24
+ Requires-Dist: typer>=0.15 ; extra == 'blob'
25
+ Requires-Dist: rich>=15.0 ; extra == 'blob'
22
26
  Requires-Dist: lancedb>=0.39 ; extra == 'search'
23
27
  Requires-Dist: sentence-transformers>=3.0 ; extra == 'search'
24
28
  Requires-Dist: typer>=0.15 ; extra == 'search'
@@ -29,6 +33,7 @@ Project-URL: Repository, https://github.com/StatCat-Suite/statcatlib
29
33
  Project-URL: Documentation, https://statcat-suite.github.io/statcatlib
30
34
  Project-URL: Issue Tracker, https://github.com/StatCat-Suite/statcatlib/issues
31
35
  Project-URL: Changelog, https://github.com/StatCat-Suite/statcatlib/blob/main/CHANGELOG.md
36
+ Provides-Extra: blob
32
37
  Provides-Extra: search
33
38
  Description-Content-Type: text/markdown
34
39
 
@@ -462,9 +467,10 @@ just hooks-install
462
467
  ```
463
468
 
464
469
  ```bash
465
- just ci # full gate: index guard + lint + format + docstrings + prose + Any ratchet + pyrefly + unit and integration tests + strict docs build
470
+ just ci # full gate: index guard + lint + format + docstrings + prose + Any ratchet + pyrefly + unit and integration tests + stores tier + strict docs build
466
471
  just test # unit tests
467
472
  just test-integration # integration tests (no network)
473
+ just test-stores # the blob writer, the upload and remote catalogs against Azurite (needs a container runtime)
468
474
  just test-live # live tests against real SDMX endpoints
469
475
  just lint # ruff check
470
476
  just typecheck # pyrefly
@@ -428,9 +428,10 @@ just hooks-install
428
428
  ```
429
429
 
430
430
  ```bash
431
- just ci # full gate: index guard + lint + format + docstrings + prose + Any ratchet + pyrefly + unit and integration tests + strict docs build
431
+ just ci # full gate: index guard + lint + format + docstrings + prose + Any ratchet + pyrefly + unit and integration tests + stores tier + strict docs build
432
432
  just test # unit tests
433
433
  just test-integration # integration tests (no network)
434
+ just test-stores # the blob writer, the upload and remote catalogs against Azurite (needs a container runtime)
434
435
  just test-live # live tests against real SDMX endpoints
435
436
  just lint # ruff check
436
437
  just typecheck # pyrefly
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "statcatlib"
3
- version = "0.14.0"
3
+ version = "0.16.0"
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.85,<0.86",
32
+ "sdmxlib>=0.87,<0.88",
33
33
  ]
34
34
 
35
35
  [[project.authors]]
@@ -43,6 +43,12 @@ search = [
43
43
  "typer>=0.15",
44
44
  "rich>=15.0",
45
45
  ]
46
+ blob = [
47
+ "obstore>=0.12,<0.13",
48
+ "azure-identity>=1",
49
+ "typer>=0.15",
50
+ "rich>=15.0",
51
+ ]
46
52
 
47
53
  [project.scripts]
48
54
  statcat = "statcatlib.cli:main"
@@ -77,6 +83,7 @@ markers = [
77
83
  "unit: fast, in-process tests with no network I/O",
78
84
  "integration: tests that use real DuckDB and sdmxlib but no network",
79
85
  "live: tests that make real HTTP calls to external SDMX endpoints",
86
+ "stores: tests that run against an object-store emulator in a container",
80
87
  ]
81
88
 
82
89
  [tool.coverage.run]
@@ -95,9 +102,11 @@ exclude_lines = [
95
102
 
96
103
  [dependency-groups]
97
104
  dev = [
105
+ "azure-identity>=1",
98
106
  "lancedb>=0.39",
99
107
  "mkdocs-material>=9.7.6",
100
108
  "mkdocstrings[python]>=1.0.3",
109
+ "obstore>=0.12,<0.13",
101
110
  "polars>=1.39.3",
102
111
  "pydoclint>=0.11.1,<0.12",
103
112
  "pyrefly>=1.4.0,<1.5",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "statcatlib"
3
- version = "0.14.0"
3
+ version = "0.16.0"
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,13 +27,14 @@ 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.86: every suite library is pre-1.0, so a minor bump may break
30
+ # <0.88: every suite library is pre-1.0, so a minor bump may break
31
31
  # (GH-67).
32
- "sdmxlib>=0.85,<0.86",
32
+ "sdmxlib>=0.87,<0.88",
33
33
  ]
34
34
 
35
35
  [project.optional-dependencies]
36
36
  search = ["lancedb>=0.39", "sentence-transformers>=3.0", "typer>=0.15", "rich>=15.0"]
37
+ blob = ["obstore>=0.12,<0.13", "azure-identity>=1", "typer>=0.15", "rich>=15.0"]
37
38
 
38
39
  [project.scripts]
39
40
  statcat = "statcatlib.cli:main"
@@ -83,6 +84,7 @@ markers = [
83
84
  "unit: fast, in-process tests with no network I/O",
84
85
  "integration: tests that use real DuckDB and sdmxlib but no network",
85
86
  "live: tests that make real HTTP calls to external SDMX endpoints",
87
+ "stores: tests that run against an object-store emulator in a container",
86
88
  ]
87
89
 
88
90
  [tool.coverage.run]
@@ -104,9 +106,11 @@ exclude_lines = [
104
106
 
105
107
  [dependency-groups]
106
108
  dev = [
109
+ "azure-identity>=1",
107
110
  "lancedb>=0.39",
108
111
  "mkdocs-material>=9.7.6",
109
112
  "mkdocstrings[python]>=1.0.3",
113
+ "obstore>=0.12,<0.13",
110
114
  "polars>=1.39.3",
111
115
  # The checkers are capped, as sdmxlib's are: uv.lock is gitignored, so an
112
116
  # uncapped checker's next release would change what `just ci` reports
@@ -6,7 +6,8 @@
6
6
  `model`, `sources` and `store`; `CatalogAdmin` from `admin.py`; `Catalog`, `Product`, `Edition`,
7
7
  `SealOutcomes` and `NoLiveEditionError` from `catalog.py`; the errors `InventoryConflictError`,
8
8
  `InventoryContentionError`, `InventoryRuleError`, `InventoryWriteRefusedError`,
9
- `NonFastForwardError` and `CrossProductRuleError` from `store/inventory.py`, and `WorkingCopyExistsError`,
9
+ `NonFastForwardError` and `CrossProductRuleError` from `store/inventory.py`; `BlobLocation` and `TokenIdentity`
10
+ from `store/blob.py`; `upload_edition`, `UploadReport` and `UploadCheckError` from `upload.py`; `WorkingCopyExistsError`,
10
11
  `WorkingCopyClosedError`, `SealCheckError`, `EditionCheckError` and `JournalError` from
11
12
  `store/working_copy.py`; `NotLoadedError` and `is_unloaded` from the store's
12
13
  sentinel module; and part of `expr` (listed below). It exposes `checks`,
@@ -16,7 +17,7 @@ products (`catalog.py`), the operational changes to its Inventory
16
17
  (`admin.py`), seal checks (`checks.py`), the `statcat` section of
17
18
  an sdmxlib edition manifest (`bundle.py`), the integrity and support
18
19
  checks behind `statcat verify-catalog` and `statcat doctor`
19
- (`diagnostics.py`), composable query filters
20
+ (`diagnostics.py`), the copy of a sealed edition to object storage (`upload.py`), composable query filters
20
21
  (`query.py`), traversal expressions (`expr.py`), SDMX URN builders (`urn.py`)
21
22
  and test catalogs (`testing.py`).
22
23
 
@@ -28,8 +29,8 @@ and test catalogs (`testing.py`).
28
29
  agents, distributions, SKOS concepts); imports only `InternationalString`
29
30
  from sdmxlib.
30
31
  - `store/`: [`./store/EGM.md`](./store/EGM.md) — the private `_Store`,
31
- `EditionView`, `WorkingCopy` and the seal, the Inventory writer,
32
- `QueryBuilder`, scopes, DDL, LanceDB search index.
32
+ `EditionView`, `WorkingCopy` and the seal, the Inventory writer and its
33
+ local and blob storage, `QueryBuilder`, scopes, DDL, LanceDB search index.
33
34
  - `sources/`: [`./sources/EGM.md`](./sources/EGM.md) — SDMX and CKAN ingest
34
35
  connectors.
35
36
  - `cli/`: [`./cli/EGM.md`](./cli/EGM.md) — the `statcat` Typer command.
@@ -45,7 +46,13 @@ and test catalogs (`testing.py`).
45
46
  or with `exist_ok=True` reopens it for writing and writes nothing.
46
47
  - `Catalog.open(path)`: read-only. Raises `FileNotFoundError` when *path*
47
48
  has no `catalog.json`. A catalog with no Inventory version has no
48
- products.
49
+ products. A URI string raises `ValueError` naming `BlobLocation`.
50
+ - `Catalog.open(BlobLocation)`: the same over object storage, with a
51
+ `_Opening` whose `root` is `<cache_dir>/<catalog_id>`. No `cache_dir` raises
52
+ `ValueError`, and a `catalog_id` that cannot name a directory raises
53
+ `IncompatibleCatalogError`. `catalog_id` is read once; `refresh` and
54
+ `description` read through a `BlobInventoryStorage`, and `repr` and messages
55
+ show `location.uri`. `Catalog.create` raises `TypeError` for one.
49
56
  - `description`: the `CatalogDescription` in the root's
50
57
  `extensions.statcat` of the Inventory version the live view holds (as of
51
58
  the last `refresh()`), read with `bundle.description_from_root`; `None`
@@ -202,6 +209,33 @@ The module keeps its 0.11 name and describes editions. Imported from
202
209
  - `STATCAT_SECTION = "statcat"`, `STATCAT_SECTION_FORMAT = 3`,
203
210
  `PRODUCT_KINDS = ("data", "structure")`.
204
211
 
212
+ - `check_catalog_no_index(root, manifest, reg, profile) -> list[Finding]`:
213
+ `check_catalog` without the search index check, for an install with no
214
+ `lancedb`. `store/blob.py`'s `_statcat_check()` chooses between the two.
215
+
216
+ ### `upload.py`
217
+
218
+ `statcat upload`'s work, on the `blob` extra. It reads and writes nothing under
219
+ `catalog.json`, `inventory/` or `CURRENT`.
220
+
221
+ - `upload_edition(edition_dir, location, *, release_at=None) -> UploadReport`:
222
+ refuses a store other than `AzureStore` (`TypeError`, naming W3) and a store with
223
+ no `TokenIdentity` (`ValueError`), then runs `_upload` with an `AzureBlobRest`.
224
+ - `_upload(edition_dir, location, *, release_at=None, rest)`: the steps, over any
225
+ store and any digest client with `head(path) -> RemoteFile | None`. Tests and
226
+ `record_uploaded` call it. A naive `release_at` raises `ValueError`.
227
+ - `UploadReport(edition_id, product_id, sent, kept, registered)`: `sent` and `kept`
228
+ hold store paths, and `registered` is advisory, since a retried create can report
229
+ false against its own earlier success.
230
+ - `UploadCheckError(findings)` (`RuntimeError`): any finding, an oversize file, a
231
+ path leaving its directory, a rotted local file, a differing manifest, or a
232
+ differing `pending/` document.
233
+ - `_pending_document(edition_id, release_at) -> bytes`: the `pending/` document's
234
+ bytes, the contract `docs/catalog-format.md` §1.5 states.
235
+ - `_lookup(rest, manifest)`: adapts `rest.head` to sdmxlib's `Lookup`, asking once per
236
+ file. `_store_path(manifest, entry)` normalises `../data/`, which `obstore` refuses.
237
+ - `MAX_PUT_BYTES`: Put Blob's limit, 5,000 MiB.
238
+
205
239
  ### `diagnostics.py`
206
240
 
207
241
  Imported from `statcatlib.diagnostics`; nothing here is re-exported from
@@ -312,6 +346,14 @@ read.
312
346
  SDMX-agency `CL_OBS_STATUS`, so every codelist a table cuts has a
313
347
  structure home (A5). The draws are SplitMix64 seeded by SHA-256, so they
314
348
  do not depend on Python's `random`.
349
+ - `inventory_corpus(writer, products=7000, *, embargoed=0.1, artefacts=False,
350
+ batch=500, seed=0) -> Root` writes `catalog.json` and one `try_commit` per
351
+ `batch` products, with no manifest behind them, so no rule runs. The leaves
352
+ come from sdmxlib's `record`, with hashed ids, one dataflow URN, `pull_bytes`
353
+ 13,131,776 and, with `artefacts=True`, 10 artefact records. Commits start at
354
+ a fixed time and step one millisecond, so a seed writes the same bytes for a
355
+ given actor and statcatlib version. A catalog with a `catalog.json` raises
356
+ `FileExistsError`. `bench/inventory_read.py` (GH-98) drives it.
315
357
 
316
358
  ## Invariants
317
359
 
@@ -336,8 +378,14 @@ read.
336
378
  index and before it closes the store and registry; it then seals. The
337
379
  manifest records each file's digests, so a later write fails
338
380
  `check_edition`'s files check.
339
- - `Catalog.open` writes nothing. No `Catalog` or `Product` method takes or
340
- returns a path inside the catalog.
381
+ - `Catalog.open` writes nothing to the catalog. Over a `BlobLocation`, `Product.view`
382
+ writes to `cache_dir` alone: the live edition's files, by a rename of a verified,
383
+ fsynced staged directory, and the receive stamp. No embargoed edition's files and no
384
+ leaf bytes reach it. No `Catalog` or `Product` method takes or returns a path
385
+ inside the catalog.
386
+ - A library call never takes a credential from the environment: `Catalog.open` over a
387
+ store takes a `BlobLocation` the caller built. `catalog.py` and `store/blob.py` read
388
+ no environment variable.
341
389
  - Only `CatalogAdmin` changes the Inventory, apart from a seal's record and
342
390
  the journal repair. No attribute of `Catalog`, `Product` or `EditionView`
343
391
  returns a `CatalogAdmin` or an embargoed, superseded or withdrawn edition.
@@ -349,7 +397,9 @@ read.
349
397
  `sdmxlib.LocalRegistry.edition`, which hashes the bytes of
350
398
  `manifest.json` it parses. A missing, malformed or different manifest
351
399
  raises sdmxlib's `ManifestError`, which `view` re-raises as
352
- `IncompatibleCatalogError` naming `statcat verify-catalog`, chained. Every view a `Catalog` returns shares one
400
+ `IncompatibleCatalogError`, chained. A local catalog's message names
401
+ `statcat verify-catalog`, and a blob catalog's names the cached directory to
402
+ delete, so the edition is pulled again. Every view a `Catalog` returns shares one
353
403
  `sdmxlib.EditionEngine`, and `Catalog.close` closes them all.
354
404
  - A seal check only reads: `seal` runs every check against `wc.view()`
355
405
  before writing anything, and any finding raises `SealCheckError`.
@@ -88,6 +88,7 @@ from statcatlib.store import (
88
88
  WorkingCopy,
89
89
  xhtml_to_markdown,
90
90
  )
91
+ from statcatlib.store.blob import BlobLocation, TokenIdentity
91
92
  from statcatlib.store.inventory import (
92
93
  CrossProductRuleError,
93
94
  InventoryConflictError,
@@ -104,6 +105,7 @@ from statcatlib.store.working_copy import (
104
105
  WorkingCopyClosedError,
105
106
  WorkingCopyExistsError,
106
107
  )
108
+ from statcatlib.upload import UploadCheckError, UploadReport, upload_edition
107
109
 
108
110
  __all__ = [
109
111
  "SEARCH_ANNOTATION_TYPES",
@@ -114,6 +116,7 @@ __all__ = [
114
116
  "AggregateDataset",
115
117
  "AliasedExpr",
116
118
  "BaseDataset",
119
+ "BlobLocation",
117
120
  "Catalog",
118
121
  "CatalogAdmin",
119
122
  "CatalogDescription",
@@ -186,7 +189,10 @@ __all__ = [
186
189
  "StatisticalDataset",
187
190
  "TemporalCoverage",
188
191
  "ThemesScope",
192
+ "TokenIdentity",
189
193
  "TraversalExpr",
194
+ "UploadCheckError",
195
+ "UploadReport",
190
196
  "Variable",
191
197
  "WorkingCopy",
192
198
  "WorkingCopyClosedError",
@@ -203,6 +209,7 @@ __all__ = [
203
209
  "is_unloaded",
204
210
  "prepare_sdmx",
205
211
  "query",
212
+ "upload_edition",
206
213
  "urn",
207
214
  "xhtml_to_markdown",
208
215
  ]
@@ -362,8 +362,8 @@ class CatalogAdmin:
362
362
  (``rule="structure-home"``).
363
363
 
364
364
  An empty *reason* raises `ValueError`. It acts on the live edition at
365
- this sdmxlib's store schema; rolling back another schema's live
366
- edition arrives in Phase 4.
365
+ this sdmxlib's store schema, and does not roll back another schema's
366
+ live edition.
367
367
  """
368
368
  _require_reason(reason)
369
369
  if if_live is None:
@@ -51,6 +51,7 @@ __all__ = [
51
51
  "STATCAT_SECTION_FORMAT",
52
52
  "catalog_section",
53
53
  "check_catalog",
54
+ "check_catalog_no_index",
54
55
  "description_from_json",
55
56
  "description_from_root",
56
57
  "description_to_json",
@@ -234,10 +235,19 @@ def _check_search(root: Path, declared: object) -> Iterator[Finding]:
234
235
  detail = f"{found}, which this build does not read: check it with that release, or rebuild with build_index()"
235
236
  yield Finding(STATCAT_SECTION, f"{_SEARCH_DIRNAME}/{_INDEX_MANIFEST}", detail)
236
237
  return
237
- index = LanceIndex(search_dir)
238
+ try:
239
+ index = LanceIndex(search_dir)
240
+ except ImportError:
241
+ yield Finding(STATCAT_SECTION, _SEARCH_DIRNAME, "cannot check the search index without statcatlib[search]")
242
+ return
238
243
  if (declared_version := declared.get("index_version")) != index.version:
239
244
  detail = f"the manifest declares index_version {declared_version}, {_INDEX_MANIFEST} records {index.version}"
240
245
  yield Finding(STATCAT_SECTION, MANIFEST_NAME, detail)
246
+ yield from _check_layout(index, languages)
247
+
248
+
249
+ def _check_layout(index: LanceIndex, languages: Mapping[str, object]) -> Iterator[Finding]:
250
+ """The index's tables: each declared language as declared, and none ``index.json`` does not name."""
241
251
  layout = index.describe()
242
252
  for lang in sorted(set(languages) | set(layout)):
243
253
  yield from _check_language(index, lang, layout.get(lang), languages.get(lang))
@@ -297,10 +307,36 @@ def check_catalog(root: Path, manifest: Manifest, reg: EditionRegistry, profile:
297
307
  Returns:
298
308
  Everything found wrong with the catalog's part of the edition.
299
309
  """
310
+ del profile
311
+ return _section_findings(root, manifest, reg, index=True)
312
+
313
+
314
+ def check_catalog_no_index(root: Path, manifest: Manifest, reg: EditionRegistry, profile: Profile) -> list[Finding]:
315
+ """`check_catalog` without its search index check, for an install with no `lancedb`.
316
+
317
+ A ``statcatlib[blob]`` install holds no `lancedb`, so it cannot open the
318
+ index. The ``files`` check of `sdmxlib.edition.check_edition` still covers the
319
+ index's bytes by digest.
320
+
321
+ Args:
322
+ root: The edition directory.
323
+ manifest: Its manifest.
324
+ reg: sdmxlib's registry over the edition.
325
+ profile: The `check_edition` profile that runs it. Every profile runs
326
+ the same checks.
327
+
328
+ Returns:
329
+ Everything found wrong with the rest of the catalog's part of the edition.
330
+ """
331
+ del profile
332
+ return _section_findings(root, manifest, reg, index=False)
333
+
334
+
335
+ def _section_findings(root: Path, manifest: Manifest, reg: EditionRegistry, *, index: bool) -> list[Finding]:
336
+ """The ``statcat`` checks, with the opening of the search index when *index* is true."""
300
337
  from statcatlib.store.schema import first_row # noqa: PLC0415
301
338
  from statcatlib.store.store import annotation_fingerprint, statcat_schema_mismatch # noqa: PLC0415
302
339
 
303
- del profile
304
340
  conn = reg.connection
305
341
  rows = conn.execute(
306
342
  """
@@ -332,7 +368,9 @@ def check_catalog(root: Path, manifest: Manifest, reg: EditionRegistry, profile:
332
368
  for binding in manifest.bindings
333
369
  ]
334
370
  findings += structure_content(conn)
335
- findings += [*_check_catalog_rows(conn, section), *_check_search(root, section.get("search"))]
371
+ findings += _check_catalog_rows(conn, section)
372
+ if index:
373
+ findings += _check_search(root, section.get("search"))
336
374
  search = section.get("search")
337
375
  if isinstance(search, Mapping):
338
376
  findings += _check_index_annotations(conn, search.get("annotations"), annotation_fingerprint)