adapters.caldav: Syncing a CalDAV calendar#

An adapter connects a Replica to one kind of server. 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. Adapters explains every rule they follow, and Server compatibility how each tested server behaves.

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]#

CalDAVAdapter for a calendar on an asynchronous client.

sync(), create() and change() are coroutines; each store call runs in a worker thread, so a long ingest doesn’t block the event loop. Everything else is as CalDAVAdapter describes.

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.

change(local_key: int, content: bytes) → None[source]#

See CalDAVAdapter.change().

create(content: bytes) → int[source]#

See CalDAVAdapter.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-token feature. When it’s true, schedule regular full listings with sync(full_listing=True): a listing only fetches objects whose ETag changed, so it costs little more than a sync.

property scope_id: str#

The scope this adapter syncs.

sync(*, full_listing: bool = False) → None[source]#

See CalDAVAdapter.sync().

waiting() → list[Record][source]#

See CalDAVAdapter.waiting().

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 Calendar on a synchronous client, and it’s one scope. Your application calls sync() when it decides to sync, and records its edits with create() and change(), 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 to delete() and discard().

With no server profile on the client, python-caldav searches the calendar for a create’s UID before sending it. Configuring the client’s features (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, since If-Match never matches a weak ETag; waiting() lists such edits.

Every method also raises the store’s errors, as Replica describes.

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, or 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 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:
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-token feature. When it’s true, schedule regular full listings with sync(full_listing=True): a listing only fetches objects whose ETag changed, so it costs little more than a sync.

property scope_id: str#

The scope this adapter syncs.

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-collection report, 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:
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-multiget fetches, and one page of a full listing holds.

Last updated on 2026-10-23.