Add multi-station support, GWR fares API, and Circle line improvements

- Support any station with direct trains to Paddington; station CRS code
  is now part of the URL (/results/<crs>/<slug>/<date>)
- Load station list from data/direct_to_paddington.tsv; show dropdown on
  index page; 404 for unknown station codes
- Fetch live GWR walk-on fares via api.gwr.com for all stations (SSS/SVS/SDS
  with restrictions already applied per train); cache 30 days
- Scrape Paddington arrival platform numbers from RTT
- Show unreachable morning Eurostars (before first reachable service only)
- Circle line: show actual KX St Pancras arrival times (not check-in estimate)
  and add a second backup service in the transfer column
- Widen page max-width to 1100px for longer station names

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Edward Betts 2026-04-06 20:22:44 +01:00
parent 71be0dd8cf
commit 3c787b33d3
12 changed files with 810 additions and 262 deletions

View file

@ -135,6 +135,19 @@ def next_service(earliest_board: datetime) -> tuple[datetime, datetime] | None:
The caller is responsible for adding any walk time from the GWR platform
before passing *earliest_board*.
"""
services = upcoming_services(earliest_board, count=1)
return services[0] if services else None
def upcoming_services(
earliest_board: datetime, count: int = 2
) -> list[tuple[datetime, datetime]]:
"""
Return up to *count* Circle line services from Paddington (H&C Line) to
King's Cross St Pancras, starting from *earliest_board*.
Each element is (depart_paddington, arrive_kings_cross) as datetimes.
"""
timetable = _get_timetable()[_day_type(earliest_board.weekday())]
board_secs = (
earliest_board.hour * 3600
@ -142,7 +155,13 @@ def next_service(earliest_board: datetime) -> tuple[datetime, datetime] | None:
+ earliest_board.second
)
midnight = earliest_board.replace(hour=0, minute=0, second=0, microsecond=0)
results = []
for pad_secs, kxp_secs in timetable:
if pad_secs >= board_secs:
return midnight + timedelta(seconds=pad_secs), midnight + timedelta(seconds=kxp_secs)
return None
results.append((
midnight + timedelta(seconds=pad_secs),
midnight + timedelta(seconds=kxp_secs),
))
if len(results) == count:
break
return results