session: One sync of one scope#
A session, one sync of one scope: it ingests what changed, then pushes.
- class calendaring_sync.session.Session(replica: Replica, scope_id: str, *, guarded_by_version: bool, unique_logical_key: bool)[source]#
Bases:
objectOne sync of one scope, opened with
session().A session ingests every page first, then the adapter pushes your pending edits: for each record
to_push()lists, itclaimsit, sends it, andsettlesit with how the push ended.Every method raises
TypeErrorfor an argument of the wrong type, before it changes anything.Examples
The adapter pushes an edit after ingesting what changed on the server, here nothing:
>>> from calendaring_sync import Applied >>> key = replica.find_record("work", "a").local_key >>> replica.change(key, b"Team meeting, moved", fingerprint="fp-a2") >>> with replica.session( ... "work", fingerprint_scheme="sha256", media_type="text/plain" ... ) as session: ... session.ingest(ChangeSet("work", "t1", "t2", {})) ... for record in session.to_push(): ... claimed = session.claim(record.local_key) ... # The adapter sends claimed.edit to the server here. ... session.settle(claimed, Applied()) True >>> replica.record(key).state, replica.record(key).content (<RecordState.SYNCED: 'synced'>, b'Team meeting, moved')
- claim(local_key: int, *, object_id: str | None = None) Record | None[source]#
Mark a pending edit in flight, so no other process pushes it.
Send the record this returns, then report how the push ended with
settle(). Runs in a write transaction of its own; when two processes claim one record, only one gets it.- Parameters:
local_key – The record to push.
object_id – For a create the server takes ids for, the id it’s about to be sent under. It’s stored with the claim, so the create is recognized by it even if the answer never arrives. An id a removed record holds passes to this one.
- Returns:
The claimed record, or
Nonewhen it no longer holds a pending edit to push: another process claimed it, the edit was discarded or resolved, or it waits for a version.- Raises:
TransactionError – If a store transaction is already open in this thread.
SessionError – If the session hasn’t ingested its last page, or has ended.
DuplicateObjectError – If a record that isn’t removed holds
object_id.OtherScopeError – If the record belongs to another scope.
ObjectIdAssignedError – If
object_idis given for a record that has one.LoneSurrogateError – If a string holds a lone surrogate.
Examples
A second claim of the same record gets nothing:
>>> key = replica.find_record("work", "a").local_key >>> replica.change(key, b"Team meeting, moved", fingerprint="fp-a2") >>> with replica.session( ... "work", fingerprint_scheme="sha256", media_type="text/plain" ... ) as session: ... session.ingest(ChangeSet("work", "t1", "t2", {})) ... claimed = session.claim(key) ... claimed.state, session.claim(key) (<RecordState.IN_FLIGHT: 'in_flight'>, None)
- ingest(change_set: ChangeSet) None[source]#
Ingest one change set, or one page of it, in one transaction.
The core applies each object in it: a new object gets a record, a changed one gets the new data, and a removed one is marked removed. An object whose fingerprint, containers and logical key didn’t change only gets its version and the server’s content, and isn’t reported. A truncated page of an incremental change set moves the token to its checkpoint. A full listing moves the token only on its last page, which also removes every record the listing never mentioned. A complete change set, or the last page, also settles every push still in flight from what the server reported, and one it doesn’t mention is pending again. If anything fails, nothing changes.
- Raises:
TransactionError – If a store transaction is already open in this thread.
StaleChangeSetError – If the change set was fetched from a token the scope no longer has, or is a later page of a listing with another token, such as one another session has replaced. The session goes back to idle, so the adapter can fetch again and ingest from the start in the same session.
SessionError – If the session has ended, already ingested its last page, has started pushing, or the page is a listing page in an incremental session or the other way round.
OtherScopeError – If the change set is for another scope.
ListingRequiredError – If an incremental change set arrives for a scope with no token.
UnknownVersionError – If an
Unchangedentry names a record with no version.
- settle(claimed: Record, outcome: Applied | Rejected | Gone | LogicalKeyTaken | Unknown) bool[source]#
Record how the push of a claimed record ended.
Runs in a write transaction of its own, and writes a change feed entry for every outcome but
Unknown. If another process already settled this push, for example by ingesting while it was in flight, the outcome only counts while the record still holds the same edit and no one has claimed it again.- Parameters:
claimed – The record
claim()returned.outcome – How the push ended.
- Returns:
Trueif the store now agrees with the outcome,Falseif the push had already been settled another way and nothing was written.- Raises:
TransactionError – If a store transaction is already open in this thread.
SessionError – If the session has ended.
DuplicateObjectError – If
Appliednames an id a record that isn’t removed holds.ValueError – If
claimedisn’t a record this session’sclaim()returned, or the outcome is one no push of its edit can have:Gonefor a create,LogicalKeyTakenfor a delete,Appliedwith an id for a delete, orAppliedwithout one for a create that has none.
Examples
The server refused the write, so the edit is pending again:
>>> from calendaring_sync import Rejected >>> key = replica.find_record("work", "a").local_key >>> replica.change(key, b"Team meeting, moved", fingerprint="fp-a2") >>> with replica.session( ... "work", fingerprint_scheme="sha256", media_type="text/plain" ... ) as session: ... session.ingest(ChangeSet("work", "t1", "t2", {})) ... session.settle(session.claim(key), Rejected()) True >>> replica.record(key).state <RecordState.PENDING: 'pending'>
- property state: SessionState#
Where the session is.
- to_push() list[Record][source]#
Return the pending edits ready to push, by local key.
With
guarded_by_version, a change or delete of a record with no version waits for an ingest that gives it one, and isn’t listed. Runs in a read transaction of its own.- Raises:
TransactionError – If a store transaction is already open in this thread.
SessionError – If the session hasn’t ingested its last page, or has ended.
Examples
>>> key = replica.find_record("work", "a").local_key >>> replica.change(key, b"Team meeting, moved", fingerprint="fp-a2") >>> with replica.session( ... "work", fingerprint_scheme="sha256", media_type="text/plain" ... ) as session: ... session.ingest(ChangeSet("work", "t1", "t2", {})) ... [record.local_key for record in session.to_push()] == [key] True