Skip to content

Commit

Permalink
Formatting updates. Adds ruff configuration, enforces line length. Fi…
Browse files Browse the repository at this point in the history
…xes type hints on teams endpoints. In teams it also changes id to team_id for each method. closes #12 (#13)
coreyjs authored Nov 9, 2023
1 parent 9cc1779 commit 9d68eaf
Showing 12 changed files with 144 additions and 124 deletions.
2 changes: 1 addition & 1 deletion nhlpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .nhl_client import NHLClient
from .nhl_client import NHLClient # noqa: F401
2 changes: 1 addition & 1 deletion nhlpy/_version.py
Original file line number Diff line number Diff line change
@@ -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"
12 changes: 4 additions & 8 deletions nhlpy/api/games.py
Original file line number Diff line number Diff line change
@@ -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]:
91 changes: 29 additions & 62 deletions nhlpy/api/helpers.py
Original file line number Diff line number Diff line change
@@ -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,19 +74,15 @@ 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:
:param goals_against:
: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"],
8 changes: 2 additions & 6 deletions nhlpy/api/players.py
Original file line number Diff line number Diff line change
@@ -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:
"""
8 changes: 2 additions & 6 deletions nhlpy/api/standings.py
Original file line number Diff line number Diff line change
@@ -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"]
44 changes: 21 additions & 23 deletions nhlpy/api/teams.py
Original file line number Diff line number Diff line change
@@ -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"]
33 changes: 31 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>"]
readme = "README.md"
@@ -36,4 +36,33 @@ pytest-mock = "*"
mypy = "*"
ruff = "*"
black = "*"
ipykernel = "*"
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
8 changes: 2 additions & 6 deletions tests/test_core.py
Original file line number Diff line number Diff line change
@@ -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")
5 changes: 5 additions & 0 deletions tests/test_schedule.py
Original file line number Diff line number Diff line change
@@ -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")
12 changes: 3 additions & 9 deletions tests/test_standings.py
Original file line number Diff line number Diff line change
@@ -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")
43 changes: 43 additions & 0 deletions tests/test_teams.py
Original file line number Diff line number Diff line change
@@ -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")

0 comments on commit 9d68eaf

Please sign in to comment.