diff --git a/app/main.py b/app/main.py index 17d60f6..aa7c52a 100644 --- a/app/main.py +++ b/app/main.py @@ -17,8 +17,9 @@ logging.basicConfig( # from fastapi.staticfiles import StaticFiles # from fastapi.templating import Jinja2Templates -from .db_engine import db # noqa: E402 -from .routers_v2 import ( # noqa: E402 +from .db_engine import db +from .routers_v2.players import get_browser, shutdown_browser +from .routers_v2 import ( current, awards, teams, @@ -55,9 +56,12 @@ from .routers_v2 import ( # noqa: E402 @asynccontextmanager -async def lifespan(app: FastAPI): +async def lifespan(app): + # Startup: warm up the persistent Chromium browser + await get_browser() yield - await players.shutdown_browser() + # Shutdown: clean up browser and playwright + await shutdown_browser() app = FastAPI( diff --git a/app/routers_v2/players.py b/app/routers_v2/players.py index 060ae91..7ebd1b5 100644 --- a/app/routers_v2/players.py +++ b/app/routers_v2/players.py @@ -9,7 +9,9 @@ from typing import Optional, List, Literal import logging import pydantic from pandas import DataFrame -from playwright.async_api import async_playwright +import asyncio as _asyncio + +from playwright.async_api import async_playwright, Browser, Playwright from ..card_creation import get_batter_card_data, get_pitcher_card_data from ..db_engine import ( @@ -31,30 +33,62 @@ from ..db_engine import ( from ..db_helpers import upsert_players from ..dependencies import oauth2_scheme, valid_token -_browser = None -_playwright = None +# --------------------------------------------------------------------------- +# Persistent browser instance (WP-02) +# --------------------------------------------------------------------------- + +_browser: Browser | None = None +_playwright: Playwright | None = None +_browser_lock = _asyncio.Lock() -async def get_browser(): +async def get_browser() -> Browser: + """Get or create persistent Chromium browser instance. + + Reuses a single browser across all card renders, eliminating the ~1-1.5s + per-request launch/teardown overhead. Automatically reconnects if the + browser process has died. + + Uses an asyncio.Lock to prevent concurrent requests from racing to + launch multiple Chromium processes. + """ global _browser, _playwright - if _browser is not None and _browser.is_connected(): - return _browser - if _playwright is None: - _playwright = await async_playwright().start() - _browser = await _playwright.chromium.launch() + async with _browser_lock: + if _browser is None or not _browser.is_connected(): + if _playwright is not None: + try: + await _playwright.stop() + except Exception: + pass + _playwright = await async_playwright().start() + _browser = await _playwright.chromium.launch( + args=["--no-sandbox", "--disable-dev-shm-usage"] + ) return _browser async def shutdown_browser(): + """Clean shutdown of the persistent browser. + + Called by the FastAPI lifespan handler on application exit so the + Chromium process is not left orphaned. + """ global _browser, _playwright - if _browser is not None: - await _browser.close() + if _browser: + try: + await _browser.close() + except Exception: + pass _browser = None - if _playwright is not None: - await _playwright.stop() + if _playwright: + try: + await _playwright.stop() + except Exception: + pass _playwright = None +# --------------------------------------------------------------------------- # Franchise normalization: Convert city+team names to city-agnostic team names # This enables cross-era player matching (e.g., 'Oakland Athletics' -> 'Athletics') FRANCHISE_NORMALIZE = { @@ -318,21 +352,16 @@ async def get_players( else: return_val = {"count": len(final_players), "players": []} - dex_by_player = {} - if inc_dex: - player_ids = [p.player_id for p in final_players] - if player_ids: - for row in Paperdex.select().where(Paperdex.player_id << player_ids): - dex_by_player.setdefault(row.player_id, []).append(row) for x in final_players: this_record = model_to_dict(x, recurse=not (flat or short_output)) if inc_dex: - entries = dex_by_player.get(x.player_id, []) - this_record["paperdex"] = { - "count": len(entries), - "paperdex": [model_to_dict(y, recurse=False) for y in entries], - } + this_dex = Paperdex.select().where(Paperdex.player == x) + this_record["paperdex"] = {"count": this_dex.count(), "paperdex": []} + for y in this_dex: + this_record["paperdex"]["paperdex"].append( + model_to_dict(y, recurse=False) + ) if inc_keys and (flat or short_output): if this_record["mlbplayer"] is not None: @@ -500,21 +529,16 @@ async def get_random_player( return Response(content=return_val, media_type="text/csv") else: - final_players = list(final_players) return_val = {"count": len(final_players), "players": []} - player_ids = [p.player_id for p in final_players] - dex_by_player = {} - if player_ids: - for row in Paperdex.select().where(Paperdex.player_id << player_ids): - dex_by_player.setdefault(row.player_id, []).append(row) for x in final_players: this_record = model_to_dict(x) - entries = dex_by_player.get(x.player_id, []) - this_record["paperdex"] = { - "count": len(entries), - "paperdex": [model_to_dict(y, recurse=False) for y in entries], - } + this_dex = Paperdex.select().where(Paperdex.player == x) + this_record["paperdex"] = {"count": this_dex.count(), "paperdex": []} + for y in this_dex: + this_record["paperdex"]["paperdex"].append( + model_to_dict(y, recurse=False) + ) return_val["players"].append(this_record) # return_val['players'].append(model_to_dict(x)) @@ -708,6 +732,9 @@ async def get_batter_card( ) headers = {"Cache-Control": "public, max-age=86400"} + _filename = ( + f"{this_player.description} {this_player.p_name} {card_type} {d}-v{variant}" + ) if ( os.path.isfile( f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png" diff --git a/storage/templates/player_card.html b/storage/templates/player_card.html index 83750ed..9cdf814 100644 --- a/storage/templates/player_card.html +++ b/storage/templates/player_card.html @@ -2,9 +2,6 @@ {% include 'style.html' %} - - -
diff --git a/storage/templates/style.html b/storage/templates/style.html index 6ca98a2..4f9ebcf 100644 --- a/storage/templates/style.html +++ b/storage/templates/style.html @@ -1,4 +1,19 @@