Work with events#
Events are passed as iCalendar strings, so existing iCalendar-producing code works unchanged.
Add an event#
cal = calendars[0]
ical = (
"BEGIN:VCALENDAR\r\n"
"VERSION:2.0\r\n"
"PRODID:-//example//EN\r\n"
"BEGIN:VEVENT\r\n"
"UID:meeting-2026-01-15@example.com\r\n"
"SUMMARY:Team meeting\r\n"
"DTSTART:20260115T100000Z\r\n"
"DTEND:20260115T110000Z\r\n"
"END:VEVENT\r\n"
"END:VCALENDAR\r\n"
)
# Returns the server-assigned JMAP event ID
event_id = cal.add_event(ical)
Look up an event#
By its iCalendar UID, from a JMAPCalendar:
obj = cal.get_object_by_uid("meeting-2026-01-15@example.com")
Or by its JMAP event ID, directly on the client, for example if you already have the ID from a get_objects_by_sync_token() result:
obj = client.get_event(event_id)
Both return a JMAPCalendarObject. Call get_icalendar_instance() on it for an icalendar.Calendar, or get_data() for the raw JSCalendar dict.
Update an event#
Edit the object in place, then call save():
obj = cal.get_object_by_uid("meeting-2026-01-15@example.com")
with obj.edit_icalendar_instance() as ical:
ical.subcomponents[0]["SUMMARY"] = "Team standup"
obj.save()
save() needs an object with its parent calendar set, since it uses that calendar’s client to send the update. Objects from cal.get_object_by_uid and search() have it set; objects from get_event() don’t, since they aren’t fetched through a calendar. It’s also sync-only; on an async-backed calendar, call update_event() directly instead.
If you already have a full replacement iCalendar string, update_event also works directly on the client:
client.update_event(event_id, updated_ical_str)
Delete an event#
client.delete_event(event_id)
Search events#
Use search() on a calendar object:
cal = calendars[0]
# All events in this calendar
results = cal.search()
# Time-range filter: events that overlap [start, end)
# start: only events ending after this datetime
# end: only events starting before this datetime
results = cal.search(
start="2026-01-01T00:00:00",
end="2026-02-01T00:00:00",
)
# Free-text search across title, description, locations, and participants
results = cal.search(text="standup")
for obj in results:
print(obj.get_icalendar_instance())
All parameters are optional. Omitting all of them returns every event in the calendar. Results are a list of JMAPCalendarObject. The search uses a single batched JMAP request (CalendarEvent/query plus a result reference into CalendarEvent/get), so only one HTTP round trip is made regardless of how many events match.
Search across every calendar#
search_events() searches the whole account rather than one calendar, and adds four filters cal.search() doesn’t have:
results = client.search_events(
has_attachment=True,
participant_email="alice@example.com",
participation_status="accepted",
participant_role="chair",
)
These four filter client-side, in Python, against whatever the server-side filters already matched, since none of them have a corresponding JMAP FilterCondition property. All filters combine as AND, but the three participant-based ones are each satisfied independently across an event’s participants: an event where one participant matches participant_email and a different participant matches participant_role still matches both.
Cyrus drops an attachment Link’s rel: "enclosure" on read-back (seen against a running instance), so has_attachment=True misses real attachments there. Both Cyrus and Stalwart drop the attendee role from a participant’s roles map, so participant_role="attendee" misses matches on both; a participant who is also chair keeps that role and still matches on it.
Results from search_events have no parent calendar set, unlike results from cal.search(), so save() raises JMAPMethodError on them; use cal.search() instead when you need to edit results in place with save().
Use the async client#
AsyncJMAPClient mirrors every method of JMAPClient as a coroutine. Use it as an async with context manager (sync with isn’t supported):
import asyncio
from calendaring_jmap import get_async_jmap_client
async def main():
async with get_async_jmap_client(
url="https://jmap.example.com/.well-known/jmap",
username="alice",
password="secret",
) as client:
calendars = await client.get_calendars()
cal = calendars[0]
# Calendar-scoped methods return coroutines when the calendar
# was obtained from an async client
results = await cal.search()
obj = await cal.get_object_by_uid("some-uid@example.com")
event_id = await cal.add_event(obj.get_icalendar_instance().to_ical().decode())
asyncio.run(main())
Event CRUD, search, sync, and task operations are all available as coroutines with identical signatures. The async client uses niquests.AsyncSession internally; niquests is already a required dependency, so no extra install step is needed for async use.