Fix migration 001 - add base table creation for mtgonline_users, mtgonline_decklist_files, and mtgonline_rooms

This commit is contained in:
2026-08-08 05:23:39 +00:00
parent 1556a6d1aa
commit 6bb4034098
16 changed files with 498 additions and 89 deletions
+2 -2
View File
@@ -19,10 +19,10 @@ class Settings(BaseSettings):
JWT_SECRET_KEY: str = "change-me-in-production"
# Database - Primary (mtgonline app)
DATABASE_URL: str = "postgresql+asyncpg://mtgonline_user:mtgonline_password@postgres:5432/mtgonline"
DATABASE_URL: str = "postgresql+asyncpg://mtgonline:mtgonline_pass@postgres:5432/mtgonline"
# Database - Secondary (mtgjson data)
MTG_DATABASE_URL: str = "postgresql+asyncpg://mtgonline_user:mtgonline_password@mtgdata:5432/mtgdata"
MTG_DATABASE_URL: str = "postgresql+asyncpg://mtgonline:mtgonline_pass@mtgdata:5432/mtgdata"
# Redis
REDIS_URL: str = "redis://redis:6379/0"
+1 -21
View File
@@ -1,5 +1,5 @@
"""SQLAlchemy ORM model for card import batches."""
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, JSON
from sqlalchemy import Column, Integer, String, DateTime, ForeignKey, Text, JSON, Boolean
from sqlalchemy.orm import relationship
from sqlalchemy.sql import func
from app.core.database import Base
@@ -34,23 +34,3 @@ class CardImportBatch(Base):
return f"<CardImportBatch id={self.id} user={self.user_id} status={self.status}>"
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}>"
+1 -1
View File
@@ -16,7 +16,7 @@ from app.core.database import get_db
from app.core.security import get_current_user
from app.models.card_import_batch import CardImportBatch
from app.models.user_card_import_record import UserCardImportRecord
from app.models.user_card_collection import UserCardCollection
from app.models.user_data import UserCardCollection
from app.models.models import MtgonlineCard
from app.models.user_deck import UserDeck, UserDeckCard
from app.services.file_parser import FileParser
+1 -1
View File
@@ -1174,7 +1174,7 @@ async def add_group_member(
return {"message": "Member added", "member_id": member.id}
@router.patch("/groups/{group_id}/members/{member_id}", status_model=GroupMemberUpdate)
@router.patch("/groups/{group_id}/members/{member_id}", response_model=GroupMemberUpdate)
async def update_group_member(
group_id: int,
member_id: int,
+159
View File
@@ -0,0 +1,159 @@
"""
Pydantic schemas for user card collection features.
Covers card collection CRUD operations, wishlist management, and collection statistics.
"""
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from datetime import datetime
from enum import Enum
# ===== Enum Types =====
class CardCondition(str, Enum):
"""Card condition ratings."""
NEAR_MINT = "NEAR_MINT"
LIGHTLY_PLAYED = "LIGHTLY_PLAYED"
MODERATELY_PLAYED = "MODERATELY_PLAYED"
HEAVILY_PLAYED = "HEAVILY_PLAYED"
DAMAGED = "DAMAGED"
class AcquisitionMethod(str, Enum):
"""How a card was acquired."""
PACK_OPENING = "PACK_OPENING"
TRADE = "TRADE"
PURCHASE = "PURCHASE"
GIFT = "GIFT"
CONTEST = "CONTEST"
OTHER = "OTHER"
# ===== Card Collection Schemas =====
class CardCollectionCreate(BaseModel):
"""Card collection item creation request."""
card_id: int
quantity: int = Field(1, ge=1)
condition: CardCondition = CardCondition.NEAR_MINT
language: str = Field("EN", max_length=5)
is_foil: bool = False
is_alt_art: bool = False
acquired_date: Optional[datetime] = None
acquisition_method: Optional[AcquisitionMethod] = None
notes: Optional[str] = None
class CardCollectionUpdate(BaseModel):
"""Card collection item update request."""
quantity: Optional[int] = None
condition: Optional[CardCondition] = None
language: Optional[str] = None
is_foil: Optional[bool] = None
is_alt_art: Optional[bool] = None
acquired_date: Optional[datetime] = None
acquisition_method: Optional[AcquisitionMethod] = None
notes: Optional[str] = None
class CardCollectionResponse(BaseModel):
"""Card collection item response."""
id: int
user_id: int
card_id: int
quantity: int
condition: str
language: str
is_foil: bool
is_alt_art: bool
acquired_date: Optional[datetime]
acquisition_method: Optional[str]
notes: Optional[str]
created_at: datetime
updated_at: datetime
class Config:
from_attributes = True
class CardCollectionListResponse(BaseModel):
"""List of user card collection."""
cards: List[CardCollectionResponse]
total: int
page: int
page_size: int
total_pages: int
# ===== Wishlist Schemas =====
class WishlistCreate(BaseModel):
"""Wishlist item creation request."""
card_id: int
max_price: Optional[float] = None
notes: Optional[str] = None
class WishlistUpdate(BaseModel):
"""Wishlist item update request."""
max_price: Optional[float] = None
notes: Optional[str] = None
class WishlistResponse(BaseModel):
"""Wishlist item response."""
id: int
user_id: int
card_id: int
max_price: Optional[float]
notes: Optional[str]
created_at: datetime
class Config:
from_attributes = True
class WishlistListResponse(BaseModel):
"""List of wishlist items."""
items: List[WishlistResponse]
total: int
# ===== Collection Statistics Schemas =====
class CollectionStatistics(BaseModel):
"""User collection statistics summary."""
total_cards: int
unique_cards: int
total_quantity: int
foil_count: int
alt_art_count: int
condition_breakdown: Dict[str, int]
language_breakdown: Dict[str, int]
acquisition_breakdown: Dict[str, int]
class CollectionSummaryResponse(BaseModel):
"""Collection summary with statistics."""
statistics: CollectionStatistics
recent_acquisitions: List[CardCollectionResponse]
top_cards: List[CardCollectionResponse]
# ===== Generic Response Schemas =====
class MessageResponse(BaseModel):
"""Generic message response."""
message: str
class CountResponse(BaseModel):
"""Generic count response."""
count: int
class ErrorDetail(BaseModel):
"""Error detail."""
error: str
detail: str
+30 -8
View File
@@ -1,11 +1,11 @@
"""Services package initialization."""
from app.services.deck_parser import DeckParser
from app.services.card_database import search_cards, get_card_by_name, get_cards_by_set, get_card_types, get_card_rarities, get_sets, get_set_by_code, get_card_statistics
from app.services.card_mirror_service import CardMirrorService
from app.services.card_mirror_service import upsert_card_mirror, get_card_mirror_by_name, search_card_mirrors, add_card_to_deck, remove_card_from_deck, get_deck_cards, get_user_deck_summaries, sync_mirrors_from_mtg_cards, get_card_statistics
from app.services.mtgjson_manager import MTGJSONManager, get_manager
from app.services.mtgjson_downloader import MTGJSONDownloader
from app.services.mtgjson_loader import MTGJSONLoader
from app.services.mtgjson_uploader import MTGJSONUploader
from app.services.mtgjson_downloader import download_all_files, verify_downloads, get_file_list
from app.services.mtgjson_loader import create_tables, download_file, extract_zip, get_all_printings_psql_file, parse_psql_file, import_cards, import_all_printings_psql, import_all_set_files, import_all_identifiers, import_all_deck_files, import_simple_json, create_indexes, show_summary, main
from app.services.mtgjson_uploader import create_tables, import_all_printings_psql, import_all_set_files, import_all_identifiers, import_all_deck_files, import_json_files, download_file, extract_zip, main
from app.services.file_parser import FileParser
from app.services.fuzzy_card_matcher import FuzzyCardMatcher
from app.services.import_batch_processor import ImportBatchProcessor
@@ -23,12 +23,34 @@ __all__ = [
"get_sets",
"get_set_by_code",
"get_card_statistics",
"CardMirrorService",
"upsert_card_mirror",
"get_card_mirror_by_name",
"search_card_mirrors",
"add_card_to_deck",
"remove_card_from_deck",
"get_deck_cards",
"get_user_deck_summaries",
"sync_mirrors_from_mtg_cards",
"MTGJSONManager",
"get_manager",
"MTGJSONDownloader",
"MTGJSONLoader",
"MTGJSONUploader",
"download_all_files",
"verify_downloads",
"get_file_list",
"create_tables",
"download_file",
"extract_zip",
"get_all_printings_psql_file",
"parse_psql_file",
"import_cards",
"import_all_printings_psql",
"import_all_set_files",
"import_all_identifiers",
"import_all_deck_files",
"import_simple_json",
"import_json_files",
"create_indexes",
"show_summary",
"main",
"FileParser",
"FuzzyCardMatcher",
"ImportBatchProcessor",
@@ -8,7 +8,7 @@ from sqlalchemy.sql import func
from app.models.card_import_batch import CardImportBatch
from app.models.user_card_import_record import UserCardImportRecord
from app.models.user_card_collection import UserCardCollection
from app.models.user_data import UserCardCollection
from app.models.models import MtgonlineCard
from app.services.fuzzy_card_matcher import FuzzyCardMatcher
+115 -1
View File
@@ -22,11 +22,125 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.orm import sessionmaker
from sqlalchemy import text, insert, update, select, and_
from sqlalchemy import text, insert, update, select, and_, Table, MetaData, Column, Integer, String, Text, Boolean, DateTime, Date, ForeignKey
from sqlalchemy.dialects.postgresql import insert as pg_insert
from app.core.settings import get_settings
# Define table metadata
metadata = MetaData()
# Define table objects
mtg_sets_table = Table('mtg_sets', metadata,
Column('id', Integer, primary_key=True),
Column('code', String(10), unique=True, nullable=False),
Column('name', String(255)),
Column('type', String(100)),
Column('release_date', Date),
Column('base_set_size', Integer),
Column('total_size', Integer),
Column('is_foil_only', Boolean),
Column('is_non_foil_only', Boolean),
Column('digital', Boolean),
Column('icon_svg_url', Text),
Column('parent_code', String(10)),
Column('mtgo_code', String(10)),
Column('card_count', Integer),
Column('image_url', Text),
Column('created_at', DateTime, default=datetime.utcnow),
Column('updated_at', DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
mtg_cards_table = Table('mtg_cards', metadata,
Column('id', Integer, primary_key=True),
Column('set_id', Integer, ForeignKey('mtg_sets.id')),
Column('name', String(255)),
Column('mana_cost', String(255)),
Column('type_line', String(255)),
Column('oracle_text', Text),
Column('power', String(50)),
Column('toughness', String(50)),
Column('rarity', String(50)),
Column('layout', String(50)),
Column('artist', String(255)),
Column('flavor_text', Text),
Column('numbers', String(100)),
Column('identifiers', Text),
Column('images', Text),
Column('created_at', DateTime, default=datetime.utcnow),
Column('updated_at', DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
card_identifiers_table = Table('card_identifiers', metadata,
Column('id', Integer, primary_key=True),
Column('uuid', String, unique=True, nullable=False),
Column('name', String(255)),
Column('mana_cost', String(255)),
Column('type_line', String(255)),
Column('oracle_text', Text),
Column('power', String(50)),
Column('toughness', String(50)),
Column('rarity', String(50)),
Column('layout', String(50)),
Column('artist', String(255)),
Column('flavor_text', Text),
Column('set_code', String(10)),
Column('created_at', DateTime, default=datetime.utcnow),
Column('updated_at', DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
decks_table = Table('decks', metadata,
Column('id', Integer, primary_key=True),
Column('name', String(255), nullable=False),
Column('description', Text),
Column('format', String(50)),
Column('command', Text),
Column('commander', Text),
Column('creation_date', DateTime, default=datetime.utcnow),
Column('updated_at', DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
card_types_table = Table('card_types', metadata,
Column('id', Integer, primary_key=True),
Column('type', String(100), unique=True, nullable=False),
Column('description', Text),
Column('created_at', DateTime, default=datetime.utcnow),
Column('updated_at', DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
deck_list_table = Table('deck_list', metadata,
Column('id', Integer, primary_key=True),
Column('deck_id', String(100), unique=True, nullable=False),
Column('name', String(255)),
Column('description', Text),
Column('format', String(50)),
Column('command', Text),
Column('commander', Text),
Column('total_cards', Integer),
Column('created_at', DateTime, default=datetime.utcnow),
Column('updated_at', DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
card_keywords_table = Table('card_keywords', metadata,
Column('id', Integer, primary_key=True),
Column('keyword', String(100), unique=True, nullable=False),
Column('description', Text),
Column('created_at', DateTime, default=datetime.utcnow),
Column('updated_at', DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
set_list_table = Table('set_list', metadata,
Column('id', Integer, primary_key=True),
Column('set_code', String(10), unique=True, nullable=False),
Column('set_name', String(255)),
Column('set_type', String(100)),
Column('release_date', Date),
Column('base_set_size', Integer),
Column('total_size', Integer),
Column('created_at', DateTime, default=datetime.utcnow),
Column('updated_at', DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
)
# MTGJSON API v5 base URL
MTGJSON_API_V5 = "https://mtgjson.com/api/v5"