Files
hoopscout/apps/providers/adapters/mvp_provider.py

100 lines
3.9 KiB
Python

import json
import logging
import os
import time
from pathlib import Path
from django.conf import settings
from apps.providers.exceptions import ProviderRateLimitError, ProviderTransientError
from apps.providers.interfaces import BaseProviderAdapter
logger = logging.getLogger(__name__)
class MvpDemoProviderAdapter(BaseProviderAdapter):
"""MVP provider backed by a local JSON payload for deterministic development syncs."""
namespace = "mvp_demo"
def __init__(self):
default_path = Path(settings.BASE_DIR) / "apps" / "providers" / "data" / "mvp_provider.json"
self.data_file = Path(os.getenv("PROVIDER_MVP_DATA_FILE", str(default_path)))
self.max_retries = int(os.getenv("PROVIDER_REQUEST_RETRIES", "3"))
self.retry_sleep_seconds = float(os.getenv("PROVIDER_REQUEST_RETRY_SLEEP", "1"))
def _load_payload(self) -> dict:
for attempt in range(1, self.max_retries + 1):
try:
if os.getenv("PROVIDER_MVP_FORCE_RATE_LIMIT", "0") == "1":
raise ProviderRateLimitError("Simulated provider rate limit", retry_after_seconds=15)
with self.data_file.open("r", encoding="utf-8") as handle:
return json.load(handle)
except ProviderRateLimitError:
raise
except FileNotFoundError as exc:
logger.exception("Provider data file not found: %s", self.data_file)
raise ProviderTransientError(str(exc)) from exc
except json.JSONDecodeError as exc:
logger.exception("Invalid provider payload JSON in %s", self.data_file)
raise ProviderTransientError(str(exc)) from exc
except OSError as exc:
if attempt >= self.max_retries:
logger.exception("Provider payload read failed after retries")
raise ProviderTransientError(str(exc)) from exc
time.sleep(self.retry_sleep_seconds * attempt)
raise ProviderTransientError("Unable to read provider payload")
def _payload_list(self, key: str) -> list[dict]:
payload = self._load_payload()
value = payload.get(key, [])
return value if isinstance(value, list) else []
def search_players(self, *, query: str = "", limit: int = 50, offset: int = 0) -> list[dict]:
players = self.fetch_players()
if query:
query_lower = query.lower()
players = [p for p in players if query_lower in p.get("full_name", "").lower()]
return players[offset : offset + limit]
def fetch_player(self, *, external_player_id: str) -> dict | None:
for payload in self.fetch_players():
if payload.get("external_id") == external_player_id:
return payload
return None
def fetch_players(self) -> list[dict]:
return self._payload_list("players")
def fetch_competitions(self) -> list[dict]:
return self._payload_list("competitions")
def fetch_teams(self) -> list[dict]:
return self._payload_list("teams")
def fetch_seasons(self) -> list[dict]:
return self._payload_list("seasons")
def fetch_player_stats(self) -> list[dict]:
return self._payload_list("player_stats")
def fetch_player_careers(self) -> list[dict]:
return self._payload_list("player_careers")
def sync_all(self) -> dict:
return {
"players": self.fetch_players(),
"competitions": self.fetch_competitions(),
"teams": self.fetch_teams(),
"seasons": self.fetch_seasons(),
"player_stats": self.fetch_player_stats(),
"player_careers": self.fetch_player_careers(),
"cursor": None,
}
def sync_incremental(self, *, cursor: str | None = None) -> dict:
payload = self.sync_all()
# MVP source has no change feed yet; returns full snapshot.
payload["cursor"] = cursor
return payload