From b88d23a270b371288f73dda1f7de8fa3afc11b97 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Wed, 1 Apr 2026 15:33:07 +0100 Subject: [PATCH 1/4] Use destination-specific result page metadata --- templates/base.html | 10 +++++++++- templates/results.html | 5 +++++ tests/test_app.py | 17 +++++++++++++++++ 3 files changed, 31 insertions(+), 1 deletion(-) diff --git a/templates/base.html b/templates/base.html index 47c41b5..aa65c78 100644 --- a/templates/base.html +++ b/templates/base.html @@ -3,7 +3,15 @@ - Bristol to Europe via Eurostar + {% block title %}Bristol to Europe via Eurostar{% endblock %} + + + + + + + +

← New search @@ -100,10 +105,10 @@ Bristol Paddington GWR Fare - Transfer + Transfer Depart St Pancras - {{ destination }} - + {{ destination }} + ES Std Total @@ -138,7 +143,7 @@ £{{ "%.2f"|format(row.ticket_price) }}
{{ row.ticket_name }} - + {{ row.connection_duration }} @@ -149,7 +154,14 @@ {{ row.arrive_destination }} (CET) - + + {% if row.eurostar_price is not none %} + £{{ row.eurostar_price }} + {% else %} + + {% endif %} + + {% if row.total_minutes <= best_mins + 5 and trips | length > 1 %} {{ row.total_duration }} ⚡ {% elif row.total_minutes >= worst_mins - 5 and trips | length > 1 %} @@ -157,11 +169,14 @@ {% else %} {{ row.total_duration }} {% endif %} + {% if row.total_price is not none %} +
£{{ "%.2f"|format(row.total_price) }} + {% endif %} {% else %} — — - — + — Unavailable {{ row.depart_st_pancras }} @@ -171,8 +186,15 @@ {{ row.arrive_destination }} (CET) + + {% if row.eurostar_price is not none %} + £{{ row.eurostar_price }} + {% else %} + + {% endif %} + - Unavailable from Bristol + Unavailable from Bristol {% endif %} @@ -186,7 +208,8 @@ {% if unreachable_morning_services %} Morning means Eurostar departures before 12:00 from St Pancras. {% endif %} - Eurostar times are from the general timetable and may vary; always check + GWR walk-on single prices for Bristol Temple Meads → Paddington. + Eurostar Standard prices are for 1 adult in GBP; always check eurostar.com to book.  ·  Paddington arrivals on RTT diff --git a/tests/test_app.py b/tests/test_app.py index b79a992..4b8261e 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -6,7 +6,7 @@ def _client(): return app_module.app.test_client() -def _stub_data(monkeypatch): +def _stub_data(monkeypatch, prices=None): monkeypatch.setattr(app_module, 'get_cached', lambda key: None) monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None) monkeypatch.setattr( @@ -33,6 +33,8 @@ def _stub_data(monkeypatch): 'timetable_url', lambda destination: f'https://example.test/{destination.lower().replace(" ", "-")}', ) + _prices = prices if prices is not None else {} + monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: _prices) def test_index_shows_fixed_departure_and_destination_radios(): @@ -94,6 +96,7 @@ def test_results_title_and_social_meta_include_destination(monkeypatch): def test_results_marks_trips_within_five_minutes_of_fastest_and_slowest(monkeypatch): monkeypatch.setattr(app_module, 'get_cached', lambda key: None) monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None) + monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {}) monkeypatch.setattr( app_module.rtt_scraper, 'fetch', @@ -165,6 +168,7 @@ def test_results_marks_trips_within_five_minutes_of_fastest_and_slowest(monkeypa def test_results_shows_unreachable_morning_eurostar_services(monkeypatch): monkeypatch.setattr(app_module, 'get_cached', lambda key: None) monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None) + monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {}) monkeypatch.setattr( app_module.rtt_scraper, 'fetch', @@ -215,9 +219,23 @@ def test_results_shows_unreachable_morning_eurostar_services(monkeypatch): assert html.index('09:30') < html.index('10:15') +def test_results_shows_eurostar_price_and_total(monkeypatch): + # 07:00 on Friday 2026-04-10 → Anytime £138.70 (weekday, 05:05–08:25 window) + _stub_data(monkeypatch, prices={'10:01': 59}) + client = _client() + + resp = client.get('/results/paris/2026-04-10?min_connection=60&max_connection=120') + html = resp.get_data(as_text=True) + + assert resp.status_code == 200 + assert '£59' in html # Eurostar Standard price + assert '£197.70' in html # Anytime £138.70 + ES £59 + + def test_results_can_show_only_unreachable_morning_services(monkeypatch): monkeypatch.setattr(app_module, 'get_cached', lambda key: None) monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None) + monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {}) monkeypatch.setattr( app_module.rtt_scraper, 'fetch', From 6b044b9493dfe84f2f224b633b3056a7be6398e5 Mon Sep 17 00:00:00 2001 From: Edward Betts Date: Sat, 4 Apr 2026 10:38:47 +0100 Subject: [PATCH 4/4] Add 24-hour TTL to Eurostar price cache Cache reads now accept an optional ttl (seconds). get_cached checks the file mtime and returns None if the entry is older than the TTL, triggering a fresh fetch. Eurostar prices use a 24-hour TTL; timetable caches remain indefinite (date-scoped keys become irrelevant once the date passes). Co-Authored-By: Claude Sonnet 4.6 --- app.py | 2 +- cache.py | 6 +++++- tests/test_app.py | 8 ++++---- tests/test_cache.py | 42 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 52 insertions(+), 6 deletions(-) create mode 100644 tests/test_cache.py diff --git a/app.py b/app.py index aa61d15..cdfa4ef 100644 --- a/app.py +++ b/app.py @@ -85,7 +85,7 @@ def results(slug, travel_date): cached_rtt = get_cached(rtt_cache_key) cached_es = get_cached(es_cache_key) - cached_prices = get_cached(prices_cache_key) + cached_prices = get_cached(prices_cache_key, ttl=24 * 3600) from_cache = bool(cached_rtt and cached_es and cached_prices) error = None diff --git a/cache.py b/cache.py index 668103f..2e82500 100644 --- a/cache.py +++ b/cache.py @@ -1,5 +1,6 @@ import json import os +import time CACHE_DIR = os.path.join(os.path.dirname(__file__), 'cache') @@ -9,10 +10,13 @@ def _cache_path(key: str) -> str: return os.path.join(CACHE_DIR, f"{safe_key}.json") -def get_cached(key: str): +def get_cached(key: str, ttl: int | None = None): + """Return cached data, or None if missing or older than ttl seconds.""" path = _cache_path(key) if not os.path.exists(path): return None + if ttl is not None and time.time() - os.path.getmtime(path) > ttl: + return None with open(path) as f: return json.load(f) diff --git a/tests/test_app.py b/tests/test_app.py index 4b8261e..cd21232 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -7,7 +7,7 @@ def _client(): def _stub_data(monkeypatch, prices=None): - monkeypatch.setattr(app_module, 'get_cached', lambda key: None) + monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None) monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None) monkeypatch.setattr( app_module.rtt_scraper, @@ -94,7 +94,7 @@ def test_results_title_and_social_meta_include_destination(monkeypatch): def test_results_marks_trips_within_five_minutes_of_fastest_and_slowest(monkeypatch): - monkeypatch.setattr(app_module, 'get_cached', lambda key: None) + monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None) monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None) monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {}) monkeypatch.setattr( @@ -166,7 +166,7 @@ def test_results_marks_trips_within_five_minutes_of_fastest_and_slowest(monkeypa def test_results_shows_unreachable_morning_eurostar_services(monkeypatch): - monkeypatch.setattr(app_module, 'get_cached', lambda key: None) + monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None) monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None) monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {}) monkeypatch.setattr( @@ -233,7 +233,7 @@ def test_results_shows_eurostar_price_and_total(monkeypatch): def test_results_can_show_only_unreachable_morning_services(monkeypatch): - monkeypatch.setattr(app_module, 'get_cached', lambda key: None) + monkeypatch.setattr(app_module, 'get_cached', lambda key, ttl=None: None) monkeypatch.setattr(app_module, 'set_cached', lambda key, data: None) monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {}) monkeypatch.setattr( diff --git a/tests/test_cache.py b/tests/test_cache.py new file mode 100644 index 0000000..006af57 --- /dev/null +++ b/tests/test_cache.py @@ -0,0 +1,42 @@ +import os +import time +import pytest +from cache import get_cached, set_cached + + +@pytest.fixture +def tmp_cache(tmp_path, monkeypatch): + import cache as cache_module + monkeypatch.setattr(cache_module, 'CACHE_DIR', str(tmp_path)) + return tmp_path + + +def test_get_cached_returns_none_for_missing_key(tmp_cache): + assert get_cached('no_such_key') is None + + +def test_set_and_get_cached_roundtrip(tmp_cache): + set_cached('my_key', {'a': 1}) + assert get_cached('my_key') == {'a': 1} + + +def test_get_cached_no_ttl_never_expires(tmp_cache): + set_cached('k', [1, 2, 3]) + # Backdate the file by 2 days + path = tmp_cache / 'k.json' + old = time.time() - 2 * 86400 + os.utime(path, (old, old)) + assert get_cached('k') == [1, 2, 3] + + +def test_get_cached_within_ttl(tmp_cache): + set_cached('k', 'fresh') + assert get_cached('k', ttl=3600) == 'fresh' + + +def test_get_cached_expired_returns_none(tmp_cache): + set_cached('k', 'stale') + path = tmp_cache / 'k.json' + old = time.time() - 25 * 3600 # 25 hours ago + os.utime(path, (old, old)) + assert get_cached('k', ttl=24 * 3600) is None