Sync a calendar to a local cache#

A complete script that keeps a SQLite cache of one calendar’s events in sync, using authentication, incremental sync, and error handling together.

import sqlite3

from calendaring_jmap import get_jmap_client
from calendaring_jmap.error import JMAPMethodError

DB_PATH = "events.db"


def init_db(conn):
    conn.execute(
        "CREATE TABLE IF NOT EXISTS events ("
        "  id TEXT PRIMARY KEY,"
        "  calendar_id TEXT NOT NULL,"
        "  ical TEXT NOT NULL"
        ")"
    )
    conn.execute(
        "CREATE TABLE IF NOT EXISTS sync_state ("
        "  calendar_id TEXT PRIMARY KEY,"
        "  token TEXT NOT NULL"
        ")"
    )
    conn.commit()


def load_token(conn, calendar_id):
    row = conn.execute(
        "SELECT token FROM sync_state WHERE calendar_id = ?", (calendar_id,)
    ).fetchone()
    return row[0] if row else None


def save_token(conn, calendar_id, token):
    conn.execute(
        "INSERT INTO sync_state (calendar_id, token) VALUES (?, ?) "
        "ON CONFLICT(calendar_id) DO UPDATE SET token = excluded.token",
        (calendar_id, token),
    )
    conn.commit()


def sync_calendar(client, conn, calendar_id):
    token = load_token(conn, calendar_id)
    if token is None:
        token = client.get_sync_token()
        save_token(conn, calendar_id, token)
        return

    try:
        added, modified, deleted, token = client.get_objects_by_sync_token(token)
    except JMAPMethodError as e:
        if e.error_type == "serverPartialFail":
            # The server dropped our baseline; start over from a fresh token.
            token = client.get_sync_token()
            save_token(conn, calendar_id, token)
            return
        raise

    for obj in list(added) + list(modified):
        ical_str = obj.get_icalendar_instance().to_ical().decode()
        conn.execute(
            "INSERT INTO events (id, calendar_id, ical) VALUES (?, ?, ?) "
            "ON CONFLICT(id) DO UPDATE SET ical = excluded.ical",
            (obj.id, calendar_id, ical_str),
        )
    for event_id in deleted:
        conn.execute("DELETE FROM events WHERE id = ?", (event_id,))

    save_token(conn, calendar_id, token)
    conn.commit()


if __name__ == "__main__":
    conn = sqlite3.connect(DB_PATH)
    init_db(conn)

    with get_jmap_client() as client:
        for cal in client.get_calendars():
            sync_calendar(client, conn, cal.id)

    conn.close()

This reads configuration the same way every other example does, through get_jmap_client()’s explicit-argument/environment-variable/config-file chain; see Authenticate for how to set that up.

The first run for a calendar has no stored token, so it only records a baseline and caches nothing; the next run starts fetching real deltas. get_objects_by_sync_token()’s JMAPMethodError with error_type="serverPartialFail" means the server truncated the change list; the same error-handling pattern applies here as anywhere else this error can occur.

Last updated on 2026-10-08.