feat: complete deckbuilding feature with local card mirror

- Add MtgonlineCard model (local card data mirror in mtgonline DB)
- Create user_deck.py models (UserDeck, UserDeckCard, DeckPrecedent, CardSuggestion)
- Create user_deck_schemas.py with Pydantic schemas
- Update decks.py router to use local MtgonlineCard instead of cross-DB MtgCard
- Add migration 002 (user deck building tables)
- Add migration 003 (mtgonline_cards table)
- All card lookups now use local mirror for fast queries
- No cross-DB joins in deckbuilding endpoints
This commit is contained in:
2026-07-24 04:42:22 +00:00
parent 356c2121b2
commit c23f88cd41
10 changed files with 1348 additions and 226 deletions
+242
View File
@@ -0,0 +1,242 @@
"""Pydantic schemas for user deck building features."""
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from datetime import datetime
from enum import Enum
# ===== Enum Types =====
class DeckStatus(str, Enum):
DRAFT = "DRAFT"
FINAL = "FINAL"
class DeckZone(str, Enum):
MAIN = "main"
SIDEBOARD = "sideboard"
class SuggestionType(str, Enum):
SIMILAR = "SIMILAR"
PAIRING = "PAIRING"
ALTERNATIVE = "ALTERNATIVE"
# ===== Deck Schemas =====
class UserDeckCreate(BaseModel):
"""Deck creation request."""
name: str = Field(..., min_length=1, max_length=255)
folder_id: Optional[int] = None
format: Optional[str] = Field("standard", max_length=50)
notes: Optional[str] = None
is_precedent: bool = False
precedent_name: Optional[str] = None
class UserDeckUpdate(BaseModel):
"""Deck update request."""
name: Optional[str] = None
folder_id: Optional[int] = None
format: Optional[str] = None
notes: Optional[str] = None
status: Optional[DeckStatus] = None
is_precedent: Optional[bool] = None
precedent_name: Optional[str] = None
class UserDeckResponse(BaseModel):
"""Deck response with card count."""
id: int
user_id: int
name: str
status: str
folder_id: Optional[int]
format: Optional[str]
notes: Optional[str]
is_precedent: bool
precedent_name: Optional[str]
created_at: datetime
updated_at: datetime
card_count: int = 0
is_owner: bool = False
class Config:
from_attributes = True
class UserDeckListResponse(BaseModel):
"""List of user decks."""
decks: List[UserDeckResponse]
total: int
page: int
page_size: int
total_pages: int
# ===== Deck Card Schemas =====
class DeckCardCreate(BaseModel):
"""Add card to deck."""
card_id: int
quantity: int = Field(1, ge=1)
zone: DeckZone = DeckZone.MAIN
position: Optional[int] = None
class DeckCardUpdate(BaseModel):
"""Update card in deck."""
quantity: Optional[int] = None
zone: Optional[DeckZone] = None
position: Optional[int] = None
class DeckCardResponse(BaseModel):
"""Deck card response."""
id: int
deck_id: int
card_id: int
quantity: int
zone: str
position: Optional[int]
created_at: datetime
class Config:
from_attributes = True
class DeckCardWithDetailsResponse(DeckCardResponse):
"""Deck card with card details."""
card_name: str = ""
card_type_line: str = ""
card_image: Optional[str] = None
class DeckCardListResponse(BaseModel):
"""List of deck cards."""
cards: List[DeckCardWithDetailsResponse]
total: int
# ===== Deck Precedent Schemas =====
class PrecedentCreate(BaseModel):
"""Create deck precedent."""
name: str = Field(..., min_length=1, max_length=255)
description: Optional[str] = None
format: Optional[str] = Field("standard", max_length=50)
is_public: bool = True
class PrecedentUpdate(BaseModel):
"""Update deck precedent."""
name: Optional[str] = None
description: Optional[str] = None
format: Optional[str] = None
is_public: Optional[bool] = None
class PrecedentResponse(BaseModel):
"""Deck precedent response."""
id: int
name: str
description: Optional[str]
format: Optional[str]
is_public: bool
created_by: Optional[int]
created_at: datetime
updated_at: datetime
card_count: int = 0
class Config:
from_attributes = True
class PrecedentListResponse(BaseModel):
"""List of deck precedents."""
precedents: List[PrecedentResponse]
total: int
# ===== Card Suggestion Schemas =====
class SuggestionCreate(BaseModel):
"""Create card suggestion."""
card_id: int
source_card_id: Optional[int] = None
suggestion_type: SuggestionType = SuggestionType.SIMILAR
confidence: Optional[float] = Field(None, ge=0.0, le=1.0)
notes: Optional[str] = None
class SuggestionResponse(BaseModel):
"""Card suggestion response."""
id: int
deck_id: int
card_id: int
source_card_id: Optional[int]
suggestion_type: str
confidence: Optional[float]
notes: Optional[str]
created_at: datetime
card_name: str = ""
class Config:
from_attributes = True
class SuggestionListResponse(BaseModel):
"""List of card suggestions."""
suggestions: List[SuggestionResponse]
total: int
# ===== Deck Action Schemas =====
class DeckFinalizeRequest(BaseModel):
"""Request to finalize a deck."""
status: DeckStatus = DeckStatus.FINAL
class DeckFinalizeResponse(BaseModel):
"""Response after finalizing a deck."""
deck_id: int
status: str
message: str
class DeckDeleteResponse(BaseModel):
"""Response after deleting a deck."""
deck_id: int
message: str
# ===== Search Schemas =====
class CardSearchRequest(BaseModel):
"""Card search request."""
query: str = Field(..., min_length=1, max_length=100)
limit: int = Field(50, ge=1, le=200)
offset: int = Field(0, ge=0)
class CardSearchResponse(BaseModel):
"""Card search response."""
cards: List[Dict[str, Any]]
total: int
page: int
page_size: int
total_pages: int
# ===== Generic Schemas =====
class MessageResponse(BaseModel):
"""Generic message response."""
message: str
class CountResponse(BaseModel):
"""Generic count response."""
count: int