A simple key/value store with multiple backends.
Project description
🥫 Preserve — A simple Python key/value store with multiple backends
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.
Contents
Installation
pip install preserve
# or, with uv:
uv add preserve
Install from source:
pip install git+https://github.com/evhart/preserve#egg=preserve
Requirements: Python ≥ 3.9, pydantic v2, python-dotenv.
Optional — MongoDB backend:
pip install preserve[mongo]
# or
uv add "preserve[mongo]"
Quick Start
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"
Backends
| 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)
Multi-Collection API
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'}
Type Coercion
Connectors use Pydantic v2 to coerce retrieved values to a specific type. Coercion is applied on read, not on write.
Per-connector defaults
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)
Per-key mapping
with SQLite(filename=":memory:", key_types={"score": float, "count": int}) as db:
db["score"] = "7.5"
print(db.get("score")) # 7.5 (str → float)
Per-call override
with SQLite(filename=":memory:") as db:
db["n"] = 5
print(db.get("n", value_type=float)) # 5.0
Per-collection override (multi-connector)
with 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.0
Note: Pydantic v2 uses strict validation for primitives by default.
int → floatandstr → int(when the string is a valid integer) work;int → strdoes not.
Caching
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.
Decorator
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 cache
The 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 ** 2
Context manager
from 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__
Environment variables
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 |
CLI
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.db
Running Tests
uv sync --group dev
uv run pytest
Test coverage report:
uv run pytest --cov=preserve --cov-report=term-missing
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file preserve-2.0.1.tar.gz.
File metadata
- Download URL: preserve-2.0.1.tar.gz
- Upload date:
- Size: 136.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f1023046c54942941ffbbe767127ee1137c25bcd287c7af3218d52e968cba28
|
|
| MD5 |
44e5539955a7b60676084054283d4678
|
|
| BLAKE2b-256 |
06e447b7a6274282cb2592ecca31d105b6453f77be701d4cb6715a102a319755
|
File details
Details for the file preserve-2.0.1-py3-none-any.whl.
File metadata
- Download URL: preserve-2.0.1-py3-none-any.whl
- Upload date:
- Size: 34.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.10.10 {"installer":{"name":"uv","version":"0.10.10","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4d5dd24cd790491a9eeea9def993a71cbf1b5af25ea46cd4480fbd6ca6836633
|
|
| MD5 |
307cdae09ca9a7b993e276309056f2ad
|
|
| BLAKE2b-256 |
a3ec32acdf0219468d64cae1c2cdbdfdb5fe2cf2a268be6ec9992dc33492e348
|