Merge main into next-release
Resolve conflicts in app/main.py and app/routers_v2/players.py: keep main's render pipeline optimization (Phase 0) with asyncio.Lock, error-tolerant shutdown, and --no-sandbox launch args. The next-release browser code was an earlier version of the same feature. Add evolution router import and inclusion from next-release. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
419fb757df
12
app/main.py
12
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(
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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