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:
@@ -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
|
||||
Reference in New Issue
Block a user