import logging
from contextlib import suppress
from datetime import datetime
from types import NoneType
from typing_extensions import Self
from trackmania.errors import TMIOException
from ._util import _frmt_str_to_datetime
from .api import _APIClient
from .base import COTDObject
from .config import get_from_cache, set_in_cache
from .constants import _TMIO
from .errors import InvalidIDError, TMIOException
_log = logging.getLogger(__name__)
__all__ = (
"BestCOTDStats",
"PlayerCOTDStats",
"PlayerCOTDResults",
"PlayerCOTD",
"COTD",
)
async def _get_trophy_page(player_id: str, page: int) -> dict:
_log.debug(f"Getting COTD Stats for Player {player_id} and page {page}")
player_cotd = get_from_cache(f"playercotd:{player_id}:{page}")
if player_cotd is not None:
return player_cotd
api_client = _APIClient()
page_data = await api_client.get(
_TMIO.build([_TMIO.TABS.PLAYER, player_id, _TMIO.TABS.COTD, str(page)])
)
await api_client.close()
with suppress(KeyError, TypeError):
raise TMIOException(page_data["error"])
if isinstance(page_data, NoneType):
raise InvalidIDError("Invalid PlayerID Given")
set_in_cache(f"playercotd:{player_id}:{page}", page_data)
return page_data
async def _get_cotd_page(page: int) -> dict:
_log.debug(f"Getting COTD Page {page}")
cotd_page = get_from_cache(f"cotd:{page}")
if cotd_page is not None:
return cotd_page.get("competitions", [])
api_client = _APIClient()
all_cotds = await api_client.get(_TMIO.build([_TMIO.TABS.COTD, page]))
await api_client.close()
with suppress(KeyError, TypeError):
raise TMIOException(all_cotds["error"])
set_in_cache(f"cotd:{page}", all_cotds, ex=7200)
return all_cotds["competitions"]
[docs]class BestCOTDStats(COTDObject):
"""
.. versionadded :: 0.3.0
Represents the Best COTD Stats of player as shows in the COTD Stats Page.
Parameters
----------
best_rank : int
The best rank achieved by the player.
best_rank_time : :class:`datetime`
The time when `best_rank` was achieved.
best_rank_div_rank : int
The rank achieved in the division of the `best_rank`.
best_div : int
The best division of the player.
best_div_time : :class:`datetime`
The time when `best_div` was achieved.
best_rank_in_div : int
The best rank the player achieved in any division.
best_rank_in_div_time : :class:`datetime`
The time when `best_rank_in_div` was achieved.
best_rank_in_div_div : int
The division of the `best_rank_in_div`.
"""
def __init__(
self,
best_rank: int,
best_rank_time: datetime,
best_rank_div_rank: int,
best_div: int,
best_div_time: datetime,
best_rank_in_div: int,
best_rank_in_div_time: datetime,
best_rank_in_div_div: int,
):
self.best_rank = best_rank
self.best_rank_time = best_rank_time
self.best_rank_div_rank = best_rank_div_rank
self.best_div = best_div
self.best_div_time = best_div_time
self.best_rank_in_div = best_rank_in_div
self.best_rank_in_div_time = best_rank_in_div_time
self.best_rank_in_div_div = best_rank_in_div_div
@classmethod
def _from_dict(cls: Self, raw: dict) -> Self:
_log.debug("Creating a BestCOTDStats class from given dictionary")
best_rank_time = _frmt_str_to_datetime(raw.get("bestranktime"))
best_div_time = _frmt_str_to_datetime(raw.get("bestdivtime"))
best_rank_div_time = _frmt_str_to_datetime(raw.get("bestrankindivtime"))
args = [
raw.get("bestrank"),
best_rank_time,
raw.get("bestrankdivrank"),
raw.get("bestdiv"),
best_div_time,
raw.get("bestrankindiv"),
best_rank_div_time,
raw.get("bestrankindivdiv"),
]
return cls(*args)
[docs]class PlayerCOTDStats(COTDObject):
"""
.. versionadded :: 0.3.0
Represents the COTD Stats of a player as shows in the COTD Stats Page.
Parameters
----------
average_div : float
The average div of the player
average_div_rank : float
The average div rank of the player
average_rank : float
The average rank of the player
best_overall : :class:`BestCOTDStats`
The best overall rank of the player
best_primary : :class:`BestCOTDStats`
The best primary rank of the player
div_win_streak : int
The div win streak of the player
total_div_wins : int
The total div wins of the player
total_wins : int
The total wins of the player
win_streak : int
The win streak of the player
"""
def __init__(
self,
average_div: float,
average_div_rank: float,
average_rank: float,
best_overall: BestCOTDStats,
best_primary: BestCOTDStats,
div_win_streak: int,
total_div_wins: int,
total_wins: int,
win_streak: int,
):
self.average_div = average_div
self.average_div_rank = average_div_rank
self.average_rank = average_rank
self.best_overall = best_overall
self.best_primary = best_primary
self.div_win_streak = div_win_streak
self.total_div_wins = total_div_wins
self.total_wins = total_wins
self.win_streak = win_streak
@classmethod
def _from_dict(cls, raw: dict) -> Self:
_log.debug("Creating a PlayerCOTDStats class from given dictionary")
args = [
raw.get("avgdiv"),
raw.get("avgdivrank"),
raw.get("avgrank"),
BestCOTDStats._from_dict(raw.get("bestoverall")),
BestCOTDStats._from_dict(raw.get("bestprimary")),
raw.get("divwinstreak"),
raw.get("totaldivwins"),
raw.get("totalwins"),
raw.get("winstreak"),
]
return cls(*args)
[docs]class PlayerCOTDResults(COTDObject):
"""
.. versionadded :: 0.3.0
Represents a Player's COTD Result.
Parameters
----------
id : int
The ID of the COTD.
timestamp : :class:`datetime`
The timestamp of the COTD.
name : str
The name of the COTD
div : int
The division the player achieved
rank : int
The rank the player achieved
div_rank : int | None
The rank the player achieved in the division. If the player did not play in the division, this will be ``None``.
score : int | None
The score of the player. If the player did not play after qualifying for the COTD, this will be ``None``.
total_players : int
The total players that played this COTD.
"""
def __init__(
self,
id: int,
timestamp: datetime,
name: str,
div: int,
rank: int,
div_rank: int | None,
score: int | None,
total_players: int,
):
self.id = id
self.timestamp = timestamp
self.name = name
self.div = div
self.rank = rank
self.div_rank = div_rank
self.score = score
self.total_players = total_players
@classmethod
def _from_dict(cls, raw: dict) -> Self:
_log.debug("Creating a PlayerCOTDResults class from given dictionary")
id = raw.get("id")
timestamp = _frmt_str_to_datetime(raw.get("timestamp"))
name = raw.get("name")
div = raw.get("div")
rank = raw.get("rank")
if raw.get("divrank") != 0:
div_rank = raw.get("divrank")
score = raw.get("score")
else:
div_rank = score = 0
total_players = raw.get("totalplayers")
return cls(
id,
timestamp,
name,
div,
rank,
div_rank,
score,
total_players,
)
[docs]class PlayerCOTD(COTDObject):
"""
.. versionadded :: 0.3.0
The Player's COTD Data
Parameters
----------
total : int
Total COTD's Played
recent_results : :class:`list[PlayerCOTDResults]`
Represents the recent COTD results the player has gotten
stats : :class:`PlayerCOTDStats`
Represents the Statistics of the Player's COTD career
player_id : str
The player's ID
"""
def __init__(
self,
total: int,
recent_results: list[PlayerCOTDResults],
stats: PlayerCOTDStats,
player_id: str,
):
self.total = total
self.recent_results = recent_results
self.stats = stats
self.player_id = player_id
@classmethod
def _from_dict(cls, page_data: dict, player_id: str) -> Self:
_log.debug("Creating a PlayerCOTD class from given dictionary")
total = page_data.get("total")
stats = PlayerCOTDStats._from_dict(page_data.get("stats"))
player_id = player_id
recent_results = []
for cotd in page_data.get("cotds"):
recent_results.append(PlayerCOTDResults._from_dict(cotd))
return cls(
total,
recent_results,
stats,
player_id,
)
[docs] @classmethod
async def get_page(cls: Self, player_id: str, page: int = 0) -> Self:
"""
.. versionadded :: 0.3.0
Gets the Player's COTD Stats of a particular page.
Parameters
----------
player_id : str
The player's ID
page : int, optional
The page of the ID, by default 0
"""
return cls._from_dict(await _get_trophy_page(player_id, page), player_id)
[docs]class COTD(COTDObject):
"""
.. versionadded :: 0.3.0
Represents a Cup of the Day
Parameters
----------
cotd_id : int
The cotd id
name : str
The name of the cotd
player_count : int
The number of players that played the :class:`COTD`
start_date : :class:`datetime`
The start date of the COTD
end_date : :class:`datetime`
The end date of the COTD
"""
def __init__(
self,
cotd_id: int,
name: str,
player_count: int,
start_date: datetime,
end_date: datetime,
):
self.cotd_id = cotd_id
self.name = name
self.player_count = player_count
self.start_date = start_date
self.end_date = end_date
@classmethod
def _from_dict(cls, raw: dict) -> Self:
_log.debug("Creating a COTD class from given dictionary")
cotd_id = raw.get("id")
name = raw.get("name")
player_count = raw.get("players")
start_date = datetime.utcfromtimestamp(raw.get("starttime"))
end_date = datetime.utcfromtimestamp(raw.get("endtime"))
return cls(
cotd_id,
name,
player_count,
start_date,
end_date,
)
[docs] @classmethod
async def get_cotd(cls: Self, page: int = 0) -> list[Self]:
"""
.. versionadded :: 0.3.0
Fetches the Latest COTDs and returns its data.
Parameters
----------
page : int, optional
The page, each page contains 12 items. by default 0
Returns
-------
:class:`list[COTD]`
The COTDs
"""
all_cotds = await _get_cotd_page(page)
acotd_fmt = []
for cotd in all_cotds:
acotd_fmt.append(cls._from_dict(cotd))
return acotd_fmt