Source code for psnawp_api.models.title_stats

"""Provides TitleStats class for retrieving a user's statistics on played titles and games."""

from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import datetime, timedelta
from enum import Enum
from typing import TYPE_CHECKING, Any, Final, Literal

from typing_extensions import Self, override

from psnawp_api.models.listing import PaginationIterator
from psnawp_api.utils.endpoints import API_PATH, BASE_PATH
from psnawp_api.utils.misc import iso_format_to_datetime

if TYPE_CHECKING:
    from collections.abc import Generator

    from psnawp_api.core import Authenticator
    from psnawp_api.models.listing import PaginationArguments


[docs] class PlatformCategory(Enum): """Represents the PlayStation platform associated with title.""" UNKNOWN = "unknown" PS4 = "ps4_game" PS5 = "ps5_native_game" @classmethod def _missing_(cls, value: object) -> Literal[PlatformCategory.UNKNOWN]: _ = value return cls.UNKNOWN
PT_REGEX: Final[re.Pattern[str]] = re.compile(r"PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?")
[docs] def play_duration_to_timedelta(play_duration: str | None) -> timedelta: """Provides a timedelta object for the play duration PSN sends. If for some reason the string is malformed or None, timedelta will return 0. Valid patters: PT243H18M48S, PT21M18S, PT18H, PT18H20S, PT4H21M :param play_duration: String from API :returns: String parsed into a timedelta object .. note:: PSN API returns the duration in this format: PT243H18M48S. The maximum time Unit is Hours, it does not extend to Days or Months. """ hours = 0 minutes = 0 seconds = 0 if play_duration: match = re.search(PT_REGEX, play_duration) if not match: return timedelta(hours=hours, minutes=minutes, seconds=seconds) hours = int(match.group(1)) if match.group(1) else 0 minutes = int(match.group(2)) if match.group(2) else 0 seconds = int(match.group(3)) if match.group(3) else 0 return timedelta(hours=hours, minutes=minutes, seconds=seconds)
[docs] @dataclass(frozen=True) class TitleStats: """A class that represents a PlayStation Video Game Play Time Stats. .. note:: This class is intended to be used via PSNAWP. """ title_id: str | None "Game title id" name: str | None "Game title name" image_url: str | None "Image URL" category: PlatformCategory | None "Category/Platform Type" play_count: int | None "Number of times the game has been played" first_played_date_time: datetime | None "First time the game was played" last_played_date_time: datetime | None "Last time the game was played" play_duration: timedelta | None "Total time the game has been played. Example: PT1H51M21S"
[docs] @classmethod def from_dict(cls, game_stats_dict: dict[str, Any]) -> TitleStats: """Creates an instance of :py:class:`TitleStats` from a dictionary.""" return cls( title_id=game_stats_dict.get("titleId"), name=game_stats_dict.get("name"), image_url=game_stats_dict.get("imageUrl"), category=PlatformCategory(game_stats_dict.get("category")), play_count=game_stats_dict.get("playCount"), first_played_date_time=iso_format_to_datetime( game_stats_dict.get("firstPlayedDateTime"), ), last_played_date_time=iso_format_to_datetime( game_stats_dict.get("lastPlayedDateTime"), ), play_duration=play_duration_to_timedelta( game_stats_dict.get("playDuration"), ), )
[docs] class TitleStatsIterator(PaginationIterator[TitleStats]): """An iterator for fetching and paginating through TitleStats objects from the PlayStation Network API. .. note:: This class is intended to be used via Client or User class. See :py:meth:`psnawp_api.models.client.Client.title_stats` or :py:meth:`psnawp_api.models.user.User.title_stats`. """
[docs] def __init__( self, authenticator: Authenticator, url: str, pagination_args: PaginationArguments, ) -> None: """Init for TitleStatsIterator.""" super().__init__( authenticator=authenticator, url=url, pagination_args=pagination_args, )
[docs] @override def fetch_next_page(self) -> Generator[TitleStats, None, None]: """Fetches the next page of TitleStats objects from the API. :yield: A generator yielding TitleStats objects. """ response = self.authenticator.get( url=self._url, params=self._pagination_args.get_params_dict(), ).json() self._total_item_count = response.get("totalItemCount", 0) titles: list[dict[str, Any]] = response.get("titles") for title in titles: title_instance = TitleStats.from_dict(title) self._pagination_args.increment_offset() yield title_instance offset = response.get("nextOffset") or 0 if offset > 0: self._has_next = True else: self._has_next = False
[docs] @classmethod def from_endpoint( cls, authenticator: Authenticator, account_id: str, pagination_args: PaginationArguments, ) -> Self: """Creates an instance of TitleStatsIterator from the given endpoint. :param authenticator: The Authenticator instance used for making authenticated requests to the API. :param account_id: The account ID for which to fetch title stats. :param pagination_args: Arguments for handling pagination, including limit, offset, and page size. :returns: An instance of TitleStatsIterator. """ url = f"{BASE_PATH['games_list']}{API_PATH['user_game_data'].format(account_id=account_id)}" return cls( authenticator=authenticator, url=url, pagination_args=pagination_args, )