diff --git a/pyproject.toml b/pyproject.toml index b84a1a0..0fb856a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,7 +14,8 @@ dependencies = [ "openpyxl>=3.1.0", "PyYaml==6.0.3", "playwright==1.58.0", - "requests>=2.32.0" + "requests>=2.32.0", + "diskcache>=5.6", ] [project.optional-dependencies] diff --git a/src/beaky/resolvers/config.py b/src/beaky/resolvers/config.py index 1949148..4738ab0 100644 --- a/src/beaky/resolvers/config.py +++ b/src/beaky/resolvers/config.py @@ -5,3 +5,4 @@ from pydantic.dataclasses import dataclass class ResolverConfig: api_key: str league_map: dict[str, int] + cache_path: str = "data/fixture_cache" diff --git a/src/beaky/resolvers/resolver.py b/src/beaky/resolvers/resolver.py index 07bfc2b..8cb6267 100644 --- a/src/beaky/resolvers/resolver.py +++ b/src/beaky/resolvers/resolver.py @@ -5,6 +5,7 @@ from difflib import SequenceMatcher from enum import Enum from typing import Any +import diskcache import requests from beaky import _ansi @@ -83,6 +84,7 @@ class TicketResolver: def __init__(self, config: ResolverConfig): self._headers = {"x-apisports-key": config.api_key} self._league_map = config.league_map + self._disk_cache: diskcache.Cache = diskcache.Cache(config.cache_path) # Cache maps (center_date_str, league_id | None) -> list of fixture dicts self._fixture_cache: dict[tuple[str, int | None], list[dict[str, Any]]] = {} # Cache maps league name -> (league_id, confidence) @@ -150,19 +152,27 @@ class TicketResolver: cache_key = (date_str, league_id) if cache_key not in self._fixture_cache: - date_from = (center - timedelta(days=_DATE_WINDOW)).strftime("%Y-%m-%d") - date_to = (center + timedelta(days=_DATE_WINDOW)).strftime("%Y-%m-%d") - params: dict[str, str | int] = {"from": date_from, "to": date_to} - if league_id is not None: - params["league"] = league_id - params["season"] = center.year if center.month >= 7 else center.year - 1 - print(_ansi.gray(f" │ GET /fixtures {params}")) - resp = _get(f"{_API_BASE}/fixtures", headers=self._headers, params=params) - resp.raise_for_status() - self._fixture_cache[cache_key] = resp.json().get("response", []) - print(_ansi.gray(f" │ {len(self._fixture_cache[cache_key])} fixtures returned (cached)")) + if cache_key in self._disk_cache: + self._fixture_cache[cache_key] = self._disk_cache[cache_key] + print(_ansi.gray(f" │ /fixtures served from disk cache ({len(self._fixture_cache[cache_key])} fixtures)")) + else: + date_from = (center - timedelta(days=_DATE_WINDOW)).strftime("%Y-%m-%d") + date_to = (center + timedelta(days=_DATE_WINDOW)).strftime("%Y-%m-%d") + params: dict[str, str | int] = {"from": date_from, "to": date_to} + if league_id is not None: + params["league"] = league_id + params["season"] = center.year if center.month >= 7 else center.year - 1 + print(_ansi.gray(f" │ GET /fixtures {params}")) + resp = _get(f"{_API_BASE}/fixtures", headers=self._headers, params=params) + resp.raise_for_status() + self._fixture_cache[cache_key] = resp.json().get("response", []) + print(_ansi.gray(f" │ {len(self._fixture_cache[cache_key])} fixtures returned")) + finished = [f for f in self._fixture_cache[cache_key] if _is_finished(f) == 1.0] + if finished: + self._disk_cache[cache_key] = finished + print(_ansi.gray(f" │ {len(finished)} finished fixture(s) written to disk cache")) else: - print(_ansi.gray(f" │ /fixtures (±{_DATE_WINDOW}d of {date_str}, league={league_id}) served from cache")) + print(_ansi.gray(f" │ /fixtures (±{_DATE_WINDOW}d of {date_str}, league={league_id}) served from memory")) fixture, name_match, date_prox = _best_fixture_match( self._fixture_cache[cache_key], bet.team1Name, bet.team2Name, center