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
+309 -229
View File
@@ -1,21 +1,27 @@
"""
Card import router endpoints.
Provides endpoints for importing card collections, viewing import status,
and using imported cards for deckbuilding. This feature allows users to
upload their owned cards as a list, which then informs deckbuilding.
Provides endpoints for importing card collections from files,
viewing import status, and confirming imports.
"""
import json
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List, Optional, Dict, Any
from fastapi import APIRouter, Depends, HTTPException, status, UploadFile, File
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, func, update
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from typing import Optional, List
from datetime import datetime
from app.core.database import get_db
from app.core.security import get_current_user
from app.models.models import User, MtgonlineCard
from app.models.user_card_import import UserCardImport
from app.models.card_import_batch import CardImportBatch
from app.models.user_card_import_record import UserCardImportRecord
from app.models.user_card_collection import UserCardCollection
from app.models.models import MtgonlineCard
from app.models.user_deck import UserDeck, UserDeckCard
from app.services.file_parser import FileParser
from app.services.import_batch_processor import ImportBatchProcessor
from app.services.fuzzy_card_matcher import FuzzyCardMatcher
from app.schemas.card_import_schemas import (
CardImportRequest,
CardImportResponse,
@@ -24,265 +30,339 @@ from app.schemas.card_import_schemas import (
CardImportSummary,
MessageResponse,
)
from app.schemas.user_deck_schemas import DeckCardResponse, DeckCardListResponse
router = APIRouter()
@router.get("/status", response_model=CardImportStatusResponse)
async def get_card_import_status(
@router.post("/import", response_model=CardImportResponse, status_code=status.HTTP_201_CREATED)
async def upload_card_import(
file: UploadFile = File(...),
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get current card import status for the current user.
Upload a card import file (XLSX, CSV, JSON, ODS).
Returns whether a card import exists, the card count,
and the names of all imported cards.
Parses the file, performs fuzzy matching, and creates an import batch.
"""
user_id = int(current_user["user_id"])
stmt = select(UserCardImport).where(UserCardImport.user_id == user_id)
result = await db.execute(stmt)
card_import = result.scalar_one_or_none()
if not card_import:
return CardImportStatusResponse(has_import=False)
# Parse card names from JSON
try:
card_names = json.loads(card_import.card_names_json)
except (json.JSONDecodeError, TypeError):
# Validate file type
if not file.filename:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Invalid card import data format"
status_code=status.HTTP_400_BAD_REQUEST,
detail="File must have a filename"
)
file_type = file.filename.split(".")[-1].lower()
supported_types = ["xlsx", "csv", "json", "ods"]
if file_type not in supported_types:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Unsupported file type: {file_type}. Supported types: {supported_types}"
)
# Read file content
content = await file.read()
file_size = len(content)
# Parse file
from pathlib import Path
import tempfile
with tempfile.NamedTemporaryFile(suffix=f".{file_type}", delete=False) as tmp_file:
tmp_file.write(content)
tmp_file_path = Path(tmp_file.name)
try:
card_names = await FileParser.parse_file(tmp_file_path)
except Exception as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Failed to parse file: {str(e)}"
)
if not card_names:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="File contains no card names"
)
# Create import batch
batch = await ImportBatchProcessor.create_batch(
db=db,
user_id=user_id,
filename=file.filename,
file_type=file_type,
file_size=file_size,
card_names=card_names,
)
# Process batch
result = await ImportBatchProcessor.process_batch(
db=db,
batch=batch,
card_names=card_names,
)
return CardImportResponse(
message=f"Import batch created successfully",
batch_id=batch.id,
card_count=len(card_names),
status=result["status"],
imported_at=datetime.utcnow(),
)
@router.get("/import/{import_id}/status", response_model=CardImportStatusResponse)
async def get_import_status(
import_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""Get the status of an import batch."""
user_id = int(current_user["user_id"])
batch = await ImportBatchProcessor.get_batch_status(db, import_id)
if not batch:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Import batch {import_id} not found"
)
if batch.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
return CardImportStatusResponse(
has_import=True,
card_count=len(card_names),
card_names=card_names,
last_imported=card_import.updated_at,
batch_id=batch.id,
status=batch.status,
card_count=batch.total_cards,
matched_count=batch.matched_cards,
unmatched_count=batch.unmatched_cards,
last_imported=batch.updated_at,
error_message=batch.error_message,
)
@router.post("/", response_model=CardImportResponse)
async def import_cards(
request: CardImportRequest,
@router.get("/import/{import_id}/results", response_model=CardImportSummary)
async def get_import_results(
import_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Import a card collection for the current user.
Accepts a list of card names and stores them as the user's
owned card collection. This replaces any existing import.
Uses fuzzy matching to find card IDs in the mtgonline_cards table.
"""
"""Get the match results for an import batch."""
user_id = int(current_user["user_id"])
# Normalize card names: strip whitespace, title case
normalized_names = [name.strip().title() for name in request.card_names]
# Fetch all cards from the mtgonline_cards mirror
stmt = select(MtgonlineCard).options(selectinload(MtgonlineCard.deck_cards))
result = await db.execute(stmt)
all_cards = result.scalars().all()
# Build lookup dictionaries
card_by_name = {} # exact title case name -> MtgonlineCard
card_by_lower = {} # lowercase name -> MtgonlineCard (for fuzzy matching)
for card in all_cards:
if card.name:
# Exact match (title case)
card_by_name[card.name] = card
# Lowercase for case-insensitive matching
card_by_lower[card.name.lower()] = card
# Match each imported card name to a database card
matched_cards = []
unmatched_cards = []
matched_card_names = []
for card_name in normalized_names:
# Try exact match first
if card_name in card_by_name:
matched_cards.append(CardMatchResult(
card_id=card_by_name[card_name].id,
card_name=card_name,
matched_name=card_by_name[card_name].name,
match_type="exact",
confidence=1.0,
))
matched_card_names.append(card_by_name[card_name].name)
continue
# Try case-insensitive match
if card_name.lower() in card_by_lower:
matched_cards.append(CardMatchResult(
card_id=card_by_lower[card_name.lower()].id,
card_name=card_name,
matched_name=card_by_lower[card_name.lower()].name,
match_type="exact",
confidence=0.95,
))
matched_card_names.append(card_by_lower[card_name.lower()].name)
continue
# Try partial match (fuzzy)
best_match = None
best_confidence = 0.0
for db_name, db_card in card_by_lower.items():
# Simple partial match: check if one contains the other
if card_name.lower() in db_name or db_name.lower() in card_name.lower():
# Calculate confidence based on length similarity
min_len = min(len(card_name), len(db_name))
max_len = max(len(card_name), len(db_name))
if max_len > 0:
confidence = min_len / max_len
if confidence > best_confidence:
best_confidence = confidence
best_match = db_card
if best_match and best_confidence >= 0.6: # 60% similarity threshold
matched_cards.append(CardMatchResult(
card_id=best_match.id,
card_name=card_name,
matched_name=best_match.name,
match_type="partial",
confidence=best_confidence,
))
matched_card_names.append(best_match.name)
else:
unmatched_cards.append(card_name)
# Create or update the card import
card_names_json = json.dumps(matched_card_names)
existing = select(UserCardImport).where(UserCardImport.user_id == user_id)
existing_result = await db.execute(existing)
existing_import = existing_result.scalar_one_or_none()
if existing_import:
# Update existing import
stmt = (
update(UserCardImport)
.where(UserCardImport.id == existing_import.id)
.values(
card_names_json=card_names_json,
updated_at=func.now(),
)
)
await db.execute(stmt)
await db.flush()
# Fetch updated import
stmt = select(UserCardImport).where(UserCardImport.id == existing_import.id)
result = await db.execute(stmt)
updated_import = result.scalar_one_or_none()
else:
# Create new import
new_import = UserCardImport(
user_id=user_id,
card_names_json=card_names_json,
)
db.add(new_import)
await db.flush()
updated_import = new_import
return CardImportResponse(
message=f"Imported {len(matched_card_names)} cards successfully",
card_count=len(matched_card_names),
card_names=matched_card_names,
imported_at=updated_import.updated_at,
)
@router.delete("/", response_model=MessageResponse)
async def delete_card_import(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""Delete the current user's card import."""
user_id = int(current_user["user_id"])
stmt = select(UserCardImport).where(UserCardImport.user_id == user_id)
result = await db.execute(stmt)
card_import = result.scalar_one_or_none()
if not card_import:
return MessageResponse(message="No card import found to delete")
# Delete the import (cascade will handle related data if any)
await db.delete(card_import)
await db.flush()
return MessageResponse(message="Card import deleted successfully")
@router.get("/summary", response_model=CardImportSummary)
async def get_card_import_summary(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""
Get a summary of the card import including match results.
Returns the full match results with confidence scores and
lists of unmatched cards for review.
"""
user_id = int(current_user["user_id"])
stmt = select(UserCardImport).where(UserCardImport.user_id == user_id)
result = await db.execute(stmt)
card_import = result.scalar_one_or_none()
if not card_import:
return CardImportSummary(
total_cards=0,
matched_cards=[],
unmatched_cards=[],
)
try:
card_names = json.loads(card_import.card_names_json)
except (json.JSONDecodeError, TypeError):
batch = await ImportBatchProcessor.get_batch_status(db, import_id)
if not batch:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Invalid card import data format"
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Import batch {import_id} not found"
)
# Fetch all cards for matching
stmt = select(MtgonlineCard)
result = await db.execute(stmt)
all_cards = result.scalars().all()
if batch.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
# Build lookup
card_by_lower = {card.name.lower(): card for card in all_cards if card.name}
if batch.status != "completed":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Import batch {import_id} has not completed yet (status: {batch.status})"
)
match_results = batch.match_results or []
# Match cards
matched_cards = []
unmatched_cards = []
for card_name in card_names:
normalized = card_name.strip().title()
if normalized.lower() in card_by_lower:
db_card = card_by_lower[normalized.lower()]
for original_name, card_id, matched_name, confidence, match_type in match_results:
if matched_name:
matched_cards.append(CardMatchResult(
card_id=db_card.id,
card_name=normalized,
matched_name=db_card.name,
match_type="exact",
confidence=1.0,
card_id=card_id,
card_name=original_name,
matched_name=matched_name,
match_type=match_type,
confidence=confidence,
))
else:
unmatched_cards.append(card_name)
unmatched_cards.append(original_name)
return CardImportSummary(
total_cards=len(card_names),
total_cards=len(match_results),
matched_cards=matched_cards,
unmatched_cards=unmatched_cards,
import_id=card_import.id,
import_id=batch.id,
)
@router.post("/import/{import_id}/confirm", response_model=MessageResponse)
async def confirm_import(
import_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""Confirm an import batch and save to user's card collection."""
user_id = int(current_user["user_id"])
batch = await ImportBatchProcessor.get_batch_status(db, import_id)
if not batch:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Import batch {import_id} not found"
)
if batch.user_id != user_id:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied"
)
if batch.status != "completed":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"Import batch {import_id} has not completed yet (status: {batch.status})"
)
# Confirm batch
try:
await ImportBatchProcessor.confirm_batch(db, import_id, user_id)
except ValueError as e:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(e)
)
# Save matched cards to user's collection
match_results = batch.match_results or []
saved_count = 0
for original_name, card_id, matched_name, confidence, match_type in match_results:
if card_id and match_type in ["exact", "high_confidence"]:
# Check if already in collection
stmt = select(UserCardCollection).where(
UserCardCollection.user_id == user_id,
UserCardCollection.card_id == card_id,
)
result = await db.execute(stmt)
existing = result.scalar_one_or_none()
if existing:
existing.quantity += 1
else:
new_collection = UserCardCollection(
user_id=user_id,
card_id=card_id,
quantity=1,
)
db.add(new_collection)
saved_count += 1
await db.flush()
return MessageResponse(
message=f"Import confirmed. {saved_count} cards added to your collection."
)
@router.get("/user/cards", response_model=List[Dict[str, Any]])
async def get_user_cards(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""List user's imported cards."""
user_id = int(current_user["user_id"])
stmt = select(UserCardCollection).where(UserCardCollection.user_id == user_id).order_by(UserCardCollection.created_at.desc())
result = await db.execute(stmt)
collections = result.scalars().all()
# Fetch card details
card_ids = [c.card_id for c in collections]
card_details = {}
if card_ids:
card_stmt = select(MtgonlineCard).where(MtgonlineCard.id.in_(card_ids))
card_result = await db.execute(card_stmt)
for card in card_result.scalars().all():
card_details[card.id] = card
result_list = []
for collection in collections:
card = card_details.get(collection.card_id)
result_list.append({
"collection_id": collection.id,
"card_id": collection.card_id,
"card_name": card.name if card else f"Card#{collection.card_id}",
"card_type_line": card.type_line if card else "",
"quantity": collection.quantity,
"condition": collection.condition,
"language": collection.language,
"is_foil": collection.is_foil,
"is_alt_art": collection.is_alt_art,
"acquired_date": collection.acquired_date,
})
return result_list
@router.delete("/user/cards/{card_import_id}", response_model=MessageResponse)
async def delete_user_card(
card_import_id: int,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""Remove a card from user's collection."""
user_id = int(current_user["user_id"])
stmt = select(UserCardCollection).where(
UserCardCollection.id == card_import_id,
UserCardCollection.user_id == user_id,
)
result = await db.execute(stmt)
collection = result.scalar_one_or_none()
if not collection:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Card not found in your collection"
)
await db.delete(collection)
await db.flush()
return MessageResponse(message="Card removed from your collection")
@router.get("/user/decks", response_model=List[Dict[str, Any]])
async def get_user_decks(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""List user's decks."""
user_id = int(current_user["user_id"])
stmt = select(UserDeck).where(UserDeck.user_id == user_id).order_by(UserDeck.updated_at.desc())
result = await db.execute(stmt)
decks = result.scalars().all()
result_list = []
for deck in decks:
result_list.append({
"deck_id": deck.id,
"name": deck.name,
"status": deck.status,
"format": deck.format,
"folder_id": deck.folder_id,
"notes": deck.notes,
"is_precedent": deck.is_precedent,
"created_at": deck.created_at,
"updated_at": deck.updated_at,
})
return result_list
+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