Catalogue Management
This document covers how datasets are defined, imported, reconciled, and cleaned up.
Catalogue as Code
Catalogue state is defined in YAML and treated like application config: - version controlled - reviewed - validated on import
The API database stores the result of the import, but YAML remains the source of truth.
YAML Structure
Two formats are involved.
Source: governance.yaml
Pipelines declare governance next to the data, as defaults plus rules matched by
dataset glob under sources, with governance.<app>.yaml overlays. It is parsed by
celine.governance (celine-utils):
defaults:
access_level: internal
classification: green
sources:
datasets.ds_dev_gold.example:
title: Example dataset
description: Curated indicator for X.
expose: true
access_level: open
tags: [gold, example]
documentation_url: https://example.org/docs
dataspace:
expose: true
Rule fields include title, description, expose, access_level
(open|internal|restricted|secret), classification (green|yellow|red|pii),
tags, ownership, license, documentation_url, source_system,
retention_days, row_filters, dcat, ontology and dataspace.
Import format
dataset-cli export governance turns governance files into the format
dataset-cli import catalogue reads, a top-level datasets map keyed by
dataset_id (a file without it is skipped with a warning):
datasets:
datasets.ds_dev_gold.example:
title: Example dataset
description: Curated indicator for X.
backend_type: postgres # postgres|s3|fs|quantumleap|context_broker
backend_config: {table: ds_dev_gold.example}
expose: true # catalogue gate (default false)
dataspace_expose: true # offered to the dataspace (default false)
access_level: open # open|internal|restricted|secret
tags: {keywords: [gold, example, "classification:green"]}
lineage: {name: datasets.ds_dev_gold.example, facets: {governance: {}}}
landing_page: https://example.org/docs # from documentation_url
title and backend_type are required. access_level, when given, must be one of
open, internal, restricted, secret (any case); an entry that states none is
stored as internal. classification becomes the keyword
classification:<value>; source_system and retention_days live only in the
governance facet. Only postgres datasets are queryable; the other backend types
are catalogue entries only. See cli-operations.md for the
exporters (governance, postgres, openlineage).
Import Semantics
POST /admin/catalogue (called by dataset-cli import catalogue) requires the
dataset.admin scope or the platform-admin realm role, and upserts on dataset_id:
- missing entries are created
- existing entries have every field overwritten
- entries absent from the input are left in place; removal happens only through the stale-entry cleanup below
The response is {created, updated}.
Physical validation
For postgres datasets the table is checked by reflection before create or
update: backend_config.table, or when the entry states none, the table its id
names (datasets.<schema>.<table> → <schema>.<table>) — the same table the query
path would use. A dataset whose table does not exist is skipped (logged server-side, not
reported in the response). Column schema is not stored: GET
/catalogue/{id}/schema reflects it on request.
Every row filter's args.column must be a column of that table: an import naming a
column the table lacks is refused with 422, listing each such filter, and changes
nothing — no create, update or cleanup (GS-09). A filter's
binds must agree with its handler (GS-08).
Selection & Filters
dataset-cli import catalogue resolves the selection before sending anything:
--input/-i— file or glob, repeatable; inputs are sorted and de-duplicated--ns— namespace filter onlineage.namespace(defaultdefault); supports*,+ns,-ns--datasets—dataset_idglobs, repeatable:+pattern(or bare) includes,-patternexcludes; includes are applied first, then excludes
Example:
- include only gold: --datasets '+datasets.*.gold.*'
- exclude one: --datasets '-datasets.*.gold.experimental_*'
A dataset_id declared differently in two inputs is refused unless
--allow-conflicts, which keeps the declaration from the last file in sorted order.
An invalid entry fails the import (strict by default); --no-strict skips it with a warning.
Dry Run
--dry-run prints the dataset_ids selected after --ns and --datasets and exits.
It does not validate entries and does not contact the API, so it cannot say what
would be created, updated or cleaned up.
Cleanup of Stale Entries
Every import ends with a cleanup, in the same transaction, over the whole catalogue (not just the selection):
- only
postgresentries are checked, against the same table the query path uses (stated or derived from the id) - an entry whose table no longer exists is deleted
- tables validated earlier in this import are skipped
There is no protection or pinning. The number removed is logged server-side.
This addresses real-world drift when pipelines drop or rename tables.
DCAT Exposure Rules
Every catalogue surface (GET /catalogue, GET /catalogue/{id}, POST
/catalogue/search, /schema, /vocabulary, the HTML pages) lists each entry with
expose: true, except access_level: secret. Metadata of internal and
restricted datasets is public; access to their rows is governed separately at
/query. dataspace_expose does not affect the catalogue: it only gates the
dataspace path.
Operational Tips
- keep titles/descriptions in YAML (reviewable)
- use the governance facet and the namespace for policy scoping
- keep dataset_ids stable; rename through controlled migration
- do not overload YAML with physical implementation details unless necessary
Ontology conformance checking
POST /catalogue/{dataset_id}/conformance maps a bounded sample of a dataset's rows
through its declared mapping and validates the resulting RDF graph against the SHACL
shapes of the ontology version that mapping pins.
Off by default. Set CONFORMANCE_ENABLED=true and install the extra:
uv pip install 'dataset[conformance]' # celine-ontologies[mapper] — adds pyshacl
When the setting is on and the extra is missing, the service fails at startup rather than at the first request. When the setting is off the route is not registered at all, so it does not appear in the OpenAPI document — "not deployed" rather than "deployed and refusing".
What it is
An audit, on request. It is deliberately not:
- a gate —
POST /admin/cataloguedoes not check conformance and does not refuse an import over it; - a filter — no row is ever dropped from
/querybecause of a violation. A result that depended on shape conformance would be indistinguishable, to the consumer, from a small one; - stored — there is no "last checked" column and no timestamp on the catalogue entry. A stored conformance claim is a claim with an expiry that nobody watches.
What a green result asserts
That the graph produced by applying this mapping to these rows satisfies the shapes.
Nothing about meaning. A spec mapping kwh onto the wrong observed property, or
onto the right one with the wrong unit, produces a perfectly conformant graph of wrong
statements. SHACL closes the structural half of the promise dct:conformsTo makes; the
semantic half remains the producer's assertion and no validator recovers it.
Note also that the CELINE SHACL profile constrains CELINE classes. A mapping whose
target_type is a class the profile carries no shape for — sosa:Observation today —
conforms because there is nothing to violate. The report says which version ran; it
does not claim the version had anything to say.
Versions
The mapping spec pins the ontology version (profile: {name, version}), and the check
runs against that version, not against the newest one installed. A newer ontology
release must not decide retroactively that a dataset stopped conforming. The library
packages a window of versions (v0.8–v0.10 at the time of writing); a pin outside it
fails loudly and names what is available.
profile_version in the request body overrides the pin — for deciding an upgrade
before making it. The report then reports profile_pinned: false, because a what-if is
not the dataset's own claim.
Access
Authorised exactly like /query, and through the same executor: same governance and
policy checks, same row filters. The report quotes row values back in its violation
messages, so anything weaker would be a row-level leak wearing a metadata endpoint's
clothes. 404 when the dataset is not exposed or declares no mapping, matching
/vocabulary.
The request body is {limit, profile_version, context}, all optional. limit
defaults to CONFORMANCE_SAMPLE_LIMIT (100) and is capped at
CONFORMANCE_MAX_SAMPLE (1000). An unknown profile_version is a 400 naming the
available versions.
Reading the response
{
"dataset_id": "datasets.ds_dev_gold.kpi_definitions",
"conforms": false,
"sample_size": 100,
"violations": ["..."], // capped; `violations_truncated` says when
"violations_truncated": false,
"profile_name": "celine",
"profile_version": "v0.10",
"profile_pinned": true,
"checked_at": "2026-08-14T07:51:44+00:00"
}
conforms: false comes back as 200. A 4xx would conflate "the check failed to run"
with "the check ran and found violations", and the second is the endpoint working. A
check that could not run at all — shapes unresolvable, stored mapping no longer a valid
spec — is 503, because neither is a finding about the data.
sample_size: 0 conforms, over an empty graph. That is why the field is in the report.