Skip to content

Commit

Permalink
Added new /maps endpoint from static data (#12)
Browse files Browse the repository at this point in the history
  • Loading branch information
Valentin Porchet committed Jan 29, 2023
1 parent 748948c commit 88ccc40
Show file tree
Hide file tree
Showing 66 changed files with 400 additions and 122 deletions.
3 changes: 2 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
16 changes: 11 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -22,6 +18,7 @@
* [🐋 Docker](#-docker)
* [👨‍💻 Technical details](#-technical-details)
* [🤝 Contributing](#-contributing)
* [🙏 Credits](#-credits)
* [📝 License](#-license)


Expand All @@ -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

Expand Down Expand Up @@ -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).
Expand Down
12 changes: 9 additions & 3 deletions overfastapi/commands/check_and_update_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -20,6 +21,7 @@
"GamemodesParser": GamemodesParser,
"HeroParser": HeroParser,
"HeroesParser": HeroesParser,
"MapsParser": MapsParser,
"PlayerParser": PlayerParser,
"PlayerStatsSummaryParser": PlayerStatsSummaryParser,
"RolesParser": RolesParser,
Expand All @@ -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]
Expand All @@ -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 : {}",
Expand Down
2 changes: 1 addition & 1 deletion overfastapi/commands/check_new_hero.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
14 changes: 14 additions & 0 deletions overfastapi/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
9 changes: 9 additions & 0 deletions overfastapi/common/helpers.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Parser Helpers module"""
import csv
import json
from pathlib import Path

import requests
from fastapi import HTTPException, Request, status
Expand Down Expand Up @@ -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=";")
38 changes: 38 additions & 0 deletions overfastapi/data/maps.csv
Original file line number Diff line number Diff line change
@@ -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
5 changes: 0 additions & 5 deletions overfastapi/handlers/api_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
1 change: 0 additions & 1 deletion overfastapi/handlers/get_hero_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
1 change: 0 additions & 1 deletion overfastapi/handlers/get_player_career_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,5 @@ class GetPlayerCareerRequestHandler(APIRequestHandler):
PlayerParser class.
"""

api_root_url = "/players"
parser_classes = [PlayerParser]
timeout = CAREER_PATH_CACHE_TIMEOUT
1 change: 0 additions & 1 deletion overfastapi/handlers/list_gamemodes_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 0 additions & 1 deletion overfastapi/handlers/list_heroes_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions overfastapi/handlers/list_maps_request_handler.py
Original file line number Diff line number Diff line change
@@ -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
1 change: 0 additions & 1 deletion overfastapi/handlers/list_roles_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
19 changes: 11 additions & 8 deletions overfastapi/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.",
Expand All @@ -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)
Expand All @@ -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")
10 changes: 10 additions & 0 deletions overfastapi/models/gamemodes.py
Original file line number Diff line number Diff line change
@@ -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(
...,
Expand Down
28 changes: 28 additions & 0 deletions overfastapi/models/maps.py
Original file line number Diff line number Diff line change
@@ -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",
)
Loading

0 comments on commit 88ccc40

Please sign in to comment.