diff --git a/README.md b/README.md index 2958196..42bdef8 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Each exposed endpoint from the APIs can be queried with a method named after it: ```python from aoe2netwrapper import AoE2NetAPI -client = AoE2NetAPI(timeout=10) +client = AoE2NetAPI(timeout=10) # specify timeout limit for ALL requests at instantiation # Get the first 100 ranked accounts in 1v1 Random Map top_accounts = client.leaderboard(game="aoe2de", leaderboard_id=3, start=1, count=100) @@ -92,7 +92,7 @@ open_lobbies = client.lobbies(game="aoe2de") ```python from aoe2netwrapper import AoE2NightbotAPI -nightbot = AoE2NightbotAPI(timeout=10) +nightbot = AoE2NightbotAPI(timeout=10) # specify timeout limit for ALL requests at instantiation # Get quick rank information on a specific player in 1v1 Random Map viper_details = nightbot.rank(leaderboard_id=3, search="GL.TheViper") diff --git a/aoe2netwrapper/api.py b/aoe2netwrapper/api.py index 364d1bc..6a50865 100644 --- a/aoe2netwrapper/api.py +++ b/aoe2netwrapper/api.py @@ -12,6 +12,14 @@ from loguru import logger from aoe2netwrapper.exceptions import Aoe2NetException +from aoe2netwrapper.models import ( + LastMatchResponse, + LeaderBoardResponse, + MatchLobby, + NumOnlineResponse, + RatingTimePoint, + StringsResponse, +) class AoE2NetAPI: @@ -38,7 +46,7 @@ def __init__(self, timeout: Union[float, Tuple[float, float]] = 5): def __repr__(self) -> str: return f"Client for <{self.API_BASE_URL}>" - def strings(self, game: str = "aoe2de") -> dict: + def strings(self, game: str = "aoe2de") -> StringsResponse: """ Requests a list of strings used by the API. @@ -48,18 +56,19 @@ def strings(self, game: str = "aoe2de") -> dict: Empires 2: Definitive Edition). Returns: - A dictionnary with different strings used by the API, each with their list of possible - values (also a dict each). + A StringsResponse validated object encapsulating the strings used by the API. """ logger.debug("Preparing parameters for strings query") query_params = {"game": game} - return _get_request_response_json( + processed_response = _get_request_response_json( session=self.session, url=self.STRINGS_ENDPOINT, params=query_params, timeout=self.timeout, ) + logger.trace(f"Validating response from '{self.STRINGS_ENDPOINT}'") + return StringsResponse(**processed_response) def leaderboard( self, @@ -70,7 +79,7 @@ def leaderboard( search: str = None, steam_id: int = None, profile_id: int = None, - ) -> dict: + ) -> LeaderBoardResponse: """ Request the current leaderboards. @@ -85,18 +94,19 @@ def leaderboard( Defaults to 1. count (int): Number of leaderboard entries to get (warning: must be 10000 or less). Defaults to 10. - search (str): To perform the search for a specific player, from their name. - steam_id (int): To perform the search for a specific player, from their steamID64 - (ex: 76561199003184910). - profile_id (int): To perform the search for a specific player, from their profile ID - (ex: 459658). + search (str): Optional. To perform the search for a specific player, from their name. + steam_id (int): Optional. To perform the search for a specific player, from their + steamID64 (ex: 76561199003184910). + profile_id (int): Optional. To perform the search for a specific player, from their + profile ID (ex: 459658). Raises: Aoe2NetException: if the 'count' parameter exceeds 10 000. Returns: - A dictionnary with the different parameters used for the query, the 'total' amount of - hits, and the leaderboard as a list profile entries for each ranking (a dict each). + A LeaderBoardResponse validated object with the different parameters used for the + query, the total amount of hits, and the leaderboard as a list profile entries for + each ranking. """ if count > 10_000: logger.error(f"'count' has to be 10000 or less, but {count} was provided.") @@ -113,14 +123,16 @@ def leaderboard( "profile_id": profile_id, } - return _get_request_response_json( + processed_response = _get_request_response_json( session=self.session, url=self.LEADERBOARD_ENDPOINT, params=query_params, timeout=self.timeout, ) + logger.trace(f"Validating response from '{self.LEADERBOARD_ENDPOINT}'") + return LeaderBoardResponse(**processed_response) - def lobbies(self, game: str = "aoe2de") -> List[dict]: + def lobbies(self, game: str = "aoe2de") -> List[MatchLobby]: """ Request all open lobbies. @@ -130,21 +142,24 @@ def lobbies(self, game: str = "aoe2de") -> List[dict]: Empires 2: Definitive Edition). Returns: - A list of dictionaries, each one encapsulating the data for a currently open lobby. + A list of MatchLobby valideted objects, each one encapsulating the data for a currently + open lobby. """ logger.debug("Preparing parameters for open lobbies query") query_params = {"game": game} - return _get_request_response_json( + processed_response = _get_request_response_json( session=self.session, url=self.LOBBIES_ENDPOINT, params=query_params, timeout=self.timeout, ) + logger.trace(f"Validating response from '{self.LOBBIES_ENDPOINT}'") + return [MatchLobby(**lobby) for lobby in processed_response] def last_match( self, game: str = "aoe2de", steam_id: int = None, profile_id: int = None - ) -> dict: + ) -> LastMatchResponse: """ Request the last match the player started playing, this will be the current match if they are still in game. Either 'steam_id' or 'profile_id' required. @@ -160,9 +175,9 @@ def last_match( Aoe2NetException: if the not one of 'steam_id' or 'profile_id' are provided. Returns: - A dictionary with the information of the game, including the following keys: - 'profile_id', 'steam_id', 'name', 'clan', 'country' & 'last_match', with 'last_match' - being a dictionary with the match's information. + A LastMatchResponse validated object with the information of the game, including the + following attributes: 'profile_id', 'steam_id', 'name', 'clan', 'country' and + 'last_match'. """ if not steam_id and not profile_id: logger.error("Missing one of 'steam_id', 'profile_id'.") @@ -173,12 +188,14 @@ def last_match( logger.debug("Preparing parameters for last match query") query_params = {"game": game, "steam_id": steam_id, "profile_id": profile_id} - return _get_request_response_json( + processed_response = _get_request_response_json( session=self.session, url=self.LAST_MATCH_ENDPOINT, params=query_params, timeout=self.timeout, ) + logger.trace(f"Validating response from '{self.LAST_MATCH_ENDPOINT}'") + return LastMatchResponse(**processed_response) def match_history( self, @@ -187,7 +204,7 @@ def match_history( count: int = 10, steam_id: int = None, profile_id: int = None, - ) -> List[dict]: + ) -> List[MatchLobby]: """ Request the match history for a player. Either 'steam_id' or 'profile_id' required. @@ -205,8 +222,8 @@ def match_history( Aoe2NetException: if the not one of 'steam_id' or 'profile_id' are provided. Returns: - A list of dictionaries, each one encapsulating the data for one of the player's - previous matches. + A list of MatchLobby validated objects, each one encapsulating the data for one of the + player's previous matches. """ if count > 1_000: logger.error(f"'count' has to be 1000 or less, but {count} was provided.") @@ -227,22 +244,24 @@ def match_history( "profile_id": profile_id, } - return _get_request_response_json( + processed_response = _get_request_response_json( session=self.session, url=self.MATCH_HISTORY_ENDPOINT, params=query_params, timeout=self.timeout, ) + logger.trace(f"Validating response from '{self.MATCH_HISTORY_ENDPOINT}'") + return [MatchLobby(**lobby) for lobby in processed_response] def rating_history( self, game: str = "aoe2de", leaderboard_id: int = 3, start: int = 0, - count: int = 100, + count: int = 20, steam_id: int = None, profile_id: int = None, - ) -> List[dict]: + ) -> List[RatingTimePoint]: """ Requests the rating history for a player. Either 'steam_id' or 'profile_id' required. @@ -264,9 +283,9 @@ def rating_history( Aoe2NetException: if the not one of 'steam_id' or 'profile_id' are provided. Returns: - A list of dictionaries, each one encapsulating data at a certain point in time - corresponding to a match played by the player, including the rating, timestamp of the - match, streak etc. + A list of RatingTimePoint validated objects, each one encapsulating data at a certain + point in time corresponding to a match played by the player, including the rating, + timestamp of the match, streaks etc. """ if count > 10_000: logger.error(f"'count' has to be 10 000 or less, but {count} was provided.") @@ -288,14 +307,16 @@ def rating_history( "profile_id": profile_id, } - return _get_request_response_json( + processed_response = _get_request_response_json( session=self.session, url=self.RATING_HISTORY_ENDPOINT, params=query_params, timeout=self.timeout, ) + logger.trace(f"Validating response from '{self.RATING_HISTORY_ENDPOINT}'") + return [RatingTimePoint(**rating) for rating in processed_response] - def matches(self, game: str = "aoe2de", count: int = 10, since: int = None) -> List[dict]: + def matches(self, game: str = "aoe2de", count: int = 10, since: int = None) -> List[MatchLobby]: """ Request matches after a specific time: the match history in an optionally given time window. @@ -314,8 +335,8 @@ def matches(self, game: str = "aoe2de", count: int = 10, since: int = None) -> L Aoe2NetException: if the 'count' parameter exceeds 1000. Returns: - A list of dictionaries, each one encapsulating the data for one of the played matches - during the time window queried for. + A list of MatchLobby validated objects, each one encapsulating the data for one of the + played matches during the time window queried for. """ if count > 1000: logger.error(f"'count' has to be 1000 or less, but {count} was provided.") @@ -328,14 +349,16 @@ def matches(self, game: str = "aoe2de", count: int = 10, since: int = None) -> L "since": since, } - return _get_request_response_json( + processed_response = _get_request_response_json( session=self.session, url=self.MATCHES_ENDPOINT, params=query_params, timeout=self.timeout, ) + logger.trace(f"Validating response from '{self.MATCHES_ENDPOINT}'") + return [MatchLobby(**lobby) for lobby in processed_response] - def match(self, game: str = "aoe2de", uuid: str = None, match_id: int = None) -> dict: + def match(self, game: str = "aoe2de", uuid: str = None, match_id: int = None) -> MatchLobby: """ Request details about a match. Either 'uuid' or 'match_id' required. @@ -350,7 +373,7 @@ def match(self, game: str = "aoe2de", uuid: str = None, match_id: int = None) -> Aoe2NetException: if the not one of 'uuid' or 'match_id' are provided. Returns: - A dictionary with the information of the specific match, including. + A MatchLobby validated object with the information of the specific match, including. """ if not uuid and not match_id: logger.error("Missing one of 'uuid', 'match_id'.") @@ -363,14 +386,16 @@ def match(self, game: str = "aoe2de", uuid: str = None, match_id: int = None) -> "match_id": match_id, } - return _get_request_response_json( + processed_response = _get_request_response_json( session=self.session, url=self.MATCH_ENDPOINT, params=query_params, timeout=self.timeout, ) + logger.trace(f"Validating response from '{self.MATCH_ENDPOINT}'") + return MatchLobby(**processed_response) - def num_online(self, game: str = "aoe2de") -> dict: + def num_online(self, game: str = "aoe2de") -> NumOnlineResponse: """ Number of players in game and an estimate of the number current playing multiplayer. @@ -380,19 +405,21 @@ def num_online(self, game: str = "aoe2de") -> dict: Empires 2: Definitive Edition). Returns: - A dictionary with the app id and a list of the estimated 'num_players' metrics at - different timestamps ('steam', 'multiplayer', 'looking', 'in_game', 'multiplayer_1h' & - 'multiplayer_24h'). + A NumOnlineResponse validated object with the app id and a list of PlayerCountTimePoint + validated objects encapsulating estimated metrics at different timestamps ('steam', + 'multiplayer', 'looking', 'in_game', 'multiplayer_1h' & 'multiplayer_24h'). """ logger.debug("Preparing parameters for number of online players query") query_params = {"game": game} - return _get_request_response_json( + processed_response = _get_request_response_json( session=self.session, url=self.NUMBER_ONLINE_ENDPOINT, params=query_params, timeout=self.timeout, ) + logger.trace(f"Validating response from '{self.NUMBER_ONLINE_ENDPOINT}'") + return NumOnlineResponse(**processed_response) # ----- Helpers ----- # diff --git a/aoe2netwrapper/models/__init__.py b/aoe2netwrapper/models/__init__.py new file mode 100644 index 0000000..b3f2fdc --- /dev/null +++ b/aoe2netwrapper/models/__init__.py @@ -0,0 +1,16 @@ +""" +aoe2netwrapper.models +--------------------- + +This subpackage contains the model objects used to encapsulate responses from the API. +Each module therein contains the models for a specific API endpoint. +""" +from .last_match import LastMatchResponse +from .leaderboard import LeaderBoardResponse +from .lobbies import MatchLobby +from .match import MatchLobby +from .match_history import MatchLobby +from .matches import MatchLobby +from .num_online import NumOnlineResponse +from .rating_history import RatingTimePoint +from .strings import StringsResponse diff --git a/aoe2netwrapper/models/last_match.py b/aoe2netwrapper/models/last_match.py new file mode 100644 index 0000000..3a94586 --- /dev/null +++ b/aoe2netwrapper/models/last_match.py @@ -0,0 +1,20 @@ +""" +aoe2netwrapper.models.lobbies +----------------------------- + +This module contains the model objects to encapsulate the responses from the endpoint at +https://aoe2.net/api/lobbies +""" +from typing import Optional + +from pydantic import BaseModel, Field + +from aoe2netwrapper.models.lobbies import MatchLobby + + +class LastMatchResponse(BaseModel): + profile_id: Optional[int] = Field(None) + steam_id: Optional[int] = Field(None) + name: Optional[str] = Field(None) + country: Optional[str] = Field(None) + last_match: Optional[MatchLobby] = Field(None) diff --git a/aoe2netwrapper/models/leaderboard.py b/aoe2netwrapper/models/leaderboard.py new file mode 100644 index 0000000..b690d6f --- /dev/null +++ b/aoe2netwrapper/models/leaderboard.py @@ -0,0 +1,40 @@ +""" +aoe2netwrapper.models.leaderboard +--------------------------------- + +This module contains the model objects to encapsulate the responses from the endpoint at +https://aoe2.net/api/leaderboard +""" +from typing import Any, List, Optional + +from pydantic import BaseModel, Field + + +class LeaderBoardSpot(BaseModel): + profile_id: Optional[int] = Field(None) + rank: Optional[int] = Field(None) + rating: Optional[int] = Field(None) + steam_id: Optional[int] = Field(None) + icon: Any = None + name: Optional[str] = Field(None) + clan: Optional[str] = Field(None) + country: Optional[str] = Field(None) + previous_rating: Optional[int] = Field(None) + highest_rating: Optional[int] = Field(None) + streak: Optional[int] = Field(None) + lowest_streak: Optional[int] = Field(None) + highest_streak: Optional[int] = Field(None) + games: Optional[int] = Field(None) + wins: Optional[int] = Field(None) + losses: Optional[int] = Field(None) + drops: Optional[int] = Field(None) + last_match: Optional[int] = Field(None) + last_match_time: Optional[int] = Field(None) + + +class LeaderBoardResponse(BaseModel): + total: Optional[int] = Field(None) + leaderboard_id: Optional[int] = Field(None) + start: Optional[int] = Field(None) + count: Optional[int] = Field(None) + leaderboard: Optional[List[LeaderBoardSpot]] = Field(None) diff --git a/aoe2netwrapper/models/lobbies.py b/aoe2netwrapper/models/lobbies.py new file mode 100644 index 0000000..f96b24b --- /dev/null +++ b/aoe2netwrapper/models/lobbies.py @@ -0,0 +1,74 @@ +""" +aoe2netwrapper.models.lobbies +----------------------------- + +This module contains the model objects to encapsulate the responses from the endpoint at +https://aoe2.net/api/lobbies +""" +from typing import Any, List, Optional + +from pydantic import BaseModel, Field + + +class LobbyMember(BaseModel): + profile_id: Optional[int] = Field(None) + steam_id: Optional[int] = Field(None) + name: Optional[str] = Field(None) + clan: Optional[str] = Field(None) + country: Optional[str] = Field(None) + slot: Optional[int] = Field(None) + slot_type: Optional[int] = Field(None) + rating: Optional[int] = Field(None) + rating_change: Any = None + games: Optional[int] = Field(None) + wins: Optional[int] = Field(None) + streak: Optional[int] = Field(None) + drops: Optional[int] = Field(None) + color: Optional[str] = Field(None) + team: Optional[str] = Field(None) + civ: Optional[int] = Field(None) + won: Optional[int] = Field(None) + + +class MatchLobby(BaseModel): + match_id: Optional[int] = Field(None) + lobby_id: Optional[int] = Field(None) + match_uuid: Optional[str] = Field(None) + version: Optional[int] = Field(None) + name: Optional[str] = Field(None) + num_players: Optional[int] = Field(None) + num_slots: Optional[int] = Field(None) + average_rating: Optional[int] = Field(None) + cheats: Optional[bool] = Field(None) + full_tech_tree: Optional[bool] = Field(None) + ending_age: Optional[int] = Field(None) + expansion: Optional[str] = Field(None) + game_type: Optional[int] = Field(None) + has_custom_content: Optional[bool] = Field(None) + has_password: Optional[bool] = Field(None) + lock_speed: Optional[bool] = Field(None) + lock_teams: Optional[bool] = Field(None) + map_size: Optional[int] = Field(None) + map_type: Optional[int] = Field(None) + pop: Optional[int] = Field(None) + ranked: Optional[bool] = Field(None) + leaderboard_id: Optional[int] = Field(None) + rating_type: Optional[int] = Field(None) + resources: Optional[int] = Field(None) + rms: Optional[str] = Field(None) + scenario: Optional[str] = Field(None) + server: Optional[str] = Field(None) + shared_exploration: Optional[bool] = Field(None) + speed: Optional[int] = Field(None) + starting_age: Optional[int] = Field(None) + team_together: Optional[bool] = Field(None) + team_positions: Optional[bool] = Field(None) + treaty_length: Optional[int] = Field(None) + turbo: Optional[bool] = Field(None) + victory: Optional[int] = Field(None) + victory_time: Optional[int] = Field(None) + visibility: Optional[int] = Field(None) + opened: Optional[int] = Field(None) + started: Any = None + finished: Any = None + players: Optional[List[LobbyMember]] = Field(None) diff --git a/aoe2netwrapper/models/match.py b/aoe2netwrapper/models/match.py new file mode 100644 index 0000000..2dff4e6 --- /dev/null +++ b/aoe2netwrapper/models/match.py @@ -0,0 +1,8 @@ +""" +aoe2netwrapper.models.match +--------------------------- + +This module contains the model objects to encapsulate the responses from the endpoint at +https://aoe2.net/api/match +""" +from aoe2netwrapper.models.lobbies import MatchLobby diff --git a/aoe2netwrapper/models/match_history.py b/aoe2netwrapper/models/match_history.py new file mode 100644 index 0000000..3c4a880 --- /dev/null +++ b/aoe2netwrapper/models/match_history.py @@ -0,0 +1,8 @@ +""" +aoe2netwrapper.models.match_history +----------------------------------- + +This module contains the model objects to encapsulate the responses from the endpoint at +https://aoe2.net/api/player/matches +""" +from aoe2netwrapper.models.lobbies import MatchLobby diff --git a/aoe2netwrapper/models/matches.py b/aoe2netwrapper/models/matches.py new file mode 100644 index 0000000..ffa0435 --- /dev/null +++ b/aoe2netwrapper/models/matches.py @@ -0,0 +1,8 @@ +""" +aoe2netwrapper.models.matches +----------------------------- + +This module contains the model objects to encapsulate the responses from the endpoint at +https://aoe2.net/api/matches +""" +from aoe2netwrapper.models.lobbies import MatchLobby diff --git a/aoe2netwrapper/models/num_online.py b/aoe2netwrapper/models/num_online.py new file mode 100644 index 0000000..f70dfb9 --- /dev/null +++ b/aoe2netwrapper/models/num_online.py @@ -0,0 +1,29 @@ +""" +aoe2netwrapper.models.num_online +-------------------------------- + +This module contains the model objects to encapsulate the responses from the endpoint at +https://aoe2.net/api/stats/players +""" +from typing import Any, List, Optional + +from pydantic import BaseModel, Field + + +class NumPlayers(BaseModel): + steam: Optional[int] = Field(None) + multiplayer: Optional[int] = Field(None) + looking: Optional[int] = Field(None) + in_game: Optional[int] = Field(None) + multiplayer_1h: Optional[int] = Field(None) + multiplayer_24h: Optional[int] = Field(None) + + +class PlayerCountTimePoint(BaseModel): + time: Optional[int] = Field(None) + num_players: Optional[NumPlayers] = Field(None) + + +class NumOnlineResponse(BaseModel): + app_id: Optional[int] = Field(None) + player_stats: Optional[List[PlayerCountTimePoint]] = Field(None) diff --git a/aoe2netwrapper/models/rating_history.py b/aoe2netwrapper/models/rating_history.py new file mode 100644 index 0000000..d1e02ea --- /dev/null +++ b/aoe2netwrapper/models/rating_history.py @@ -0,0 +1,19 @@ +""" +aoe2netwrapper.models.rating_history +------------------------------------ + +This module contains the model objects to encapsulate the responses from the endpoint at +https://aoe2.net/api/player/ratinghistory +""" +from typing import Optional + +from pydantic import BaseModel, Field + + +class RatingTimePoint(BaseModel): + rating: Optional[int] = Field(None) + num_wins: Optional[int] = Field(None) + num_losses: Optional[int] = Field(None) + streak: Optional[int] = Field(None) + drops: Optional[int] = Field(None) + timestamp: Optional[int] = Field(None) diff --git a/aoe2netwrapper/models/strings.py b/aoe2netwrapper/models/strings.py new file mode 100644 index 0000000..a04b616 --- /dev/null +++ b/aoe2netwrapper/models/strings.py @@ -0,0 +1,80 @@ +""" +aoe2netwrapper.models.strings +----------------------------- + +This module contains the model objects to encapsulate the responses from the endpoint at +https://aoe2.net/api/strings +""" +from typing import List, Optional + +from pydantic import BaseModel, Field + + +class AgeString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class CivilizationString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class GameTypeString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class LeaderBoardString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class MapSizeString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class MapTypeString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class RatingTypeString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class ResourcesString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class SpeedString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class VictoryString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class VisibilityString(BaseModel): + id: Optional[int] = Field(None) + string: Optional[str] = Field(None) + + +class StringsResponse(BaseModel): + language: Optional[str] = Field(None) + age: Optional[List[AgeString]] = Field(None) + civ: Optional[List[CivilizationString]] = Field(None) + game_type: Optional[List[GameTypeString]] = Field(None) + leaderboard: Optional[List[LeaderBoardString]] = Field(None) + map_size: Optional[List[MapSizeString]] = Field(None) + map_type: Optional[List[MapTypeString]] = Field(None) + rating_type: Optional[List[RatingTypeString]] = Field(None) + resources: Optional[List[ResourcesString]] = Field(None) + speed: Optional[List[SpeedString]] = Field(None) + victory: Optional[List[VictoryString]] = Field(None) + visibility: Optional[List[VisibilityString]] = Field(None) diff --git a/poetry.lock b/poetry.lock index f77bda5..08f5d1d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -134,7 +134,7 @@ toml = ["toml"] name = "dataclasses" version = "0.8" description = "A backport of the dataclasses module for Python 3.6" -category = "dev" +category = "main" optional = false python-versions = ">=3.6, <3.7" @@ -288,6 +288,22 @@ category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +[[package]] +name = "pydantic" +version = "1.7.3" +description = "Data validation and settings management using python 3.6 type hinting" +category = "main" +optional = false +python-versions = ">=3.6" + +[package.dependencies] +dataclasses = {version = ">=0.6", markers = "python_version < \"3.7\""} + +[package.extras] +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] +typing_extensions = ["typing-extensions (>=3.7.2)"] + [[package]] name = "pylint" version = "2.6.0" @@ -469,7 +485,7 @@ testing = ["pytest (>=3.5,!=3.7.3)", "pytest-checkdocs (>=1.2.3)", "pytest-flake [metadata] lock-version = "1.1" python-versions = "^3.6" -content-hash = "467b72e92f0d8e3f967372a00f9e3df8be27744241a63b52024c957756ac1a0d" +content-hash = "6f12d3fd213574d6860a60aa600bdd750d367046bd24a5cbc48467c8c74af77b" [metadata.files] aiocontextvars = [ @@ -666,6 +682,30 @@ py = [ {file = "py-1.10.0-py2.py3-none-any.whl", hash = "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"}, {file = "py-1.10.0.tar.gz", hash = "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3"}, ] +pydantic = [ + {file = "pydantic-1.7.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c59ea046aea25be14dc22d69c97bee629e6d48d2b2ecb724d7fe8806bf5f61cd"}, + {file = "pydantic-1.7.3-cp36-cp36m-manylinux1_i686.whl", hash = "sha256:a4143c8d0c456a093387b96e0f5ee941a950992904d88bc816b4f0e72c9a0009"}, + {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_i686.whl", hash = "sha256:d8df4b9090b595511906fa48deda47af04e7d092318bfb291f4d45dfb6bb2127"}, + {file = "pydantic-1.7.3-cp36-cp36m-manylinux2014_x86_64.whl", hash = "sha256:514b473d264671a5c672dfb28bdfe1bf1afd390f6b206aa2ec9fed7fc592c48e"}, + {file = "pydantic-1.7.3-cp36-cp36m-win_amd64.whl", hash = "sha256:dba5c1f0a3aeea5083e75db9660935da90216f8a81b6d68e67f54e135ed5eb23"}, + {file = "pydantic-1.7.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:59e45f3b694b05a69032a0d603c32d453a23f0de80844fb14d55ab0c6c78ff2f"}, + {file = "pydantic-1.7.3-cp37-cp37m-manylinux1_i686.whl", hash = "sha256:5b24e8a572e4b4c18f614004dda8c9f2c07328cb5b6e314d6e1bbd536cb1a6c1"}, + {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_i686.whl", hash = "sha256:b2b054d095b6431cdda2f852a6d2f0fdec77686b305c57961b4c5dd6d863bf3c"}, + {file = "pydantic-1.7.3-cp37-cp37m-manylinux2014_x86_64.whl", hash = "sha256:025bf13ce27990acc059d0c5be46f416fc9b293f45363b3d19855165fee1874f"}, + {file = "pydantic-1.7.3-cp37-cp37m-win_amd64.whl", hash = "sha256:6e3874aa7e8babd37b40c4504e3a94cc2023696ced5a0500949f3347664ff8e2"}, + {file = "pydantic-1.7.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e682f6442ebe4e50cb5e1cfde7dda6766fb586631c3e5569f6aa1951fd1a76ef"}, + {file = "pydantic-1.7.3-cp38-cp38-manylinux1_i686.whl", hash = "sha256:185e18134bec5ef43351149fe34fda4758e53d05bb8ea4d5928f0720997b79ef"}, + {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_i686.whl", hash = "sha256:f5b06f5099e163295b8ff5b1b71132ecf5866cc6e7f586d78d7d3fd6e8084608"}, + {file = "pydantic-1.7.3-cp38-cp38-manylinux2014_x86_64.whl", hash = "sha256:24ca47365be2a5a3cc3f4a26dcc755bcdc9f0036f55dcedbd55663662ba145ec"}, + {file = "pydantic-1.7.3-cp38-cp38-win_amd64.whl", hash = "sha256:d1fe3f0df8ac0f3a9792666c69a7cd70530f329036426d06b4f899c025aca74e"}, + {file = "pydantic-1.7.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f6864844b039805add62ebe8a8c676286340ba0c6d043ae5dea24114b82a319e"}, + {file = "pydantic-1.7.3-cp39-cp39-manylinux1_i686.whl", hash = "sha256:ecb54491f98544c12c66ff3d15e701612fc388161fd455242447083350904730"}, + {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_i686.whl", hash = "sha256:ffd180ebd5dd2a9ac0da4e8b995c9c99e7c74c31f985ba090ee01d681b1c4b95"}, + {file = "pydantic-1.7.3-cp39-cp39-manylinux2014_x86_64.whl", hash = "sha256:8d72e814c7821125b16f1553124d12faba88e85405b0864328899aceaad7282b"}, + {file = "pydantic-1.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:475f2fa134cf272d6631072554f845d0630907fce053926ff634cc6bc45bf1af"}, + {file = "pydantic-1.7.3-py3-none-any.whl", hash = "sha256:38be427ea01a78206bcaf9a56f835784afcba9e5b88fbdce33bbbfbcd7841229"}, + {file = "pydantic-1.7.3.tar.gz", hash = "sha256:213125b7e9e64713d16d988d10997dabc6a1f73f3991e1ff8e35ebb1409c7dc9"}, +] pylint = [ {file = "pylint-2.6.0-py3-none-any.whl", hash = "sha256:bfe68f020f8a0fece830a22dd4d5dddb4ecc6137db04face4c3420a46a52239f"}, {file = "pylint-2.6.0.tar.gz", hash = "sha256:bb4a908c9dadbc3aac18860550e870f58e1a02c9f2c204fdf5693d73be061210"}, diff --git a/pyproject.toml b/pyproject.toml index 554e69c..e86f8eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ classifiers = [ python = "^3.6" requests = "^2.25.1" loguru = "^0.5.3" +pydantic = "^1.7.3" [tool.poetry.dev-dependencies] pytest = "^6.2.1" diff --git a/tests/test_aoe2netapi.py b/tests/test_aoe2netapi.py index 2baf669..b727e9e 100644 --- a/tests/test_aoe2netapi.py +++ b/tests/test_aoe2netapi.py @@ -6,6 +6,14 @@ from aoe2netwrapper.api import AoE2NetAPI, _get_request_response_json from aoe2netwrapper.exceptions import Aoe2NetException +from aoe2netwrapper.models import ( + LastMatchResponse, + LeaderBoardResponse, + MatchLobby, + NumOnlineResponse, + RatingTimePoint, + StringsResponse, +) CURRENT_DIR = pathlib.Path(__file__).parent INPUTS_DIR = CURRENT_DIR / "inputs" @@ -122,7 +130,8 @@ def test_strings_endpoint(self, strings_defaults_payload): ) result = self.client.strings() - assert result == strings_defaults_payload + assert isinstance(result, StringsResponse) + assert result == StringsResponse(**strings_defaults_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == {"game": "aoe2de"} @@ -138,7 +147,8 @@ def test_leaderboard_endpoint_defaults(self, leaderboard_defaults_payload): ) result = self.client.leaderboard() - assert result == leaderboard_defaults_payload + assert isinstance(result, LeaderBoardResponse) + assert result == LeaderBoardResponse(**leaderboard_defaults_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -161,8 +171,10 @@ def test_leaderboard_endpoint_with_search(self, leaderboard_search_payload): status=200, ) + assert isinstance(leaderboard_search_payload, dict) result = self.client.leaderboard(search="GL.TheViper") - assert result == leaderboard_search_payload + assert isinstance(result, LeaderBoardResponse) + assert result == LeaderBoardResponse(**leaderboard_search_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -188,7 +200,8 @@ def test_leaderboard_endpoint_with_steamid(self, leaderboard_steamid_payload): ) result = self.client.leaderboard(steam_id=76561199003184910) - assert result == leaderboard_steamid_payload + assert isinstance(result, LeaderBoardResponse) + assert result == LeaderBoardResponse(**leaderboard_steamid_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -214,7 +227,8 @@ def test_leaderboard_endpoint_with_profileid(self, leaderboard_profileid_payload ) result = self.client.leaderboard(profile_id=459658) - assert result == leaderboard_profileid_payload + assert isinstance(result, LeaderBoardResponse) + assert result == LeaderBoardResponse(**leaderboard_profileid_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -240,7 +254,9 @@ def test_lobbies_endpoint(self, lobbies_defaults_payload): ) result = self.client.lobbies() - assert result == lobbies_defaults_payload + assert isinstance(result, list) + assert isinstance(result[0], MatchLobby) + assert result == [MatchLobby(**lobby) for lobby in lobbies_defaults_payload] assert len(responses.calls) == 1 assert responses.calls[0].request.params == {"game": "aoe2de"} @@ -256,7 +272,8 @@ def test_last_match_endpoint_with_steamid(self, last_match_steamid_payload): ) result = self.client.last_match(steam_id=76561199003184910) - assert result == last_match_steamid_payload + assert isinstance(result, LastMatchResponse) + assert result == LastMatchResponse(**last_match_steamid_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -278,7 +295,8 @@ def test_last_match_endpoint_with_profileid(self, last_match_profileid_payload): ) result = self.client.last_match(profile_id=459658) - assert result == last_match_profileid_payload + assert isinstance(result, LastMatchResponse) + assert result == LastMatchResponse(**last_match_profileid_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -300,7 +318,9 @@ def test_match_history_endpoint_with_steamid(self, match_history_steamid_payload ) result = self.client.match_history(steam_id=76561199003184910) - assert result == match_history_steamid_payload + assert isinstance(result, list) + assert isinstance(result[0], MatchLobby) + assert result == [MatchLobby(**lobby) for lobby in match_history_steamid_payload] assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -325,7 +345,9 @@ def test_match_history_endpoint_with_profileid(self, match_history_profileid_pay ) result = self.client.match_history(profile_id=459658) - assert result == match_history_profileid_payload + assert isinstance(result, list) + assert isinstance(result[0], MatchLobby) + assert result == [MatchLobby(**lobby) for lobby in match_history_profileid_payload] assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -349,20 +371,22 @@ def test_rating_history_endpoint_with_steamid(self, rating_history_steamid_paylo ) result = self.client.rating_history(steam_id=76561199003184910) - assert result == rating_history_steamid_payload + assert isinstance(result, list) + assert isinstance(result[0], RatingTimePoint) + assert result == [RatingTimePoint(**rating) for rating in rating_history_steamid_payload] assert len(responses.calls) == 1 assert responses.calls[0].request.params == { "game": "aoe2de", "leaderboard_id": "3", "start": "0", - "count": "100", + "count": "20", "steam_id": "76561199003184910", } assert ( responses.calls[0].request.url == "https://aoe2.net/api/player/ratinghistory?game=aoe2de&leaderboard_id=3&start=0&" - "count=100&steam_id=76561199003184910" + "count=20&steam_id=76561199003184910" ) @responses.activate @@ -375,20 +399,22 @@ def test_rating_history_endpoint_with_profileid(self, rating_history_profileid_p ) result = self.client.rating_history(profile_id=459658) - assert result == rating_history_profileid_payload + assert isinstance(result, list) + assert isinstance(result[0], RatingTimePoint) + assert result == [RatingTimePoint(**rating) for rating in rating_history_profileid_payload] assert len(responses.calls) == 1 assert responses.calls[0].request.params == { "game": "aoe2de", "leaderboard_id": "3", "start": "0", - "count": "100", + "count": "20", "profile_id": "459658", } assert ( responses.calls[0].request.url == "https://aoe2.net/api/player/ratinghistory?game=aoe2de&leaderboard_id=3&start=0&" - "count=100&profile_id=459658" + "count=20&profile_id=459658" ) @responses.activate @@ -401,7 +427,9 @@ def test_matches_endpoint_defaults(self, matches_defaults_payload): ) result = self.client.matches() - assert result == matches_defaults_payload + assert isinstance(result, list) + assert isinstance(result[0], MatchLobby) + assert result == [MatchLobby(**lobby) for lobby in matches_defaults_payload] assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -420,7 +448,9 @@ def test_matches_endpoint_with_since(self, matches_since_payload): ) result = self.client.matches(since=1596775000) - assert result == matches_since_payload + assert isinstance(result, list) + assert isinstance(result[0], MatchLobby) + assert result == [MatchLobby(**lobby) for lobby in matches_since_payload] assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -443,7 +473,8 @@ def test_match_endpoint_with_uuid(self, match_uuid_payload): ) result = self.client.match(uuid="66ec2575-5ee4-d241-a1fc-d7ffeffb48b6") - assert result == match_uuid_payload + assert isinstance(result, MatchLobby) + assert result == MatchLobby(**match_uuid_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -465,7 +496,8 @@ def test_match_endpoint_with_matchid(self, match_matchid_payload): ) result = self.client.match(match_id=32435313) - assert result == match_matchid_payload + assert isinstance(result, MatchLobby) + assert result == MatchLobby(**match_matchid_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == { @@ -487,7 +519,8 @@ def test_num_online_endpoint(self, num_online_defaults_payload): ) result = self.client.num_online() - assert result == num_online_defaults_payload + assert isinstance(result, NumOnlineResponse) + assert result == NumOnlineResponse(**num_online_defaults_payload) assert len(responses.calls) == 1 assert responses.calls[0].request.params == {"game": "aoe2de"}