Skip to main content

A simple key/value store with multiple backends.

Project description

🥫 Preserve — A simple Python key/value store with multiple backends

CI PyPI Python License

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 → float and str → int (when the string is a valid integer) work; int → str does 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


Download files

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

Source Distribution

preserve-2.0.1.tar.gz (136.5 kB view details)

Uploaded Source

Built Distribution

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

preserve-2.0.1-py3-none-any.whl (34.0 kB view details)

Uploaded Python 3

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

Hashes for preserve-2.0.1.tar.gz
Algorithm Hash digest
SHA256 3f1023046c54942941ffbbe767127ee1137c25bcd287c7af3218d52e968cba28
MD5 44e5539955a7b60676084054283d4678
BLAKE2b-256 06e447b7a6274282cb2592ecca31d105b6453f77be701d4cb6715a102a319755

See more details on using hashes here.

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

Hashes for preserve-2.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4d5dd24cd790491a9eeea9def993a71cbf1b5af25ea46cd4480fbd6ca6836633
MD5 307cdae09ca9a7b993e276309056f2ad
BLAKE2b-256 a3ec32acdf0219468d64cae1c2cdbdfdb5fe2cf2a268be6ec9992dc33492e348

See more details on using hashes here.

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