from __future__ import annotations import time from typing import Any import structlog from config import settings logger = structlog.get_logger() DEFAULT_BASE_URL = "https://api.sofascore.com/api/v1" DEFAULT_IMPERSONATE = "chrome124" class SofascoreClientError(RuntimeError): pass class SofascoreClient: def __init__( self, *, base_url: str | None = None, impersonate: str | None = None, min_interval_sec: float | None = None, ): self._base = (base_url or settings.sofascore_base_url).rstrip("/") self._impersonate = impersonate or settings.sofascore_impersonate self._min_interval = min_interval_sec or settings.sofascore_min_interval_sec self._last_request_at = 0.0 self._session = self._build_session() def _build_session(self): try: from curl_cffi import requests as curl_requests except ImportError as exc: raise SofascoreClientError( "curl_cffi é obrigatório para a API Sofascore. " 'Instale com: pip install -e ".[sofascore]"' ) from exc return curl_requests.Session(impersonate=self._impersonate) def _throttle(self) -> None: elapsed = time.monotonic() - self._last_request_at if elapsed < self._min_interval: time.sleep(self._min_interval - elapsed) def get_json(self, path: str, *, params: dict | None = None) -> dict[str, Any]: url = path if path.startswith("http") else f"{self._base}/{path.lstrip('/')}" self._throttle() response = self._session.get( url, params=params, timeout=settings.sofascore_timeout_sec, headers={"Accept": "application/json"}, ) self._last_request_at = time.monotonic() if response.status_code == 403: raise SofascoreClientError( "Sofascore retornou 403 (WAF). Verifique curl_cffi e rate limits." ) if response.status_code >= 400: raise SofascoreClientError( f"Sofascore HTTP {response.status_code} para {path}: {response.text[:200]}" ) payload = response.json() if not isinstance(payload, dict): raise SofascoreClientError(f"Resposta inesperada de {path}") return payload def scheduled_events(self, sport: str, date_iso: str) -> list[dict[str, Any]]: data = self.get_json(f"sport/{sport}/scheduled-events/{date_iso}") return list(data.get("events") or []) def event(self, event_id: int) -> dict[str, Any]: data = self.get_json(f"event/{event_id}") return data.get("event") or data def event_lineups(self, event_id: int) -> dict[str, Any]: return self.get_json(f"event/{event_id}/lineups") def event_statistics(self, event_id: int) -> dict[str, Any]: return self.get_json(f"event/{event_id}/statistics") def event_incidents(self, event_id: int) -> dict[str, Any]: return self.get_json(f"event/{event_id}/incidents") def event_pregame_form(self, event_id: int) -> dict[str, Any]: """Forma pré-jogo: posição, pontos, avgRating, sequência de resultados.""" return self.get_json(f"event/{event_id}/pregame-form") def event_team_streaks(self, event_id: int) -> dict[str, Any]: """Séries do time: invencibilidade, vitórias seguidas, etc.""" return self.get_json(f"event/{event_id}/team-streaks") def event_h2h(self, event_id: int) -> dict[str, Any]: """Histórico direto (head-to-head) entre os dois times.""" return self.get_json(f"event/{event_id}/h2h") def search_team(self, query: str) -> dict[str, Any] | None: data = self.get_json("search/teams", params={"q": query}) for item in data.get("results") or []: entity = item.get("entity") or {} if entity.get("national"): return entity return None def player_statistics(self, player_id: int) -> list[dict[str, Any]]: data = self.get_json(f"player/{player_id}/statistics") return list(data.get("seasons") or []) def team_recent_events(self, team_id: int, page: int = 0) -> list[dict[str, Any]]: try: data = self.get_json(f"team/{team_id}/events/last/{page}") except SofascoreClientError as exc: if "HTTP 404" in str(exc): return [] raise return list(data.get("events") or []) def team_upcoming_events(self, team_id: int, page: int = 0) -> list[dict[str, Any]]: try: data = self.get_json(f"team/{team_id}/events/next/{page}") except SofascoreClientError as exc: if "HTTP 404" in str(exc): return [] raise return list(data.get("events") or [])