From 9d68eaf77281c234ae3da9b11534588353f42da7 Mon Sep 17 00:00:00 2001 From: Corey Schaf Date: Thu, 9 Nov 2023 10:10:28 -0500 Subject: [PATCH] Formatting updates. Adds ruff configuration, enforces line length. Fixes type hints on teams endpoints. In teams it also changes id to team_id for each method. closes #12 (#13) --- nhlpy/__init__.py | 2 +- nhlpy/_version.py | 2 +- nhlpy/api/games.py | 12 ++---- nhlpy/api/helpers.py | 91 +++++++++++++---------------------------- nhlpy/api/players.py | 8 +--- nhlpy/api/standings.py | 8 +--- nhlpy/api/teams.py | 44 ++++++++++---------- pyproject.toml | 33 ++++++++++++++- tests/test_core.py | 8 +--- tests/test_schedule.py | 5 +++ tests/test_standings.py | 12 ++---- tests/test_teams.py | 43 +++++++++++++++++++ 12 files changed, 144 insertions(+), 124 deletions(-) create mode 100644 tests/test_teams.py diff --git a/nhlpy/__init__.py b/nhlpy/__init__.py index 2069d39..bffba0e 100644 --- a/nhlpy/__init__.py +++ b/nhlpy/__init__.py @@ -1 +1 @@ -from .nhl_client import NHLClient +from .nhl_client import NHLClient # noqa: F401 diff --git a/nhlpy/_version.py b/nhlpy/_version.py index 26b553a..d686a6e 100644 --- a/nhlpy/_version.py +++ b/nhlpy/_version.py @@ -1,3 +1,3 @@ # Should this be driven by the main pyproject.toml file? yes, is it super convoluted? yes, can it wait? sure -__version__ = "0.4.12" +__version__ = "0.4.13" diff --git a/nhlpy/api/games.py b/nhlpy/api/games.py index af1fa0a..4bc055d 100644 --- a/nhlpy/api/games.py +++ b/nhlpy/api/games.py @@ -42,8 +42,8 @@ def get_game_live_feed(self, game_id: Union[str, int]) -> dict: Returns a live feed for the game with the id supplied. WARNING, this tends to be a large response. :param game_id: int, NHL game id, - GameIDS are in the format {Season}{GameType}{GameNumber}. For example, the first game of the 2020-2021 season - would be 2020020001. This is also the gamePk field that can be found in the team schedule endpoints. + GameIDS are in the format {Season}{GameType}{GameNumber}. For example, the first game of the 2020-2021 + season would be 2020020001. This is also the gamePk field that can be found in the team schedule endpoints. For playoffs the {GameNumber} portion will be formatted as RRMG where RR is the round (01, 02, 03, 04), M is the matchup and G is the game out of 7. So 2022030412 is the 2022-2023 NHL season, 03 means playoffs @@ -59,18 +59,14 @@ def get_game_live_feed(self, game_id: Union[str, int]) -> dict: """ return self._get(resource=f"game/{game_id}/feed/live").json() - def get_game_live_feed_diff_after_timestamp( - self, game_id: Union[str, int], timestamp: str - ) -> dict: + def get_game_live_feed_diff_after_timestamp(self, game_id: Union[str, int], timestamp: str) -> dict: """ Returns the difference in the live feed game data, from since the given timestamp: param. :param game_id: :param timestamp: :return: """ - warnings.warn( - "This endpoint is still experimental and may not work as expected" - ) + warnings.warn("This endpoint is still experimental and may not work as expected") return self._get(resource=f"game/{game_id}/feed/live/diffPath").json() def get_game_boxscore(self, game_id: Union[str, int]) -> List[dict]: diff --git a/nhlpy/api/helpers.py b/nhlpy/api/helpers.py index 16d71b8..b96a59f 100644 --- a/nhlpy/api/helpers.py +++ b/nhlpy/api/helpers.py @@ -7,49 +7,33 @@ from nhlpy.api.games import Games -def _parse_team_specific_game_data( - game_item: dict, team_side: str, game_boxscore_data: dict -) -> None: +def _parse_team_specific_game_data(game_item: dict, team_side: str, game_boxscore_data: dict) -> None: """ Parser helper method :param team_side: :param game_boxscore_data: :return: """ - game_item[f"{team_side}_pim"] = game_boxscore_data[team_side]["teamStats"][ - "teamSkaterStats" - ]["pim"] - game_item[f"{team_side}_shots"] = game_boxscore_data[team_side]["teamStats"][ - "teamSkaterStats" - ]["shots"] + game_item[f"{team_side}_pim"] = game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"]["pim"] + game_item[f"{team_side}_shots"] = game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"]["shots"] game_item[f"{team_side}_pp_percent"] = float( - game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"][ - "powerPlayPercentage" - ] + game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"]["powerPlayPercentage"] ) - game_item[f"{team_side}_pp_goals"] = game_boxscore_data[team_side]["teamStats"][ - "teamSkaterStats" - ]["powerPlayGoals"] - game_item[f"{team_side}_pp_opps"] = game_boxscore_data[team_side]["teamStats"][ - "teamSkaterStats" - ]["powerPlayOpportunities"] + game_item[f"{team_side}_pp_goals"] = game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"]["powerPlayGoals"] + game_item[f"{team_side}_pp_opps"] = game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"][ + "powerPlayOpportunities" + ] game_item[f"{team_side}_fo_win_percent"] = float( - game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"][ - "faceOffWinPercentage" - ] + game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"]["faceOffWinPercentage"] ) - game_item[f"{team_side}_shots_blocked"] = game_boxscore_data[team_side][ - "teamStats" - ]["teamSkaterStats"]["blocked"] - game_item[f"{team_side}_shots_takeaways"] = game_boxscore_data[team_side][ - "teamStats" - ]["teamSkaterStats"]["takeaways"] - game_item[f"{team_side}_shots_giveaways"] = game_boxscore_data[team_side][ - "teamStats" - ]["teamSkaterStats"]["giveaways"] - game_item[f"{team_side}_shots_hits"] = game_boxscore_data[team_side]["teamStats"][ - "teamSkaterStats" - ]["hits"] + game_item[f"{team_side}_shots_blocked"] = game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"]["blocked"] + game_item[f"{team_side}_shots_takeaways"] = game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"][ + "takeaways" + ] + game_item[f"{team_side}_shots_giveaways"] = game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"][ + "giveaways" + ] + game_item[f"{team_side}_shots_hits"] = game_boxscore_data[team_side]["teamStats"]["teamSkaterStats"]["hits"] def _get_shooter(event) -> (int, str): @@ -90,9 +74,7 @@ def _get_assists(event) -> List[Tuple[int, str]]: class Helpers: - def pythagorean_expectation( - self, goals_for: int, goals_against: int, exponent: float = 2.37 - ): + def pythagorean_expectation(self, goals_for: int, goals_against: int, exponent: float = 2.37): """ Calculates the pythagorean expectation for a team based on the goals for and goals against. :param goals_for: @@ -100,9 +82,7 @@ def pythagorean_expectation( :param exponent: :return: """ - return goals_for**exponent / ( - goals_for**exponent + goals_against**exponent - ) + return goals_for**exponent / (goals_for**exponent + goals_against**exponent) def league_standings(self, season: str, py_exp_ex: float = 2.37) -> List[dict]: """ @@ -116,9 +96,7 @@ def league_standings(self, season: str, py_exp_ex: float = 2.37) -> List[dict]: :param season: Season in format of 20202021 :return: """ - schedule_data: dict = Standings().get_standings( - season=season, detailed_record=True - ) + schedule_data: dict = Standings().get_standings(season=season, detailed_record=True) records = schedule_data["records"] teams = [] for division in records: @@ -158,19 +136,18 @@ def get_all_game_results( """ :param season: - :param detailed_game_data: If True, will return the full game data for each game. If False, will only return simple game data. + :param detailed_game_data: If True, will return the full game data for each game. + If False, will only return simple game data. :param game_type: :param team_ids: :return: """ warnings.warn( "This endpoint will query the schedule API to get the games, and then sequentially query the boxscore API" - " for each game. This is a slow endpoint, do not call this while in a loop, or multiple times in succession" + " for each game. This is a slow endpoint, DO NOT call this while in a loop, or multiple times in succession" ) games = [] - game_dates = Schedule().get_schedule( - season=season, game_type=game_type, team_ids=team_ids - )["dates"] + game_dates = Schedule().get_schedule(season=season, game_type=game_type, team_ids=team_ids)["dates"] for d in game_dates: date = d["date"] for game in d["games"]: @@ -191,12 +168,8 @@ def get_all_game_results( game_client = Games() for game in games: data = game_client.get_game_boxscore(game_id=game["game_id"])["teams"] - _parse_team_specific_game_data( - game_item=game, team_side="away", game_boxscore_data=data - ) - _parse_team_specific_game_data( - game_item=game, team_side="home", game_boxscore_data=data - ) + _parse_team_specific_game_data(game_item=game, team_side="away", game_boxscore_data=data) + _parse_team_specific_game_data(game_item=game, team_side="home", game_boxscore_data=data) return games @@ -225,9 +198,7 @@ def parse_shot_data_by_game(self, game_id: str) -> List[dict]: continue player_id, player_name = ( - _get_shooter(event_type) - if event_type["result"]["event"] == "Shot" - else _get_goal_scorer(event_type) + _get_shooter(event_type) if event_type["result"]["event"] == "Shot" else _get_goal_scorer(event_type) ) try: @@ -238,12 +209,8 @@ def parse_shot_data_by_game(self, game_id: str) -> List[dict]: "game_start": game_start, "player_id": player_id, "player_name": player_name, - "player_side": "HOME" - if home_team == event_type["team"]["id"] - else "AWAY", - "shot_type": event_type["result"].get( - "secondaryType", "Wrist Shot" - ), + "player_side": "HOME" if home_team == event_type["team"]["id"] else "AWAY", + "shot_type": event_type["result"].get("secondaryType", "Wrist Shot"), "home_team": home_team, "away_team": away_team, "event": event_type["result"]["event"], diff --git a/nhlpy/api/players.py b/nhlpy/api/players.py index 5337e09..6f62fbf 100644 --- a/nhlpy/api/players.py +++ b/nhlpy/api/players.py @@ -10,9 +10,7 @@ def get_player(self, person_id: str) -> dict: """ return self._get(resource=f"people/{person_id}").json()["people"] - def get_player_stats( - self, person_id: str, season: str = None, stat_type: str = "statsSingleSeason" - ) -> dict: + def get_player_stats(self, person_id: str, season: str = None, stat_type: str = "statsSingleSeason") -> dict: """ This returns a players statistics based on the param stat_type: and season: An example of this may be client.players.get_player_stats(stat_type="yearByYear", season="20202021"). In some instances season: @@ -30,9 +28,7 @@ def get_player_stats( :return: """ query = f"stats={stat_type}" if stat_type else "" - return self._get( - resource=f"people/{person_id}/stats?season={season}&{query}" - ).json()["stats"] + return self._get(resource=f"people/{person_id}/stats?season={season}&{query}").json()["stats"] def get_player_stat_types(self) -> dict: """ diff --git a/nhlpy/api/standings.py b/nhlpy/api/standings.py index 06234fe..703fc10 100644 --- a/nhlpy/api/standings.py +++ b/nhlpy/api/standings.py @@ -24,9 +24,7 @@ def get_standings(self, season: str, detailed_record: bool = False) -> dict: response: dict = self._get(resource=f"standings?{modifier}{detailed}").json() return response["records"] - def get_standings_by_standing_type( - self, season: str, standing_type: str, detailed_records: bool = False - ) -> dict: + def get_standings_by_standing_type(self, season: str, standing_type: str, detailed_records: bool = False) -> dict: """ :param detailed_records: bool, indicates whether or not to return detailed records for each team @@ -38,7 +36,5 @@ def get_standings_by_standing_type( """ query: str = f"season={season}&" detailed: str = "expand=standings.record&" if detailed_records else "" - response: dict = self._get( - resource=f"standings/{standing_type}?{query}{detailed}" - ).json() + response: dict = self._get(resource=f"standings/{standing_type}?{query}{detailed}").json() return response["records"] diff --git a/nhlpy/api/teams.py b/nhlpy/api/teams.py index 0a8eb55..0109ec0 100644 --- a/nhlpy/api/teams.py +++ b/nhlpy/api/teams.py @@ -1,3 +1,5 @@ +from typing import List + from nhlpy.api import BaseNHLAPIClient @@ -7,65 +9,61 @@ def all(self) -> dict: Returns a list of all teams. :return: dict """ - response: dict = self._get(resource="/teams").json() + response: dict = self._get(resource="teams").json() return response["teams"] def get_by_id( self, - id: int, + team_id: int, roster: bool = False, ) -> dict: """ Returns a team by id. - :param id: int, NHL team id + :param team_id: int, NHL team id :param roster: bool, Should include the roster for the team :return: dict """ query: str = "" if roster: query += "?expand=team.roster" - return self._get(resource=f"teams/{id}{query}").json()["teams"] + return self._get(resource=f"teams/{team_id}{query}").json()["teams"] - def get_team_next_game(self, id: int) -> dict: + def get_team_next_game(self, team_id: int) -> dict: """ Returns the next game for the team with the id supplied. - :param id: int, NHL team id + :param team_id: int, NHL team id :return: dict """ - return self._get(resource=f"teams/{id}?expand=team.schedule.next").json()[ - "teams" - ] + return self._get(resource=f"teams/{team_id}?expand=team.schedule.next").json()["teams"] - def get_team_previous_game(self, id: int) -> dict: + def get_team_previous_game(self, team_id: int) -> dict: """ Returns the previous game for the team with the id supplied. - :param id: int, NHL team id + :param team_id: int, NHL team id :return: dict """ - return self._get(resource=f"teams/{id}?expand=team.schedule.previous").json()[ - "teams" - ] + return self._get(resource=f"teams/{team_id}?expand=team.schedule.previous").json()["teams"] - def get_team_with_stats(self, id: int) -> dict: + def get_team_with_stats(self, team_id: int) -> dict: """ Returns the team with stats for the team with the id supplied. - :param id: int, NHL team id + :param team_id: int, NHL team id :return: dict """ - return self._get(resource=f"teams/{id}?expand=team.stats").json()["teams"] + return self._get(resource=f"teams/{team_id}?expand=team.stats").json()["teams"] - def get_team_roster(self, id: int) -> dict: + def get_team_roster(self, team_id: int) -> List[dict]: """ Returns the roster for the team with the id supplied. - :param id: int, NHL team id + :param team_id: int, NHL team id :return: dict """ - return self._get(resource=f"teams/{id}/roster").json()["roster"] + return self._get(resource=f"teams/{team_id}/roster").json()["roster"] - def get_team_stats(self, id: int) -> dict: + def get_team_stats(self, team_id: int) -> dict: """ Returns the stats for the team with the id supplied. - :param id: + :param team_id: :return: dict """ - return self._get(resource=f"teams/{id}/stats").json()["stats"] + return self._get(resource=f"teams/{team_id}/stats").json()["stats"] diff --git a/pyproject.toml b/pyproject.toml index 17b7df8..6f70549 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "nhl-api-py" -version = "0.4.12" +version = "0.4.13" description = "NHL API. For standings, team stats, outcomes, player information. Contains each individual API endpoint as well as convience methods for easy data loading in Pandas or any ML applications." authors = ["Corey Schaf "] readme = "README.md" @@ -36,4 +36,33 @@ pytest-mock = "*" mypy = "*" ruff = "*" black = "*" -ipykernel = "*" \ No newline at end of file +ipykernel = "*" + +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] +line-length = 121 + +[tool.black] +line-length = 121 \ No newline at end of file diff --git a/tests/test_core.py b/tests/test_core.py index ebe90ab..f2d9be1 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -4,9 +4,7 @@ @mock.patch("httpx.get") def test_get_configuration(h_m, nhl_client): nhl_client.core.get_configurations() - h_m.assert_called_once_with( - url="https://statsapi.web.nhl.com/api/v1/configurations" - ) + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/configurations") @mock.patch("httpx.get") @@ -18,6 +16,4 @@ def test_get_game_types(h_m, nhl_client): @mock.patch("httpx.get") def test_get_standings_types(h_m, nhl_client): nhl_client.core.get_standings_types() - h_m.assert_called_once_with( - url="https://statsapi.web.nhl.com/api/v1/standingsTypes" - ) + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/standingsTypes") diff --git a/tests/test_schedule.py b/tests/test_schedule.py index f40acb9..10f757e 100644 --- a/tests/test_schedule.py +++ b/tests/test_schedule.py @@ -1,25 +1,30 @@ from unittest import mock + @mock.patch("httpx.get") def test_get_schedule(h_m, nhl_client): nhl_client.schedule.get_schedule() h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/schedule?") + @mock.patch("httpx.get") def test_get_schedule_with_season(h_m, nhl_client): nhl_client.schedule.get_schedule(season="20222023") h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/schedule?season=20222023") + @mock.patch("httpx.get") def test_get_schedule_with_season_and_game_type(h_m, nhl_client): nhl_client.schedule.get_schedule(season="20222023", game_type="PR") h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/schedule?season=20222023&gameType=PR") + @mock.patch("httpx.get") def test_get_schedule_with_game_type(h_m, nhl_client): nhl_client.schedule.get_schedule(game_type="PR") h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/schedule?gameType=PR") + @mock.patch("httpx.get") def test_get_schedule_with_date(h_m, nhl_client): nhl_client.schedule.get_schedule(date="2022-10-01") diff --git a/tests/test_standings.py b/tests/test_standings.py index c9989b7..eca9cac 100644 --- a/tests/test_standings.py +++ b/tests/test_standings.py @@ -18,19 +18,13 @@ def test_get_standings_with_details(h_m, nhl_client): @mock.patch("httpx.get") def get_standing_types(h_m, nhl_client): nhl_client.standings.get_standing_types() - h_m.assert_called_once_with( - url="https://statsapi.web.nhl.com/api/v1/standingsTypes" - ) + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/standingsTypes") @mock.patch("httpx.get") def test_get_standings_by_type(h_m, nhl_client): - nhl_client.standings.get_standings_by_standing_type( - season="20222023", standing_type="wildCard" - ) - h_m.assert_called_once_with( - url="https://statsapi.web.nhl.com/api/v1/standings/wildCard?season=20222023&" - ) + nhl_client.standings.get_standings_by_standing_type(season="20222023", standing_type="wildCard") + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/standings/wildCard?season=20222023&") @mock.patch("httpx.get") diff --git a/tests/test_teams.py b/tests/test_teams.py new file mode 100644 index 0000000..2be8ef5 --- /dev/null +++ b/tests/test_teams.py @@ -0,0 +1,43 @@ +from unittest import mock + + +@mock.patch("httpx.get") +def test_get_teams(h_m, nhl_client): + nhl_client.teams.all() + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/teams") + + +@mock.patch("httpx.get") +def test_get_team(h_m, nhl_client): + nhl_client.teams.get_by_id(team_id=1) + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/teams/1") + + +@mock.patch("httpx.get") +def test_get_team_next_game(h_m, nhl_client): + nhl_client.teams.get_team_next_game(team_id=1) + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/teams/1?expand=team.schedule.next") + + +@mock.patch("httpx.get") +def test_get_team_previous_game(h_m, nhl_client): + nhl_client.teams.get_team_previous_game(team_id=1) + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/teams/1?expand=team.schedule.previous") + + +@mock.patch("httpx.get") +def test_get_team_with_stats(h_m, nhl_client): + nhl_client.teams.get_team_with_stats(team_id=1) + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/teams/1?expand=team.stats") + + +@mock.patch("httpx.get") +def test_get_team_roster(h_m, nhl_client): + nhl_client.teams.get_team_roster(team_id=1) + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/teams/1/roster") + + +@mock.patch("httpx.get") +def test_get_team_stats(h_m, nhl_client): + nhl_client.teams.get_team_stats(team_id=1) + h_m.assert_called_once_with(url="https://statsapi.web.nhl.com/api/v1/teams/1/stats")