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:
@@ -0,0 +1,204 @@
|
||||
"""Import batch processor service."""
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, insert, delete
|
||||
from sqlalchemy.sql import func
|
||||
|
||||
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.services.fuzzy_card_matcher import FuzzyCardMatcher
|
||||
|
||||
|
||||
class ImportBatchProcessor:
|
||||
"""Process card import batches."""
|
||||
|
||||
@staticmethod
|
||||
async def create_batch(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
filename: str,
|
||||
file_type: str,
|
||||
file_size: int,
|
||||
card_names: List[str]
|
||||
) -> CardImportBatch:
|
||||
"""Create a new import batch."""
|
||||
batch = CardImportBatch(
|
||||
user_id=user_id,
|
||||
filename=filename,
|
||||
file_type=file_type,
|
||||
file_size=file_size,
|
||||
status="pending",
|
||||
total_cards=len(card_names),
|
||||
)
|
||||
db.add(batch)
|
||||
await db.flush()
|
||||
return batch
|
||||
|
||||
@staticmethod
|
||||
async def process_batch(
|
||||
db: AsyncSession,
|
||||
batch: CardImportBatch,
|
||||
card_names: List[str],
|
||||
threshold: float = FuzzyCardMatcher.MANUAL_REVIEW_THRESHOLD
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
Process an import batch.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
batch: Import batch to process
|
||||
card_names: List of card names from the file
|
||||
threshold: Minimum similarity threshold for matching
|
||||
|
||||
Returns:
|
||||
Dictionary with processing results
|
||||
"""
|
||||
# Update status to processing
|
||||
batch.status = "processing"
|
||||
await db.flush()
|
||||
|
||||
try:
|
||||
# Fetch all cards from database
|
||||
stmt = select(MtgonlineCard)
|
||||
result = await db.execute(stmt)
|
||||
db_cards = result.scalars().all()
|
||||
|
||||
# Build candidate list
|
||||
candidate_names = [card.name for card in db_cards if card.name]
|
||||
|
||||
# Perform batch matching
|
||||
match_results = FuzzyCardMatcher.batch_match_with_database(
|
||||
card_names=card_names,
|
||||
db_session=db,
|
||||
mtgonline_card_model=MtgonlineCard,
|
||||
threshold=threshold
|
||||
)
|
||||
|
||||
# Count matches
|
||||
matched_count = sum(1 for _, _, matched_name, _, _ in match_results if matched_name)
|
||||
unmatched_count = sum(1 for _, _, matched_name, _, _ in match_results if not matched_name)
|
||||
|
||||
# Update batch
|
||||
batch.matched_cards = matched_count
|
||||
batch.unmatched_cards = unmatched_count
|
||||
batch.match_results = match_results
|
||||
batch.status = "completed"
|
||||
batch.updated_at = func.now()
|
||||
await db.flush()
|
||||
|
||||
return {
|
||||
"batch_id": batch.id,
|
||||
"status": "completed",
|
||||
"total_cards": len(card_names),
|
||||
"matched_cards": matched_count,
|
||||
"unmatched_cards": unmatched_count,
|
||||
"match_results": match_results,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
batch.status = "failed"
|
||||
batch.error_message = str(e)
|
||||
batch.updated_at = func.now()
|
||||
await db.flush()
|
||||
|
||||
return {
|
||||
"batch_id": batch.id,
|
||||
"status": "failed",
|
||||
"error": str(e),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_batch_status(db: AsyncSession, batch_id: int) -> Optional[CardImportBatch]:
|
||||
"""Get the status of an import batch."""
|
||||
stmt = select(CardImportBatch).where(CardImportBatch.id == batch_id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_batch_results(db: AsyncSession, batch_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""Get the match results for an import batch."""
|
||||
batch = await ImportBatchProcessor.get_batch_status(db, batch_id)
|
||||
if not batch:
|
||||
return None
|
||||
return batch.match_results
|
||||
|
||||
@staticmethod
|
||||
async def confirm_batch(db: AsyncSession, batch_id: int, user_id: int) -> UserCardImportRecord:
|
||||
"""
|
||||
Confirm an import batch.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
batch_id: ID of the batch to confirm
|
||||
user_id: ID of the user confirming
|
||||
|
||||
Returns:
|
||||
UserCardImportRecord for the confirmed import
|
||||
"""
|
||||
batch = await ImportBatchProcessor.get_batch_status(db, batch_id)
|
||||
if not batch:
|
||||
raise ValueError(f"Import batch {batch_id} not found")
|
||||
|
||||
if batch.status != "completed":
|
||||
raise ValueError(f"Import batch {batch_id} is not completed (status: {batch.status})")
|
||||
|
||||
# Check if already confirmed
|
||||
stmt = select(UserCardImportRecord).where(
|
||||
UserCardImportRecord.user_id == user_id,
|
||||
UserCardImportRecord.batch_id == batch_id,
|
||||
)
|
||||
result = await db.execute(stmt)
|
||||
existing = result.scalar_one_or_none()
|
||||
|
||||
if existing:
|
||||
return existing
|
||||
|
||||
# Create confirmation record
|
||||
record = UserCardImportRecord(
|
||||
user_id=user_id,
|
||||
batch_id=batch_id,
|
||||
is_confirmed=True,
|
||||
)
|
||||
db.add(record)
|
||||
await db.flush()
|
||||
|
||||
return record
|
||||
|
||||
@staticmethod
|
||||
async def get_user_imports(db: AsyncSession, user_id: int) -> List[CardImportBatch]:
|
||||
"""Get all import batches for a user."""
|
||||
stmt = select(CardImportBatch).where(CardImportBatch.user_id == user_id).order_by(CardImportBatch.created_at.desc())
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def delete_batch(db: AsyncSession, batch_id: int, user_id: int) -> bool:
|
||||
"""
|
||||
Delete an import batch.
|
||||
|
||||
Args:
|
||||
db: Database session
|
||||
batch_id: ID of the batch to delete
|
||||
user_id: ID of the user deleting
|
||||
|
||||
Returns:
|
||||
True if deleted successfully, False if not found
|
||||
"""
|
||||
batch = await ImportBatchProcessor.get_batch_status(db, batch_id)
|
||||
if not batch or batch.user_id != user_id:
|
||||
return False
|
||||
|
||||
# Delete confirmation records
|
||||
stmt = delete(UserCardImportRecord).where(UserCardImportRecord.batch_id == batch_id)
|
||||
await db.execute(stmt)
|
||||
|
||||
# Delete batch
|
||||
stmt = delete(CardImportBatch).where(CardImportBatch.id == batch_id)
|
||||
await db.execute(stmt)
|
||||
await db.flush()
|
||||
|
||||
return True
|
||||
Reference in New Issue
Block a user