Skip to content

Commit

Permalink
Added gamemodes in local CSV file instead of calling Blizzard
Browse files Browse the repository at this point in the history
  • Loading branch information
TeKrop committed Sep 2, 2023
1 parent 4ac095c commit e8eea21
Show file tree
Hide file tree
Showing 46 changed files with 258 additions and 143 deletions.
2 changes: 1 addition & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ USE_API_CACHE_IN_APP=false
EXPIRED_CACHE_REFRESH_LIMIT=3600
HEROES_PATH_CACHE_TIMEOUT=86400
HERO_PATH_CACHE_TIMEOUT=86400
HOME_PATH_CACHE_TIMEOUT=86400
CSV_CACHE_TIMEOUT=86400
CAREER_PATH_CACHE_TIMEOUT=7200
SEARCH_ACCOUNT_PATH_CACHE_TIMEOUT=3600
NAMECARDS_TIMEOUT=7200
Expand Down
7 changes: 7 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ This guide aims to help you in contributing in OverFast API. The first step for

As of now, only some specific stuff can easily be updated by anyone, even without any knowledge in Python or FastAPI framework. If I take too much time to update them, don't hesitate to make a PR if you need up-to-date data :
- The CSV file containing basic heroes data : name, role, and some statistics like health, armor and shields
- The CSV file containing the list of gamemodes of the game
- The CSV file containing the list of maps of the game

## 🦸 Heroes data
Expand All @@ -16,6 +17,12 @@ The CSV file containing heroes statistics data is located in `app/data/heroes.cs
- `armor` : Armor of the hero, mainly possessed by tanks
- `shields` : Shields of the hero

## 🎲 Gamemodes list
The CSV file containing gamemodes list is located in `app/data/gamemodes.csv`. Data is divided into 3 columns :
- `key` : Key of the gamemode, used in URLs of the API, and for the name of the corresponding screenshot and icon files
- `name` : Name of the gamemode (in english)
- `description` : Description of the gamemode (in english)

## 🗺️ Maps list
The CSV file containing maps list is located in `app/data/maps.csv`. Data is divided into 5 columns :
- `key` : Key of the map, used in URLs of the API, and for the name of the corresponding screenshot file
Expand Down
2 changes: 1 addition & 1 deletion app/commands/check_and_update_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
from app.common.helpers import overfast_internal_error
from app.common.logging import logger
from app.config import settings
from app.parsers.abstract_parser import AbstractParser
from app.parsers.gamemodes_parser import GamemodesParser
from app.parsers.generics.abstract_parser import AbstractParser
from app.parsers.hero_parser import HeroParser
from app.parsers.heroes_parser import HeroesParser
from app.parsers.heroes_stats_parser import HeroesStatsParser
Expand Down
23 changes: 10 additions & 13 deletions app/common/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,13 @@ class Locale(StrEnum):
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"
FLASHPOINT = "flashpoint"
HYBRID = "hybrid"
PUSH = "push"
TEAM_DEATHMATCH = "team-deathmatch"
# Dynamically create the MapGamemode enum by using the CSV File
gamemodes_data = read_csv_data_file("gamemodes.csv")
MapGamemode = StrEnum(
"MapGamemode",
{
gamemode["key"].upper().replace("-", "_"): gamemode["key"]
for gamemode in gamemodes_data
},
)
MapGamemode.__doc__ = "Maps gamemodes keys"
4 changes: 2 additions & 2 deletions app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ class Settings(BaseSettings):
# Cache TTL for specific hero data (seconds)
hero_path_cache_timeout: int = 86400

# Cache TTL for Blizzard homepage data : gamemodes and roles (seconds)
home_path_cache_timeout: int = 86400
# Cache TTL for local CSV-based data : heroes stats, gamemodes and maps
csv_cache_timeout: int = 86400

# Cache TTL for career pages data (seconds)
career_path_cache_timeout: int = 7200
Expand Down
11 changes: 11 additions & 0 deletions app/data/gamemodes.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
key,name,description
assault,Assault,"Teams fight to capture or defend two successive points against the enemy team. It's an inactive Overwatch 1 gamemode, also called 2CP."
capture-the-flag,Capture the Flag,Teams compete to capture the enemy team’s flag while defending their own.
control,Control,Teams fight to hold a single objective. The first team to win two rounds wins the map.
deathmatch,Deathmatch,Race to reach 20 points first by racking up kills in a free-for-all format.
elimination,Elimination,"Dispatch all enemies to win the round. Win three rounds to claim victory. Available with teams of one, three, or six."
escort,Escort,"One team escorts a payload to its delivery point, while the other races to stop them."
flashpoint,Flashpoint,"Teams fight for control of key positions across the map, aiming to capture three of them before their opponents do."
hybrid,Hybrid,"Attackers capture a payload, then escort it to its destination; defenders try to hold them back."
push,Push,Teams battle to take control of a robot and push it toward the enemy base.
team-deathmatch,Team Deathmatch,Team up and triumph over your enemies by scoring the most kills.
2 changes: 1 addition & 1 deletion app/handlers/list_gamemodes_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ class ListGamemodesRequestHandler(APIRequestHandler):
"""

parser_classes: ClassVar[list] = [GamemodesParser]
timeout = settings.home_path_cache_timeout
timeout = settings.csv_cache_timeout
2 changes: 1 addition & 1 deletion app/handlers/list_maps_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,4 @@ class ListMapsRequestHandler(APIRequestHandler):
"""

parser_classes: ClassVar[list] = [MapsParser]
timeout = settings.home_path_cache_timeout
timeout = settings.csv_cache_timeout
50 changes: 15 additions & 35 deletions app/parsers/gamemodes_parser.py
Original file line number Diff line number Diff line change
@@ -1,47 +1,27 @@
"""Gamemodes Parser module"""
from app.config import settings

from .api_parser import APIParser
from .generics.csv_parser import CSVParser


class GamemodesParser(APIParser):
class GamemodesParser(CSVParser):
"""Overwatch map gamemodes list page Parser class"""

root_path = settings.home_path
timeout = settings.home_path_cache_timeout
filename = "gamemodes"

def parse_data(self) -> list[dict]:
gamemodes_container = (
self.root_tag.find("div", class_="maps", recursive=False)
.find("blz-carousel-section", recursive=False)
.find("blz-carousel", recursive=False)
)

gamemodes_extras = [
{
"key": feature_div["label"],
"description": (
feature_div.find("blz-header")
.find("div", slot="description")
.get_text()
.strip()
),
"screenshot": feature_div.find("blz-image")["src:min-plus"],
}
for feature_div in gamemodes_container.find_all("blz-feature")
]

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"],
"screenshot": gamemodes_extras[gamemode_index]["screenshot"],
}
for gamemode_index, gamemode_div in enumerate(
gamemodes_container.find("blz-tab-controls").find_all(
"blz-tab-control",
"key": gamemode["key"],
"name": gamemode["name"],
"icon": self.get_static_url(
f"{gamemode['key']}-icon",
extension="svg",
),
)
"description": gamemode["description"],
"screenshot": self.get_static_url(
gamemode["key"],
extension="avif",
),
}
for gamemode in self.csv_data
]
Empty file.
File renamed without changes.
File renamed without changes.
45 changes: 45 additions & 0 deletions app/parsers/generics/csv_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
"""Abstract API Parser module"""
from abc import abstractmethod

from app.common.helpers import read_csv_data_file
from app.config import settings

from .abstract_parser import AbstractParser


class CSVParser(AbstractParser):
"""CSV Parser class used to define generic behavior for parsers used
to extract data from local CSV files.
"""

# Timeout to use for every CSV-based data
timeout = settings.csv_cache_timeout

# Name of CSV file to retrieve (without extension), also
# used as a sub-folder name for storing related static files
filename: str

async def retrieve_and_parse_data(self) -> None:
"""Method used to retrieve data from CSV file and storing
it into self.data attribute
"""

# Read the CSV file
self.csv_data = read_csv_data_file(f"{self.filename}.csv")

# Parse the data
self.data = self.parse_data()

# Update the Parser Cache
self.cache_manager.update_parser_cache(self.cache_key, self.data, self.timeout)

@abstractmethod
def parse_data(self) -> dict | list[dict]:
"""Main submethod of the parser, mainly doing the parsing of CSV data and
returning a dict, which will be cached and used by the API. Can
raise an error if there is an issue when parsing the data.
"""

def get_static_url(self, key: str, extension: str = "jpg") -> str:
"""Method used to retrieve the URL of a local static file"""
return f"{settings.app_base_url}/static/{self.filename}/{key}.{extension}"
2 changes: 1 addition & 1 deletion app/parsers/hero_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from app.common.exceptions import ParserBlizzardError
from app.config import settings

from .api_parser import APIParser
from .generics.api_parser import APIParser
from .helpers import get_full_url, get_role_from_icon_url


Expand Down
2 changes: 1 addition & 1 deletion app/parsers/heroes_parser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Heroes page Parser module"""
from app.config import settings

from .api_parser import APIParser
from .generics.api_parser import APIParser


class HeroesParser(APIParser):
Expand Down
20 changes: 6 additions & 14 deletions app/parsers/heroes_stats_parser.py
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
"""Heroes Stats Parser module"""
from typing import ClassVar

from app.common.helpers import read_csv_data_file
from app.config import settings
from .generics.csv_parser import CSVParser

from .abstract_parser import AbstractParser


class HeroesStatsParser(AbstractParser):
class HeroesStatsParser(CSVParser):
"""Heroes stats (health, armor, shields) Parser class"""

timeout = settings.hero_path_cache_timeout
filename = "heroes"
hitpoints_keys: ClassVar[set] = {"health", "armor", "shields"}

async def retrieve_and_parse_data(self) -> None:
heroes_stats_data = read_csv_data_file("heroes.csv")

self.data = {
def parse_data(self) -> dict:
return {
hero_stats["key"]: {"hitpoints": self.__get_hitpoints(hero_stats)}
for hero_stats in heroes_stats_data
for hero_stats in self.csv_data
}

# Update the Parser Cache
self.cache_manager.update_parser_cache(self.cache_key, self.data, self.timeout)

def __get_hitpoints(self, hero_stats: dict) -> dict:
hitpoints = {hp_key: int(hero_stats[hp_key]) for hp_key in self.hitpoints_keys}
hitpoints["total"] = sum(hitpoints.values())
Expand Down
26 changes: 8 additions & 18 deletions app/parsers/maps_parser.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,25 @@
"""Maps Parser module"""
from app.common.helpers import read_csv_data_file
from app.config import settings

from .abstract_parser import AbstractParser
from .generics.csv_parser import CSVParser


class MapsParser(AbstractParser):
class MapsParser(CSVParser):
"""Overwatch maps list page Parser class"""

timeout = settings.home_path_cache_timeout
filename = "maps"

async def retrieve_and_parse_data(self) -> None:
maps_data = read_csv_data_file("maps.csv")

self.data = [
def parse_data(self) -> list[dict]:
return [
{
"name": map_dict["name"],
"screenshot": self.get_screenshot_url(map_dict["key"]),
"screenshot": self.get_static_url(map_dict["key"]),
"gamemodes": map_dict["gamemodes"].split(","),
"location": map_dict["location"],
"country_code": map_dict["country_code"] or None,
"country_code": map_dict.get("country_code") or None,
}
for map_dict in maps_data
for map_dict in self.csv_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 (
Expand All @@ -36,6 +29,3 @@ def filter_request_using_query(self, **kwargs) -> list:
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"{settings.app_base_url}/static/maps/{map_key}.jpg"
2 changes: 1 addition & 1 deletion app/parsers/namecard_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from app.common.logging import logger
from app.config import settings

from .api_parser import APIParser
from .generics.api_parser import APIParser


class NamecardParser(APIParser):
Expand Down
2 changes: 1 addition & 1 deletion app/parsers/player_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
from app.common.exceptions import ParserBlizzardError
from app.config import settings

from .api_parser import APIParser
from .generics.api_parser import APIParser
from .helpers import (
get_computed_stat_value,
get_division_from_rank_icon,
Expand Down
2 changes: 1 addition & 1 deletion app/parsers/roles_parser.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Roles Parser module"""
from app.config import settings

from .api_parser import APIParser
from .generics.api_parser import APIParser
from .helpers import get_role_from_icon_url


Expand Down
13 changes: 5 additions & 8 deletions app/routers/gamemodes.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
"""Gamemodes endpoints router : gamemodes list, etc."""
from fastapi import APIRouter, Query, Request
from fastapi import APIRouter, Request

from app.common.decorators import validation_error_handler
from app.common.enums import Locale, RouteTag
from app.common.enums import RouteTag
from app.common.helpers import routes_responses
from app.handlers.list_gamemodes_request_handler import ListGamemodesRequestHandler
from app.models.gamemodes import GamemodeDetails
Expand All @@ -16,14 +16,11 @@
tags=[RouteTag.GAMEMODES],
summary="Get a list of gamemodes",
description=(
"Get a list of Overwatch gamemodes : Assault, Escort, Hybrid, etc."
"Get a list of Overwatch gamemodes : Assault, Escort, Flashpoint, Hybrid, etc."
"<br />**Cache TTL : 1 day.**"
),
operation_id="list_map_gamemodes",
)
@validation_error_handler(response_model=GamemodeDetails)
async def list_map_gamemodes(
request: Request,
locale: Locale = Query(Locale.ENGLISH_US, title="Locale to be displayed"),
) -> list[GamemodeDetails]:
return await ListGamemodesRequestHandler(request).process_request(locale=locale)
async def list_map_gamemodes(request: Request) -> list[GamemodeDetails]:
return await ListGamemodesRequestHandler(request).process_request()
10 changes: 5 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "overfast-api"
version = "2.19.3"
version = "2.19.4"
description = "Overwatch API giving data about heroes, maps, and players statistics."
license = "MIT"
authors = ["TeKrop <[email protected]>"]
Expand All @@ -12,14 +12,14 @@ documentation = "https://overfast-api.tekrop.fr/"
[tool.poetry.dependencies]
python = "^3.11"
beautifulsoup4 = "^4.12.2"
fastapi = "^0.101.0"
fastapi = "^0.103.0"
httpx = {extras = ["http2"], version = "^0.24.1"}
loguru = "^0.7.0"
lxml = "^4.9.3"
redis = "^4.6.0"
redis = "^5.0.0"
uvicorn = {extras = ["standard"], version = "^0.23.2"}
pydantic = "^2.1.1"
pydantic-settings = "^2.0.2"
pydantic = "^2.3.0"
pydantic-settings = "^2.0.3"

[tool.poetry.group.dev.dependencies]
black = "^23.7.0"
Expand Down
Loading

0 comments on commit e8eea21

Please sign in to comment.