675 lines
21 KiB
Python
675 lines
21 KiB
Python
"""
|
|
Card interaction router for MTG card interaction database.
|
|
|
|
Provides endpoints for searching card synergies, counters, evolutions,
|
|
and getting recommendations based on card interactions.
|
|
"""
|
|
from typing import Optional, List, Dict, Any
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import text
|
|
|
|
from app.core.database import mtg_get_db
|
|
from app.core.redis_client import cache_get, cache_set
|
|
|
|
router = APIRouter(prefix="/interactions", tags=["Card Interactions"])
|
|
|
|
|
|
@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),
|
|
):
|
|
"""
|
|
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 = ["card_a_id = :card_id OR card_b_id = :card_id"]
|
|
params = {"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 = []
|
|
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)}")
|
|
|
|
|
|
@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),
|
|
):
|
|
"""
|
|
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 = ["card_a_id = :card_id OR card_b_id = :card_id"]
|
|
params = {"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 = []
|
|
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,
|
|
}
|
|
}
|
|
|
|
|
|
@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),
|
|
):
|
|
"""
|
|
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 = ["card_id = :card_id"]
|
|
params = {"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 = []
|
|
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,
|
|
}
|
|
}
|
|
|
|
|
|
@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),
|
|
):
|
|
"""
|
|
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 = {"card_id": card_id, "limit": limit}
|
|
|
|
result = await db.execute(text(query), params)
|
|
rows = result.fetchall()
|
|
|
|
recommendations = []
|
|
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,
|
|
}
|
|
|
|
|
|
@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),
|
|
):
|
|
"""
|
|
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 = []
|
|
params = {}
|
|
|
|
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 = []
|
|
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,
|
|
}
|
|
}
|
|
|
|
|
|
@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),
|
|
):
|
|
"""
|
|
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 = []
|
|
params = {}
|
|
|
|
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 = []
|
|
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,
|
|
}
|
|
}
|
|
|
|
|
|
@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),
|
|
):
|
|
"""
|
|
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 = []
|
|
params = {}
|
|
|
|
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 = []
|
|
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,
|
|
}
|
|
}
|
|
|
|
|
|
@router.get("/stats/{card_id}")
|
|
async def get_card_interaction_stats(
|
|
card_id: int,
|
|
db: AsyncSession = Depends(mtg_get_db),
|
|
):
|
|
"""
|
|
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 = {"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 = {
|
|
"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,
|
|
}
|