Skip to content

Repository files navigation

Alma Calendar Sync

Push centrally-managed holidays (exceptions) and semester dates (events) to CUNY Alma IZ calendars via the Open Hours API.

Why this exists: Alma's NZ-to-IZ calendar distribution is additive — it adds entries but never removes old ones. When a date changes, you end up with duplicates in every IZ. This script replaces NZ distribution with direct API updates that cleanly replace your entries without touching each campus's opening hours.

How It Works

  1. You define your events and exceptions in calendar_entries.json
  2. The script reads the config and, for each IZ:
    • GETs the institution-level calendar
    • Removes existing entries that match your config entries (by date or description)
    • Adds the current versions of your entries
    • Leaves opening hours untouched (WEEK entries are never modified)
    • PUTs the updated calendar back
  3. Dry-run by default — use --apply to commit changes

Setup

1. Create a virtual environment

cd alma-calendar-sync
python3 -m venv venv
source venv/bin/activate
pip install -r requirements.txt

2. Add API keys

Copy the example and fill in your keys:

cp api_keys.csv.example api_keys.csv

Each IZ needs an API key with "Configuration - Production Read/Write" permission.

3. Create your calendar config

Copy the example and edit your dates:

cp calendar_entries.json.example calendar_entries.json

See Config File Format below.

Usage

Dry run (default) — see what would change

python3 src/sync_calendar.py

Apply changes

python3 src/sync_calendar.py --apply

Test with one IZ first

python3 src/sync_calendar.py --iz 01CUNY_QC
python3 src/sync_calendar.py --iz 01CUNY_QC --apply

All options

--apply           Actually push changes (default: dry-run)
--iz CODE         Process only this IZ code (e.g., 01CUNY_QC)
--config PATH     Path to calendar_entries.json (default: ./calendar_entries.json)
--keys PATH       Path to api_keys.csv (default: ./api_keys.csv)

Config File Format

calendar_entries.json has three sections:

Groups

Define which IZs belong to which calendar group. An IZ can be in one group.

"groups": {
  "default": {
    "description": "Most CUNY schools",
    "iz_codes": ["01CUNY_BB", "01CUNY_BC", ...]
  },
  "alt_calendar": {
    "description": "Guttman, Kingsborough, LaGuardia",
    "iz_codes": ["01CUNY_NC", "01CUNY_KB", "01CUNY_LG"]
  }
}

Exceptions (holidays and closures)

"exceptions": [
  {
    "desc": "Independence Day",
    "from_date": "2026-07-03",
    "to_date": "2026-07-03",
    "status": "CLOSED",
    "applies_to": "all"
  }
]
  • applies_to: "all" for every IZ, or a group name like "default" or "alt_calendar"
  • status: Always "CLOSED" for full-day closures
  • Don't put the year in desc. The date is already shown alongside the description in both Alma and Primo VE, so "Labor Day" reads the same as "Labor Day 2026". Yearless names also help the script auto-sweep last year's entries via description match when you roll to a new academic year.

Events (semester dates)

"events": [
  {
    "desc": "End of Spring Semester",
    "from_date": "2026-06-16",
    "to_date": "2026-06-16",
    "applies_to": "default"
  }
]

Events are informational dates that appear on the calendar but don't affect open/closed status.

Retired entries (one-time cleanup)

Use retired_entries for past entries you previously pushed but no longer want anywhere. The script removes them but never re-adds them.

"retired_entries": [
  {
    "desc": "Memorial Day 2026",
    "from_date": "2026-05-25",
    "to_date": "2026-05-25"
  }
]

Matching is strict: desc AND from_date AND to_date must all match exactly. This protects any campus-created entries that happen to share a description. Once every IZ is cleaned up, you can delete the entry from this section.

How Matching Works

When the script decides which existing entries to remove, it uses three rules:

  1. Description match — if an existing entry has the same description as any active config entry, it gets removed
  2. Exact date match — if an existing entry's date range is identical to an active config entry's date range, it gets removed
  3. Retired exact match — if desc + from_date + to_date all match a retired_entries row, it gets removed (and not re-added)

This handles both cases:

  • You change the name of a holiday but not the date → the old entry is caught by exact date match
  • You move a date (e.g., semester end June 15 → June 16) → the old entry is caught by description match

Anything that doesn't match any rule is left alone (campus-managed entries, opening hours). Match rule #2 is intentionally exact (not overlap), so long-running campus entries like "Summer Hours" (6/1–8/19) aren't swept just because a single-day config entry falls inside them.

Safety Features

  • Dry-run by default — nothing changes unless you pass --apply
  • Backup before every PUT — saved to data/backups/
  • Verification after PUT — confirms WEEK entries weren't lost
  • Abort on error — stops processing remaining IZs if one fails

API Key Permissions

Each IZ needs an API key with: Configuration - Production Read/Write

Generate keys at: https://developers.exlibrisgroup.com/

About

Sync holidays and semester dates to CUNY Alma institution calendars via the Open Hours API. Dry-run by default; supports per-institution overrides.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages