Skip to content

feat(realtime): real-time GTFS-RT departure delay buffer#12

Open
Multipixelone wants to merge 2 commits into
mainfrom
feat/realtime-gtfs-rt-departures
Open

feat(realtime): real-time GTFS-RT departure delay buffer#12
Multipixelone wants to merge 2 commits into
mainfrom
feat/realtime-gtfs-rt-departures

Conversation

@Multipixelone

Copy link
Copy Markdown
Owner

What

Adds C1 — real-time GTFS-RT departures: when the subway/LIRR/bus line a rider is about to board is actually running late, fold a capped, observed delay into the departure buffer so the alarm fires earlier on a degraded-service morning.

Conservative by design (the "honest, not misleading" call): a live delay can only move the leave time earlier — never later — and it never claims a precise new arrival (that would need static trip data we don't carry). It mirrors the existing weather-buffer seam end to end.

How it works

  • realtime.pyrealtime_delay(route, at_time, config) -> RealtimeDelay, with an injectable fetcher, a 60s per-system TTL feed memo, and a fail-open contract (any error/miss → zero buffer, never a failed plan). It takes the first boarding leg, fuzzy-matches its stop name to GTFS stop_ids (rapidfuzz over bundled tables, scoped by system), fetches only that system's trip-update feeds, picks the predicted departure closest to schedule, floors at zero, caps at max_buffer_minutes, and ignores sub-threshold jitter.
  • gtfs_rt.py — shared feed fetch/validate helper, reused by mta.py (alerts) and realtime.py (trip updates).
  • Bundled datadata/stops_{subway,lirr,bus}.csv (generated from MTA static GTFS by scripts/build_stops.py).
  • WiringRealtimeConfig (disabled by default); Plan.realtime_buffer_minutes / realtime_reason; planner folds the delay into the buffer alongside weather; format.py surfaces a 🚇 +N min — Q running ~6 min late advisory.
  • CLI + skill paritycommutecompass realtime diagnostic command, realtime.sh, SKILL.md row, allowlist + example-config docs.

Design decisions (settled up front)

Decision Choice
Delay semantics Conservative buffer + advisory (leave earlier only)
Stop mapping Bundled stops.txt subset + rapidfuzz
Feed scope Subway + LIRR + bus

Testing

  • tests/test_realtime.py: delay/cap/threshold/window/fuzzy/system-scoping/LIRR/bus/fail-open + feed parsing.
  • Planner + CLI coverage added.
  • mypy strict: clean · ruff: clean · full suite 645 passing · coverage 84% (≥80% gate).

Packaging note

Couldn't run a real python -m build in the dev sandbox (no build frontend / offline). The CSVs are confirmed not gitignored (hatchling bundles them by default) and explicit artifacts/sdist entries were added as backup. Worth a quick python -m build && unzip -l dist/*.whl | grep stops before release.

Out of scope (follow-ups)

  • Full real-time reschedule (pick a different train, recompute arrival) — needs static schedule data.
  • Delay on downstream/transfer legs (only the first boarding leg is actionable here).
  • GTFS-RT vehicle positions; LIRR branch/track-level precision.

… (GTFS-RT)

Google Directions plans against the static schedule. When the subway/LIRR/bus
line a rider is about to board is actually running late, fold a capped, observed
delay into the departure buffer so the alarm fires earlier on a degraded-service
morning. Conservative by design: a live delay can only move the leave time
*earlier* (never later) and never claims a precise new arrival.

Mirrors the weather-buffer seam end to end:
- realtime.py: realtime_delay(route, at_time, config) -> RealtimeDelay, with an
  injectable fetcher, a 60s per-system TTL feed memo, and a fail-open contract
  (any error/miss -> zero buffer, never a failed plan). Matches the first
  boarding leg's stop name to GTFS stop_ids via rapidfuzz over bundled tables,
  scoped by system; picks the predicted departure closest to schedule; floors at
  zero, caps at max_buffer_minutes, ignores sub-threshold jitter.
- gtfs_rt.py: shared feed fetch/validate helper, reused by mta.py (alerts) and
  realtime.py (trip updates).
- data/stops_{subway,lirr,bus}.csv + scripts/build_stops.py: bundled GTFS stop
  tables (generated from MTA static GTFS) and their refresh generator.
- config.RealtimeConfig (disabled by default); Plan.realtime_buffer_minutes /
  realtime_reason; planner folds the delay into the buffer; format.py surfaces a
  "+N min - Q running ~6 min late" advisory.
- `commutecompass realtime` diagnostic command + skill-script parity and docs.

Tests: tests/test_realtime.py (delay/cap/threshold/window/fuzzy/system-scoping/
LIRR/bus/fail-open + feed parsing), plus planner and CLI coverage. mypy strict
clean; full suite 645 passing; coverage 84% (>=80% gate).
The nix flake check sandbox sets SSL_CERT_FILE to a non-existent path, so
constructing a real httpx.Client in _fetch_predictions raised FileNotFoundError
during create_ssl_context. The tests mock fetch_feed_message (the I/O) but the
client was still built. Patch httpx.Client too, matching the existing
test_geocode/test_ha_client idiom, so these tests no longer touch real SSL.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant