import json
import logging
from contextlib import suppress
from datetime import datetime
from typing_extensions import Self
from ._util import _add_commas, _frmt_str_to_datetime, _regex_it
from .api import _APIClient
from .base import TrophyObject
from .config import get_from_cache, set_in_cache
from .constants import _TMIO
from .errors import InvalidIDError, InvalidTrophyNumber, TMIOException
_log = logging.getLogger(__name__)
__all__ = ("PlayerTrophies", "TrophyLeaderboardPlayer")
[docs]class TrophyLeaderboardPlayer(TrophyObject):
"""
.. versionadded :: 0.4.0
Represents a player on the trophy leaderboards
Parameters
----------
player_name : str
The player's name
club_tag : str | None
The player's club tag
player_id : str
The player's ID
rank : int
The player's rank
score : str
The player's score
zones : `list[PlayerZone]`
The player's zones
"""
def __init__(
self,
player_name: str,
club_tag: str | None,
player_id: str,
rank: int,
score: str,
zones: list,
):
self.player_name = player_name
self.club_tag = club_tag
self.player_id = player_id
self.rank = rank
self.score = score
self.zones = zones
@classmethod
def _from_dict(cls: Self, raw: dict) -> Self:
from .player import PlayerZone
args = []
player = raw.get("player")
args.append(player.get("name"))
args.append(_regex_it(player.get("tag", None)))
args.append(player.get("id"))
args.append(raw.get("rank"))
args.append(_add_commas(int(raw.get("score"))))
args.append(PlayerZone._parse_zones(player.get("zone"), [0, 0, 0, 0, 0]))
return cls(*args)
[docs] async def get_player(self: Self):
"""
.. versionadded :: 0.4.0
Gets the player object using the player ID.
Returns
-------
:class:`Player`
The player object
"""
from .player import Player
return await Player.get_player(self.player_id)
[docs]class PlayerTrophies(TrophyObject):
"""
.. versionadded :: 0.1.0
Represents Player Trophies
Parameters
----------
echelon : int
The trophy echelon of the player.
last_change : str
The date of the last change of the player's self.
points : ints: int
The number of points of the player.
trophies : :class:`list[int]`
The number of trophies of the player.
player_id : str | :class:`NoneType`, optional
The Trackmania ID of the player
"""
def __init__(
self,
echelon: int,
last_change: datetime,
points: int,
trophies: list[int],
player_id: str | None = None,
):
"""Constructor for the class."""
self.echelon = echelon
self._last_change = last_change
self.points = points
self.trophies = trophies
self._player_id = player_id
@classmethod
def _from_dict(cls: Self, raw_trophy_data: dict, player_id: str) -> Self:
"""
Creates a :class:`PlayerTrophies` object from the given dictionary.
Parameters
----------
raw_trophy_data : :class:`dict`
The raw trophy data to parse.
player_id : str
The player ID to set.
Returns
-------
:class:`PlayerTrophies`
The parsed trophy data.
"""
_log.debug("Creating a PlayerTrophies class from the given dictionary.")
return cls(
echelon=raw_trophy_data.get("echelon"),
last_change=_frmt_str_to_datetime(raw_trophy_data.get("timestamp")),
points=raw_trophy_data.get("points"),
trophies=raw_trophy_data.get("counts"),
player_id=player_id,
)
@property
def last_change(self):
"""Last change property."""
return self._last_change
@property
def player_id(self):
"""player_id property"""
return self._player_id
[docs] def set_id(self, player_id: str):
"""Setter for player_id"""
self._player_id = player_id
[docs] def trophy(self, number: int) -> int:
"""
.. versionadded :: 0.3.0
Returns the trophies by tier.
Parameters
----------
number : int
The trophy number, from 1 (T1) to 9 (T9).
Returns
-------
int
the number of trophies for that specific tier.
"""
_log.debug(f"Returning trophy T{number} for player {self.player_id}")
if number > 9 or number < 1:
raise InvalidTrophyNumber(
"Trophy Number cannot be less than 1 or greater than 9"
)
return self.trophies[number - 1]
[docs] def score(self) -> int:
"""
.. versionadded :: 0.3.0
Returns the total trophy score of the player.
Returns
-------
int
The total score.
"""
score = (
0
+ self.trophy(1) * 1
+ self.trophy(2) * 10
+ self.trophy(3) * 100
+ self.trophy(4) * 1_000
+ self.trophy(5) * 10_000
+ self.trophy(6) * 100_000
+ self.trophy(7) * 1_000_000
+ self.trophy(8) * 10_000_000
+ self.trophy(9) * 100_000_000
)
_log.debug(f"Score of {self.player_id} is {score}")
return score
def __str__(self) -> str:
trophy_str = ""
for i, trophyd in enumerate(self.trophies):
trophy_str = trophy_str + f"T{i + 1} - " + _add_commas(trophyd) + "\n"
trophy_str = trophy_str + f"\nTotal Trophies: {_add_commas(self.score())}"
return trophy_str
[docs] async def history(self, page: int = 0) -> dict:
"""
.. versionadded :: 0.3.0
Retrieves Trophy Gain and Loss history of a player.
Parameters
----------
page : int, optional
page number of trophy history, by default 0
Returns
-------
:class:`dict`
Trophy history data.
Raises
------
InvalidIDError
If an ID has not been set for the object.
InvalidIDError
If an invalid id has been set for the object.
"""
_log.debug(
f"Getting Trophy Leaderboard for Page: {page} and Player Id: {self.player_id}"
)
trophy_leaderboard_data = get_from_cache(f"trophy:{page}")
if trophy_leaderboard_data is not None:
return trophy_leaderboard_data.get("gains")
api_client = _APIClient()
if self.player_id is None:
raise InvalidIDError("ID Has not been set for the Object")
history = await api_client.get(
_TMIO.build(
[_TMIO.TABS.PLAYER, self.player_id, _TMIO.TABS.TROPHIES, str(page)]
)
)
await api_client.close()
with suppress(KeyError, TypeError):
raise TMIOException(history["error"])
set_in_cache(f"trophy:{page}", json.dumps(history), ex=3600)
return history["gains"]
[docs] @staticmethod
async def top_trophies(page: int = 0) -> list[TrophyLeaderboardPlayer]:
"""
.. versionadded :: 0.3.0
Get's the top players ranked by trophies
Parameters
----------
page : int, optional
The page of the leaderboards, by default 0
Returns
-------
:class:`list[TrophyLeaderboardPlayer]`
The players as a list of :class:`TrophyLeaderboardPlayer` objects.
"""
_log.debug(f"Getting Page {page} of Trophy Leaderboards")
trophy_leaderboard_data = get_from_cache(f"trophies:{page}")
if trophy_leaderboard_data is not None:
lb_players = []
for top_player in trophy_leaderboard_data.get("ranks", []):
lb_players.append(TrophyLeaderboardPlayer._from_dict(top_player))
api_client = _APIClient()
top_trophies = await api_client.get(
_TMIO.build([_TMIO.TABS.TOP_TROPHIES, str(page)])
)
await api_client.close()
with suppress(KeyError, TypeError):
raise TMIOException(top_trophies["error"])
set_in_cache(f"trophies:{page}", json.dumps(top_trophies), ex=3600)
lb_players = []
for top_player in top_trophies["ranks"]:
lb_players.append(TrophyLeaderboardPlayer._from_dict(top_player))
return lb_players