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,246 @@
|
||||
"""Deck manager service."""
|
||||
from typing import List, Dict, Any, Optional
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, update, delete
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from app.models.user_deck import UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard
|
||||
from app.models.models import MtgonlineCard
|
||||
|
||||
|
||||
class DeckManager:
|
||||
"""Deck manager service."""
|
||||
|
||||
@staticmethod
|
||||
async def create_deck(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
name: str,
|
||||
folder_id: Optional[int] = None,
|
||||
format: str = "standard",
|
||||
notes: Optional[str] = None,
|
||||
is_precedent: bool = False,
|
||||
precedent_name: Optional[str] = None,
|
||||
) -> UserDeck:
|
||||
"""Create a new deck."""
|
||||
deck = UserDeck(
|
||||
user_id=user_id,
|
||||
name=name,
|
||||
folder_id=folder_id,
|
||||
format=format,
|
||||
notes=notes,
|
||||
is_precedent=is_precedent,
|
||||
precedent_name=precedent_name,
|
||||
)
|
||||
db.add(deck)
|
||||
await db.flush()
|
||||
return deck
|
||||
|
||||
@staticmethod
|
||||
async def get_deck(db: AsyncSession, deck_id: int, user_id: int) -> Optional[UserDeck]:
|
||||
"""Get a deck by ID."""
|
||||
stmt = select(UserDeck).where(UserDeck.id == deck_id, UserDeck.user_id == user_id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def list_decks(
|
||||
db: AsyncSession,
|
||||
user_id: int,
|
||||
status_filter: Optional[str] = None,
|
||||
folder_id: Optional[int] = None,
|
||||
is_precedent: Optional[bool] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 50,
|
||||
) -> List[UserDeck]:
|
||||
"""List user's decks with filtering."""
|
||||
conditions = [UserDeck.user_id == user_id]
|
||||
if status_filter:
|
||||
conditions.append(UserDeck.status == status_filter)
|
||||
if folder_id:
|
||||
conditions.append(UserDeck.folder_id == folder_id)
|
||||
if is_precedent is not None:
|
||||
conditions.append(UserDeck.is_precedent == is_precedent)
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
stmt = select(UserDeck).where(*conditions).order_by(UserDeck.updated_at.desc()).offset(offset).limit(page_size)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def update_deck(
|
||||
db: AsyncSession,
|
||||
deck_id: int,
|
||||
user_id: int,
|
||||
name: Optional[str] = None,
|
||||
folder_id: Optional[int] = None,
|
||||
format: Optional[str] = None,
|
||||
notes: Optional[str] = None,
|
||||
) -> Optional[UserDeck]:
|
||||
"""Update a deck."""
|
||||
deck = await DeckManager.get_deck(db, deck_id, user_id)
|
||||
if not deck:
|
||||
return None
|
||||
|
||||
if deck.status == "FINAL":
|
||||
raise ValueError("Cannot modify a finalized deck")
|
||||
|
||||
if name:
|
||||
deck.name = name
|
||||
if folder_id is not None:
|
||||
deck.folder_id = folder_id
|
||||
if format:
|
||||
deck.format = format
|
||||
if notes is not None:
|
||||
deck.notes = notes
|
||||
|
||||
await db.flush()
|
||||
return deck
|
||||
|
||||
@staticmethod
|
||||
async def delete_deck(db: AsyncSession, deck_id: int, user_id: int) -> bool:
|
||||
"""Delete a deck."""
|
||||
deck = await DeckManager.get_deck(db, deck_id, user_id)
|
||||
if not deck:
|
||||
return False
|
||||
|
||||
await db.execute(delete(UserDeck).where(UserDeck.id == deck_id))
|
||||
await db.flush()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def finalize_deck(db: AsyncSession, deck_id: int, user_id: int) -> Optional[UserDeck]:
|
||||
"""Transition a deck from DRAFT to FINAL status."""
|
||||
deck = await DeckManager.get_deck(db, deck_id, user_id)
|
||||
if not deck:
|
||||
return None
|
||||
|
||||
if deck.status == "FINAL":
|
||||
raise ValueError("Deck is already finalized")
|
||||
|
||||
# Check deck has cards
|
||||
card_count_stmt = select(func.count()).select_from(UserDeckCard).where(UserDeckCard.deck_id == deck_id)
|
||||
card_count_result = await db.execute(card_count_stmt)
|
||||
card_count = card_count_result.scalar() or 0
|
||||
if card_count == 0:
|
||||
raise ValueError("Cannot finalize an empty deck")
|
||||
|
||||
deck.status = "FINAL"
|
||||
await db.flush()
|
||||
return deck
|
||||
|
||||
@staticmethod
|
||||
async def add_card_to_deck(
|
||||
db: AsyncSession,
|
||||
deck_id: int,
|
||||
card_id: int,
|
||||
quantity: int = 1,
|
||||
zone: str = "main",
|
||||
position: Optional[int] = None,
|
||||
) -> UserDeckCard:
|
||||
"""Add a card to a deck."""
|
||||
deck_card = UserDeckCard(
|
||||
deck_id=deck_id,
|
||||
card_id=card_id,
|
||||
quantity=quantity,
|
||||
zone=zone,
|
||||
position=position,
|
||||
)
|
||||
db.add(deck_card)
|
||||
await db.flush()
|
||||
return deck_card
|
||||
|
||||
@staticmethod
|
||||
async def get_deck_cards(db: AsyncSession, deck_id: int, zone: Optional[str] = None) -> List[UserDeckCard]:
|
||||
"""Get cards in a deck."""
|
||||
conditions = [UserDeckCard.deck_id == deck_id]
|
||||
if zone:
|
||||
conditions.append(UserDeckCard.zone == zone)
|
||||
|
||||
stmt = select(UserDeckCard).where(*conditions).order_by(UserDeckCard.id)
|
||||
result = await db.execute(stmt)
|
||||
return result.scalars().all()
|
||||
|
||||
@staticmethod
|
||||
async def update_deck_card(
|
||||
db: AsyncSession,
|
||||
deck_card_id: int,
|
||||
quantity: Optional[int] = None,
|
||||
zone: Optional[str] = None,
|
||||
position: Optional[int] = None,
|
||||
) -> Optional[UserDeckCard]:
|
||||
"""Update a card in a deck."""
|
||||
stmt = select(UserDeckCard).where(UserDeckCard.id == deck_card_id)
|
||||
result = await db.execute(stmt)
|
||||
deck_card = result.scalar_one_or_none()
|
||||
|
||||
if not deck_card:
|
||||
return None
|
||||
|
||||
if quantity is not None:
|
||||
deck_card.quantity = quantity
|
||||
if zone:
|
||||
deck_card.zone = zone
|
||||
if position is not None:
|
||||
deck_card.position = position
|
||||
|
||||
await db.flush()
|
||||
return deck_card
|
||||
|
||||
@staticmethod
|
||||
async def remove_card_from_deck(db: AsyncSession, deck_card_id: int) -> bool:
|
||||
"""Remove a card from a deck."""
|
||||
stmt = select(UserDeckCard).where(UserDeckCard.id == deck_card_id)
|
||||
result = await db.execute(stmt)
|
||||
deck_card = result.scalar_one_or_none()
|
||||
|
||||
if not deck_card:
|
||||
return False
|
||||
|
||||
await db.execute(delete(UserDeckCard).where(UserDeckCard.id == deck_card_id))
|
||||
await db.flush()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def clone_precedent(
|
||||
db: AsyncSession,
|
||||
precedent_id: int,
|
||||
user_id: int,
|
||||
name: Optional[str] = None,
|
||||
) -> UserDeck:
|
||||
"""Clone a precedent into a new deck."""
|
||||
# Get precedent
|
||||
stmt = select(DeckPrecedent).where(DeckPrecedent.id == precedent_id)
|
||||
result = await db.execute(stmt)
|
||||
precedent = result.scalar_one_or_none()
|
||||
|
||||
if not precedent:
|
||||
raise ValueError(f"Precedent {precedent_id} not found")
|
||||
|
||||
# Create new deck
|
||||
new_name = name or f"Copy of {precedent.name}"
|
||||
new_deck = UserDeck(
|
||||
user_id=user_id,
|
||||
name=new_name,
|
||||
format=precedent.format,
|
||||
is_precedent=False,
|
||||
)
|
||||
db.add(new_deck)
|
||||
await db.flush()
|
||||
|
||||
# Copy cards from precedent
|
||||
card_stmt = select(DeckPrecedentCard).where(DeckPrecedentCard.precedent_id == precedent_id)
|
||||
card_result = await db.execute(card_stmt)
|
||||
precedent_cards = card_result.scalars().all()
|
||||
|
||||
for pc in precedent_cards:
|
||||
new_dc = UserDeckCard(
|
||||
deck_id=new_deck.id,
|
||||
card_id=pc.card_id,
|
||||
quantity=pc.quantity,
|
||||
zone=pc.zone,
|
||||
)
|
||||
db.add(new_dc)
|
||||
|
||||
await db.flush()
|
||||
return new_deck
|
||||
Reference in New Issue
Block a user