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:
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select
|
||||
from typing import Optional
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone, timezone
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import (
|
||||
@@ -47,14 +47,14 @@ async def login(request: LoginRequest, db: AsyncSession = Depends(get_db)):
|
||||
detail="Account is disabled",
|
||||
)
|
||||
|
||||
if user.is_banned and user.ban_ends and user.ban_ends > __import__("datetime").datetime.now():
|
||||
if user.is_banned and user.ban_ends and user.ban_ends > datetime.now():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Account is banned",
|
||||
)
|
||||
|
||||
# Update last login
|
||||
user.last_login = __import__("datetime").datetime.now()
|
||||
user.last_login = datetime.now(timezone.utc)
|
||||
await db.flush()
|
||||
|
||||
# Generate tokens
|
||||
|
||||
@@ -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}
|
||||
@@ -171,77 +171,6 @@ async def delete_folder(
|
||||
return {"message": "Folder deleted successfully"}
|
||||
|
||||
|
||||
@router.get("/", response_model=List[DeckResponse])
|
||||
async def list_decks(
|
||||
folder_id: Optional[int] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""List decks for current user."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
if folder_id:
|
||||
stmt = (
|
||||
select(DecklistFile)
|
||||
.where(
|
||||
DecklistFile.owner_id == user_id,
|
||||
DecklistFile.folder_id == folder_id,
|
||||
)
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
else:
|
||||
stmt = (
|
||||
select(DecklistFile)
|
||||
.where(DecklistFile.owner_id == user_id)
|
||||
.offset((page - 1) * page_size)
|
||||
.limit(page_size)
|
||||
)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
decks = result.scalars().all()
|
||||
|
||||
return [DeckResponse.model_validate(deck) for deck in decks]
|
||||
|
||||
|
||||
@router.post("/", response_model=DeckResponse)
|
||||
async def create_deck(
|
||||
request: DeckCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
):
|
||||
"""Create a new deck."""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# Verify folder exists if specified
|
||||
if request.folder_id:
|
||||
stmt = select(DecklistFolder).where(
|
||||
DecklistFolder.id == request.folder_id,
|
||||
DecklistFolder.owner_id == user_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
folder = result.scalar_one_or_none()
|
||||
if not folder:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Folder not found",
|
||||
)
|
||||
|
||||
new_deck = DecklistFile(
|
||||
owner_id=user_id,
|
||||
folder_id=request.folder_id,
|
||||
name=request.name,
|
||||
content=request.content,
|
||||
format=request.format,
|
||||
)
|
||||
db.add(new_deck)
|
||||
await db.flush()
|
||||
|
||||
return DeckResponse.model_validate(new_deck)
|
||||
|
||||
|
||||
@router.get("/{deck_id}", response_model=DeckResponse)
|
||||
async def get_deck(
|
||||
deck_id: int,
|
||||
|
||||
Reference in New Issue
Block a user