Files
kontextual-engine/src/kontextual_engine/ports/blob_storage.py

96 lines
2.6 KiB
Python

"""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}"