Skip to main content

dr-store

CI PyPI

Definitions · Terms · Contracts · Changelog · dr-serialize

dr-store provides domain-neutral storage primitives for immutable records and document artifacts:

  • Content addressing identifies complete records by their declared schemas and SHA-256 hashes of their Canonical JSON Text under dr-serialize's frozen profile.
  • Object Store provides immutable puts, verified reads, and atomic bindings from opaque caller-owned keys to object references.
  • Storage backends supply the Object Store's atomic, append-only operations. MemoryBackend is process-local; SqliteBackend persists data for cross-process use.
  • Record Cache memoizes records under opaque caller-owned keys. Reads return typed hits; absent, missing, or unverifiable stored values are misses, while invalid requested schemas and operational backend faults raise. Entries are never rebound, so callers invalidate by selecting a new key; derive_cache_key provides a canonical scheme using a versioned namespace and payload.
  • Document Directory publishes one canonical Manifest beside streamed binary Sidecars.

Installation

dr-store requires Python 3.12 or newer.

python -m pip install dr-store

Usage

from dr_store import MemoryBackend, ObjectStore

store = ObjectStore(MemoryBackend())
reference, _ = store.put("example.note.v1", {"title": "hello"})
store.bind("notes/latest", reference)

assert store.resolve("notes/latest") == reference
assert store.get(reference) == {"title": "hello"}

The same store can provide memoization without introducing another backend:

from dr_store import CacheHit, RecordCache, derive_cache_key

cache = RecordCache(store)
key = derive_cache_key("example.summary.v1", {"document": "note-42"})
cache.put(key, "example.summary.v1", {"summary": "hello"})

assert cache.get(key, schema="example.summary.v1") == CacheHit(
    record={"summary": "hello"}
)

Use SqliteBackend(path) when the stored objects and bindings must persist across processes. The rendered definitions, authoritative terms, and binding contracts describe the vocabulary, public-export mappings, and behavioral boundaries.

Content addressing

Content addressing validates each schema-qualified reference and derives its content hash through dr-serialize's canonical JSON profile. Its stable public shape is:

@dataclass(frozen=True, slots=True)
class ObjectReference:
    schema: str
    content_hash: str

    @classmethod
    def for_record(cls, schema: str, record: Jsonable) -> ObjectReference: ...
    def verify_record(self, record: Jsonable) -> None: ...

def compute_content_hash(record: Jsonable) -> str: ...
def is_content_hash(value: str) -> bool: ...

Object Store

The Object Store owns immutable record operations and opaque key bindings. Its statuses and store surface keep storage outcomes distinct from stored records:

class PutStatus(Enum):
    STORED = "stored"
    IDEMPOTENT = "idempotent"

class BindStatus(Enum):
    BOUND = "bound"
    IDEMPOTENT = "idempotent"

class ObjectStore:
    def __init__(self, backend: Backend) -> None: ...
    def put(
        self, schema: str, record: Jsonable
    ) -> tuple[ObjectReference, PutStatus]: ...
    def get(self, reference: ObjectReference) -> Jsonable: ...
    def bind(
        self, key: str, reference: ObjectReference
    ) -> BindStatus: ...
    def resolve(self, key: str) -> ObjectReference | None: ...

Storage backends

Storage backends implement one atomic protocol beneath the Object Store. Outcome objects carry the existing row when an append-only operation does not insert:

@dataclass(frozen=True, slots=True)
class PutOutcome:
    inserted: bool
    stored_schema: str
    stored_canonical: str

@dataclass(frozen=True, slots=True)
class BindOutcome:
    bound: bool
    existing_schema: str
    existing_content_hash: str
class Backend(Protocol):
    def put_object(
        self, *, schema: str, content_hash: str, canonical: str
    ) -> PutOutcome: ...
    def get_object(
        self, *, schema: str, content_hash: str
    ) -> tuple[str, str] | None: ...
    def bind(
        self, *, key: str, schema: str, content_hash: str
    ) -> BindOutcome: ...
    def get_binding(self, *, key: str) -> tuple[str, str] | None: ...

class MemoryBackend: ...
class SqliteBackend:
    def __init__(self, path: str | Path) -> None: ...

Record Cache

The Record Cache is a memoization facade over an existing ObjectStore. It accepts opaque caller-owned keys, with derive_cache_key as the canonical helper for content-derived memoization. A typed hit keeps every strict JSON record, including null, distinct from a miss:

def derive_cache_key(namespace: str, payload: Jsonable) -> str: ...

@dataclass(frozen=True, slots=True)
class CacheHit:
    record: Jsonable

class RecordCache:
    def __init__(self, store: ObjectStore) -> None: ...
    def get(self, key: str, *, schema: str) -> CacheHit | None: ...
    def put(
        self, key: str, schema: str, record: Jsonable
    ) -> ObjectReference: ...

Document Directory

A Document Directory groups one canonical JSON Manifest with streamed binary Sidecars. The directory owns allocation and publication while SidecarWriter owns bounded retention:

class DocumentDirectory:
    def __init__(self, path: Path, manifest_name: str) -> None: ...

    @classmethod
    def allocate(
        cls, root: str | Path, *, prefix: str, manifest_name: str
    ) -> DocumentDirectory: ...

    def publish(self, manifest: Jsonable) -> None: ...
    def open_sidecar(
        self,
        name: str,
        *,
        head_cap: int | None = None,
        tail_cap: int | None = None,
    ) -> SidecarWriter: ...

    @classmethod
    def read_manifest(
        cls, path: str | Path, *, manifest_name: str
    ) -> Jsonable: ...

    def verify_sidecar(
        self,
        name: str,
        *,
        expected_digest: str,
        expected_head_length: int,
        expected_tail_length: int,
    ) -> None: ...
@dataclass(frozen=True, slots=True)
class SidecarSummary:
    head_length: int
    tail_length: int
    produced: int
    dropped: int
    digest: str

class SidecarWriter:
    def write(self, chunk: bytes) -> None: ...
    def finalize(self) -> SidecarSummary: ...

Filesystem and failure semantics

A Document Directory is intended for caller-coordinated single-writer use; dr-store does not enforce that policy with a lock. Allocation uses a timestamp and UUID4, but a generated-name collision raises AllocationError rather than being retried. Allocation does not flush the caller-owned root directory.

Manifest publication writes and flushes a temporary file, replaces the Manifest in the same directory, and then flushes the Document Directory. All-or-nothing visibility depends on the underlying filesystem honoring atomic same-directory replacement; network, synchronized, or other filesystems whose rename semantics are not established are outside current evidence. A failure of the final flush raises even though the replacement may already be visible, and it does not roll the Manifest back. These operations do not promise power-loss durability.

Name validation prevents lexical traversal syntax only. Manifest reads and Sidecar creation and writes follow existing final-component symlinks, so those paths require trusted, caller-controlled directory contents. Sidecar finalization flushes the Sidecar descriptor before returning its summary, but it does not flush the Sidecar's directory entry or impose ordering on an arbitrary Manifest publication. Sidecar verification is the no-follow path: it refuses final-component symlinks for both the Document Directory and named child, requires a regular direct child, and reads from the descriptor it inspected.

A failed Sidecar write raises AllocationError and may leave its descriptor open and its accounting state advanced. The writer is unusable by contract and must be abandoned; retrying it or finalizing it has no supported outcome.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dr_store-0.1.3.tar.gz (14.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dr_store-0.1.3-py3-none-any.whl (20.2 kB view details)

Uploaded Python 3

File details

Details for the file dr_store-0.1.3.tar.gz.

File metadata

  • Download URL: dr_store-0.1.3.tar.gz
  • Upload date:
  • Size: 14.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for dr_store-0.1.3.tar.gz
Algorithm Hash digest
SHA256 abeb65f8095347cfe90db7a1ff3ded79605f4e6c68f0d465a7ddce7a7cca64a1
MD5 d564b368905d20a157bf497b38e63329
BLAKE2b-256 b37d059a46b025d312f0568d4b4ca29eb86ca3b554ce30cf960b21eefdd802bf

See more details on using hashes here.

Provenance

The following attestation bundles were made for dr_store-0.1.3.tar.gz:

Publisher: release.yml on danielle-rothermel/dr-store

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dr_store-0.1.3-py3-none-any.whl.

File metadata

  • Download URL: dr_store-0.1.3-py3-none-any.whl
  • Upload date:
  • Size: 20.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for dr_store-0.1.3-py3-none-any.whl
Algorithm Hash digest
SHA256 e4ca9e791836357571267d3d97c6360e148fa8bbcb50aca36954139379429bfb
MD5 5ddc147066ddd2e26bea38b1255ee38c
BLAKE2b-256 63ca2a023e2d0a3950af616f206069872631b3034d0b6fb1136e837bba280992

See more details on using hashes here.

Provenance

The following attestation bundles were made for dr_store-0.1.3-py3-none-any.whl:

Publisher: release.yml on danielle-rothermel/dr-store

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page