Sync an account to a local cache#

A complete script that keeps a SQLite cache of an account’s events in sync, using authentication and incremental sync together.

import sqlite3

from calendaring_jmap import get_jmap_client

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 ("
        "  id INTEGER PRIMARY KEY CHECK (id = 0),"
        "  token TEXT NOT NULL"
        ")"
    )
    conn.commit()


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


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


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

    while True:
        added, modified, deleted, old_state, new_state, has_more_changes = (
            client.get_objects_by_sync_token(token)
        )

        for obj in list(added) + list(modified):
            ical_str = obj.get_icalendar_instance().to_ical().decode()
            calendar_id = next(iter(obj.get_data()["calendarIds"]))
            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, new_state)
        conn.commit()

        token = new_state
        if not has_more_changes:
            break


if __name__ == "__main__":
    with get_jmap_client() as client:
        conn = sqlite3.connect(DB_PATH)
        init_db(conn)
        sync_account(client, conn)
        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.

JMAP state covers the whole account, not one calendar, so sync_state holds a single row for the whole account’s token, not one per calendar: get_objects_by_sync_token() returns every changed event across every calendar in the account in one call, and obj.get_data()["calendarIds"] says which calendar each one actually belongs to. An event can belong to more than one calendar; this cache only has room for one row per event id, so it stores whichever calendar id comes first.

The first run has no stored token, so it only records a baseline and caches nothing; the next run starts fetching real deltas. has_more_changes being True means the server truncated the page, not an error; the loop calls get_objects_by_sync_token() again with new_state until it’s False. See Handle errors for the errors this can still raise, such as an account-wide capability or auth failure.

Last updated on 2026-10-24.