Skip to content

dol.content: ContentRef + flat content-addressed storage (blob layer) - #79

Merged
thorwhalen merged 1 commit into
masterfrom
feature/content-addressed-store
Aug 1, 2026
Merged

dol.content: ContentRef + flat content-addressed storage (blob layer)#79
thorwhalen merged 1 commit into
masterfrom
feature/content-addressed-store

Conversation

@thorwhalen

Copy link
Copy Markdown
Member

Closes #78.

Adds dol.content — the content half of the content–metadata bifurcation: a flat bytes store plus a small serializable ContentRef that stands in for the bytes inside a record. Pure stdlib, zero new dependencies.

API

  • ContentRef — frozen dataclass (item_id, field, hash?, url?, mime_type?, size?); to_json/from_json; is_content_ref.
  • put_content (location-addressed) / add_content (content-addressed, idempotent CAS) / get_content / delete_content — over an injected MutableMapping backend.
  • ContentAddressedStore / with_content_addressing — CAS facade: store.add(bytes) -> ContentRef.
  • SupportsUrlFor Protocol + content_url — the URL seam.

Design rationales worth noting

  1. URLs are resolved on demand, never baked into a ref. put_content/add_content leave ContentRef.url empty; content_url(store, ref) resolves it when needed (preferring a url the ref already carries, else the backend's url_for). Reason: a backend's url_for may mint a presigned, expiring URL (S3), and a ContentRef is designed to be persisted inside a record — freezing an expiring URL into it would be a latent bug. This mirrors zodal's "getUrl on demand is preferred."
  2. S3 stays out of dol entirely. The only cloud-aware bit — presigned-URL minting — is reached through the duck-typed url_for seam (SupportsUrlFor). dol.content imports nothing beyond dataclasses/hashlib/mimetypes; the S3 url_for implementation belongs in s3dol.
  3. Backends are injected, tested with dict. Every function/store takes the backend as a parameter (dict in tests → Files locally → s3dol in the cloud), per dol convention — no backend is imported here.
  4. CAS invariant is guarded. ContentAddressedStore.__setitem__ rejects a key that isn't the content hash of the value (raise, don't silently corrupt); minting goes through .add().
  5. Cross-language parity. ContentRef.to_json() emits the camelCase {_tag, itemId, mimeType, …} shape, byte-identical to zodal's ContentRef, so a reference crosses the Python↔JS boundary unchanged.
  6. CAS-by-hash is opt-in. zodal's research docs defer content-addressing to an opt-in wrapper; shipping it here (as with_content_addressing) puts the Python side slightly ahead while staying contract-compatible. Truncated hashes (length=) document the collision tradeoff.

Scope

The record/metadata half (a queryable DataProvider-style facade + bifurcated composition) is intentionally out of scope — a separate follow-up.

Tests

11 doctests + 17 unit tests (dol/tests/test_content.py); full dol suite green (188 passed, 3 skipped).

https://claude.ai/code/session_01Gw5RPgrQhC88Hc3DyACYWF

…yer)

The "content half" of the content-metadata bifurcation (see
misc/docs/dol_content_metadata_bifurcation.md): a flat bytes store plus a small
serializable ContentRef token that stands in for the bytes inside a record.

- ContentRef: frozen dataclass; to_json/from_json emit the camelCase wire shape
  (_tag/itemId/mimeType) matching the zodal TypeScript ContentRef, so refs cross
  the Python<->JS boundary unchanged. is_content_ref predicate.
- put_content (location-addressed) / add_content (content-addressed, idempotent
  CAS) / get_content / delete_content — all over an INJECTED MutableMapping
  backend (dict in tests, dol.Files locally, s3dol in the cloud).
- ContentAddressedStore / with_content_addressing: CAS facade; add(data)->ContentRef
  mints the hash key; direct __setitem__ guards the key==hash invariant.
- url_for seam (SupportsUrlFor Protocol + content_url): reads can redirect to a
  CDN/presigned/static URL while writes stay on the backend. URLs resolved ON
  DEMAND, never baked into a (persisted) ref — a backend's url_for may mint an
  expiring presigned URL. All S3/presigning knowledge stays in s3dol; dol.content
  is pure stdlib (dataclasses/hashlib/mimetypes), zero new dependencies.

CAS-by-hash is a capability zodal explicitly defers to an opt-in wrapper, so this
puts the Python side slightly ahead while staying contract-compatible.

11 doctests + 17 unit tests; full dol suite green.

Claude-Session: https://claude.ai/code/session_01Gw5RPgrQhC88Hc3DyACYWF
@thorwhalen
thorwhalen merged commit 8441a9d into master Aug 1, 2026
8 checks passed
@thorwhalen
thorwhalen deleted the feature/content-addressed-store branch August 1, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A content/blob layer: serializable ContentRef + content-addressed store facade

1 participant