Skip to content

Commit 9fe97cc

Browse files
tobixenclaude
andcommitted
docs: add async tutorial
Add docs/source/async_tutorial.rst, a step-by-step async tutorial that mirrors the existing sync tutorial.rst. Covers the same topics (creating calendars, accessing calendars, creating events, searching, investigating events, modifying events, tasks) plus a "Parallel Operations" section demonstrating asyncio.gather(). Also update tutorial.rst to link to the new async tutorial instead of saying "will come soon", and add async_tutorial to the docs index. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 6d6d442 commit 9fe97cc

4 files changed

Lines changed: 384 additions & 2 deletions

File tree

docs/source/async_tutorial.rst

Lines changed: 380 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,380 @@
1+
==============
2+
Async Tutorial
3+
==============
4+
5+
This tutorial covers async usage of the Python CalDAV client library.
6+
It mirrors :doc:`tutorial`, but uses the ``caldav.aio`` module. This
7+
tutorial assumes you've already browsed through the sync tutorial.
8+
9+
Copy code examples into a Python file and run them with ``python``. Do not
10+
name your file ``caldav.py`` or ``calendar.py``, as this may break imports.
11+
12+
All examples run inside an ``async def`` function launched via
13+
``asyncio.run()``. You are encouraged to add a ``breakpoint()`` inside the
14+
``async with`` blocks to inspect return objects.
15+
16+
Go through the tutorial twice, first against a Xandikos test server, and then
17+
against a server of your own choice.
18+
19+
Configuration
20+
-------------
21+
22+
The same applies here as in the sync tutorial, use ``export PYTHON_CALDAV_USE_TEST_SERVER=1`` and install Xandikos and the instructions below will give you a test server. Unset ``PYTHON_CALDAV_USE_TEST_SERVER`` and edit ``~/.config/caldav/calendar.conf`` or adjust the environment variables to test with a real server.
23+
24+
Creating Calendars
25+
------------------
26+
27+
The async API lives in ``caldav.aio``. Obtain a client by awaiting
28+
:func:`~caldav.aio.get_async_davclient`, then use it as an async context
29+
manager. When the ``async with`` block exits the HTTP session is closed.
30+
31+
.. code-block:: python
32+
33+
import asyncio
34+
from caldav import aio
35+
36+
async def main():
37+
client = await aio.get_async_davclient()
38+
async with client:
39+
my_principal = await client.get_principal()
40+
my_new_calendar = await my_principal.make_calendar(name="Teest calendar")
41+
## Enable the debug breakpoint to investigate the calendar object
42+
#breakpoint()
43+
await my_new_calendar.delete()
44+
45+
asyncio.run(main())
46+
47+
The delete step is unimportant when running towards an ephemeral test server.
48+
49+
The async version probes the server with an OPTIONS request by default (``probe=True``). It may and may not cause an immediate failure on wrong credentials, depending on the server setup. Feel free to play with it. This code will never fail:
50+
51+
.. code-block:: python
52+
53+
import asyncio
54+
from caldav import aio
55+
56+
async def main():
57+
## Invalid domain, invalid password ...
58+
## ... this probably ought to raise an error?
59+
client = await aio.get_async_davclient(
60+
username='alice',
61+
password='hunter2',
62+
url='https://calendar.example.com/dav/',
63+
probe=False)
64+
async with client:
65+
...
66+
67+
asyncio.run(main())
68+
69+
Accessing Calendars
70+
-------------------
71+
72+
Use :func:`aio.get_calendars` to list all calendars in one call. Like the
73+
sync version it returns a collection that can be used as an async context
74+
manager — the HTTP session is terminated on exit:
75+
76+
.. code-block:: python
77+
78+
import asyncio
79+
from caldav import aio
80+
81+
async def main():
82+
async with await aio.get_calendars() as calendars:
83+
for calendar in calendars:
84+
print(f"Calendar \"{await calendar.get_display_name()}\" has URL {calendar.url}")
85+
86+
asyncio.run(main())
87+
88+
:func:`aio.get_calendar` is the async counterpart of :func:`caldav.get_calendar`
89+
and is the **recommended starting point** for most code:
90+
91+
.. code-block:: python
92+
93+
import asyncio
94+
from caldav import aio
95+
96+
async def main():
97+
async with await aio.get_calendar() as calendar:
98+
print(f"Calendar \"{await calendar.get_display_name()}\" has URL {calendar.url}")
99+
## You may add a debugger breakpoint and investigate the object
100+
#breakpoint()
101+
102+
asyncio.run(main())
103+
104+
The calendar has a ``.client`` property which gives the client.
105+
106+
Creating Events
107+
---------------
108+
109+
From the :class:`~caldav.collection.Calendar` object, use
110+
:meth:`~caldav.collection.Calendar.add_event` to create an event:
111+
112+
.. code-block:: python
113+
114+
import asyncio
115+
import datetime
116+
from caldav import aio
117+
118+
async def main():
119+
async with await aio.get_calendar() as cal:
120+
## Add a may 17 event
121+
may17 = await cal.add_event(
122+
dtstart=datetime.datetime(2020,5,17,8),
123+
dtend=datetime.datetime(2020,5,18,1),
124+
uid="may17",
125+
summary="Do the needful",
126+
rrule={'FREQ': 'YEARLY'})
127+
## You may want to inspect the event
128+
#breakpoint()
129+
130+
asyncio.run(main())
131+
132+
You have icalendar code and want to put it into the calendar? Easy!
133+
134+
.. code-block:: python
135+
136+
import asyncio
137+
from caldav import aio
138+
139+
async def main():
140+
async with await aio.get_calendar() as cal:
141+
may17 = await cal.add_event("""BEGIN:VCALENDAR
142+
VERSION:2.0
143+
PRODID:-//Example Corp.//CalDAV Client//EN
144+
BEGIN:VEVENT
145+
UID:20200516T060000Z-123401@example.com
146+
DTSTAMP:20200516T060000Z
147+
DTSTART:20200517T060000Z
148+
DTEND:20200517T230000Z
149+
RRULE:FREQ=YEARLY
150+
SUMMARY:Do the needful
151+
END:VEVENT
152+
END:VCALENDAR
153+
""")
154+
#breakpoint()
155+
156+
asyncio.run(main())
157+
158+
159+
Searching
160+
---------
161+
162+
The search API is identical to the sync version; just add ``await``:
163+
164+
.. code-block:: python
165+
166+
import asyncio
167+
from caldav import aio
168+
from datetime import datetime, date
169+
170+
async def main():
171+
async with await aio.get_calendar() as cal:
172+
await cal.add_event(
173+
dtstart=datetime(2023,5,17,8),
174+
dtend=datetime(2023,5,18,1),
175+
uid="may17",
176+
summary="Do the needful",
177+
rrule={'FREQ': 'YEARLY'})
178+
179+
my_events = await cal.search(
180+
event=True,
181+
start=date(2026,5,1),
182+
end=date(2026,6,1),
183+
expand=True)
184+
185+
print(my_events[0].data)
186+
#breakpoint()
187+
188+
asyncio.run(main())
189+
190+
The ``expand``, ``event``, and other parameters work exactly as in the sync
191+
API. See the sync tutorial for a full explanation of the search options.
192+
193+
Investigating Events
194+
--------------------
195+
196+
Use ``.data`` for raw icalendar data, or
197+
:meth:`~caldav.calendarobjectresource.CalendarObjectResource.get_icalendar_component`
198+
for convenient property access:
199+
200+
.. code-block:: python
201+
202+
import asyncio
203+
from caldav import aio
204+
from datetime import datetime, date
205+
206+
async def main():
207+
async with await aio.get_calendar() as cal:
208+
await cal.add_event(
209+
dtstart=datetime(2023,5,17,8),
210+
dtend=datetime(2023,5,18,1),
211+
uid="may17",
212+
summary="Do the needful",
213+
rrule={'FREQ': 'YEARLY'})
214+
215+
my_events = await cal.search(
216+
event=True,
217+
start=date(2026,5,1),
218+
end=date(2026,6,1),
219+
expand=True)
220+
221+
print(my_events[0].get_icalendar_component()['summary'])
222+
print(my_events[0].get_icalendar_component().duration)
223+
#breakpoint()
224+
225+
asyncio.run(main())
226+
227+
The caveat about recurring events from the sync tutorial applies here too:
228+
``get_icalendar_component()`` is safe after an expanded search.
229+
230+
Modifying Events
231+
----------------
232+
233+
Replace the raw ``data`` string:
234+
235+
.. code-block:: python
236+
237+
import asyncio
238+
from caldav import aio
239+
from datetime import date
240+
import datetime
241+
242+
async def main():
243+
async with await aio.get_calendar() as cal:
244+
await cal.add_event(
245+
dtstart=datetime.datetime(2023,5,17,8),
246+
dtend=datetime.datetime(2023,5,18,1),
247+
uid="may17",
248+
summary="Do the needful",
249+
rrule={'FREQ': 'YEARLY'})
250+
251+
my_events = await cal.search(
252+
event=True,
253+
start=date(2026,5,1),
254+
end=date(2026,6,1),
255+
expand=True)
256+
257+
my_events[0].data = my_events[0].data.replace("Do the needful", "Have fun!")
258+
await my_events[0].save()
259+
#breakpoint()
260+
261+
asyncio.run(main())
262+
263+
Best practice is to use
264+
:meth:`~caldav.calendarobjectresource.CalendarObjectResource.edit_icalendar_component`:
265+
266+
.. code-block:: python
267+
268+
import asyncio
269+
from caldav import aio
270+
from datetime import date
271+
import datetime
272+
273+
async def main():
274+
async with await aio.get_calendar() as cal:
275+
await cal.add_event(
276+
dtstart=datetime.datetime(2023,5,17,8),
277+
dtend=datetime.datetime(2023,5,18,1),
278+
uid="may17",
279+
summary="Do the needful",
280+
rrule={'FREQ': 'YEARLY'})
281+
282+
my_events = await cal.search(
283+
event=True,
284+
start=date(2026,5,1),
285+
end=date(2026,6,1),
286+
expand=True)
287+
288+
## Edit the summary using the "borrowing pattern":
289+
with my_events[0].edit_icalendar_component() as event_ical:
290+
## "component" is always safe after an expanded search
291+
event_ical['summary'] = "Norwegian national day celebrations"
292+
await my_events[0].save()
293+
294+
## Let's take out the event again:
295+
may17 = await cal.get_event_by_uid('may17')
296+
297+
## Inspect may17 in a debug breakpoint
298+
#breakpoint()
299+
300+
asyncio.run(main())
301+
302+
Note that ``edit_icalendar_component()`` is a plain (synchronous) context
303+
manager — no ``await`` or ``async with`` needed there.
304+
305+
Tasks
306+
-----
307+
308+
Tasks work just like events, with ``await`` added:
309+
310+
.. code-block:: python
311+
312+
import asyncio
313+
from caldav import aio
314+
from datetime import date
315+
316+
async def main():
317+
client = await aio.get_async_davclient()
318+
async with client:
319+
my_principal = await client.get_principal()
320+
## This can be read as "create me a tasklist"
321+
cal = await my_principal.make_calendar(
322+
name="Test tasklist", supported_calendar_component_set=['VTODO'])
323+
## ... but for most servers it's an ordinary calendar!
324+
await cal.add_todo(
325+
summary="prepare for the Norwegian national day", due=date(2025,5,16))
326+
327+
my_tasks = await cal.search(todo=True)
328+
assert len(my_tasks) == 1
329+
await my_tasks[0].complete()
330+
my_tasks = await cal.search(todo=True)
331+
assert len(my_tasks) == 0
332+
my_tasks = await cal.search(todo=True, include_completed=True)
333+
assert my_tasks
334+
335+
asyncio.run(main())
336+
337+
The :meth:`~caldav.calendarobjectresource.Todo.complete` method is awaitable in
338+
async mode. See the sync tutorial for a note on tasklist vs calendar support
339+
differences between servers.
340+
341+
Parallel Operations
342+
-------------------
343+
344+
The main benefit of the async API is the ability to run multiple I/O operations
345+
*concurrently* using :func:`asyncio.gather`. The following example fetches
346+
events from all calendars at the same time, instead of one by one:
347+
348+
.. code-block:: python
349+
350+
import asyncio
351+
from caldav import aio
352+
353+
async def main():
354+
async with await aio.get_calendars() as calendars:
355+
## Kick off all searches in parallel, then collect the results
356+
results = await asyncio.gather(
357+
*[cal.search(event=True) for cal in calendars])
358+
359+
for cal, events in zip(calendars, results):
360+
print(f"{await cal.get_display_name()}: {len(events)} event(s)")
361+
362+
asyncio.run(main())
363+
364+
``asyncio.gather`` runs all the coroutines concurrently. For a single server
365+
the speed gain is modest (one connection), but when talking to multiple servers
366+
or doing many independent fetches the difference can be significant.
367+
368+
Further Reading
369+
---------------
370+
371+
See the :ref:`examples:examples` folder for more code, including
372+
`async examples <https://github.com/python-caldav/caldav/blob/master/examples/async_usage_examples.py>`_
373+
and `sync examples <https://github.com/python-caldav/caldav/blob/master/examples/basic_usage_examples.py>`_
374+
for comparison.
375+
376+
See :doc:`async` for the async API reference, including a migration guide from
377+
the sync API.
378+
379+
The `integration tests <https://github.com/python-caldav/caldav/blob/master/tests/test_async_integration.py>`_
380+
cover most async features.

docs/source/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Contents
2020
about
2121
v3-migration
2222
tutorial
23+
async_tutorial
2324
configfile
2425
async
2526
jmap

docs/source/tutorial.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ imports.
1111
Go through the tutorial twice, first against a Xandikos test server,
1212
and then against a server of your own choice.
1313

14-
This tutorial only covers the sync API. The async API is quite
15-
similar. A tutorial on the async API will come soon.
14+
This tutorial only covers the sync API. See :doc:`async_tutorial` for the
15+
async equivalent.
1616

1717
Ad-hoc Configuration
1818
--------------------

0 commit comments

Comments
 (0)