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:
ABCWhere 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 withwrite=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()andclose()can raise:StoreError, if the store can’t read or write its dataUnexpectedStoreError, for any other failure inside the store, which is a bug
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__callssuper().__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:
TransactionRequiredError – If no transaction is open in this thread.
TransactionPermissionError – If the transaction only reads.
- 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:
TransactionRequiredError – If no transaction is open in this thread.
TransactionPermissionError – If the transaction only reads.
- delete_scope(scope_id: str) None[source]#
Delete the scope and every record in it.
Runs inside a write transaction.
- Raises:
TransactionRequiredError – If no transaction is open in this thread.
TransactionPermissionError – If the transaction only reads.
- entries(after: int, limit: int) list[FeedEntry][source]#
Return up to
limitkept entries numbered aboveafter, 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_by_logical_key(scope_id: str, logical_key: str) list[Record][source]#
Return the scope’s records with this logical key, ordered by local key.
A record matches if its synced side or its pending edit has the key. Several records can share one. 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_keyis ignored. A key is never given out twice, even after the record it went to was deleted. Runs inside a write transaction.- Raises:
RecordValueError – If the record holds what its state can’t. A record holds an edit exactly when it’s pending, in flight or in conflict, the server’s side of a conflict exactly when it’s in conflict, and no synced content exactly when its edit is a create. An edit holds a copy of the object unless it’s a delete, a conflict’s edit and server’s side match its kind, and only a refused create’s conflict names a holder. The record is checked first, so this is raised outside a transaction too.
TransactionRequiredError – If no transaction is open in this thread.
TransactionPermissionError – If the transaction only reads.
UnknownScopeError – If the record’s scope doesn’t exist.
DuplicateObjectError – If another record in the scope has the record’s object id.
- put_scope(scope: Scope) None[source]#
Insert the scope, or replace the one with its id.
Runs inside a write transaction.
- Raises:
TransactionRequiredError – If no transaction is open in this thread.
TransactionPermissionError – If the transaction only reads.
- removed_records(through: int) list[Record][source]#
Return the removed records no feed entry after
throughis about.throughis 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, *, states: frozenset[RecordState] | None = None) list[Record][source]#
Return the scope’s records, ordered by local key.
Runs inside any transaction.
- Parameters:
scope_id – The scope to read.
states – Only the records in one of these states;
Nonefor every record. An empty set matches none, without asking the store, since not every database takes an empty list.
- 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 –
Falsefor 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 highestthroughtrimmed so far. Runs inside a write transaction.- Raises:
TransactionRequiredError – If no transaction is open in this thread.
TransactionPermissionError – If the transaction only reads.
- update_record(record: Record) None[source]#
Replace the record with the same local key.
Runs inside a write transaction.
- Raises:
RecordValueError – If the record holds what its state can’t, as for
insert_record().TransactionRequiredError – If no transaction is open in this thread.
TransactionPermissionError – If the transaction only reads.
UnknownRecordError – If no record has the record’s local key.
DuplicateObjectError – If another record in the scope has the record’s object id.
A store that keeps everything in memory, for tests.
- class calendaring_sync.store.memory.MemoryStore[source]#
Bases:
StateStoreA 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
SQLiteStoreto 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:
StateStoreA 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
timeoutseconds 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:
StoreVersionError – If a newer calendaring-sync wrote the file.
StoreBusyError – If another process kept the file busy past
timeout.StoreError – If the file can’t be opened or isn’t a store.