coopy¶
Persistence for plain Python objects. No database, no ORM, no schema: wrap an object once, keep calling its methods, and it survives restarts.
What is it?¶
Most applications keep their data in a database and copy it into objects when they need it. coopy turns this around: your objects live in memory, and the disk is just a log of what happened to them. The technique is called object prevalence.
coopy wraps your object in a proxy. Every time you call a method that changes state, the call itself (method name and arguments) is appended to a journal on disk before the result is returned. Every so often, coopy saves a snapshot of the whole object so the journal stays short. When your program starts again, coopy loads the latest snapshot and replays the calls made after it: same calls, same order, same state.
your code ──► proxy ──► your object (in memory)
│
└──► journal: deposit("alice", 100)
transfer("alice", "bob", 30)
...
restart: latest snapshot ──► replay the journal ──► the same object
A complete example¶
A bank whose accounts survive a restart. Bank is an ordinary class; the
only coopy-specific line is coopy.open:
import tempfile
import coopy
from coopy.decorators import readonly
class Bank:
"""Plain Python: no base class, no ORM, no schema."""
_clock: coopy.Clock # injected by coopy
def __init__(self) -> None:
self.accounts: dict[str, int] = {}
self.history: list[tuple[str, str, int]] = []
def deposit(self, name: str, amount: int) -> None:
self.accounts[name] = self.accounts.get(name, 0) + amount
self.history.append((self._clock.now().isoformat(), name, amount))
def transfer(self, source: str, target: str, amount: int) -> None:
if self.accounts.get(source, 0) < amount:
raise ValueError("insufficient funds")
self.accounts[source] -= amount
self.accounts[target] = self.accounts.get(target, 0) + amount
@readonly
def balance(self, name: str) -> int:
return self.accounts.get(name, 0)
data = tempfile.mkdtemp()
# First run: every method call is appended to a journal on disk.
with coopy.control(coopy.open(Bank, data)) as bank:
bank.deposit("alice", 100)
bank.transfer("alice", "bob", 30)
print(bank.balance("alice"), bank.balance("bob")) # 70 30
# Later (a restart, a crash, a deploy): the same object comes back.
with coopy.control(coopy.open(Bank, data)) as bank:
print(bank.balance("alice"), bank.balance("bob")) # 70 30
print(coopy.control(bank).recovery_report.replayed, "calls replayed") # 2 calls replayed
Run it with python docs/examples/quickstart.py. Install coopy with
python -m pip install coopy (Python 3.10 or newer); see Installation.
Why coopy¶
Your code stays yours. Domain classes are plain Python: no base class, no mapping layer, no queries. Type checkers see the proxy as your class.
Memory speed. Reads are attribute access. A query is a Python expression over your own objects.
History for free. The journal records every change as a named command with its arguments and recorded time. Rebuild any earlier moment or inspect it with the command-line tool.
Safe by default. Calls are serialized under a lock, a data directory has one writer, recovery stops on corruption instead of guessing, and
durability="sync"acknowledges a call only after it is on disk.Grows with you. Snapshots, pruning, migrations, signed storage, JSON storage, async callers,
@transactionalrollback and experimental Primary/replica replication.
The one rule: replay must give the same result¶
Recovery runs your methods again, so they must be deterministic:
Change state only by calling methods on the proxy.
Use
self._clock.now()instead ofdatetime.now(), andself._envfor random numbers, UUIDs and tokens. coopy records the values and returns the same ones on replay (see Recorded clock values and Recorded random numbers, UUIDs and tokens).Do network calls, file reads and other side effects outside, and pass the results in as arguments.
coopy checks your methods for the usual mistakes when it opens a system.
When not to use it¶
Your data must fit in memory, one process writes to a data directory, and
snapshots and journals use pickle by default, so only load data you
trust. If you need ad-hoc SQL from other teams or data larger than RAM, use a
database.
Where next¶
Persistence and recovery tutorial: open, checkpoint, close and reopen a system step by step, then try the interactive todo-list demo.
Persistence and recovery guide: the durability and recovery contract.
Snapshots, Migrating stored data and Command-line tool for running coopy in production.
Typed domain objects and persistence controls and Generated API reference for typed APIs and the full reference.
coopy is released under the BSD License. Source and issues: https://github.com/felipecruz/coopy