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:
parent
71be0dd8cf
commit
3c787b33d3
12 changed files with 810 additions and 262 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import pytest
|
||||
from trip_planner import combine_trips, find_unreachable_morning_eurostars, _fmt_duration, cheapest_gwr_ticket
|
||||
from trip_planner import combine_trips, find_unreachable_morning_eurostars, _fmt_duration
|
||||
|
||||
DATE = '2026-03-30'
|
||||
|
||||
|
|
@ -23,7 +23,7 @@ def test_fmt_duration_minutes_only():
|
|||
# ---------------------------------------------------------------------------
|
||||
|
||||
GWR_FAST = {'depart_bristol': '07:00', 'arrive_paddington': '08:45'} # 1h 45m
|
||||
GWR_SLOW = {'depart_bristol': '07:00', 'arrive_paddington': '09:26'} # 2h 26m — over limit
|
||||
GWR_SLOW = {'depart_bristol': '07:00', 'arrive_paddington': '09:26'} # 2h 26m — connection too short for ES_PARIS
|
||||
|
||||
ES_PARIS = {'depart_st_pancras': '10:01', 'arrive_destination': '13:34', 'destination': 'Paris Gare du Nord'}
|
||||
ES_EARLY = {'depart_st_pancras': '09:00', 'arrive_destination': '12:00', 'destination': 'Paris Gare du Nord'}
|
||||
|
|
@ -41,7 +41,7 @@ def test_valid_trip_is_returned():
|
|||
|
||||
|
||||
def test_gwr_too_slow_excluded():
|
||||
# 2h 26m GWR journey exceeds MAX_GWR_MINUTES (110)
|
||||
# arrive 09:26, Eurostar 10:01 → 35 min connection < 50 min minimum
|
||||
trips = combine_trips([GWR_SLOW], [ES_PARIS], DATE)
|
||||
assert trips == []
|
||||
|
||||
|
|
@ -148,56 +148,6 @@ def test_find_unreachable_eurostars_excludes_connectable_services():
|
|||
assert [s['depart_st_pancras'] for s in unreachable] == ['09:30', '12:30']
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# cheapest_gwr_ticket — Bristol Temple Meads → Paddington
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 2026-03-30 is a Monday; 2026-03-28 is a Saturday
|
||||
|
||||
def test_cheapest_ticket_weekday_super_off_peak_morning():
|
||||
# 05:00 on Monday: dep ≤ 05:04 → Super Off-Peak
|
||||
t = cheapest_gwr_ticket('05:00', '2026-03-30')
|
||||
assert t['ticket'] == 'Super Off-Peak'
|
||||
assert t['price'] == 45.00
|
||||
|
||||
def test_cheapest_ticket_weekday_anytime_window():
|
||||
# 07:00 on Monday: 05:05–08:25 → Anytime only
|
||||
t = cheapest_gwr_ticket('07:00', '2026-03-30')
|
||||
assert t['ticket'] == 'Anytime'
|
||||
assert t['price'] == 138.70
|
||||
|
||||
def test_cheapest_ticket_weekday_off_peak():
|
||||
# 08:30 on Monday: dep ≥ 08:26 but < 09:58 → Off-Peak
|
||||
t = cheapest_gwr_ticket('08:30', '2026-03-30')
|
||||
assert t['ticket'] == 'Off-Peak'
|
||||
assert t['price'] == 63.60
|
||||
|
||||
def test_cheapest_ticket_weekday_super_off_peak_late():
|
||||
# 10:00 on Monday: dep ≥ 09:58 → Super Off-Peak
|
||||
t = cheapest_gwr_ticket('10:00', '2026-03-30')
|
||||
assert t['ticket'] == 'Super Off-Peak'
|
||||
assert t['price'] == 45.00
|
||||
|
||||
def test_cheapest_ticket_boundary_super_off_peak_cutoff():
|
||||
# 05:04 is last valid minute for early Super Off-Peak
|
||||
assert cheapest_gwr_ticket('05:04', '2026-03-30')['ticket'] == 'Super Off-Peak'
|
||||
# 05:05 falls into the Anytime window (off-peak starts at 08:26)
|
||||
assert cheapest_gwr_ticket('05:05', '2026-03-30')['ticket'] == 'Anytime'
|
||||
|
||||
def test_cheapest_ticket_boundary_off_peak_start():
|
||||
assert cheapest_gwr_ticket('08:25', '2026-03-30')['ticket'] == 'Anytime'
|
||||
assert cheapest_gwr_ticket('08:26', '2026-03-30')['ticket'] == 'Off-Peak'
|
||||
|
||||
def test_cheapest_ticket_boundary_super_off_peak_resumes():
|
||||
assert cheapest_gwr_ticket('09:57', '2026-03-30')['ticket'] == 'Off-Peak'
|
||||
assert cheapest_gwr_ticket('09:58', '2026-03-30')['ticket'] == 'Super Off-Peak'
|
||||
|
||||
def test_cheapest_ticket_weekend_always_super_off_peak():
|
||||
# Saturday — no restrictions
|
||||
t = cheapest_gwr_ticket('07:00', '2026-03-28')
|
||||
assert t['ticket'] == 'Super Off-Peak'
|
||||
assert t['price'] == 45.00
|
||||
|
||||
def test_combine_trips_includes_ticket_fields():
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE)
|
||||
assert len(trips) == 1
|
||||
|
|
@ -206,6 +156,18 @@ def test_combine_trips_includes_ticket_fields():
|
|||
assert 'ticket_price' in t
|
||||
assert 'ticket_code' in t
|
||||
|
||||
def test_combine_trips_uses_gwr_fares_when_provided():
|
||||
fares = {'07:00': {'ticket': 'Super Off-Peak Single', 'price': 49.30, 'code': 'SSS'}}
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE, gwr_fares=fares)
|
||||
assert len(trips) == 1
|
||||
assert trips[0]['ticket_price'] == 49.30
|
||||
assert trips[0]['ticket_code'] == 'SSS'
|
||||
|
||||
def test_combine_trips_ticket_price_none_when_no_fares():
|
||||
trips = combine_trips([GWR_FAST], [ES_PARIS], DATE, gwr_fares={})
|
||||
assert len(trips) == 1
|
||||
assert trips[0]['ticket_price'] is None
|
||||
|
||||
|
||||
def test_find_unreachable_eurostars_returns_empty_when_all_connectable():
|
||||
gwr = [
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue