Align balldontlie OpenAPI integration and clarify search metric semantics
This commit is contained in:
@ -1,5 +1,4 @@
|
||||
import logging
|
||||
from itertools import islice
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@ -30,6 +29,7 @@ class BalldontlieProviderAdapter(BaseProviderAdapter):
|
||||
"""HTTP MVP adapter for balldontlie (NBA-centric data source)."""
|
||||
|
||||
namespace = "balldontlie"
|
||||
nba_prefix = "/nba/v1"
|
||||
|
||||
def __init__(self, client: BalldontlieClient | None = None):
|
||||
self.client = client or BalldontlieClient()
|
||||
@ -38,46 +38,23 @@ class BalldontlieProviderAdapter(BaseProviderAdapter):
|
||||
def configured_seasons(self) -> list[int]:
|
||||
return settings.PROVIDER_BALLDONTLIE_SEASONS
|
||||
|
||||
@staticmethod
|
||||
def _chunked(values: list[int], size: int):
|
||||
iterator = iter(values)
|
||||
while True:
|
||||
chunk = list(islice(iterator, size))
|
||||
if not chunk:
|
||||
return
|
||||
yield chunk
|
||||
|
||||
def _fetch_game_ids(self) -> list[int]:
|
||||
game_ids: set[int] = set()
|
||||
for season in self.configured_seasons:
|
||||
rows = self.client.list_paginated(
|
||||
"games",
|
||||
params={"seasons[]": season},
|
||||
per_page=settings.PROVIDER_BALLDONTLIE_GAMES_PER_PAGE,
|
||||
page_limit=settings.PROVIDER_BALLDONTLIE_GAMES_PAGE_LIMIT,
|
||||
)
|
||||
for row in rows:
|
||||
game_id = row.get("id")
|
||||
if isinstance(game_id, int):
|
||||
game_ids.add(game_id)
|
||||
return sorted(game_ids)
|
||||
def _api_path(self, path: str) -> str:
|
||||
# Support both base URL variants:
|
||||
# - https://api.balldontlie.io
|
||||
# - https://api.balldontlie.io/nba/v1
|
||||
base = getattr(self.client, "base_url", "").rstrip("/")
|
||||
if base.endswith("/nba/v1"):
|
||||
return path.lstrip("/")
|
||||
return f"{self.nba_prefix}/{path.lstrip('/')}"
|
||||
|
||||
def _fetch_stats_rows(self) -> list[dict]:
|
||||
game_ids = self._fetch_game_ids()
|
||||
if not game_ids:
|
||||
logger.info(
|
||||
"provider_stats_skipped_no_games",
|
||||
extra={"provider": self.namespace, "seasons": self.configured_seasons},
|
||||
)
|
||||
return []
|
||||
|
||||
all_rows: list[dict] = []
|
||||
try:
|
||||
# Use game_ids[] query as documented in balldontlie getting-started flow.
|
||||
for game_id_chunk in self._chunked(game_ids, 25):
|
||||
# OpenAPI supports seasons[] directly for /nba/v1/stats.
|
||||
for season in self.configured_seasons:
|
||||
rows = self.client.list_paginated(
|
||||
"stats",
|
||||
params={"game_ids[]": game_id_chunk},
|
||||
self._api_path("stats"),
|
||||
params={"seasons[]": season},
|
||||
per_page=settings.PROVIDER_BALLDONTLIE_STATS_PER_PAGE,
|
||||
page_limit=settings.PROVIDER_BALLDONTLIE_STATS_PAGE_LIMIT,
|
||||
)
|
||||
@ -101,7 +78,7 @@ class BalldontlieProviderAdapter(BaseProviderAdapter):
|
||||
def search_players(self, *, query: str = "", limit: int = 50, offset: int = 0) -> list[PlayerPayload]:
|
||||
params = {"search": query} if query else None
|
||||
rows = self.client.list_paginated(
|
||||
"players",
|
||||
self._api_path("players"),
|
||||
params=params,
|
||||
per_page=min(limit, settings.PROVIDER_BALLDONTLIE_PLAYERS_PER_PAGE),
|
||||
page_limit=1,
|
||||
@ -113,7 +90,7 @@ class BalldontlieProviderAdapter(BaseProviderAdapter):
|
||||
if not external_player_id.startswith("player-"):
|
||||
return None
|
||||
player_id = external_player_id.replace("player-", "", 1)
|
||||
payload = self.client.get_json(f"players/{player_id}")
|
||||
payload = self.client.get_json(self._api_path(f"players/{player_id}"))
|
||||
data = payload.get("data")
|
||||
if not isinstance(data, dict):
|
||||
return None
|
||||
@ -122,7 +99,7 @@ class BalldontlieProviderAdapter(BaseProviderAdapter):
|
||||
|
||||
def fetch_players(self) -> list[PlayerPayload]:
|
||||
rows = self.client.list_paginated(
|
||||
"players",
|
||||
self._api_path("players"),
|
||||
per_page=settings.PROVIDER_BALLDONTLIE_PLAYERS_PER_PAGE,
|
||||
page_limit=settings.PROVIDER_BALLDONTLIE_PLAYERS_PAGE_LIMIT,
|
||||
)
|
||||
@ -132,7 +109,7 @@ class BalldontlieProviderAdapter(BaseProviderAdapter):
|
||||
return map_competitions()
|
||||
|
||||
def fetch_teams(self) -> list[TeamPayload]:
|
||||
payload = self.client.get_json("teams")
|
||||
payload = self.client.get_json(self._api_path("teams"))
|
||||
rows = payload.get("data") or []
|
||||
return map_teams(rows if isinstance(rows, list) else [])
|
||||
|
||||
|
||||
@ -123,9 +123,6 @@ class BalldontlieClient:
|
||||
request_query["per_page"] = per_page
|
||||
if cursor is not None:
|
||||
request_query["cursor"] = cursor
|
||||
else:
|
||||
# Keep backwards compatibility for endpoints still supporting page-based pagination.
|
||||
request_query["page"] = page
|
||||
|
||||
payload = self.get_json(path, params=request_query)
|
||||
data = payload.get("data") or []
|
||||
@ -139,11 +136,6 @@ class BalldontlieClient:
|
||||
page += 1
|
||||
continue
|
||||
|
||||
next_page = meta.get("next_page")
|
||||
if next_page:
|
||||
page = int(next_page)
|
||||
continue
|
||||
|
||||
break
|
||||
|
||||
return rows
|
||||
|
||||
Reference in New Issue
Block a user