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:
+65
-3
@@ -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",
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user