========================================= 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. .. contents:: Contents :local: :depth: 2 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: .. code-block:: python from caldav.objects import Event, Todo, Calendar # REMOVED replace it with: .. code-block:: python 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 ------------------------------------------------- .. list-table:: :widths: 45 55 :header-rows: 1 * - v2.x - v3.x (recommended) * - ``DAVClient(url=..., username=..., password=...)`` - ``get_davclient(url=..., username=..., password=...)`` :func:`caldav.get_davclient` reads credentials from environment variables and config files, selects an appropriate HTTP library, and handles server-specific compatibility hints automatically: .. code-block:: python 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: .. code-block::python from caldav import get_calendar with my_calendar as get_calendar(config_section='work_calendar'): ... See :doc:`configfile` 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. .. list-table:: :widths: 45 55 :header-rows: 1 * - v2.x - v3.x * - ``client.principal()`` - ``client.get_principal()`` * - ``principal.calendars()`` - ``principal.get_calendars()`` * - ``client.principals(name=...)`` - ``client.search_principals(name=...)`` .. code-block:: python # 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: .. list-table:: :widths: 45 55 :header-rows: 1 * - v2.x - v3.x * - ``calendar.save_event(ical_str)`` - ``calendar.add_event(ical_str)`` * - ``calendar.save_todo(ical_str)`` - ``calendar.add_todo(ical_str)`` * - ``calendar.save_journal(ical_str)`` - ``calendar.add_journal(ical_str)`` * - ``calendar.save_object(ical_str)`` - ``calendar.add_object(ical_str)`` To **update** an existing object, fetch it and call ``object.save()``: .. code-block:: python 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 https://github.com/python-caldav/caldav/issues/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 ----------------------------- .. list-table:: :widths: 45 55 :header-rows: 1 * - v2.x - v3.x * - ``calendar.events()`` - ``calendar.get_events()`` * - ``calendar.todos()`` - ``calendar.get_todos()`` * - ``calendar.journals()`` - ``calendar.get_journals()`` * - ``calendar.event_by_uid(uid)`` - ``calendar.get_event_by_uid(uid)`` * - ``calendar.todo_by_uid(uid)`` - ``calendar.get_todo_by_uid(uid)`` * - ``calendar.journal_by_uid(uid)`` - ``calendar.get_journal_by_uid(uid)`` * - ``calendar.object_by_uid(uid)`` - ``calendar.get_object_by_uid(uid)`` * - ``calendar.objects_by_sync_token()`` - ``calendar.get_objects_by_sync_token()`` Rationale: Those methods are actively querying the server for data, hence a verb is more fitting. Searching ---------- ``date_search()`` is deprecated. Use ``search()`` instead: .. code-block:: python 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 ----------------- .. list-table:: :widths: 45 55 :header-rows: 1 * - v2.x - v3.x * - ``client.check_dav_support()`` - ``client.supports_dav()`` * - ``client.check_cdav_support()`` - ``client.supports_caldav()`` * - ``client.check_scheduling_support()`` - ``client.supports_scheduling()`` 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: .. code-block:: python # 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): .. code-block:: python # 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): .. code-block:: python 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): .. code-block:: python 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). .. code-block:: python ## 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:** .. list-table:: :widths: 30 70 :header-rows: 1 * - Method / property - Purpose * - ``event.get_data()`` - Raw iCalendar string, always a copy * - ``event.get_icalendar_instance()`` - icalendar.Calendar copy, safe for read-only use * - ``event.get_vobject_instance()`` - vobject component copy, safe for read-only use * - ``event.component`` - Alias for the inner VEVENT/VTODO/VJOURNAL component (copy) * - ``event.edit_icalendar_instance()`` - Context manager — exclusive write access via icalendar * - ``event.edit_vobject_instance()`` - Context manager — exclusive write access via vobject * - ``event.data = "..."`` - 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 :class:`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: .. code-block:: python 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 :doc:`async` 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 :doc:`jmap`. Advanced search --------------- The new ``CalDAVSearcher`` / ``calendar.searcher()`` API allows building composite search queries: .. code-block:: python 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: .. code-block:: python client = get_davclient(url="https://...", features="nextcloud", username="alice", password="secret") You can also override individual flags: .. code-block:: python 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: .. code-block:: yaml my-server: caldav_url: https://caldav.example.com/ caldav_username: alice caldav_password: secret features: base: nextcloud search.text: unsupported See :ref:`about:Compatibility` for more on the compatibility hints system. Object UID as ``.id`` ---------------------- ``CalendarObjectResource.id`` is now available as a shortcut for the ``UID`` property: .. code-block:: python 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 :doc:`http-libraries`. 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: .. code-block:: python client = get_davclient(url="...", rate_limit_handle=True) This may also be configured: .. code-block:: yaml 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()`` — use ``calendar.search()`` * ``client.principals()`` — use ``client.search_principals()`` * ``obj.split_expanded`` attribute * ``obj.expand_rrule`` attribute * ``.instance`` property on calendar objects — use ``.vobject_instance`` * ``response.find_objects_and_props()`` — use ``response.results`` The following are deprecated but do **not yet** emit warnings: * All ``save_*`` methods → use ``add_*`` * All ``*_by_uid()`` methods → use ``get_*_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()``