Add Eurostar Plus prices and NR advance fare support
- Eurostar scraper now fetches both Standard and Plus (PLUS class code) prices/seats in a single API call; each service dict gains plus_price and plus_seats fields - GWR fares scraper gains fetch_advance() which makes two sets of paginated calls (standard advance + first-class advance) and returns cheapest per departure; shared _run_pages() generator reduces duplication in fetch() - New /api/advance_fares/<station_crs>/<travel_date> endpoint returns advance fares as JSON, cached for 24 hours - Results page gains NR ticket selector (Walk-on / Std Advance / 1st Advance) and Eurostar selector (Standard / Plus); total column is JS-computed from the selected combination with cheapest/priciest highlighting - Load advance prices button fetches the API lazily; if advance fares are already cached they are embedded in the page and applied on load so the button is hidden automatically Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5583a20143
commit
89a536dfd3
8 changed files with 515 additions and 83 deletions
34
app.py
34
app.py
|
|
@ -2,7 +2,7 @@
|
|||
Combine GWR Bristol→Paddington trains with Eurostar St Pancras→destination trains.
|
||||
"""
|
||||
|
||||
from flask import Flask, render_template, redirect, url_for, request, abort
|
||||
from flask import Flask, render_template, redirect, url_for, request, abort, jsonify
|
||||
from datetime import date, timedelta
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
|
@ -155,10 +155,12 @@ def results(station_crs, slug, travel_date):
|
|||
rtt_cache_key = f"rtt_{station_crs}_{travel_date}"
|
||||
es_cache_key = f"eurostar_{travel_date}_{destination}"
|
||||
gwr_fares_cache_key = f"gwr_fares_{station_crs}_{travel_date}"
|
||||
gwr_advance_cache_key = f"gwr_advance_{station_crs}_{travel_date}"
|
||||
|
||||
cached_rtt = get_cached(rtt_cache_key)
|
||||
cached_es = get_cached(es_cache_key, ttl=24 * 3600)
|
||||
cached_gwr_fares = get_cached(gwr_fares_cache_key, ttl=30 * 24 * 3600)
|
||||
cached_advance_fares = get_cached(gwr_advance_cache_key, ttl=24 * 3600)
|
||||
from_cache = bool(cached_rtt and cached_es)
|
||||
|
||||
error = None
|
||||
|
|
@ -197,7 +199,12 @@ def results(station_crs, slug, travel_date):
|
|||
|
||||
eurostar_trains = eurostar_services
|
||||
eurostar_prices = {
|
||||
s["depart_st_pancras"]: {"price": s.get("price"), "seats": s.get("seats")}
|
||||
s["depart_st_pancras"]: {
|
||||
"price": s.get("price"),
|
||||
"seats": s.get("seats"),
|
||||
"plus_price": s.get("plus_price"),
|
||||
"plus_seats": s.get("plus_seats"),
|
||||
}
|
||||
for s in eurostar_services
|
||||
}
|
||||
|
||||
|
|
@ -210,12 +217,14 @@ def results(station_crs, slug, travel_date):
|
|||
gwr_fares,
|
||||
)
|
||||
|
||||
# Annotate each trip with Eurostar Standard price, seats, and total cost
|
||||
# Annotate each trip with Eurostar prices and total cost (walk-on + standard)
|
||||
for trip in trips:
|
||||
es = eurostar_prices.get(trip["depart_st_pancras"], {})
|
||||
es_price = es.get("price")
|
||||
trip["eurostar_price"] = es_price
|
||||
trip["eurostar_seats"] = es.get("seats")
|
||||
trip["eurostar_plus_price"] = es.get("plus_price")
|
||||
trip["eurostar_plus_seats"] = es.get("plus_seats")
|
||||
gwr_p = trip.get("ticket_price")
|
||||
trip["total_price"] = (
|
||||
gwr_p + es_price if (gwr_p is not None and es_price is not None) else None
|
||||
|
|
@ -241,6 +250,8 @@ def results(station_crs, slug, travel_date):
|
|||
es = eurostar_prices.get(svc["depart_st_pancras"], {})
|
||||
svc["eurostar_price"] = es.get("price")
|
||||
svc["eurostar_seats"] = es.get("seats")
|
||||
svc["eurostar_plus_price"] = es.get("plus_price")
|
||||
svc["eurostar_plus_seats"] = es.get("plus_seats")
|
||||
|
||||
# Only keep unreachable services that depart before the first reachable Eurostar.
|
||||
# Services after the first reachable one are omitted (they aren't "Too early").
|
||||
|
|
@ -301,10 +312,27 @@ def results(station_crs, slug, travel_date):
|
|||
default_max_connection=default_max,
|
||||
url_min_connection=url_min,
|
||||
url_max_connection=url_max,
|
||||
cached_advance_fares=cached_advance_fares,
|
||||
valid_min_connections=sorted(VALID_MIN_CONNECTIONS),
|
||||
valid_max_connections=sorted(VALID_MAX_CONNECTIONS),
|
||||
)
|
||||
|
||||
|
||||
@app.route("/api/advance_fares/<station_crs>/<travel_date>")
|
||||
def api_advance_fares(station_crs, travel_date):
|
||||
if station_crs not in STATION_BY_CRS:
|
||||
abort(404)
|
||||
cache_key = f"gwr_advance_{station_crs}_{travel_date}"
|
||||
cached = get_cached(cache_key, ttl=24 * 3600)
|
||||
if cached is not None:
|
||||
return jsonify(cached)
|
||||
try:
|
||||
fares = gwr_fares_scraper.fetch_advance(station_crs, travel_date)
|
||||
set_cached(cache_key, fares)
|
||||
return jsonify(fares)
|
||||
except Exception as e:
|
||||
return jsonify({"error": str(e)}), 500
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app.run(debug=True, host="0.0.0.0")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue