feat: render pipeline optimization (Phase 0) #94
15
app/main.py
15
app/main.py
@ -1,5 +1,6 @@
|
||||
import logging
|
||||
import os
|
||||
from contextlib import asynccontextmanager
|
||||
from datetime import datetime
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
@ -17,6 +18,7 @@ logging.basicConfig(
|
||||
# from fastapi.templating import Jinja2Templates
|
||||
|
||||
from .db_engine import db
|
||||
from .routers_v2.players import get_browser, shutdown_browser
|
||||
from .routers_v2 import (
|
||||
current,
|
||||
awards,
|
||||
@ -51,8 +53,19 @@ from .routers_v2 import (
|
||||
scout_claims,
|
||||
)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app):
|
||||
# Startup: warm up the persistent Chromium browser
|
||||
await get_browser()
|
||||
yield
|
||||
# Shutdown: clean up browser and playwright
|
||||
await shutdown_browser()
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
# root_path='/api',
|
||||
lifespan=lifespan,
|
||||
responses={404: {"description": "Not found"}},
|
||||
docs_url="/api/docs",
|
||||
redoc_url="/api/redoc",
|
||||
@ -114,4 +127,4 @@ async def get_docs(req: Request):
|
||||
|
||||
@app.get("/api/openapi.json", include_in_schema=False)
|
||||
async def openapi():
|
||||
return get_openapi(title="Paper Dynasty API", version=f"0.1.1", routes=app.routes)
|
||||
return get_openapi(title="Paper Dynasty API", version="0.1.1", routes=app.routes)
|
||||
|
||||
@ -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,6 +33,62 @@ from ..db_engine import (
|
||||
from ..db_helpers import upsert_players
|
||||
from ..dependencies import oauth2_scheme, valid_token
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Persistent browser instance (WP-02)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_browser: Browser | None = None
|
||||
_playwright: Playwright | None = None
|
||||
_browser_lock = _asyncio.Lock()
|
||||
|
||||
|
||||
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
|
||||
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:
|
||||
try:
|
||||
await _browser.close()
|
||||
except Exception:
|
||||
pass
|
||||
_browser = None
|
||||
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 = {
|
||||
@ -144,7 +202,7 @@ async def get_players(
|
||||
):
|
||||
all_players = Player.select()
|
||||
if all_players.count() == 0:
|
||||
raise HTTPException(status_code=404, detail=f"There are no players to filter")
|
||||
raise HTTPException(status_code=404, detail="There are no players to filter")
|
||||
|
||||
if name is not None:
|
||||
all_players = all_players.where(fn.Lower(Player.p_name) == name.lower())
|
||||
@ -674,7 +732,7 @@ async def get_batter_card(
|
||||
)
|
||||
|
||||
headers = {"Cache-Control": "public, max-age=86400"}
|
||||
filename = (
|
||||
_filename = (
|
||||
f"{this_player.description} {this_player.p_name} {card_type} {d}-v{variant}"
|
||||
)
|
||||
if (
|
||||
@ -799,16 +857,17 @@ async def get_batter_card(
|
||||
logging.debug(f"body:\n{html_response.body.decode('UTF-8')}")
|
||||
|
||||
file_path = f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png"
|
||||
async with async_playwright() as p:
|
||||
browser = await p.chromium.launch()
|
||||
page = await browser.new_page()
|
||||
browser = await get_browser()
|
||||
page = await browser.new_page(viewport={"width": 1280, "height": 720})
|
||||
try:
|
||||
await page.set_content(html_response.body.decode("UTF-8"))
|
||||
await page.screenshot(
|
||||
path=file_path,
|
||||
type="png",
|
||||
clip={"x": 0.0, "y": 0, "width": 1200, "height": 600},
|
||||
)
|
||||
await browser.close()
|
||||
finally:
|
||||
await page.close()
|
||||
|
||||
# hti = Html2Image(
|
||||
# browser='chrome',
|
||||
|
||||
@ -2,9 +2,6 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% include 'style.html' %}
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;700&family=Source+Sans+3:wght@400;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="fullCard" style="width: 1200px; height: 600px;">
|
||||
|
||||
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user