Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: enhanced async client usage #187

Merged
merged 2 commits into from
Sep 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions app/commands/check_and_update_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

import asyncio

import httpx
from fastapi import HTTPException

from app.common.cache_manager import CacheManager
from app.common.exceptions import ParserBlizzardError, ParserParsingError
from app.common.helpers import overfast_internal_error
from app.common.helpers import overfast_client_settings, overfast_internal_error
from app.common.logging import logger
from app.config import settings
from app.parsers.gamemodes_parser import GamemodesParser
Expand Down Expand Up @@ -106,14 +107,20 @@ async def main():
keys_to_update = get_soon_expired_cache_keys()
logger.info("Done ! Retrieved keys : {}", len(keys_to_update))

# Instanciate one HTTPX Client to use for all the updates
client = httpx.AsyncClient(**overfast_client_settings)

tasks = []
for key in keys_to_update:
parser_class, kwargs = get_request_parser_class(key)
parser = parser_class(**kwargs)
parser = parser_class(client=client, **kwargs)
tasks.append(retrieve_data(key, parser))

await asyncio.gather(*tasks)

# Properly close HTTPX Async Client
await client.aclose()

logger.info("Redis cache update finished !")


Expand Down
21 changes: 14 additions & 7 deletions app/commands/check_new_hero.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,22 @@

import asyncio

import httpx
from fastapi import HTTPException

from app.common.enums import HeroKey
from app.common.helpers import send_discord_webhook_message
from app.common.helpers import overfast_client_settings, send_discord_webhook_message
from app.common.logging import logger
from app.config import settings
from app.parsers.heroes_parser import HeroesParser


def get_distant_hero_keys() -> set[str]:
async def get_distant_hero_keys(client: httpx.AsyncClient) -> set[str]:
"""Get a set of Overwatch hero keys from the Blizzard heroes page"""
heroes_parser = HeroesParser()
heroes_parser = HeroesParser(client=client)

try:
asyncio.run(heroes_parser.retrieve_and_parse_data())
await heroes_parser.retrieve_and_parse_data()
except HTTPException as error:
raise SystemExit from error

Expand All @@ -31,17 +32,23 @@ def get_local_hero_keys() -> set[str]:
return {h.value for h in HeroKey}


def main():
async def main():
"""Main method of the script"""
logger.info("Checking if a Discord webhook is configured...")
if not settings.discord_webhook_enabled:
logger.info("No Discord webhook configured ! Exiting...")
raise SystemExit

logger.info("OK ! Starting to check if a new hero is here...")
distant_hero_keys = get_distant_hero_keys()

# Instanciate one HTTPX Client to use for all the updates
client = httpx.AsyncClient(**overfast_client_settings)

distant_hero_keys = await get_distant_hero_keys(client)
local_hero_keys = get_local_hero_keys()

await client.aclose()

# Compare both sets. If we have a difference, notify the developer
new_hero_keys = distant_hero_keys - local_hero_keys
if len(new_hero_keys) > 0:
Expand All @@ -56,4 +63,4 @@ def main():

if __name__ == "__main__": # pragma: no cover
logger = logger.patch(lambda record: record.update(name="check_new_hero"))
main()
asyncio.run(main())
7 changes: 2 additions & 5 deletions app/common/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,15 +63,12 @@
"follow_redirects": True,
}

# Instanciate global httpx client
overfast_client = httpx.AsyncClient(**overfast_client_settings)


async def overfast_request(url: str) -> httpx.Response:
async def overfast_request(client: httpx.AsyncClient, url: str) -> httpx.Response:
TeKrop marked this conversation as resolved.
Show resolved Hide resolved
"""Make an HTTP GET request with custom headers and retrieve the result"""
try:
logger.debug("Requesting {}...", url)
response = await overfast_client.get(url)
response = await client.get(url)
except httpx.TimeoutException as error:
raise blizzard_response_error(
status_code=0,
Expand Down
3 changes: 2 additions & 1 deletion app/handlers/api_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ class APIRequestHandler(ABC):

def __init__(self, request: Request):
self.cache_key = CacheManager.get_cache_key_from_request(request)
self.overfast_client = request.app.overfast_client

@property
@abstractmethod
Expand Down Expand Up @@ -51,7 +52,7 @@ async def process_request(self, **kwargs) -> dict:
# Instanciate the parser, it will check if a Parser Cache is here.
# If not, it will retrieve its associated Blizzard
# page and use the kwargs to generate the appropriate URL
parser = parser_class(**kwargs)
parser = parser_class(client=self.overfast_client, **kwargs)

# Do the parsing. Internally, it will check for Parser Cache
# before doing a real parsing using BeautifulSoup
Expand Down
17 changes: 13 additions & 4 deletions app/handlers/search_players_request_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class SearchPlayersRequestHandler:

def __init__(self, request: Request):
self.cache_key = CacheManager.get_cache_key_from_request(request)
self.overfast_client = request.app.overfast_client

async def process_request(self, **kwargs) -> dict:
"""Main method used to process the request from user and return final data.
Expand All @@ -42,7 +43,9 @@ async def process_request(self, **kwargs) -> dict:
"""

# Request the data from Blizzard URL
req = await overfast_request(self.get_blizzard_url(**kwargs))
req = await overfast_request(
client=self.overfast_client, url=self.get_blizzard_url(**kwargs)
)
if req.status_code != status.HTTP_200_OK:
raise blizzard_response_error_from_request(req)

Expand Down Expand Up @@ -110,11 +113,17 @@ def get_blizzard_url(**kwargs) -> str:
return f"{settings.blizzard_host}/{locale}{settings.search_account_path}/{kwargs.get('name')}/"

def get_avatar_url(self, player: dict, player_id: str) -> str | None:
return PortraitParser(player_id=player_id).retrieve_data_value(player)
return PortraitParser(
client=self.overfast_client, player_id=player_id
).retrieve_data_value(player)

def get_namecard_url(self, player: dict, player_id: str) -> str | None:
return NamecardParser(player_id=player_id).retrieve_data_value(player)
return NamecardParser(
client=self.overfast_client, player_id=player_id
).retrieve_data_value(player)

def get_title(self, player: dict, player_id: str) -> str | None:
title = TitleParser(player_id=player_id).retrieve_data_value(player)
title = TitleParser(
client=self.overfast_client, player_id=player_id
).retrieve_data_value(player)
return get_player_title(title)
11 changes: 10 additions & 1 deletion app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

from contextlib import asynccontextmanager, suppress

import httpx
from fastapi import FastAPI, Request
from fastapi.openapi.docs import get_redoc_html, get_swagger_ui_html
from fastapi.openapi.utils import get_openapi
Expand All @@ -11,21 +12,29 @@

from .commands.update_search_data_cache import update_search_data_cache
from .common.enums import RouteTag
from .common.helpers import overfast_client_settings
from .common.logging import logger
from .config import settings
from .routers import gamemodes, heroes, maps, players, roles


@asynccontextmanager
async def lifespan(_: FastAPI): # pragma: no cover
async def lifespan(app: FastAPI): # pragma: no cover
# Update search data list from Blizzard before starting up
if settings.redis_caching_enabled:
logger.info("Updating search data cache (avatars, namecards, titles)")
with suppress(SystemExit):
update_search_data_cache()

# Instanciate HTTPX Async Client
logger.info("Instanciating HTTPX AsyncClient...")
app.overfast_client = httpx.AsyncClient(**overfast_client_settings)

yield

# Properly close HTTPX Async Client
await app.overfast_client.aclose()


app = FastAPI(title="OverFast API", docs_url=None, redoc_url=None, lifespan=lifespan)
description = f"""OverFast API provides comprehensive data on Overwatch 2 heroes,
Expand Down
2 changes: 1 addition & 1 deletion app/parsers/generics/abstract_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ class AbstractParser(ABC):
cache_manager = CacheManager()

def __init__(self, **_):
self.data: dict | list = None
self.data: dict | list | None = None

@property
@abstractmethod
Expand Down
6 changes: 4 additions & 2 deletions app/parsers/generics/api_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from functools import cached_property
from typing import ClassVar

import httpx
from bs4 import BeautifulSoup
from fastapi import status

Expand All @@ -23,8 +24,9 @@ class APIParser(AbstractParser):
# List of valid HTTP codes when retrieving Blizzard pages
valid_http_codes: ClassVar[list] = [status.HTTP_200_OK]

def __init__(self, **kwargs):
def __init__(self, client: httpx.AsyncClient, **kwargs):
self.blizzard_url = self.get_blizzard_url(**kwargs)
self.overfast_client = client
super().__init__(**kwargs)

@property
Expand All @@ -51,7 +53,7 @@ async 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.
"""
req = await overfast_request(self.blizzard_url)
req = await overfast_request(client=self.overfast_client, url=self.blizzard_url)
if req.status_code not in self.valid_http_codes:
raise blizzard_response_error_from_request(req)

Expand Down
2 changes: 1 addition & 1 deletion app/parsers/search_data_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ async def retrieve_and_parse_data(self) -> None:
"""Method used to retrieve data from Blizzard (JSON data), parsing it
and storing it into self.data attribute.
"""
req = await overfast_request(self.blizzard_url)
req = await overfast_request(client=self.overfast_client, url=self.blizzard_url)
if req.status_code not in self.valid_http_codes:
raise blizzard_response_error_from_request(req)

Expand Down
19 changes: 13 additions & 6 deletions app/routers/players.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Players endpoints router : players search, players career, statistics, etc."""

from typing import Annotated

from fastapi import APIRouter, Depends, Path, Query, Request, status

from app.common.decorators import validation_error_handler
Expand Down Expand Up @@ -51,8 +53,11 @@ async def get_player_common_parameters(
return {"player_id": player_id}


CommonsPlayerDep = Annotated[dict, Depends(get_player_common_parameters)]
TeKrop marked this conversation as resolved.
Show resolved Hide resolved


async def get_player_career_common_parameters(
commons: dict = Depends(get_player_common_parameters),
commons: CommonsPlayerDep,
gamemode: PlayerGamemode = Query(
...,
title="Gamemode",
Expand Down Expand Up @@ -86,6 +91,8 @@ async def get_player_career_common_parameters(
}


CommonsPlayerCareerDep = Annotated[dict, Depends(get_player_career_common_parameters)]

router = APIRouter()


Expand Down Expand Up @@ -138,7 +145,7 @@ async def search_players(
@validation_error_handler(response_model=PlayerSummary)
async def get_player_summary(
request: Request,
commons: dict = Depends(get_player_common_parameters),
commons: CommonsPlayerDep,
) -> PlayerSummary:
return await GetPlayerCareerRequestHandler(request).process_request(
summary=True,
Expand Down Expand Up @@ -167,7 +174,7 @@ async def get_player_summary(
@validation_error_handler(response_model=PlayerStatsSummary)
async def get_player_stats_summary(
request: Request,
commons: dict = Depends(get_player_common_parameters),
commons: CommonsPlayerDep,
gamemode: PlayerGamemode = Query(
None,
title="Gamemode",
Expand Down Expand Up @@ -212,7 +219,7 @@ async def get_player_stats_summary(
@validation_error_handler(response_model=PlayerCareerStats)
async def get_player_career_stats(
request: Request,
commons: dict = Depends(get_player_career_common_parameters),
commons: CommonsPlayerCareerDep,
) -> PlayerCareerStats:
return await GetPlayerCareerStatsRequestHandler(request).process_request(
stats=True,
Expand All @@ -236,7 +243,7 @@ async def get_player_career_stats(
@validation_error_handler(response_model=CareerStats)
async def get_player_stats(
request: Request,
commons: dict = Depends(get_player_career_common_parameters),
commons: CommonsPlayerCareerDep,
) -> CareerStats:
return await GetPlayerCareerRequestHandler(request).process_request(
stats=True,
Expand All @@ -257,7 +264,7 @@ async def get_player_stats(
@validation_error_handler(response_model=Player)
async def get_player_career(
request: Request,
commons: dict = Depends(get_player_common_parameters),
commons: CommonsPlayerDep,
) -> Player:
return await GetPlayerCareerRequestHandler(request).process_request(
player_id=commons.get("player_id"),
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "overfast-api"
version = "2.35.2"
version = "2.36.0"
description = "Overwatch API giving data about heroes, maps, and players statistics."
license = {file = "LICENSE"}
authors = [
Expand Down
Loading