Skip to content

Commit

Permalink
Added Support for Unofficial Endpoints (#12)
Browse files Browse the repository at this point in the history
### Added

- API Clients for unofficial endpoints
- Examples for new API Clients
- Enum values

### Fixed

- Missing imports
  • Loading branch information
joeyagreco authored Sep 16, 2022
1 parent 6c0b5a2 commit 56c3110
Show file tree
Hide file tree
Showing 45 changed files with 1,565 additions and 38 deletions.
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,15 @@ All notable changes to this project will be documented in this file.

## [Unreleased]

- N/A
### Added

- API Clients for unofficial endpoints
- Examples for new API Clients
- Enum values

### Fixed

- Missing imports

## [1.2.0]

Expand Down
58 changes: 58 additions & 0 deletions example/unofficial/player_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from sleeper.api.unofficial import UPlayerAPIClient
from sleeper.enum import Sport
from sleeper.enum.nfl import NFLPosition
from sleeper.model import PlayerStats

if __name__ == "__main__":
# get player stats for a player for a particular sport, week, and season
player_stats_week: PlayerStats = UPlayerAPIClient.get_player_stats(sport=Sport.NFL,
player_id="1234",
season="2020",
week=1)

# get player stats for a player for the entire season in a particular sport and season
player_stats_season: PlayerStats = UPlayerAPIClient.get_player_stats(sport=Sport.NFL,
player_id="1234",
season="2020")

# get player projections for a player for a particular sport, week, and season
player_projections_week: PlayerStats = UPlayerAPIClient.get_player_projections(sport=Sport.NFL,
player_id="1234",
season="2020",
week=1)

# get player projections for a player for the entire season in a particular sport and season
player_projections_season: PlayerStats = UPlayerAPIClient.get_player_projections(sport=Sport.NFL,
player_id="1234",
season="2020")

# get all player stats for a particular sport, season, and week
all_player_stats: list[PlayerStats] = UPlayerAPIClient.get_all_player_stats(sport=Sport.NFL,
season="2020",
week=1)

# get all player stats for QBs and RBs for a particular sport, season, and week
all_player_stats_qbs_rbs: list[PlayerStats] = UPlayerAPIClient.get_all_player_stats(sport=Sport.NFL,
season="2020",
week=1,
positions=[NFLPosition.QB,
NFLPosition.RB])

# get all player projections for a particular sport, season, and week
all_player_projections: list[PlayerStats] = UPlayerAPIClient.get_all_player_projections(sport=Sport.NFL,
season="2020",
week=1)

# get all player projections for QBs and RBs for a particular sport, season, and week
all_player_projections_qbs_rbs: list[PlayerStats] = UPlayerAPIClient.get_all_player_projections(sport=Sport.NFL,
season="2020",
week=1,
positions=[
NFLPosition.QB,
NFLPosition.RB])

# get a player's headshot and save locally
# the file path should save to a file that has the extension '.png'
UPlayerAPIClient.get_player_head_shot(sport=Sport.NFL,
player_id="1234",
save_to_path="C:\\Desktop\\avatar\\my_headshot.png")
7 changes: 7 additions & 0 deletions example/unofficial/sport_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
from sleeper.api.unofficial import USportAPIClient
from sleeper.enum import Sport
from sleeper.model import Game

if __name__ == "__main__":
# get regular season schedule for a particular sport and season
regular_season: list[Game] = USportAPIClient.get_regular_season_schedule(sport=Sport.NFL, season="2020")
8 changes: 8 additions & 0 deletions example/unofficial/team_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from sleeper.api.unofficial import UTeamAPIClient
from sleeper.enum import Sport
from sleeper.enum.nfl import NFLTeam
from sleeper.model import DepthChart

if __name__ == "__main__":
# get depth chart for a particular sport and team
depth_chart: DepthChart = UTeamAPIClient.get_team_depth_chart(sport=Sport.NFL, team=NFLTeam.GB)
13 changes: 10 additions & 3 deletions sleeper/api/SleeperAPIClient.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,22 @@ class SleeperAPIClient(ABC):

# ROUTES
_AVATARS_ROUTE = ConfigReader.get("api", "avatars_route")
_CONTENT_ROUTE = ConfigReader.get("api", "content_route")
_DEPTH_CHART_ROUTE = ConfigReader.get("api", "depth_chart_route")
_DRAFT_ROUTE = ConfigReader.get("api", "draft_route")
_DRAFTS_ROUTE = ConfigReader.get("api", "drafts_route")
_LEAGUE_ROUTE = ConfigReader.get("api", "league_route")
_LEAGUES_ROUTE = ConfigReader.get("api", "leagues_route")
_LOSERS_BRACKET_ROUTE = ConfigReader.get("api", "losers_bracket_route")
_MATCHUPS_ROUTE = ConfigReader.get("api", "matchups_route")
_PICKS_ROUTE = ConfigReader.get("api", "picks_route")
_PLAYER_ROUTE = ConfigReader.get("api", "player_route")
_PLAYERS_ROUTE = ConfigReader.get("api", "players_route")
_PROJECTIONS_ROUTE = ConfigReader.get("api", "projections_route")
_ROSTERS_ROUTE = ConfigReader.get("api", "rosters_route")
_SCHEDULE_ROUTE = ConfigReader.get("api", "schedule_route")
_STATE_ROUTE = ConfigReader.get("api", "state_route")
_STATS_ROUTE = ConfigReader.get("api", "stats_route")
_THUMBS_ROUTE = ConfigReader.get("api", "thumbs_route")
_TRADED_PICKS_ROUTE = ConfigReader.get("api", "traded_picks_route")
_TRANSACTIONS_ROUTE = ConfigReader.get("api", "transactions_route")
Expand All @@ -57,11 +63,12 @@ def _add_filters(cls, url: str, *args) -> str:
for i, arg in enumerate(args):
if i > 0:
symbol = "&"
url = f"{url}{symbol}{arg[0]}={arg[1]}"
if arg[0] is not None and arg[1] is not None:
url = f"{url}{symbol}{arg[0]}={arg[1]}"
return url

@staticmethod
def _get(url: str) -> Optional[dict]:
def _get(url: str) -> Optional[dict | list]:
response = requests.get(url)
response.raise_for_status()
return response.json()
Expand All @@ -72,6 +79,6 @@ def _get_image_file(url: str) -> Image:
response.raise_for_status()
image_bytes = response.content
if image_bytes is None:
raise SleeperAPIException(f"No avatar found.")
raise SleeperAPIException(f"No image found.")
image_stream = io.BytesIO(image_bytes)
return Image.open(image_stream)
108 changes: 108 additions & 0 deletions sleeper/api/unofficial/UPlayerAPIClient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
from typing import Any

from sleeper.api.SleeperAPIClient import SleeperAPIClient
from sleeper.enum import SeasonType, PlayerPosition
from sleeper.enum.Sport import Sport
from sleeper.model.PlayerStats import PlayerStats


class UPlayerAPIClient(SleeperAPIClient):

@classmethod
def get_player_stats(cls, *, sport: Sport, player_id: str, season: str, **kwargs) -> PlayerStats:
"""
Gets player stats for the given season OR just the given week.
"""
season_type: SeasonType = kwargs.pop("season_type", SeasonType.REGULAR)
week: int = kwargs.pop("week", None)

url = cls._build_route(cls._SLEEPER_APP_BASE_URL, None, cls._STATS_ROUTE, sport.name.lower(), cls._PLAYER_ROUTE,
player_id)
url = cls._add_filters(url, ("season_type", season_type.name.lower()), ("season", season), ("week", week))

response_dict = cls._get(url)
if response_dict is None:
error_message = f"Could not get PlayerStats for sport: '{sport.name}', player_id: '{player_id}', season_type: '{season_type}', season: '{season}'"
if week is not None:
error_message += f", week: '{week}'"
error_message += "."
raise ValueError(error_message)
return PlayerStats.from_dict(response_dict)

@classmethod
def get_player_projections(cls, *, sport: Sport, player_id: str, season: str, **kwargs) -> PlayerStats:
"""
Gets player projections for the given season OR just the given week.
"""
season_type: SeasonType = kwargs.pop("season_type", SeasonType.REGULAR)
week: int = kwargs.pop("week", None)

url = cls._build_route(cls._SLEEPER_APP_BASE_URL, None, cls._PROJECTIONS_ROUTE, sport.name.lower(),
cls._PLAYER_ROUTE, player_id)
url = cls._add_filters(url, ("season_type", season_type.name.lower()), ("season", season), ("week", week))

response_dict = cls._get(url)
if response_dict is None:
error_message = f"Could not get PlayerStats for sport: '{sport.name}', player_id: '{player_id}', season_type: '{season_type}', season: '{season}'"
if week is not None:
error_message += f", week: '{week}'"
error_message += "."
raise ValueError(error_message)
return PlayerStats.from_dict(response_dict)

@classmethod
def get_all_player_stats(cls, *, sport: Sport, season: str, week: int, **kwargs) -> list[PlayerStats]:
season_type: SeasonType = kwargs.pop("season_type", SeasonType.REGULAR)
positions: list[PlayerPosition] = kwargs.pop("positions", list())

url = cls._build_route(cls._SLEEPER_APP_BASE_URL, None, cls._STATS_ROUTE, sport.name.lower(), season, week)
filters: list[tuple[str, Any]] = [("season_type", season_type.name.lower())]
for position in positions:
filters.append(("position[]", position.name.upper()))
url = cls._add_filters(url, *filters)

response_list = cls._get(url)
if response_list is None:
error_message = f"Could not get PlayerStats list for sport: '{sport.name}', season_type: '{season_type}', season: '{season}', week: '{week}'"
if week is not None:
error_message += f", week: '{week}'"
if len(positions) > 0:
error_message += f", positions: '{positions}'"
error_message += "."
raise ValueError(error_message)
return PlayerStats.from_dict_list(response_list)

@classmethod
def get_all_player_projections(cls, *, sport: Sport, season: str, week: int, **kwargs) -> list[PlayerStats]:
season_type: SeasonType = kwargs.pop("season_type", SeasonType.REGULAR)
positions: list[PlayerPosition] = kwargs.pop("positions", list())

url = cls._build_route(cls._SLEEPER_APP_BASE_URL, None, cls._PROJECTIONS_ROUTE, sport.name.lower(), season,
week)
filters: list[tuple[str, Any]] = [("season_type", season_type.name.lower())]
for position in positions:
filters.append(("position[]", position.name.upper()))
url = cls._add_filters(url, *filters)

response_list = cls._get(url)
if response_list is None:
error_message = f"Could not get PlayerStats list for sport: '{sport.name}', season_type: '{season_type}', season: '{season}', week: '{week}'"
if week is not None:
error_message += f", week: '{week}'"
if len(positions) > 0:
error_message += f", positions: '{positions}'"
error_message += "."
raise ValueError(error_message)
return PlayerStats.from_dict_list(response_list)

@classmethod
def get_player_head_shot(cls, *, sport: Sport, player_id: str, save_to_path: str) -> None:
"""
save_to_path should end in ".png".
"""

url = cls._build_route(cls._SLEEPER_CDN_BASE_URL, None, cls._CONTENT_ROUTE, sport.name.lower(),
cls._PLAYERS_ROUTE, player_id)
url += ".jpg"
image_file = cls._get_image_file(url)
image_file.save(save_to_path)
19 changes: 19 additions & 0 deletions sleeper/api/unofficial/USportAPIClient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from sleeper.api.SleeperAPIClient import SleeperAPIClient
from sleeper.enum import SeasonType
from sleeper.enum.Sport import Sport
from sleeper.model.Game import Game


class USportAPIClient(SleeperAPIClient):

@classmethod
def get_regular_season_schedule(cls, *, sport: Sport, season: str, **kwargs) -> list[Game]:
season_type: SeasonType = kwargs.pop("season_type", SeasonType.REGULAR)
url = cls._build_route(cls._SLEEPER_APP_BASE_URL, None, cls._SCHEDULE_ROUTE, sport.name.lower(),
season_type.name.lower(), season)

response_list = cls._get(url)
if response_list is None:
raise ValueError(
f"Could not get Game list for sport: '{sport.name}', season: '{season}', season_type: '{season_type.name}'.")
return Game.from_dict_list(response_list, sport)
17 changes: 17 additions & 0 deletions sleeper/api/unofficial/UTeamAPIClient.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from sleeper.api.SleeperAPIClient import SleeperAPIClient
from sleeper.enum import SportTeam
from sleeper.enum.Sport import Sport
from sleeper.model.DepthChart import DepthChart


class UTeamAPIClient(SleeperAPIClient):

@classmethod
def get_team_depth_chart(cls, *, sport: Sport, team: SportTeam) -> DepthChart:
url = cls._build_route(cls._SLEEPER_APP_BASE_URL, None, cls._PLAYERS_ROUTE, sport.name.lower(),
team.name.lower(), cls._DEPTH_CHART_ROUTE)

response_dict = cls._get(url)
if response_dict is None:
raise ValueError(f"Could not get DepthChart for sport: '{sport.name}', team: '{team.name}'")
return DepthChart.model(sport).from_dict(response_dict)
3 changes: 3 additions & 0 deletions sleeper/api/unofficial/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .UPlayerAPIClient import UPlayerAPIClient
from .USportAPIClient import USportAPIClient
from .UTeamAPIClient import UTeamAPIClient
6 changes: 6 additions & 0 deletions sleeper/app.properties
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,22 @@ sleeper_cdn_base_url=https://sleepercdn.com
version=v1
# ROUTES
avatars_route=/avatars
content_route=/content
depth_chart_route=/depth_chart
draft_route=/draft
drafts_route=/drafts
league_route=/league
leagues_route=/leagues
losers_bracket_route=/losers_bracket
matchups_route=/matchups
picks_route=/picks
player_route=/player
players_route=/players
projections_route=/projections
rosters_route=/rosters
schedule_route=/schedule
state_route=/state
stats_route=/stats
thumbs_route=/thumbs
traded_picks_route=/traded_picks
transactions_route=/transactions
Expand Down
20 changes: 20 additions & 0 deletions sleeper/enum/Category.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

from enum import unique

from sleeper.enum.ModelEnum import ModelEnum


@unique
class Category(ModelEnum):
PROJ = "PROJ"
STAT = "STAT"

@classmethod
def from_str(cls, s: str) -> Category:
if s.upper() == "PROJ":
return Category.PROJ
elif s.upper() == "STAT":
return Category.STAT
else:
cls._handle_unknown_value(Category, s)
20 changes: 20 additions & 0 deletions sleeper/enum/Company.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
from __future__ import annotations

from enum import unique

from sleeper.enum.ModelEnum import ModelEnum


@unique
class Company(ModelEnum):
ROTOWIRE = "ROTOWIRE"
SPORTRADAR = "SPORTRADAR"

@classmethod
def from_str(cls, s: str) -> Company:
if s.upper() == "ROTOWIRE":
return Company.ROTOWIRE
elif s.upper() == "SPORTRADAR":
return Company.SPORTRADAR
else:
cls._handle_unknown_value(Company, s)
2 changes: 1 addition & 1 deletion sleeper/enum/PlayerPosition.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ def enum(sport: Sport) -> PlayerPosition:
if sport == Sport.NFL:
return NFLPosition
else:
raise ValueError(f"Cannot find PlayerPosition for sport: 'sport'.")
raise ValueError(f"Cannot find PlayerPosition for sport: '{sport.name}'.")
2 changes: 1 addition & 1 deletion sleeper/enum/PlayerStatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ def enum(sport: Sport) -> PlayerStatus:
if sport == Sport.NFL:
return NFLPlayerStatus
else:
raise ValueError(f"Cannot find PlayerStatus for sport: 'sport'.")
raise ValueError(f"Cannot find PlayerStatus for sport: '{sport.name}'.")
2 changes: 1 addition & 1 deletion sleeper/enum/RosterPosition.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ def enum(sport: Sport) -> RosterPosition:
if sport == Sport.NFL:
return NFLRosterPosition
else:
raise ValueError(f"Cannot find RosterPosition for sport: 'sport'.")
raise ValueError(f"Cannot find RosterPosition for sport: '{sport.name}'.")
3 changes: 3 additions & 0 deletions sleeper/enum/SeasonStatus.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ class SeasonStatus(ModelEnum):
COMPLETE = "COMPLETE"
DRAFTING = "DRAFTING"
IN_SEASON = "IN_SEASON"
POSTPONED = "POSTPONED"
PRE_DRAFT = "PRE_DRAFT"

@classmethod
Expand All @@ -20,6 +21,8 @@ def from_str(cls, s: str) -> SeasonStatus:
return SeasonStatus.DRAFTING
elif s.upper() == "IN_SEASON":
return SeasonStatus.IN_SEASON
elif s.upper() == "POSTPONED":
return SeasonStatus.POSTPONED
elif s.upper() == "PRE_DRAFT":
return SeasonStatus.PRE_DRAFT
else:
Expand Down
2 changes: 1 addition & 1 deletion sleeper/enum/SportTeam.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ def enum(sport: Sport) -> SportTeam:
if sport == Sport.NFL:
return NFLTeam
else:
raise ValueError(f"Cannot find SportTeam for sport: 'sport'.")
raise ValueError(f"Cannot find SportTeam for sport: '{sport.name}'.")
Loading

0 comments on commit 56c3110

Please sign in to comment.