Preserve is a simple, dict-like key/value store for Python 3.9+ that supports multiple storage backends (SQLite, in-memory, shelf, MongoDB) with a unified API. It also provides a response-caching decorator and context manager for memoising expensive function calls.
pip install preserve
# or, with uv:
uv add preserveInstall from source:
pip install git+https://github.com/evhart/preserve#egg=preserveRequirements: Python ≥ 3.9, pydantic v2, python-dotenv.
Optional — MongoDB backend:
pip install preserve[mongo]
# or
uv add "preserve[mongo]"The API mirrors a standard Python dict. Open a connector, use it as a dictionary, and close it (or use it as a context manager).
import preserve
# Open an SQLite-backed store (file persists across runs)
with preserve.open("sqlite", filename="my_store.db") as db:
db["user:1"] = {"name": "Alice", "score": 42}
print(db["user:1"]) # {'name': 'Alice', 'score': 42}
print("user:1" in db) # True
del db["user:1"]
# Open an in-memory store (ephemeral)
with preserve.open("memory") as db:
db["temp"] = [1, 2, 3]
# Open via URI
with preserve.from_uri("sqlite:///my_store.db") as db:
db["key"] = "value"| Scheme | Class | Notes |
|---|---|---|
sqlite |
SQLite |
Persisted JSON in SQLite; supports :memory: |
memory |
Memory |
In-process dict; lost when closed |
shelf |
Shelf |
Python shelve file |
mongodb |
Mongo |
Requires pymongo |
List available backends at runtime:
from preserve.preserve import connectors
for c in connectors():
print(c.scheme())Register a third-party connector:
from preserve import Preserve
Preserve.register(MyCustomConnector)open_multi / from_uri_multi return a MultiConnector that maps collection names to individual stores (e.g. one SQLite table per collection, one file per Shelf collection).
import preserve
with preserve.open_multi("sqlite", filename="app.db") as db:
db["users"]["alice"] = {"role": "admin"}
db["logs"]["2024-01-01"] = {"event": "login"}
# Same collection reference is stable
users = db["users"]
users["bob"] = {"role": "viewer"}
with preserve.from_uri_multi("sqlite:///app.db") as db:
print(db["users"]["alice"]) # {'role': 'admin'}Connectors use Pydantic v2 to coerce retrieved values to a specific type. Coercion is applied on read, not on write.
from preserve.connectors import SQLite
with SQLite(filename=":memory:", default_value_type=float) as db:
db["score"] = 9 # stored as int
print(db.get("score")) # 9.0 (coerced to float on read)with SQLite(filename=":memory:", key_types={"score": float, "count": int}) as db:
db["score"] = "7.5"
print(db.get("score")) # 7.5 (str → float)with SQLite(filename=":memory:") as db:
db["n"] = 5
print(db.get("n", value_type=float)) # 5.0with preserve.open_multi("sqlite", filename="app.db") as db:
typed = db.open("metrics", default_value_type=float)
db["metrics"]["latency"] = "12"
print(typed.get("latency")) # 12.0Note: Pydantic v2 uses strict validation for primitives by default.
int → floatandstr → int(when the string is a valid integer) work;int → strdoes not.
Preserve ships a cache decorator and Cache context manager for memoising function results. The cache key is derived from the function name and its arguments.
from preserve import cache
@cache(backend="sqlite", connector_kwargs={"filename": "cache.db"})
def fetch_data(url: str) -> dict:
... # expensive HTTP call
return {}
fetch_data("https://example.com/api") # computed and stored
fetch_data("https://example.com/api") # returned from cache
fetch_data("https://example.com/api", use_cache=False) # bypass cacheThe use_cache keyword argument is injected by the decorator; it is never passed through to the wrapped function.
Key customisation:
# Cache only on selected arguments
@cache(backend="sqlite", connector_kwargs={"filename": "cache.db"}, key=["user_id"])
def get_profile(user_id: int, noise: str = "") -> dict:
...
# Use a callable to compute the key
@cache(backend="sqlite", connector_kwargs={"filename": "cache.db"},
key=lambda user_id, **_: f"profile:{user_id}")
def get_profile(user_id: int) -> dict:
...Multi-collection backend:
@cache(multi=True, collection="results", backend="sqlite",
connector_kwargs={"filename": "cache.db"})
def compute(n: int) -> int:
return n ** 2from preserve import Cache
c = Cache(key="my_key", backend="sqlite", connector_kwargs={"filename": "cache.db"})
with c as ctx:
if ctx:
result = ctx.get() # cache hit
else:
result = expensive_call()
ctx.set(result) # write back on __exit__All cache() / Cache() defaults can be set via environment variables (loaded automatically from a .env file):
| Variable | Default | Description |
|---|---|---|
PRESERVE_CACHE_BACKEND |
sqlite |
Backend scheme |
PRESERVE_CACHE_URI |
— | Full URI (overrides backend + file) |
PRESERVE_CACHE_MULTI |
false |
Use multi-collection backend |
PRESERVE_CACHE_COLLECTION |
preserve_cache |
Collection name (multi only) |
PRESERVE_CACHE_FILE |
~/.local/share/preserve/preserve.db |
File path for file-backed backends |
Usage: preserve [OPTIONS] COMMAND [ARGS]...
🥫 Preserve — A simple Key/Value database with multiple backends.
Commands:
connectors List available connectors.
export Export a database to a different output.
header Show the first rows of a database.
Example:
preserve connectors
preserve export sqlite:///source.db sqlite:///dest.db
preserve header sqlite:///my_store.dbuv sync --group dev
uv run pytestTest coverage report:
uv run pytest --cov=preserve --cov-report=term-missing