Improve balldontlie query flow and dev container write stability

This commit is contained in:
Alfredo Di Stasio
2026-03-12 11:13:05 +01:00
parent e0e75cfb0c
commit c9dd10a438
10 changed files with 196 additions and 46 deletions

View File

@ -9,7 +9,7 @@ import requests
from apps.providers.adapters.balldontlie_provider import BalldontlieProviderAdapter
from apps.providers.adapters.mvp_provider import MvpDemoProviderAdapter
from apps.providers.clients.balldontlie import BalldontlieClient
from apps.providers.exceptions import ProviderRateLimitError, ProviderTransientError
from apps.providers.exceptions import ProviderRateLimitError, ProviderTransientError, ProviderUnauthorizedError
from apps.providers.registry import get_default_provider_namespace, get_provider
from apps.providers.services.balldontlie_mappings import map_seasons
@ -28,8 +28,10 @@ class _FakeResponse:
class _FakeSession:
def __init__(self, responses: list[Any]):
self._responses = responses
self.calls: list[dict[str, Any]] = []
def get(self, *args, **kwargs):
self.calls.append(kwargs)
item = self._responses.pop(0)
if isinstance(item, Exception):
raise item
@ -69,6 +71,9 @@ class _FakeBalldontlieClient:
}
]
if path == "stats":
requested_ids = (params or {}).get("game_ids[]") or []
if requested_ids and 9902 not in requested_ids:
return []
return [
{
"pts": 20,
@ -83,7 +88,7 @@ class _FakeBalldontlieClient:
"min": "35:12",
"player": {"id": 237},
"team": {"id": 14},
"game": {"season": 2024},
"game": {"id": 9901, "season": 2024},
},
{
"pts": 30,
@ -98,9 +103,14 @@ class _FakeBalldontlieClient:
"min": "33:00",
"player": {"id": 237},
"team": {"id": 14},
"game": {"season": 2024},
"game": {"id": 9902, "season": 2024},
},
]
if path == "games":
return [
{"id": 9901, "season": 2024},
{"id": 9902, "season": 2024},
]
return []
@ -165,6 +175,30 @@ def test_balldontlie_map_seasons_marks_latest_as_current():
assert [row["external_id"] for row in seasons] == ["season-2022", "season-2023", "season-2024"]
@pytest.mark.django_db
def test_balldontlie_adapter_degrades_when_stats_unauthorized(settings):
class _UnauthorizedStatsClient(_FakeBalldontlieClient):
def list_paginated(self, path: str, *, params=None, per_page=100, page_limit=1):
if path == "stats":
raise ProviderUnauthorizedError(
provider="balldontlie",
path="stats",
status_code=401,
detail="Unauthorized",
)
return super().list_paginated(path, params=params, per_page=per_page, page_limit=page_limit)
settings.PROVIDER_BALLDONTLIE_SEASONS = [2024]
settings.PROVIDER_BALLDONTLIE_STATS_STRICT = False
adapter = BalldontlieProviderAdapter(client=_UnauthorizedStatsClient())
payload = adapter.sync_all()
assert payload["players"]
assert payload["teams"]
assert payload["player_stats"] == []
assert payload["player_careers"] == []
@pytest.mark.django_db
def test_balldontlie_client_retries_after_rate_limit(monkeypatch, settings):
monkeypatch.setattr(time, "sleep", lambda _: None)
@ -212,3 +246,26 @@ def test_balldontlie_client_raises_rate_limit_after_max_retries(monkeypatch, set
with pytest.raises(ProviderRateLimitError):
client.get_json("players")
@pytest.mark.django_db
def test_balldontlie_client_cursor_pagination(settings):
session = _FakeSession(
responses=[
_FakeResponse(
status_code=200,
payload={"data": [{"id": 1}], "meta": {"next_cursor": 101}},
),
_FakeResponse(
status_code=200,
payload={"data": [{"id": 2}], "meta": {"next_cursor": None}},
),
]
)
client = BalldontlieClient(session=session)
rows = client.list_paginated("players", per_page=1, page_limit=5)
assert rows == [{"id": 1}, {"id": 2}]
assert session.calls[0]["params"]["page"] == 1
assert "cursor" not in session.calls[0]["params"]
assert session.calls[1]["params"]["cursor"] == 101