Skip to content

Commit

Permalink
Feature/14 new nhl api (#15)
Browse files Browse the repository at this point in the history
* First commit on rewrite using new NHL APIs.  Basically tearing everything down.  Standings, Schedule have been updated with known routes

* Deletes old api code, tests.  Refactors tests using arg checks.  Adds the ability to follow redirects on GET requests which apparently this new API loves to do.  Updates build steps to add black and ruff

* README fixes.  Major  version bump
  • Loading branch information
coreyjs authored Nov 11, 2023
1 parent 7376f06 commit c81353e
Show file tree
Hide file tree
Showing 15 changed files with 1,452 additions and 288 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,11 @@ jobs:
# And finally run tests. I'm using pytest and all my pytest config is in my `pyproject.toml`
# so this line is super-simple. But it could be as complex as you need.
- run: poetry run pytest

# run a check for black
- name: poetry run black . --check
run: poetry run black . --check

# run a lint check with ruff
- name: poetry run ruff .
run: poetry run ruff .
65 changes: 26 additions & 39 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@

# NHL-API-PY

# This is now broked due to the NHL updating their API. Work is in progress to update this lib.

## This is updated with the new, also undocumented, NHL API. More endpoints will be flushed out and completed as they
are discovered. If you find any, please submit a PR.


NHL-api-py is a Python package that provides a simple wrapper around the
NHL API, allowing you to easily access and retrieve NHL data in your Python
Expand Down Expand Up @@ -34,60 +37,44 @@ client = NHLClient()
```

Available methods:

## Schedule

```python
client.teams.all()
client.teams.get_by_id(id=1, roster=False)
client.teams.get_team_next_game(id=1)
client.teams.get_team_previous_game(id=1)
client.teams.get_team_stats(id=1)
client.schedule.get_schedule() # returns today's games
client.schedule.get_schedule(date="2023-11-10") # returns games for supplied date
client.schedule.get_schedule_by_team_by_month(team_abbr="BUF", month="2023-11") # returns games for supplied team and month
client.schedule.get_schedule_by_team_by_week(team_abbr="BUF") # returns games for supplied team for current week
client.schedule.get_season_schedule(team_abbr="BUF", season="20222023") # returns games for supplied team for supplied season
```

# Standings
client.standings.get_standings(season="20222023", detailed_record=False)
client.standings.get_standing_types()

# Player Stats
client.players.get_player_stats(person_id=8477949, season="20222023", stat_type="statsSingleSeason")
client.players.get_player_stats(person_id=8477949, season="20222023", stat_type="goalsByGameSituation")
client.players.get_player_stats(person_id=8477949, season="20222023", stat_type="yearByYear")
## Standings
```python

# Schedule
client.schedule.get_schedule(season="20222023")

# Get Todays Games
client.schedule.get_schedule(season="20222023")
client.schedule.get_schedule(date="2021-10-01")
client.schedule.get_schedule(season="20222023", team_id=7)
client.standings.get_standings(date="2023-11-10") # returns standings for supplied date
client.standings.get_standings(season="20222023") # returns standings for supplied season
client.standings.season_standing_manifest() # returns information about every season, start date, end date, etc.

# Games
client.games.get_game_types()
client.games.get_game_play_types()
client.games.get_game_status_codes()
client.games.get_game_live_feed(game_id=2020020001)
client.games.get_game_live_feed_diff_after_timestamp(game_id=2020020001, timestamp=1633070400)
client.games.get_game_boxscore(game_id=2020020001)
client.games.get_game_linescore(game_id=2020020001)
client.games.get_game_content(game_id=2020020001)
```

# Players
client.players.get_player(person_id=8477949)
client.players.get_player_stats(person_id=8477949, season="20222023", stat_type="statsSingleSeason")
client.players.get_player_stat_types()

# Helpers - Common use cases, data extraction, etc. For easier dataframe initialization.
# These return data that has been parsed
# out, with some additional calculations as well.
standings_list = nhl_client.helpers.league_standings(season="20222023")
standings_df = pd.DataFrame(standings_list)
standings_df.head(20)
## Teams
```python

game_results = nhl_client.helpers.get_all_game_results(season="20222023", detailed_game_data=True, game_type="R", team_ids=[7])
client.teams.team_stats_summary()
client.teams.team_stats_summary(lang='fr')

```




- - -

# Below is out of date, will be updated soon.

As mentioned at the top, I created a notebook to go over some of the available methods in more detail. Below is an export md of that notebook, with out cell executions.

```python
Expand Down
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.13"
__version__ = "2.0.1"
12 changes: 9 additions & 3 deletions nhlpy/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,15 @@

class BaseNHLAPIClient:
def __init__(self) -> None:
self.base_url = "https://statsapi.web.nhl.com"
self.api_ver = "/api/v1/"
self.base_url = "https://api-web.nhle.com"
self.api_ver = "/v1/"

def _get(self, resource: str) -> httpx.request:
r: httpx.request = httpx.get(url=f"{self.base_url}{self.api_ver}{resource}")
"""
Private method to make a get request to the NHL API. This wraps the lib httpx functionality.
:param resource:
:return:
"""
print(f"{self.base_url}{self.api_ver}{resource}")
r: httpx.request = httpx.get(url=f"{self.base_url}{self.api_ver}{resource}", follow_redirects=True)
return r
63 changes: 33 additions & 30 deletions nhlpy/api/schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,43 @@


class Schedule(BaseNHLAPIClient):
def get_schedule(
self,
season: Optional[str] = None,
game_type: str = None,
date: str = None,
team_ids: Optional[List[int]] = None,
) -> dict:
def get_schedule(self, date: Optional[str] = None) -> dict:
"""
:param date: Exact date in formate of YYYY-MM-DD
:param season: str - Season in format of 20202021
:param game_type: str - Game type, R (default) for regular season, P for playoffs,
PR for preseason, A for all-star. This can also be a comma separated list of game types such as "R,P,PR".
:param team_ids: List[int] - List of team ids
example:
c.schedule.get_schedule(season="20222023", team_ids=[7], game_type='PR')
c.schedule.get_schedule()
Get the schedule for the NHL for the given date. If no date is supplied it will
default to today.
:param date: In format YYYY-MM-DD. If no date is supplied, it will default to "Today". Which in case
of the NHL could be today or yesterday depending on how early you call it.
:return: dict
"""
query_p = []

if season:
query_p.append(f"season={season}")
res = date if date else "now"

if team_ids:
query_p.append(f"teamId={','.join(str(t) for t in team_ids)}")
return self._get(resource=f"schedule/{res}").json()

if game_type:
query_p.append(f"gameType={game_type}")
def get_schedule_by_team_by_month(self, team_abbr: str, month: Optional[str] = None) -> List[dict]:
"""
Get the schedule for the team (team_abbr) for the given month. If no month is supplied it will
:param team_abbr: The 3 letter abbreviation of the team. BUF, TOR, etc
:param month: In format YYYY-MM. 2021-10, 2021-11, etc. Defaults to "now" otherwise.
:return:
"""
resource = f"club-schedule/{team_abbr}/month/{month if month else 'now'}"
return self._get(resource=resource).json()["games"]

if date:
query_p.append(f"date={date}")
def get_schedule_by_team_by_week(self, team_abbr: str) -> List[dict]:
"""
This returns the schedule for the team (team_abbr) for the current week.
:param team_abbr: The 3 letter abbreviation of the team. BUF, TOR, etc
:return:
"""
resource = f"club-schedule/{team_abbr}/week/now"
return self._get(resource=resource).json()["games"]

return self._get(resource=f"schedule?{ '&'.join(query_p) }").json()
def get_season_schedule(self, team_abbr: str, season: str) -> dict:
"""
This returns the schedule for the team (team_abbr) for the current season. This also
contains all the metadata from the base api request.
:param team_abbr: Team abbreviation. BUF, TOR, etc
:param season: Season in format YYYYYYYY. 20202021, 20212022, etc
:return:
"""
return self._get(resource=f"club-schedule-season/{team_abbr}/{season}").json()
72 changes: 46 additions & 26 deletions nhlpy/api/standings.py
Original file line number Diff line number Diff line change
@@ -1,40 +1,60 @@
from typing import List, Optional

from nhlpy.api import BaseNHLAPIClient


class Standings(BaseNHLAPIClient):
def get_standing_types(self) -> dict:
"""
Returns a list of standing types that can be used in get_standings_by_standing_type()
:return: dict of standing types
"""
return self._get(resource="standingsTypes").json()

def get_standings(self, season: str, detailed_record: bool = False) -> dict:
def get_standings(self, date: Optional[str] = None, season: Optional[str] = None, cache=True) -> dict:
"""
Gets the standings for the season supplied via season: param.
:param season:
:param detailed_record: Detailed information for each team including
home and away records, record in shootouts, last ten games, and split
head-to-head records against divisions and conferences.
:param date: str, Date in format YYYY-MM-DD. If no date is supplied, it will default to "Today".
:param season: The season to return the final standings from. This takes precedence over date.
:param cache: bool, Load from hard file of data instead of making api call. Possible the cache gets out
of date if I dont update this yearly.
:return: dict
"""
modifier: str = f"season={season}"
detailed: str = "&expand=standings.record&" if detailed_record else ""

response: dict = self._get(resource=f"standings?{modifier}{detailed}").json()
return response["records"]
# We need to look up the last date of the season and use that as the date, since it doesnt seem to take
# season as a param.
if season:
if cache:
# load json from data/seasonal_information_manifest.json
import json
import os

with open(os.path.join(os.getcwd(), "nhlpy/data/seasonal_information_manifest.json"), "r") as f:
seasons = json.load(f)["seasons"]
else:
seasons = self.season_standing_manifest()

def get_standings_by_standing_type(self, season: str, standing_type: str, detailed_records: bool = False) -> dict:
season_data = next((s for s in seasons if s["id"] == int(season)), None)
if not season_data:
raise ValueError(f"Invalid Season Id {season}")
date = season_data["standingsEnd"]

res = date if date else "now"

return self._get(resource=f"standings/{res}").json()

def season_standing_manifest(self) -> List[dict]:
"""
Returns information about what seems like every season. Start date, end date, etc.
:example
[{
"id": 20232024,
"conferencesInUse": true,
"divisionsInUse": true,
"pointForOTlossInUse": true,
"regulationWinsInUse": true,
"rowInUse": true,
"standingsEnd": "2023-11-10",
"standingsStart": "2023-10-10",
"tiesInUse": false,
"wildcardInUse": true
}]
:param detailed_records: bool, indicates whether or not to return detailed records for each team
:param season: str, Season in the format of 20202021
:param standing_type: str, full list found in get_standing_types() with the following options:
regularSeason, wildCard,divisionLeaders, wildCardWithLeaders, preseason,
postseason, byDivision, byConference, byLeague
:return: dict
"""
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()
return response["records"]

return self._get(resource="standings-season").json()["seasons"]
67 changes: 8 additions & 59 deletions nhlpy/api/teams.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,66 +4,15 @@


class Teams(BaseNHLAPIClient):
def all(self) -> dict:
"""
Returns a list of all teams.
:return: dict
"""
response: dict = self._get(resource="teams").json()
return response["teams"]

def get_by_id(
self,
team_id: int,
roster: bool = False,
) -> dict:
"""
Returns a team by 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/{team_id}{query}").json()["teams"]

def get_team_next_game(self, team_id: int) -> dict:
"""
Returns the next game for the team with the id supplied.
:param team_id: int, NHL team id
:return: dict
"""
return self._get(resource=f"teams/{team_id}?expand=team.schedule.next").json()["teams"]
def __init__(self):
super().__init__()
self.base_url = "https://api.nhle.com"
self.api_ver = "/stats/rest/"

def get_team_previous_game(self, team_id: int) -> dict:
def team_stats_summary(self, lang="en") -> List[dict]:
"""
Returns the previous game for the team with the id supplied.
:param team_id: int, NHL team id
:return: dict
"""
return self._get(resource=f"teams/{team_id}?expand=team.schedule.previous").json()["teams"]

def get_team_with_stats(self, team_id: int) -> dict:
"""
Returns the team with stats for the team with the id supplied.
:param team_id: int, NHL team id
:return: dict
"""
return self._get(resource=f"teams/{team_id}?expand=team.stats").json()["teams"]
def get_team_roster(self, team_id: int) -> List[dict]:
"""
Returns the roster for the team with the id supplied.
:param team_id: int, NHL team id
:return: dict
"""
return self._get(resource=f"teams/{team_id}/roster").json()["roster"]

def get_team_stats(self, team_id: int) -> dict:
"""
Returns the stats for the team with the id supplied.
:param team_id:
:return: dict
:param lang: Language param. 'en' for English, 'fr' for French
:return:
"""
return self._get(resource=f"teams/{team_id}/stats").json()["stats"]
return self._get(resource=f"{lang}/team/summary").json()["data"]
Loading

0 comments on commit c81353e

Please sign in to comment.