adapters: Connecting to servers#
The adapter base classes, and the adapters that ship with calendaring-sync.
Each shipped adapter is a module of its own that needs the extra of the
same name. Importing this package imports no protocol’s client library;
import the adapter’s module, such as calendaring_sync.adapters.caldav,
to use it.
Base classes#
The base classes every adapter is built on.
Adapter holds the order of a sync, written once for every
protocol: open a session, read and ingest what changed,
then push the pending edits. An adapter subclasses it, fills in
how its protocol reads and sends, and adds SyncAdapter or
AsyncAdapter for the public methods.
Both variants run the same steps. A step is a generator that yields what
a client call returned, the answer itself from a synchronous client or a
coroutine from an asynchronous one, or an InStore for a call into
the store, and gets each result sent back. SyncAdapter
runs the steps as they come; AsyncAdapter awaits the coroutines
and runs the store calls as its description says.
- class calendaring_sync.adapters.base.Adapter(replica: Replica, scope_id: str)[source]#
Bases:
ABCSyncs one scope with a
Replica, for any protocol.A sync opens a session, reads what changed on the server and ingests it, then pushes the pending edits one at a time: claimed, sent, settled. When the server refuses the scope’s sync token, the scope is reset and listed in full in a new session. An adapter subclasses it together with
SyncAdapterorAsyncAdapter, which add the public methods.A subclass sets these class attributes, which open each session:
_fingerprint_scheme: the name of the adapter’s fingerprint scheme._media_type: the media type of the scope’s content._guarded_by_version:Trueif a push applies only while the object still has the version the edit was based on._unique_logical_key:Trueif no two objects in the scope can share a logical key._guard_covers_scope:Trueif the server checks a push against the whole scope rather than the object, so after one rejection every later push would be rejected too, and they wait for the next sync.
It writes these methods for its protocol, each a generator of
Stepsexcept_waits(),_new_object_id()and_edit_fields():_read()fetches what changed and ingests it._send()sends one claimed edit._waits()says whether a pending edit can’t be sent yet._new_object_id()picks the object id of a create._edit_fields()reads an edit’s fields from its content.
These names start with an underscore because your application calls the adapter’s public methods: a read or a send called on its own would skip the session that orders them.
- Parameters:
replica – Where the sync state is kept.
scope_id – The scope’s id.
- abstractmethod _edit_fields(content: bytes, *, create: bool) EditFields[source]#
Read an edit’s fields from its content, the same way a sync reads the server’s copy.
- Parameters:
content – The edit’s content.
create – Whether the edit is a create, which may need more of the content, such as a logical key.
- Raises:
ValueError – If the content can’t be an object of the scope.
- abstractmethod _new_object_id() str | None[source]#
Return an object id for a create, or
Noneif the server picks it.
- abstractmethod _read(session: Session, scope: Scope, *, full_listing: bool) Generator[Any, Any, ReadResult][source]#
Fetch what changed in the scope, ingest it, and say how the read ended.
Without a token in
scope.token, or withfull_listing, list every object; otherwise ask for the changes since the token. Ingest eachChangeSetwithyield InStore(session.ingest, (change_set,)).- Parameters:
session – The open session to ingest into.
scope – The scope as the store holds it.
full_listing – Whether the caller asked for a full listing.
- Returns:
ReadResult.INVALID_TOKENif the server refused the token, before anything was ingested;ReadResult.TRUNCATEDif the server cut the changes short;ReadResult.COMPLETEotherwise.
- abstractmethod _send(claimed: Record) Generator[Any, Any, Applied | Rejected | Gone | LogicalKeyTaken | Unknown][source]#
Send one claimed edit and return how it ended.
An error raised here reaches the caller and leaves the push in flight, since the request may have reached the server; the next complete ingest settles it.
- Parameters:
claimed – The record
claim()returned, holding the edit to send.
- abstractmethod _waits(record: Record) bool[source]#
Whether a pending edit can’t be sent yet, so it waits for a later sync.
- Parameters:
record – A record with a pending edit.
- edit_fields(content: bytes) EditFields[source]#
Read an object’s fields from its content, the same way a sync reads the server’s copy.
Use it for content your application made itself, such as a
Merge. Reads nothing from the store or the server.- Raises:
ValueError – If the content can’t be an object of the scope, as for a create.
- property needs_full_listings: bool#
Whether the server can lose changes without refusing the scope’s token.
If it’s
True, an incremental sync may miss a change or a removal, so schedule a sync withfull_listing=Truenow and then, such as once a day.Falseby default; an adapter whose server can lose changes says so.
- class calendaring_sync.adapters.base.AsyncAdapter(replica: Replica, scope_id: str)[source]#
Bases:
AdapterThe public methods of an adapter for an asynchronous client.
The same as
SyncAdapter’s, as coroutines. Each store call runs in a worker thread, so a long ingest doesn’t block the event loop.
- class calendaring_sync.adapters.base.InStore(call: ~collections.abc.Callable[[...], ~typing.Any], args: tuple[~typing.Any, ...] = (), kwargs: ~collections.abc.Mapping[str, ~typing.Any] = <factory>)[source]#
Bases:
objectA call into the store, which an asynchronous adapter runs in a worker thread.
- call#
The function to call, such as
session.ingest.- Type:
collections.abc.Callable[[…], Any]
- kwargs#
Its keyword arguments.
- Type:
collections.abc.Mapping[str, Any]
Examples
A step that ingests a change set:
yield InStore(session.ingest, (change_set,))
- class calendaring_sync.adapters.base.ReadResult(*values)[source]#
Bases:
EnumHow the read part of a sync ended, which decides what the sync does next.
- COMPLETE = 'complete'#
Every change was ingested, so the pushes go out.
- INVALID_TOKEN = 'invalid token'#
The server refused the scope’s sync token, so the scope is reset and listed in full.
- TRUNCATED = 'truncated'#
The server cut the changes short. The pushes wait, and the next sync continues from the checkpoint.
- calendaring_sync.adapters.base.Steps#
The steps of a sync, as a generator that yields client answers or
InStorecalls, is sent back what each one returned, and returns its result.
- class calendaring_sync.adapters.base.SyncAdapter(replica: Replica, scope_id: str)[source]#
Bases:
AdapterThe public methods of an adapter for a synchronous client.
Your application calls
sync()when it decides to sync, and records its edits withcreate()andchange(). Deletes and discards need nothing from the content, so they go todelete()anddiscard(). The adapter’s class says what content it takes and which of its client’s errors reach you; the next sync settles a push such an error interrupted. Every method also raises the store’s errors, asReplicadescribes.- change(local_key: int, content: bytes) None[source]#
Record a change of an object to this content; the next
sync()sends it.Reads the record, then writes the change, each in a transaction of its own. A change to a create that wasn’t sent yet stays a create, so the content must pass what a create’s does.
- Raises:
ValueError – If the content can’t be an object of the scope.
OtherScopeError – If the record is in another scope.
UnknownRecordError – If no record has this local key.
EditRefusedError – If the record can’t take an edit, such as one with an open conflict.
TransactionError – If a store transaction is open in this thread.
- create(content: bytes) int[source]#
Record a create of an object with this content, and return its local key.
The next
sync()sends it. Runs in a write transaction.- Raises:
ValueError – If the content can’t be an object of the scope.
UnknownScopeError – If the scope was never synced.
TransactionError – If a store transaction is open in this thread.
- sync(*, full_listing: bool = False) None[source]#
Sync the scope: ingest what changed on the server, then push your edits.
A scope with no token is listed in full. When the server refuses the token, the scope is reset and listed in full. Your edits are pushed only once everything the server reported was ingested. Each step runs in a store transaction of its own.
- Parameters:
full_listing – List every object instead of asking for changes.
- Raises:
ScopeGoneError – If the server no longer has the scope.
IncompleteListingError – If the server cut a full listing short.
StaleChangeSetError – If another process ingested the scope first.
SessionError – If this replica already has a session open for the scope.
ScopeMismatchError – If the scope’s content has another media type.
TransactionError – If a store transaction is open in this thread.
- waiting() list[Record][source]#
Return the pending edits the next
sync()won’t send, because it can’t yet.Runs in a read transaction.
- Raises:
UnknownScopeError – If the scope was never synced.
TransactionError – If a store transaction is open in this thread.
CalDAV#
The CalDAV adapter, which syncs one calendar collection through python-caldav.
It needs the caldav extra, as Install shows.
Adapters explains every rule it follows, and
Server compatibility how each tested server behaves.
- class calendaring_sync.adapters.caldav.AsyncCalDAVAdapter(replica: Replica, calendar: Calendar, *, scope_id: str | None = None)[source]#
CalDAVAdapterfor a calendar on an asynchronous client.Its methods are coroutines, as
AsyncAdapterdescribes; everything else is asCalDAVAdapterdescribes.- Raises:
TypeError – If the calendar is on a synchronous client, or an argument has the wrong type.
ValueError – If the calendar has no URL or no client.
- edit_fields(content: bytes) EditFields#
Read an object’s fields from its content, the same way a sync reads the server’s copy.
Use it for content your application made itself, such as a
Merge. Reads nothing from the store or the server.- Raises:
ValueError – If the content can’t be an object of the scope, as for a create.
- property needs_full_listings: bool#
Whether the server may leave changes out without refusing the scope’s token.
Read from the server profile configured on the client, python-caldav’s
sync-token.invalid-tokenfeature;Adapter.needs_full_listingssays what to do when it’sTrue.
- class calendaring_sync.adapters.caldav.CalDAVAdapter(replica: Replica, calendar: Calendar, *, scope_id: str | None = None)[source]#
Syncs one calendar collection through a synchronous python-caldav client.
The calendar is one scope. The content
create()andchange()take is iCalendar data in UTF-8, and a create’s must hold an object with aUID; anything else raisesValueError. A failure python-caldav reports, such as a refused login, reaches you as itscaldav.lib.error.DAVError, unchanged.- Parameters:
replica – Where the sync state is kept.
calendar – The calendar to sync.
scope_id – The scope’s id. Defaults to the calendar’s URL, without a login and spelled one way, so calendars on two servers never share a scope and one calendar always gets the same one.
- Raises:
TypeError – If the calendar is on an asynchronous client, or an argument has the wrong type.
ValueError – If the calendar has no URL or no client.
Examples
A sync, an edit, and the sync that pushes it:
adapter = CalDAVAdapter(replica, calendar) adapter.sync() key = adapter.create(ics_bytes) adapter.sync()
- change(local_key: int, content: bytes) None#
Record a change of an object to this content; the next
sync()sends it.Reads the record, then writes the change, each in a transaction of its own. A change to a create that wasn’t sent yet stays a create, so the content must pass what a create’s does.
- Raises:
ValueError – If the content can’t be an object of the scope.
OtherScopeError – If the record is in another scope.
UnknownRecordError – If no record has this local key.
EditRefusedError – If the record can’t take an edit, such as one with an open conflict.
TransactionError – If a store transaction is open in this thread.
- create(content: bytes) int#
Record a create of an object with this content, and return its local key.
The next
sync()sends it. Runs in a write transaction.- Raises:
ValueError – If the content can’t be an object of the scope.
UnknownScopeError – If the scope was never synced.
TransactionError – If a store transaction is open in this thread.
- edit_fields(content: bytes) EditFields#
Read an object’s fields from its content, the same way a sync reads the server’s copy.
Use it for content your application made itself, such as a
Merge. Reads nothing from the store or the server.- Raises:
ValueError – If the content can’t be an object of the scope, as for a create.
- property needs_full_listings: bool#
Whether the server may leave changes out without refusing the scope’s token.
Read from the server profile configured on the client, python-caldav’s
sync-token.invalid-tokenfeature;Adapter.needs_full_listingssays what to do when it’sTrue.
- sync(*, full_listing: bool = False) None#
Sync the scope: ingest what changed on the server, then push your edits.
A scope with no token is listed in full. When the server refuses the token, the scope is reset and listed in full. Your edits are pushed only once everything the server reported was ingested. Each step runs in a store transaction of its own.
- Parameters:
full_listing – List every object instead of asking for changes.
- Raises:
ScopeGoneError – If the server no longer has the scope.
IncompleteListingError – If the server cut a full listing short.
StaleChangeSetError – If another process ingested the scope first.
SessionError – If this replica already has a session open for the scope.
ScopeMismatchError – If the scope’s content has another media type.
TransactionError – If a store transaction is open in this thread.
- waiting() list[Record]#
Return the pending edits the next
sync()won’t send, because it can’t yet.Runs in a read transaction.
- Raises:
UnknownScopeError – If the scope was never synced.
TransactionError – If a store transaction is open in this thread.
- calendaring_sync.adapters.caldav.FINGERPRINT_SCHEME = 'caldav-canonical-1'#
The name of the canonical fingerprint this adapter computes. A change to how it’s computed gets a new name, so every scope is listed again.
- calendaring_sync.adapters.caldav.MEDIA_TYPE = 'text/calendar'#
The media type of a CalDAV scope’s content, iCalendar data.
- calendaring_sync.adapters.caldav.MULTIGET_SIZE = 100#
How many objects one
calendar-multigetfetches, and one page of a full listing holds.
JMAP#
The JMAP adapter, which syncs one account’s calendar events through calendaring-jmap.
It needs the jmap extra, as Install shows.
Adapters explains every rule it follows, and
Server compatibility how each tested server behaves.
- class calendaring_sync.adapters.jmap.AsyncJMAPAdapter(replica: Replica, client: AsyncJMAPClient, *, account_id: str, scope_id: str | None = None)[source]#
JMAPAdapterfor an asynchronous client.Its methods are coroutines, as
AsyncAdapterdescribes; everything else is asJMAPAdapterdescribes.- Raises:
TypeError – If an argument has the wrong type, such as a synchronous client.
- edit_fields(content: bytes) EditFields#
Read an object’s fields from its content, the same way a sync reads the server’s copy.
Use it for content your application made itself, such as a
Merge. Reads nothing from the store or the server.- Raises:
ValueError – If the content can’t be an object of the scope, as for a create.
- property needs_full_listings: bool#
Whether the server can lose changes without refusing the scope’s token.
If it’s
True, an incremental sync may miss a change or a removal, so schedule a sync withfull_listing=Truenow and then, such as once a day.Falseby default; an adapter whose server can lose changes says so.
- calendaring_sync.adapters.jmap.FINGERPRINT_SCHEME = 'jmap-canonical-1'#
The name of the canonical fingerprint this adapter computes. A change to how it’s computed gets a new name, so every scope is listed again.
- class calendaring_sync.adapters.jmap.JMAPAdapter(replica: Replica, client: JMAPClient, *, account_id: str, scope_id: str | None = None)[source]#
Syncs one account’s calendar events through a synchronous calendaring-jmap client.
The account’s events are one scope. The content
create()andchange()take is a JSCalendar event as UTF-8 JSON. A create’s must have auidand at least one calendar incalendarIds, and so must a change to a create not sent yet; anything else raisesValueError. The server picks a created event’s id, andwaiting()always returns an empty list. A failure calendaring-jmap reports, such as a refused login, reaches you as itsJMAPError, unchanged.- Parameters:
replica – Where the sync state is kept.
client – The server’s client.
account_id – The account whose events to sync, such as the one the client’s
get_account_id()returns, or a shared account’s id.scope_id – The scope’s id. Defaults to the client’s session URL, without a login and spelled one way, then
#, the account id and/CalendarEvent, so accounts on two servers never share a scope.
- Raises:
TypeError – If an argument has the wrong type, such as an asynchronous client.
Examples
A sync, an edit, and the sync that pushes it:
adapter = JMAPAdapter(replica, client, account_id=client.get_account_id()) adapter.sync() key = adapter.create(event_json_bytes) adapter.sync()
- change(local_key: int, content: bytes) None#
Record a change of an object to this content; the next
sync()sends it.Reads the record, then writes the change, each in a transaction of its own. A change to a create that wasn’t sent yet stays a create, so the content must pass what a create’s does.
- Raises:
ValueError – If the content can’t be an object of the scope.
OtherScopeError – If the record is in another scope.
UnknownRecordError – If no record has this local key.
EditRefusedError – If the record can’t take an edit, such as one with an open conflict.
TransactionError – If a store transaction is open in this thread.
- create(content: bytes) int#
Record a create of an object with this content, and return its local key.
The next
sync()sends it. Runs in a write transaction.- Raises:
ValueError – If the content can’t be an object of the scope.
UnknownScopeError – If the scope was never synced.
TransactionError – If a store transaction is open in this thread.
- edit_fields(content: bytes) EditFields#
Read an object’s fields from its content, the same way a sync reads the server’s copy.
Use it for content your application made itself, such as a
Merge. Reads nothing from the store or the server.- Raises:
ValueError – If the content can’t be an object of the scope, as for a create.
- property needs_full_listings: bool#
Whether the server can lose changes without refusing the scope’s token.
If it’s
True, an incremental sync may miss a change or a removal, so schedule a sync withfull_listing=Truenow and then, such as once a day.Falseby default; an adapter whose server can lose changes says so.
- sync(*, full_listing: bool = False) None#
Sync the scope: ingest what changed on the server, then push your edits.
A scope with no token is listed in full. When the server refuses the token, the scope is reset and listed in full. Your edits are pushed only once everything the server reported was ingested. Each step runs in a store transaction of its own.
- Parameters:
full_listing – List every object instead of asking for changes.
- Raises:
ScopeGoneError – If the server no longer has the scope.
IncompleteListingError – If the server cut a full listing short.
StaleChangeSetError – If another process ingested the scope first.
SessionError – If this replica already has a session open for the scope.
ScopeMismatchError – If the scope’s content has another media type.
TransactionError – If a store transaction is open in this thread.
- waiting() list[Record]#
Return the pending edits the next
sync()won’t send, because it can’t yet.Runs in a read transaction.
- Raises:
UnknownScopeError – If the scope was never synced.
TransactionError – If a store transaction is open in this thread.
- calendaring_sync.adapters.jmap.MEDIA_TYPE = 'application/jscalendar+json'#
The media type of a JMAP scope’s content, JSCalendar (RFC 8984).
- calendaring_sync.adapters.jmap.PAGE_SIZE = 100#
How many events one
/changespage reports, and one page of a full listing holds. Below every tested server’smaxObjectsInGet.