statcatlib 0.13.0__tar.gz → 0.15.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.
- {statcatlib-0.13.0 → statcatlib-0.15.0}/PKG-INFO +9 -3
- {statcatlib-0.13.0 → statcatlib-0.15.0}/README.md +2 -1
- {statcatlib-0.13.0 → statcatlib-0.15.0}/pyproject.toml +11 -2
- {statcatlib-0.13.0 → statcatlib-0.15.0}/pyproject.toml.orig +7 -3
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/EGM.md +75 -12
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/__init__.py +9 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/admin.py +2 -2
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/bundle.py +41 -3
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/catalog.py +190 -45
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/cli/EGM.md +18 -5
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/cli/__init__.py +156 -12
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/diagnostics.py +27 -6
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/EGM.md +141 -17
- statcatlib-0.15.0/src/statcatlib/store/blob.py +850 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/cross_product.py +2 -15
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/inventory.py +333 -59
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/working_copy.py +71 -6
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/testing.py +302 -19
- statcatlib-0.15.0/src/statcatlib/upload.py +353 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/LICENSE +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/checks.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/expr.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/EGM.md +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/__init__.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/agents.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/catalog.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/classification.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/dataset.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/distribution.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/licence.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/quality.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/skos.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/spatial.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/temporal.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/model/types.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/py.typed +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/query.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/sources/EGM.md +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/sources/__init__.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/sources/ckan.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/sources/sdmx.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/__init__.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/lance.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/row.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/schema.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/scopes.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/sentinel.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/store.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/src/statcatlib/store/xhtml.py +0 -0
- {statcatlib-0.13.0 → statcatlib-0.15.0}/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.15.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.
|
|
21
|
+
Requires-Dist: sdmxlib>=0.86,<0.87
|
|
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.
|
|
3
|
+
version = "0.15.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.
|
|
32
|
+
"sdmxlib>=0.86,<0.87",
|
|
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.
|
|
3
|
+
version = "0.15.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.
|
|
30
|
+
# <0.87: every suite library is pre-1.0, so a minor bump may break
|
|
31
31
|
# (GH-67).
|
|
32
|
-
"sdmxlib>=0.
|
|
32
|
+
"sdmxlib>=0.86,<0.87",
|
|
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
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
`statcatlib` is the package root. `__init__.py` re-exports the public API of
|
|
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
|
-
`InventoryContentionError`, `InventoryRuleError`, `
|
|
9
|
-
`CrossProductRuleError` from `store/inventory.py
|
|
8
|
+
`InventoryContentionError`, `InventoryRuleError`, `InventoryWriteRefusedError`,
|
|
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`
|
|
@@ -94,8 +101,8 @@ and test catalogs (`testing.py`).
|
|
|
94
101
|
`Edition`, all recorded in one version) and `refused` (product id to the
|
|
95
102
|
error: `SealCheckError`, `NonFastForwardError` or another
|
|
96
103
|
`InventoryRuleError` (`CrossProductRuleError` from the pre-check or the
|
|
97
|
-
record included), `ManifestError`, `EditionCheckError`,
|
|
98
|
-
`InventoryContentionError`).
|
|
104
|
+
record included), `ManifestError`, `EditionCheckError`,
|
|
105
|
+
`InventoryContentionError`, or `InventoryWriteRefusedError`).
|
|
99
106
|
- `Edition` is re-exported from `store/working_copy.py`.
|
|
100
107
|
|
|
101
108
|
### `admin.py`
|
|
@@ -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
|
|
@@ -211,7 +245,9 @@ read.
|
|
|
211
245
|
- `verify_catalog(path) -> VerifyReport`: reads `FullInventory`; reports each
|
|
212
246
|
root the read protocol skipped (`full.skipped`), each leaf that fails its
|
|
213
247
|
hash or `validate_leaf`, and for every leaf entry a `manifest.json` whose
|
|
214
|
-
SHA-256 differs from the entry's,
|
|
248
|
+
SHA-256 differs from the entry's, or, when it matches, each filled routing
|
|
249
|
+
field or artefact record `sdmxlib.inventory.verify_entry` finds differing,
|
|
250
|
+
with the leaf as the finding's file, then runs
|
|
215
251
|
`check_edition(at="publish", structures=..., extensions={"statcat":
|
|
216
252
|
check_catalog})`, with `structures=` from `_structures_for`: every recorded
|
|
217
253
|
edition of every structure product for a data edition, the other
|
|
@@ -295,10 +331,29 @@ read.
|
|
|
295
331
|
dataflow per table and the rows from `(seed, product_id)`, fills with
|
|
296
332
|
`ingest_dataflows(..., partials=True)`, indexes `CL_GEO`'s codes with an
|
|
297
333
|
`fts_only` `CodelistIndexPolicy` when lancedb is installed, and seals.
|
|
334
|
+
`codr_product` is `codr_working_copy(...).seal()`: `codr_working_copy`
|
|
335
|
+
takes the same arguments and returns the filled, indexed working copy,
|
|
336
|
+
unsealed. `codr_corpus(catalog, groups=(CorpusGroup(7000),), *, seed=0,
|
|
337
|
+
shape=..., embargoed=0.1, on_sealed=None) -> list[str]` seals each
|
|
338
|
+
`CorpusGroup(products, tables=1)`'s products, named by
|
|
339
|
+
`corpus_product_id` (`codr-<tables>t-<index:05d>`), through
|
|
340
|
+
`codr_product`, skipping products the Inventory records and discarding a
|
|
341
|
+
working copy a crash left; it seals the share `corpus_embargoed` picks (a
|
|
342
|
+
SplitMix64 draw per product id) between two `CatalogAdmin.set_embargo`
|
|
343
|
+
versions, turns the embargo off first and leaves it off, and returns the
|
|
344
|
+
products it sealed. `bench/corpus.py` (GH-124) drives it.
|
|
298
345
|
`structure-classifications` holds `CL_CLASS_*`, `CL_UNIT` and the
|
|
299
346
|
SDMX-agency `CL_OBS_STATUS`, so every codelist a table cuts has a
|
|
300
347
|
structure home (A5). The draws are SplitMix64 seeded by SHA-256, so they
|
|
301
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.
|
|
302
357
|
|
|
303
358
|
## Invariants
|
|
304
359
|
|
|
@@ -323,8 +378,14 @@ read.
|
|
|
323
378
|
index and before it closes the store and registry; it then seals. The
|
|
324
379
|
manifest records each file's digests, so a later write fails
|
|
325
380
|
`check_edition`'s files check.
|
|
326
|
-
- `Catalog.open` writes nothing.
|
|
327
|
-
|
|
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.
|
|
328
389
|
- Only `CatalogAdmin` changes the Inventory, apart from a seal's record and
|
|
329
390
|
the journal repair. No attribute of `Catalog`, `Product` or `EditionView`
|
|
330
391
|
returns a `CatalogAdmin` or an embargoed, superseded or withdrawn edition.
|
|
@@ -336,7 +397,9 @@ read.
|
|
|
336
397
|
`sdmxlib.LocalRegistry.edition`, which hashes the bytes of
|
|
337
398
|
`manifest.json` it parses. A missing, malformed or different manifest
|
|
338
399
|
raises sdmxlib's `ManifestError`, which `view` re-raises as
|
|
339
|
-
`IncompatibleCatalogError
|
|
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
|
|
340
403
|
`sdmxlib.EditionEngine`, and `Catalog.close` closes them all.
|
|
341
404
|
- A seal check only reads: `seal` runs every check against `wc.view()`
|
|
342
405
|
before writing anything, and any finding raises `SealCheckError`.
|
|
@@ -88,11 +88,13 @@ 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,
|
|
94
95
|
InventoryContentionError,
|
|
95
96
|
InventoryRuleError,
|
|
97
|
+
InventoryWriteRefusedError,
|
|
96
98
|
NonFastForwardError,
|
|
97
99
|
)
|
|
98
100
|
from statcatlib.store.sentinel import NotLoadedError, is_unloaded
|
|
@@ -103,6 +105,7 @@ from statcatlib.store.working_copy import (
|
|
|
103
105
|
WorkingCopyClosedError,
|
|
104
106
|
WorkingCopyExistsError,
|
|
105
107
|
)
|
|
108
|
+
from statcatlib.upload import UploadCheckError, UploadReport, upload_edition
|
|
106
109
|
|
|
107
110
|
__all__ = [
|
|
108
111
|
"SEARCH_ANNOTATION_TYPES",
|
|
@@ -113,6 +116,7 @@ __all__ = [
|
|
|
113
116
|
"AggregateDataset",
|
|
114
117
|
"AliasedExpr",
|
|
115
118
|
"BaseDataset",
|
|
119
|
+
"BlobLocation",
|
|
116
120
|
"Catalog",
|
|
117
121
|
"CatalogAdmin",
|
|
118
122
|
"CatalogDescription",
|
|
@@ -159,6 +163,7 @@ __all__ = [
|
|
|
159
163
|
"InventoryConflictError",
|
|
160
164
|
"InventoryContentionError",
|
|
161
165
|
"InventoryRuleError",
|
|
166
|
+
"InventoryWriteRefusedError",
|
|
162
167
|
"JournalError",
|
|
163
168
|
"LicenceDocument",
|
|
164
169
|
"MicrodataDataset",
|
|
@@ -184,7 +189,10 @@ __all__ = [
|
|
|
184
189
|
"StatisticalDataset",
|
|
185
190
|
"TemporalCoverage",
|
|
186
191
|
"ThemesScope",
|
|
192
|
+
"TokenIdentity",
|
|
187
193
|
"TraversalExpr",
|
|
194
|
+
"UploadCheckError",
|
|
195
|
+
"UploadReport",
|
|
188
196
|
"Variable",
|
|
189
197
|
"WorkingCopy",
|
|
190
198
|
"WorkingCopyClosedError",
|
|
@@ -201,6 +209,7 @@ __all__ = [
|
|
|
201
209
|
"is_unloaded",
|
|
202
210
|
"prepare_sdmx",
|
|
203
211
|
"query",
|
|
212
|
+
"upload_edition",
|
|
204
213
|
"urn",
|
|
205
214
|
"xhtml_to_markdown",
|
|
206
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
|
|
366
|
-
edition
|
|
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
|
-
|
|
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 +=
|
|
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)
|