This project builds an iCalendar (.ics) feed for a curated set of US holidays. The repository is optimized around serving that calendar from a stable URL that clients such as Apple Calendar, Google Calendar, and Outlook can subscribe to directly.
- Generates a static
us_holidays.icsfile. - Bundles the holiday definitions with the Python package so the CLI works outside the repo root.
- Serves the current year plus the next year by default, which keeps the feed practical without overcommitting to a long forecasting window.
- Supports editing the holiday definition file with
add-holidayandremove-holiday. - Treats weekend observance and per-holiday enable/disable switches as data in holidays.yaml, including leap-day safety.
- Python 3.13+
- Poetry
poetry installGenerate the current year plus the next year into us_holidays.ics:
poetry run generate_calendarGenerate a custom range:
poetry run generate_calendar --year 2025 --end-year 2030Write to a different path:
poetry run generate_calendar --output /tmp/us-holidays.icsPreview the build without writing a file:
poetry run generate_calendar --dry-runThe bundled holiday definitions live at src/generate_calendar/holidays.yaml.
Add a holiday to a specific YAML file:
poetry run generate_calendar add-holiday --holidays-file src/generate_calendar/holidays.yaml "National Pizza Day" 2 9Remove a holiday from a specific YAML file:
poetry run generate_calendar remove-holiday --holidays-file src/generate_calendar/holidays.yaml "National Pizza Day"If you run these commands against an installed, read-only package, pass --holidays-file so the CLI knows which editable YAML file to modify.
You can also disable a holiday without deleting it:
manual_holidays:
- name: National Ice Cream Day
month: 7
day: 21
enabled: falseIf enabled is omitted, the holiday is included by default.
The recommended permanent deployment path is Cloudflare Workers using the root-level wrangler.toml and package.json.
How it works:
- Wrangler runs build_static_calendar.py at deploy time to generate a bundled
.icsfallback artifact - the Worker serves the most recently stored calendar from Cloudflare KV and only falls back to the deploy-built artifact if KV is empty
- a monthly Cloudflare cron trigger refreshes the stored calendar on the schedule in wrangler.toml
- subscriber traffic only reads the stored calendar; it does not regenerate the feed
- the Worker returns the file with
text/calendarheaders from a stable HTTPS URL
The monthly cron runs at 00:00 UTC on the first day of each month (0 0 1 * *).
Setup steps:
- From the repo root, run
npm install. - Create the KV namespace once with
npx wrangler kv namespace create CALENDAR_CACHE. - Copy the returned IDs into the commented
[[kv_namespaces]]block in wrangler.toml. - Deploy from the repo root with
npx wrangler deploy. - Subscribe iCloud or any other calendar client to the Worker URL.
When the repository is connected to Cloudflare, merging to the configured production branch triggers the Cloudflare build and deploy. The Worker compares the bundled deploy-time calendar timestamp with CALENDAR_CACHE; if KV is stale, it serves the fresh bundled calendar and refreshes KV in the background so subscribers receive the new feed without waiting for the monthly cron.
update-calendar.yml is validation-only. It checks formatting, linting, typing, tests, and a dry-run calendar build on pushes and pull requests.
poetry run black --check .
poetry run flake8 .
poetry run mypy .
poetry run pytest -q
npm run worker:verify- src/generate_calendar/init.py: calendar generation logic and CLI entrypoint
- src/generate_calendar/holidays.yaml: bundled holiday definitions
- cloudflare/src/calendar.js: shared Worker calendar logic used by deploys and parity checks
- cloudflare/src/runtime.js: testable Worker runtime for fetch and scheduled behavior
- cloudflare/src/index.js: Worker runtime for KV-backed serving and scheduled refresh
- cloudflare/scripts/check-parity.mjs: parity check between Worker and Python holiday generation
- cloudflare/scripts/build_static_calendar.py: deploy-time build step for the bundled fallback
.ics - cloudflare/scripts/smoke-fetch.mjs: smoke test for bundle fallback, KV reads, and scheduled refresh writes
- tests/test_generate_calendar.py: hermetic tests
MIT