- Switched from psycopg2 to asyncpg for async SQLAlchemy support - Fixed router registration in main.py - removed duplicate prefixes - Added user_data export to routers/__init__.py - Refactored decks router to use DeckManager service layer - Integrated FuzzyCardMatcher into card_router search endpoints - Made WishlistCreate.card_id optional for proper schema validation - Set PostgreSQL password and configured scram-sha-256 auth - Updated alembic.ini to use local PostgreSQL instead of Docker hostname - Created generic_schemas.py for reusable schema patterns - Added test_routers.py and test_schema_validation.py test files - All 6 Alembic migrations applied successfully (37 tables created) - Application running on port 8000 with all services connected
218 lines
5.0 KiB
Python
218 lines
5.0 KiB
Python
"""Pydantic schemas for user deck building features."""
|
|
from pydantic import BaseModel, Field, ConfigDict
|
|
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."""
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
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 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."""
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
id: int
|
|
deck_id: int
|
|
card_id: int
|
|
quantity: int
|
|
zone: str
|
|
position: Optional[int]
|
|
|
|
|
|
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."""
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
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 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."""
|
|
model_config = ConfigDict(from_attributes=True)
|
|
|
|
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 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)
|
|
|