Phase 3: Schema Layer - Pydantic v2 migration, deduplication, and missing schemas

- Migrated all schemas to Pydantic v2 syntax (model_config, ConfigDict)
- Fixed mutable default in ProtoMessageBase using Field(default_factory=datetime.now)
- Consolidated CardCollection and Wishlist schemas in user_card_collection.py
- Created game_schemas.py with GameCreate, GameResponse, GameJoinRequest, etc.
- Created mtg_card_schemas.py with MtgCardResponse, MtgCardSearchRequest, etc.
- Added CardImportBatchCreate, CardImportBatchResponse, UserCardImportCreate/Response schemas
- Fixed duplicate UserCardImportRecord class between card_import_batch.py and user_card_import_record.py
- Updated __init__.py with comprehensive schema exports
- Created verify_schemas.py for schema-model matching verification
This commit is contained in:
2026-08-16 05:22:17 +00:00
parent 5f324cf8a9
commit bea91db64d
20 changed files with 2733 additions and 342 deletions
+6
View File
@@ -11,6 +11,11 @@ from app.models.user_data import (
from app.models.user_deck import UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion
from app.models.card_import_batch import CardImportBatch
from app.models.user_card_import_record import UserCardImportRecord
from app.models.user_card_import import UserCardImport
# Import relationships LAST to avoid circular imports
# This defines cross-model references (e.g., DecklistFile.card_links)
from app.models import relationships
__all__ = [
"User",
@@ -47,4 +52,5 @@ __all__ = [
"CardSuggestion",
"CardImportBatch",
"UserCardImportRecord",
"UserCardImport",
]
+53 -15
View File
@@ -1,5 +1,13 @@
"""SQLAlchemy ORM model for card import batches."""
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, JSON, Boolean
"""
SQLAlchemy ORM models for card import tracking.
This module provides models for tracking card import batches
and individual card import records.
"""
from sqlalchemy import (
Column, Integer, String, BigInteger, Boolean, DateTime, Text,
ForeignKey, UniqueConstraint
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
@@ -7,30 +15,60 @@ from app.core.database import Base
class CardImportBatch(Base):
"""
Card import batch record.
Card import batch tracking.
Tracks a single file import with its status and match results.
Tracks a batch of card imports from a file, including
matching results and error information.
"""
__tablename__ = "card_import_batches"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True)
id = Column(BigInteger, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
filename = Column(String(255), nullable=False)
file_type = Column(String(10), nullable=False) # xlsx, csv, json, ods
file_size = Column(Integer, nullable=False) # File size in bytes
status = Column(String(20), nullable=False, default="pending", index=True) # pending, processing, completed, failed
file_type = Column(String(50), nullable=True) # 'mtgjson', 'csv', etc.
file_size = Column(BigInteger, nullable=True) # Size in bytes
status = Column(String(20), default="PENDING") # PENDING, PROCESSING, COMPLETED, FAILED
total_cards = Column(Integer, default=0)
matched_cards = Column(Integer, default=0)
unmatched_cards = Column(Integer, default=0)
match_results = Column(JSON, nullable=True) # Store match results for later retrieval
match_results = Column(Text, nullable=True) # JSON of match details
error_message = Column(Text, nullable=True)
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
user = relationship("User", backref="import_batches")
user = relationship("User", backref="card_import_batches")
records = relationship(
"UserCardImportRecord",
back_populates="batch",
cascade="all, delete-orphan"
)
def __repr__(self) -> str:
return f"<CardImportBatch id={self.id} user={self.user_id} status={self.status}>"
return f"<CardImportBatch {self.filename} (status={self.status})>"
class UserCardImportRecord(Base):
"""
Individual card import record within a batch.
Tracks whether each card in an import batch was confirmed
and when the confirmation happened.
"""
__tablename__ = "user_card_imports_confirmed"
id = Column(BigInteger, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
batch_id = Column(BigInteger, ForeignKey("card_import_batches.id", ondelete="CASCADE"), nullable=False)
is_confirmed = Column(Boolean, default=False)
confirmed_at = Column(DateTime, nullable=True)
# Relationships
batch = relationship("CardImportBatch", back_populates="records")
__table_args__ = (
UniqueConstraint('user_id', 'batch_id', name='uq_user_card_import_batch'),
)
def __repr__(self) -> str:
return f"<UserCardImportRecord user={self.user_id} batch={self.batch_id} confirmed={self.is_confirmed}>"
+6 -13
View File
@@ -11,7 +11,6 @@ from sqlalchemy import (
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
from app.models.models import DecklistFile
class MtgCardMirror(Base):
@@ -25,7 +24,12 @@ class MtgCardMirror(Base):
__tablename__ = "mtg_cards_mirror"
id = Column(Integer, primary_key=True, index=True)
source_id = Column(Integer, nullable=True, index=True) # References mtg_cards.id
source_id = Column(
Integer,
nullable=True,
index=True,
comment="Logical reference to mtg_cards.id in mtg_data database (cross-DB reference, no FK constraint)"
)
name = Column(String(255), nullable=False, index=True)
mana_cost = Column(String(255), nullable=True)
type_line = Column(String(255), nullable=True)
@@ -91,14 +95,3 @@ class DeckCardLink(Base):
f"<DeckCardLink deck={self.deck_id} card={self.card_id} "
f"qty={self.quantity} zone={self.zone}>"
)
# Add back-references to existing models
from app.models.models import DecklistFile
DecklistFile.card_links = relationship(
"DeckCardLink",
back_populates="deck",
cascade="all, delete-orphan",
order_by="DeckCardLink.id"
)
+19 -1
View File
@@ -80,8 +80,21 @@ class MtgonlineCard(Base):
synced_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
created_at = Column(DateTime, server_default=func.now())
# Relationships
deck_cards = relationship(
"UserDeckCard",
back_populates="card",
cascade="all, delete-orphan"
)
# Indexes
__table_args__ = (
Index('idx_mtgonline_cards_name', 'name'),
Index('idx_mtgonline_cards_set', 'set_code'),
)
def __repr__(self) -> str:
return f"<MtonlineCard {self.name} (ID: {self.id}, source: {self.source_id})>"
return f"<MtgonlineCard {self.name} (ID: {self.id}, source: {self.source_id})>"
class DecklistFolder(Base):
@@ -99,6 +112,11 @@ class DecklistFolder(Base):
children = relationship("DecklistFolder", back_populates="parent", cascade="all, delete-orphan")
parent = relationship("DecklistFolder", back_populates="children", remote_side=[id])
files = relationship("DecklistFile", back_populates="folder", cascade="all, delete-orphan")
user_decks = relationship(
"UserDeck",
back_populates="folder",
lazy="select"
)
class DecklistFile(Base):
+24
View File
@@ -0,0 +1,24 @@
"""
Cross-model relationships.
This module defines relationships that reference models from other
model files (e.g., DecklistFile.card_links → DeckCardLink). These
must be defined AFTER all model classes have been imported to avoid
circular import issues.
Import this module LAST in app/models/__init__.py.
"""
from sqlalchemy.orm import relationship
from app.models.models import DecklistFile
from app.models.mirror_models import DeckCardLink
# Add the back-reference from DecklistFile to DeckCardLink.
# This was previously defined dynamically at the bottom of mirror_models.py,
# but that caused circular imports. Now it lives here and is imported last.
DecklistFile.card_links = relationship(
"DeckCardLink",
back_populates="deck",
cascade="all, delete-orphan",
order_by="DeckCardLink.id"
)
+18 -11
View File
@@ -1,11 +1,13 @@
"""
SQLAlchemy ORM model for user card imports.
Stores a user's imported card collection as a JSON string containing
a list of card names. This is the source data for building decks
from the user's actual card collection.
This model corresponds to the user_card_imports table created in migration 004.
It stores a user's imported card collection as a JSON array of card names.
"""
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, UniqueConstraint
from sqlalchemy import (
Column, Integer, String, BigInteger, DateTime, Text,
ForeignKey, UniqueConstraint
)
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
@@ -13,21 +15,26 @@ from app.core.database import Base
class UserCardImport(Base):
"""
User's imported card collection.
User card import record.
Stores a JSON string of card names that the user owns.
Used as the source for building decks from user's actual cards.
Stores a user's imported card collection as a JSON array of card names.
This table was created in migration 004 and may be superseded by
CardImportBatch and UserCardImportRecord in migration 005.
"""
__tablename__ = "user_card_imports"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, unique=True, index=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
card_names_json = Column(Text, nullable=False) # JSON array of card names
created_at = Column(DateTime, server_default=func.now())
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
# Relationships
user = relationship("User", backref="card_imports")
__table_args__ = (
UniqueConstraint('user_id', name='uq_user_card_imports_user_id'),
)
def __repr__(self) -> str:
return f"<UserCardImport user={self.user_id}>"
+9 -25
View File
@@ -1,27 +1,11 @@
"""SQLAlchemy ORM model for confirmed card imports."""
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
"""
SQLAlchemy ORM models for user card import records.
This module is deprecated. UserCardImportRecord is now defined in
card_import_batch.py along with CardImportBatch.
"""
# This file is kept for backward compatibility but the actual model
# is now in card_import_batch.py
from app.models.card_import_batch import UserCardImportRecord as UserCardImportRecord
class UserCardImportRecord(Base):
"""
Confirmed user card import record.
Stores the confirmed state of an imported card collection.
"""
__tablename__ = "user_card_imports_confirmed"
id = Column(Integer, primary_key=True, autoincrement=True)
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True)
batch_id = Column(Integer, ForeignKey("card_import_batches.id", ondelete="CASCADE"), nullable=False, index=True)
is_confirmed = Column(Boolean, nullable=False, default=True)
confirmed_at = Column(DateTime, server_default=func.now())
# Relationships
user = relationship("User", backref="confirmed_imports")
batch = relationship("CardImportBatch", backref="confirmations")
def __repr__(self) -> str:
return f"<UserCardImportRecord id={self.id} user={self.user_id} confirmed={self.is_confirmed}>"
__all__ = ["UserCardImportRecord"]
+1 -1
View File
@@ -69,7 +69,7 @@ class UserDeckCard(Base):
# Relationships
deck = relationship("UserDeck", back_populates="cards")
card = relationship("MtgonlineCard", backref="deck_cards")
card = relationship("MtgonlineCard", back_populates="deck_cards")
def __repr__(self) -> str:
return f"<UserDeckCard deck={self.deck_id} card={self.card_id} qty={self.quantity}>"
+133 -1
View File
@@ -1 +1,133 @@
# Schemas package
"""Schemas package initialization."""
from app.schemas.schemas import (
LoginRequest, LoginResponse, RefreshTokenRequest, TokenResponse,
UserBase, UserCreate, UserUpdate, UserResponse,
DeckCreate, DeckUpdate, DeckResponse,
FolderCreate, FolderResponse,
GameCreate, GameResponse,
RoomResponse,
BanCreate, BanResponse,
ErrorResponse, ValidationErrorResponse,
PaginationParams, PaginatedResponse,
CardMirrorResponse, DeckCardLinkResponse, DeckWithCardsResponse,
)
from app.schemas.user_data_schemas import (
DeckVersionStatus, GameReplayStatus, GameOutcomeType,
GroupMemberRole, NetworkMemberRole, UserPreferenceTheme, ActivityType,
SessionResponse, SessionCleanupResponse,
DeckVersionCreate, DeckVersionUpdate, DeckVersionResponse, DeckVersionListResponse,
GameReplayCreate, GameReplayUpdate, GameReplayResponse, GameReplayListResponse,
GameOutcomeCreate, GameOutcomeResponse, GameOutcomeListResponse,
UserStatisticsResponse, StatisticsUpdateResponse,
CardCollectionCreate, CardCollectionUpdate, CardCollectionResponse, CardCollectionListResponse,
WishlistCreate, WishlistUpdate, WishlistResponse, WishlistListResponse,
GroupCreate, GroupUpdate, GroupMemberCreate, GroupMemberUpdate, GroupMemberRemove,
GroupResponse, GroupListResponse, GroupChatMessageCreate, GroupChatMessageResponse, GroupChatMessageListResponse,
NetworkCreate, NetworkUpdate, NetworkMemberCreate,
NetworkResponse, NetworkListResponse,
UserPreferenceUpdate, UserPreferenceResponse,
ActivityLogEntry, ActivityLogListResponse,
MessageResponse, CountResponse, ErrorDetail,
)
from app.schemas.user_deck_schemas import (
DeckStatus, DeckZone, SuggestionType,
UserDeckCreate, UserDeckUpdate, UserDeckResponse, UserDeckListResponse,
DeckCardCreate, DeckCardUpdate, DeckCardResponse, DeckCardWithDetailsResponse, DeckCardListResponse,
PrecedentCreate, PrecedentUpdate, PrecedentResponse, PrecedentListResponse,
SuggestionCreate, SuggestionResponse, SuggestionListResponse,
DeckFinalizeRequest, DeckFinalizeResponse, DeckDeleteResponse,
CardSearchRequest, CardSearchResponse,
)
from app.schemas.user_card_collection import (
CardCondition, AcquisitionMethod,
CollectionStatistics, CollectionSummaryResponse,
)
from app.schemas.card_import_schemas import (
CardImportRequest, CardImportResponse, CardImportStatusResponse,
CardMatchResult, CardImportSummary,
CardImportBatchCreate, CardImportBatchResponse,
UserCardImportCreate, UserCardImportResponse, UserCardImportRecordResponse,
)
from app.schemas.card_search_schemas import (
CardResponse, SetResponse, CardTypeResponse,
)
from app.schemas.proto_messages import (
ProtoMessageBase, SessionCommand, GameCommand, GameEvent, Response,
ServerInfoUser, ServerInfoDeckStorageFile, ServerInfoDeckStorageFolder,
ServerInfoDeckStorageTreeItem, ServerInfoCard, ServerInfoZone, ServerInfoGame,
)
from app.schemas.protocol_constants import (
SessionCommandType, GameCommandType, GameEventType, ResponseCode,
ZoneType, UserLevelFlag,
)
from app.schemas.game_schemas import (
GameCreate, GameResponse, GameJoinRequest, GameLeaveRequest,
GameListResponse, GamePlayerResponse, GameStateResponse,
)
from app.schemas.mtg_card_schemas import (
MtgCardResponse, MtgCardSearchRequest, MtgCardSearchResponse,
MtgSetResponse, MtgCardMirrorResponse, DeckCardLinkResponse, DeckWithCardsResponse,
)
__all__ = [
# Auth
"LoginRequest", "LoginResponse", "RefreshTokenRequest", "TokenResponse",
# User
"UserBase", "UserCreate", "UserUpdate", "UserResponse",
# Deck
"DeckCreate", "DeckUpdate", "DeckResponse",
"FolderCreate", "FolderResponse",
# Game
"GameCreate", "GameResponse", "RoomResponse",
"GameJoinRequest", "GameLeaveRequest", "GameListResponse", "GamePlayerResponse", "GameStateResponse",
# Ban
"BanCreate", "BanResponse",
# Error
"ErrorResponse", "ValidationErrorResponse",
# Pagination
"PaginationParams", "PaginatedResponse",
# Card Mirror
"CardMirrorResponse", "DeckCardLinkResponse", "DeckWithCardsResponse",
# User Data
"DeckVersionStatus", "GameReplayStatus", "GameOutcomeType",
"GroupMemberRole", "NetworkMemberRole", "UserPreferenceTheme", "ActivityType",
"SessionResponse", "SessionCleanupResponse",
"DeckVersionCreate", "DeckVersionUpdate", "DeckVersionResponse", "DeckVersionListResponse",
"GameReplayCreate", "GameReplayUpdate", "GameReplayResponse", "GameReplayListResponse",
"GameOutcomeCreate", "GameOutcomeResponse", "GameOutcomeListResponse",
"UserStatisticsResponse", "StatisticsUpdateResponse",
"CardCollectionCreate", "CardCollectionUpdate", "CardCollectionResponse", "CardCollectionListResponse",
"WishlistCreate", "WishlistUpdate", "WishlistResponse", "WishlistListResponse",
"GroupCreate", "GroupUpdate", "GroupMemberCreate", "GroupMemberUpdate", "GroupMemberRemove",
"GroupResponse", "GroupListResponse", "GroupChatMessageCreate", "GroupChatMessageResponse", "GroupChatMessageListResponse",
"NetworkCreate", "NetworkUpdate", "NetworkMemberCreate",
"NetworkResponse", "NetworkListResponse",
"UserPreferenceUpdate", "UserPreferenceResponse",
"ActivityLogEntry", "ActivityLogListResponse",
"MessageResponse", "CountResponse", "ErrorDetail",
# User Deck
"DeckStatus", "DeckZone", "SuggestionType",
"UserDeckCreate", "UserDeckUpdate", "UserDeckResponse", "UserDeckListResponse",
"DeckCardCreate", "DeckCardUpdate", "DeckCardResponse", "DeckCardWithDetailsResponse", "DeckCardListResponse",
"PrecedentCreate", "PrecedentUpdate", "PrecedentResponse", "PrecedentListResponse",
"SuggestionCreate", "SuggestionResponse", "SuggestionListResponse",
"DeckFinalizeRequest", "DeckFinalizeResponse", "DeckDeleteResponse",
"CardSearchRequest", "CardSearchResponse",
# Card Collection
"CardCondition", "AcquisitionMethod",
"CollectionStatistics", "CollectionSummaryResponse",
# Card Import
"CardImportRequest", "CardImportResponse", "CardImportStatusResponse",
"CardMatchResult", "CardImportSummary",
"CardImportBatchCreate", "CardImportBatchResponse",
"UserCardImportCreate", "UserCardImportResponse", "UserCardImportRecordResponse",
# Card Search
"CardResponse", "SetResponse", "CardTypeResponse",
# Proto Messages
"ProtoMessageBase", "SessionCommand", "GameCommand", "GameEvent", "Response",
"ServerInfoUser", "ServerInfoDeckStorageFile", "ServerInfoDeckStorageFolder",
"ServerInfoDeckStorageTreeItem", "ServerInfoCard", "ServerInfoZone", "ServerInfoGame",
# Protocol Constants
"SessionCommandType", "GameCommandType", "GameEventType", "ResponseCode",
"ZoneType", "UserLevelFlag",
]
+60 -1
View File
@@ -4,7 +4,7 @@ Pydantic schemas for card import feature.
Provides request/response models for importing card collections
and using them for deckbuilding.
"""
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, ConfigDict
from typing import List, Optional, Dict, Any
from datetime import datetime
@@ -53,6 +53,65 @@ class CardImportSummary(BaseModel):
import_id: Optional[int] = None
# ===== Card Import Batch Schemas =====
class CardImportBatchCreate(BaseModel):
"""Card import batch creation request."""
user_id: int
card_names: List[str] = Field(..., min_length=1, max_length=10000)
source: Optional[str] = None # 'manual', 'mtgjson', 'deck_text'
class CardImportBatchResponse(BaseModel):
"""Card import batch response."""
id: int
user_id: int
card_names: List[str]
source: Optional[str]
status: str # 'pending', 'processing', 'completed', 'failed'
total_cards: int
matched_cards: int
unmatched_cards: int
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class UserCardImportCreate(BaseModel):
"""User card import request."""
batch_id: int
card_id: int
quantity: int = Field(1, ge=1)
match_confidence: Optional[float] = None
class UserCardImportResponse(BaseModel):
"""User card import response."""
id: int
user_id: int
batch_id: int
card_id: int
quantity: int
match_confidence: Optional[float]
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class UserCardImportRecordResponse(BaseModel):
"""User card import record response."""
id: int
user_id: int
card_id: int
batch_id: int
quantity: int
source: Optional[str]
created_at: datetime
model_config = ConfigDict(from_attributes=True)
# Generic response models
class MessageResponse(BaseModel):
"""Generic message response."""
+64
View File
@@ -0,0 +1,64 @@
"""Pydantic schemas for game features."""
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List, Dict, Any
from datetime import datetime
class GameCreate(BaseModel):
"""Game creation request."""
room_id: int
game_type: Optional[str] = None
description: Optional[str] = None
password: Optional[str] = None
max_players: int = Field(4, ge=2, le=8)
class GameResponse(BaseModel):
"""Game response payload."""
id: int
room_id: int
game_type: Optional[str]
description: Optional[str]
with_password: bool
max_players: int
player_count: int
started: bool
creation_date: datetime
model_config = ConfigDict(from_attributes=True)
class GameJoinRequest(BaseModel):
"""Game join request."""
game_id: int
password: Optional[str] = None
class GameLeaveRequest(BaseModel):
"""Game leave request."""
game_id: int
class GameListResponse(BaseModel):
"""List of games."""
games: List[GameResponse]
total: int
class GamePlayerResponse(BaseModel):
"""Game player response."""
user_id: int
username: str
deck_id: Optional[int] = None
is_ready: bool = False
is_host: bool = False
class GameStateResponse(BaseModel):
"""Game state response."""
game_id: int
players: List[GamePlayerResponse]
turn: int
phase: str
zones: Dict[str, Any]
updated_at: datetime
+122
View File
@@ -0,0 +1,122 @@
"""Pydantic schemas for MTG card data."""
from pydantic import BaseModel, Field, ConfigDict
from typing import Optional, List, Dict, Any
from datetime import datetime
class MtgCardResponse(BaseModel):
"""MTG card response with full details."""
id: int
source_id: Optional[int] = None
name: str
mana_cost: Optional[str] = None
type_line: Optional[str] = None
oracle_text: Optional[str] = None
power: Optional[str] = None
toughness: Optional[str] = None
rarity: Optional[str] = None
layout: Optional[str] = None
artist: Optional[str] = None
flavor_text: Optional[str] = None
numbers: Optional[str] = None
identifiers: Optional[Dict[str, Any]] = None
images: Optional[Dict[str, Any]] = None
image: Optional[str] = None
card_parts: Optional[List[str]] = None
keywords: Optional[List[str]] = None
legalities: Optional[Dict[str, str]] = None
set_code: Optional[str] = None
set_name: Optional[str] = None
synced_at: Optional[datetime] = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class MtgCardSearchRequest(BaseModel):
"""MTG card search request."""
query: str = Field(..., min_length=1, max_length=100)
set_code: Optional[str] = None
rarity: Optional[str] = None
type_line: Optional[str] = None
limit: int = Field(50, ge=1, le=200)
offset: int = Field(0, ge=0)
class MtgCardSearchResponse(BaseModel):
"""MTG card search response."""
cards: List[MtgCardResponse]
total: int
page: int
page_size: int
total_pages: int
class MtgSetResponse(BaseModel):
"""MTG set response."""
id: int
code: str
name: str
release_date: Optional[datetime] = None
card_count: Optional[int] = None
type: Optional[str] = None
border: Optional[str] = None
mcm_id: Optional[int] = None
model_config = ConfigDict(from_attributes=True)
class MtgCardMirrorResponse(BaseModel):
"""Mirrored card data for user decks."""
id: int
source_id: Optional[int] = None
name: str
mana_cost: Optional[str] = None
type_line: Optional[str] = None
oracle_text: Optional[str] = None
power: Optional[str] = None
toughness: Optional[str] = None
rarity: Optional[str] = None
layout: Optional[str] = None
artist: Optional[str] = None
flavor_text: Optional[str] = None
numbers: Optional[str] = None
identifiers: Optional[Dict[str, Any]] = None
images: Optional[Dict[str, Any]] = None
image: Optional[str] = None
card_parts: Optional[str] = None
keywords: Optional[str] = None
legalities: Optional[Dict[str, str]] = None
set_code: Optional[str] = None
set_name: Optional[str] = None
synced_at: Optional[datetime] = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class DeckCardLinkResponse(BaseModel):
"""Deck-card link response."""
id: int
deck_id: int
card_id: int
quantity: int
zone: str
card: MtgCardMirrorResponse
model_config = ConfigDict(from_attributes=True)
class DeckWithCardsResponse(BaseModel):
"""Deck response with card links."""
id: int
name: str
content: str
format: str
status: str
folder_id: Optional[int] = None
owner_id: int
creation_date: datetime
card_links: List[DeckCardLinkResponse] = []
model_config = ConfigDict(from_attributes=True)