pantheon-ical
Round-trip iCal (RFC 5545) read + write for booking / availability calendars — in under 200 lines.
The guarantee: import busy dates from the systems that speak iCal (Airbnb, Booking.com, Google Calendar, Vrbo, …), and export your own busy dates back as a feed they import — with the same half-open date model on both sides, so a block on any channel propagates to the others without double-booking.
Extracted from PANTHEON, where it drives two-way calendar sync for the holiday-let and appointment businesses it hosts.
Most iCal libraries are large, general-purpose, and read-only. This is the small, opinionated subset you actually need to keep an availability calendar in sync — and it does the export side too, which is the half most libraries skip.
What it does
from pantheon_ical import parse_ical, build_ical, parse_ical_slots
# 1. Import: an OTA feed → busy DATE ranges (half-open, checkout day exclusive)
busy = parse_ical(text) # [(date(2026,7,1), date(2026,7,5)), ...] — the 1st–5th = 4 nights, 5th free
# 2. Export: your busy ranges → a VCALENDAR feed OTHER systems import
feed = build_ical(busy, uid_ns="my-listing")
# → hand this URL to Booking.com/Airbnb "Import calendar" and your blocks propagate outward
# 3. Appointments: DATETIME slots, timezone-aware (for class/booking calendars)
slots = parse_ical_slots(text) # [(datetime, datetime), ...]
Design choices that matter (learned against real OTA feeds)
- Half-open intervals, checkout-day exclusive.
DTSTART:20260701 .. DTEND:20260705blocks four nights and leaves the 5th free for the next arrival — matching how every booking system models a stay. - RRULE recurrence is expanded around now, not DTSTART. An "every Monday since 2024" rule has a years-old start date; a naive expander would only produce historical dates and leave the live window bookable. This anchors the horizon to the present.
- Unparseable rules fall back to the master occurrence. A recurrence syntax we don't model can never silently under-block real availability (which is what causes a double-booking) — the safe failure direction.
build_icalis deterministic (no wall-clock) and emits only busy dates + a fixed generic summary — so a public feed leaks no guest data, and an unchanged calendar is byte-identical (ETag/cache friendly).
Install
pip install pantheon-ical # or copy the single pantheon_ical.py file
Depends only on python-dateutil (for RRULE expansion).
Changelog
-
0.2.0 — an oversized calendar now raises
ICalTooLargeinstead of silently returning a truncated busy-list.parse_icalcapped atmax_events=730and returned a plain list, so a caller could not tell "this calendar has 700 busy periods" from "this calendar had 5,000 and you are seeing 730". For an availability feed the error runs the wrong way: a busy period that was never parsed reads as FREE, so the caller offers a date the owner has already sold.ICalTooLargesubclassesValueError; passon_overflow="truncate"to opt back into a partial list as an explicit decision. The work bound is unchanged either way. -
0.1.1 — DTSTART+DURATION support (OTA feeds emit it) so a durational booking blocks its full span, not a single night (the exact silent under-block → double-book this guards against); and the RRULE expansion fallback now logs a warning instead of failing silently, so a broken/missing dateutil is visible rather than quietly under-blocking recurring dates.
-
0.1.0 — initial release.
License
Apache-2.0. See LICENSE.
Release files for pantheon-ical 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pantheon_ical-0.3.1.tar.gz | 17.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pantheon_ical-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 31.3 kB
Release files / pantheon_ical-0.3.1.tar.gz
| Download URL | pantheon_ical-0.3.1.tar.gz |
|---|---|
| Size | 17.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
acc8059ab16ef356b381ddf553288f45513def6fad3792f348c1d34d8dc57c59
|
|
BLAKE2b-256 checksum How to use checksums |
bad90cb3de5fc93b81a8835d1c524aecb896886ee00c6fea041c937bd6b486ce
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency logRelease files / pantheon_ical-0.3.1-py3-none-any.whl
| Download URL | pantheon_ical-0.3.1-py3-none-any.whl |
|---|---|
| Size | 13.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
33fbbad50a07fb51c9b16aac2fee91ceff3fd1b3bdadc84b45369c363a966726
|
|
BLAKE2b-256 checksum How to use checksums |
639531d35181db52d36b053717610199f5f1c915f1c817dce15e599b77901853
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 10, 2026.
Transparency log