adapters: Syncing a CalDAV calendar or a JMAP account#
An adapter connects a Replica to one kind of server. Each comes in a synchronous and an asynchronous variant, takes the matching client, and is installed with an extra of its own. Adapters explains every rule they follow, and Server compatibility how each tested server behaves.
CalDAV#
CalDAVAdapter syncs one CalDAV calendar collection through a python-caldav client, and AsyncCalDAVAdapter does the same through an asynchronous one. Install them with the caldav extra.
The CalDAV adapter, which syncs one calendar collection through python-caldav.
Install it with the caldav extra: pip install 'calendaring-sync[caldav]'.
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.sync(),create()andchange()are coroutines; each store call runs in a worker thread, so a long ingest doesn’t block the event loop. 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.
- 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. When it’s true, schedule regular full listings withsync(full_listing=True): a listing only fetches objects whose ETag changed, so it costs little more than a sync.
- sync(*, full_listing: bool = False) None[source]#
See
CalDAVAdapter.sync().
- class calendaring_sync.adapters.caldav.CalDAVAdapter(replica: Replica, calendar: Calendar, *, scope_id: str | None = None)[source]#
Syncs one calendar collection with a
Replica.The calendar is a python-caldav
Calendaron a synchronous client, and it’s one scope. Your application callssync()when it decides to sync, and records its edits withcreate()andchange(), which build an object’s fingerprint and logical key the same way a sync does. Deletes and discards need nothing from the content, so they go todelete()anddiscard().With no server profile on the client, python-caldav searches the calendar for a create’s
UIDbefore sending it. Configuring the client’sfeatures(features="radicale", for example) lets the server check instead, in the same request.A change or delete of an object whose stored ETag is weak (
W/) waits, unsent, until a sync stores a strong one, sinceIf-Matchnever matches a weak ETag;waiting()lists such edits.Every method also raises the store’s errors, as
Replicadescribes.- Parameters:
replica – Where the sync state is kept.
calendar – The calendar collection 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[source]#
Record a change of an object to this iCalendar data; the next
sync()sends it.Reads the record, then writes the change, each in a transaction of its own.
- Raises:
ValueError – If the content isn’t UTF-8 iCalendar data, the record is in another scope, or the record is a create not sent yet and the content holds no
UID.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 iCalendar data, and return its local key.
The object gets an href of its own; the next
sync()sends it. Runs in a write transaction.- Raises:
ValueError – If the content isn’t UTF-8 iCalendar data with a
UID.UnknownScopeError – If the calendar was never synced.
TransactionError – If a store transaction is open in this thread.
- 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. When it’s true, schedule regular full listings withsync(full_listing=True): a listing only fetches objects whose ETag changed, so it costs little more than a sync.
- sync(*, full_listing: bool = False) None[source]#
Sync the calendar: ingest what changed on the server, then push your edits.
Changes since the scope’s token come from a
sync-collectionreport, and a calendar that has none, or a scope that has no token, is listed in full instead. 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. Members whose ETag didn’t change aren’t fetched.
- Raises:
ScopeGoneError – If the server has no calendar at its URL.
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.
caldav.lib.error.DAVError – For any other failure python-caldav reports, such as a refused login, unchanged. A push it interrupted is settled by the next sync.
- waiting() list[Record][source]#
Return the pending edits the next
sync()won’t send, and can’t yet.That’s a change or delete of an object whose stored ETag is weak. Runs in a read transaction.
- Raises:
UnknownScopeError – If the calendar 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'#
iCalendar data.
- Type:
The media type of a CalDAV scope’s content
- calendaring_sync.adapters.caldav.MULTIGET_SIZE = 100#
How many objects one
calendar-multigetfetches, and one page of a full listing holds.
JMAP#
JMAPAdapter syncs one JMAP account’s calendar events through a calendaring-jmap client, and AsyncJMAPAdapter does the same through an asynchronous one. Install them with the jmap extra.
The JMAP adapter, which syncs one account’s calendar events through calendaring-jmap.
Install it with the jmap extra: pip install 'calendaring-sync[jmap]'.
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.sync(),create(),change()andwaiting()are coroutines; each store call runs in a worker thread, so a long ingest doesn’t block the event loop. Everything else is asJMAPAdapterdescribes.- Raises:
TypeError – If an argument has the wrong type, such as a synchronous client.
- create(content: bytes) int[source]#
See
JMAPAdapter.create().
- property needs_full_listings: bool#
Whether the server may leave changes out without refusing the scope’s state; never.
Every tested server refuses a state it can’t calculate changes from, and the scope is listed again. The one answer that looks like a real one: Cyrus answers a state newer than any it issued as if nothing changed. So after restoring a server from a backup, list each scope once with
sync(full_listing=True).
- sync(*, full_listing: bool = False) None[source]#
See
JMAPAdapter.sync().
- 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 JMAP account’s calendar events with a
Replica.The client is a calendaring-jmap
JMAPClient, and the account’sCalendarEventrecords are one scope. Your application callssync()when it decides to sync, and records its edits withcreate()andchange(), which build an event’s fingerprint, calendars and logical key the same way a sync does. Deletes and discards need nothing from the content, so they go todelete()anddiscard().Every write carries
ifInStatewith the state the sync last saw, and that state covers every event in the account. When the server refuses a write because the state moved, the remaining edits wait, unsent, for the next sync.Every method also raises the store’s errors, as
Replicadescribes.- Parameters:
replica – Where the sync state is kept.
client – The client of the JMAP server.
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[source]#
Record a change of an event to this JSCalendar content; the next
sync()sends it.Reads the record, then writes the change, each in a transaction of its own.
- Raises:
ValueError – If the content isn’t UTF-8 JSON holding an object, the record is in another scope, or the record is a create not sent yet and the content has no
uidor no calendar.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 event with this JSCalendar content, and return its local key.
The server picks the event’s id when the next
sync()sends it. Runs in a write transaction.- Raises:
ValueError – If the content isn’t UTF-8 JSON holding an event with a
uidand at least one calendar incalendarIds.UnknownScopeError – If the account was never synced.
TransactionError – If a store transaction is open in this thread.
- property needs_full_listings: bool#
Whether the server may leave changes out without refusing the scope’s state; never.
Every tested server refuses a state it can’t calculate changes from, and the scope is listed again. The one answer that looks like a real one: Cyrus answers a state newer than any it issued as if nothing changed. So after restoring a server from a backup, list each scope once with
sync(full_listing=True).
- sync(*, full_listing: bool = False) None[source]#
Sync the account’s events: ingest what changed on the server, then push your edits.
Changes since the scope’s token come from
CalendarEvent/changes, a page at a time, and a scope that has no token is listed in full instead. 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 event instead of asking for changes. JMAP has no version per event, so every event is fetched.
- Raises:
ScopeGoneError – If the server has no such account.
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.
calendaring_jmap.JMAPError – For any other failure calendaring-jmap reports, such as a refused login, unchanged. A push it interrupted is settled by the next sync.
- waiting() list[Record][source]#
Return the pending edits the next
sync()won’t send; none for JMAP.Every pending edit goes out with the next sync, unless the server rejects one and the rest wait for the sync after. Runs in a read transaction.
- Raises:
UnknownScopeError – If the account was never synced.
TransactionError – If a store transaction is open in this thread.
- calendaring_sync.adapters.jmap.MEDIA_TYPE = 'application/jscalendar+json'#
JSCalendar (RFC 8984).
- Type:
The media type of a JMAP scope’s content
- 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.