store: Where sync state lives#

A store keeps every scope, its records, and the change feed. Use SQLiteStore to keep state between runs, and MemoryStore in tests.

The storage interface every store implements.

A store only keeps data. Every sync rule lives in Replica, so each store implements the same small set of typed operations and nothing more.

class calendaring_sync.store.base.StateStore[source]#

Bases: ABC

Where the core keeps each scope, its records, and the change feed.

Every operation runs inside transaction(): one that only reads runs inside any transaction, and one that writes needs a transaction opened with write=True. One thread’s transaction runs to the end before another’s starts, so a store is safe to share between threads.

Besides the errors each method lists, every operation, transaction() and close() can raise:

No exception of the storage underneath a store reaches the caller.

The public methods check that they run inside a transaction of the right kind, then call a method of the same name with a leading underscore. A store implements those, along with _begin(), _commit() and _rollback(), and needs no checks of its own. It turns the errors of the storage it uses into store errors by overriding _store_error(). Its __init__ calls super().__init__() first.

append_entry(scope_id: str, kind: FeedEntryKind, local_key: int | None, object_id: str | None, at: datetime) → FeedEntry[source]#

Write the next change feed entry and return it.

Its number is one more than the last number written. Runs inside a write transaction.

Raises:
close() → None[source]#

Close the store. Every later transaction raises StoreError.

Closing a closed store does nothing.

Raises:
  • TransactionError – If this thread has a transaction open.

  • StoreError – If the store couldn’t release what it holds open. It counts as closed anyway.

delete_record(local_key: int) → None[source]#

Delete the record with this local key.

Runs inside a write transaction.

Raises:
delete_scope(scope_id: str) → None[source]#

Delete the scope and every record in it.

Runs inside a write transaction.

Raises:
entries(after: int, limit: int) → list[FeedEntry][source]#

Return up to limit kept entries numbered above after, in order.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

feed_bounds() → tuple[int, int][source]#

Return the number the feed is trimmed through, and the last number written.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

find_record(scope_id: str, object_id: str) → Record | None[source]#

Return the scope’s record with this object id, or None.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

get_record(local_key: int) → Record | None[source]#

Return the record with this local key, or None.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

get_scope(scope_id: str) → Scope | None[source]#

Return the scope with this id, or None.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

insert_record(record: Record) → int[source]#

Insert a new record and return the local key it got.

The record’s own local_key is ignored. A key is never given out twice, even after the record it went to was deleted. Runs inside a write transaction.

Raises:
put_scope(scope: Scope) → None[source]#

Insert the scope, or replace the one with its id.

Runs inside a write transaction.

Raises:
removed_records(through: int) → list[Record][source]#

Return the removed records no feed entry after through is about.

through is the number of a change feed entry. Each record keeps the number of the last entry about it, so these are the records removed by that entry or an earlier one, ordered by local key. Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

scope_records(scope_id: str) → list[Record][source]#

Return every record in the scope, ordered by local key.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

scopes() → list[Scope][source]#

Return every scope, ordered by id.

Runs inside any transaction.

Raises:

TransactionRequiredError – If no transaction is open in this thread.

transaction(*, write: bool = True) → Iterator[None][source]#

Run the operations inside the block as one transaction.

Everything the block wrote is kept if it ends normally, and nothing is kept if it raises.

Parameters:

write – False for a block that only reads. Such a block can’t write, and a store may run it without blocking other processes’ writes.

Raises:
  • TransactionError – If a transaction is already open in this thread.

  • StoreBusyError – If another process kept the store busy for too long.

  • StoreError – If the store was closed, or can’t start or keep the transaction.

trim_entries(through: int) → None[source]#

Delete every entry numbered at or below through.

feed_bounds() reports the highest through trimmed so far. Runs inside a write transaction.

Raises:
update_record(record: Record) → None[source]#

Replace the record with the same local key.

Runs inside a write transaction.

Raises:

A store that keeps everything in memory, for tests.

class calendaring_sync.store.memory.MemoryStore[source]#

Bases: StateStore

A store held in memory and lost when the process ends.

A write transaction works on a copy of the data, which replaces the data only when the transaction ends normally. Lookups scan every record, so it suits test data, not large stores. Meant for tests; use SQLiteStore to keep state.

A store kept in a SQLite database file.

class calendaring_sync.store.sqlite.SQLiteStore(path: str | PathLike[str], *, timeout: float = 5.0)[source]#

Bases: StateStore

A store in a SQLite database file, shared between threads and processes.

One connection serves every thread, one transaction at a time. Writes take SQLite’s write lock for their whole transaction, so two processes on one file never interleave. Reads don’t take that lock, but an open read still makes another process’s commit wait, so the store keeps reads short. A write waits up to timeout seconds for another process’s transaction to end.

Setting the file to SQLite’s WAL journal mode lets one process’s reads and another’s writes overlap. The mode is kept in the file, so set it once with PRAGMA journal_mode = WAL; WAL doesn’t work on a network filesystem.

Parameters:
  • path – The database file. It’s created, with the schema, if missing.

  • timeout – Seconds to wait for another process before giving up.

Raises:

Last updated on 2026-10-13.