Final project commit: MTGJSON data integration and backend API
This commit is contained in:
+85
-127
@@ -1,37 +1,79 @@
|
||||
"""
|
||||
FastAPI application factory and middleware setup.
|
||||
MTG Online Backend Application
|
||||
|
||||
Configures CORS, authentication, and error handling.
|
||||
FastAPI application for the MTG Online multiplayer platform.
|
||||
Mounts all routers and provides centralized configuration.
|
||||
|
||||
## Routers
|
||||
- Authentication: /auth/*
|
||||
- Users: /users/*
|
||||
- Decks: /decks/*
|
||||
- Rooms: /rooms/*
|
||||
- Games: /games/*
|
||||
- Admin: /admin/*
|
||||
- MTG Cards: /api/cards/*
|
||||
- Card Interactions: /interactions/*
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import AsyncGenerator
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import JSONResponse
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from sqlalchemy import text
|
||||
|
||||
from app.core.settings import get_settings
|
||||
from app.routers import auth, users, decks, rooms, games, admin, card_router
|
||||
from app.core.database import engine, mtg_engine, async_session, mtg_async_session
|
||||
from app.routers import auth, users, decks, rooms, games, admin, card_router, interactions
|
||||
|
||||
settings = get_settings()
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=getattr(logging, settings.LOG_LEVEL))
|
||||
logger = logging.getLogger(__name__)
|
||||
def setup_logging(debug: bool = False) -> None:
|
||||
"""Configure application logging with verbose support."""
|
||||
level = logging.DEBUG if debug else logging.INFO
|
||||
|
||||
logging.basicConfig(
|
||||
level=level,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
handlers=[
|
||||
logging.StreamHandler(),
|
||||
]
|
||||
)
|
||||
|
||||
if debug:
|
||||
logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
|
||||
logging.getLogger('sqlalchemy.pool').setLevel(logging.WARNING)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"Logging initialized at level {logging.getLevelName(level)}")
|
||||
|
||||
|
||||
def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
||||
"""Application lifespan events for startup and shutdown."""
|
||||
settings = get_settings()
|
||||
|
||||
# Startup
|
||||
setup_logging(debug=settings.DEBUG)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.info(f"MTG Online Backend starting (v{settings.APP_VERSION})")
|
||||
logger.info(f"Database: {settings.DATABASE_URL.split('@')[1] if '@' in settings.DATABASE_URL else 'configured'}")
|
||||
logger.info(f"MTG Database: {settings.MTG_DATABASE_URL.split('@')[1] if '@' in settings.MTG_DATABASE_URL else 'configured'}")
|
||||
logger.info(f"Redis: {settings.REDIS_URL}")
|
||||
|
||||
yield
|
||||
|
||||
# Shutdown
|
||||
logger.info("Shutting down MTG Online Backend")
|
||||
# Engine disposal is handled by FastAPI's shutdown events
|
||||
|
||||
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.APP_VERSION,
|
||||
docs_url="/docs",
|
||||
redoc_url="/redoc",
|
||||
title="MTG Online Backend API",
|
||||
description="Backend API for the MTG Online multiplayer platform",
|
||||
version="0.2.0",
|
||||
lifespan=lifespan,
|
||||
)
|
||||
|
||||
# CORS configuration
|
||||
# CORS middleware
|
||||
settings = get_settings()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
@@ -40,116 +82,32 @@ app.add_middleware(
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Track initialization status
|
||||
mtg_data_ready = False
|
||||
mtg_data_count = 0
|
||||
|
||||
# Mount all routers
|
||||
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||
app.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
app.include_router(decks.router, prefix="/decks", tags=["Decks"])
|
||||
app.include_router(rooms.router, prefix="/rooms", tags=["Rooms"])
|
||||
app.include_router(games.router, prefix="/games", tags=["Games"])
|
||||
app.include_router(admin.router, prefix="/admin", tags=["Admin"])
|
||||
app.include_router(card_router.router, prefix="/api", tags=["MTG Cards"])
|
||||
app.include_router(interactions.router, tags=["Card Interactions"])
|
||||
|
||||
|
||||
async def check_mtg_data_count() -> int:
|
||||
"""Check if MTG data has been loaded."""
|
||||
try:
|
||||
engine = create_async_engine(settings.MTG_DATABASE_URL)
|
||||
async with AsyncSession(engine) as session:
|
||||
stmt = text("SELECT COUNT(*) FROM mtg_cards")
|
||||
result = await session.execute(stmt)
|
||||
count = result.scalar()
|
||||
await engine.dispose()
|
||||
return count or 0
|
||||
except Exception as e:
|
||||
logger.error(f"Error checking MTG data count: {e}")
|
||||
return 0
|
||||
|
||||
|
||||
async def trigger_initial_mtg_download():
|
||||
"""Trigger initial MTGJSON download on first startup."""
|
||||
global mtg_data_ready, mtg_data_count
|
||||
|
||||
logger.info("Checking MTG data status...")
|
||||
count = await check_mtg_data_count()
|
||||
|
||||
if count == 0:
|
||||
logger.info("No MTG data found. Triggering initial download...")
|
||||
mtg_data_ready = False
|
||||
|
||||
try:
|
||||
# Import and run the refresh script
|
||||
from app.scripts.refresh_mtg import main as refresh_main
|
||||
await refresh_main()
|
||||
|
||||
# Re-check count after download
|
||||
mtg_data_count = await check_mtg_data_count()
|
||||
mtg_data_ready = mtg_data_count > 0
|
||||
|
||||
if mtg_data_ready:
|
||||
logger.info(f"MTG data loaded successfully: {mtg_data_count} cards")
|
||||
else:
|
||||
logger.warning("MTG data download completed but no cards found")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to load MTG data: {e}")
|
||||
mtg_data_ready = False
|
||||
else:
|
||||
logger.info(f"MTG data already loaded: {count} cards")
|
||||
mtg_data_ready = True
|
||||
mtg_data_count = count
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""Run initial MTG data download on startup."""
|
||||
logger.info("Starting MTG Online backend...")
|
||||
|
||||
# Run MTG data download in background to not block startup
|
||||
asyncio.create_task(trigger_initial_mtg_download())
|
||||
|
||||
|
||||
# Global exception handlers
|
||||
@app.exception_handler(Exception)
|
||||
async def global_exception_handler(request, exc):
|
||||
"""Handle unhandled exceptions gracefully."""
|
||||
return JSONResponse(
|
||||
status_code=500,
|
||||
content={"detail": "Internal server error"},
|
||||
)
|
||||
|
||||
|
||||
# Include routers
|
||||
app.include_router(auth.router, prefix="/api/v1/auth", tags=["Authentication"])
|
||||
app.include_router(card_router.router, prefix="/api/v1/mtg/cards", tags=["MTG Cards"])
|
||||
app.include_router(users.router, prefix="/api/v1/users", tags=["Users"])
|
||||
app.include_router(decks.router, prefix="/api/v1/decks", tags=["Decks"])
|
||||
app.include_router(rooms.router, prefix="/api/v1/rooms", tags=["Rooms"])
|
||||
app.include_router(games.router, prefix="/api/v1/games", tags=["Games"])
|
||||
app.include_router(admin.router, prefix="/api/v1/admin", tags=["Admin"])
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check() -> Dict[str, Any]:
|
||||
"""Health check endpoint with MTG data status."""
|
||||
health_status = {
|
||||
@app.get("/health", tags=["Health"])
|
||||
async def health_check():
|
||||
"""Health check endpoint."""
|
||||
return {
|
||||
"status": "healthy",
|
||||
"version": settings.APP_VERSION,
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
}
|
||||
|
||||
# Check database connectivity
|
||||
try:
|
||||
engine = create_async_engine(settings.DATABASE_URL)
|
||||
async with AsyncSession(engine) as session:
|
||||
await session.execute(text("SELECT 1"))
|
||||
await engine.dispose()
|
||||
health_status["database"] = "connected"
|
||||
except Exception as e:
|
||||
health_status["status"] = "degraded"
|
||||
health_status["database"] = f"error: {str(e)}"
|
||||
|
||||
# Check MTG data status
|
||||
health_status["mtg_data"] = {
|
||||
"ready": mtg_data_ready,
|
||||
"count": mtg_data_count,
|
||||
|
||||
|
||||
@app.get("/", tags=["Root"])
|
||||
async def root():
|
||||
"""Root endpoint with API information."""
|
||||
return {
|
||||
"name": settings.APP_NAME,
|
||||
"version": settings.APP_VERSION,
|
||||
"docs": "/docs",
|
||||
}
|
||||
|
||||
if not mtg_data_ready:
|
||||
health_status["status"] = "initializing"
|
||||
|
||||
return health_status
|
||||
|
||||
Reference in New Issue
Block a user