DSP minimal and DCAT-AP minimal public catalogues

httk.serve.dsp implements a DSP 2025-1 minimal public-catalogue feature set. Publications can be declared inline or stored under the non-OPTIMADE DspPublicationEntry family. Negotiation and transfer state remains in memory, while every catalogue request reads a fresh snapshot from the caller-owned store.

Official DSP discovery uses:

GET  /dsp/.well-known/dspace-version
POST /dsp/2025-1/catalog/request

GET /dsp/2025-1/catalog is intentionally not implemented: it is not a DSP 2025-1 endpoint. A caller that also publishes a JSON-LD representation can mount the vocabulary-neutral httk.serve.http.jsonld_get_app; discovery and protocol-specific validation belong to that caller.

Store-native catalogue

from httk.core import Dataset, DatasetDistribution
from httk.store.db import Database, SqlStore
from httk.serve.dsp import (
    DCAT_AP_3_0_1_PROFILE,
    DCAT_AP_MINIMAL_PROFILE,
    DspDatasetPublication,
    DspProvider,
    DspProviderConfig,
    DspPublicationEntry,
    DspPublicationRecord,
)
from httk.core import Service

publication = DspDatasetPublication(
    dataset=Dataset(
        id="https://provider.example/datasets/one",
        title="Dataset one",
        description="An example dataset.",
        publisher_id="https://provider.example/participants/provider",
        publisher_name="Provider",
        distributions=(
            DatasetDistribution(
                access_url="https://provider.example/files/dataset.csv",
                byte_size=123,
                sha256="…publisher-computed lowercase SHA-256…",
            ),
        ),
    ),
)
store = SqlStore(
    Database.sqlite(),
    entry_records={DspPublicationEntry: DspPublicationRecord},
)
store.save(DspPublicationRecord(dataset=publication))
store.save(DspPublicationRecord(service=Service(
    id="https://catalogue.example/services/dcat-ap",
    title="Public DCAT-AP catalogue",
    endpoint_url="https://catalogue.example/dcat-ap",
    conforms_to=(DCAT_AP_MINIMAL_PROFILE, DCAT_AP_3_0_1_PROFILE),
)))

config = DspProviderConfig(
    public_base_url="https://provider.example",
    dsp_mount="/dsp",
    service_id="https://provider.example/services/dsp",
    service_title="Public DSP service",
    participant_id="https://provider.example/participants/provider",
    catalog_id="https://provider.example/catalogues/main",
    catalog_title="Main catalogue",
    catalog_description="Published datasets.",
    dcat_ap_content_negotiation=True,
)
provider = DspProvider(config, store=store)

The store must declare DspPublicationEntry mapped solely to DspPublicationRecord; neither application closes the store or database. Later dataset or service envelope saves become visible without rebuilding the provider. Duplicate dataset, offer, or distribution IDs and inconsistent publishers are rejected when a snapshot is built. For fixed declarations, use DspProvider(config, publications=(DspPublicationRecord(dataset=publication),)); exactly one source is required.

Replaceable catalogue policy

DspProvider keeps storage access, DSP message validation, negotiation and transfer state, callbacks, and data-address handling fixed. The outbound callback transport is the generic httk.serve.http.webhook implementation (PinnedHttpsJsonPoster and deliver_with_retries), retained under the DSP-facing names DefaultCallbackSender and callback_url. Applications that are developing a stricter publication profile can replace only the catalogue requirements and projections through the public DspCataloguePolicy contract:

provider = DspProvider(
    config,
    store=store,
    catalogue_policy=ProjectCataloguePolicy(),
)

The policy receives the current DspPublicationRecord values whenever a live snapshot is requested. It builds the public CatalogueProfile, validates profile-specific catalogue-request constraints, serializes catalogues, datasets, and offers, and selects the ordinary or alternate catalogue representation. DspCatalogueRepresentation carries the selected media type, projection flag, and additional HTTP headers. The media type must correspond to a response declared by the packaged DSP OpenAPI contract.

MinimalDspCataloguePolicy is the default and preserves the feature set described on this page. A standards prototype should implement the protocol independently rather than subclassing this default, so its normative decisions remain visible and cannot change when the built-in minimal profile evolves. The lower-level policy does not replace DSP routing, schemas, error documents, or process state machines.

Representations

Every DSP Distribution uses its full EU File Type IRI as both its DSP transfer format and its DCAT dct:format resource. CSV and JSON therefore have distinct transfer values. dcat:mediaType, direct access and download URLs, explicitly typed byte size, and SPDX SHA-256 metadata are included in the common payload.

Ordinary catalogue requests use application/json and the protected DSP context. When dcat_ap_content_negotiation=True, an explicit Accept: application/ld+json on the same POST returns the same common JSON value with an owned context and the profiled DCAT-AP media type. A generic JSON-LD GET application can expose that same live document independently. It handles GET/HEAD, JSON-LD content acceptance, ETags, caching, CORS, and an optional profile link, but does not define discovery or claim conformance to a retrieval protocol.

DspDatasetPublication is the DSP offer envelope around one neutral Dataset. The dataset must contain exactly one DatasetDistribution with an absolute HTTPS access URL. It never opens, measures, or hashes a local file. Size and digest are publisher-supplied distribution metadata. .csv and .json infer the authoritative EU File Type and IANA media-type IRIs; other representations require format_iri and media_type_iri explicitly on the distribution.

The package vendors pinned DSP schemas, DCAT-AP 3.0.1 SHACL material, EU CSV/JSON vocabulary concepts, and the SPDX ontology with provenance and integrity hashes. The complete runnable example is in examples/dsp_store_catalogue/.