generated from coulomb/repo-seed
96 lines
2.6 KiB
Python
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}"
|