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 <noreply@anthropic.com>
This commit is contained in:
parent
0dee942e16
commit
6b044b9493
4 changed files with 52 additions and 6 deletions
2
app.py
2
app.py
|
|
@ -85,7 +85,7 @@ def results(slug, travel_date):
|
||||||
|
|
||||||
cached_rtt = get_cached(rtt_cache_key)
|
cached_rtt = get_cached(rtt_cache_key)
|
||||||
cached_es = get_cached(es_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)
|
from_cache = bool(cached_rtt and cached_es and cached_prices)
|
||||||
|
|
||||||
error = None
|
error = None
|
||||||
|
|
|
||||||
6
cache.py
6
cache.py
|
|
@ -1,5 +1,6 @@
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
CACHE_DIR = os.path.join(os.path.dirname(__file__), 'cache')
|
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")
|
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)
|
path = _cache_path(key)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
return None
|
return None
|
||||||
|
if ttl is not None and time.time() - os.path.getmtime(path) > ttl:
|
||||||
|
return None
|
||||||
with open(path) as f:
|
with open(path) as f:
|
||||||
return json.load(f)
|
return json.load(f)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ def _client():
|
||||||
|
|
||||||
|
|
||||||
def _stub_data(monkeypatch, prices=None):
|
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, 'set_cached', lambda key, data: None)
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
app_module.rtt_scraper,
|
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):
|
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, 'set_cached', lambda key, data: None)
|
||||||
monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {})
|
monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {})
|
||||||
monkeypatch.setattr(
|
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):
|
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, 'set_cached', lambda key, data: None)
|
||||||
monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {})
|
monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {})
|
||||||
monkeypatch.setattr(
|
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):
|
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, 'set_cached', lambda key, data: None)
|
||||||
monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {})
|
monkeypatch.setattr(app_module, 'fetch_eurostar_prices', lambda dest, date: {})
|
||||||
monkeypatch.setattr(
|
monkeypatch.setattr(
|
||||||
|
|
|
||||||
42
tests/test_cache.py
Normal file
42
tests/test_cache.py
Normal file
|
|
@ -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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue