Skip to main content

CalDAV-Cal-API

[!WARNING] This project is fully vibecoded. Every line was written by Claude Code, from an architecture I specified but did not hand-write, and I have not audited it line by line. Read it before pointing it at a calendar you care about, and keep backups.

It exists because I wanted what my CalDAV-Tasks-API gives me for tasks (VTODOs), but for calendar events (VEVENTs). It therefore mimics that project's layout and architecture on purpose, so the two feel like the same library from the outside.

Python library and command-line interface for CalDAV calendars (VEVENTs). Connect to a CalDAV server, read your calendars and events as plain Python objects, and create, modify or delete events without touching iCalendar text by hand.

Table of Contents

Motivation and Purpose

Reading a calendar from Python usually means either driving the caldav library directly, which leaves you parsing iCalendar and juggling timezones, or reaching for something that hides so much you cannot tell what it sends to the server. This library sits between the two:

  1. Calendars and events as ordinary dataclasses you can read, mutate and write back.
  2. Timezone handling that is correct by default, including across DST transitions.
  3. A small read-only CLI for the things you would otherwise write a throwaway script for.
  4. Few dependencies, so it stays easy to drop into a larger project.

Compatibility

Developed and tested against Nextcloud. It should work with any RFC 4791 server; reports from other servers are welcome. Pass nextcloud_mode=False if your server does not use Nextcloud's remote.php/dav/ path layout.

Features

  • Read calendars and events into plain Python objects.
  • Create, update and delete events.
  • Timezone-aware datetimes, normalized to UTC internally while preserving the original TZID.
  • All-day events as date objects, with the exclusive DTEND handled correctly.
  • Recurrence stored raw (RRULE / RDATE / EXDATE) with an opt-in get_occurrences() expander that respects DST.
  • Properties this library does not model (attendees, organizer, alarms, and so on) are preserved verbatim across updates.
  • Read-only mode, for dry runs and for code that must not write.
  • A read-only CLI: list-calendars, list-upcoming, search, dump.
  • Configuration via environment variables.

Design decisions worth knowing

The cache is a window, not the calendar. A calendar can hold decades of events, so load_remote_data() fetches a bounded date range: by default the last 30 days and the next year. An event outside that range is simply not loaded, and get_event_by_global_uid() will return None for it. Pass fetch_all=True (or set CALDAV_CAL_API_FETCH_ALL) when you genuinely need everything.

Only the master event is modeled. Recurrence is stored raw and expanded on demand. Per-occurrence modifications (RECURRENCE-ID overrides) are not represented; when one is encountered it is dropped with a warning rather than silently. There is no "this event / this and future / all events" edit distinction.

DTEND is exclusive. A single all-day event on 4 March has dtstart=date(2026, 3, 4) and dtend=date(2026, 3, 5). Use the last_day property when you want the inclusive final day for display.

Unmodeled properties survive. Each event keeps its source component, and writing rewrites only the properties this library owns. Changing a meeting's summary will not strip its attendees or everyone's reminders.

Installation

uv pip install caldav-cal-api

From source:

git clone <repository_url>
cd caldav_cal_api_repo

uv pip install .          # Runtime only
uv pip install -e .       # Editable
uv pip install -e ".[dev]"  # With test and release tooling

Requires Python 3.10 or newer.

Configuration

Every setting can come from an environment variable; see .env.example for a copy-paste starting point.

Variable Purpose
CALDAV_CAL_API_URL CalDAV server URL
CALDAV_CAL_API_USERNAME CalDAV username
CALDAV_CAL_API_PASSWORD CalDAV password
CALDAV_CAL_API_DEFAULT_CALENDAR_UID Calendar used when a call does not name one
CALDAV_CAL_API_WINDOW_START_DAYS Load window start, in days from now (default -30)
CALDAV_CAL_API_WINDOW_END_DAYS Load window end, in days from now (default 365)
CALDAV_CAL_API_FETCH_ALL 1/true/yes/on to ignore the window and load everything
CALDAV_CAL_API_LOG_LEVEL Console log level (default INFO; the log file is always DEBUG)
CALDAV_CAL_API_TEST_URL Test server URL (server-backed tests only)
CALDAV_CAL_API_TEST_USERNAME Test username
CALDAV_CAL_API_TEST_PASSWORD Test password
CALDAV_CAL_API_TEST_CALENDAR_NAME Scratch calendar the write tests may modify

Usage

Python API

import datetime
from zoneinfo import ZoneInfo

from caldav_cal_api import CalendarAPI, EventData

api = CalendarAPI(
    url="https://your-server.com/",
    username="your-username",
    password="your-password",
    # target_calendars=["Personal"],  # Loading fewer calendars is the biggest speedup
    # window_start=-7, window_end=90,  # Day offsets from now; default -30 / 365
    # fetch_all=True,                  # Ignore the window entirely (can be slow)
    # read_only=True,                  # Refuse every write
)

api.load_remote_data()

for calendar in api.calendars:
    print(f"{calendar.name}: {len(calendar.events)} event(s)")
    for event in calendar:
        print(f"  {event.dtstart_local}  {event.summary}")

# Create an event
paris = ZoneInfo("Europe/Paris")
start = datetime.datetime(2026, 5, 1, 14, 30, tzinfo=paris)
event = EventData(
    summary="Coffee with Sam",
    location="The usual place",
    dtstart=start,
    dtend=start + datetime.timedelta(hours=1),
    calendar_uid=api.calendars[0].uid,
)
created = api.add_event(event)

# Update it
created.summary = "Coffee with Sam (moved)"
api.update_event(created)

# An all-day event: DTEND is exclusive, so this covers 4 and 5 March only
holiday = EventData(
    summary="Long weekend",
    dtstart=datetime.date(2026, 3, 4),
    dtend=datetime.date(2026, 3, 6),
    calendar_uid=api.calendars[0].uid,
)
api.add_event(holiday)
print(holiday.all_day, holiday.last_day)  # True, 2026-03-05

# Expand a recurring event, correctly across DST
weekly = EventData(
    summary="Standup",
    dtstart=datetime.datetime(2026, 3, 11, 9, 0, tzinfo=paris),
    dtend=datetime.datetime(2026, 3, 11, 9, 15, tzinfo=paris),
    rrule="FREQ=WEEKLY;BYDAY=WE",
    calendar_uid=api.calendars[0].uid,
)
for occurrence in weekly.get_occurrences(
    datetime.datetime(2026, 3, 1, tzinfo=datetime.timezone.utc),
    datetime.datetime(2026, 4, 15, tzinfo=datetime.timezone.utc),
):
    print(occurrence.dtstart.astimezone(paris))  # Always 09:00 local

# Delete it
created.delete()

Command Line Interface

All CLI commands are read-only.

# List the calendars on the server (JSON)
caldav-cal-api list-calendars

# The next week's agenda, recurring events expanded
caldav-cal-api list-upcoming --days 7

# Restrict to one calendar and emit JSON
caldav-cal-api list-upcoming --calendar Personal --json

# Find events mentioning "dentist" in a given range
caldav-cal-api search dentist --start 2026-01-01 --end 2026-12-31

# Dump raw VEVENTs, e.g. to diff a calendar into version control
caldav-cal-api dump --calendar Personal

Every command also works as python -m caldav_cal_api <command>, and supports --help, --debug (verbose logging plus an interactive console with api in scope), and the shared connection options.

Examples

The examples/ directory holds runnable scripts: weekly_agenda.py, shift_events.py, dump_all_calendars_for_git.py (one text file per calendar) and dump_calendars_for_git.py (one .ics file per event). Each carries PEP 723 inline metadata, so uv resolves their dependencies on the fly:

uv run examples/weekly_agenda.py --days 7

They read a .env file at the repository root, so nothing needs exporting by hand.

Testing

uv pip install -e ".[dev]"
pytest

The offline tests (data model, iCalendar serialization, recurrence expansion) run in a bare checkout. The server-backed tests are skipped unless the four CALDAV_CAL_API_TEST_* variables are set; point CALDAV_CAL_API_TEST_CALENDAR_NAME at a scratch calendar, since those tests create and delete events in it.

Contributing

Issues and pull requests are welcome. Please keep to the existing style (black, NumPy-style docstrings) and add a test for any bug you fix.

This project was written with the help of Claude Code.

Release files for caldav-cal-api 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for caldav-cal-api 0.1.0
File Size Uploaded
caldav_cal_api-0.1.0.tar.gz 61.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for caldav-cal-api 0.1.0
File Interpreter ABI Platform
caldav_cal_api-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 112.0 kB

Release files / caldav_cal_api-0.1.0.tar.gz

Download URL caldav_cal_api-0.1.0.tar.gz
Size 61.6 kB
Tags Source
SHA-256 checksum
How to use checksums
2b20d10df7c1661e700d51e0fab6fe68e447ed8d5311c7400db651042eb52a36
BLAKE2b-256 checksum
How to use checksums
1b250e2694c849e397a5cd1d18c83d6127ad226d276e2df501dfdca94224f647
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.5

Release files / caldav_cal_api-0.1.0-py3-none-any.whl

Download URL caldav_cal_api-0.1.0-py3-none-any.whl
Size 50.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
47ccd64557ace0368757a0e533e13c5905abe9e6f94628fc66016b49228812a5
BLAKE2b-256 checksum
How to use checksums
3d5e6623e10ffa57a5c6d05d4ab4c96034345048430bb41fcd1b2f86dbbc2c3b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.13.5

Release history Release notifications | RSS feed

0.1.1

2 release files

This release

0.1.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page