mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
feat(fetch): one ESPN scoreboard cache key and a max-age response cache (fetch service stage 2) (#728)
Fetch service stage 2: one ESPN scoreboard cache key shared across the sports base classes (legacy keys still read), and a max-age response cache in the fetch service. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,493 @@
|
||||
"""One cache key per ESPN scoreboard (fetch service stage 2).
|
||||
|
||||
- every core caller names a scoreboard with the same key;
|
||||
- the keys each consumer used before are read as a fallback;
|
||||
- a shared entry is never returned older than the reader's own max_age,
|
||||
whoever wrote it and whatever ttl they stored (checked against a real
|
||||
CacheManager, whose stored ttl otherwise wins over the reader's);
|
||||
- two consumers of one scoreboard cost one request;
|
||||
- the counters see it.
|
||||
|
||||
No network: sessions are fakes, and the fetch service is a fresh one per test.
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import threading
|
||||
import time
|
||||
from datetime import date, datetime
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
from requests.structures import CaseInsensitiveDict
|
||||
|
||||
from src.common import espn_dates
|
||||
from src.common import fetch_service as fs
|
||||
from src.common.espn_dates import (
|
||||
ESPN_MAX_LIMIT,
|
||||
espn_scoreboard_cache_key,
|
||||
espn_scoreboard_cache_key_for_url,
|
||||
espn_scoreboard_url,
|
||||
get_espn_scoreboard,
|
||||
read_espn_scoreboard_cache,
|
||||
store_espn_scoreboard_cache,
|
||||
)
|
||||
from src.common.fetch_service import FetchService, plugin_scope
|
||||
from src.common.sports_fetch import SportsFetchMixin
|
||||
|
||||
NFL_URL = "https://site.api.espn.com/apis/site/v2/sports/football/nfl/scoreboard"
|
||||
|
||||
|
||||
# --- fakes ---------------------------------------------------------------------------
|
||||
|
||||
class Clock:
|
||||
def __init__(self):
|
||||
self.t = 1000.0
|
||||
|
||||
def now(self):
|
||||
return self.t
|
||||
|
||||
|
||||
def _response(body, url, max_age=None):
|
||||
response = requests.Response()
|
||||
response.status_code = 200
|
||||
response._content = json.dumps(body).encode()
|
||||
response.headers = CaseInsensitiveDict(
|
||||
{"Cache-Control": f"max-age={max_age}"} if max_age else {})
|
||||
response.url = url
|
||||
response.encoding = "utf-8"
|
||||
return response
|
||||
|
||||
|
||||
class Espn(requests.Session):
|
||||
"""A Session answering every scoreboard GET with ``{"events": [<dates>]}``."""
|
||||
|
||||
def __init__(self, max_age=None, fail=None):
|
||||
super().__init__()
|
||||
self.calls = []
|
||||
self.max_age = max_age
|
||||
self.fail = fail
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def get(self, url, **kwargs):
|
||||
with self._lock:
|
||||
self.calls.append((url, dict(kwargs.get("params") or {})))
|
||||
if self.fail:
|
||||
raise self.fail
|
||||
dates = (kwargs.get("params") or {}).get("dates", "current")
|
||||
return _response({"events": [{"id": dates}]}, url, self.max_age)
|
||||
|
||||
|
||||
class RecordCache:
|
||||
"""The CacheManager surface these helpers use, records and all."""
|
||||
|
||||
def __init__(self):
|
||||
self.records = {}
|
||||
self.deleted = []
|
||||
|
||||
def set(self, key, data, ttl=None):
|
||||
record = {"timestamp": time.time()}
|
||||
if ttl is not None:
|
||||
record["ttl"] = ttl
|
||||
record["data"] = data
|
||||
self.records[key] = record
|
||||
|
||||
def put(self, key, data, age, ttl=None):
|
||||
self.set(key, data, ttl)
|
||||
self.records[key]["timestamp"] -= age
|
||||
|
||||
def get_cached_data(self, key, max_age=300, memory_ttl=None):
|
||||
return self.records.get(key)
|
||||
|
||||
def get(self, key, max_age=300, memory_ttl=None):
|
||||
record = self.records.get(key)
|
||||
return record["data"] if record else None
|
||||
|
||||
def delete(self, key):
|
||||
self.deleted.append(key)
|
||||
self.records.pop(key, None)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def service(monkeypatch):
|
||||
svc = FetchService({"rate_limits": {}})
|
||||
monkeypatch.setattr(fs, "_service", svc)
|
||||
return svc
|
||||
|
||||
|
||||
def _totals(svc):
|
||||
return svc.snapshot()["totals"]
|
||||
|
||||
|
||||
# --- the key -------------------------------------------------------------------------
|
||||
|
||||
class TestCanonicalKey:
|
||||
|
||||
def test_one_spelling_for_one_scoreboard(self):
|
||||
key = "espn_scoreboard_football_nfl_20261004"
|
||||
assert espn_scoreboard_cache_key("football", "nfl", "20261004") == key
|
||||
assert espn_scoreboard_cache_key(" Football ", "NFL", "20261004") == key
|
||||
assert espn_scoreboard_cache_key("football", "nfl", date(2026, 10, 4)) == key
|
||||
assert espn_scoreboard_cache_key("football", "nfl", datetime(2026, 10, 4, 23, 59)) == key
|
||||
assert espn_scoreboard_cache_key("football", "nfl", ("20261004", date(2026, 10, 4))) == key
|
||||
assert espn_scoreboard_cache_key_for_url(NFL_URL, "20261004") == key
|
||||
assert espn_scoreboard_cache_key_for_url(NFL_URL + "?limit=500", "20261004") == key
|
||||
|
||||
@pytest.mark.parametrize("dates,suffix", [
|
||||
(None, "current"),
|
||||
("", "current"),
|
||||
("2026", "2026"),
|
||||
("202610", "202610"),
|
||||
("20260925-20261016", "20260925-20261016"),
|
||||
((date(2026, 9, 25), date(2026, 10, 16)), "20260925-20261016"),
|
||||
])
|
||||
def test_every_dates_form_espn_takes(self, dates, suffix):
|
||||
assert espn_scoreboard_cache_key("soccer", "eng.1", dates) == f"espn_scoreboard_soccer_eng.1_{suffix}"
|
||||
|
||||
@pytest.mark.parametrize("sport,league,dates", [
|
||||
("football", "nfl", "2026-10-04"),
|
||||
("football", "nfl", "tomorrow"),
|
||||
("football", "nfl", ("20261004",)),
|
||||
("football", "nfl", True),
|
||||
("football", "", "20261004"),
|
||||
("foot ball", "nfl", "20261004"),
|
||||
("football", "nfl/../x", "20261004"),
|
||||
])
|
||||
def test_anything_else_is_refused_not_guessed(self, sport, league, dates):
|
||||
with pytest.raises(ValueError):
|
||||
espn_scoreboard_cache_key(sport, league, dates)
|
||||
|
||||
def test_a_url_that_is_not_a_scoreboard_has_no_key(self):
|
||||
assert espn_scoreboard_cache_key_for_url(
|
||||
"https://site.api.espn.com/apis/site/v2/sports/football/nfl/teams") is None
|
||||
|
||||
def test_the_url_it_names(self):
|
||||
assert espn_scoreboard_url("Football", "college-football") == (
|
||||
"https://site.api.espn.com/apis/site/v2/sports/football/college-football/scoreboard")
|
||||
|
||||
|
||||
class Host(SportsFetchMixin):
|
||||
def __init__(self, session, cache):
|
||||
self.session = session
|
||||
self.headers = {"User-Agent": "test"}
|
||||
self.cache_manager = cache
|
||||
self.logger = logging.getLogger("test.espn_cache")
|
||||
self._games_lock = threading.RLock()
|
||||
self.sport = "football"
|
||||
self.league = "nfl"
|
||||
|
||||
|
||||
class TestEveryCallerUsesTheKey:
|
||||
"""The core helpers that cache a scoreboard all write the same key."""
|
||||
|
||||
DAY = "20261004"
|
||||
KEY = "espn_scoreboard_football_nfl_20261004"
|
||||
|
||||
def test_get_espn_scoreboard(self, service):
|
||||
cache = RecordCache()
|
||||
get_espn_scoreboard(Espn(), "football", "nfl", self.DAY, cache_manager=cache)
|
||||
assert list(cache.records) == [self.KEY]
|
||||
|
||||
def test_api_helper(self, service):
|
||||
from src.common.api_helper import APIHelper
|
||||
|
||||
cache = RecordCache()
|
||||
helper = APIHelper(cache_manager=cache)
|
||||
helper.set_rate_limit(0)
|
||||
helper.session = Espn()
|
||||
helper.fetch_espn_scoreboard("football", "nfl", date=self.DAY)
|
||||
assert list(cache.records) == [self.KEY]
|
||||
|
||||
def test_the_scoreboard_mixin(self, service):
|
||||
cache = RecordCache()
|
||||
host = Host(Espn(), cache)
|
||||
assert host._schedule_cache_key(self.DAY) == self.KEY
|
||||
host._fetch_season_directly(NFL_URL, self.DAY, None, "today")
|
||||
assert list(cache.records) == [self.KEY]
|
||||
|
||||
def test_the_mixin_without_a_scoreboard_url(self, service):
|
||||
cache = RecordCache()
|
||||
Host(Espn(), cache)._fetch_season_directly(
|
||||
"https://example.test/feed", self.DAY, None, "today")
|
||||
assert list(cache.records) == [self.KEY]
|
||||
|
||||
def test_an_explicit_key_is_still_the_callers(self, service):
|
||||
cache = RecordCache()
|
||||
Host(Espn(), cache)._fetch_season_directly(NFL_URL, self.DAY, "mine", "today")
|
||||
assert list(cache.records) == ["mine"]
|
||||
|
||||
|
||||
# --- reading ---------------------------------------------------------------------------
|
||||
|
||||
class TestRead:
|
||||
|
||||
KEY = "espn_scoreboard_football_nfl_20261004"
|
||||
OLD = "scoreboard_data_football_nfl_20261004"
|
||||
|
||||
def test_a_fresh_canonical_entry(self, service):
|
||||
cache = RecordCache()
|
||||
cache.put(self.KEY, {"events": ["new"]}, age=10)
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 30) == {"events": ["new"]}
|
||||
assert (_totals(service)["cache_hits"], _totals(service)["legacy_cache_hits"]) == (1, 0)
|
||||
|
||||
def test_the_old_key_is_read_after_an_upgrade(self, service):
|
||||
cache = RecordCache()
|
||||
cache.put(self.OLD, {"events": ["old"]}, age=10)
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 30, [self.OLD]) == {"events": ["old"]}
|
||||
assert (_totals(service)["cache_hits"], _totals(service)["legacy_cache_hits"]) == (1, 1)
|
||||
|
||||
def test_the_canonical_key_wins_over_an_old_one(self, service):
|
||||
cache = RecordCache()
|
||||
cache.put(self.OLD, {"events": ["old"]}, age=1)
|
||||
cache.put(self.KEY, {"events": ["new"]}, age=20)
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 30, [self.OLD]) == {"events": ["new"]}
|
||||
|
||||
def test_a_stale_old_key_is_a_miss_too(self, service):
|
||||
cache = RecordCache()
|
||||
cache.put(self.OLD, {"events": ["old"]}, age=31)
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 30, [self.OLD]) is None
|
||||
assert _totals(service)["cache_hits"] == 0
|
||||
|
||||
def test_age_is_judged_against_an_injected_clock(self, service):
|
||||
cache = RecordCache()
|
||||
cache.records[self.KEY] = {"timestamp": 5000.0, "data": {"events": []}}
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 30, now=5030.0) == {"events": []}
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 30, now=5030.5) is None
|
||||
|
||||
def test_never_older_than_the_readers_ttl_whatever_the_writer_stored(self, service):
|
||||
# A writer that stored ttl=3600 must not make a 30 s reader take an
|
||||
# entry a minute old (CacheManager itself would let the ttl win).
|
||||
cache = RecordCache()
|
||||
cache.put(self.KEY, {"events": []}, age=60, ttl=3600)
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 30) is None
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 90) == {"events": []}
|
||||
|
||||
def test_accept_can_turn_down_an_entry_and_sees_its_age(self, service):
|
||||
cache = RecordCache()
|
||||
cache.records[self.KEY] = {"timestamp": 5000.0, "data": {"events": ["live"]}}
|
||||
seen = []
|
||||
|
||||
def live_needs_30s(data, age):
|
||||
seen.append(age)
|
||||
return age <= 30
|
||||
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 300, now=5040.0,
|
||||
accept=live_needs_30s) is None
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, 300, now=5020.0,
|
||||
accept=live_needs_30s) == {"events": ["live"]}
|
||||
assert seen == [40.0, 20.0]
|
||||
assert _totals(service)["cache_hits"] == 1 # the turned-down read is no hit
|
||||
|
||||
@pytest.mark.parametrize("max_age", [0, -5])
|
||||
def test_no_max_age_no_read(self, service, max_age):
|
||||
cache = RecordCache()
|
||||
cache.put(self.KEY, {"events": []}, age=0)
|
||||
assert read_espn_scoreboard_cache(cache, self.KEY, max_age) is None
|
||||
|
||||
def test_a_broken_cache_is_a_miss(self, service):
|
||||
class Broken:
|
||||
def get_cached_data(self, *a, **k):
|
||||
raise OSError("disk gone")
|
||||
assert read_espn_scoreboard_cache(Broken(), self.KEY, 30) is None
|
||||
assert read_espn_scoreboard_cache(None, self.KEY, 30) is None
|
||||
|
||||
def test_a_cache_without_records(self, service):
|
||||
class Plain:
|
||||
def get(self, key, max_age=None):
|
||||
return {"events": [key, max_age]}
|
||||
assert read_espn_scoreboard_cache(Plain(), self.KEY, 29.5) == {"events": [self.KEY, 30]}
|
||||
|
||||
|
||||
class TestWithARealCacheManager:
|
||||
"""The same promise against CacheManager, memory and disk tiers."""
|
||||
|
||||
KEY = "espn_scoreboard_football_nfl_20261004"
|
||||
|
||||
@pytest.fixture
|
||||
def cm(self, tmp_path):
|
||||
from src.cache.disk_cache import DiskCache
|
||||
from src.cache.memory_cache import MemoryCache
|
||||
from src.cache_manager import CacheManager
|
||||
|
||||
cm = CacheManager()
|
||||
cm._disk_cache_component = DiskCache(cache_dir=str(tmp_path))
|
||||
cm._memory_cache_component = MemoryCache()
|
||||
return cm
|
||||
|
||||
def _age_on_disk(self, cm, seconds):
|
||||
path = cm._disk_cache_component.get_cache_path(self.KEY)
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
record = json.load(fh)
|
||||
record["timestamp"] = time.time() - seconds
|
||||
with open(path, "w", encoding="utf-8") as fh:
|
||||
json.dump(record, fh)
|
||||
cm._memory_cache_component.clear()
|
||||
|
||||
def test_a_writers_long_ttl_does_not_outlast_the_readers(self, cm, service):
|
||||
cm.set(self.KEY, {"events": [1]}, ttl=3600)
|
||||
self._age_on_disk(cm, 60)
|
||||
assert cm.get(self.KEY, max_age=30) == {"events": [1]} # what a plain read does
|
||||
assert read_espn_scoreboard_cache(cm, self.KEY, 30) is None
|
||||
assert read_espn_scoreboard_cache(cm, self.KEY, 120) == {"events": [1]}
|
||||
|
||||
def test_a_copy_loaded_into_memory_keeps_its_real_age(self, cm, service):
|
||||
store_espn_scoreboard_cache(cm, self.KEY, {"events": [1]})
|
||||
self._age_on_disk(cm, 60)
|
||||
assert read_espn_scoreboard_cache(cm, self.KEY, 120) == {"events": [1]} # now in memory
|
||||
assert read_espn_scoreboard_cache(cm, self.KEY, 30) is None
|
||||
|
||||
def test_the_canonical_entry_stores_no_ttl(self, cm, service):
|
||||
store_espn_scoreboard_cache(cm, self.KEY, {"events": []})
|
||||
path = cm._disk_cache_component.get_cache_path(self.KEY)
|
||||
with open(path, encoding="utf-8") as fh:
|
||||
assert "ttl" not in json.load(fh)
|
||||
|
||||
|
||||
# --- fetching through the cache ----------------------------------------------------------------
|
||||
|
||||
class TestGetEspnScoreboard:
|
||||
|
||||
def test_a_miss_fetches_a_whole_page_and_caches_it(self, service):
|
||||
cache, espn = RecordCache(), Espn()
|
||||
data = get_espn_scoreboard(espn, "football", "nfl", "20261004", cache_manager=cache)
|
||||
assert data == {"events": [{"id": "20261004"}]}
|
||||
assert espn.calls == [(NFL_URL, {"dates": "20261004", "limit": ESPN_MAX_LIMIT})]
|
||||
assert "ttl" not in cache.records["espn_scoreboard_football_nfl_20261004"]
|
||||
|
||||
def test_a_hit_sends_nothing(self, service):
|
||||
cache, espn = RecordCache(), Espn()
|
||||
get_espn_scoreboard(espn, "football", "nfl", "20261004", cache_manager=cache)
|
||||
get_espn_scoreboard(espn, "football", "nfl", "20261004", cache_manager=cache, max_age=60)
|
||||
assert len(espn.calls) == 1
|
||||
assert _totals(service)["cache_hits"] == 1
|
||||
|
||||
def test_max_age_zero_always_fetches_and_still_caches(self, service):
|
||||
cache, espn = RecordCache(), Espn()
|
||||
get_espn_scoreboard(espn, "football", "nfl", "20261004", cache_manager=cache, max_age=0)
|
||||
get_espn_scoreboard(espn, "football", "nfl", "20261004", cache_manager=cache, max_age=0)
|
||||
assert len(espn.calls) == 2
|
||||
assert "espn_scoreboard_football_nfl_20261004" in cache.records
|
||||
|
||||
def test_the_current_scoreboard_sends_no_dates(self, service):
|
||||
espn = Espn()
|
||||
get_espn_scoreboard(espn, "football", "nfl", cache_manager=RecordCache())
|
||||
assert espn.calls == [(NFL_URL, {"limit": ESPN_MAX_LIMIT})]
|
||||
|
||||
def test_a_range_is_fetched_in_chunks_once_espn_rejects_it(self, service, monkeypatch):
|
||||
monkeypatch.setattr(espn_dates, "_ranges_rejected_until", time.monotonic() + 60)
|
||||
cache, espn = RecordCache(), Espn()
|
||||
data = get_espn_scoreboard(espn, "football", "nfl", "20261003-20261004", cache_manager=cache)
|
||||
assert [e["id"] for e in data["events"]] == ["20261003", "20261004"]
|
||||
assert list(cache.records) == ["espn_scoreboard_football_nfl_20261003-20261004"]
|
||||
|
||||
def test_a_failure_raises_and_caches_nothing(self, service):
|
||||
cache = RecordCache()
|
||||
with pytest.raises(requests.ConnectionError):
|
||||
get_espn_scoreboard(Espn(fail=requests.ConnectionError("down")), "football", "nfl",
|
||||
"20261004", cache_manager=cache)
|
||||
assert cache.records == {}
|
||||
|
||||
def test_no_cache_manager_just_fetches(self, service):
|
||||
espn = Espn()
|
||||
get_espn_scoreboard(espn, "football", "nfl", "20261004")
|
||||
get_espn_scoreboard(espn, "football", "nfl", "20261004")
|
||||
assert len(espn.calls) == 2
|
||||
|
||||
def test_the_response_cache_is_bounded_by_the_callers_ttl(self, monkeypatch):
|
||||
clock = Clock()
|
||||
svc = FetchService({"rate_limits": {}}, clock=clock.now)
|
||||
monkeypatch.setattr(fs, "_service", svc)
|
||||
espn = Espn(max_age=450)
|
||||
get_espn_scoreboard(espn, "football", "nfl", "20261004", max_age=0)
|
||||
clock.t += 20
|
||||
get_espn_scoreboard(espn, "football", "nfl", "20261004", max_age=10)
|
||||
assert len(espn.calls) == 2 # 20 s old > the caller's 10
|
||||
clock.t += 5
|
||||
get_espn_scoreboard(espn, "football", "nfl", "20261004", max_age=10)
|
||||
assert len(espn.calls) == 2 # 5 s old, and ESPN said 450
|
||||
assert svc.snapshot()["totals"]["memo_hits"] == 1
|
||||
|
||||
|
||||
class TestTwoConsumersOneFetch:
|
||||
"""A scoreboard's live poll and odds-ticker asking for the same day."""
|
||||
|
||||
def test_the_second_consumer_is_served_from_the_first_ones_copy(self, service):
|
||||
cache, espn = RecordCache(), Espn()
|
||||
with plugin_scope("football-scoreboard"):
|
||||
live = get_espn_scoreboard(espn, "football", "nfl", "20261004",
|
||||
cache_manager=cache, max_age=0)
|
||||
with plugin_scope("odds-ticker"):
|
||||
ticker = get_espn_scoreboard(
|
||||
espn, "football", "nfl", "20261004", cache_manager=cache, max_age=300,
|
||||
legacy_keys=["scoreboard_data_football_nfl_20261004"])
|
||||
assert ticker == live
|
||||
assert len(espn.calls) == 1
|
||||
snap = service.snapshot()
|
||||
assert snap["plugins"]["odds-ticker"]["cache_hits"] == 1
|
||||
assert snap["plugins"]["odds-ticker"]["requests"] == 0
|
||||
assert snap["plugins"]["football-scoreboard"]["requests"] == 1
|
||||
|
||||
def test_concurrent_misses_go_to_espn_once(self, service):
|
||||
gate = threading.Event()
|
||||
|
||||
class Slow(Espn):
|
||||
def get(self, url, **kwargs):
|
||||
gate.wait(5)
|
||||
return super().get(url, **kwargs)
|
||||
|
||||
espn, results = Slow(), []
|
||||
threads = [threading.Thread(target=lambda: results.append(get_espn_scoreboard(
|
||||
espn, "football", "nfl", "20261004", cache_manager=RecordCache(), max_age=60)))
|
||||
for _ in range(2)]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
deadline = time.monotonic() + 5
|
||||
while time.monotonic() < deadline:
|
||||
with service._lock:
|
||||
if any(f.waiters for f in service._inflight.values()):
|
||||
break
|
||||
time.sleep(0.005)
|
||||
gate.set()
|
||||
for thread in threads:
|
||||
thread.join(5)
|
||||
assert len(espn.calls) == 1 and results[0] == results[1]
|
||||
|
||||
|
||||
# --- the scoreboards' schedule window ----------------------------------------------------------
|
||||
|
||||
class TestCachedSchedule:
|
||||
|
||||
KEY = "espn_scoreboard_football_nfl_20260925-20261016"
|
||||
YESTERDAY = "espn_scoreboard_football_nfl_20260924-20261015"
|
||||
OLD = "nfl_schedule_window_7_14"
|
||||
|
||||
def test_the_canonical_copy(self, service):
|
||||
cache = RecordCache()
|
||||
cache.put(self.KEY, {"events": ["new"]}, age=0)
|
||||
assert Host(Espn(), cache)._cached_schedule(self.KEY, [self.OLD]) == {"events": ["new"]}
|
||||
assert cache.deleted == []
|
||||
|
||||
def test_the_old_key_after_an_upgrade_counted_as_legacy(self, service):
|
||||
cache = RecordCache()
|
||||
cache.put(self.OLD, {"events": ["old"]}, age=0)
|
||||
assert Host(Espn(), cache)._cached_schedule(self.KEY, [self.OLD]) == {"events": ["old"]}
|
||||
totals = _totals(service)
|
||||
assert (totals["legacy_cache_hits"], totals["cache_hits"]) == (1, 0)
|
||||
|
||||
def test_a_miss_retires_yesterdays_window(self, service):
|
||||
cache = RecordCache()
|
||||
cache.put(self.YESTERDAY, {"events": []}, age=0)
|
||||
assert Host(Espn(), cache)._cached_schedule(self.KEY) is None
|
||||
assert cache.deleted == [self.YESTERDAY]
|
||||
assert self.YESTERDAY not in cache.records
|
||||
|
||||
def test_a_key_that_is_not_a_window_retires_nothing(self, service):
|
||||
cache = RecordCache()
|
||||
Host(Espn(), cache)._cached_schedule("espn_scoreboard_football_nfl_20261004")
|
||||
Host(Espn(), cache)._cached_schedule(self.OLD)
|
||||
assert cache.deleted == []
|
||||
|
||||
def test_a_cache_without_delete(self, service):
|
||||
class NoDelete(RecordCache):
|
||||
delete = None
|
||||
assert Host(Espn(), NoDelete())._cached_schedule(self.KEY) is None
|
||||
Reference in New Issue
Block a user