Upgrading¶
This page lists each breaking change, newest first, with what replaces it. statcatlib is pre-1.0, so a minor version may break; the CHANGELOG lists every change.
0.14.0: Inventory history fields, on sdmxlib 0.85¶
statcatlib requires sdmxlib>=0.85,<0.86. sdmxlib's
upgrading guide lists
its own changes. No catalog is rebuilt: a 0.13 catalog opens and seals under
0.14, and its next version carries the history fields. The Inventory format
becomes 1.2, which every reader of an Inventory 1.x reads. The store schema, the
manifest, the statcat schema, the statcat section format and the search
index manifest stay as they were.
InventoryWriter writes previous_root_sha256 and changes in every root,
computed from root seq - 1 and its leaves. Nothing expires yet.
Two commits that used to land are now refused with the new
InventoryWriteRefusedError, a subclass of sdmxlib.inventory.InventoryError
exported from statcatlib:
- A base version whose
created_atis more than 5 minutes ahead of this machine's clock. The message names the base version's time and this machine's. Fix the clock that is wrong. A clock behind by less than that writes the base version's time plus 1 ms. - A
catalog.jsonat format 2.x, which marks a catalog opted in to expiry. A later statcatlib writes to it. Readers,Catalog.open,CatalogAdmin.openandstatcat verify-catalogstill read it.
WorkingCopy.seal raises it before it writes anything, and leaves the working
copy open. Raised at the record, it keeps the journal entry for a later
working_copy(), as InventoryContentionError does. Catalog.seal reports it
in SealOutcomes.refused, and statcat prune prints it and exits 1. Code that
catches InventoryRuleError around a commit also needs to catch it.
InventoryWriter.try_commit takes built_from= and now=, and Change takes
built_from, for a correction built from an older version.
InventoryWriter.check_writable(base) runs the two refusals alone.
Routing fields¶
Each edition a seal or the journal repair records carries dataflows and
pull_bytes, from sdmxlib.inventory.routing_fields, and a structure edition
also carries artefacts. An entry an earlier statcatlib recorded keeps none
of them, and readers fall back to its manifest. A release, a rollback and a
reinstate keep what the entry holds.
statcat verify-catalog reports a filled field that differs from the
manifest as FAIL inventory/leaves/<sha256>.json: ..., and skips an entry
without the fields.
statcatlib.store.cross_product.manifest_records is gone. Import
sdmxlib.inventory.manifest_records, which is the same function.
0.13.0: structure products, on sdmxlib 0.84¶
statcatlib requires sdmxlib>=0.84,<0.85. sdmxlib's
upgrading guide lists
its own changes. No catalog is rebuilt: the store schema stays 2.52, the
manifest 3.0, the Inventory 1.0, the statcat schema 2.2, the statcat
section format 3 and the search index manifest 3. A 0.12 catalog opens and
seals under 0.13.
Structure products¶
A product is now a data product or a structure product (Structure products):
Product.working_copy(kind=...)takes"data"or"structure". The default,None, takes the kind the product is recorded with, or"data"for a new product, so existing calls are unchanged.WorkingCopy.kindreturns it.WorkingCopy.fill_structures(source, *, families, agency, ids, artefacts)copies full artefacts into a structure product's working copy, throughsdmxlib.edition.fill_structures.- A product reopened with the other kind raises
InventoryRuleErrorwithrule="kind"before anything is built, as one reopened with another lifecycle does withrule="lifecycle". - A structure working copy holding a dataflow, a binding or a partial fails
its seal with
SealCheckError, andcheck_catalogreports each one in a sealed structure edition. - A structure edition's seal runs
sdmxlib.edition.check_editionatpublishagainst the catalog's other structure products before the Inventory commit. A finding raises the newEditionCheckError(exported fromstatcatlib) and leaves the sealed edition unrecorded. InCatalog.sealthe product goes toSealOutcomes.refused. The journal repair runs the same check, and clears a failing edition's entry with a WARNING. Catalog.sealseals and checks the batch's structure products before its data products, and drops a structure edition whose publish check was given a batch edition that was not recorded.statcatlib.bundle.catalog_section(store, *, kind="data")takes the kind it writes.
Partials in data products¶
WorkingCopy.fill_dataflows(source, dataflows, *, observations, partials)fills a data product's working copy throughsdmxlib.edition.fill_edition, cutting each codelist to a partial, and keeps the map of cuts instatcat._meta.partial_of(Partials in data products). The seal passes it to sdmxlib aspartial_of, also afterworking_copy(resume=True)in another process.ingest_dataflows(wc, source, dataflows, *, observations, partials, publisher, themes), exported fromstatcatlib, fills throughfill_dataflowsand stores a dataset per dataflow.ingest_sdmxstill copies an in-memory registry whole.- The seal of an edition whose partials record a
partial_ofruns the publish check with every structure product's live and embargoed editions, and raisesEditionCheckErrorfor a partial that matches none of them or holds a code its full codelist lacks, and for a partial in a catalog with no structure edition. A data edition with no partial seals as in 0.12. - The seal refuses, with
SealCheckError, a working copy whosefill_dataflowsstopped before it recorded its cuts, and one holding a partial cut outsidefill_dataflows(a directfill_editiononwc.registry) that would carry the previous edition'spartial_of. statcatlib.testinggains the CODR generator:CodrShape,STRUCTURE_PRODUCTS,CODR_AGENCY,codr_structuresandcodr_product.
The cross-product rules¶
The Inventory writer now refuses a version that breaks specification ยง6.5
item 4, 7 or 8 for an edition entering live or embargoed, with the new
CrossProductRuleError (exported from statcatlib, a subclass of
InventoryRuleError). It names the rule, the item, the URN and the editions
compared, and nothing is written
(The cross-product rules):
rule="full-artefacts"(item 4): a data edition's full artefact that a structure product holds differs from every live or embargoed edition of that structure home; a partial'spartial_ofmatches none; a cut codelist has no structure home; or a full artefact no structure product holds, such as a DSD two data products share, differs from another data product's live copy when it enterslive.rule="structure-home"(item 7): a structure edition enteringliveholds a full artefact another structure product holds live.rule="publish-order"(item 8): a data edition enteringliveholds a partial or a full artefact whose fingerprint only an embargoed structure edition holds; release that structure edition first.
The rules run in every seal, Catalog.seal, CatalogAdmin.release and
CatalogAdmin.rollback. The seal checks them before sdmxlib's seal, so a
refusal there leaves the working copy open; a refusal at the record, after
another writer changed the head, leaves the sealed edition unrecorded.
Catalog.seal checks the batch together, so data products that change a
shared full artefact with no structure home pass when they seal in one
batch. CatalogAdmin.release releases one product per call, so under
embargo such a change cannot go live until the embargo is off and the
products are sealed in one batch.
What changes for existing code:
- A data product filled with
fill_dataflowsoringest_dataflowsneeds a structure product holding each codelist it cuts, orpartials=False. - Two data products holding one URN with different content (two copies of a
DSD or a codelist, from
ingest_sdmxincluded) can no longer both be live: the second seal raisesCrossProductRuleError. InventoryWriter(root, *, artefacts=None)reads each live and embargoed edition'smanifest.jsonfor the rules, and raisessdmxlib.edition.ManifestErrorfor one it cannot read or whose SHA-256 differs from the Inventory's. Code that commits leaves naming editions with no manifest on disk passes anartefactsreader.InventoryRuleError.rulealso takes"full-artefacts","structure-home"and"publish-order".
A structure edit is accepted while data editions hold the old copy. Those
data editions are stale: a stale live edition keeps serving until its
product is republished, and a stale embargoed edition is refused at release.
statcat verify-catalog prints each as a STALE line
(VerifyReport.stale, of statcatlib.diagnostics.StaleEdition) without
counting it as a finding, and statcat doctor reports it under
stale data editions. statcat doctor also warns, under partials with no
structure home, about each data edition holding a partial whose URN no
structure edition holds full any more; item 4 refuses its release.
verify-catalog also checks each data edition's
partials against the structure editions they were cut from, and each
structure edition's references in the other structure products' live and
embargoed editions. CatalogAdmin.prune keeps a structure edition that a
live or embargoed data edition's partial_of cites.
The journal repair is catalog-wide: a product's next working_copy()
records every product's sealed, unrecorded editions in one version,
leaving out the products of a running Catalog.seal and a product whose
first edition has no leaf yet.
Write a catalog that holds a structure product with statcatlib 0.13 only. A
0.12 writer records every edition as kind: "data", so sdmxlib refuses its
record of a structure product's edition after the seal, and the edition stays
unrecorded; it also applies none of the cross-product rules.
0.12.0: catalogs of products and editions, on sdmxlib 0.81¶
statcatlib requires sdmxlib>=0.81.0,<0.82. sdmxlib's
upgrading guide lists
its own breaking changes from 0.73 to 0.81; the ones below are those a
statcatlib user sees.
Serve a 0.12 catalog with sdmxlib-rest 0.9.0 or later, which reads editions
and requires sdmxlib 0.81. Earlier sdmxlib-rest releases cannot read it.
sdmxlib 0.81 adds the Inventory event kind describe, which
CatalogAdmin.describe writes; sdmxlib 0.80 and older readers skip a
describe version and stay on the version before it, so upgrade every other
reader of a catalog to sdmxlib 0.81 before you describe it.
A catalog is now a directory of products, each a chain of immutable editions, with an Inventory that records which edition of each product is live (Catalog format). statcatlib 0.12 refuses every 0.11 catalog and 0.72 bundle, and there is no migrator: rebuild each catalog by re-ingesting it.
Rebuild every catalog by re-ingesting¶
sdmxlib 0.80 stores SDMX structure at store schema 2.52, and statcatlib 0.11
wrote 2.48; the layout changed too. Catalog.open and Catalog.create
refuse a directory holding a 0.11 catalog (a root-level meta.duckdb), a
0.72 bundle (bundle.manifest.json), or other files, with
IncompatibleCatalogError, and change nothing in it. To rebuild:
- Create a catalog in a new, empty directory:
cat = Catalog.create("~/stats"). - For each product, start a working copy and ingest into it as before:
wc = cat.product("14100287").working_copy(), theningest_sdmx(wc, registry),ingest_data(wc, rest_registry, identifier),ingest_ckan(wc, base_url)orwc.put(...). - Optionally call
wc.build_index(...)with the options your index needs; the seal rebuilds the index with the same arguments. wc.seal(). The edition is recorded live.- Read it with
Catalog.open("~/stats").product("14100287").view().
The catalog API¶
| 0.11 | 0.12 |
|---|---|
Catalog(path), Catalog(path).open() |
Catalog.create(path) creates a catalog, Catalog.create(path, exist_ok=True) reopens one for writing, Catalog.open(path) opens one read-only. Catalog(...) raises TypeError naming these. |
Catalog(), Catalog(":memory:") |
statcatlib.testing.temporary_catalog(), a catalog in a temporary directory |
Catalog(path, read_only=True) |
Catalog.open(path); every view is read-only |
Catalog.from_connection(conn) |
none; a catalog owns its storage |
cat.put, put_concept, add_lineage, add_language_alternates, bulk_load, transaction, build_index |
the same methods on cat.product(id).working_copy() |
cat.registry for writes (ingest, add), cat.data.write_observations |
wc.registry, the working copy's writable LocalRegistry; wc.registry.write_observations |
cat.get, query, search, facets, the scopes, code_map, metadata_for, index_layout and every other read |
the same methods on an EditionView: wc.view(), or Catalog.open(path).product(id).view(). On a Catalog, each raises AttributeError naming product(id).view(). |
cat.registry for reads (get, query, obs) |
view.registry |
Catalog(path, embedding_model=m, document_prefix=..., query_prefix=...) |
product.working_copy(embedding_model=m, ...) to build, product.view(embedding_model=m, query_prefix=...) to search |
cat.connection, cat.cursor, cat.bundle_dir, cat.uri, cat.data |
none: callers reach storage only through the API |
ingest_sdmx(cat, ...), ingest_sdmx_registry, ingest_data, ingest_ckan |
the same, given a WorkingCopy; given an EditionView they raise TypeError naming working_copy() |
statcat build-index BUNDLE |
statcat build-index CATALOG PRODUCT, which indexes the product's unsealed working copy |
statcatlib.testing.synthetic_catalog(path, shape, *, seed=0, index=True) returning a SyntheticCatalog |
synthetic_catalog(path, shape, *, product_id="synthetic", seed=0), which creates the catalog, seals one edition and returns the Edition |
statcatlib.bundle.catalog_section(cat) |
WorkingCopy.seal writes the section (format 3); catalog_section takes the private store |
STATCAT_SECTION_FORMAT 2 |
3, which adds kind; check_catalog refuses format 2 |
search/index.json version 2, tables search_<lang>__<generation> |
version 3, tables search_<lang>__<edition_id without hyphens>; version 2 is refused |
New names: Catalog, CatalogAdmin, Product, Edition, EditionView, WorkingCopy,
the module statcatlib.checks (seal checks, such as checks.conf_status()),
and the errors NoLiveEditionError, WorkingCopyExistsError,
WorkingCopyClosedError, SealCheckError, JournalError,
InventoryRuleError, NonFastForwardError, InventoryConflictError and
InventoryContentionError.
What a seal does¶
- A product has one working copy at a time. A second
working_copy()raisesWorkingCopyExistsError;working_copy(resume=True)reopens the unsealed edition, anddiscard()deletes it.close()leaves it unsealed for a later resume. wc.seal()rebuilds the BM25 search index from the edition's rows whenstatcatlib[search]is installed, writes the manifest, and records the editionlivein a new Inventory version, superseding the edition it was built on. After a seal or a discard, every method on the working copy and its views raisesWorkingCopyClosedError.- A working copy built on an edition that is no longer live is refused at
seal with
NonFastForwardError. Discard it and start from the live one. add_language_alternatesand the CKANlanguagelist store tags lowercase, assdmxlib.normalize_tagspells them.
Checking a catalog and reporting on it¶
Two new commands only read a catalog (Catalog administration):
statcat verify-catalog CATALOGchecks the Inventory and every edition it records,data/files included. It exits 0 when clean, 1 on a finding, naming the file, and 2 when the catalog cannot be read.statcat doctor CATALOGreports a synced-folder path, unsealed editions, seal journal entries, sealed editions no Inventory version names, and files a writer left ininventory/. It writes nothing.
Rollback, embargo, the catalog description and prune¶
CatalogAdmin.open(path) makes the operational changes to a catalog
(Catalog administration). Each takes a reason and writes
one Inventory version:
rollback(product, reason=...)withdraws the live edition and makes the edition it superseded live again.Product.rollbackraisesAttributeErrornaming it.set_embargo(on=True, reason=...)makes later seals record their editionsembargoed;release(product, edition, reason=...)makes one live, andcancel(product, edition, reason=...)withdraws it.releaseuses the local clock.describe(description, reason=...)records aCatalogDescription's catalog-level fields, whichCatalog.descriptionreturns.editions(product)lists every edition in every state.prune(older_than=timedelta(days=30), reason=...)retires superseded and withdrawn editions older than the window, deletes the directories of editions tombstoned for longer, and deletesdata/files no manifest lists. It skips a product with a working copy or a seal journal entry. It is a dry run unlessdry_run=False;statcat prune CATALOG --older-than DAYS --reason TEXT --applyruns it from the command line.
The bundle API and the data-path policies are removed¶
| Removed | Replacement |
|---|---|
open_bundle, inspect_bundle |
Catalog.open(path).product(id).view() |
Embedded, Colocated, At, Auto, BundleInfo, BundleDataPathError |
none; an edition reads its data files from its product's data/ |
read_current_data_path, normalize_lake_data_path |
none |
statcatlib.bundle.publish_bundle |
WorkingCopy.seal |
statcatlib.bundle.check_bundle |
sdmxlib.edition.check_edition(path, at=..., extensions={"statcat": check_catalog}) on an edition directory |
statcat bundle check |
statcat verify-catalog CATALOG, which checks every edition the Inventory records; sdmxlib edition check checks one edition directory |
statcat bundle explain, rebind, attach-cmd |
sdmxlib edition copy and files; rebind has no replacement, because an edition resolves its data path against its own directory |
check_catalog(root, manifest, reg, profile) takes sdmxlib 0.80's Check
arguments: an EditionRegistry and the check_edition profile.
A deployment that keeps observations on object storage through a remote
data_path stays on statcatlib 0.11 until Phase 4 brings remote editions.
Records need an identifier, and no text is invented in English¶
put()raisesValueErrorfor a publisher or creator without an id, where it used to hash the English (else French) name. SDMX ingest passes the agency id and CKAN ingest the organization slug; a CKAN organization with neither slug nor UUID now gives no publisher.- A contact point is identified by its email, else its URL, else its phone,
else every (tag, text) pair of its name, so contact ids change. A contact
point with none of these raises
ValueError. - A dimension without a concept name is labelled by its id under
zxx, and SDMX ingest labels an unnamed agency and an unresolved concept the same way, where it wrote the id asen(andfr). - CKAN ingest stores each language under the tag the portal sends,
including open.canada.ca's
en-t-frandfr-t-en, which it dropped.fallback_enis removed, so a package with no English title no longer gets its id as one. Untagged text is stored underund, and codes stored as text (subject slugs, frequency codes) underzxx. ingest_ckanrecords the language pairs a portal's schema declares (CkanClient.alternate_languages()); read them withEditionView.language_alternates().
Language tags are lowercase and checked¶
sdmxlib 0.79 stores every language tag in the form sdmxlib.normalize_tag
returns: lowercase, and well-formed by RFC 5646. statcatlib follows it:
- Every public
langparameter, every.lang()setter and the filtershas_spatial,has_keyword,title_containsanddescription_containsnormalise the tag they are given, solang="fr-CA"finds a label stored underfr-ca. A malformed tag, such asfr_CA!orfr_CA, raisessdmxlib.InvalidTagError, aValueError. - CKAN ingest strips each tag and replaces
_with-before it normalises it, sofr_CAis stored asfr-ca. A tag that is still malformed is dropped from its field with a WARNING, andingest_ckanlogs how many it dropped. When two tags normalise to one (ENanden), the first text is kept, with a WARNING.
InternationalString lookups return sdmxlib.LocalisedString. Read the
text as name["en"].text, or take a plain dict from name.as_dict();
name.get("en") returns a LocalisedString or None.