diff --git a/Dockerfile b/Dockerfile index d9db039..0c165fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -22,8 +22,9 @@ COPY pyproject.toml app-start.sh favicon.png /code/ RUN poetry config virtualenvs.create false && \ poetry install --only main --no-interaction --no-ansi -# Copy the code +# Copy code and static folders COPY ./overfastapi /code/overfastapi +COPY ./static /code/static # Configure the command CMD ["sh", "/code/app-start.sh"] diff --git a/README.md b/README.md index 4cf050a..0565ed8 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,7 @@ [![License: MIT](https://img.shields.io/github/license/TeKrop/overfast-api)](https://github.com/TeKrop/overfast-api/blob/master/LICENSE) ![Mockup OverFast API](https://files.tekrop.fr/overfast_api_logo_full_1000.png) -> OverFast API gives data about Overwatch 2 heroes, gamemodes, and players statistics by scraping Blizzard pages. Built with **FastAPI** and **Beautiful Soup**, and uses **nginx** as reverse proxy and **Redis** for caching. By using a specific cache system, it minimizes calls to Blizzard pages (which can be very slow), and quickly returns accurate data to users. All duration values are also returned in seconds for convenience. - -## 👷 W.I.P. 👷 - -- Additional data about gamemodes and maps +> OverFast API gives data about Overwatch 2 heroes, gamemodes, maps and players statistics by scraping Blizzard pages. Built with **FastAPI** and **Beautiful Soup**, and uses **nginx** as reverse proxy and **Redis** for caching. By using a specific cache system, it minimizes calls to Blizzard pages (which can be very slow), and quickly returns accurate data to users. All duration values are also returned in seconds for convenience. ## Table of contents * [✨ Live instance](#-live-instance) @@ -22,6 +18,7 @@ * [🐋 Docker](#-docker) * [👨‍💻 Technical details](#-technical-details) * [🤝 Contributing](#-contributing) +* [🙏 Credits](#-credits) * [📝 License](#-license) @@ -44,6 +41,7 @@ Here is the list of all TTL values configured for API Cache : * Hero specific data : 1 day * Roles list : 1 day * Gamemodes list : 1 day +* Maps list : 1 day * Players career : 1 hour * Players search : 1 hour @@ -240,6 +238,14 @@ Contributions, issues and feature requests are welcome! Feel free to check [issues page](https://github.com/TeKrop/overfast-api/issues). + +## 🙏 Credits + +All maps screenshots hosted by the API are owned by Blizzard. Sources : +- Blizzard Press Center (https://blizzard.gamespress.com) +- Overwatch Wiki (https://overwatch.fandom.com/wiki/) + + ## 📝 License Copyright © 2021-2023 [Valentin PORCHET](https://github.com/TeKrop). diff --git a/overfastapi/commands/check_and_update_cache.py b/overfastapi/commands/check_and_update_cache.py index 153e75f..e2c4a6c 100644 --- a/overfastapi/commands/check_and_update_cache.py +++ b/overfastapi/commands/check_and_update_cache.py @@ -11,6 +11,7 @@ from overfastapi.parsers.gamemodes_parser import GamemodesParser from overfastapi.parsers.hero_parser import HeroParser from overfastapi.parsers.heroes_parser import HeroesParser +from overfastapi.parsers.maps_parser import MapsParser from overfastapi.parsers.player_parser import PlayerParser from overfastapi.parsers.player_stats_summary_parser import PlayerStatsSummaryParser from overfastapi.parsers.roles_parser import RolesParser @@ -20,6 +21,7 @@ "GamemodesParser": GamemodesParser, "HeroParser": HeroParser, "HeroesParser": HeroesParser, + "MapsParser": MapsParser, "PlayerParser": PlayerParser, "PlayerStatsSummaryParser": PlayerStatsSummaryParser, "RolesParser": RolesParser, @@ -42,11 +44,15 @@ def get_request_parser_class(cache_key: str) -> tuple[type, dict]: specific_cache_key = cache_key.removeprefix(f"{PARSER_CACHE_KEY_PREFIX}:") parser_class_name = specific_cache_key.split("-")[0] + cache_parser_class = PARSER_CLASSES_MAPPING[parser_class_name] + + # If the cache is related to local data + if BLIZZARD_HOST not in specific_cache_key: + return cache_parser_class, cache_kwargs + uri = specific_cache_key.removeprefix(f"{parser_class_name}-{BLIZZARD_HOST}").split( "/" ) - cache_parser_class = PARSER_CLASSES_MAPPING[parser_class_name] - cache_kwargs["locale"] = uri[1] if parser_class_name in ["PlayerParser", "PlayerStatsSummaryParser"]: cache_kwargs["player_id"] = uri[3] @@ -70,7 +76,7 @@ def main(): parser = parser_class(**kwargs) try: - parser.retrieve_and_parse_blizzard_data() + parser.retrieve_and_parse_data() except ParserBlizzardError as error: logger.error( "Failed to instanciate Parser when refreshing : {}", diff --git a/overfastapi/commands/check_new_hero.py b/overfastapi/commands/check_new_hero.py index e290230..41b7047 100644 --- a/overfastapi/commands/check_new_hero.py +++ b/overfastapi/commands/check_new_hero.py @@ -16,7 +16,7 @@ def get_distant_hero_keys() -> set[str]: heroes_parser = HeroesParser() try: - heroes_parser.retrieve_and_parse_blizzard_data() + heroes_parser.retrieve_and_parse_data() except HTTPException as error: raise SystemExit from error diff --git a/overfastapi/common/enums.py b/overfastapi/common/enums.py index 3341135..38bf2b3 100644 --- a/overfastapi/common/enums.py +++ b/overfastapi/common/enums.py @@ -188,3 +188,17 @@ class Locale(StrEnum): PORTUGUESE_BRAZIL = "pt-br" RUSSIAN = "ru-ru" CHINESE_TAIWAN = "zh-tw" + + +class MapGamemode(StrEnum): + """Maps gamemodes keys""" + + ASSAULT = "assault" + CAPTURE_THE_FLAG = "capture-the-flag" + CONTROL = "control" + DEATHMATCH = "deathmatch" + ELIMINATION = "elimination" + ESCORT = "escort" + HYBRID = "hybrid" + PUSH = "push" + TEAM_DEATHMATCH = "team-deathmatch" diff --git a/overfastapi/common/helpers.py b/overfastapi/common/helpers.py index adf9338..dcee993 100644 --- a/overfastapi/common/helpers.py +++ b/overfastapi/common/helpers.py @@ -1,5 +1,7 @@ """Parser Helpers module""" +import csv import json +from pathlib import Path import requests from fastapi import HTTPException, Request, status @@ -128,3 +130,10 @@ def read_json_file(filepath: str) -> dict | list: f"{TEST_FIXTURES_ROOT_PATH}/json/{filepath}", encoding="utf-8" ) as json_file: return json.load(json_file) + + +def read_csv_data_file(filepath: str) -> csv.DictReader: + with open( + f"{Path.cwd()}/overfastapi/data/{filepath}", encoding="utf-8" + ) as csv_file: + yield from csv.DictReader(csv_file, delimiter=";") diff --git a/overfastapi/data/maps.csv b/overfastapi/data/maps.csv new file mode 100644 index 0000000..1ae6e10 --- /dev/null +++ b/overfastapi/data/maps.csv @@ -0,0 +1,38 @@ +key;name;gamemodes;location;country_code +hanamura;Hanamura;assault;Tokyo, Japan;JP +horizon;Horizon Lunar Colony;assault;Earth's moon; +paris;Paris;assault;Paris, France;FR +anubis;Temple of Anubis;assault;Giza Plateau, Egypt;EG +volskaya;Volskaya Industries;assault;St. Petersburg, Russia;RU +ayutthaya;Ayutthaya;capture-the-flag;Thailand;TH +busan;Busan;control;South Korea;KR +nepal;Nepal;control;Nepal;NP +ilios;Ilios;control;Greece;GR +oasis;Oasis;control;Iraq;IQ +lijiang;Lijiang Tower;control;China;CN +chateau_guillard;Château Guillard;deathmatch,team-deathmatch;Annecy, France;FR +kanezaka;Kanezaka;deathmatch,team-deathmatch;Tokyo, Japan;JP +malevento;Malevento;deathmatch,team-deathmatch;Italy;IT +petra;Petra;deathmatch,team-deathmatch;Southern Jordan;JO +black_forest;Black Forest;elimination;Germany;DE +castillo;Castillo;elimination;Mexico;MX +ecopoint_antarctica;Ecopoint: Antarctica;elimination;Antarctica;AQ +necropolis;Necropolis;elimination;Egypt;EG +circuit_royal;Circuit Royal;escort;Monte Carlo, Monaco;MC +dorado;Dorado;escort;Mexico;MX +route_66;Route 66;escort;Albuquerque, New Mexico, United States;US +junkertown;Junkertown;escort;Central Australia;AU +rialto;Rialto;escort;Venice, Italy;IT +havana;Havana;escort;Havana, Cuba;CU +gibraltar;Watchpoint: Gibraltar;escort;Gibraltar;GI +shambali;Shambali Monastery;escort;Nepal;NP +blizzard_world;Blizzard World;hybrid;Irvine, California, United States;US +numbani;Numbani;hybrid;Numbani (near Nigeria); +hollywood;Hollywood;hybrid;Los Angeles, United States;US +eichenwalde;Eichenwalde;hybrid;Stuttgart, Germany;DE +kings_row;King’s Row;hybrid;London, United Kingdom;UK +midtown;Midtown;hybrid;New York, United States;US +paraiso;Paraíso;hybrid;Rio de Janeiro, Brazil;BR +colosseo;Colosseo;push;Rome, Italy;IT +esperanca;Esperança;push;Portugal;PT +new_queen_street;New Queen Street;push;Toronto, Canada;CA diff --git a/overfastapi/handlers/api_request_handler.py b/overfastapi/handlers/api_request_handler.py index 3822873..81fb120 100644 --- a/overfastapi/handlers/api_request_handler.py +++ b/overfastapi/handlers/api_request_handler.py @@ -23,11 +23,6 @@ class APIRequestHandler(ApiRequestMixin, ABC): def __init__(self, request: Request): self.cache_key = CacheManager.get_cache_key_from_request(request) - @property - @abstractmethod - def api_root_url(self) -> str: - """Root URL used for this specific handler (/players, /heroes, etc.)""" - @property @abstractmethod def parser_classes(self) -> type: diff --git a/overfastapi/handlers/get_hero_request_handler.py b/overfastapi/handlers/get_hero_request_handler.py index c1fb952..14b25e2 100644 --- a/overfastapi/handlers/get_hero_request_handler.py +++ b/overfastapi/handlers/get_hero_request_handler.py @@ -11,7 +11,6 @@ class GetHeroRequestHandler(APIRequestHandler): should be used to display data about a specific hero. """ - api_root_url = "/heroes" parser_classes = [HeroParser, HeroesParser] timeout = HERO_PATH_CACHE_TIMEOUT diff --git a/overfastapi/handlers/get_player_career_request_handler.py b/overfastapi/handlers/get_player_career_request_handler.py index a265a72..9a895a0 100644 --- a/overfastapi/handlers/get_player_career_request_handler.py +++ b/overfastapi/handlers/get_player_career_request_handler.py @@ -10,6 +10,5 @@ class GetPlayerCareerRequestHandler(APIRequestHandler): PlayerParser class. """ - api_root_url = "/players" parser_classes = [PlayerParser] timeout = CAREER_PATH_CACHE_TIMEOUT diff --git a/overfastapi/handlers/list_gamemodes_request_handler.py b/overfastapi/handlers/list_gamemodes_request_handler.py index 96d721c..fbaef04 100644 --- a/overfastapi/handlers/list_gamemodes_request_handler.py +++ b/overfastapi/handlers/list_gamemodes_request_handler.py @@ -9,6 +9,5 @@ class ListGamemodesRequestHandler(APIRequestHandler): available Overwatch gamemodes, using the GamemodesParser class. """ - api_root_url = "/gamemodes" parser_classes = [GamemodesParser] timeout = HOME_PATH_CACHE_TIMEOUT diff --git a/overfastapi/handlers/list_heroes_request_handler.py b/overfastapi/handlers/list_heroes_request_handler.py index c73adbd..78c9dc3 100644 --- a/overfastapi/handlers/list_heroes_request_handler.py +++ b/overfastapi/handlers/list_heroes_request_handler.py @@ -9,6 +9,5 @@ class ListHeroesRequestHandler(APIRequestHandler): retrieve a list of available Overwatch heroes. """ - api_root_url = "/heroes" parser_classes = [HeroesParser] timeout = HEROES_PATH_CACHE_TIMEOUT diff --git a/overfastapi/handlers/list_maps_request_handler.py b/overfastapi/handlers/list_maps_request_handler.py new file mode 100644 index 0000000..add620f --- /dev/null +++ b/overfastapi/handlers/list_maps_request_handler.py @@ -0,0 +1,13 @@ +"""List Maps Request Handler module""" +from overfastapi.config import HOME_PATH_CACHE_TIMEOUT +from overfastapi.handlers.api_request_handler import APIRequestHandler +from overfastapi.parsers.maps_parser import MapsParser + + +class ListMapsRequestHandler(APIRequestHandler): + """List Maps Request Handler used in order to retrieve a list of + available Overwatch maps, using the MapsParser class. + """ + + parser_classes = [MapsParser] + timeout = HOME_PATH_CACHE_TIMEOUT diff --git a/overfastapi/handlers/list_roles_request_handler.py b/overfastapi/handlers/list_roles_request_handler.py index 6f29722..8799a3a 100644 --- a/overfastapi/handlers/list_roles_request_handler.py +++ b/overfastapi/handlers/list_roles_request_handler.py @@ -9,6 +9,5 @@ class ListRolesRequestHandler(APIRequestHandler): retrieve a list of available Overwatch roles. """ - api_root_url = "/roles" parser_classes = [RolesParser] timeout = HEROES_PATH_CACHE_TIMEOUT diff --git a/overfastapi/main.py b/overfastapi/main.py index b17cec3..d41c0cc 100644 --- a/overfastapi/main.py +++ b/overfastapi/main.py @@ -4,28 +4,24 @@ from fastapi.openapi.docs import get_redoc_html from fastapi.openapi.utils import get_openapi from fastapi.responses import JSONResponse +from fastapi.staticfiles import StaticFiles from starlette.exceptions import HTTPException as StarletteHTTPException from overfastapi.common.enums import RouteTag from overfastapi.common.logging import logger from overfastapi.config import OVERFAST_API_VERSION -from overfastapi.routers import gamemodes, heroes, players, roles +from overfastapi.routers import gamemodes, heroes, maps, players, roles app = FastAPI( title="OverFast API", docs_url=None, redoc_url=None, ) -description = """OverFast API gives data about Overwatch 2 heroes, gamemodes, and players +description = """OverFast API gives data about Overwatch 2 heroes, gamemodes, maps and players statistics by scraping Blizzard pages. Built with **FastAPI** and **Beautiful Soup**, and uses **nginx** as reverse proxy and **Redis** for caching. By using a Refresh-Ahead cache system, it minimizes calls to Blizzard pages (which can be very slow), and quickly returns accurate -data to users. All duration values are also returned in seconds for convenience. - -## 👷 W.I.P. 👷 - -- Additional data about gamemodes and maps -""" +data to users. All duration values are also returned in seconds for convenience.""" def custom_openapi(): # pragma: no cover @@ -63,6 +59,10 @@ def custom_openapi(): # pragma: no cover "url": "https://overwatch.blizzard.com/en-us/", }, }, + { + "name": RouteTag.MAPS, + "description": "Overwatch maps details", + }, { "name": RouteTag.PLAYERS, "description": "Overwatch players data : summary, statistics, etc.", @@ -83,6 +83,8 @@ def custom_openapi(): # pragma: no cover app.openapi = custom_openapi +app.mount("/static", StaticFiles(directory="static"), name="static") + app.logger = logger logger.info("OverFast API... Online !") logger.info("Version : {}", OVERFAST_API_VERSION) @@ -108,4 +110,5 @@ def overridden_redoc(): app.include_router(heroes.router, prefix="/heroes") app.include_router(roles.router, prefix="/roles") app.include_router(gamemodes.router, prefix="/gamemodes") +app.include_router(maps.router, prefix="/maps") app.include_router(players.router, prefix="/players") diff --git a/overfastapi/models/gamemodes.py b/overfastapi/models/gamemodes.py index 1007af1..3aab11b 100644 --- a/overfastapi/models/gamemodes.py +++ b/overfastapi/models/gamemodes.py @@ -1,8 +1,18 @@ """Set of pydantic models used for Gamemodes API routes""" from pydantic import BaseModel, Field, HttpUrl +from overfastapi.common.enums import MapGamemode + class GamemodeDetails(BaseModel): + key: MapGamemode = Field( + ..., + description=( + "Key corresponding to the gamemode. Can be " + "used as filter on the maps endpoint." + ), + example="push", + ) name: str = Field(..., description="Name of the gamemode", example="Push") icon: HttpUrl = Field( ..., diff --git a/overfastapi/models/maps.py b/overfastapi/models/maps.py new file mode 100644 index 0000000..d8a6f02 --- /dev/null +++ b/overfastapi/models/maps.py @@ -0,0 +1,28 @@ +"""Set of pydantic models used for Maps API routes""" +from pydantic import BaseModel, Field, HttpUrl + +from overfastapi.common.enums import MapGamemode + + +class Map(BaseModel): + name: str = Field(..., description="Name of the map", example="Hanamura") + screenshot: HttpUrl = Field( + ..., + description="Screenshot of the map", + example="https://overfast-api.tekrop.fr/static/maps/hanamura.jpg", + ) + gamemodes: list[MapGamemode] = Field( + ..., description="Main gamemodes on which the map is playable" + ) + location: str = Field( + ..., description="Location of the map", example="Tokyo, Japan" + ) + country_code: str | None = Field( + None, + min_length=2, + max_length=2, + description=( + "Country Code of the location of the map. If not defined, it's null." + ), + example="JP", + ) diff --git a/overfastapi/parsers/abstract_parser.py b/overfastapi/parsers/abstract_parser.py new file mode 100644 index 0000000..77c60f6 --- /dev/null +++ b/overfastapi/parsers/abstract_parser.py @@ -0,0 +1,60 @@ +"""Abstract API Parser module""" +from abc import ABC, abstractmethod +from functools import cached_property + +from overfastapi.common.cache_manager import CacheManager +from overfastapi.common.logging import logger + + +class AbstractParser(ABC): + """Abstract Parser class used to define generic behavior for parsers. + + A parser is meant to convert some input data into meaningful data + in dict/list format. The Parse Cache system is handled here. + """ + + cache_manager = CacheManager() + + def __init__(self, **kwargs): + self.data = None + + @property + @abstractmethod + def timeout(self) -> int: + """Timeout used for Parser Cache storage for this specific parser""" + + @cached_property + def cache_key(self) -> str: + """Key used for caching using Parser Cache""" + return type(self).__name__ + + @abstractmethod + def retrieve_and_parse_data(self) -> None: + """Method used to retrieve data, parsing it and + storing it into self.data attribute. + """ + + def parse(self) -> None: + """Main parsing method, first checking if there is any Parser Cache. If + not, it's calling the main submethod to retrieve and parse data. + """ + logger.info("Checking Parser Cache...") + parser_cache = self.cache_manager.get_parser_cache(self.cache_key) + if parser_cache is not None: + # Parser cache is here + logger.info("Parser Cache found !") + self.data = parser_cache + return + + # No cache is available, it's the first time the user requested the + # data or the Parser Cache has expired : retrieve and parse data ( + # Blizzard page for API Parser or local file for others parsers) + self.retrieve_and_parse_data() + + def filter_request_using_query(self, **kwargs) -> dict | list: + """If the route contains subroutes accessible using GET queries, this method + will filter data using the query data. This method should be + redefined in child classes if needed. The default behaviour is to return + the parsed data directly. + """ + return self.data diff --git a/overfastapi/parsers/api_parser.py b/overfastapi/parsers/api_parser.py index d35d7a2..ae0d502 100644 --- a/overfastapi/parsers/api_parser.py +++ b/overfastapi/parsers/api_parser.py @@ -1,47 +1,36 @@ """Abstract API Parser module""" -from abc import ABC, abstractmethod +from abc import abstractmethod from functools import cached_property from bs4 import BeautifulSoup -from overfastapi.common.cache_manager import CacheManager from overfastapi.common.enums import Locale from overfastapi.common.exceptions import ParserParsingError from overfastapi.common.helpers import ( blizzard_response_error_from_request, overfast_request, ) -from overfastapi.common.logging import logger from overfastapi.config import BLIZZARD_HOST +from overfastapi.parsers.abstract_parser import AbstractParser -class APIParser(ABC): - """Abstract Parser class used to define generic behavior for parsers. - - A parser is meant to convert Blizzard HTML page data into - meaningful data in dict/list format. The blizzard URL - call, and the API Cache system are handled here. +class APIParser(AbstractParser): + """Abstract API Parser class used to define generic behavior for parsers used + to extract data from Blizzard HTML pages. The blizzard URL call is handled here. """ - cache_manager = CacheManager() - # List of valid HTTP codes when retrieving Blizzard pages valid_http_codes = [200] def __init__(self, **kwargs): self.blizzard_url = self.get_blizzard_url(**kwargs) - self.data = None + super().__init__(**kwargs) @property @abstractmethod def root_path(self) -> str: """Root path of the Blizzard URL containing the data (/en-us/career/, etc.""" - @property - @abstractmethod - def timeout(self) -> int: - """Timeout used for Parser Cache storage for this specific parser""" - @cached_property def cache_key(self) -> str: """Key used for caching using Parser Cache. Blizzard URL @@ -57,24 +46,7 @@ def root_tag_params(self) -> dict: """ return {"name": "div", "class_": "main-content", "recursive": False} - def parse(self) -> None: - """Main parsing method, first checking if there is any Parser Cache. If - not, it's calling the main submethod and catching BeautifulSoup exceptions. - If there is any, a ParserParsingError is raised. - """ - logger.info("Checking Parser Cache...") - parser_cache = self.cache_manager.get_parser_cache(self.cache_key) - if parser_cache is not None: - # Parser cache is here - logger.info("Parser Cache found !") - self.data = parser_cache - return - - # No cache is available, it's the first time the user requested the - # data or the Parser Cache has expired : retrieve and parse Blizzard page - self.retrieve_and_parse_blizzard_data() - - def retrieve_and_parse_blizzard_data(self) -> None: + def retrieve_and_parse_data(self) -> None: """Method used to retrieve data from Blizzard (HTML data), parsing it and storing it into self.data attribute. """ @@ -111,11 +83,3 @@ def get_blizzard_url(self, **kwargs) -> str: """ locale = kwargs.get("locale") or Locale.ENGLISH_US return f"{BLIZZARD_HOST}/{locale}{self.root_path}" - - def filter_request_using_query(self, **kwargs) -> dict | list: - """If the route contains subroutes accessible using GET queries, this method - will filter Blizzard data using the query data. This method should be - redefined in child classes if needed. The default behaviour is to return - the parsed data directly. - """ - return self.data diff --git a/overfastapi/parsers/gamemodes_parser.py b/overfastapi/parsers/gamemodes_parser.py index 53be6a6..ce6c871 100644 --- a/overfastapi/parsers/gamemodes_parser.py +++ b/overfastapi/parsers/gamemodes_parser.py @@ -18,6 +18,7 @@ def parse_data(self) -> list[dict]: gamemodes_extras = [ { + "key": feature_div["label"], "description": ( feature_div.find("blz-header") .find("div", slot="description") @@ -31,6 +32,7 @@ def parse_data(self) -> list[dict]: return [ { + "key": gamemodes_extras[gamemode_index]["key"], "name": gamemode_div.get_text(), "icon": gamemode_div.find("blz-image")["src:min-plus"], "description": gamemodes_extras[gamemode_index]["description"], diff --git a/overfastapi/parsers/maps_parser.py b/overfastapi/parsers/maps_parser.py new file mode 100644 index 0000000..21b8062 --- /dev/null +++ b/overfastapi/parsers/maps_parser.py @@ -0,0 +1,40 @@ +"""Maps Parser module""" +from overfastapi.common.helpers import read_csv_data_file +from overfastapi.config import HOME_PATH_CACHE_TIMEOUT, OVERFAST_API_BASE_URL +from overfastapi.parsers.abstract_parser import AbstractParser + + +class MapsParser(AbstractParser): + """Overwatch maps list page Parser class""" + + timeout = HOME_PATH_CACHE_TIMEOUT + + def retrieve_and_parse_data(self) -> None: + maps_data = read_csv_data_file("maps.csv") + + self.data = [ + { + "name": map_dict["name"], + "screenshot": self.get_screenshot_url(map_dict["key"]), + "gamemodes": map_dict["gamemodes"].split(","), + "location": map_dict["location"], + "country_code": map_dict["country_code"] or None, + } + for map_dict in maps_data + ] + + # Update the Parser Cache + self.cache_manager.update_parser_cache(self.cache_key, self.data, self.timeout) + + def filter_request_using_query(self, **kwargs) -> list: + gamemode = kwargs.get("gamemode") + return ( + self.data + if not gamemode + else [ + map_dict for map_dict in self.data if gamemode in map_dict["gamemodes"] + ] + ) + + def get_screenshot_url(self, map_key: str) -> str: + return f"{OVERFAST_API_BASE_URL}/static/maps/{map_key}.jpg" diff --git a/overfastapi/routers/maps.py b/overfastapi/routers/maps.py new file mode 100644 index 0000000..5469641 --- /dev/null +++ b/overfastapi/routers/maps.py @@ -0,0 +1,34 @@ +"""Maps endpoints router : maps list, etc.""" +from fastapi import APIRouter, BackgroundTasks, Query, Request + +from overfastapi.common.decorators import validation_error_handler +from overfastapi.common.enums import MapGamemode, RouteTag +from overfastapi.handlers.list_maps_request_handler import ListMapsRequestHandler +from overfastapi.models.maps import Map + +router = APIRouter() + + +@router.get( + "", + tags=[RouteTag.MAPS], + summary="Get a list of maps", + description=( + "Get a list of Overwatch maps : Hanamura, King's Row, Dorado, etc." + "
**Cache TTL : 1 day.**" + ), +) +@validation_error_handler(response_model=Map) +async def list_maps( + background_tasks: BackgroundTasks, + request: Request, + gamemode: MapGamemode + | None = Query( + None, + title="Gamemode filter", + description="Filter maps available for a specific gamemode", + ), +) -> list[Map]: + return ListMapsRequestHandler(request).process_request( + background_tasks=background_tasks, gamemode=gamemode + ) diff --git a/pyproject.toml b/pyproject.toml index 85e6d62..282efb7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "overfast-api" -version = "2.6.0" +version = "2.7.0" description = "Overwatch API giving data about heroes, maps, and players statistics." authors = ["TeKrop "] license = "MIT" diff --git a/static/maps/anubis.jpg b/static/maps/anubis.jpg new file mode 100644 index 0000000..fae5f93 Binary files /dev/null and b/static/maps/anubis.jpg differ diff --git a/static/maps/ayutthaya.jpg b/static/maps/ayutthaya.jpg new file mode 100644 index 0000000..8f32df4 Binary files /dev/null and b/static/maps/ayutthaya.jpg differ diff --git a/static/maps/black_forest.jpg b/static/maps/black_forest.jpg new file mode 100644 index 0000000..bbcdd67 Binary files /dev/null and b/static/maps/black_forest.jpg differ diff --git a/static/maps/blizzard_world.jpg b/static/maps/blizzard_world.jpg new file mode 100644 index 0000000..7f78ddc Binary files /dev/null and b/static/maps/blizzard_world.jpg differ diff --git a/static/maps/busan.jpg b/static/maps/busan.jpg new file mode 100644 index 0000000..f86cda0 Binary files /dev/null and b/static/maps/busan.jpg differ diff --git a/static/maps/castillo.jpg b/static/maps/castillo.jpg new file mode 100644 index 0000000..6ca2a1c Binary files /dev/null and b/static/maps/castillo.jpg differ diff --git a/static/maps/chateau_guillard.jpg b/static/maps/chateau_guillard.jpg new file mode 100644 index 0000000..f8829da Binary files /dev/null and b/static/maps/chateau_guillard.jpg differ diff --git a/static/maps/circuit_royal.jpg b/static/maps/circuit_royal.jpg new file mode 100644 index 0000000..c4a5eb9 Binary files /dev/null and b/static/maps/circuit_royal.jpg differ diff --git a/static/maps/colosseo.jpg b/static/maps/colosseo.jpg new file mode 100644 index 0000000..2697640 Binary files /dev/null and b/static/maps/colosseo.jpg differ diff --git a/static/maps/dorado.jpg b/static/maps/dorado.jpg new file mode 100644 index 0000000..067f286 Binary files /dev/null and b/static/maps/dorado.jpg differ diff --git a/static/maps/ecopoint_antarctica.jpg b/static/maps/ecopoint_antarctica.jpg new file mode 100644 index 0000000..31dde40 Binary files /dev/null and b/static/maps/ecopoint_antarctica.jpg differ diff --git a/static/maps/eichenwalde.jpg b/static/maps/eichenwalde.jpg new file mode 100644 index 0000000..207d8d3 Binary files /dev/null and b/static/maps/eichenwalde.jpg differ diff --git a/static/maps/esperanca.jpg b/static/maps/esperanca.jpg new file mode 100644 index 0000000..e14a0dd Binary files /dev/null and b/static/maps/esperanca.jpg differ diff --git a/static/maps/gibraltar.jpg b/static/maps/gibraltar.jpg new file mode 100644 index 0000000..5791053 Binary files /dev/null and b/static/maps/gibraltar.jpg differ diff --git a/static/maps/hanamura.jpg b/static/maps/hanamura.jpg new file mode 100644 index 0000000..5df879d Binary files /dev/null and b/static/maps/hanamura.jpg differ diff --git a/static/maps/havana.jpg b/static/maps/havana.jpg new file mode 100644 index 0000000..43e9729 Binary files /dev/null and b/static/maps/havana.jpg differ diff --git a/static/maps/hollywood.jpg b/static/maps/hollywood.jpg new file mode 100644 index 0000000..0491fe8 Binary files /dev/null and b/static/maps/hollywood.jpg differ diff --git a/static/maps/horizon.jpg b/static/maps/horizon.jpg new file mode 100644 index 0000000..8282b28 Binary files /dev/null and b/static/maps/horizon.jpg differ diff --git a/static/maps/ilios.jpg b/static/maps/ilios.jpg new file mode 100644 index 0000000..302a3d8 Binary files /dev/null and b/static/maps/ilios.jpg differ diff --git a/static/maps/junkertown.jpg b/static/maps/junkertown.jpg new file mode 100644 index 0000000..a21676d Binary files /dev/null and b/static/maps/junkertown.jpg differ diff --git a/static/maps/kanezaka.jpg b/static/maps/kanezaka.jpg new file mode 100644 index 0000000..ae6caf7 Binary files /dev/null and b/static/maps/kanezaka.jpg differ diff --git a/static/maps/kings_row.jpg b/static/maps/kings_row.jpg new file mode 100644 index 0000000..1eb9a68 Binary files /dev/null and b/static/maps/kings_row.jpg differ diff --git a/static/maps/lijiang.jpg b/static/maps/lijiang.jpg new file mode 100644 index 0000000..79d3f46 Binary files /dev/null and b/static/maps/lijiang.jpg differ diff --git a/static/maps/malevento.jpg b/static/maps/malevento.jpg new file mode 100644 index 0000000..ef7d4b4 Binary files /dev/null and b/static/maps/malevento.jpg differ diff --git a/static/maps/midtown.jpg b/static/maps/midtown.jpg new file mode 100644 index 0000000..4c122a2 Binary files /dev/null and b/static/maps/midtown.jpg differ diff --git a/static/maps/necropolis.jpg b/static/maps/necropolis.jpg new file mode 100644 index 0000000..00377da Binary files /dev/null and b/static/maps/necropolis.jpg differ diff --git a/static/maps/nepal.jpg b/static/maps/nepal.jpg new file mode 100644 index 0000000..c8befd3 Binary files /dev/null and b/static/maps/nepal.jpg differ diff --git a/static/maps/new_queen_street.jpg b/static/maps/new_queen_street.jpg new file mode 100644 index 0000000..822f416 Binary files /dev/null and b/static/maps/new_queen_street.jpg differ diff --git a/static/maps/numbani.jpg b/static/maps/numbani.jpg new file mode 100644 index 0000000..8ca1d7a Binary files /dev/null and b/static/maps/numbani.jpg differ diff --git a/static/maps/oasis.jpg b/static/maps/oasis.jpg new file mode 100644 index 0000000..0e0c9ae Binary files /dev/null and b/static/maps/oasis.jpg differ diff --git a/static/maps/paraiso.jpg b/static/maps/paraiso.jpg new file mode 100644 index 0000000..31fc16d Binary files /dev/null and b/static/maps/paraiso.jpg differ diff --git a/static/maps/paris.jpg b/static/maps/paris.jpg new file mode 100644 index 0000000..e2785d7 Binary files /dev/null and b/static/maps/paris.jpg differ diff --git a/static/maps/petra.jpg b/static/maps/petra.jpg new file mode 100644 index 0000000..fe020c2 Binary files /dev/null and b/static/maps/petra.jpg differ diff --git a/static/maps/rialto.jpg b/static/maps/rialto.jpg new file mode 100644 index 0000000..af0e3ad Binary files /dev/null and b/static/maps/rialto.jpg differ diff --git a/static/maps/route_66.jpg b/static/maps/route_66.jpg new file mode 100644 index 0000000..c7a008f Binary files /dev/null and b/static/maps/route_66.jpg differ diff --git a/static/maps/shambali.jpg b/static/maps/shambali.jpg new file mode 100644 index 0000000..3f4491c Binary files /dev/null and b/static/maps/shambali.jpg differ diff --git a/static/maps/volskaya.jpg b/static/maps/volskaya.jpg new file mode 100644 index 0000000..e876ba9 Binary files /dev/null and b/static/maps/volskaya.jpg differ diff --git a/tests/commands/test_check_and_update_cache.py b/tests/commands/test_check_and_update_cache.py index 7b209a0..c412204 100644 --- a/tests/commands/test_check_and_update_cache.py +++ b/tests/commands/test_check_and_update_cache.py @@ -109,6 +109,32 @@ def test_check_and_update_specific_hero_to_update( assert cache_manager.get_parser_cache(ana_cache_key) == hero_data +def test_check_and_update_maps_to_update( + cache_manager: CacheManager, maps_json_data: dict +): + cache_key = "MapsParser" + complete_cache_key = f"{PARSER_CACHE_KEY_PREFIX}:{cache_key}" + + # Add some data (to update and not to update) + cache_manager.update_parser_cache(cache_key, [], EXPIRED_CACHE_REFRESH_LIMIT - 5) + + # Check data in db (assert no Parser Cache data) + assert cache_manager.get_parser_cache(cache_key) == [] + assert get_soon_expired_cache_keys() == {complete_cache_key} + + # check and update (only maps should be updated) + logger_info_mock = Mock() + + with patch("overfastapi.common.logging.logger.info", logger_info_mock): + check_and_update_cache_main() + + # Check data in db (assert we created API Cache for subroutes) + logger_info_mock.assert_any_call("Done ! Retrieved keys : {}", 1) + logger_info_mock.assert_any_call("Updating data for {} key...", complete_cache_key) + + assert cache_manager.get_parser_cache(cache_key) == maps_json_data + + def test_check_and_update_cache_no_update(cache_manager: CacheManager, locale: str): # Add some data (to update and not to update) cache_manager.update_parser_cache( diff --git a/tests/conftest.py b/tests/conftest.py index 863336e..0903bd6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,3 +83,8 @@ def search_players_blizzard_json_data(): @pytest.fixture(scope="session") def search_players_api_json_data(): return read_json_file("search_players/search_players_api_result.json") + + +@pytest.fixture(scope="session") +def maps_json_data(): + return read_json_file("maps.json") diff --git a/tests/fixtures/json/gamemodes.json b/tests/fixtures/json/gamemodes.json index b2f782b..b807ed5 100644 --- a/tests/fixtures/json/gamemodes.json +++ b/tests/fixtures/json/gamemodes.json @@ -1,50 +1 @@ -[ - { - "name": "Push", - "icon": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt054b513cd6e95acf/62fd5b4a8972f93d1e325243/Push.svg", - "description": "Teams battle to take control of a robot and push it toward the enemy base.", - "screenshot": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt93eefb6e91347639/62fc2d9eda42240856c1459c/Toronto_Push.jpg" - }, - { - "name": "Control", - "icon": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt5ea0d3baf0e2d03f/62fc2d8bda42240856c14598/Control.svg", - "description": "Teams fight to hold a single objective. The first team to win two rounds wins the map.", - "screenshot": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blta3f8e647a52bb9e9/62fc312456388515882767ed/Oasis_Control.jpg" - }, - { - "name": "Escort", - "icon": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/bltba08041a1eb32c43/62fc2d8bc317e303559ab5b1/Escort.svg", - "description": "One team escorts a payload to its delivery point, while the other races to stop them.", - "screenshot": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt7d04bb6ad434cafa/62fc332a7ea1d970140fa139/Monte_Carlo_Escort.jpg" - }, - { - "name": "Hybrid", - "icon": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/bltd55839866311dd1e/62fc2d8bd62b1d3a8d1e5318/Hybrid.svg", - "description": "Attackers capture a payload, then escort it to its destination; defenders try to hold them back.", - "screenshot": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt253b3f05c32db72b/62fc2d9e947fcf14cd224b18/Rio_de_Janeiro_Hybrid.jpg" - }, - { - "name": "Capture the Flag", - "icon": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt81ca1f07a1cf5a6e/62fc2d8bc8d34f7f53197dc6/Capture_the_Flag.svg", - "description": "Teams compete to capture the enemy team’s flag while defending their own.", - "screenshot": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt42d587e346415db3/62fc2d9e8282cd7f515e855b/Lijang_Tower_Capture_the_Flag.jpg" - }, - { - "name": "Elimination", - "icon": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt4a8d26b9e2ae0739/62fc2d8b7198180857a9e75d/Elimination.svg", - "description": "Dispatch all enemies to win the round. Win three rounds to claim victory. Available with teams of one, three, or six.", - "screenshot": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt4ccc6acd0dbed78a/62fc2d9eb60eb7158600e04c/Black_Forest_Elimination.jpg" - }, - { - "name": "Deathmatch", - "icon": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt9b57125d24705a16/62fc2d8b8972f93d1e32520e/Deathmatch.svg", - "description": "Race to reach 20 points first by racking up kills in a free-for-all format.", - "screenshot": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt88ad6ddb9de11b3e/62fc2d9e26d7fa3ee92d03e2/Eichenwalde_Deathmatch.png" - }, - { - "name": "Team Deathmatch", - "icon": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blta97d85f157469bd5/62fc2d8b3a2b8b6d5939b85a/Team_Deathmatch.svg", - "description": "Team up and triumph over your enemies by scoring the most kills.", - "screenshot": "https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt63df614431a3f554/62fc2d9ead1dcd16f35de8e9/Kanezaka_Team_Deathmatch.jpg" - } -] \ No newline at end of file +[{"key":"push","name":"Push","icon":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt054b513cd6e95acf/62fd5b4a8972f93d1e325243/Push.svg","description":"Teams battle to take control of a robot and push it toward the enemy base.","screenshot":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt93eefb6e91347639/62fc2d9eda42240856c1459c/Toronto_Push.jpg"},{"key":"control","name":"Control","icon":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt5ea0d3baf0e2d03f/62fc2d8bda42240856c14598/Control.svg","description":"Teams fight to hold a single objective. The first team to win two rounds wins the map.","screenshot":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blta3f8e647a52bb9e9/62fc312456388515882767ed/Oasis_Control.jpg"},{"key":"escort","name":"Escort","icon":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/bltba08041a1eb32c43/62fc2d8bc317e303559ab5b1/Escort.svg","description":"One team escorts a payload to its delivery point, while the other races to stop them.","screenshot":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt7d04bb6ad434cafa/62fc332a7ea1d970140fa139/Monte_Carlo_Escort.jpg"},{"key":"hybrid","name":"Hybrid","icon":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/bltd55839866311dd1e/62fc2d8bd62b1d3a8d1e5318/Hybrid.svg","description":"Attackers capture a payload, then escort it to its destination; defenders try to hold them back.","screenshot":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt253b3f05c32db72b/62fc2d9e947fcf14cd224b18/Rio_de_Janeiro_Hybrid.jpg"},{"key":"capture-the-flag","name":"Capture the Flag","icon":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt81ca1f07a1cf5a6e/62fc2d8bc8d34f7f53197dc6/Capture_the_Flag.svg","description":"Teams compete to capture the enemy team’s flag while defending their own.","screenshot":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt42d587e346415db3/62fc2d9e8282cd7f515e855b/Lijang_Tower_Capture_the_Flag.jpg"},{"key":"elimination","name":"Elimination","icon":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt4a8d26b9e2ae0739/62fc2d8b7198180857a9e75d/Elimination.svg","description":"Dispatch all enemies to win the round. Win three rounds to claim victory. Available with teams of one, three, or six.","screenshot":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt4ccc6acd0dbed78a/62fc2d9eb60eb7158600e04c/Black_Forest_Elimination.jpg"},{"key":"deathmatch","name":"Deathmatch","icon":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt9b57125d24705a16/62fc2d8b8972f93d1e32520e/Deathmatch.svg","description":"Race to reach 20 points first by racking up kills in a free-for-all format.","screenshot":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt88ad6ddb9de11b3e/62fc2d9e26d7fa3ee92d03e2/Eichenwalde_Deathmatch.png"},{"key":"team-deathmatch","name":"Team Deathmatch","icon":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blta97d85f157469bd5/62fc2d8b3a2b8b6d5939b85a/Team_Deathmatch.svg","description":"Team up and triumph over your enemies by scoring the most kills.","screenshot":"https://blz-contentstack-images.akamaized.net/v3/assets/blt9c12f249ac15c7ec/blt63df614431a3f554/62fc2d9ead1dcd16f35de8e9/Kanezaka_Team_Deathmatch.jpg"}] \ No newline at end of file diff --git a/tests/fixtures/json/maps.json b/tests/fixtures/json/maps.json new file mode 100644 index 0000000..650f1f4 --- /dev/null +++ b/tests/fixtures/json/maps.json @@ -0,0 +1 @@ +[{"name":"Hanamura","screenshot":"https://overfast-api.tekrop.fr/static/maps/hanamura.jpg","gamemodes":["assault"],"location":"Tokyo, Japan","country_code":"JP"},{"name":"Horizon Lunar Colony","screenshot":"https://overfast-api.tekrop.fr/static/maps/horizon.jpg","gamemodes":["assault"],"location":"Earth's moon","country_code":null},{"name":"Paris","screenshot":"https://overfast-api.tekrop.fr/static/maps/paris.jpg","gamemodes":["assault"],"location":"Paris, France","country_code":"FR"},{"name":"Temple of Anubis","screenshot":"https://overfast-api.tekrop.fr/static/maps/anubis.jpg","gamemodes":["assault"],"location":"Giza Plateau, Egypt","country_code":"EG"},{"name":"Volskaya Industries","screenshot":"https://overfast-api.tekrop.fr/static/maps/volskaya.jpg","gamemodes":["assault"],"location":"St. Petersburg, Russia","country_code":"RU"},{"name":"Ayutthaya","screenshot":"https://overfast-api.tekrop.fr/static/maps/ayutthaya.jpg","gamemodes":["capture-the-flag"],"location":"Thailand","country_code":"TH"},{"name":"Busan","screenshot":"https://overfast-api.tekrop.fr/static/maps/busan.jpg","gamemodes":["control"],"location":"South Korea","country_code":"KR"},{"name":"Nepal","screenshot":"https://overfast-api.tekrop.fr/static/maps/nepal.jpg","gamemodes":["control"],"location":"Nepal","country_code":"NP"},{"name":"Ilios","screenshot":"https://overfast-api.tekrop.fr/static/maps/ilios.jpg","gamemodes":["control"],"location":"Greece","country_code":"GR"},{"name":"Oasis","screenshot":"https://overfast-api.tekrop.fr/static/maps/oasis.jpg","gamemodes":["control"],"location":"Iraq","country_code":"IQ"},{"name":"Lijiang Tower","screenshot":"https://overfast-api.tekrop.fr/static/maps/lijiang.jpg","gamemodes":["control"],"location":"China","country_code":"CN"},{"name":"Château Guillard","screenshot":"https://overfast-api.tekrop.fr/static/maps/chateau_guillard.jpg","gamemodes":["deathmatch","team-deathmatch"],"location":"Annecy, France","country_code":"FR"},{"name":"Kanezaka","screenshot":"https://overfast-api.tekrop.fr/static/maps/kanezaka.jpg","gamemodes":["deathmatch","team-deathmatch"],"location":"Tokyo, Japan","country_code":"JP"},{"name":"Malevento","screenshot":"https://overfast-api.tekrop.fr/static/maps/malevento.jpg","gamemodes":["deathmatch","team-deathmatch"],"location":"Italy","country_code":"IT"},{"name":"Petra","screenshot":"https://overfast-api.tekrop.fr/static/maps/petra.jpg","gamemodes":["deathmatch","team-deathmatch"],"location":"Southern Jordan","country_code":"JO"},{"name":"Black Forest","screenshot":"https://overfast-api.tekrop.fr/static/maps/black_forest.jpg","gamemodes":["elimination"],"location":"Germany","country_code":"DE"},{"name":"Castillo","screenshot":"https://overfast-api.tekrop.fr/static/maps/castillo.jpg","gamemodes":["elimination"],"location":"Mexico","country_code":"MX"},{"name":"Ecopoint: Antarctica","screenshot":"https://overfast-api.tekrop.fr/static/maps/ecopoint_antarctica.jpg","gamemodes":["elimination"],"location":"Antarctica","country_code":"AQ"},{"name":"Necropolis","screenshot":"https://overfast-api.tekrop.fr/static/maps/necropolis.jpg","gamemodes":["elimination"],"location":"Egypt","country_code":"EG"},{"name":"Circuit Royal","screenshot":"https://overfast-api.tekrop.fr/static/maps/circuit_royal.jpg","gamemodes":["escort"],"location":"Monte Carlo, Monaco","country_code":"MC"},{"name":"Dorado","screenshot":"https://overfast-api.tekrop.fr/static/maps/dorado.jpg","gamemodes":["escort"],"location":"Mexico","country_code":"MX"},{"name":"Route 66","screenshot":"https://overfast-api.tekrop.fr/static/maps/route_66.jpg","gamemodes":["escort"],"location":"Albuquerque, New Mexico, United States","country_code":"US"},{"name":"Junkertown","screenshot":"https://overfast-api.tekrop.fr/static/maps/junkertown.jpg","gamemodes":["escort"],"location":"Central Australia","country_code":"AU"},{"name":"Rialto","screenshot":"https://overfast-api.tekrop.fr/static/maps/rialto.jpg","gamemodes":["escort"],"location":"Venice, Italy","country_code":"IT"},{"name":"Havana","screenshot":"https://overfast-api.tekrop.fr/static/maps/havana.jpg","gamemodes":["escort"],"location":"Havana, Cuba","country_code":"CU"},{"name":"Watchpoint: Gibraltar","screenshot":"https://overfast-api.tekrop.fr/static/maps/gibraltar.jpg","gamemodes":["escort"],"location":"Gibraltar","country_code":"GI"},{"name":"Shambali Monastery","screenshot":"https://overfast-api.tekrop.fr/static/maps/shambali.jpg","gamemodes":["escort"],"location":"Nepal","country_code":"NP"},{"name":"Blizzard World","screenshot":"https://overfast-api.tekrop.fr/static/maps/blizzard_world.jpg","gamemodes":["hybrid"],"location":"Irvine, California, United States","country_code":"US"},{"name":"Numbani","screenshot":"https://overfast-api.tekrop.fr/static/maps/numbani.jpg","gamemodes":["hybrid"],"location":"Numbani (near Nigeria)","country_code":null},{"name":"Hollywood","screenshot":"https://overfast-api.tekrop.fr/static/maps/hollywood.jpg","gamemodes":["hybrid"],"location":"Los Angeles, United States","country_code":"US"},{"name":"Eichenwalde","screenshot":"https://overfast-api.tekrop.fr/static/maps/eichenwalde.jpg","gamemodes":["hybrid"],"location":"Stuttgart, Germany","country_code":"DE"},{"name":"King’s Row","screenshot":"https://overfast-api.tekrop.fr/static/maps/kings_row.jpg","gamemodes":["hybrid"],"location":"London, United Kingdom","country_code":"UK"},{"name":"Midtown","screenshot":"https://overfast-api.tekrop.fr/static/maps/midtown.jpg","gamemodes":["hybrid"],"location":"New York, United States","country_code":"US"},{"name":"Paraíso","screenshot":"https://overfast-api.tekrop.fr/static/maps/paraiso.jpg","gamemodes":["hybrid"],"location":"Rio de Janeiro, Brazil","country_code":"BR"},{"name":"Colosseo","screenshot":"https://overfast-api.tekrop.fr/static/maps/colosseo.jpg","gamemodes":["push"],"location":"Rome, Italy","country_code":"IT"},{"name":"Esperança","screenshot":"https://overfast-api.tekrop.fr/static/maps/esperanca.jpg","gamemodes":["push"],"location":"Portugal","country_code":"PT"},{"name":"New Queen Street","screenshot":"https://overfast-api.tekrop.fr/static/maps/new_queen_street.jpg","gamemodes":["push"],"location":"Toronto, Canada","country_code":"CA"}] \ No newline at end of file diff --git a/tests/parsers/test_maps_parser.py b/tests/parsers/test_maps_parser.py new file mode 100644 index 0000000..c01614f --- /dev/null +++ b/tests/parsers/test_maps_parser.py @@ -0,0 +1,8 @@ +from overfastapi.parsers.maps_parser import MapsParser + + +def test_maps_page_parsing(maps_json_data: list): + parser = MapsParser() + parser.parse() + + assert parser.data == maps_json_data diff --git a/tests/views/test_maps_route.py b/tests/views/test_maps_route.py new file mode 100644 index 0000000..1d40d60 --- /dev/null +++ b/tests/views/test_maps_route.py @@ -0,0 +1,69 @@ +import pytest +from fastapi.testclient import TestClient + +from overfastapi.common.enums import MapGamemode +from overfastapi.config import OVERFAST_API_BASE_URL +from overfastapi.main import app + +client = TestClient(app) + + +def test_get_maps(maps_json_data: list): + response = client.get("/maps") + json_response = response.json() + assert response.status_code == 200 + assert json_response == maps_json_data + + # Check if all the images link are valid + for map_dict in json_response: + image_response = client.get( + map_dict["screenshot"].removeprefix(OVERFAST_API_BASE_URL) + ) + assert image_response.status_code == 200 + + +@pytest.mark.parametrize("gamemode", [g.value for g in MapGamemode]) +def test_get_maps_filter_by_gamemode(gamemode: MapGamemode, maps_json_data: list): + response = client.get(f"/maps?gamemode={gamemode}") + assert response.status_code == 200 + assert response.json() == [ + map_dict for map_dict in maps_json_data if gamemode in map_dict["gamemodes"] + ] + + +def test_get_maps_invalid_role(): + response = client.get("/maps?gamemode=invalid") + assert response.status_code == 422 + assert response.json() == { + "detail": [ + { + "loc": ["query", "gamemode"], + "msg": ( + "value is not a valid enumeration member; " + "permitted: 'assault', 'capture-the-flag', 'control', " + "'deathmatch', 'elimination', 'escort', 'hybrid', 'push', " + "'team-deathmatch'" + ), + "type": "type_error.enum", + "ctx": { + "enum_values": [ + "assault", + "capture-the-flag", + "control", + "deathmatch", + "elimination", + "escort", + "hybrid", + "push", + "team-deathmatch", + ] + }, + } + ] + } + + +def test_get_maps_images(maps_json_data: list): + response = client.get("/maps") + assert response.status_code == 200 + assert response.json() == maps_json_data