Migrating from caldav 2.x to 3.x#
v3.0 is mostly backward-compatible with v2.x. Existing code should generally continue to run. New method names and usage patterns exists alongside the old ones. The purpose of this document is to give a primer on the “best current usage practice” as of v3.0. It may be needed to implement some of the changes below before using future major versions like v4 or v5.
Breaking Changes#
Python version#
Python 3.10 or later is now required. Python 3.8 and 3.9 are no longer supported.
caldav.objects import shim removed#
The caldav/objects.py backward-compatibility re-export module has been
deleted. If you have:
from caldav.objects import Event, Todo, Calendar # REMOVED
replace it with:
from caldav import Event, Todo, Calendar # OK
All public symbols that were in caldav.objects should remain available directly
from the caldav namespace
Wildcard import into caldav.* removed#
Earlier a wildcard import from caldav.objects import * was done into the caldav namespace. This has been removed. In normal circumstances, your imports should continue to work - but I cannot give any guarantees that YOUR imports will continue working. If you have any issues, then reach out.
..todo:: how to add a link from “reach out” to the contact page?
Config-file parse errors now raise exceptions#
read_config() used to log and return an empty dict on YAML/JSON parse
errors. It now raises ValueError. This means misconfigured files fail
loudly rather than silently.
Recommended API Changes#
The changes below are not yet breaking — the old names still work. Some of them
emits DeprecationWarning, others will do so in an upcoming release. New code
should use the new names.
Factory function instead of direct instantiation#
v2.x |
v3.x (recommended) |
|---|---|
|
|
caldav.get_davclient() reads credentials from environment variables and
config files, selects an appropriate HTTP library, and handles server-specific
compatibility hints automatically:
from caldav import get_davclient
# Credentials from env vars or ~/.config/caldav/calendar.conf
with get_davclient() as client:
principal = client.get_principal()
...
# Or supply them explicitly
with get_davclient(url="https://caldav.example.com/",
username="alice", password="secret") as client:
principal = client.get_principal()
...
# Use a named compatibility profile (iCloud, google, nextcloud, …)
with get_davclient(features="icloud",
username="alice@icloud.com", password="...") as client:
...
Calendars may be configured in the config file, and it’s also possible to get a celndar directly through factory method:
See Config file format for the config-file format and the full list of parameters.
Principal and calendar access#
Quite some methods have been renamed for consistency both within the package and with the python ecosystem as such.
v2.x |
v3.x |
|---|---|
|
|
|
|
|
|
# v2.x
principal = client.principal()
calendars = principal.calendars()
# v3.x
principal = client.get_principal()
calendars = principal.get_calendars()
Rationale: Those methods are actively querying the server for data, hence a verb is more fitting.
Adding calendar objects#
The save_* family is deprecated. Use add_* for adding new objects:
v2.x |
v3.x |
|---|---|
|
|
|
|
|
|
|
|
To update an existing object, fetch it and call object.save():
event = calendar.get_event_by_uid("some-uid")
event.data = new_ical_string
event.save()
Rationale: In the tests, documentation and examples I’m always adding new content with those methods, so add feels more right than save. This is a revert of a change that was done in v0.7.0. See python-caldav/caldav#71 for details. While the add_object-method possibly MAY be used for updating an object, it SHOULD not be used for this purpose.
Listing and fetching objects#
v2.x |
v3.x |
|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Rationale: Those methods are actively querying the server for data, hence a verb is more fitting.
Searching#
date_search() is deprecated. Use search() instead:
from datetime import datetime
# v2.x
events = calendar.date_search(start=datetime(2024,1,1),
end=datetime(2024,12,31),
expand=True)
# v3.x — note the keyword arguments and the explicit event=True flag
events = calendar.search(start=datetime(2024,1,1),
end=datetime(2024,12,31),
event=True, expand=True)
search() also accepts todo=True, journal=True, comp_class=Event,
free-text filters, category filters, and more. See the API reference for the
full signature.
Rationale: date_search has actually been deprecated since 2.0, if not longer. It’s just a special case of search.
Capability checks#
v2.x |
v3.x |
|---|---|
|
|
|
|
|
|
Rationale: Those methods are also querying the server actively, and not just looking up things in a feature-matrix, hnence a verb is more fitting.
Accessing and editing calendar data#
This is the most significant new API in v3.0, addressing a long-standing ambiguity in how calendar object data was accessed and modified.
The old vobject_instance, icalendar_instance, icalendar_component are now deprecated.
The Problem with the 2.x API#
In 2.x, the properties data, icalendar_instance, and
vobject_instance on a CalendarObjectResource all shared a single
internal slot. Accessing one representation could silently invalidate another:
# 2.x — silent bug
event = calendar.search(...)[0]
comp = event.icalendar_component # get a reference
_ = event.data # accessing data invalidates comp!
comp["SUMMARY"] = "Updated"
event.save() # change is NOT saved
The 3.x Solution: read and edit methods#
v3.0 adds explicit read-only getters that always return copies, and context-manager “borrow” methods that give exclusive, safe write access.
Read-only access (safe at any time, returns a copy):
# Get raw iCalendar string
raw = event.get_data()
# Get a copy of the icalendar.Calendar object — changes are NOT saved
cal_copy = event.get_icalendar_instance()
summary = cal_copy.subcomponents[0]["SUMMARY"]
# Get a copy of the vobject component — changes are NOT saved
vobj_copy = event.get_vobject_instance()
summary = vobj_copy.vevent.summary.value
# Quick access to the inner VEVENT/VTODO/VJOURNAL component
summary = event.component["SUMMARY"] # component is always a copy
Editing with icalendar (borrowing pattern):
with event.edit_icalendar_instance() as cal:
for comp in cal.subcomponents:
if comp.name == "VEVENT":
comp["SUMMARY"] = "Updated summary"
event.save()
Editing with vobject (borrowing pattern):
with event.edit_vobject_instance() as vobj:
vobj.vevent.summary.value = "Updated summary"
event.save()
While inside the with block, the borrowed representation is the
single source of truth. Attempting to borrow a different
representation raises RuntimeError. This means the current
pattern is not completely thread-safe as of v3.0 - but an explicit
error is often better than updates silently being dropped.
The data representation remains the same:
The interface for the string representation is still the same. Strings are immutable, so the concern above is not relevant for strings. (Of course, the code below will have bad side effects if the event was modified simultaneously by another thread, as well as if the event was modified on the server by another client).
## Get the raw iCalendar string
ical_string = event.data
new_ical_string = modify_ical(ical_string)
# Replace all data from a raw iCalendar string
event.data = new_ical_string
event.save()
Summary of the new data API:
Method / property |
Purpose |
|---|---|
|
Raw iCalendar string, always a copy |
|
icalendar.Calendar copy, safe for read-only use |
|
vobject component copy, safe for read-only use |
|
Alias for the inner VEVENT/VTODO/VJOURNAL component (copy) |
|
Context manager — exclusive write access via icalendar |
|
Context manager — exclusive write access via vobject |
|
Replace all data from a raw string |
New in v3.0#
Async client#
Async operations are often the “best current practice” in the Python world. Now it’s possible also with the CalDAV library.
A new caldav.async_davclient.AsyncDAVClient provides the same API
with async/await support. All domain objects (Calendar, Event,
Todo, …) work with both the sync and async clients:
from caldav.async_davclient import get_davclient
async def main():
async with await get_davclient(url="...", username="...",
password="...") as client:
principal = await client.get_principal()
calendars = await principal.get_calendars()
for cal in calendars:
events = await cal.get_events()
See Async API for more details.
JMAP client (experimental)#
A new caldav.jmap package provides JMAPClient and AsyncJMAPClient
for servers implementing RFC 8620 (JMAP Core) and RFC 8984 (JMAP Calendars).
The public API may change in minor releases. See JMAP.
Advanced search#
The new CalDAVSearcher / calendar.searcher() API allows building
composite search queries:
from caldav import CalDAVSearcher, Todo
searcher = calendar.searcher()
searcher.add_property_filter("category", "WORK")
searcher.add_property_filter("status", "NEEDS-ACTION")
todos = searcher.search(calendar)
# Or as a one-liner
searcher = CalDAVSearcher(comp_class=Todo)
searcher.add_property_filter("category", "WORK", case_sensitive=False)
results = searcher.search(calendar)
Compatibility hints / features parameter#
Server-specific quirks are now encoded in named profiles in
caldav/compatibility_hints.py. Pass the profile name via the features
parameter to get automatic workarounds:
client = get_davclient(url="https://...", features="nextcloud",
username="alice", password="secret")
You can also override individual flags:
client = get_davclient(url="https://...",
features={"search.text": {"support": "unsupported"}})
In the config file it’s possible to combine a base profile with overrides in the config file:
my-server:
caldav_url: https://caldav.example.com/
caldav_username: alice
caldav_password: secret
features:
base: nextcloud
search.text: unsupported
See Compatibility for more on the compatibility hints system.
Object UID as .id#
CalendarObjectResource.id is now available as a shortcut for the
UID property:
event = calendar.add_event(ical_string)
print(event.id) # same as event.icalendar_component["UID"]
Other notable changes#
caldav 3.x uses niquests by default for HTTP communication. Niquests is a
backward-compatible fork of requests that adds HTTP/2, HTTP/3, and async
support. If you need to switch to requests or httpx, see
HTTP Library Configuration.
Rate-limit support is now built into the CalDAV library. Pass
rate_limit_handle=True to automatically sleep and retry on 429
Too Many Requests / 503 Service Unavailable responses that
include a Retry-After header:
client = get_davclient(url="...", rate_limit_handle=True)
This may also be configured:
my-server:
caldav_url: https://caldav.example.com/
caldav_username: alice
caldav_password: secret
features:
rate_limit:
enable: True
default_sleep: 4
max_sleep: 120
If the server gives a retry-after-header on 429 or 530 it will be respected, otherwise the default_sleep will be utilized on 429. This happens in a loop, the sleep period will be multiplied with 1.5 on every retry.
The total sleep period will never exceed 120, no matter if retry-after is given or not.
Deprecated in v3.0 (will be removed in v4.0)#
The following emit DeprecationWarning:
calendar.date_search()— usecalendar.search()client.principals()— useclient.search_principals()obj.split_expandedattributeobj.expand_rruleattribute.instanceproperty on calendar objects — use.vobject_instanceresponse.find_objects_and_props()— useresponse.results
The following are deprecated but do not yet emit warnings:
All
save_*methods → useadd_*All
*_by_uid()methods → useget_*_by_uid()principal.calendars()→principal.get_calendars()calendar.events()→calendar.get_events()calendar.todos()→calendar.get_todos()calendar.journals()→calendar.get_journals()calendar.objects_by_sync_token()→calendar.get_objects_by_sync_token()client.principal()→client.get_principal()client.check_dav_support()→client.supports_dav()client.check_cdav_support()→client.supports_caldav()client.check_scheduling_support()→client.supports_scheduling()