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.staticfiles import StaticFiles
|
||||||
# from fastapi.templating import Jinja2Templates
|
# from fastapi.templating import Jinja2Templates
|
||||||
|
|
||||||
from .db_engine import db # noqa: E402
|
from .db_engine import db
|
||||||
from .routers_v2 import ( # noqa: E402
|
from .routers_v2.players import get_browser, shutdown_browser
|
||||||
|
from .routers_v2 import (
|
||||||
current,
|
current,
|
||||||
awards,
|
awards,
|
||||||
teams,
|
teams,
|
||||||
@ -55,9 +56,12 @@ from .routers_v2 import ( # noqa: E402
|
|||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app):
|
||||||
|
# Startup: warm up the persistent Chromium browser
|
||||||
|
await get_browser()
|
||||||
yield
|
yield
|
||||||
await players.shutdown_browser()
|
# Shutdown: clean up browser and playwright
|
||||||
|
await shutdown_browser()
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
|
|||||||
@ -9,7 +9,9 @@ from typing import Optional, List, Literal
|
|||||||
import logging
|
import logging
|
||||||
import pydantic
|
import pydantic
|
||||||
from pandas import DataFrame
|
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 ..card_creation import get_batter_card_data, get_pitcher_card_data
|
||||||
from ..db_engine import (
|
from ..db_engine import (
|
||||||
@ -31,30 +33,62 @@ from ..db_engine import (
|
|||||||
from ..db_helpers import upsert_players
|
from ..db_helpers import upsert_players
|
||||||
from ..dependencies import oauth2_scheme, valid_token
|
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
|
global _browser, _playwright
|
||||||
if _browser is not None and _browser.is_connected():
|
async with _browser_lock:
|
||||||
return _browser
|
if _browser is None or not _browser.is_connected():
|
||||||
if _playwright is None:
|
if _playwright is not None:
|
||||||
_playwright = await async_playwright().start()
|
try:
|
||||||
_browser = await _playwright.chromium.launch()
|
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
|
return _browser
|
||||||
|
|
||||||
|
|
||||||
async def shutdown_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
|
global _browser, _playwright
|
||||||
if _browser is not None:
|
if _browser:
|
||||||
await _browser.close()
|
try:
|
||||||
|
await _browser.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
_browser = None
|
_browser = None
|
||||||
if _playwright is not None:
|
if _playwright:
|
||||||
await _playwright.stop()
|
try:
|
||||||
|
await _playwright.stop()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
_playwright = None
|
_playwright = None
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
# Franchise normalization: Convert city+team names to city-agnostic team names
|
# Franchise normalization: Convert city+team names to city-agnostic team names
|
||||||
# This enables cross-era player matching (e.g., 'Oakland Athletics' -> 'Athletics')
|
# This enables cross-era player matching (e.g., 'Oakland Athletics' -> 'Athletics')
|
||||||
FRANCHISE_NORMALIZE = {
|
FRANCHISE_NORMALIZE = {
|
||||||
@ -318,21 +352,16 @@ async def get_players(
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
return_val = {"count": len(final_players), "players": []}
|
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:
|
for x in final_players:
|
||||||
this_record = model_to_dict(x, recurse=not (flat or short_output))
|
this_record = model_to_dict(x, recurse=not (flat or short_output))
|
||||||
|
|
||||||
if inc_dex:
|
if inc_dex:
|
||||||
entries = dex_by_player.get(x.player_id, [])
|
this_dex = Paperdex.select().where(Paperdex.player == x)
|
||||||
this_record["paperdex"] = {
|
this_record["paperdex"] = {"count": this_dex.count(), "paperdex": []}
|
||||||
"count": len(entries),
|
for y in this_dex:
|
||||||
"paperdex": [model_to_dict(y, recurse=False) for y in entries],
|
this_record["paperdex"]["paperdex"].append(
|
||||||
}
|
model_to_dict(y, recurse=False)
|
||||||
|
)
|
||||||
|
|
||||||
if inc_keys and (flat or short_output):
|
if inc_keys and (flat or short_output):
|
||||||
if this_record["mlbplayer"] is not None:
|
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")
|
return Response(content=return_val, media_type="text/csv")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
final_players = list(final_players)
|
|
||||||
return_val = {"count": len(final_players), "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:
|
for x in final_players:
|
||||||
this_record = model_to_dict(x)
|
this_record = model_to_dict(x)
|
||||||
|
|
||||||
entries = dex_by_player.get(x.player_id, [])
|
this_dex = Paperdex.select().where(Paperdex.player == x)
|
||||||
this_record["paperdex"] = {
|
this_record["paperdex"] = {"count": this_dex.count(), "paperdex": []}
|
||||||
"count": len(entries),
|
for y in this_dex:
|
||||||
"paperdex": [model_to_dict(y, recurse=False) for y in entries],
|
this_record["paperdex"]["paperdex"].append(
|
||||||
}
|
model_to_dict(y, recurse=False)
|
||||||
|
)
|
||||||
|
|
||||||
return_val["players"].append(this_record)
|
return_val["players"].append(this_record)
|
||||||
# return_val['players'].append(model_to_dict(x))
|
# return_val['players'].append(model_to_dict(x))
|
||||||
@ -708,6 +732,9 @@ async def get_batter_card(
|
|||||||
)
|
)
|
||||||
|
|
||||||
headers = {"Cache-Control": "public, max-age=86400"}
|
headers = {"Cache-Control": "public, max-age=86400"}
|
||||||
|
_filename = (
|
||||||
|
f"{this_player.description} {this_player.p_name} {card_type} {d}-v{variant}"
|
||||||
|
)
|
||||||
if (
|
if (
|
||||||
os.path.isfile(
|
os.path.isfile(
|
||||||
f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png"
|
f"storage/cards/cardset-{this_player.cardset.id}/{card_type}/{player_id}-{d}-v{variant}.png"
|
||||||
|
|||||||
@ -2,9 +2,6 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
{% include 'style.html' %}
|
{% 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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="fullCard" style="width: 1200px; height: 600px;">
|
<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