From 69cee4fbe4645b2055e7221056be38f6c3e3830a Mon Sep 17 00:00:00 2001 From: Brandon Hawi Date: Tue, 2 Dec 2025 19:35:16 -0800 Subject: [PATCH 1/2] fix: resolve get_normalized_dict() returning empty for V3 endpoints Add support for V3 endpoints with custom parsers in get_normalized_dict() and get_normalized_json() methods. Previously, these methods only handled legacy format (resultSets/resultSet) and returned empty dictionaries for V3 endpoints like BoxScoreTraditionalV3. Changes: - Store endpoint name in NBAStatsResponse when get_data_sets() is called - Update get_normalized_dict() to use parser for V3 endpoints - Convert parsed V3 data to normalized format (list of dicts) - Add comprehensive unit tests for normalized methods Fixes #602 --- src/nba_api/stats/library/http.py | 31 +++ .../test_boxscoretraditionalv3_normalized.py | 182 ++++++++++++++++++ 2 files changed, 213 insertions(+) create mode 100644 tests/unit/stats/endpoints/test_boxscoretraditionalv3_normalized.py diff --git a/src/nba_api/stats/library/http.py b/src/nba_api/stats/library/http.py index 5a98136f..20187b9a 100644 --- a/src/nba_api/stats/library/http.py +++ b/src/nba_api/stats/library/http.py @@ -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 # Store endpoint for V3 normalization + def get_normalized_dict(self): raw_data = self.get_dict() @@ -55,6 +59,29 @@ def get_normalized_dict(self): row[headers[i]] = raw_row[i] rows.append(row) data[name] = rows + elif self._endpoint is not None: + # Handle V3 endpoints with custom parsers + 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() + + # Normalize the data_sets into the same format as legacy + 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): + # If parser not found or import fails, return empty dict + pass return data @@ -96,6 +123,10 @@ def get_headers_from_data_sets(self): def get_data_sets(self, endpoint=None): raw_dict = self.get_dict() + # Store endpoint for use in get_normalized_dict() + if endpoint is not None: + self._endpoint = endpoint + if endpoint is None: # Process Tabular Json if "resultSets" in raw_dict: diff --git a/tests/unit/stats/endpoints/test_boxscoretraditionalv3_normalized.py b/tests/unit/stats/endpoints/test_boxscoretraditionalv3_normalized.py new file mode 100644 index 00000000..cc152446 --- /dev/null +++ b/tests/unit/stats/endpoints/test_boxscoretraditionalv3_normalized.py @@ -0,0 +1,182 @@ +"""Unit tests for BoxScoreTraditionalV3 get_normalized_dict() and get_normalized_json() methods. + +Tests for GitHub issue #602: https://github.com/swar/nba_api/issues/602 +""" + +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): + """Mock NBA Stats HTTP response.""" + + def __init__(self, data): + # Call parent with dummy values for response, status_code, and url + super().__init__(json.dumps(data), 200, "http://mock.url") + self._mock_data = data + + def get_dict(self): + return self._mock_data + + +class TestBoxScoreTraditionalV3Normalized: + """Test normalized dict/json methods for BoxScoreTraditionalV3 endpoint.""" + + def test_get_normalized_dict_returns_data(self): + """Test that get_normalized_dict() returns non-empty data for V3 endpoints.""" + # Create endpoint with mocked response + endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) + endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) + endpoint.load_response() + + # Call get_normalized_dict() + result = endpoint.get_normalized_dict() + + # Verify it returns non-empty data + assert isinstance(result, dict) + assert len(result) > 0, "get_normalized_dict() should not return empty dict" + + # Verify expected datasets are present + assert "PlayerStats" in result + assert "TeamStats" in result + assert "TeamStarterBenchStats" in result + + def test_get_normalized_dict_structure(self): + """Test that get_normalized_dict() returns correct structure.""" + endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) + endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) + endpoint.load_response() + + result = endpoint.get_normalized_dict() + + # Each dataset should be a list of dicts + assert isinstance(result["PlayerStats"], list) + assert isinstance(result["TeamStats"], list) + assert isinstance(result["TeamStarterBenchStats"], list) + + # Check that rows are dicts with proper keys + 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): + """Test that get_normalized_json() returns non-empty JSON string.""" + endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) + endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) + endpoint.load_response() + + result = endpoint.get_normalized_json() + + # Verify it returns non-empty JSON string + 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" + + # Verify it's valid JSON + parsed = json.loads(result) + assert isinstance(parsed, dict) + assert len(parsed) > 0 + + def test_get_normalized_json_is_valid_json(self): + """Test that get_normalized_json() returns valid, parseable JSON.""" + 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) + + # Verify structure matches get_normalized_dict() + dict_result = endpoint.get_normalized_dict() + assert parsed == dict_result + + def test_get_normalized_dict_player_data_correctness(self): + """Test that normalized PlayerStats data is correct.""" + 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"] + + # Should have 2 players based on fixture + assert len(player_stats) == 2 + + # Check first player (Jayson Tatum) + tatum = player_stats[0] + assert tatum["gameId"] == "0022500165" + assert tatum["teamId"] == 1610612738 # BOS + 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): + """Test that normalized TeamStats data is correct.""" + 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"] + + # Should have 2 teams + assert len(team_stats) == 2 + + # Check home team (Celtics) + 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 + + # Check away team (Warriors) + 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): + """Regression test for GitHub issue #602. + + Issue #602 reported that get_normalized_dict() and get_normalized_json() + returned empty values for BoxScoreTraditionalV3 endpoint. + + This test ensures the bug does not reoccur. + """ + endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) + endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) + endpoint.load_response() + + # Test get_normalized_dict() + 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" + + # Test get_normalized_json() + 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" + + # Verify the JSON is parseable and non-empty + parsed = json.loads(normalized_json) + assert len(parsed) > 0, "Issue #602: Parsed JSON is empty" From c399957201ed1595c46ad81d52880e6e749deb4a Mon Sep 17 00:00:00 2001 From: Brandon Hawi Date: Sun, 7 Dec 2025 11:28:37 -0800 Subject: [PATCH 2/2] refactor: remove comments from code for cleaner implementation Remove all inline comments and docstrings from the V3 endpoint normalization implementation and associated unit tests to improve code readability and reduce clutter. --- src/nba_api/stats/library/http.py | 9 +---- .../test_boxscoretraditionalv3_normalized.py | 40 +------------------ 2 files changed, 2 insertions(+), 47 deletions(-) diff --git a/src/nba_api/stats/library/http.py b/src/nba_api/stats/library/http.py index 20187b9a..68f71ea5 100644 --- a/src/nba_api/stats/library/http.py +++ b/src/nba_api/stats/library/http.py @@ -28,7 +28,7 @@ class NBAStatsResponse(http.NBAResponse): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self._endpoint = None # Store endpoint for V3 normalization + self._endpoint = None def get_normalized_dict(self): raw_data = self.get_dict() @@ -60,14 +60,12 @@ def get_normalized_dict(self): rows.append(row) data[name] = rows elif self._endpoint is not None: - # Handle V3 endpoints with custom parsers 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() - # Normalize the data_sets into the same format as legacy for name, dataset in data_sets.items(): headers = dataset["headers"] row_data = dataset["data"] @@ -80,7 +78,6 @@ def get_normalized_dict(self): rows.append(row) data[name] = rows except (KeyError, ImportError): - # If parser not found or import fails, return empty dict pass return data @@ -123,12 +120,10 @@ def get_headers_from_data_sets(self): def get_data_sets(self, endpoint=None): raw_dict = self.get_dict() - # Store endpoint for use in get_normalized_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: @@ -150,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()) diff --git a/tests/unit/stats/endpoints/test_boxscoretraditionalv3_normalized.py b/tests/unit/stats/endpoints/test_boxscoretraditionalv3_normalized.py index cc152446..520927be 100644 --- a/tests/unit/stats/endpoints/test_boxscoretraditionalv3_normalized.py +++ b/tests/unit/stats/endpoints/test_boxscoretraditionalv3_normalized.py @@ -1,8 +1,3 @@ -"""Unit tests for BoxScoreTraditionalV3 get_normalized_dict() and get_normalized_json() methods. - -Tests for GitHub issue #602: https://github.com/swar/nba_api/issues/602 -""" - import json import pytest from nba_api.stats.endpoints.boxscoretraditionalv3 import BoxScoreTraditionalV3 @@ -11,10 +6,8 @@ class MockResponse(NBAStatsResponse): - """Mock NBA Stats HTTP response.""" def __init__(self, data): - # Call parent with dummy values for response, status_code, and url super().__init__(json.dumps(data), 200, "http://mock.url") self._mock_data = data @@ -23,41 +16,32 @@ def get_dict(self): class TestBoxScoreTraditionalV3Normalized: - """Test normalized dict/json methods for BoxScoreTraditionalV3 endpoint.""" def test_get_normalized_dict_returns_data(self): - """Test that get_normalized_dict() returns non-empty data for V3 endpoints.""" - # Create endpoint with mocked response endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) endpoint.load_response() - # Call get_normalized_dict() result = endpoint.get_normalized_dict() - # Verify it returns non-empty data assert isinstance(result, dict) assert len(result) > 0, "get_normalized_dict() should not return empty dict" - # Verify expected datasets are present assert "PlayerStats" in result assert "TeamStats" in result assert "TeamStarterBenchStats" in result def test_get_normalized_dict_structure(self): - """Test that get_normalized_dict() returns correct structure.""" endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) endpoint.load_response() result = endpoint.get_normalized_dict() - # Each dataset should be a list of dicts assert isinstance(result["PlayerStats"], list) assert isinstance(result["TeamStats"], list) assert isinstance(result["TeamStarterBenchStats"], list) - # Check that rows are dicts with proper keys if result["PlayerStats"]: first_player = result["PlayerStats"][0] assert isinstance(first_player, dict) @@ -75,25 +59,21 @@ def test_get_normalized_dict_structure(self): assert "points" in first_team def test_get_normalized_json_returns_data(self): - """Test that get_normalized_json() returns non-empty JSON string.""" endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) endpoint.load_response() result = endpoint.get_normalized_json() - # Verify it returns non-empty JSON string 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" - # Verify it's valid JSON parsed = json.loads(result) assert isinstance(parsed, dict) assert len(parsed) > 0 def test_get_normalized_json_is_valid_json(self): - """Test that get_normalized_json() returns valid, parseable JSON.""" endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) endpoint.load_response() @@ -101,12 +81,10 @@ def test_get_normalized_json_is_valid_json(self): json_str = endpoint.get_normalized_json() parsed = json.loads(json_str) - # Verify structure matches get_normalized_dict() dict_result = endpoint.get_normalized_dict() assert parsed == dict_result def test_get_normalized_dict_player_data_correctness(self): - """Test that normalized PlayerStats data is correct.""" endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) endpoint.load_response() @@ -114,13 +92,11 @@ def test_get_normalized_dict_player_data_correctness(self): result = endpoint.get_normalized_dict() player_stats = result["PlayerStats"] - # Should have 2 players based on fixture assert len(player_stats) == 2 - # Check first player (Jayson Tatum) tatum = player_stats[0] assert tatum["gameId"] == "0022500165" - assert tatum["teamId"] == 1610612738 # BOS + assert tatum["teamId"] == 1610612738 assert tatum["personId"] == 1630162 assert tatum["firstName"] == "Jayson" assert tatum["familyName"] == "Tatum" @@ -128,7 +104,6 @@ def test_get_normalized_dict_player_data_correctness(self): assert tatum["plusMinusPoints"] == 12 def test_get_normalized_dict_team_data_correctness(self): - """Test that normalized TeamStats data is correct.""" endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) endpoint.load_response() @@ -136,10 +111,8 @@ def test_get_normalized_dict_team_data_correctness(self): result = endpoint.get_normalized_dict() team_stats = result["TeamStats"] - # Should have 2 teams assert len(team_stats) == 2 - # Check home team (Celtics) celtics = team_stats[0] assert celtics["gameId"] == "0022500165" assert celtics["teamId"] == 1610612738 @@ -148,7 +121,6 @@ def test_get_normalized_dict_team_data_correctness(self): assert celtics["points"] == 122 assert celtics["plusMinusPoints"] == 14 - # Check away team (Warriors) warriors = team_stats[1] assert warriors["teamId"] == 1610612744 assert warriors["teamCity"] == "Golden State" @@ -156,27 +128,17 @@ def test_get_normalized_dict_team_data_correctness(self): assert warriors["plusMinusPoints"] == -14 def test_regression_issue_602(self): - """Regression test for GitHub issue #602. - - Issue #602 reported that get_normalized_dict() and get_normalized_json() - returned empty values for BoxScoreTraditionalV3 endpoint. - - This test ensures the bug does not reoccur. - """ endpoint = BoxScoreTraditionalV3(game_id="0022500165", get_request=False) endpoint.nba_response = MockResponse(BOXSCORETRADITIONALV3_SAMPLE) endpoint.load_response() - # Test get_normalized_dict() 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" - # Test get_normalized_json() 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" - # Verify the JSON is parseable and non-empty parsed = json.loads(normalized_json) assert len(parsed) > 0, "Issue #602: Parsed JSON is empty"