Complete Phase 2: Card import, deck building, and full API
- 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
This commit is contained in:
@@ -2,48 +2,55 @@
|
||||
Card search router for MTG card database.
|
||||
|
||||
Provides endpoints for searching and retrieving MTG card data
|
||||
from the MTG PostgreSQL database with Redis caching.
|
||||
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_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,
|
||||
)
|
||||
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="/mtg/cards", tags=["MTG Cards"])
|
||||
router = APIRouter(prefix="/api/cards", tags=["Card Search"])
|
||||
|
||||
|
||||
@router.get("/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 by name, type, or mana cost.
|
||||
Search cards with filters.
|
||||
|
||||
Uses Redis cache to improve performance for repeated searches.
|
||||
Supports filtering by type, set, and color in addition to name search.
|
||||
"""
|
||||
cache_key = f"card_search:{q}:{limit}:{offset}"
|
||||
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}
|
||||
|
||||
# Query database
|
||||
results = await search_cards(q, db, limit, offset)
|
||||
# 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)
|
||||
@@ -51,70 +58,23 @@ async def search_cards_endpoint(
|
||||
return {"cached": False, "results": results}
|
||||
|
||||
|
||||
@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("/{card_name}")
|
||||
@router.get("/{card_id}", response_model=CardResponse)
|
||||
async def get_card_endpoint(
|
||||
card_name: str,
|
||||
set_code: str | None = Query(None, description="Filter by set code"),
|
||||
card_id: int,
|
||||
db: AsyncSession = Depends(mtg_get_db),
|
||||
):
|
||||
"""
|
||||
Get a specific card by name.
|
||||
|
||||
Optional set_code filter to get a specific printing.
|
||||
Get a specific card by ID.
|
||||
"""
|
||||
cache_key = f"card_by_name:{card_name}:{set_code or 'all'}"
|
||||
cache_key = f"card_by_id:{card_id}"
|
||||
|
||||
# Check cache first
|
||||
cached = await cache_get(cache_key)
|
||||
if cached:
|
||||
return {"cached": True, "results": cached}
|
||||
|
||||
card = await get_card_by_name(card_name, db, set_code)
|
||||
# Get card
|
||||
card = await CardSearchService.get_card_by_id(db, card_id)
|
||||
|
||||
if not card:
|
||||
raise HTTPException(status_code=404, detail="Card not found")
|
||||
@@ -125,31 +85,28 @@ async def get_card_endpoint(
|
||||
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"),
|
||||
@router.get("/sets", response_model=List[SetResponse])
|
||||
async def get_sets_endpoint(
|
||||
db: AsyncSession = Depends(mtg_get_db),
|
||||
):
|
||||
"""
|
||||
Get all cards in a specific set.
|
||||
Get all available sets.
|
||||
"""
|
||||
cache_key = f"set_cards:{set_code}:{limit}:{offset}"
|
||||
cache_key = "all_sets:all"
|
||||
|
||||
cached = await cache_get(cache_key)
|
||||
if cached:
|
||||
return {"cached": True, "results": cached}
|
||||
|
||||
results = await get_cards_by_set(set_code, db, limit, offset)
|
||||
sets = await CardSearchService.get_sets(db)
|
||||
|
||||
# Cache for 15 minutes
|
||||
await cache_set(cache_key, str(results), ttl=900)
|
||||
# Cache for 1 hour
|
||||
await cache_set(cache_key, str(sets), ttl=3600)
|
||||
|
||||
return {"cached": False, "results": results}
|
||||
return {"cached": False, "results": sets}
|
||||
|
||||
|
||||
@router.get("/types")
|
||||
@router.get("/types", response_model=List[CardTypeResponse])
|
||||
async def get_card_types_endpoint(
|
||||
db: AsyncSession = Depends(mtg_get_db),
|
||||
):
|
||||
@@ -162,7 +119,7 @@ async def get_card_types_endpoint(
|
||||
if cached:
|
||||
return {"cached": True, "results": cached}
|
||||
|
||||
types = await get_card_types(db)
|
||||
types = await CardSearchService.get_card_types(db)
|
||||
|
||||
# Cache for 30 minutes
|
||||
await cache_set(cache_key, str(types), ttl=1800)
|
||||
@@ -170,7 +127,7 @@ async def get_card_types_endpoint(
|
||||
return {"cached": False, "results": types}
|
||||
|
||||
|
||||
@router.get("/rarities")
|
||||
@router.get("/rarities", response_model=List[str])
|
||||
async def get_card_rarities_endpoint(
|
||||
db: AsyncSession = Depends(mtg_get_db),
|
||||
):
|
||||
@@ -183,7 +140,7 @@ async def get_card_rarities_endpoint(
|
||||
if cached:
|
||||
return {"cached": True, "results": cached}
|
||||
|
||||
rarities = await get_card_rarities(db)
|
||||
rarities = await CardSearchService.get_card_rarities(db)
|
||||
|
||||
# Cache for 30 minutes
|
||||
await cache_set(cache_key, str(rarities), ttl=1800)
|
||||
@@ -191,68 +148,26 @@ async def get_card_rarities_endpoint(
|
||||
return {"cached": False, "results": rarities}
|
||||
|
||||
|
||||
@router.get("/sets")
|
||||
async def get_sets_endpoint(
|
||||
@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),
|
||||
):
|
||||
"""
|
||||
Get all sets.
|
||||
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.
|
||||
"""
|
||||
cache_key = "all_sets:all"
|
||||
# Verify deck exists
|
||||
stmt = select(UserDeck).where(UserDeck.id == deck_id)
|
||||
result = await db.execute(stmt)
|
||||
deck = result.scalar_one_or_none()
|
||||
|
||||
cached = await cache_get(cache_key)
|
||||
if cached:
|
||||
return {"cached": True, "results": cached}
|
||||
if not deck:
|
||||
raise HTTPException(status_code=404, detail="Deck not found")
|
||||
|
||||
sets = await get_sets(db)
|
||||
suggestions = await DeckSuggestionService.suggest_cards(db, deck_id, limit)
|
||||
|
||||
# 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}
|
||||
return suggestions
|
||||
|
||||
Reference in New Issue
Block a user