feat: MTG database integration with Redis caching

- Added MTG card ORM models (mtg_cards, mtg_sets tables)
- Created card_database service with search, get_by_name, get_by_set
- Added Redis client with caching layer (3600s TTL default)
- Created card router with caching on all endpoints:
  - Search cards (5min cache)
  - Get card by name (10min cache)
  - Get cards by set (15min cache)
  - Get card types/rarities (30min cache)
  - Get sets (1hr cache)
  - Get statistics (1hr cache)
- Updated settings.py:
  - Added JWT_SECRET_KEY field
  - Added DB_CONFIG and REDIS_CONFIG dictionaries
- Updated security.py to use JWT_SECRET_KEY with fallback
- Updated auth.py to use timezone-aware datetimes
- Updated refresh_mtg.py to use settings instead of os.environ
- Updated mtg_monitor.py to use settings for connections
- Added services package with __init__.py

All 20 tests passing.
This commit is contained in:
2026-07-18 18:41:05 +00:00
parent 167a352d44
commit 915242b330
20 changed files with 1804 additions and 287 deletions
+212
View File
@@ -0,0 +1,212 @@
"""
Card search router for MTG card database.
Provides endpoints for searching and retrieving MTG card data
from the MTG PostgreSQL database with Redis caching.
"""
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.database import mtg_get_db
from app.core.redis_client import cache_get, cache_set
from app.services.card_database import (
search_cards,
get_card_by_name,
get_cards_by_set,
get_card_types,
get_card_rarities,
get_sets,
get_set_by_code,
get_card_statistics,
)
router = APIRouter(prefix="/mtg/cards", tags=["MTG Cards"])
@router.get("/search")
async def search_cards_endpoint(
q: str = Query(..., min_length=1, description="Search query"),
limit: int = Query(100, ge=1, le=500, description="Maximum results"),
offset: int = Query(0, ge=0, description="Number of results to skip"),
db: AsyncSession = Depends(mtg_get_db),
):
"""
Search cards by name, type, or mana cost.
Uses Redis cache to improve performance for repeated searches.
"""
cache_key = f"card_search:{q}:{limit}:{offset}"
# Check cache first
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
# Query database
results = await search_cards(q, db, limit, offset)
# Cache results for 5 minutes
await cache_set(cache_key, str(results), ttl=300)
return {"cached": False, "results": results}
@router.get("/{card_name}")
async def get_card_endpoint(
card_name: str,
set_code: str | None = Query(None, description="Filter by set code"),
db: AsyncSession = Depends(mtg_get_db),
):
"""
Get a specific card by name.
Optional set_code filter to get a specific printing.
"""
cache_key = f"card_by_name:{card_name}:{set_code or 'all'}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
card = await get_card_by_name(card_name, db, set_code)
if not card:
raise HTTPException(status_code=404, detail="Card not found")
# Cache for 10 minutes
await cache_set(cache_key, str(card), ttl=600)
return {"cached": False, "results": card}
@router.get("/set/{set_code}")
async def get_cards_by_set_endpoint(
set_code: str,
limit: int = Query(1000, ge=1, le=5000, description="Maximum results"),
offset: int = Query(0, ge=0, description="Number of results to skip"),
db: AsyncSession = Depends(mtg_get_db),
):
"""
Get all cards in a specific set.
"""
cache_key = f"set_cards:{set_code}:{limit}:{offset}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
results = await get_cards_by_set(set_code, db, limit, offset)
# Cache for 15 minutes
await cache_set(cache_key, str(results), ttl=900)
return {"cached": False, "results": results}
@router.get("/types")
async def get_card_types_endpoint(
db: AsyncSession = Depends(mtg_get_db),
):
"""
Get all unique card types.
"""
cache_key = "card_types:all"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
types = await get_card_types(db)
# Cache for 30 minutes
await cache_set(cache_key, str(types), ttl=1800)
return {"cached": False, "results": types}
@router.get("/rarities")
async def get_card_rarities_endpoint(
db: AsyncSession = Depends(mtg_get_db),
):
"""
Get all unique card rarities.
"""
cache_key = "card_rarities:all"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
rarities = await get_card_rarities(db)
# Cache for 30 minutes
await cache_set(cache_key, str(rarities), ttl=1800)
return {"cached": False, "results": rarities}
@router.get("/sets")
async def get_sets_endpoint(
db: AsyncSession = Depends(mtg_get_db),
):
"""
Get all sets.
"""
cache_key = "all_sets:all"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
sets = await get_sets(db)
# Cache for 1 hour
await cache_set(cache_key, str(sets), ttl=3600)
return {"cached": False, "results": sets}
@router.get("/sets/{set_code}")
async def get_set_endpoint(
set_code: str,
db: AsyncSession = Depends(mtg_get_db),
):
"""
Get a specific set by code.
"""
cache_key = f"set_by_code:{set_code}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
mtg_set = await get_set_by_code(set_code, db)
if not mtg_set:
raise HTTPException(status_code=404, detail="Set not found")
# Cache for 1 hour
await cache_set(cache_key, str(mtg_set), ttl=3600)
return {"cached": False, "results": mtg_set}
@router.get("/statistics")
async def get_card_statistics_endpoint(
db: AsyncSession = Depends(mtg_get_db),
):
"""
Get overall card database statistics.
"""
cache_key = "card_statistics:all"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
stats = await get_card_statistics(db)
# Cache for 1 hour
await cache_set(cache_key, str(stats), ttl=3600)
return {"cached": False, "results": stats}