Phase 7: End-to-End API Testing - Database setup, migrations, and application fixes
- Switched from psycopg2 to asyncpg for async SQLAlchemy support - Fixed router registration in main.py - removed duplicate prefixes - Added user_data export to routers/__init__.py - Refactored decks router to use DeckManager service layer - Integrated FuzzyCardMatcher into card_router search endpoints - Made WishlistCreate.card_id optional for proper schema validation - Set PostgreSQL password and configured scram-sha-256 auth - Updated alembic.ini to use local PostgreSQL instead of Docker hostname - Created generic_schemas.py for reusable schema patterns - Added test_routers.py and test_schema_validation.py test files - All 6 Alembic migrations applied successfully (37 tables created) - Application running on port 8000 with all services connected
This commit is contained in:
@@ -13,7 +13,9 @@ from app.core.database import mtg_get_db
|
||||
from app.core.redis_client import cache_get, cache_set
|
||||
from app.services.card_search_service import CardSearchService
|
||||
from app.services.deck_suggestion_service import DeckSuggestionService
|
||||
from app.services.fuzzy_card_matcher import FuzzyCardMatcher
|
||||
from app.models.user_deck import UserDeck
|
||||
from app.models.mtg_models import MtgCard
|
||||
from app.schemas.card_search_schemas import CardSearchResponse, CardResponse, SetResponse, CardTypeResponse
|
||||
|
||||
router = APIRouter(prefix="/api/cards", tags=["Card Search"])
|
||||
@@ -33,6 +35,7 @@ async def search_cards_endpoint(
|
||||
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}"
|
||||
|
||||
@@ -41,7 +44,10 @@ async def search_cards_endpoint(
|
||||
if cached:
|
||||
return {"cached": True, "results": cached}
|
||||
|
||||
# Search cards
|
||||
# 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,
|
||||
@@ -52,12 +58,125 @@ async def search_cards_endpoint(
|
||||
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.
|
||||
"""
|
||||
from sqlalchemy import select, or_
|
||||
|
||||
# Fetch candidate cards based on non-name filters
|
||||
conditions = []
|
||||
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 = {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 = []
|
||||
if matched_name and matched_name.lower() in card_lookup:
|
||||
card = card_lookup[matched_name.lower()]
|
||||
card_data = {
|
||||
"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,
|
||||
}
|
||||
|
||||
|
||||
@router.get("/{card_id}", response_model=CardResponse)
|
||||
async def get_card_endpoint(
|
||||
card_id: int,
|
||||
|
||||
Reference in New Issue
Block a user