diff --git a/src/nba_api/library/http.py b/src/nba_api/library/http.py index aeac18ee..b67785ae 100644 --- a/src/nba_api/library/http.py +++ b/src/nba_api/library/http.py @@ -34,15 +34,21 @@ def __init__(self, response, status_code, url): self._response = response self._status_code = status_code self._url = url + self._dict_cache = None + self._json_cache = None def get_response(self): return self._response def get_dict(self): - return json.loads(self._response) + if self._dict_cache is None: + self._dict_cache = json.loads(self._response) + return self._dict_cache def get_json(self): - return json.dumps(self.get_dict()) + if self._json_cache is None: + self._json_cache = json.dumps(self.get_dict()) + return self._json_cache def valid_json(self): try: @@ -54,6 +60,9 @@ def valid_json(self): def get_url(self): return self._url + def get_status_code(self): + return self._status_code + class NBAHTTP: nba_response = NBAResponse @@ -126,8 +135,8 @@ def send_api_request( contents = None file_path = None - # Sort parameters by key... for some reason this matters for some requests... - parameters = sorted(parameters.items(), key=lambda kv: kv[0]) + # tuples are faster to handle and iterate + parameters = tuple(sorted(parameters.items(), key=lambda kv: kv[0])) if DEBUG and DEBUG_STORAGE: print(endpoint, parameters) @@ -173,7 +182,14 @@ def send_api_request( data = self.nba_response(response=contents, status_code=status_code, url=url) - if raise_exception_on_error and not data.valid_json(): - raise Exception("InvalidResponse: Response is not in a valid JSON format.") + if raise_exception_on_error: + if status_code is not None and status_code >= 400: + raise Exception( + f"HTTPError: Request failed with status code {status_code}." + ) + if not data.valid_json(): + raise Exception( + "InvalidResponse: Response is not in a valid JSON format." + ) return data diff --git a/src/nba_api/live/nba/endpoints/_base.py b/src/nba_api/live/nba/endpoints/_base.py index 382006c6..a3fb9221 100644 --- a/src/nba_api/live/nba/endpoints/_base.py +++ b/src/nba_api/live/nba/endpoints/_base.py @@ -4,7 +4,6 @@ class Endpoint: class DataSet: key = None - data = {} def __init__(self, data=None): if data is None: @@ -23,6 +22,9 @@ def get_request_url(self): def get_response(self): return self.nba_response.get_response() + def get_status_code(self): + return self.nba_response.get_status_code() + def get_dict(self): return self.nba_response.get_dict() diff --git a/src/nba_api/stats/endpoints/_base.py b/src/nba_api/stats/endpoints/_base.py index 1520de65..d8ee0a71 100644 --- a/src/nba_api/stats/endpoints/_base.py +++ b/src/nba_api/stats/endpoints/_base.py @@ -16,7 +16,6 @@ class Endpoint: class DataSet: key: str | None = None - data: dict[str, Any] = {} def __init__(self, data: dict[str, Any]) -> None: self.data = data @@ -88,6 +87,10 @@ def get_response(self) -> str: """Return the raw response string.""" return self.nba_response.get_response() + def get_status_code(self) -> int: + """Return the HTTP status code of the response.""" + return self.nba_response.get_status_code() + def get_dict(self) -> dict[str, Any]: """Return the response as a dictionary.""" return self.nba_response.get_dict() diff --git a/src/nba_api/stats/library/http.py b/src/nba_api/stats/library/http.py index 558ee814..d82ab1a5 100644 --- a/src/nba_api/stats/library/http.py +++ b/src/nba_api/stats/library/http.py @@ -29,26 +29,29 @@ class NBAStatsResponse(http.NBAResponse): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self._endpoint = None + self._normalized_dict_cache = None @staticmethod def _build_rows(headers, row_set): return [dict(zip(headers, raw_row, strict=False)) for raw_row in row_set] def get_normalized_dict(self): + if self._normalized_dict_cache is not None: + return self._normalized_dict_cache + raw_data = self.get_dict() data = {} - legacy_headers = ["resultSets", "resultSet"] - is_legacy = set(legacy_headers) & set(raw_data.keys()) + legacy_headers = {"resultSets", "resultSet"} + raw_keys = raw_data.keys() + is_legacy = bool(legacy_headers & raw_keys) if is_legacy: - if "resultSets" in raw_data: - results = raw_data["resultSets"] - if "Meta" in results: - return results - else: - results = raw_data["resultSet"] + results = raw_data.get("resultSets") or raw_data.get("resultSet") + if results and "Meta" in results: + self._normalized_dict_cache = results + return results if isinstance(results, dict): results = [results] for result in results: @@ -61,27 +64,31 @@ def get_normalized_dict(self): endpoint_parser = get_parser_for_endpoint(self._endpoint, raw_data) for name, dataset in endpoint_parser.get_data_sets().items(): data[name] = self._build_rows(dataset["headers"], dataset["data"]) - except (KeyError, ImportError): + except KeyError: pass + self._normalized_dict_cache = data return data def get_normalized_json(self): + if self._normalized_dict_cache is not None: + return json.dumps(self._normalized_dict_cache) return json.dumps(self.get_normalized_dict()) def get_parameters(self): - if not self.valid_json() or "parameters" not in self.get_dict(): + raw = self.get_dict() if self.valid_json() else None + if raw is None or "parameters" not in raw: return None - parameters = self.get_dict()["parameters"] + parameters = raw["parameters"] if isinstance(parameters, dict): return parameters - parameters = {} - for parameter in self.get_dict()["parameters"]: + result = {} + for parameter in parameters: for key, value in parameter.items(): - parameters.update({key: value}) - return parameters + result[key] = value + return result def get_headers_from_data_sets(self): raw_dict = self.get_dict() diff --git a/src/nba_api/stats/static/players.py b/src/nba_api/stats/static/players.py index 4b4e007a..b7291211 100644 --- a/src/nba_api/stats/static/players.py +++ b/src/nba_api/stats/static/players.py @@ -1,3 +1,4 @@ +import functools import re import unicodedata @@ -11,17 +12,22 @@ wnba_players, ) +# Pre-built index for O(1) ID lookup +_players_by_id = {p[player_index_id]: p for p in players} +_wnba_players_by_id = {p[player_index_id]: p for p in wnba_players} -def _find_players(regex_pattern, row_id, players=players): - players_found = [] - for player in players: - if re.search( - _strip_accents(regex_pattern), - _strip_accents(str(player[row_id])), - flags=re.I, - ): - players_found.append(_get_player_dict(player)) - return players_found +# Pre-computed cached lists +_cached_players = None +_cached_active_players = None +_cached_inactive_players = None +_cached_wnba_players = None +_cached_wnba_active_players = None +_cached_wnba_inactive_players = None + + +@functools.lru_cache(maxsize=128) +def _compile_regex(pattern): + return re.compile(_strip_accents(pattern), flags=re.I) def _strip_accents(inputstr: str) -> str: @@ -36,38 +42,72 @@ def _strip_accents(inputstr: str) -> str: ) -def _find_player_by_id(player_id, players=players): - regex_pattern = f"^{player_id}$" - players_list = _find_players(regex_pattern, player_index_id, players=players) - if len(players_list) > 1: - raise Exception("Found more than 1 id") - elif not players_list: - return None - else: - return players_list[0] - - -def _get_players(players=players): - players_list = [] - for player in players: - players_list.append(_get_player_dict(player)) - return players_list - - -def _get_active_players(players=players): - players_list = [] - for player in players: - if player[player_index_is_active]: - players_list.append(_get_player_dict(player)) - return players_list - - -def _get_inactive_players(players=players): - players_list = [] - for player in players: - if not player[player_index_is_active]: - players_list.append(_get_player_dict(player)) - return players_list +def _find_players(regex_pattern, row_id, players=players): + compiled = _compile_regex(regex_pattern) + return [ + _get_player_dict(player) + for player in players + if compiled.search(_strip_accents(str(player[row_id]))) + ] + + +def _find_player_by_id(player_id, _index=_players_by_id): + player = _index.get(player_id) + return _get_player_dict(player) if player is not None else None + + +def _get_players(players=players, _cache=False): + global _cached_players, _cached_wnba_players + if _cache: + if players is wnba_players: + if _cached_wnba_players is None: + _cached_wnba_players = [_get_player_dict(p) for p in players] + return _cached_wnba_players + else: + if _cached_players is None: + _cached_players = [_get_player_dict(p) for p in players] + return _cached_players + return [_get_player_dict(p) for p in players] + + +def _get_active_players(players=players, _cache=False): + global _cached_active_players, _cached_wnba_active_players + if _cache: + if players is wnba_players: + if _cached_wnba_active_players is None: + _cached_wnba_active_players = [ + _get_player_dict(p) for p in players if p[player_index_is_active] + ] + return _cached_wnba_active_players + else: + if _cached_active_players is None: + _cached_active_players = [ + _get_player_dict(p) for p in players if p[player_index_is_active] + ] + return _cached_active_players + return [_get_player_dict(p) for p in players if p[player_index_is_active]] + + +def _get_inactive_players(players=players, _cache=False): + global _cached_inactive_players, _cached_wnba_inactive_players + if _cache: + if players is wnba_players: + if _cached_wnba_inactive_players is None: + _cached_wnba_inactive_players = [ + _get_player_dict(p) + for p in players + if not p[player_index_is_active] + ] + return _cached_wnba_inactive_players + else: + if _cached_inactive_players is None: + _cached_inactive_players = [ + _get_player_dict(p) + for p in players + if not p[player_index_is_active] + ] + return _cached_inactive_players + return [_get_player_dict(p) for p in players if not p[player_index_is_active]] def _get_player_dict(player_row): @@ -97,15 +137,15 @@ def find_player_by_id(player_id): def get_players(): - return _get_players() + return _get_players(_cache=True) def get_active_players(): - return _get_active_players() + return _get_active_players(_cache=True) def get_inactive_players(): - return _get_inactive_players() + return _get_inactive_players(_cache=True) def find_wnba_players_by_full_name(regex_pattern): @@ -121,16 +161,16 @@ def find_wnba_players_by_last_name(regex_pattern): def find_wnba_player_by_id(player_id): - return _find_player_by_id(player_id, players=wnba_players) + return _find_player_by_id(player_id, _index=_wnba_players_by_id) def get_wnba_players(): - return _get_players(players=wnba_players) + return _get_players(players=wnba_players, _cache=True) def get_wnba_active_players(): - return _get_active_players(players=wnba_players) + return _get_active_players(players=wnba_players, _cache=True) def get_wnba_inactive_players(): - return _get_inactive_players(players=wnba_players) + return _get_inactive_players(players=wnba_players, _cache=True) diff --git a/src/nba_api/stats/static/teams.py b/src/nba_api/stats/static/teams.py index e494f8fe..009978e8 100644 --- a/src/nba_api/stats/static/teams.py +++ b/src/nba_api/stats/static/teams.py @@ -1,4 +1,6 @@ +import functools import re +import unicodedata from nba_api.stats.library.data import ( team_index_abbreviation, @@ -13,57 +15,72 @@ wnba_teams, ) +# Pre-built indexes for O(1) lookups +_teams_by_id = {t[team_index_id]: t for t in teams} +_teams_by_abbreviation = {t[team_index_abbreviation]: t for t in teams} +_wnba_teams_by_id = {t[team_index_id]: t for t in wnba_teams} +_wnba_teams_by_abbreviation = {t[team_index_abbreviation]: t for t in wnba_teams} + +# Pre-computed cached lists +_cached_teams = None +_cached_wnba_teams = None + + +@functools.lru_cache(maxsize=128) +def _compile_regex(pattern): + return re.compile(_strip_accents(pattern), flags=re.I) + + +def _strip_accents(inputstr: str) -> str: + normalizedstr = unicodedata.normalize("NFD", inputstr) + return "".join(c for c in normalizedstr if unicodedata.category(c) != "Mn") + def _find_teams(regex_pattern, row_id, teams=teams): - teams_found = [] - for team in teams: - if re.search(regex_pattern, str(team[row_id]), flags=re.I): - teams_found.append(_get_team_dict(team)) - return teams_found - - -def _find_team_name_by_id(team_id, teams=teams): - regex_pattern = f"^{team_id}$" - teams_list = _find_teams(regex_pattern, team_index_id, teams=teams) - if len(teams_list) > 1: - raise Exception("Found more than 1 id") - elif not teams_list: - return None - else: - return teams_list[0] - - -def _find_team_by_abbreviation(abbreviation, teams=teams): - regex_pattern = f"^{abbreviation}$" - teams_list = _find_teams(regex_pattern, team_index_abbreviation, teams=teams) - if len(teams_list) > 1: - raise Exception("Found more than 1 id") - elif not teams_list: - return None - else: - return teams_list[0] + compiled = _compile_regex(regex_pattern) + return [ + _get_team_dict(team) + for team in teams + if compiled.search(_strip_accents(str(team[row_id]))) + ] + + +def _find_team_name_by_id(team_id, _index=_teams_by_id): + team = _index.get(team_id) + return _get_team_dict(team) if team is not None else None + + +def _find_team_by_abbreviation(abbreviation, _index=_teams_by_abbreviation): + team = _index.get(abbreviation.upper()) + return _get_team_dict(team) if team is not None else None def _find_teams_by_championship_year(year, teams=teams): - for team in teams: - if year in team[team_index_championship_year]: - result = team[team_index_full_name] - return result + return [ + _get_team_dict(team) + for team in teams + if year in team[team_index_championship_year] + ] def _find_teams_by_year_founded(year, teams=teams): - teams_found = [] - for team in teams: - if team[team_index_year_founded] == year: - teams_found.append(_get_team_dict(team)) - return teams_found + return [ + _get_team_dict(team) for team in teams if team[team_index_year_founded] == year + ] -def _get_teams(teams=teams): - teams_list = [] - for team in teams: - teams_list.append(_get_team_dict(team)) - return teams_list +def _get_teams(teams=teams, _cache=False): + global _cached_teams, _cached_wnba_teams + if _cache: + if teams is wnba_teams: + if _cached_wnba_teams is None: + _cached_wnba_teams = [_get_team_dict(t) for t in teams] + return _cached_wnba_teams + else: + if _cached_teams is None: + _cached_teams = [_get_team_dict(t) for t in teams] + return _cached_teams + return [_get_team_dict(t) for t in teams] def _get_team_dict(team_row): @@ -111,7 +128,7 @@ def find_team_name_by_id(team_id): def get_teams(): - return _get_teams() + return _get_teams(_cache=True) def find_wnba_teams_by_full_name(regex_pattern): @@ -139,12 +156,12 @@ def find_wnba_teams_by_championship_year(year): def find_wnba_team_by_abbreviation(abbreviation): - return _find_team_by_abbreviation(abbreviation, teams=wnba_teams) + return _find_team_by_abbreviation(abbreviation, _index=_wnba_teams_by_abbreviation) def find_wnba_team_name_by_id(team_id): - return _find_team_name_by_id(team_id, teams=wnba_teams) + return _find_team_name_by_id(team_id, _index=_wnba_teams_by_id) def get_wnba_teams(): - return _get_teams(teams=wnba_teams) + return _get_teams(teams=wnba_teams, _cache=True)