Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 27 additions & 3 deletions src/nba_api/stats/library/http.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
class NBAStatsResponse(http.NBAResponse):
"""Response handler for NBA Stats API requests."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self._endpoint = None

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the intention behind self._endpoint?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

self._endpoint stores the endpoint name from get_data_sets(endpoint="...") so that get_normalized_dict() knows which V3 parser to use.

Without it, get_normalized_dict() only has access to the raw response data (via self.get_dict()), which doesn't contain the endpoint name needed to select the correct parser.

Set to None by default, so legacy endpoints continue working unchanged.

def get_normalized_dict(self):
raw_data = self.get_dict()

Expand Down Expand Up @@ -55,6 +59,26 @@ def get_normalized_dict(self):
row[headers[i]] = raw_row[i]
rows.append(row)
data[name] = rows
elif self._endpoint is not None:
try:
from nba_api.stats.endpoints._parsers import get_parser_for_endpoint

endpoint_parser = get_parser_for_endpoint(self._endpoint, raw_data)
data_sets = endpoint_parser.get_data_sets()

for name, dataset in data_sets.items():
headers = dataset["headers"]
row_data = dataset["data"]

rows = []
for raw_row in row_data:
row = {}
for i in range(len(headers)):
row[headers[i]] = raw_row[i]
rows.append(row)
data[name] = rows
except (KeyError, ImportError):
pass

return data

Expand Down Expand Up @@ -96,8 +120,10 @@ def get_headers_from_data_sets(self):
def get_data_sets(self, endpoint=None):
raw_dict = self.get_dict()

if endpoint is not None:
self._endpoint = endpoint

if endpoint is None:
# Process Tabular Json
if "resultSets" in raw_dict:
results = raw_dict["resultSets"]
else:
Expand All @@ -119,8 +145,6 @@ def get_data_sets(self, endpoint=None):
for result_set in results
}
else:
# Process V3 endpoint with custom parser
# Lazy import to avoid circular dependency
from nba_api.stats.endpoints._parsers import get_parser_for_endpoint

endpoint_parser = get_parser_for_endpoint(endpoint, self.get_dict())
Expand Down
144 changes: 144 additions & 0 deletions tests/unit/stats/endpoints/test_boxscoretraditionalv3_normalized.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import json
import pytest
from nba_api.stats.endpoints.boxscoretraditionalv3 import BoxScoreTraditionalV3
from nba_api.stats.library.http import NBAStatsResponse
from .data.boxscoretraditionalv3 import BOXSCORETRADITIONALV3_SAMPLE


class MockResponse(NBAStatsResponse):

def __init__(self, data):
super().__init__(json.dumps(data), 200, "http://mock.url")
self._mock_data = data

def get_dict(self):
return self._mock_data


class TestBoxScoreTraditionalV3Normalized:

def test_get_normalized_dict_returns_data(self):
endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False)
endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE)
endpoint.load_response()

result = endpoint.get_normalized_dict()

assert isinstance(result, dict)
assert len(result) > 0, "get_normalized_dict() should not return empty dict"

assert "PlayerStats" in result
assert "TeamStats" in result
assert "TeamStarterBenchStats" in result

def test_get_normalized_dict_structure(self):
endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False)
endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE)
endpoint.load_response()

result = endpoint.get_normalized_dict()

assert isinstance(result["PlayerStats"], list)
assert isinstance(result["TeamStats"], list)
assert isinstance(result["TeamStarterBenchStats"], list)

if result["PlayerStats"]:
first_player = result["PlayerStats"][0]
assert isinstance(first_player, dict)
assert "gameId" in first_player
assert "personId" in first_player
assert "firstName" in first_player
assert "points" in first_player

if result["TeamStats"]:
first_team = result["TeamStats"][0]
assert isinstance(first_team, dict)
assert "gameId" in first_team
assert "teamId" in first_team
assert "teamName" in first_team
assert "points" in first_team

def test_get_normalized_json_returns_data(self):
endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False)
endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE)
endpoint.load_response()

result = endpoint.get_normalized_json()

assert isinstance(result, str)
assert len(result) > 2, "get_normalized_json() should not return empty JSON"
assert result != "{}", "get_normalized_json() should not return empty object"

parsed = json.loads(result)
assert isinstance(parsed, dict)
assert len(parsed) > 0

def test_get_normalized_json_is_valid_json(self):
endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False)
endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE)
endpoint.load_response()

json_str = endpoint.get_normalized_json()
parsed = json.loads(json_str)

dict_result = endpoint.get_normalized_dict()
assert parsed == dict_result

def test_get_normalized_dict_player_data_correctness(self):
endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False)
endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE)
endpoint.load_response()

result = endpoint.get_normalized_dict()
player_stats = result["PlayerStats"]

assert len(player_stats) == 2

tatum = player_stats[0]
assert tatum["gameId"] == "0022500165"
assert tatum["teamId"] == 1610612738
assert tatum["personId"] == 1630162
assert tatum["firstName"] == "Jayson"
assert tatum["familyName"] == "Tatum"
assert tatum["points"] == 32
assert tatum["plusMinusPoints"] == 12

def test_get_normalized_dict_team_data_correctness(self):
endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False)
endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE)
endpoint.load_response()

result = endpoint.get_normalized_dict()
team_stats = result["TeamStats"]

assert len(team_stats) == 2

celtics = team_stats[0]
assert celtics["gameId"] == "0022500165"
assert celtics["teamId"] == 1610612738
assert celtics["teamCity"] == "Boston"
assert celtics["teamName"] == "Celtics"
assert celtics["points"] == 122
assert celtics["plusMinusPoints"] == 14

warriors = team_stats[1]
assert warriors["teamId"] == 1610612744
assert warriors["teamCity"] == "Golden State"
assert warriors["points"] == 108
assert warriors["plusMinusPoints"] == -14

def test_regression_issue_602(self):
endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False)
endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE)
endpoint.load_response()

normalized_dict = endpoint.get_normalized_dict()
assert normalized_dict != {}, "Issue #602: get_normalized_dict() returns empty dict"
assert len(normalized_dict) > 0, "Issue #602: get_normalized_dict() has no data"

normalized_json = endpoint.get_normalized_json()
assert normalized_json != "{}", "Issue #602: get_normalized_json() returns empty JSON"
assert len(normalized_json) > 2, "Issue #602: get_normalized_json() has no data"

parsed = json.loads(normalized_json)
assert len(parsed) > 0, "Issue #602: Parsed JSON is empty"