feat: MTGJSON data manager service with download, unpack, and upsert

- Created MTGJSONManager service for complete data lifecycle
- Handles download, unpack (gzip/zip), and PostgreSQL upsert
- ON CONFLICT DO UPDATE preserves existing data
- Startup triggers initial download on first container init
- Health check verifies MTG data exists in database
- Weekly refresh via MTG_REFRESH_INTERVAL_DAYS setting
- Updated docker-compose start_period to 600s for download time
This commit is contained in:
2026-07-20 03:17:23 +00:00
parent bb231a5f5d
commit 99c7d08bb1
4 changed files with 770 additions and 246 deletions
+65 -3
View File
@@ -14,6 +14,7 @@ Mounts all routers and provides centralized configuration.
- MTG Cards: /api/cards/*
- Card Interactions: /interactions/*
"""
import asyncio
import logging
from contextlib import asynccontextmanager
from typing import AsyncGenerator
@@ -24,6 +25,7 @@ from fastapi.middleware.cors import CORSMiddleware
from app.core.settings import get_settings
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
from app.services.mtgjson_manager import MTGJSONManager
def setup_logging(debug: bool = False) -> None:
@@ -46,6 +48,46 @@ def setup_logging(debug: bool = False) -> None:
logger.info(f"Logging initialized at level {logging.getLevelName(level)}")
async def run_initial_download():
"""Run initial MTGJSON data download and upsert."""
logger = logging.getLogger(__name__)
try:
settings = get_settings()
manager = MTGJSONManager(settings.DATA_DIR)
# Check if data already exists
last_refresh = await manager.get_last_refresh()
if last_refresh:
logger.info(f"MTGJSON data already exists (last refresh: {last_refresh})")
return
logger.info("Running initial MTGJSON data download...")
logger.info("This may take several minutes depending on network speed...")
# Download files
success = await manager.download_files()
if not success:
logger.error("Failed to download MTGJSON files")
return
# Unpack files
await manager.unpack_files()
# Upsert data
counts = await manager.upsert_data()
# Log success
await manager.log_refresh("SUCCESS", counts, 0)
logger.info(f"Initial MTGJSON data load complete!")
logger.info(f" Sets: {counts.get('sets', 0)}")
logger.info(f" Cards: {counts.get('cards', 0)}")
except Exception as e:
logger.error(f"Failed to run initial download: {e}")
raise
def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
"""Application lifespan events for startup and shutdown."""
settings = get_settings()
@@ -58,6 +100,13 @@ def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
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}")
# Run initial download in background
try:
asyncio.create_task(run_initial_download())
except RuntimeError:
# Event loop already running
asyncio.get_event_loop().create_task(run_initial_download())
yield
# Shutdown
@@ -96,10 +145,23 @@ app.include_router(interactions.router, tags=["Card Interactions"])
@app.get("/health", tags=["Health"])
async def health_check():
"""Health check endpoint."""
"""Health check endpoint with MTGJSON data status."""
from app.services.mtgjson_manager import get_manager
# Get MTGJSON health status
try:
manager = get_manager()
mtg_status = await manager.get_health_status()
except Exception as e:
mtg_status = {
"status": "unhealthy",
"error": str(e),
}
return {
"status": "healthy",
"status": "healthy" if mtg_status.get("status") == "healthy" else "degraded",
"version": settings.APP_VERSION,
"mtgjson": mtg_status,
}
@@ -110,4 +172,4 @@ async def root():
"name": settings.APP_NAME,
"version": settings.APP_VERSION,
"docs": "/docs",
}
}