Files
akadmin 900f51af3a Complete architecture simplification: 54+ files → 37 files
- Removed 11 duplicate router files (auth.py, users.py, decks.py, card_router.py, interactions.py, user_data.py, rooms.py, games/router.py, refresh.py, card_import.py, ws.py)
- Removed duplicate core files (logging_config.py, settings.py)
- Removed unused services (card_mirror_service.py, mtgjson_loader.py, mtgjson_uploader.py)
- Removed unused schemas (proto_messages.py, protocol_constants.py)
- Created run.py startup script for simple execution
- Created app/core/config.py for unified configuration management
- Created app/core/logging.py for logging configuration
- Consolidated routers into 6 focused files (auth_router.py, users_router.py, decks_router.py, cards_router.py, user_data_router.py, games_router.py)
- Updated all imports to use consolidated model paths
- Added backward compatibility aliases in database.py
- Created SIMPLIFICATION_SUMMARY.md documentation

Architecture improvements:
- Single unified database engine with auto-detection (SQLite/PostgreSQL)
- Consolidated models into 2 files (models.py, mtg_models.py)
- Clean router structure with one file per domain
- Simplified configuration with sensible defaults
- SQLite by default for development, PostgreSQL for production

Testing:
- All imports verified working
- App loads successfully
- Ready for development and testing
2026-08-26 15:15:36 +00:00

996 lines
31 KiB
Python

"""
Cards Router - Consolidated Card Data and Interaction Endpoints.
This module provides two FastAPI routers for the MTG Online card database:
1. **Card Search Router** (`/api/cards`) - Card data endpoints:
- Search cards with filters and fuzzy matching fallback
- Retrieve individual cards by ID
- List available sets, card types, and rarities
- Suggest cards for a given deck
2. **Card Interactions Router** (`/interactions`) - Interaction data endpoints:
- Get synergies, counters, and evolutions for a card
- Search interactions with multi-filter support
- Get interaction recommendations
- Retrieve aggregated interaction statistics
The card search endpoints use SQLAlchemy ORM models and dedicated services
with Redis caching. The interaction endpoints use raw SQL queries against
the `mtg_card_*` tables, also with Redis caching.
Both routers share the `mtg_get_db` dependency (secondary database session).
"""
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy import select, text
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.models.mtg_models import MtgCard
from app.models.models import UserDeck
from app.schemas.card_search_schemas import (
CardResponse,
CardSearchResponse,
CardTypeResponse,
SetResponse,
)
from app.services.card_search_service import CardSearchService
from app.services.deck_suggestion_service import DeckSuggestionService
from app.services.fuzzy_card_matcher import FuzzyCardMatcher
# ---------------------------------------------------------------------------
# Card Search Router (/api/cards)
# ---------------------------------------------------------------------------
card_router = APIRouter(prefix="/api/cards", tags=["Card Search"])
@card_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),
) -> Dict[str, Any]:
"""
Search cards with filters.
Supports filtering by type, set, and color in addition to name search.
Uses fuzzy matching as a fallback when exact/partial matches are not found.
"""
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}
# Normalize the search query for consistency
normalized_query = FuzzyCardMatcher.normalize_card_name(q)
# Search cards using the existing service
results = await CardSearchService.search_cards(
db=db,
query=q,
card_type=card_type,
set_code=set_code,
color=color,
limit=limit,
offset=offset,
)
# If no results found, try fuzzy matching as a fallback
if results["total"] == 0:
fuzzy_results = await _fuzzy_search_fallback(
db=db,
query=q,
normalized_query=normalized_query,
card_type=card_type,
set_code=set_code,
color=color,
limit=limit,
offset=offset,
)
results = fuzzy_results
# Add fuzzy matching metadata to results
results["query_normalized"] = normalized_query
results["fuzzy_match"] = True
# Cache results for 5 minutes
await cache_set(cache_key, str(results), ttl=300)
return {"cached": False, "results": results}
async def _fuzzy_search_fallback(
db: AsyncSession,
query: str,
normalized_query: str,
card_type: Optional[str] = None,
set_code: Optional[str] = None,
color: Optional[str] = None,
limit: int = 100,
offset: int = 0,
) -> Dict[str, Any]:
"""
Fallback fuzzy search when exact/partial matches yield no results.
Fetches all cards matching the type/set/color filters, then uses
fuzzy matching to find the best card name matches.
"""
# Fetch candidate cards based on non-name filters
conditions: list = []
if card_type:
conditions.append(MtgCard.type_line.ilike(f"%{card_type}%"))
if set_code:
conditions.append(MtgCard.set_code == set_code)
if color:
colors = [c.strip() for c in color.upper().split(",")]
for c in colors:
if c in ["W", "U", "B", "R", "G"]:
conditions.append(MtgCard.colors.ilike(f"%{c}%"))
# If no filters, fetch a broader set for fuzzy matching
if not conditions:
stmt = select(MtgCard).limit(limit * 5)
else:
stmt = select(MtgCard).where(*conditions).limit(limit * 5)
result = await db.execute(stmt)
candidate_cards = result.scalars().all()
if not candidate_cards:
return {
"cards": [],
"total": 0,
"page": offset // limit + 1,
"page_size": limit,
"total_pages": 0,
"fuzzy_fallback": True,
"message": "No cards found matching your query.",
}
# Build candidate name list and lookup
candidate_names = [card.name for card in candidate_cards if card.name]
card_lookup: Dict[str, Any] = {card.name.lower(): card for card in candidate_cards if card.name}
# Use fuzzy matching to find best matches
matched_name, confidence, match_type = FuzzyCardMatcher.find_best_match(
normalized_query, candidate_names, threshold=FuzzyCardMatcher.MIN_MATCH_THRESHOLD
)
# Build results from fuzzy matches
card_list: List[Dict[str, Any]] = []
if matched_name and matched_name.lower() in card_lookup:
card = card_lookup[matched_name.lower()]
card_data: Dict[str, Any] = {
"id": card.id,
"name": card.name,
"mana_cost": card.mana_cost,
"type_line": card.type_line,
"oracle_text": card.oracle_text,
"power": card.power,
"toughness": card.toughness,
"rarity": card.rarity,
"layout": card.layout,
"colors": card.colors,
"set_code": card.set_code,
"set_name": card.set_name,
"fuzzy_match": True,
"match_confidence": confidence,
"match_type": match_type,
"original_query": query,
}
card_list.append(card_data)
return {
"cards": card_list,
"total": len(card_list),
"page": offset // limit + 1,
"page_size": limit,
"total_pages": (len(card_list) + limit - 1) // limit if card_list else 0,
"fuzzy_fallback": True,
"match_type": match_type,
"confidence": confidence,
}
@card_router.get("/{card_id}", response_model=CardResponse)
async def get_card_endpoint(
card_id: int,
db: AsyncSession = Depends(mtg_get_db),
) -> Dict[str, Any]:
"""
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}
@card_router.get("/sets", response_model=List[SetResponse])
async def get_sets_endpoint(
db: AsyncSession = Depends(mtg_get_db),
) -> Dict[str, Any]:
"""
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}
@card_router.get("/types", response_model=List[CardTypeResponse])
async def get_card_types_endpoint(
db: AsyncSession = Depends(mtg_get_db),
) -> Dict[str, Any]:
"""
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}
@card_router.get("/rarities", response_model=List[str])
async def get_card_rarities_endpoint(
db: AsyncSession = Depends(mtg_get_db),
) -> Dict[str, Any]:
"""
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}
@card_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),
) -> List[Dict[str, Any]]:
"""
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
# ---------------------------------------------------------------------------
# Card Interactions Router (/interactions)
# ---------------------------------------------------------------------------
interaction_router = APIRouter(prefix="/interactions", tags=["Card Interactions"])
@interaction_router.get("/synergies/{card_id}")
async def get_card_synergies(
card_id: int,
synergy_type: Optional[str] = Query(
None, description="Filter by synergy type (archetype, mechanic, mana, combo)"
),
min_strength: int = Query(1, ge=1, le=5, description="Minimum synergy strength"),
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),
) -> Dict[str, Any]:
"""
Get synergies for a specific card.
Synergies are positive interactions where cards work well together.
"""
cache_key = f"synergies:{card_id}:{synergy_type}:{min_strength}:{limit}:{offset}"
try:
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
# Build query
conditions: list[str] = ["card_a_id = :card_id OR card_b_id = :card_id"]
params: Dict[str, Any] = {"card_id": card_id}
if synergy_type:
conditions.append("synergy_type = :synergy_type")
params["synergy_type"] = synergy_type
if min_strength:
conditions.append("strength >= :min_strength")
params["min_strength"] = min_strength
where_clause = " AND ".join(conditions)
query = f"""
SELECT id, card_a_id, card_b_id, synergy_type, strength, notes, confidence
FROM mtg_card_synergies
WHERE {where_clause}
ORDER BY strength DESC, confidence DESC
LIMIT :limit OFFSET :offset
"""
params["limit"] = limit
params["offset"] = offset
# Execute query
result = await db.execute(text(query), params)
rows = result.fetchall()
synergies: List[Dict[str, Any]] = []
for row in rows:
synergies.append({
"id": row[0],
"card_a_id": row[1],
"card_b_id": row[2],
"synergy_type": row[3],
"strength": row[4],
"notes": row[5],
"confidence": row[6],
})
# Get count for pagination
count_query = f"""
SELECT COUNT(*)
FROM mtg_card_synergies
WHERE {where_clause}
"""
count_result = await db.execute(text(count_query), params)
total = count_result.scalar()
# Cache for 10 minutes
await cache_set(cache_key, {"synergies": synergies, "total": total}, ttl=600)
return {
"cached": False,
"results": synergies,
"pagination": {
"total": total,
"limit": limit,
"offset": offset,
},
}
except Exception as e:
raise HTTPException(status_code=500, detail=f"Error fetching synergies: {str(e)}")
@interaction_router.get("/counters/{card_id}")
async def get_card_counters(
card_id: int,
counter_type: Optional[str] = Query(
None, description="Filter by counter type (color, stats, spell, keyword)"
),
min_strength: int = Query(1, ge=1, le=5, description="Minimum counter strength"),
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),
) -> Dict[str, Any]:
"""
Get counters for a specific card.
Counters are negative interactions where one card is disadvantaged by another.
"""
cache_key = f"counters:{card_id}:{counter_type}:{min_strength}:{limit}:{offset}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
conditions: list[str] = ["card_a_id = :card_id OR card_b_id = :card_id"]
params: Dict[str, Any] = {"card_id": card_id}
if counter_type:
conditions.append("counter_type = :counter_type")
params["counter_type"] = counter_type
if min_strength:
conditions.append("strength >= :min_strength")
params["min_strength"] = min_strength
where_clause = " AND ".join(conditions)
query = f"""
SELECT id, card_a_id, card_b_id, counter_type, strength, notes, confidence
FROM mtg_card_counters
WHERE {where_clause}
ORDER BY strength DESC, confidence DESC
LIMIT :limit OFFSET :offset
"""
params["limit"] = limit
params["offset"] = offset
result = await db.execute(text(query), params)
rows = result.fetchall()
counters: List[Dict[str, Any]] = []
for row in rows:
counters.append({
"id": row[0],
"card_a_id": row[1],
"card_b_id": row[2],
"counter_type": row[3],
"strength": row[4],
"notes": row[5],
"confidence": row[6],
})
count_query = f"""
SELECT COUNT(*)
FROM mtg_card_counters
WHERE {where_clause}
"""
count_result = await db.execute(text(count_query), params)
total = count_result.scalar()
await cache_set(cache_key, {"counters": counters, "total": total}, ttl=600)
return {
"cached": False,
"results": counters,
"pagination": {
"total": total,
"limit": limit,
"offset": offset,
},
}
@interaction_router.get("/evolutions/{card_id}")
async def get_card_evolutions(
card_id: int,
evolution_type: Optional[str] = Query(
None, description="Filter by evolution type (reprint, transform, double_sided)"
),
min_strength: int = Query(1, ge=1, le=5, description="Minimum strength"),
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),
) -> Dict[str, Any]:
"""
Get evolutions for a specific card.
Evolutions track when a card has been reprinted, transformed, or evolved.
"""
cache_key = f"evolutions:{card_id}:{evolution_type}:{min_strength}:{limit}:{offset}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
conditions: list[str] = ["card_id = :card_id"]
params: Dict[str, Any] = {"card_id": card_id}
if evolution_type:
conditions.append("evolution_type = :evolution_type")
params["evolution_type"] = evolution_type
if min_strength:
conditions.append("strength >= :min_strength")
params["min_strength"] = min_strength
where_clause = " AND ".join(conditions)
query = f"""
SELECT id, card_id, evolved_card_id, evolution_type, strength, notes, confidence
FROM mtg_card_evolution
WHERE {where_clause}
ORDER BY strength DESC, confidence DESC
LIMIT :limit OFFSET :offset
"""
params["limit"] = limit
params["offset"] = offset
result = await db.execute(text(query), params)
rows = result.fetchall()
evolutions: List[Dict[str, Any]] = []
for row in rows:
evolutions.append({
"id": row[0],
"card_id": row[1],
"evolved_card_id": row[2],
"evolution_type": row[3],
"strength": row[4],
"notes": row[5],
"confidence": row[6],
})
count_query = f"""
SELECT COUNT(*)
FROM mtg_card_evolution
WHERE {where_clause}
"""
count_result = await db.execute(text(count_query), params)
total = count_result.scalar()
await cache_set(cache_key, {"evolutions": evolutions, "total": total}, ttl=600)
return {
"cached": False,
"results": evolutions,
"pagination": {
"total": total,
"limit": limit,
"offset": offset,
},
}
@interaction_router.get("/recommend/{card_id}")
async def get_card_recommendations(
card_id: int,
recommendation_type: str = Query(
"synergy", description="Type of recommendation (synergy, counter, evolution)"
),
limit: int = Query(10, ge=1, le=100, description="Maximum results"),
db: AsyncSession = Depends(mtg_get_db),
) -> Dict[str, Any]:
"""
Get interaction recommendations for a card.
Provides cards that work well together or counter a specific card.
"""
cache_key = f"recommend:{card_id}:{recommendation_type}:{limit}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
if recommendation_type == "synergy":
# Get cards that synergize with this card
query = """
SELECT
CASE WHEN card_a_id = :card_id THEN card_b_id ELSE card_a_id END as recommended_card_id,
strength,
synergy_type,
confidence
FROM mtg_card_synergies
WHERE card_a_id = :card_id OR card_b_id = :card_id
ORDER BY strength DESC, confidence DESC
LIMIT :limit
"""
elif recommendation_type == "counter":
# Get cards that counter this card
query = """
SELECT
CASE WHEN card_a_id = :card_id THEN card_b_id ELSE card_a_id END as recommended_card_id,
strength,
counter_type,
confidence
FROM mtg_card_counters
WHERE card_a_id = :card_id OR card_b_id = :card_id
ORDER BY strength DESC, confidence DESC
LIMIT :limit
"""
elif recommendation_type == "evolution":
# Get evolutions of this card
query = """
SELECT evolved_card_id as recommended_card_id,
strength,
evolution_type,
confidence
FROM mtg_card_evolution
WHERE card_id = :card_id
ORDER BY strength DESC, confidence DESC
LIMIT :limit
"""
else:
raise HTTPException(
status_code=400, detail=f"Invalid recommendation type: {recommendation_type}"
)
params: Dict[str, Any] = {"card_id": card_id, "limit": limit}
result = await db.execute(text(query), params)
rows = result.fetchall()
recommendations: List[Dict[str, Any]] = []
for row in rows:
recommendations.append({
"recommended_card_id": row[0],
"strength": row[1],
"type": recommendation_type,
"subtype": row[2],
"confidence": row[3],
})
await cache_set(cache_key, recommendations, ttl=900)
return {
"cached": False,
"results": recommendations,
}
@interaction_router.get("/search/synergies")
async def search_synergies(
card_a_id: Optional[int] = Query(None, description="Card A ID"),
card_b_id: Optional[int] = Query(None, description="Card B ID"),
synergy_type: Optional[str] = Query(None, description="Filter by synergy type"),
min_strength: int = Query(1, ge=1, le=5, description="Minimum strength"),
min_confidence: float = Query(0.0, ge=0.0, le=1.0, description="Minimum confidence"),
limit: int = Query(100, ge=1, le=1000, description="Maximum results"),
offset: int = Query(0, ge=0, description="Number of results to skip"),
db: AsyncSession = Depends(mtg_get_db),
) -> Dict[str, Any]:
"""
Search synergies with multiple filters.
"""
cache_key = f"search_synergies:{card_a_id}:{card_b_id}:{synergy_type}:{min_strength}:{min_confidence}:{limit}:{offset}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
conditions: list[str] = []
params: Dict[str, Any] = {}
if card_a_id:
conditions.append("card_a_id = :card_a_id")
params["card_a_id"] = card_a_id
if card_b_id:
conditions.append("card_b_id = :card_b_id")
params["card_b_id"] = card_b_id
if synergy_type:
conditions.append("synergy_type = :synergy_type")
params["synergy_type"] = synergy_type
if min_strength:
conditions.append("strength >= :min_strength")
params["min_strength"] = min_strength
if min_confidence:
conditions.append("confidence >= :min_confidence")
params["min_confidence"] = min_confidence
where_clause = " AND ".join(conditions) if conditions else "TRUE"
query = f"""
SELECT id, card_a_id, card_b_id, synergy_type, strength, notes, confidence
FROM mtg_card_synergies
WHERE {where_clause}
ORDER BY strength DESC, confidence DESC
LIMIT :limit OFFSET :offset
"""
params["limit"] = limit
params["offset"] = offset
result = await db.execute(text(query), params)
rows = result.fetchall()
synergies: List[Dict[str, Any]] = []
for row in rows:
synergies.append({
"id": row[0],
"card_a_id": row[1],
"card_b_id": row[2],
"synergy_type": row[3],
"strength": row[4],
"notes": row[5],
"confidence": row[6],
})
count_query = f"""
SELECT COUNT(*)
FROM mtg_card_synergies
WHERE {where_clause}
"""
count_result = await db.execute(text(count_query), params)
total = count_result.scalar()
await cache_set(cache_key, {"synergies": synergies, "total": total}, ttl=600)
return {
"cached": False,
"results": synergies,
"pagination": {
"total": total,
"limit": limit,
"offset": offset,
},
}
@interaction_router.get("/search/counters")
async def search_counters(
card_a_id: Optional[int] = Query(None, description="Card A ID"),
card_b_id: Optional[int] = Query(None, description="Card B ID"),
counter_type: Optional[str] = Query(None, description="Filter by counter type"),
min_strength: int = Query(1, ge=1, le=5, description="Minimum strength"),
min_confidence: float = Query(0.0, ge=0.0, le=1.0, description="Minimum confidence"),
limit: int = Query(100, ge=1, le=1000, description="Maximum results"),
offset: int = Query(0, ge=0, description="Number of results to skip"),
db: AsyncSession = Depends(mtg_get_db),
) -> Dict[str, Any]:
"""
Search counters with multiple filters.
"""
cache_key = f"search_counters:{card_a_id}:{card_b_id}:{counter_type}:{min_strength}:{min_confidence}:{limit}:{offset}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
conditions: list[str] = []
params: Dict[str, Any] = {}
if card_a_id:
conditions.append("card_a_id = :card_a_id")
params["card_a_id"] = card_a_id
if card_b_id:
conditions.append("card_b_id = :card_b_id")
params["card_b_id"] = card_b_id
if counter_type:
conditions.append("counter_type = :counter_type")
params["counter_type"] = counter_type
if min_strength:
conditions.append("strength >= :min_strength")
params["min_strength"] = min_strength
if min_confidence:
conditions.append("confidence >= :min_confidence")
params["min_confidence"] = min_confidence
where_clause = " AND ".join(conditions) if conditions else "TRUE"
query = f"""
SELECT id, card_a_id, card_b_id, counter_type, strength, notes, confidence
FROM mtg_card_counters
WHERE {where_clause}
ORDER BY strength DESC, confidence DESC
LIMIT :limit OFFSET :offset
"""
params["limit"] = limit
params["offset"] = offset
result = await db.execute(text(query), params)
rows = result.fetchall()
counters: List[Dict[str, Any]] = []
for row in rows:
counters.append({
"id": row[0],
"card_a_id": row[1],
"card_b_id": row[2],
"counter_type": row[3],
"strength": row[4],
"notes": row[5],
"confidence": row[6],
})
count_query = f"""
SELECT COUNT(*)
FROM mtg_card_counters
WHERE {where_clause}
"""
count_result = await db.execute(text(count_query), params)
total = count_result.scalar()
await cache_set(cache_key, {"counters": counters, "total": total}, ttl=600)
return {
"cached": False,
"results": counters,
"pagination": {
"total": total,
"limit": limit,
"offset": offset,
},
}
@interaction_router.get("/search/evolutions")
async def search_evolutions(
card_id: Optional[int] = Query(None, description="Card ID"),
evolved_card_id: Optional[int] = Query(None, description="Evolved Card ID"),
evolution_type: Optional[str] = Query(None, description="Filter by evolution type"),
min_strength: int = Query(1, ge=1, le=5, description="Minimum strength"),
min_confidence: float = Query(0.0, ge=0.0, le=1.0, description="Minimum confidence"),
limit: int = Query(100, ge=1, le=1000, description="Maximum results"),
offset: int = Query(0, ge=0, description="Number of results to skip"),
db: AsyncSession = Depends(mtg_get_db),
) -> Dict[str, Any]:
"""
Search evolutions with multiple filters.
"""
cache_key = f"search_evolutions:{card_id}:{evolved_card_id}:{evolution_type}:{min_strength}:{min_confidence}:{limit}:{offset}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
conditions: list[str] = []
params: Dict[str, Any] = {}
if card_id:
conditions.append("card_id = :card_id")
params["card_id"] = card_id
if evolved_card_id:
conditions.append("evolved_card_id = :evolved_card_id")
params["evolved_card_id"] = evolved_card_id
if evolution_type:
conditions.append("evolution_type = :evolution_type")
params["evolution_type"] = evolution_type
if min_strength:
conditions.append("strength >= :min_strength")
params["min_strength"] = min_strength
if min_confidence:
conditions.append("confidence >= :min_confidence")
params["min_confidence"] = min_confidence
where_clause = " AND ".join(conditions) if conditions else "TRUE"
query = f"""
SELECT id, card_id, evolved_card_id, evolution_type, strength, notes, confidence
FROM mtg_card_evolution
WHERE {where_clause}
ORDER BY strength DESC, confidence DESC
LIMIT :limit OFFSET :offset
"""
params["limit"] = limit
params["offset"] = offset
result = await db.execute(text(query), params)
rows = result.fetchall()
evolutions: List[Dict[str, Any]] = []
for row in rows:
evolutions.append({
"id": row[0],
"card_id": row[1],
"evolved_card_id": row[2],
"evolution_type": row[3],
"strength": row[4],
"notes": row[5],
"confidence": row[6],
})
count_query = f"""
SELECT COUNT(*)
FROM mtg_card_evolution
WHERE {where_clause}
"""
count_result = await db.execute(text(count_query), params)
total = count_result.scalar()
await cache_set(cache_key, {"evolutions": evolutions, "total": total}, ttl=600)
return {
"cached": False,
"results": evolutions,
"pagination": {
"total": total,
"limit": limit,
"offset": offset,
},
}
@interaction_router.get("/stats/{card_id}")
async def get_card_interaction_stats(
card_id: int,
db: AsyncSession = Depends(mtg_get_db),
) -> Dict[str, Any]:
"""
Get aggregated interaction statistics for a card.
"""
cache_key = f"interaction_stats:{card_id}"
cached = await cache_get(cache_key)
if cached:
return {"cached": True, "results": cached}
query = """
SELECT
card_id,
total_synergies,
total_counters,
total_evolutions,
total_synergy_strength,
avg_synergy_strength
FROM mtg_card_interaction_stats
WHERE card_id = :card_id
"""
params: Dict[str, Any] = {"card_id": card_id}
result = await db.execute(text(query), params)
row = result.fetchone()
if not row:
return {
"cached": False,
"results": {
"card_id": card_id,
"total_synergies": 0,
"total_counters": 0,
"total_evolutions": 0,
"total_synergy_strength": 0,
"avg_synergy_strength": 0,
},
}
stats: Dict[str, Any] = {
"card_id": row[0],
"total_synergies": row[1],
"total_counters": row[2],
"total_evolutions": row[3],
"total_synergy_strength": row[4],
"avg_synergy_strength": row[5],
}
await cache_set(cache_key, stats, ttl=1800)
return {
"cached": False,
"results": stats,
}