"""Stdlib-only OPTIMADE-aligned calculation provenance records.
Runs are served as provider-prefixed ``_httk_runs`` entries. Their
``has_input``/``has_artifact``/``has_output`` relationships are represented by
loose labeled references rather than object references: inputs are consumed,
artifacts are created, and outputs are returned. The single-creator rule for
artifacts is a serving concern and is not enforced on an individual record.
The definition identity remains the unprefixed ``RUNS_DEFINITION_ID`` IRI.
"""
import datetime
from collections.abc import Iterable, Mapping
from dataclasses import dataclass, field, fields
from typing import Annotated, Any, ClassVar, Self
from .storage import IdentitySkip, StorageInfo, content_id
[docs]
RUNS_DEFINITION_ID = "https://schemas.httk.org/defs/v0.1/entrytypes/runs"
_TIMESTAMP_FIELDS = frozenset({"last_modified"})
def _validate_string(value: Any, field_name: str) -> None:
if not isinstance(value, str) or not value or value != value.strip():
raise ValueError(f"Field '{field_name}' must be a non-empty string without surrounding whitespace.")
def _validate_uri(value: Any, field_name: str) -> None:
if value is not None and (not isinstance(value, str) or not value or value != value.strip()):
raise ValueError(f"Field '{field_name}' must be a non-empty string without surrounding whitespace or None.")
def _validate_timestamp(value: Any, field_name: str) -> None:
if value is not None and (not isinstance(value, datetime.datetime) or value.utcoffset() is None):
raise ValueError(f"Field '{field_name}' must be a timezone-aware datetime with an explicit offset.")
def _create(cls: type[Any], obj: Any) -> Any:
if isinstance(obj, cls):
return obj
if not isinstance(obj, Mapping):
raise TypeError(f"Expected a {cls.__name__} or a mapping, got {type(obj).__name__}.")
known = {item.name for item in fields(cls)}
unknown = [key for key in obj if key not in known]
if unknown:
raise ValueError("Unknown field(s) for " + cls.__name__ + ": " + ", ".join(sorted(unknown)) + ".")
values = dict(obj)
for field_name in _TIMESTAMP_FIELDS & known:
value = values.get(field_name)
if isinstance(value, str):
try:
value = datetime.datetime.fromisoformat(value)
except ValueError as exc:
raise ValueError(f"Invalid ISO-8601 value for field '{field_name}': {value!r}.") from exc
_validate_timestamp(value, field_name)
values[field_name] = value
elif value is not None:
_validate_timestamp(value, field_name)
return cls(**values)
def _edges(values: Iterable[Any]) -> "tuple[RunEdge, ...]":
return tuple(RunEdge.create(value) for value in values)
@dataclass(frozen=True)
[docs]
class RunEdge:
"""Store one loose labeled reference from a run to another entry.
Edges deliberately keep the related entry's type and identifier as
strings rather than object references, so a run can refer to entries
served by another provider.
:param label: The relationship label.
:param entry_type: The related entry type name.
:param entry_id: The related entry identifier.
"""
__httk_storage__: ClassVar[StorageInfo] = StorageInfo(
storage_name="core_run_edge_v1", identity_name="core_run_edge_v1"
)
def __post_init__(self) -> None:
for field_name in ("label", "entry_type", "entry_id"):
_validate_string(getattr(self, field_name), field_name)
@classmethod
[docs]
def create(cls, obj: "RunEdge | Mapping[str, Any]") -> Self:
"""Coerce a mapping or existing edge into a :class:`RunEdge`.
:param obj: A run edge instance or field mapping.
:return: The existing or newly constructed run edge.
:raises TypeError: If ``obj`` is neither a run edge nor a mapping.
:raises ValueError: If the mapping has unknown or invalid fields.
"""
return _create(cls, obj)
@dataclass(frozen=True)
[docs]
class Run:
"""One workflow execution with loose provenance edges.
``inputs`` are ``has_input`` edges to consumed entries, ``artifacts`` are
``has_artifact`` edges to created entries, and ``outputs`` are
``has_output`` edges to returned entries. Artifact single-creator
exclusivity across runs is documented here, not enforced per record.
Every invariant is cheap and total, so there is deliberately no
``__httk_validate__`` hook.
Edges remain loose string triples by design, never object references.
Labels are unique independently on each of ``inputs``, ``artifacts``, and
``outputs``.
:param workflow_declaration_uri: The workflow declaration IRI, if declared.
:param inputs: The labeled entries consumed by the run.
:param artifacts: The labeled entries created by the run.
:param outputs: The labeled entries returned by the run.
:param immutable_id: An optional provider-specific immutable identifier.
:param last_modified: The optional timezone-aware metadata timestamp.
"""
__httk_storage__: ClassVar[StorageInfo] = StorageInfo(
storage_name="core_run_v1",
identity_name="core_run_v1",
indexes=(("workflow_declaration_uri",), ("immutable_id",), ("last_modified",)),
)
[docs]
workflow_declaration_uri: str | None = None
[docs]
artifacts: tuple[RunEdge, ...] = ()
[docs]
outputs: tuple[RunEdge, ...] = ()
[docs]
immutable_id: Annotated[str | None, IdentitySkip()] = field(default=None, compare=False)
[docs]
last_modified: Annotated[datetime.datetime | None, IdentitySkip()] = field(default=None, compare=False)
@property
[docs]
def type(self) -> str:
"""Return the served entry type name."""
return "_httk_runs"
@property
[docs]
def id(self) -> str:
"""Return the content identity of this run."""
return content_id(self)
def __post_init__(self) -> None:
_validate_uri(self.workflow_declaration_uri, "workflow_declaration_uri")
for side in ("inputs", "artifacts", "outputs"):
values = _edges(getattr(self, side))
labels: set[str] = set()
for edge in values:
if edge.label in labels:
raise ValueError(f"Duplicate label {edge.label!r} on Run {side}.")
labels.add(edge.label)
object.__setattr__(self, side, values)
_validate_timestamp(self.last_modified, "last_modified")
@classmethod
[docs]
def create(cls, obj: "Run | Mapping[str, Any]") -> Self:
"""Coerce a mapping or existing run into a :class:`Run`.
:param obj: A run instance or field mapping.
:return: The existing or newly constructed run.
:raises TypeError: If ``obj`` is neither a run nor a mapping.
:raises ValueError: If the mapping has unknown or invalid fields.
"""
return _create(cls, obj)
@dataclass(frozen=True)
[docs]
class ProductLink:
"""A curation ``has_product``/``is_product`` edge between data entries.
A label is unique per source entry across links; that constraint is
enforced at the serving projection rather than on each record.
:param source_type: The source entry type name.
:param source_id: The source entry identifier.
:param target_type: The target entry type name.
:param target_id: The target entry identifier.
:param label: The relationship label, unique per source entry at serving time.
:param workflow_declaration_uri: The workflow declaration IRI, if declared.
"""
__httk_storage__: ClassVar[StorageInfo] = StorageInfo(
storage_name="core_product_link_v1",
identity_name="core_product_link_v1",
dedup="by_value",
indexes=(("source_type", "source_id"), ("target_type", "target_id")),
)
[docs]
workflow_declaration_uri: str | None = None
def __post_init__(self) -> None:
for field_name in ("source_type", "source_id", "target_type", "target_id", "label"):
_validate_string(getattr(self, field_name), field_name)
if (self.source_type, self.source_id) == (self.target_type, self.target_id):
raise ValueError("ProductLink source and target must differ.")
_validate_uri(self.workflow_declaration_uri, "workflow_declaration_uri")
@classmethod
[docs]
def create(cls, obj: "ProductLink | Mapping[str, Any]") -> Self:
"""Coerce a mapping or existing link into a :class:`ProductLink`.
:param obj: A product-link instance or field mapping.
:return: The existing or newly constructed product link.
:raises TypeError: If ``obj`` is neither a product link nor a mapping.
:raises ValueError: If the mapping has unknown or invalid fields.
"""
return _create(cls, obj)
[docs]
class RunEntry:
"""Logical entry family for served :class:`Run` records.
This family is not itself storable; store a ``Run`` directly.
"""
[docs]
type = "_httk_runs"
[docs]
definition_id = RUNS_DEFINITION_ID
def __new__(cls, *args: Any, **kwargs: Any) -> Self:
raise TypeError("RunEntry is a logical entry family; store a Run directly")
__all__ = ["RUNS_DEFINITION_ID", "ProductLink", "Run", "RunEdge", "RunEntry"]