"""Blob storage port and content-addressed reference models.""" from __future__ import annotations from dataclasses import dataclass from collections.abc import Iterator from typing import Protocol from kontextual_engine.core import content_digest from kontextual_engine.errors import ValidationError @dataclass(frozen=True) class BlobRef: digest: str size_bytes: int storage_key: str storage_ref: str adapter: str media_type: str | None = None def to_dict(self) -> dict[str, object]: data: dict[str, object] = { "digest": self.digest, "size_bytes": self.size_bytes, "storage_key": self.storage_key, "storage_ref": self.storage_ref, "adapter": self.adapter, } if self.media_type: data["media_type"] = self.media_type return data @dataclass(frozen=True) class BlobWriteResult: blob: BlobRef created: bool def to_dict(self) -> dict[str, object]: return {"blob": self.blob.to_dict(), "created": self.created} @dataclass(frozen=True) class BlobCleanupResult: dry_run: bool deleted_count: int retained_count: int reclaimable_bytes: int deleted_storage_refs: tuple[str, ...] = () def to_dict(self) -> dict[str, object]: return { "dry_run": self.dry_run, "deleted_count": self.deleted_count, "retained_count": self.retained_count, "reclaimable_bytes": self.reclaimable_bytes, "deleted_storage_refs": list(self.deleted_storage_refs), } class BlobStorage(Protocol): adapter_name: str def put_bytes(self, content: bytes, *, media_type: str | None = None) -> BlobWriteResult: ... def read_bytes(self, storage_ref: str) -> bytes: ... def iter_bytes(self, storage_ref: str, *, chunk_size: int = 65536) -> Iterator[bytes]: ... def stat(self, storage_ref: str) -> BlobRef: ... def exists(self, storage_ref_or_digest: str) -> bool: ... def iter_blobs(self) -> list[BlobRef]: ... def delete_unreferenced( self, referenced_storage_refs: set[str], *, dry_run: bool = True, ) -> BlobCleanupResult: ... def blob_digest(content: bytes) -> str: return content_digest(content) def digest_storage_key(digest: str) -> str: if not digest.startswith("sha256:"): raise ValidationError("Unsupported blob digest", details={"digest": digest}) value = digest.removeprefix("sha256:") if len(value) < 4: raise ValidationError("Invalid blob digest", details={"digest": digest}) return f"sha256/{value[:2]}/{value[2:4]}/{value}"