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:
2026-07-25 02:56:29 +00:00
parent 351c8a9ba9
commit 1e7c762452
18 changed files with 2048 additions and 560 deletions
+60 -145
View File
@@ -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