109 lines
4.4 KiB
Python
109 lines
4.4 KiB
Python
import json
|
|
import logging
|
|
import os
|
|
import time
|
|
from pathlib import Path
|
|
|
|
from django.conf import settings
|
|
|
|
from apps.providers.contracts import (
|
|
CompetitionPayload,
|
|
NormalizedSyncPayload,
|
|
PlayerCareerPayload,
|
|
PlayerPayload,
|
|
PlayerStatsPayload,
|
|
SeasonPayload,
|
|
TeamPayload,
|
|
)
|
|
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[PlayerPayload]:
|
|
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) -> PlayerPayload | None:
|
|
for payload in self.fetch_players():
|
|
if payload.get("external_id") == external_player_id:
|
|
return payload
|
|
return None
|
|
|
|
def fetch_players(self) -> list[PlayerPayload]:
|
|
return self._payload_list("players") # type: ignore[return-value]
|
|
|
|
def fetch_competitions(self) -> list[CompetitionPayload]:
|
|
return self._payload_list("competitions") # type: ignore[return-value]
|
|
|
|
def fetch_teams(self) -> list[TeamPayload]:
|
|
return self._payload_list("teams") # type: ignore[return-value]
|
|
|
|
def fetch_seasons(self) -> list[SeasonPayload]:
|
|
return self._payload_list("seasons") # type: ignore[return-value]
|
|
|
|
def fetch_player_stats(self) -> list[PlayerStatsPayload]:
|
|
return self._payload_list("player_stats") # type: ignore[return-value]
|
|
|
|
def fetch_player_careers(self) -> list[PlayerCareerPayload]:
|
|
return self._payload_list("player_careers") # type: ignore[return-value]
|
|
|
|
def sync_all(self) -> NormalizedSyncPayload:
|
|
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) -> NormalizedSyncPayload:
|
|
payload = self.sync_all()
|
|
# MVP source has no change feed yet; returns full snapshot.
|
|
payload["cursor"] = cursor
|
|
return payload
|