From 206abb73098c6bb72a062daedbe2e19534be0b4d Mon Sep 17 00:00:00 2001 From: Jakub Kuczys Date: Thu, 31 Oct 2024 05:04:00 +0100 Subject: [PATCH] feat: add skill and stat leaderboard support --- docs/api.rst | 15 ++++ rlapi/__init__.py | 18 +++- rlapi/client.py | 57 ++++++++++++- rlapi/enums.py | 17 ++++ rlapi/leaderboard.py | 194 +++++++++++++++++++++++++++++++++++++++++++ rlapi/player.py | 13 +-- 6 files changed, 307 insertions(+), 7 deletions(-) create mode 100644 rlapi/leaderboard.py diff --git a/docs/api.rst b/docs/api.rst index c75525d..ad1ec64 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -88,6 +88,9 @@ All enumerations are subclasses of `enum.Enum`. .. autoclass:: rlapi.Platform :members: +.. autoclass:: rlapi.Stat + :members: + Rocket League API Models ------------------------ @@ -106,6 +109,18 @@ and are not meant to be created by the user of the library. .. autoclass:: rlapi.Playlist :members: +.. autoclass:: rlapi.SkillLeaderboard + :members: + +.. autoclass:: rlapi.SkillLeaderboardPlayer + :members: + +.. autoclass:: rlapi.StatLeaderboard + :members: + +.. autoclass:: rlapi.StatLeaderboardPlayer + :members: + .. autoclass:: rlapi.Population :members: diff --git a/rlapi/__init__.py b/rlapi/__init__.py index b23e1b6..83c816a 100644 --- a/rlapi/__init__.py +++ b/rlapi/__init__.py @@ -22,7 +22,11 @@ from . import errors as errors # noqa from .client import Client as Client # noqa -from .enums import Platform as Platform, PlaylistKey as PlaylistKey # noqa +from .enums import ( # noqa + Platform as Platform, + PlaylistKey as PlaylistKey, + Stat as Stat, +) from .errors import ( # noqa HTTPException as HTTPException, IllegalUsername as IllegalUsername, @@ -30,6 +34,12 @@ RLApiException as RLApiException, Unauthorized as Unauthorized, ) +from .leaderboard import ( # noqa + SkillLeaderboard as SkillLeaderboard, + SkillLeaderboardPlayer as SkillLeaderboardPlayer, + StatLeaderboard as StatLeaderboard, + StatLeaderboardPlayer as StatLeaderboardPlayer, +) from .player import ( # noqa DIVISIONS as DIVISIONS, PLAYLISTS_WITH_SEASON_REWARDS as PLAYLISTS_WITH_SEASON_REWARDS, @@ -64,12 +74,18 @@ # enums "Platform", "PlaylistKey", + "Stat", # errors "HTTPException", "IllegalUsername", "PlayerNotFound", "RLApiException", "Unauthorized", + # leaderboard + "SkillLeaderboard", + "SkillLeaderboardPlayer", + "StatLeaderboard", + "StatLeaderboardPlayer", # player "DIVISIONS", "PLAYLISTS_WITH_SEASON_REWARDS", diff --git a/rlapi/client.py b/rlapi/client.py index 2176ef1..7d73f65 100644 --- a/rlapi/client.py +++ b/rlapi/client.py @@ -37,7 +37,8 @@ from . import errors from ._utils import TokenInfo, json_or_text -from .enums import Platform +from .enums import Platform, PlaylistKey, Stat +from .leaderboard import SkillLeaderboard, StatLeaderboard from .player import Player from .population import Population from .typedefs import TierBreakdownType @@ -616,3 +617,57 @@ async def get_population(self) -> Population: """ data = await self._rlapi_request("/population") return Population(data) + + async def get_skill_leaderboard( + self, platform: Platform, playlist_key: PlaylistKey + ) -> SkillLeaderboard: + """ + Get skill leaderboard for the playlist on the given platform. + + Parameters + ---------- + platform: Platform + Platform to get the leaderboard for. + playlist_key: PlaylistKey + Playlist to get the leaderboard for. + + Returns + ------- + SkillLeaderboard + Skill leaderboard for the playlist on the given platform. + + Raises + ------ + HTTPException + HTTP request to Rocket League failed. + """ + endpoint = f"/leaderboard/skill/{platform.value}/{playlist_key.value}" + data = await self._rlapi_request(endpoint) + return SkillLeaderboard(platform, playlist_key, data) + + async def get_stat_leaderboard( + self, platform: Platform, stat: Stat + ) -> StatLeaderboard: + """ + Get leaderboard for the specified stat on the given platform. + + Parameters + ---------- + platform: Platform + Platform to get the leaderboard for. + stat: Stat + Stat to get the leaderboard for. + + Returns + ------- + StatLeaderboard + Leaderboard for the specified stat on the given platform. + + Raises + ------ + HTTPException + HTTP request to Rocket League failed. + """ + endpoint = f"/leaderboard/stat/{platform.value}/{stat.value}" + data = await self._rlapi_request(endpoint) + return StatLeaderboard(platform, stat, data) diff --git a/rlapi/enums.py b/rlapi/enums.py index 8419565..7ec3d5e 100644 --- a/rlapi/enums.py +++ b/rlapi/enums.py @@ -75,6 +75,23 @@ def __str__(self) -> str: return _PLATFORM_FRIENDLY_NAMES[self] +class Stat(Enum): + """Represents player stat.""" + + #: Assists. + assists = "Assists" + #: Goals. + goals = "Goals" + #: MVPs. + mvps = "MVPs" + #: Saves. + saves = "Saves" + #: Shots. + shots = "Shots" + #: Wins. + wins = "Wins" + + _PLATFORM_FRIENDLY_NAMES = { Platform.steam: "Steam", Platform.ps4: "PlayStation", diff --git a/rlapi/leaderboard.py b/rlapi/leaderboard.py new file mode 100644 index 0000000..e0342f6 --- /dev/null +++ b/rlapi/leaderboard.py @@ -0,0 +1,194 @@ +from typing import Any, Dict, Optional + +from .enums import Platform, PlaylistKey, Stat + +__all__ = ( + "SkillLeaderboardPlayer", + "SkillLeaderboard", + "StatLeaderboardPlayer", + "StatLeaderboard", +) + + +class SkillLeaderboardPlayer: + """SkillLeaderboardPlayer() + Represents Rocket League Player on a platform's skill leaderboard. + + Attributes + ---------- + platform: Platform + Platform that this leaderboard entry refers to. + playlist_key: PlaylistKey + Playlist that this leaderboard entry refers to. + user_id: str, optional + Player's user ID. + Only present for Steam and Epic Games players. + user_name: str + Player's username (display name). + tier: int + Player's tier on the specified playlist. + skill: int + Player's skill rating on the specified playlist. + + """ + + __slots__ = ("platform", "playlist_key", "user_name", "user_id", "tier", "skill") + + def __init__( + self, + platform: Platform, + playlist_key: PlaylistKey, + data: Dict[str, Any], + ) -> None: + self.platform = platform + self.playlist_key = playlist_key + self.user_name: str = data["user_name"] + self.user_id: Optional[str] = data.get("user_id") + if ( + self.user_id is not None + and self.user_id.startswith(f"{platform.value}|") + and self.user_id.endswith("|0") + ): + self.user_id = self.user_id[len(platform.value) + 1 : -2] + self.tier: int = data["tier"] + self.skill: int = data["skill"] + + def __repr__(self) -> str: + platform_repr = f"{self.platform.__class__.__name__}.{self.platform._name_}" + return ( + f"<{self.__class__.__name__}" + f" {self.playlist_key}" + f" platform={platform_repr}" + f" user_name={self.user_name!r}" + f" user_id={self.user_id!r}" + f" tier={self.tier}" + f" skill={self.skill}" + ">" + ) + + +class SkillLeaderboard: + """SkillLeaderboard() + Represents Rocket League playlist's skill leaderboard for a single platform. + + Attributes + ---------- + platform: Platform + Platform that this leaderboard refers to. + playlist_key: PlaylistKey + Playlist that this leaderboard refers to. + players: list of `StatLeaderboardPlayer` + List of playlist's top 100 players on the platform. + + """ + + __slots__ = ("platform", "playlist_key", "players") + + def __init__( + self, + platform: Platform, + playlist_key: PlaylistKey, + data: Dict[str, Any], + ) -> None: + self.platform = platform + self.playlist_key = playlist_key + self.players = [ + SkillLeaderboardPlayer(platform, playlist_key, player_data) + for player_data in data["leaderboard"] + ] + + def __repr__(self) -> str: + platform_repr = f"{self.platform.__class__.__name__}.{self.platform._name_}" + return ( + f"<{self.__class__.__name__} {self.playlist_key} platform={platform_repr}>" + ) + + +class StatLeaderboardPlayer: + """StatLeaderboardPlayer() + Represents Rocket League Player on a platform's stat leaderboard. + + Attributes + ---------- + platform: Platform + Platform that this leaderboard entry refers to. + stat: Stat + Stat that this leaderboard entry refers to. + user_id: str, optional + Player's user ID. + Only present for Steam and Epic Games players. + user_name: str + Player's username (display name). + value: int + Value of the specified stat for the player. + + """ + + __slots__ = ("platform", "stat", "user_name", "user_id", "value") + + def __init__( + self, + platform: Platform, + stat: Stat, + data: Dict[str, Any], + ) -> None: + self.platform = platform + self.stat = stat + self.user_name: str = data["user_name"] + self.user_id: Optional[str] = data.get("user_id") + if ( + self.user_id is not None + and self.user_id.startswith(f"{platform.value}|") + and self.user_id.endswith("|0") + ): + self.user_id = self.user_id[len(platform.value) + 1 : -2] + self.value: int = data[stat.value] + + def __repr__(self) -> str: + platform_repr = f"{self.platform.__class__.__name__}.{self.platform._name_}" + stat_repr = f"{self.stat.__class__.__name__}.{self.stat._name_}" + return ( + f"<{self.__class__.__name__}" + f" platform={platform_repr}" + f" user_name={self.user_name!r}" + f" user_id={self.user_id!r}" + f" stat={stat_repr}" + f" value={self.value}" + ">" + ) + + +class StatLeaderboard: + """StatLeaderboard() + Represents Rocket League stat leaderboard for a single platform. + + Attributes + ---------- + platform: Platform + Platform that this leaderboard refers to. + stat: Stat + Stat that this leaderboard refers to. + players: list of `StatLeaderboardPlayer` + List of stat's top 100 players on the platform. + + """ + + __slots__ = ("platform", "stat", "players") + + def __init__( + self, + platform: Platform, + stat: Stat, + data: Dict[str, Any], + ) -> None: + self.platform = platform + self.stat = stat + self.players = [ + StatLeaderboardPlayer(platform, stat, player_data) + for player_data in data[stat.value] + ] + + def __repr__(self) -> str: + platform_repr = f"{self.platform.__class__.__name__}.{self.platform._name_}" + stat_repr = f"{self.stat.__class__.__name__}.{self.stat._name_}" + return f"<{self.__class__.__name__} platform={platform_repr} stat={stat_repr}>" diff --git a/rlapi/player.py b/rlapi/player.py index f3773df..96c8d33 100644 --- a/rlapi/player.py +++ b/rlapi/player.py @@ -15,7 +15,7 @@ import contextlib from typing import Any, Dict, Final, List, Optional, Union -from .enums import Platform, PlaylistKey +from .enums import Platform, PlaylistKey, Stat from .tier_estimates import TierEstimates from .typedefs import PlaylistBreakdownType, TierBreakdownType @@ -260,6 +260,11 @@ class PlayerStats: """PlayerStats() Represents player stats (assists, goals, MVPs, etc.). + .. container:: operations + + ``x[key]`` + Lookup player's stat value by `Stat` enum. + Attributes ---------- assists: int @@ -294,10 +299,8 @@ def __init__(self, data: List[Dict[str, Any]]) -> None: self.shots: int = stats.get("shots", 0) self.wins: int = stats.get("wins", 0) - def __getitem__(self, key: str) -> int: - if key not in self.__slots__: - raise KeyError(key) - return int(getattr(self, key)) + def __getitem__(self, stat: Stat) -> int: + return int(getattr(self, stat.name)) def __repr__(self) -> str: attrs = " ".join(f"{key}={getattr(self, key)}" for key in self.__slots__)