Source code for slickbet.cache

"""
File-based cache for Livescore API responses.

Enables fast backtesting and hyperparameter optimization by storing
API responses locally and reusing them instead of hitting the API.

Examples
--------
>>> # First run: fetch and cache (e.g. 12 weeks backtest)
>>> client = LivescoreClient(cache_dir="data/api_cache")
>>> # ... run backtest ...

>>> # Later runs: use cache only (no API calls)
>>> client = LivescoreClient(cache_dir="data/api_cache", cache_only=True)
"""

import hashlib
import json
from datetime import datetime
from pathlib import Path
from typing import Any


def _serialize_params(params: dict[str, Any] | None) -> str:
    """
    Convert request params to a stable JSON string for cache keys.

    Parameters
    ----------
    params : dict or None
        Request query parameters (datetimes become YYYY-MM-DD).

    Returns
    -------
    str
        Sorted JSON string suitable for hashing.
    """
    if not params:
        return "{}"
    out: dict[str, Any] = {}
    for k, v in sorted(params.items()):
        if isinstance(v, datetime):
            out[k] = v.strftime("%Y-%m-%d")
        else:
            out[k] = v
    return json.dumps(out, sort_keys=True)


[docs] def cache_key(endpoint: str, params: dict[str, Any] | None) -> str: """ Produce a short, filesystem-safe cache key from endpoint and params. Parameters ---------- endpoint : str API endpoint path. params : dict or None Request query parameters. Returns ------- str 32-character hex digest. """ raw = f"{endpoint}|{_serialize_params(params)}" return hashlib.sha256(raw.encode()).hexdigest()[:32]
[docs] class ApiCache: """ File-based cache for API response bodies (JSON). Each request is stored as a single JSON file keyed by endpoint + params. """ def __init__(self, cache_dir: str | Path) -> None: """ Parameters ---------- cache_dir : str or Path Directory to store cache files (created if missing). """ self.root = Path(cache_dir) self.root.mkdir(parents=True, exist_ok=True)
[docs] def get(self, endpoint: str, params: dict[str, Any] | None) -> dict | None: """ Load cached response if present. Parameters ---------- endpoint : str API endpoint path. params : dict or None Request query parameters. Returns ------- dict or None Cached JSON body or None if miss. """ key = cache_key(endpoint, params) path = self.root / f"{key}.json" if not path.exists(): return None try: with open(path, encoding="utf-8") as f: return dict(json.load(f)) except (json.JSONDecodeError, OSError): return None
[docs] def set(self, endpoint: str, params: dict[str, Any] | None, data: dict) -> None: """ Store response in cache. Parameters ---------- endpoint : str API endpoint path. params : dict or None Request query parameters. data : dict JSON response body to store. """ key = cache_key(endpoint, params) path = self.root / f"{key}.json" try: with open(path, "w", encoding="utf-8") as f: json.dump(data, f, ensure_ascii=False) except OSError: pass
[docs] def clear(self) -> None: """ Remove all cached files in the cache directory. Returns ------- None """ for f in self.root.glob("*.json"): try: f.unlink() except OSError: pass