Fix migration 001 - add base table creation for mtgonline_users, mtgonline_decklist_files, and mtgonline_rooms
This commit is contained in:
@@ -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,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}>"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user