Files
mtgonline/backend/app/services/import_batch_processor.py
T
akadmin 1e7c762452 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
2026-07-25 02:56:29 +00:00

205 lines
6.7 KiB
Python

"""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