store: Where sync state lives#
Where the core keeps sync state.
The store, the storage interface it keeps its data in, and the stores that ship.
The store interface, and the storage interface a store keeps its data in.
A store only keeps data. Every sync rule lives in
Replica, and every rule about how a store
behaves, such as its transactions, its checks and its errors, lives in
StateStore. Where the data goes is a Storage, which
implements the same small set of typed operations and nothing more.
- class calendaring_sync.store.base.StateStore(storage: Storage)[source]#
Bases:
objectWhere the core keeps each scope, its records, and the change feed.
It keeps them in a
Storage, and adds every rule a store follows, whichever storage it uses. Every operation runs insidetransaction(): one that only reads runs inside any transaction, and one that writes needs a transaction opened withwrite=True. A store is safe to share between threads: one write transaction runs at a time, and where the storage setsreads_beside_writes, transactions that only read run in other threads meanwhile instead of waiting for it.Besides the errors each method lists, every operation,
transaction()andclose()can raise:TypeError, for an argument of the wrong type, before anything is read or written; a record or scope to write is checked field by fieldStoreError, if the store can’t read or write its dataUnexpectedStoreError, for any other failure inside the storage, which is a bug
No exception of the storage underneath reaches the caller.
- Parameters:
storage – Where the store keeps its data.
- 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.Transactions open in other threads end first: closing waits for them. 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 copy or its local 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 copy in a conflict exactly when it’s in conflict, and no synced copy 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 copy 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 always returns 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 beside a write in another thread or process.- 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.
- class calendaring_sync.store.base.Storage[source]#
Bases:
ABCWhere a
StateStorekeeps its data.To keep sync state somewhere new, such as in another database, implement this and a
StorageTransaction, and pass the storage toStateStore. The store adds every check, transaction rule, thread rule and error a store needs, so a storage only reads and writes.- abstractmethod begin(*, write: bool) StorageTransaction[source]#
Start a transaction and return it.
The store runs one write transaction at a time, and transactions that only read beside it only if
reads_beside_writesis set. If this raises, it leaves nothing open.
- close() None[source]#
Release what the storage holds open.
The store calls it once, after every transaction ended.
- error(exc: Exception) StoreError | None[source]#
Return the store error for an exception the storage raised, or
None.Turn the failures of whatever the storage uses, such as a file it can’t open, into
StoreErroror one of its subclasses. The store reports any exception this returnsNonefor as a bug in the storage.
- reads_beside_writes: bool = False#
Whether a transaction that only reads can run while another thread’s write transaction is open. With
False, every transaction waits for the one before it to end. Set it toTrueonly where a read sees the data as the last commit left it until it ends, whatever a write does meanwhile; reads in other threads then run at the same time too, sobegin()and the transactions it returns must be safe for that.
- class calendaring_sync.store.base.StorageTransaction(*, write: bool)[source]#
Bases:
ABCOne transaction on a
Storage, whichStorage.begin()returns.It holds whatever its transaction needs, such as a connection or a view of the data, so a storage keeps no state per thread. Its data operations are the ones
StateStoreoffers, without the checks:StateStorecalls them only inside this transaction, after it checked the arguments, the records and whether the transaction may write, and turns every exception they raise into a store error. Each returns what theStateStoremethod of the same name returns.- Parameters:
write – Whether the transaction may write.
- abstractmethod append_entry(scope_id: str, kind: FeedEntryKind, local_key: int | None, object_id: str | None, at: datetime) FeedEntry[source]#
Do what
StateStore.append_entry()does, without its checks.
- abstractmethod delete_record(local_key: int) None[source]#
Do what
StateStore.delete_record()does, without its checks.
- abstractmethod delete_scope(scope_id: str) None[source]#
Do what
StateStore.delete_scope()does, without its checks.
- abstractmethod entries(after: int, limit: int) list[FeedEntry][source]#
Do what
StateStore.entries()does, without its checks.
- abstractmethod feed_bounds() tuple[int, int][source]#
Do what
StateStore.feed_bounds()does, without its checks.
- abstractmethod find_by_logical_key(scope_id: str, logical_key: str) list[Record][source]#
Do what
StateStore.find_by_logical_key()does, without its checks.
- abstractmethod find_record(scope_id: str, object_id: str) Record | None[source]#
Do what
StateStore.find_record()does, without its checks.
- abstractmethod get_record(local_key: int) Record | None[source]#
Do what
StateStore.get_record()does, without its checks.
- abstractmethod get_scope(scope_id: str) Scope | None[source]#
Do what
StateStore.get_scope()does, without its checks.
- abstractmethod insert_record(record: Record) int[source]#
Do what
StateStore.insert_record()does, without its checks.
- abstractmethod put_scope(scope: Scope) None[source]#
Do what
StateStore.put_scope()does, without its checks.
- abstractmethod removed_records(through: int) list[Record][source]#
Do what
StateStore.removed_records()does, without its checks.
- abstractmethod rollback() None[source]#
Discard what the transaction wrote, and end it.
It’s also called after
commit()raised, so it ends whatever the transaction still holds open.
- abstractmethod scope_records(scope_id: str) list[Record][source]#
Return every record of the scope, ordered by local key.
- scope_records_in(scope_id: str, states: frozenset[RecordState]) list[Record][source]#
Return the scope’s records in one of
states, ordered by local key.This filters what
scope_records()returns. A storage that can filter where it keeps its records, such as with an index, overrides it. It never gets an empty set.
- abstractmethod scopes() list[Scope][source]#
Do what
StateStore.scopes()does, without its checks.
- abstractmethod trim_entries(through: int) None[source]#
Do what
StateStore.trim_entries()does, without its checks.
- abstractmethod update_record(record: Record) None[source]#
Do what
StateStore.update_record()does, without its checks.
- write#
Whether the transaction may write.
A store that keeps everything in memory, and can hand its data to a keeper to save.
- class calendaring_sync.store.memory.MemoryKeeper[source]#
Bases:
ABCKeeps a
MemoryStore’s data somewhere else, such as in a file.Pass one to
MemoryStore. It loads the data once, when the store opens, and saves it each time a write commits; reads come from memory. Ifsave()raises, the write fails and the store keeps the data it saved last, as long assave()doesn’t stop halfway: write to a new file and then replace the old one, for example.What
MemoryDataholds can change between releases, so data a keeper saved with one release may not load in another. Keep sync state that has to outlive an upgrade inSQLiteStore, whose file a newer release reads.- error(exc: Exception) StoreError | None[source]#
Return the store error for an exception
load()orsave()raised.Turn the failures of the storage underneath, such as a full disk, into
StoreError; anything this returnsNonefor is reported as a bug.
- abstractmethod load() MemoryData | None[source]#
Return the data saved last, or
Nonefor a store with nothing saved yet.
- abstractmethod save(data: MemoryData) None[source]#
Keep the data a write commits, in place of what was saved before.
- class calendaring_sync.store.memory.MemoryStore(*, keeper: MemoryKeeper | None = None)[source]#
Bases:
StateStoreA store held in memory, lost when the process ends unless a keeper saves it.
Each transaction works on its own view of the data: a read on the data the last write committed, a write on a copy of it, which becomes the committed data only when the transaction ends normally. So a read in one thread runs while another thread writes, and sees the data as it was when the read started. Lookups scan every record, so it suits test data and small stores; use
SQLiteStorefor large ones.- Parameters:
keeper – Where to save the data, such as a file, so the next process can load it.
Nonekeeps it only in memory.- Raises:
StoreError – If the keeper can’t load the data, as its
error()says.
Examples
A keeper that saves the data as pickled bytes, and a second store loaded from them:
>>> import pickle >>> from calendaring_sync import Changed, ChangeSet, MemoryKeeper, MemoryStore, Replica >>> class Pickled(MemoryKeeper): ... def __init__(self, saved=None): ... self.saved = saved ... def load(self): ... return pickle.loads(self.saved) if self.saved else None ... def save(self, data): ... self.saved = pickle.dumps(data) >>> keeper = Pickled() >>> listing = ChangeSet( ... "work", None, "t1", {"a": Changed("v1", "fp-a", b"Team meeting")}, full_listing=True ... ) >>> with Replica(MemoryStore(keeper=keeper)).session( ... "work", fingerprint_scheme="sha256", media_type="text/plain" ... ) as session: ... session.ingest(listing) >>> store = MemoryStore(keeper=Pickled(keeper.saved)) >>> Replica(store).find_record("work", "a").content b'Team meeting'
- class calendaring_sync.store.memory.MemoryData[source]#
Bases:
objectEverything a
MemoryStoreholds, as one object.A
MemoryKeepersaves and loads it whole. It’s made of plain containers holding the library’s frozen dataclasses, so it pickles; what’s inside isn’t part of the API.
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.
Writes take one connection, one transaction at a time, and SQLite’s write lock for their whole transaction, so two processes on one file never interleave. Reads take a second connection, one at a time, so a read in one thread doesn’t wait for another thread’s write. A database at
":memory:"or an empty path belongs to one connection, so it has only that one, and its transactions take turns.In SQLite’s default journal mode, a commit locks reads out of the file. So it waits for the reads open in this process, and reads in another process make it wait up to
timeoutseconds, then raiseStoreBusyError; the store keeps reads short for that. A write also keeps what it changes in memory until it commits, so that reads aren’t locked out before then: a full listing takes about as much memory again as its change set holds. In WAL mode reads and a commit don’t lock each other out, so a commit waits for no read and a write keeps nothing extra in memory. The mode is kept in the file, so set it once withPRAGMA 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:
TypeError – If
pathortimeouthas the wrong type.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.