Skip to content

Latest commit

 

History

History
264 lines (200 loc) · 13 KB

File metadata and controls

264 lines (200 loc) · 13 KB

Tutorial

In this tutorial you should learn basic usage of the python CalDAV client library. You are encouraged to copy the code examples into a file and add a breakpoint() inside the with-block so you can inspect the return objects you get from the library calls. Do not name your file caldav.py or calendar.py, this may break some imports.

To follow this tutorial as intended, each code block should be run towards a clean-slate Radicale server. To do this, you need:

  • The source code of caldav with tests: git clone https://github.com/python-caldav/caldav.git ; cd caldav
  • The Radicale python package: pip install radicale
  • An environmental variable set: export PYTHON_CALDAV_USE_TEST_SERVER=1

With this setup, the with-blocks in the code sections below will spin up a Radicale server.

When you've run the tutorial as intended, I recommend going through the examples again towards your own calendar server:

  • Set the environment variables CALDAV_URL, CALDAV_USER and CALDAV_PASSWORD to point to your personal calendar server.
  • Be aware that different calendar servers may behave differently. For instance, not all of them allows you to create a calendar. Some are even read-only.
  • You will need to revert all changes done. The code examples below does not do any cleanup. If your calendar server supports creating and deleting calendars, then it should be easy enough: `my_new_calendar.delete()` inside the with-block. Events also has a .delete()-method. Beware that there is no undo. You're adviced to have a local backup of your calendars. I'll probably write a HOWTO on that one day.
  • Usage of a context manager is considered best practice, but not really needed - you may skip the with-statement and write just client = get_davclient(). This will make it easier to test code from the python shell.

As of 2.0, it's recommended to start initiating a :class:`caldav.davclient.DAVClient` object using the get_davclient function, go from there to get a :class:`caldav.collection.Principal`-object, and from there find a :class:`caldav.collection.Calendar`-object. (I'm planning to add a get_calendar in version 3.0). This is how to do it:

from caldav.davclient import get_davclient
from caldav.lib.error import NotFoundError

with get_davclient() as client:
    my_principal = client.principal()
    try:
        my_calendar = my_principal.calendar()
        print(f"A calendar was found at URL {my_calendar.url}")
    except NotFoundError:
        print("You don't seem to have any calendars")

Caveat: Things will break if password/url/username is wrong, but perhaps not where you expect it to. To test, you may try out get_davclient(username='alice', password='hunter2',url='https://calendar.example.com/dav/').

The calendar-method above gives one calendar - if you have more calendars, it will give you the first one it can find - which may not be the correct one. To filter there are parameters name and cal_id - I recommend testing them:

from caldav.davclient import get_davclient
from caldav.lib.error import NotFoundError

with get_davclient() as client:
    my_principal = client.principal()
    try:
        my_calendar = my_principal.calendar(name="My Calendar")
    except NotFoundError:
        print("You don't seem to have a calendar named 'My Calendar'")

If you happen to know the URL or path for the calendar, you don't need to go through the principal object.

from caldav.davclient import get_davclient

with get_davclient() as client:
    my_calendar = client.calendar(url="/dav/calendars/mycalendar")

Note that in the example above, no communication is done. If the URL is wrong, you will only know it when trying to save or get objects from the server!

For servers that supports it, it may be useful to create a dedicated test calendar - that way you can test freely without risking to mess up your calendar events. Let's populate it with an event while we're at it:

from caldav.davclient import get_davclient
import datetime

with get_davclient() as client:
    my_principal = client.principal()
    my_new_calendar = my_principal.make_calendar(name="Test calendar")
    may17 = my_new_calendar.save_event(
        dtstart=datetime.datetime(2020,5,17,8),
        dtend=datetime.datetime(2020,5,18,1),
        uid="may17",
        summary="Do the needful",
        rrule={'FREQ': 'YEARLY'})

You have icalendar code and want to put it into the calendar? Easy!

from caldav.davclient import get_davclient

with get_davclient() as client:
    my_principal = client.principal()
    my_new_calendar = my_principal.make_calendar(name="Test calendar")
    may17 = my_new_calendar.save_event("""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//Example Corp.//CalDAV Client//EN
BEGIN:VEVENT
UID:20200516T060000Z-123401@example.com
DTSTAMP:20200516T060000Z
DTSTART:20200517T060000Z
DTEND:20200517T230000Z
RRULE:FREQ=YEARLY
SUMMARY:Do the needful
END:VEVENT
END:VCALENDAR
""")

The best way of getting information out from the calendar is to use the search. Currently most of the logic is done on the server side - and the different calendar servers tends to give different results given the same data and search query. In future versions of the CalDAV library the intention is to do more workarounds and logic on the client side, allowing for more consistent results across different servers.

from caldav.davclient import get_davclient
from datetime import date

with get_davclient() as client:
    my_principal = client.principal()
    my_new_calendar = my_principal.make_calendar(name="Test calendar")
    my_new_calendar.save_event(
        dtstart=datetime.datetime(2023,5,17,8),
        dtend=datetime.datetime(2023,5,18,1),
        uid="may17",
        summary="Do the needful",
        rrule={'FREQ': 'YEARLY'})

    my_events = my_new_calendar.search(
        event=True,
        start=date(2026,5,1),
        end=date(2026,6,1),
        expand=True)

    assert len(my_events) == 1
    print(my_events[0].data)

expand matters for recurring events and tasks, instead of getting returned the original event (with DTSTART set in 2023 and an RRULE set) it will return the recurrence for year 2026. Or, rather, a list of recurrences if there are more of them in the search interval.

event causes the search to only return events. There are three kind of objects that can be saved to a calendar (but not all servers support all three) - events, journals and tasks (VEVENT, VJOURNAL and VTODO). This is called Calendar Object Resources in the RFC. Now that's quite a mouthful! To ease things, the word "event" is simply used in documentation and communication. So when reading "event", be aware that it actually means "a CalenderObjectResource objects such as an event, but it could also be a task or a journal" - and if you contribute code, remember to use CalendarObjectResource rather than Event.

Without event=True explicitly set, all kind of objects should be returned. Unfortunately many servers returns nothing - so as of 2.0, it's important to always specify if you want events, tasks or journals. In future versions of CalDAV there will be workarounds for this so event=True can be safely skipped, regardless what server is used.

The return type is a list of objects of the type :class:`caldav.calendarobjectresource.Event` - for tasks and jornals there are similar classes Todo and Journal.

The data property delivers the icalendar data as a string. It can be modified:

from caldav.davclient import get_davclient
from datetime import date

with get_davclient() as client:
    my_principal = client.principal()
    my_new_calendar = my_principal.make_calendar(name="Test calendar")
    my_new_calendar.save_event(
        dtstart=datetime.datetime(2023,5,17,8),
        dtend=datetime.datetime(2023,5,18,1),
        uid="may17",
        summary="Do the needful",
        rrule={'FREQ': 'YEARLY'})

    my_events = my_new_calendar.search(
        start=date(2026,5,1),
        end=date(2026,6,1),
        expand=True)

    assert len(my_events) == 1
    my_events[0].data = my_events[0].data.replace("Do the needful", "Have fun!")
    my_events[0].save()

As seen above, we can use save() to send a modified object back to the server. In the case above, we've edited a recurrence. Now that we've saved the object, you're encouraged to test with search with and without expand set and with different years, print out my_event[0].data and see what results you'll get. The save()-method also takes a parameter all_recurrences=True if you want to edit the full series!

The code above is far from "best practice". You should not try to parse or modify event.data - it will be parsed and thrown into an object accessible as myevent.icalendar_instance. (in 3.0, probably myevent.instance will work out without yielding a DeprecationWarning).

Most of the time every event one gets out from the search contains one component - and it will always be like that when using expand=True. To ease things out for users of the library that wants easy access to the event data there is an my_events[9].icalendar_component property. From 2.0 also accessible simply as my_events[0].component:

from caldav.davclient import get_davclient
from datetime import date

with get_davclient() as client:
    my_principal = client.principal()
    my_new_calendar = my_principal.make_calendar(name="Test calendar")
    my_new_calendar.save_event(
        dtstart=datetime.datetime(2023,5,17,8),
        dtend=datetime.datetime(2023,5,18,1),
        uid="may17",
        summary="Do the needful",
        rrule={'FREQ': 'YEARLY'})

    my_events = my_new_calendar.search(
        start=date(2026,5,1),
        end=date(2026,6,1),
        expand=True)

    assert len(my_events) == 1
    print(f"Event starts at {my_events[0].component.start}")
    my_events[0].component['summary'] = "Norwegian national day celebrations"
    my_events[0].save()

There is a danger to this - there is one (and only one) exception when an event contains more than one component. If you've been observant and followed all the steps in this tutorial very carefully, you should have spotted it.

How to do operations on components and instances in the vobject and icalendar library is outside the scope of this tutorial - The icalendar library documentaiton can be found here as of 2025-06.

Usually tasks and journals can be applied directly to the same calendar as the events - but some implementations (notably Zimbra) has "task lists" and "calendars" as distinct entities. To create a task list, there is a parameter supported_calendar_component_set that can be set to ['VTODO']. Here is a quick example that features a task:

from caldav.davclient import get_davclient
from datetime import date

with get_davclient() as client:
    my_principal = client.principal()
    my_new_calendar = my_principal.make_calendar(
        name="Test calendar", supported_calendar_component_set=['VTODO'])
    my_new_calendar.save_todo(
        summary="prepare for the Norwegian national day", due=date(2025,5,16))

    my_tasks = my_new_calendar.search(
        todo=True)
    assert len(my_tasks) == 1
    my_tasks[0].complete()
    my_tasks = my_new_calendar.search(
        todo=True)
    assert len(my_tasks) == 0
    my_tasks = my_new_calendar.search(
        todo=True, include_completed=True)
    assert len(my_tasks) == 1

There are more functionality, but if you've followed the tutorial to this point, you should already know eough to deal with the very most use-cases.

There are some more examples in the examples folder, particularly basic examples. There is also a scheduling examples for sending, receiving and replying to invites, though this is not very well-tested so far. The example code is currently not tested nor maintained. Some of it will be moved into the documentation as tutorials or how-tos eventually.

The test code also covers most of the features available, though it's not much optimized for readability (at least not as of 2025-05).

Tobias Brox is also working on a command line interface built around the caldav library.