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.
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