- Add card import feature with fuzzy matching - Implement deck CRUD and management endpoints - Add user data APIs for groups, networks, preferences, activity, replays - Create comprehensive API documentation (API_DOCUMENTATION.md) - Add ENDPOINT_AUDIT.md for endpoint verification - Update documentation (README, ROADMAP, state.json) - Update architecture blueprint and Cockatrice analysis - All Phase 2 deliverables complete and documented
174 lines
5.1 KiB
Python
174 lines
5.1 KiB
Python
"""
|
|
Card search router for MTG card database.
|
|
|
|
Provides endpoints for searching and retrieving MTG card data
|
|
with filters for type, set, and color.
|
|
"""
|
|
from typing import List, Optional, Dict, Any
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
|
|
from app.core.database import mtg_get_db
|
|
from app.core.redis_client import cache_get, cache_set
|
|
from app.services.card_search_service import CardSearchService
|
|
from app.services.deck_suggestion_service import DeckSuggestionService
|
|
from app.models.user_deck import UserDeck
|
|
from app.schemas.card_search_schemas import CardSearchResponse, CardResponse, SetResponse, CardTypeResponse
|
|
|
|
router = APIRouter(prefix="/api/cards", tags=["Card Search"])
|
|
|
|
|
|
@router.get("/search", response_model=CardSearchResponse)
|
|
async def search_cards_endpoint(
|
|
q: str = Query(..., min_length=1, description="Search query"),
|
|
card_type: Optional[str] = Query(None, description="Filter by card type"),
|
|
set_code: Optional[str] = Query(None, description="Filter by set code"),
|
|
color: Optional[str] = Query(None, description="Filter by color (e.g., WU, BR)"),
|
|
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 with filters.
|
|
|
|
Supports filtering by type, set, and color in addition to name search.
|
|
"""
|
|
cache_key = f"card_search:{q}:{card_type}:{set_code}:{color}:{limit}:{offset}"
|
|
|
|
# Check cache first
|
|
cached = await cache_get(cache_key)
|
|
if cached:
|
|
return {"cached": True, "results": cached}
|
|
|
|
# Search cards
|
|
results = await CardSearchService.search_cards(
|
|
db=db,
|
|
query=q,
|
|
card_type=card_type,
|
|
set_code=set_code,
|
|
color=color,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
|
|
# Cache results for 5 minutes
|
|
await cache_set(cache_key, str(results), ttl=300)
|
|
|
|
return {"cached": False, "results": results}
|
|
|
|
|
|
@router.get("/{card_id}", response_model=CardResponse)
|
|
async def get_card_endpoint(
|
|
card_id: int,
|
|
db: AsyncSession = Depends(mtg_get_db),
|
|
):
|
|
"""
|
|
Get a specific card by ID.
|
|
"""
|
|
cache_key = f"card_by_id:{card_id}"
|
|
|
|
# Check cache first
|
|
cached = await cache_get(cache_key)
|
|
if cached:
|
|
return {"cached": True, "results": cached}
|
|
|
|
# Get card
|
|
card = await CardSearchService.get_card_by_id(db, card_id)
|
|
|
|
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("/sets", response_model=List[SetResponse])
|
|
async def get_sets_endpoint(
|
|
db: AsyncSession = Depends(mtg_get_db),
|
|
):
|
|
"""
|
|
Get all available sets.
|
|
"""
|
|
cache_key = "all_sets:all"
|
|
|
|
cached = await cache_get(cache_key)
|
|
if cached:
|
|
return {"cached": True, "results": cached}
|
|
|
|
sets = await CardSearchService.get_sets(db)
|
|
|
|
# Cache for 1 hour
|
|
await cache_set(cache_key, str(sets), ttl=3600)
|
|
|
|
return {"cached": False, "results": sets}
|
|
|
|
|
|
@router.get("/types", response_model=List[CardTypeResponse])
|
|
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 CardSearchService.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", response_model=List[str])
|
|
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 CardSearchService.get_card_rarities(db)
|
|
|
|
# Cache for 30 minutes
|
|
await cache_set(cache_key, str(rarities), ttl=1800)
|
|
|
|
return {"cached": False, "results": rarities}
|
|
|
|
|
|
@router.get("/suggest", response_model=List[Dict[str, Any]])
|
|
async def suggest_cards_endpoint(
|
|
deck_id: int = Query(..., description="Deck ID to suggest cards for"),
|
|
limit: int = Query(20, ge=1, le=100, description="Maximum suggestions"),
|
|
db: AsyncSession = Depends(mtg_get_db),
|
|
):
|
|
"""
|
|
Suggest similar cards for a deck.
|
|
|
|
Matches by: same type, same color, same set, same mana cost,
|
|
and cards often paired in existing user decks.
|
|
"""
|
|
# Verify deck exists
|
|
stmt = select(UserDeck).where(UserDeck.id == deck_id)
|
|
result = await db.execute(stmt)
|
|
deck = result.scalar_one_or_none()
|
|
|
|
if not deck:
|
|
raise HTTPException(status_code=404, detail="Deck not found")
|
|
|
|
suggestions = await DeckSuggestionService.suggest_cards(db, deck_id, limit)
|
|
|
|
return suggestions
|