WP-0001-T003: storage adapter SPI and local filesystem backend

src/artifactstore/storage/:
- spi.py: StorageBackend Protocol (backend_id, put, get, head, delete,
  health) and result dataclasses (StorageReceipt, StorageObjectMetadata,
  DeletionResult, BackendStatus). ObjectNotFoundError exception type.
- registry.py: backend lookup by string ID (register/get/list_backends/
  clear) per ADR-0004.
- backends/local.py: LocalBackend implementation.
  * Object layout <root>/<algorithm>/<hex[0:2]>/<hex[2:4]>/<hex>.
  * Atomic writes: tmpfile + fsync + rename (idempotent re-puts drain the
    stream without rewriting).
  * Defence in depth: resolves the final path and asserts it remains under
    the configured root.
  * Range reads honour HTTP-style inclusive (start, end) tuples.
  * health() returns disk usage via shutil.disk_usage and surfaces an
    unhealthy status when the root has disappeared.
  * delete() cleans up emptied shard directories opportunistically.

tests/unit/test_storage_local.py (14 cases): put/get round-trip; object
key layout matches blueprint; head returns metadata; head/get missing
raise ObjectNotFoundError; put is idempotent; delete returns True then
False; range read returns subrange; range read rejects invalid range;
health reports disk usage; health reports unhealthy when root vanished;
ContentAddress validation blocks path-traversal-flavoured inputs;
registry register/get/list/clear round-trip; idempotent re-put leaves
bytes intact.

Gates: ruff clean, mypy --strict clean on 41 files, 59 tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 02:01:25 +02:00
parent 0a49798699
commit 28ec2922d3
7 changed files with 558 additions and 3 deletions

View File

@@ -1,5 +1,42 @@
"""Storage adapter SPI and backend registry.
The SPI and local filesystem backend land in ARTIFACT-STORE-WP-0001-T003.
The S3-compatible backend lands in workplan WP-0004.
Backends address bytes by content address (ADR-0001). The SPI is small
(``put`` / ``get`` / ``head`` / ``delete`` / ``health``) so swapping or
adding adapters never touches the registry or API layers.
"""
from artifactstore.storage.backends.local import LocalBackend
from artifactstore.storage.registry import (
clear as clear_backends,
)
from artifactstore.storage.registry import (
get as get_backend,
)
from artifactstore.storage.registry import (
list_backends,
)
from artifactstore.storage.registry import (
register as register_backend,
)
from artifactstore.storage.spi import (
BackendStatus,
DeletionResult,
ObjectNotFoundError,
StorageBackend,
StorageObjectMetadata,
StorageReceipt,
)
__all__ = [
"BackendStatus",
"DeletionResult",
"LocalBackend",
"ObjectNotFoundError",
"StorageBackend",
"StorageObjectMetadata",
"StorageReceipt",
"clear_backends",
"get_backend",
"list_backends",
"register_backend",
]