From 724b4f635b5c823464321a24b37b24528ef0c08e Mon Sep 17 00:00:00 2001 From: Aryan Kothari <87589047+thearyadev@users.noreply.github.com> Date: Wed, 18 Oct 2023 01:07:13 -0400 Subject: [PATCH] Server improvements (#39) * fix some tests * add testing workflow * rename archive file * add database test steps * Add mysql port num * add sleep step to allow mysql to start * use docker instead of gh action for db server * attempt to fix connection * attempt to fix connection * modify second connection string * update archive with season 6 data * run pytest on pr * combine workflows to allow for conditional exec * adjust on param * adjust on param * fix names * Rebuild server logic * deprecate old server script * Add docs & remove unused dependencies * remove old server script --- server.py | 126 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 82 insertions(+), 44 deletions(-) diff --git a/server.py b/server.py index 41e54567..1e7672e7 100644 --- a/server.py +++ b/server.py @@ -2,11 +2,10 @@ import os from dotenv import load_dotenv -from fastapi import FastAPI, HTTPException, Request -from fastapi.responses import FileResponse, HTMLResponse, JSONResponse, RedirectResponse +from fastapi import FastAPI, Request, Depends +from fastapi.responses import FileResponse, RedirectResponse from fastapi.staticfiles import StaticFiles from fastapi.templating import Jinja2Templates -from jinja2 import Environment import leaderboards import database @@ -21,11 +20,14 @@ get_variance, ) from utils.raise_for_missing_env import raise_for_missing_env_vars +from typing import Annotated, Any +from functools import lru_cache load_dotenv() - templates = Jinja2Templates(directory="templates") - +app = FastAPI() +app.state.templates = templates # type: ignore +app.mount("/static", StaticFiles(directory="static"), name="static") db = database.DatabaseAccess( host=os.getenv("MYSQLHOST") or raise_for_missing_env_vars(), @@ -34,17 +36,30 @@ database=os.getenv("MYSQLDATABASE") or raise_for_missing_env_vars(), port=os.getenv("MYSQLPORT") or raise_for_missing_env_vars(), ) -seasons = db.get_seasons() -data = dict() -hits = 0 +@lru_cache +def seasons_list() -> list[str]: + """ + Wrapper for db.get_seasons() to cache the result + Returns: + list[str]: list of seasons + """ + return db.get_seasons() -trends_data = json.dumps(get_hero_trends_all_heroes_by_region(db=db)) +@lru_cache +def season_data() -> dict[str, Any]: + """ + Creates the data structure for use on the season page. + This function is cached. + Returns: + dict[str, Any]: data structure for use on the season page + see function implementation for exact data structure shape. (sorry) -def calculate(): - for s in seasons: + """ + data: dict = dict() + for s in seasons_list(): dataset: list[leaderboards.LeaderboardEntry] = db.get_all_records(s) data[s] = { # occurrences first most played @@ -372,69 +387,92 @@ def calculate(): "standard_deviation": round(get_stdev(graphData), 3), } data[s][key] = json.dumps(val) + return data -app = FastAPI() -app.state.templates = templates -app.mount("/static", StaticFiles(directory="static"), name="static") +@lru_cache +def trends_data() -> dict[str, dict[str, list[dict[str, int]]]]: + """ + Creates the data structure for use on the trends page. + This function is cached. + Returns: + dict[str, dict[str, list[dict[str, int]]]]: data structure for use on the trends page + """ + return get_hero_trends_all_heroes_by_region(db=db) -calculate() +@app.get("/{_}") +@app.get("/") +async def index_redirect( + request: Request, + seasons_list: Annotated[list[str], Depends(seasons_list)], + seasons_data: Annotated[dict, Depends(season_data)], +): + if "favicon.ico" in str(request.url): + return FileResponse("static/favicon.ico") -@app.get("/season/{season_number}") -async def season(request: Request, season_number: str): - global hits + if "robots.txt" in str(request.url): + return FileResponse("static/robots.txt") + return await season( + request, + season_number=seasons_list[-1], + seasons_data=seasons_data, + seasons_list=seasons_list, + ) + +@app.get("/season/{season_number}") +async def season( + request: Request, + season_number: str, + seasons_data: Annotated[dict, Depends(season_data)], + seasons_list: Annotated[list[str], Depends(seasons_list)], +): request.app.state.templates.env.filters["group_subseasons"] = group_subseasons - if season_number in seasons: - hits += 1 + if season_number in seasons_list: return templates.TemplateResponse( "season.html", { "request": request, - "seasons": seasons, + "seasons": seasons_list, "currentSeason": season_number, - **data[season_number], # type: ignore - **data[season_number]["MISC"], # type: ignore - # this does work. Im not sure why mypy is complaining. It unpacks all of the chart datas into the global scope of the template + **seasons_data[season_number], # type: ignore + **seasons_data[season_number]["MISC"], # type: ignore + # this does work. Im not sure why mypy is complaining. + # It unpacks all of the chart datas into the global scope of the template "disclaimer": db.get_season_disclaimer(season_number), }, ) - return RedirectResponse(f"/season{seasons[-1]}") - - -@app.get("/{_}") -@app.get("/") -async def index_redirect(request: Request): - if "favicon.ico" in str(request.url): - return FileResponse("static/favicon.ico") - - if "robots.txt" in str(request.url): - return FileResponse("static/robots.txt") - return await season(request, season_number=seasons[-1]) - - -@app.get("/i/hits", response_class=JSONResponse) -async def hit_endpoint(): - return {"hits": hits} + return RedirectResponse(f"/season{seasons_list[-1]}") @app.get("/trends/seasonal") -async def trendsEndpoint(request: Request): +async def trendsEndpoint( + request: Request, + seasons_list: Annotated[list[str], Depends(seasons_list)], + trends_data: Annotated[dict, Depends(trends_data)], +): request.app.state.templates.env.filters["group_subseasons"] = group_subseasons return templates.TemplateResponse( "trends.html", { "request": request, - "seasons": seasons, - "trends": trends_data, + "seasons": seasons_list, + "trends": json.dumps(trends_data), }, ) def group_subseasons(seasons: list[str]) -> dict[str, list[str]]: + """ + Groups sub seasons together, to shrink the menu size. + Args: + seasons: list of seasons + Returns: + dict[str, list[str]]: dict of subseasons and their seasons + """ subseasons: dict[str, list[str]] = {} for season in seasons: subseason = season.split("_")[0]