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}>"