Source code for httk.serve.dsp.models

"""Immutable records and protocol errors for the Data Space Protocol provider."""

from collections.abc import Mapping
from dataclasses import dataclass, field
from typing import Literal

from httk.core import Dataset

from ..jsondata import FrozenJsonValue, JsonScalar, JsonValue, freeze_json, thaw_json

[docs] type ErrorKind = Literal["catalog", "negotiation", "transfer"]
[docs] DSP_CONTEXT = "https://w3id.org/dspace/2025/1/context.jsonld"
"""Protected official DSP JSON-LD context required by the 2025-1 schemas.""" @dataclass(frozen=True, slots=True)
[docs] class DeliveryStatus: """Describe delivery health without claiming an unacknowledged DSP transition. :param last_error: Most recent callback failure, if any. :param retry_count: Number of delivery attempts made for the last callback. :param out_of_sync: Whether the remote peer may not have the acknowledged local state. """
[docs] last_error: str | None = None
[docs] retry_count: int = 0
[docs] out_of_sync: bool = False
@dataclass(frozen=True, slots=True)
[docs] class OfferProfile: """Describe the one static, unconditional offer exposed by the provider. :param id: Stable offer identifier. :param target: Dataset identifier to which a message offer must refer. """
[docs] id: str
[docs] target: str
@dataclass(frozen=True, slots=True)
[docs] class DataServiceProfile: """Describe the single service through which the dataset is delivered. :param id: Stable data-service identifier. :param title: Human-readable service title. :param endpoint_url: HTTPS endpoint used for data delivery. """
[docs] id: str
[docs] title: str
[docs] endpoint_url: str
[docs] conforms_to: tuple[str, ...]
[docs] serves_dataset_ids: tuple[str, ...]
@dataclass(frozen=True, slots=True)
[docs] class DcatDataServiceProfile: """Describe a public API included only in the owned DCAT projection. :param id: Stable service identifier. :param title: Human-readable service title. :param endpoint_url: Public HTTPS API endpoint. :param conforms_to: Technical standards implemented by the service. :param serves_dataset_ids: Catalogue dataset identifiers served by the API. :param endpoint_description: Optional IRI describing the API interface. """
[docs] id: str
[docs] title: str
[docs] endpoint_url: str
[docs] conforms_to: tuple[str, ...]
[docs] serves_dataset_ids: tuple[str, ...]
[docs] endpoint_description: str | None
@dataclass(frozen=True, slots=True)
[docs] class DistributionProfile: """Describe the one pull distribution for the provider dataset. :param id: Stable distribution identifier. :param format: DSP transfer format advertised for the distribution. :param access_url: HTTPS URL from which data are pulled. :param data_service: Embedded service description for DSP catalogue output. """
[docs] id: str
[docs] format: str
[docs] file_format: str
[docs] media_type: str
[docs] access_url: str
[docs] data_service: DataServiceProfile
[docs] byte_size: int | None = None
[docs] sha256: str | None = None
@dataclass(frozen=True, slots=True)
[docs] class DatasetProfile: """Group one dataset with its DSP offer, distribution, and data address. :param dataset: Protocol-neutral dataset metadata. :param offer: Unconditional ODRL use offer for this dataset. :param distribution: Pull distribution advertised for this dataset. :param data_service: Service embedded in the distribution. :param data_address: Immutable pull address returned for authorized transfers. """
[docs] dataset: Dataset
[docs] offer: OfferProfile
[docs] distribution: DistributionProfile
[docs] data_service: DataServiceProfile
[docs] data_address: Mapping[str, FrozenJsonValue]
@dataclass(frozen=True, slots=True)
[docs] class CatalogueProfile: """Describe the immutable multi-dataset catalogue served by this provider. :param id: Stable catalogue identifier. :param title: Human-readable catalogue title. :param description: Human-readable catalogue description. :param participant_id: Provider participant identifier. :param dcat_ap_profile: Configured minimal DCAT-AP profile IRI. :param datasets: Dataset publication profiles in stable declaration order. :param dcat_data_services: Additional public APIs for the DCAT projection. """
[docs] id: str
[docs] title: str
[docs] description: str
[docs] participant_id: str
[docs] dcat_ap_profile: str
[docs] datasets: tuple[DatasetProfile, ...]
[docs] dcat_data_services: tuple[DcatDataServiceProfile, ...]
@dataclass(frozen=True, slots=True)
[docs] class AgreementRecord: """Record the provider-created agreement associated with a negotiation. :param id: Unique agreement identifier in ``urn:uuid:`` form. :param policy: Immutable agreement policy JSON. :param target: Dataset identifier covered by the agreement. :param assigner: Provider participant identifier. :param assignee: Consumer participant identifier. :param timestamp: UTC XML Schema date-time at which the agreement was created. """
[docs] id: str
[docs] policy: Mapping[str, FrozenJsonValue]
[docs] target: str
[docs] assigner: str
[docs] assignee: str
[docs] timestamp: str
@dataclass(frozen=True, slots=True)
[docs] class NegotiationRecord: """Record an in-memory contract negotiation and its acknowledged state. :param provider_pid: Provider process identifier. :param consumer_pid: Consumer process identifier. :param callback_address: Consumer callback base URL. :param state: Last state acknowledged by both protocol processing and callback delivery. :param policy: Immutable message offer accepted for the negotiation. :param agreement: Created agreement after an agreement callback is acknowledged. :param pending_transition: Reserved transition token, if a callback is currently in flight. :param delivery: Local delivery health for the latest callback. """
[docs] provider_pid: str
[docs] consumer_pid: str
[docs] callback_address: str
[docs] state: str
[docs] policy: Mapping[str, FrozenJsonValue]
[docs] agreement: AgreementRecord | None = None
[docs] pending_transition: str | None = None
[docs] delivery: DeliveryStatus = field(default_factory=DeliveryStatus)
@dataclass(frozen=True, slots=True)
[docs] class TransferRecord: """Record an in-memory transfer process and its acknowledged state. :param provider_pid: Provider transfer-process identifier. :param consumer_pid: Consumer transfer-process identifier. :param callback_address: Consumer callback base URL. :param agreement_id: Finalized agreement authorizing this transfer. :param format: Requested transfer format. :param state: Last state acknowledged by both protocol processing and callback delivery. :param pending_transition: Reserved transition token, if a callback is currently in flight. :param delivery: Local delivery health for the latest callback. """
[docs] provider_pid: str
[docs] consumer_pid: str
[docs] callback_address: str
[docs] agreement_id: str
[docs] format: str
[docs] state: str
[docs] pending_transition: str | None = None
[docs] delivery: DeliveryStatus = field(default_factory=DeliveryStatus)
[docs] class DspProtocolError(Exception): """Represent a protocol failure that an HTTP adapter can serialize directly. :param kind: DSP area whose official error document must be emitted. :param status_code: HTTP status suitable for the adapter response. :param detail: Safe human-readable failure detail. :param code: Optional machine-readable DSP error code. :param provider_pid: Provider process identifier, when one is known. :param consumer_pid: Consumer process identifier, when one is known. """ def __init__( self, kind: ErrorKind, status_code: int, detail: str, *, code: str | None = None, provider_pid: str | None = None, consumer_pid: str | None = None, ) -> None: super().__init__(detail)
[docs] self.kind = kind
[docs] self.status_code = status_code
[docs] self.detail = detail
[docs] self.code = code
[docs] self.provider_pid = provider_pid
[docs] self.consumer_pid = consumer_pid
[docs] def as_document(self) -> dict[str, JsonValue]: """Serialize this failure as the official DSP JSON error document. :return: Error document for the exception's DSP area. """ type_name = { "catalog": "CatalogError", "negotiation": "ContractNegotiationError", "transfer": "TransferError", }[self.kind] document: dict[str, JsonValue] = { "@context": [DSP_CONTEXT], "@type": type_name, "reason": [self.detail], } if self.code is not None: document["code"] = self.code if self.provider_pid is not None: document["providerPid"] = self.provider_pid if self.consumer_pid is not None: document["consumerPid"] = self.consumer_pid return document
[docs] class DspTransitionSuperseded(RuntimeError): """Report that a callback was delivered but a concurrent transition won the commit. This is not a protocol error: by the time it is raised the peer callback has already been delivered successfully, and only the local commit lost a race with a concurrent state transition. There is nothing to report on the wire and no HTTP status to carry, so it deliberately does not subclass :class:`DspProtocolError`. """
__all__ = [ "AgreementRecord", "CatalogueProfile", "DataServiceProfile", "DatasetProfile", "DcatDataServiceProfile", "DeliveryStatus", "DistributionProfile", "DspProtocolError", "DspTransitionSuperseded", "ErrorKind", "FrozenJsonValue", "JsonScalar", "JsonValue", "NegotiationRecord", "OfferProfile", "TransferRecord", "freeze_json", "thaw_json", ]