dol is a pure-Python (no dependencies) toolkit for wrapping any storage backend (files, S3, databases, dicts) behind a uniform dict-like interface. Version 0.3.38. Python ≥ 3.10.
For a comprehensive agent-readable API reference, see llms-full.txt. For a quick orientation, see llms.txt.
| File | What's in it |
|---|---|
dol/base.py |
Collection, KvReader, KvPersister, Store — the class hierarchy |
dol/trans.py |
wrap_kvs (core), store_decorator, filt_iter, cached_keys, Codec, kv_wrap |
dol/kv_codecs.py |
ValueCodecs, KeyCodecs — ready-made codec namespaces |
dol/caching.py |
cache_this, cache_vals, store_cached, WriteBackChainMap |
dol/paths.py |
KeyTemplate, mk_relative_path_store, KeyPath, path_get/set/filter |
dol/filesys.py |
Files, TextFiles, JsonFiles, PickleFiles — filesystem stores |
dol/sources.py |
FlatReader, FanoutReader/Persister, CascadedStores |
dol/signatures.py |
Sig — signature arithmetic |
dol/util.py |
Pipe, lazyprop, partialclass, groupby |
dol/__init__.py |
Public API — all exports live here |
The fundamental operation is wrapping a backend with transforms:
from dol import wrap_kvs, Files
import json
# Add JSON serialization to a file store
JsonFileStore = wrap_kvs(Files, obj_of_data=json.loads, data_of_obj=json.dumps)
# Or wrap an instance
s = wrap_kvs(dict(), id_of_key=lambda k: k.upper(), key_of_id=str.lower)wrap_kvs parameters:
key_of_id/id_of_key— outgoing/incoming key transformsobj_of_data/data_of_obj— outgoing/incoming value transformspostget(key, data) → obj— value transform that knows the key (for reads)preset(key, obj) → data— value transform that knows the key (for writes)key_codec/value_codec—Codecobjects (encoder+decoder pair)
X_of_Ynaming:key_of_id= "give me a key, you give me an id" (outgoing).id_of_key= "give me an id, you give me a key" (incoming). Always pairs.- KvReader for read-only: subclass
KvReader(notKvPersister) when writes aren't needed. - KvPersister for read-write:
clear()is disabled — override only if you're sure. - Test with
dict, deploy with real backend:wrap_kvs(dict, ...)first, then swapdictforFiles, a DB store, etc. - Transforms are pure functions: they should be stateless and not have side effects.
from dol import wrap_kvs
MyStore = wrap_kvs(
dict,
id_of_key=lambda k: k + ".json",
key_of_id=lambda _id: _id[:-5],
obj_of_data=json.loads,
data_of_obj=json.dumps,
)from dol.base import KvReader
class MyReader(KvReader):
def __getitem__(self, k): ...
def __iter__(self): ...
def __len__(self): ... # optional, falls back to iteration countfrom dol.base import Store
class MyStore(Store):
def _id_of_key(self, k):
return k.upper()
def _key_of_id(self, _id):
return _id.lower()
def _data_of_obj(self, obj):
return json.dumps(obj)
def _obj_of_data(self, data):
return json.loads(data)from dol import ValueCodecs, KeyCodecs, Pipe
# Common value codecs
ValueCodecs.pickle() # pickle.dumps / pickle.loads
ValueCodecs.json() # json.dumps / json.loads
ValueCodecs.gzip() # compress/decompress
ValueCodecs.str_to_bytes() # encode/decode
# Key codecs
KeyCodecs.suffixed(".pkl") # add/strip suffix
KeyCodecs.prefixed("ns:") # add/strip prefix
# Chain with Pipe
MyStore = Pipe(KeyCodecs.suffixed(".pkl"), ValueCodecs.pickle())(dict)Most tools in trans.py use @store_decorator, making them work 4 ways:
from dol import filt_iter, cached_keys
# As class decorator
@filt_iter(filt=lambda k: k.endswith(".json"))
class MyStore(dict): ...
# As instance wrapper
s = filt_iter(my_store, filt=lambda k: k.endswith(".json"))
# As factory
json_only = filt_iter(filt=lambda k: k.endswith(".json"))
s = json_only(my_store)from dol import cache_this, cache_vals, store_cached
# Cache a property or method
class MyClass:
@cache_this
def expensive(self):
return sum(range(1_000_000))
# Cache fetched values from a slow store
fast = cache_vals(slow_store)
# Persist function results across sessions
@store_cached(JsonFiles("/cache"))
def compute(x, y):
return slow_computation(x, y)Always prototype with dict as the backend:
# 1. Test logic with dict
s = wrap_kvs(dict(), obj_of_data=json.loads, data_of_obj=json.dumps)
s["key"] = {"a": 1}
assert s["key"] == {"a": 1}
# 2. Swap to real backend
from dol import Files
s = wrap_kvs(Files("/data"), obj_of_data=json.loads, data_of_obj=json.dumps)Run tests: a bare pytest (from the repo root) runs exactly what CI runs — the
dol/tests/ unit tests and every module doctest, with CI's doctest flags.
Narrow it with pytest dol/tests/ (unit tests only) or pytest dol/caching.py
(one module's doctests). Do not add NORMALIZE_WHITESPACE to
doctest_optionflags: CI does not pass it, so doctests relying on it would pass
locally and fail in CI.
| Document | Contents |
|---|---|
| general_design.md | Language-agnostic design: what dol is, the KV pipeline, layered composition, patterns |
| dol_design.md | Python architecture: class hierarchy, wrap_kvs deep dive, Codec/Sig/Pipe, critique |
| dol_architecture_map.md | Code-verified structural map: module/dependency graph, public API, class hierarchy, wrap_kvs/codec machinery deep dive, ranked tech debt. Start here for refactors. |
| issues_and_discussions.md | GitHub issues/discussions themes, known limitations, open design questions |
| dol_issues_report.md | Prioritized issue triage + wave-by-wave tackle order |
| dol_issue16_design.md | Issue #16 design: optional key-path write-through / autovivification — opt-in create_missing, contextual per-level factory, the path_set_writeback boundary engine + persistent-store write-back protocol, scoped plan. Design-only (no code yet). |
| dol_issue18_design.md | Issue #18 design: self-not-wrapped delegation trap — wrapped_self (shipped) now, is-a wrapping (deferred, major) later. |
| dol_issue83_design.md | Issue #83 design study: the inverse of #18 — a delegated method receives the unmapped key. Two delegation routes (a fix for one is a no-op on the other), a 13-package census (mostly latent; 12 claims refuted), and options A–F with verified costs: wrapped_self has its own silent hole (degrades with no live strong reference), chain-walking free functions break on non-Store layers, and the only form correct by construction is routing the capability through __getitem__ as a sibling store. §5 is the carry-forward list for a future redesign. |
| dol_issue86_design.md | Discussion #86 design study: Option G — spec-carried boundary codecs on a flat proxy (wrapt lessons, KT/VT-annotated interface specs, flatten-and-compile codec stacks). Prototype in dol/_interface_wrap.py (private, experimental). Headline: is-a does not fix #83 for backend-direct method bodies — F and G serve disjoint populations; codec laws (§2.5) are the boundary invariant's fine print; flat-model guarantees are scoped to pure-codec stacks (filters/caches still nest). Open question 0: who owns the wrap_kvs endgame. |
| dol_issue10_design.md | Issues #10 + #2 (paired) design: recursive wrapping of nested stores (recursive_wrap) + a flat KvReader/KvPersister view (flat_store), sharing one (path,key,value) descent frontier and reusing the #16 path_set_writeback engine. Load-bearing fix: the recursion read-surface and the write-back boundary must be different objects (naive boundary=self infinite-loops). Model-2 read + write-into-existing in scope; persistent creation deferred to P3. Design-only (no code yet). |
| frontend_dol_ideas.md | zoddal design: TypeScript KV interface, adapters, Zod bridge, zod-collection-ui integration |
A local-only ecosystem inventory (gitignored) lives in
misc/data/: dol's 76 dependents, their usages (file:line), a pre-PR test-gate order + runner, and thewrap_kvsblast-radius scan. Regenerate with the scripts there.
Dev skills (.claude/skills/, for working on dol):
dol-dev-wrap-kvs— thewrap_kvs/store_decorator/Store.wrapmachinery: the signature-conditioning rule,FirstArgIsMapping, the delegation architecture +self/ signature traps (#18/#6), and the mandatory dependents test-gate. Read before touchingtrans.py/base.py.dol-dev-portability— Windows/POSIX landmines for path/key code.
Consumer skills (.claude/skills/, for using dol):
dol-store-building— wrap any backend behind a dict interface:wrap_kvs, codecs, the ready-made file stores,filt_iter, caching, and self-aware transforms.
Commands (.claude/commands/): /new-store, /add-codec, /explain-store — interactive scaffolds.
wrap_kvs+selfinside methods: When awrap_kvs-decorated class usesself[k]in its own methods,selfis the unwrapped inner store, so transforms are bypassed (Issue #18 — delegation architecture). Blessed fix:from dol import wrapped_selfand writewrapped_self(self)[k]to reach the outer, transform-applying store (climbs to the outermost wrapper for stacked/Pipewraps; a safe no-op on directStoresubclasses). The oldersq(self)[k]re-wrap still works. A structural fix (is-a wrapping) is proposed inmisc/docs/dol_issue18_design.md.clear()is disabled onKvPersister. Callensure_clear_to_kv_store(store)to re-enable.- No async support in core. Use synchronous wrappers for async backends (thread pool, etc.).
- Transforms wanting the store: a transform is called
f(self, data)only if its first param is namedself/store/mappingand it has ≥2 required params; otherwisef(data). Mark explicitly withFirstArgIsMapping(f). (bytes.decodeasobj_of_datanow works — Issue #9 fixed.) - Windows paths: cross-platform fixes landed (Issues #40/#52/#58 resolved, CI green). See the
dol-dev-portabilityskill before touching path/key code. - Key-path write-through / autovivification (Issue #16): by default, writing a deep path whose intermediates are missing raises
KeyError(unchanged), and — a pre-existing gotcha — writing an existing deep path on a copy-semantics/persistent store (Files, anywrap_kvswith a non-identityobj_of_data) silently mutates a detached copy and is lost. Opt in withcreate_missing=Trueonadd_path_access/KeyPath(or theautoviv(store)alias): missing intermediates are created via a contextualmk_missing(ctx)factory and every write is persisted through a single boundary write-back — which also closes the silent-loss. Creation is announced viawarnings.warn(passon_create=Noneto silence); unsafe cases raisePathCreationError/PathWritebackError. Engine lives in the dependency-free leafdol/_paths_core.py(path_set_writeback);path_setthere also fixes the old factory-not-propagated bug. Persistent store-of-stores (nestedFiles) is out of scope → deferred to #10. Full design:misc/docs/dol_issue16_design.md.