Files
akadmin bea91db64d 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
2026-08-16 05:22:17 +00:00

43 KiB

MTG Online Backend - Code Structure Documentation

Project Overview

Location: /home/wall-o/projects/mtgonline/backend/

Stack: FastAPI + SQLAlchemy (async) + Alembic + PostgreSQL + Redis

Purpose: MTG Online card database management, deck building, user management, and game infrastructure.


1. Models (app/models/)

1.1 Core Models (models.py)

User (mtgonline_users)

  • id: Integer, primary key, indexed
  • username: String(64), unique, indexed
  • password_hash: String(128), bcrypt hash
  • salt: String(128)
  • email: String(255), indexed
  • country: String(2)
  • real_name: String(128)
  • avatar_bmp: Text (Base64 encoded)
  • privlevel: String(50), default "User"
  • is_active: Boolean, default True
  • is_banned: Boolean, default False
  • ban_reason: Text
  • ban_ends: DateTime
  • vip_status: Integer (0=normal, 1=vip, 2=donator)
  • vip_expiry: DateTime
  • creation_date: DateTime, server default now()
  • last_login: DateTime
  • Relationships: decklist_files, decklist_folders (both cascade delete)

MtonlineCard (mtgonline_cards)

  • id: Integer, primary key, indexed
  • source_id: Integer, indexed (references mtg_cards.id in mtgdata)
  • name: String(255), indexed
  • mana_cost: String(255)
  • type_line: String(255)
  • oracle_text: Text
  • power: String(50)
  • toughness: String(50)
  • rarity: String(50)
  • layout: String(50)
  • artist: String(255)
  • flavor_text: Text
  • numbers: String(100)
  • identifiers: Text (JSON string)
  • images: Text (JSON string)
  • image: Text (card image URL)
  • set_code: String(10), indexed
  • set_name: String(255)
  • card_parts: Text (comma-separated face names)
  • keywords: Text (comma-separated)
  • legalities: Text (JSON of format legality)
  • synced_at: DateTime, onupdate now()
  • created_at: DateTime, server default now()

DecklistFolder (mtgonline_decklist_folders)

  • id: Integer, primary key, indexed
  • owner_id: Integer, FK to mtgonline_users.id
  • name: String(255)
  • parent_id: Integer, FK to self (recursive)
  • creation_date: DateTime
  • Relationships: owner, children (recursive), parent, files (cascade delete)

DecklistFile (mtgonline_decklist_files)

  • id: Integer, primary key, indexed
  • folder_id: Integer, FK to mtgonline_decklist_folders.id
  • owner_id: Integer, FK to mtgonline_users.id
  • name: String(255)
  • content: Text (native XML or plain text)
  • format: String(50), default "native"
  • status: String(20), default "DRAUGHT"
  • creation_date: DateTime
  • Relationships: folder, owner, card_links (added in mirror_models.py)

Room (mtgonline_rooms)

  • id: Integer, primary key, indexed
  • name: String(100), unique
  • description: Text
  • is_password_protected: Boolean
  • password_hash: String(128)
  • creation_date: DateTime
  • Relationships: game_types (cascade delete)

RoomGameType (mtgonline_rooms_gametypes)

  • id: Integer, primary key, indexed
  • room_id: Integer, FK to mtgonline_rooms.id
  • name: String(100)
  • description: Text
  • Relationships: room

Ban (mtgonline_bans)

  • id: Integer, primary key, indexed
  • user_id: Integer, FK to mtgonline_users.id
  • server_id: Integer
  • reason: Text
  • moderators: String(255)
  • ip_address: String(45)
  • expiration_time: DateTime
  • active: Boolean, default True
  • creation_date: DateTime
  • Relationships: user

GameLog (mtgonline_log)

  • id: Integer, primary key, indexed
  • room_id: Integer, FK to mtgonline_rooms.id
  • player_id: Integer, FK to mtgonline_users.id
  • message: Text
  • timestamp: DateTime
  • Relationships: room, player

AuditLog (mtgonline_audit)

  • id: Integer, primary key, indexed
  • admin_id: Integer, FK to mtgonline_users.id
  • action_type: String(50)
  • target_user_id: Integer, FK to mtgonline_users.id
  • details: Text
  • ip_address: String(45)
  • timestamp: DateTime
  • Relationships: admin, target_user

Indexes:

  • idx_decks_owner on DecklistFile.owner_id
  • idx_decks_folder on DecklistFile.folder_id
  • idx_bans_active on Ban.active
  • idx_log_timestamp on GameLog.timestamp

1.2 MTG Models (mtg_models.py)

MtgSet (mtg_sets)

  • id: Integer, primary key, indexed
  • code: String(10), unique, indexed
  • name: String(255)
  • type: String(100)
  • release_date: DateTime
  • base_set_size: Integer
  • total_size: Integer
  • is_foil_only: Integer
  • is_non_foil_only: Integer
  • digital: Integer
  • icon_svg_url: Text
  • parent_code: String(10)
  • mtgo_code: String(10)
  • image: Text
  • updated_at: DateTime
  • Relationships: cards

MtgCard (mtg_cards)

  • id: Integer, primary key, indexed
  • set_id: Integer, FK to mtg_sets.id, indexed
  • name: String(255), indexed
  • mana_cost: String(255), indexed
  • type_line: String(255), indexed
  • oracle_text: Text
  • power: String(50)
  • toughness: String(50)
  • rarity: String(50), indexed
  • layout: String(50)
  • artist: String(255)
  • flavor_text: Text
  • numbers: String(100)
  • identifiers: Text (JSON)
  • images: Text (JSON)
  • image: Text
  • updated_at: DateTime
  • Relationships: set

Indexes:

  • idx_mtg_cards_name_set on MtgCard.name, MtgCard.set_id
  • idx_mtg_cards_type on MtgCard.type_line
  • idx_mtg_cards_rarity on MtgCard.rarity

1.3 Mirror Models (mirror_models.py)

MtgCardMirror (mtg_cards_mirror)

  • id: Integer, primary key, indexed
  • source_id: Integer, indexed (references mtg_cards.id)
  • name: String(255), indexed
  • mana_cost: String(255)
  • type_line: String(255)
  • oracle_text: Text
  • power: String(50)
  • toughness: String(50)
  • rarity: String(50)
  • layout: String(50)
  • artist: String(255)
  • flavor_text: Text
  • numbers: String(100)
  • identifiers: Text (JSON)
  • images: Text (JSON)
  • image: Text
  • card_parts: Text
  • keywords: Text
  • legalities: Text (JSON)
  • set_code: String(10), indexed
  • set_name: String(255)
  • synced_at: DateTime, onupdate now()
  • created_at: DateTime
  • Relationships: deck_links (cascade delete)

DeckCardLink (deck_card_links)

  • id: Integer, primary key, indexed
  • deck_id: Integer, FK to mtgonline_decklist_files.id, CASCADE delete
  • card_id: Integer, FK to mtg_cards_mirror.id
  • quantity: Integer, default 1
  • zone: String(20), default "main"
  • Constraints: UniqueConstraint(deck_id, card_id, zone)
  • Indexes: idx_deck_card_deck, idx_deck_card_card
  • Relationships: deck, card

Note: DecklistFile has a back-reference added: card_links relationship to DeckCardLink


1.4 User Data Models (user_data.py)

UserSession (user_sessions)

  • id: BigInteger, primary key
  • user_id: Integer, FK to mtgonline_users.id, CASCADE, indexed
  • session_token_hash: String(255), unique, indexed
  • ip_address: String(45)
  • user_agent: Text
  • created_at: DateTime
  • expires_at: DateTime
  • is_active: Boolean, default True
  • Relationships: user

DeckVersion (deck_versions)

  • id: BigInteger, primary key
  • deck_id: Integer, FK to mtgonline_decklist_files.id, CASCADE, indexed
  • version_number: Integer
  • content: Text
  • status: String(20), default "DRAFT"
  • comment: Text
  • created_at: DateTime
  • Relationships: deck

GameReplay (game_replays)

  • id: BigInteger, primary key
  • game_uuid: String(36), unique
  • room_id: Integer, FK to mtgonline_rooms.id, indexed
  • game_type: String(50)
  • format: String(50)
  • duration_seconds: Integer
  • start_time: DateTime
  • end_time: DateTime
  • status: String(20), default "IN_PROGRESS"
  • replay_data: JSON
  • created_at: DateTime
  • updated_at: DateTime
  • Relationships: players (cascade), outcomes (cascade)

ReplayPlayer (replay_players)

  • id: BigInteger, primary key
  • replay_id: BigInteger, FK to game_replays.id, CASCADE, indexed
  • user_id: Integer, FK to mtgonline_users.id, indexed
  • position: Integer
  • deck_id: Integer, FK to mtgonline_decklist_files.id
  • won: Boolean
  • lost: Boolean
  • concession: Boolean, default False
  • turn_one: Boolean, default False
  • created_at: DateTime
  • Relationships: replay, user, deck

GameOutcome (game_outcomes)

  • id: BigInteger, primary key
  • user_id: Integer, FK to mtgonline_users.id, indexed
  • game_uuid: String(36), FK to game_replays.game_uuid, indexed
  • outcome: String(20), indexed
  • opponent_id: Integer, FK to mtgonline_users.id
  • format: String(50)
  • rating_before: Integer
  • rating_after: Integer
  • rating_change: Integer
  • created_at: DateTime
  • Relationships: replay, user, opponent

UserStatistics (user_statistics)

  • user_id: Integer, FK to mtgonline_users.id, primary key
  • total_games: Integer, default 0
  • total_wins: Integer, default 0
  • total_losses: Integer, default 0
  • total_concessions: Integer, default 0
  • win_rate: Float, default 0.0
  • current_streak: Integer, default 0
  • best_streak: Integer, default 0
  • average_rating: Float, default 0.0
  • last_game_date: DateTime
  • updated_at: DateTime
  • Relationships: user

UserCardCollection (user_card_collection)

  • id: BigInteger, primary key
  • user_id: Integer, FK to mtgonline_users.id, CASCADE, indexed
  • card_id: Integer, indexed
  • quantity: Integer, default 1
  • condition: String(20), default "NEAR_MINT"
  • language: String(5), default "EN"
  • is_foil: Boolean, default False
  • is_alt_art: Boolean, default False
  • acquired_date: DateTime
  • acquisition_method: String(50)
  • notes: Text
  • created_at: DateTime
  • updated_at: DateTime
  • Constraints: UniqueConstraint(user_id, card_id, is_foil, is_alt_art)
  • Indexes: idx_collection_user_card
  • Relationships: user

CardWishlist (card_wishlist)

  • id: BigInteger, primary key
  • user_id: Integer, FK to mtgonline_users.id, CASCADE
  • card_id: Integer
  • max_price: Float
  • notes: Text
  • created_at: DateTime
  • Constraints: UniqueConstraint(user_id, card_id)
  • Relationships: user

UserGroup (user_groups)

  • id: BigInteger, primary key
  • name: String(100)
  • description: Text
  • owner_id: Integer, FK to mtgonline_users.id, indexed
  • is_public: Boolean, default True
  • max_members: Integer, default 50
  • created_at: DateTime
  • updated_at: DateTime
  • Relationships: owner, members (cascade), messages (cascade)

GroupMember (group_members)

  • id: BigInteger, primary key
  • group_id: BigInteger, FK to user_groups.id, CASCADE, indexed
  • user_id: Integer, FK to mtgonline_users.id, indexed
  • role: String(20), default "MEMBER"
  • joined_at: DateTime
  • Constraints: UniqueConstraint(group_id, user_id)
  • Relationships: group, user

GroupChatMessage (group_chat_messages)

  • id: BigInteger, primary key
  • group_id: BigInteger, FK to user_groups.id, CASCADE, indexed
  • sender_id: Integer, FK to mtgonline_users.id, indexed
  • message: Text
  • created_at: DateTime, indexed
  • Relationships: group, sender

UserNetwork (user_networks)

  • id: BigInteger, primary key
  • name: String(100)
  • description: Text
  • creator_id: Integer, FK to mtgonline_users.id
  • is_public: Boolean, default True
  • created_at: DateTime
  • Relationships: creator, members (cascade)

NetworkMember (network_members)

  • id: BigInteger, primary key
  • network_id: BigInteger, FK to user_networks.id, CASCADE, indexed
  • user_id: Integer, FK to mtgonline_users.id, indexed
  • role: String(20), default "MEMBER"
  • joined_at: DateTime
  • Constraints: UniqueConstraint(network_id, user_id)
  • Relationships: network, user

UserPreference (user_preferences)

  • user_id: Integer, FK to mtgonline_users.id, primary key
  • theme: String(20), default "light"
  • notifications_enabled: Boolean, default True
  • email_notifications: Boolean, default True
  • auto_save_decks: Boolean, default True
  • default_format: String(50), default "standard"
  • language: String(5), default "EN"
  • updated_at: DateTime
  • Relationships: user

UserActivityLog (user_activity_log)

  • id: BigInteger, primary key
  • user_id: Integer, FK to mtgonline_users.id, indexed
  • activity_type: String(50), indexed
  • activity_data: JSON
  • ip_address: String(45)
  • created_at: DateTime, indexed
  • Relationships: user

1.5 User Deck Models (user_deck.py)

UserDeck (user_decks)

  • id: BigInteger, primary key, autoincrement
  • user_id: Integer, FK to mtgonline_users.id, CASCADE, indexed
  • name: String(255), indexed
  • status: String(20), default "DRAFT", indexed
  • folder_id: Integer, FK to mtgonline_decklist_folders.id
  • format: String(50), default "standard"
  • notes: Text
  • is_precedent: Boolean, default False, indexed
  • precedent_name: String(255)
  • created_at: DateTime
  • updated_at: DateTime
  • Relationships: user, folder, cards (cascade, ordered by id)

UserDeckCard (user_deck_cards)

  • id: BigInteger, primary key, autoincrement
  • deck_id: BigInteger, FK to user_decks.id, CASCADE, indexed
  • card_id: Integer, FK to mtgonline_cards.id, indexed
  • quantity: Integer, default 1
  • zone: String(20), default "main"
  • position: Integer
  • Constraints: UniqueConstraint(deck_id, card_id, zone)
  • Indexes: idx_deck_cards_deck, idx_deck_cards_card
  • Relationships: deck, card

DeckPrecedent (deck_precedents)

  • id: BigInteger, primary key, autoincrement
  • name: String(255), indexed
  • description: Text
  • format: String(50), default "standard"
  • is_public: Boolean, default True, indexed
  • created_by: Integer, FK to mtgonline_users.id
  • created_at: DateTime
  • updated_at: DateTime
  • Relationships: creator, cards (cascade)

DeckPrecedentCard (deck_precedent_cards)

  • id: BigInteger, primary key, autoincrement
  • precedent_id: BigInteger, FK to deck_precedents.id, CASCADE, indexed
  • card_id: Integer, indexed
  • quantity: Integer, default 1
  • zone: String(20), default "main"
  • Constraints: UniqueConstraint(precedent_id, card_id, zone)
  • Relationships: precedent

CardSuggestion (card_suggestions)

  • id: BigInteger, primary key, autoincrement
  • deck_id: BigInteger, FK to user_decks.id, CASCADE, indexed
  • card_id: Integer, indexed
  • source_card_id: Integer
  • suggestion_type: String(50), default "SIMILAR"
  • confidence: Float
  • notes: Text
  • created_at: DateTime
  • Constraints: UniqueConstraint(deck_id, card_id, source_card_id)
  • Relationships: deck

1.6 Card Import Models

CardImportBatch (card_import_batches)

  • id: Integer, primary key, autoincrement
  • user_id: Integer, FK to mtgonline_users.id, CASCADE, indexed
  • filename: String(255)
  • file_type: String(10) (xlsx, csv, json, ods)
  • file_size: Integer (bytes)
  • status: String(20), default "pending", indexed (pending, processing, completed, failed)
  • total_cards: Integer, default 0
  • matched_cards: Integer, default 0
  • unmatched_cards: Integer, default 0
  • match_results: JSON
  • error_message: Text
  • created_at: DateTime
  • updated_at: DateTime
  • Relationships: user

UserCardImportRecord (user_card_imports_confirmed)

  • id: Integer, primary key, autoincrement
  • user_id: Integer, FK to mtgonline_users.id, CASCADE, indexed
  • batch_id: Integer, FK to card_import_batches.id, CASCADE, indexed
  • is_confirmed: Boolean, default True
  • confirmed_at: DateTime
  • Relationships: user, batch

2. Schemas (app/schemas/)

2.1 Core Schemas (schemas.py)

Authentication Schemas:

  • LoginRequest: username (3-64 chars), password (6-128 chars)
  • LoginResponse: access_token, refresh_token, token_type="bearer", user (dict)
  • RefreshTokenRequest: refresh_token
  • TokenResponse: access_token, token_type="bearer"

User Schemas:

  • UserBase: username, email (optional), country (optional, max 2), real_name (optional)
  • UserCreate: extends UserBase, password (min 8 chars)
  • UserUpdate: email, country, real_name, new_password (optional, min 8, max 128)
  • UserResponse: id, username, email, country, real_name, privlevel, vip_status, is_active, is_banned, ban_reason, creation_date, last_login (from_attributes)

Deck Schemas:

  • DeckCreate: name (1-255 chars), content (min 1), folder_id (optional), format="native" (pattern: native|plain), status="DRAUGHT" (pattern: DRAUGHT|FINAL)
  • DeckUpdate: name, content, folder_id, status (all optional)
  • DeckResponse: id, name, content, format, status, folder_id, owner_id, creation_date (from_attributes)
  • FolderCreate: name (1-255 chars), parent_id (optional)
  • FolderResponse: id, name, parent_id, owner_id, creation_date (from_attributes)

Game Schemas:

  • GameCreate: room_id, game_type (optional), description (optional), password (optional)
  • GameResponse: id, room_id, game_type, description, with_password, max_players, player_count, started, creation_date (from_attributes)

Room Schemas:

  • RoomResponse: id, name, description, is_password_protected, game_types (List[str]), player_count, creation_date (from_attributes)

Ban Schemas:

  • BanCreate: user_id, reason (min 1, max 1000), expiration_time (optional)
  • BanResponse: id, user_id, reason, moderators, expiration_time, active, creation_date (from_attributes)

Error Schemas:

  • ErrorResponse: detail (str)
  • ValidationErrorResponse: detail (List[dict])

Pagination Schemas:

  • PaginationParams: page (1+, default 1), page_size (1-100, default 50)
  • PaginatedResponse: items (List[dict]), total, page, page_size, total_pages

Card Mirror Schemas:

  • CardMirrorResponse: id, source_id, name, mana_cost, type_line, oracle_text, power, toughness, rarity, layout, artist, flavor_text, numbers, identifiers, images, image, card_parts, keywords, legalities, set_code, set_name, synced_at, created_at (from_attributes)
  • DeckCardLinkResponse: id, deck_id, card_id, quantity, zone, card (CardMirrorResponse) (from_attributes)
  • DeckWithCardsResponse: id, name, content, format, status, folder_id, owner_id, creation_date, card_links (List[DeckCardLinkResponse]) (from_attributes)

2.2 User Data Schemas (user_data_schemas.py)

Enums:

  • DeckVersionStatus: DRAFT, FINAL, ARCHIVED
  • GameReplayStatus: IN_PROGRESS, COMPLETED, FAILED, CANCELLED
  • GameOutcomeType: WIN, LOSS, CONCESSION, DISCONNECT
  • GroupMemberRole: OWNER, ADMIN, MEMBER
  • NetworkMemberRole: OWNER, ADMIN, MEMBER
  • UserPreferenceTheme: LIGHT, DARK, SYSTEM
  • ActivityType: LOGIN, LOGOUT, DECK_EDIT, GAME_PLAYED, CARD_ACQUIRED, CARD_TRADED, GROUP_CREATED, GROUP_JOINED

Session Schemas:

  • SessionResponse: id, user_id, ip_address, user_agent, created_at, expires_at, is_active (from_attributes)
  • SessionCleanupResponse: cleaned_count, message

Deck Version Schemas:

  • DeckVersionCreate: content (min 1), status=DeckVersionStatus.DRAFT, comment (optional)
  • DeckVersionUpdate: content, status, comment (all optional)
  • DeckVersionResponse: id, deck_id, version_number, content, status, comment, created_at (from_attributes)
  • DeckVersionListResponse: versions (List[DeckVersionResponse]), total

Game Replay Schemas:

  • GameReplayCreate: game_uuid (36 chars), room_id, game_type, format, duration_seconds, start_time, end_time, status=IN_PROGRESS, replay_data (Dict)
  • GameReplayUpdate: room_id, game_type, format, duration_seconds, end_time, status, replay_data (all optional)
  • GameReplayResponse: id, game_uuid, room_id, game_type, format, duration_seconds, start_time, end_time, status, replay_data, created_at, updated_at, players (List[Dict]) (from_attributes)
  • GameReplayListResponse: replays (List[GameReplayResponse]), total, page, page_size, total_pages

Game Outcome Schemas:

  • GameOutcomeCreate: game_uuid, outcome=GameOutcomeType, opponent_id, format, rating_before, rating_after, rating_change
  • GameOutcomeResponse: id, user_id, game_uuid, outcome, opponent_id, format, rating_before, rating_after, rating_change, created_at (from_attributes)
  • GameOutcomeListResponse: outcomes (List[GameOutcomeResponse]), total

User Statistics Schemas:

  • UserStatisticsResponse: user_id, total_games, total_wins, total_losses, total_concessions, win_rate, current_streak, best_streak, average_rating, last_game_date, updated_at (from_attributes)
  • StatisticsUpdateResponse: user_id, total_games, total_wins, total_losses, win_rate, current_streak, updated_at

Card Collection Schemas:

  • CardCollectionCreate: card_id, quantity=1 (ge=1), condition="NEAR_MINT" (max 20), language="EN" (max 5), is_foil=False, is_alt_art=False, acquired_date, acquisition_method, notes
  • CardCollectionUpdate: quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes (all optional)
  • CardCollectionResponse: id, user_id, card_id, quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes, created_at, updated_at (from_attributes)
  • CardCollectionListResponse: cards (List[CardCollectionResponse]), total, page, page_size, total_pages

Wishlist Schemas:

  • WishlistCreate: card_id, max_price, notes
  • WishlistUpdate: max_price, notes (optional)
  • WishlistResponse: id, user_id, card_id, max_price, notes, created_at (from_attributes)
  • WishlistListResponse: items (List[WishlistResponse]), total

Group Schemas:

  • GroupCreate: name (1-100 chars), description, is_public=True, max_members=50 (ge=2, le=500)
  • GroupUpdate: name, description, is_public, max_members (optional)
  • GroupMemberCreate: user_id, role=GroupMemberRole.MEMBER
  • GroupMemberUpdate: role=GroupMemberRole
  • GroupMemberRemove: user_id
  • GroupResponse: id, name, description, owner_id, is_public, max_members, created_at, updated_at, member_count=0, is_member=False (from_attributes)
  • GroupListResponse: groups (List[GroupResponse]), total
  • GroupChatMessageCreate: message (1-2000 chars)
  • GroupChatMessageResponse: id, group_id, sender_id, sender_username, message, created_at (from_attributes)
  • GroupChatMessageListResponse: messages (List[GroupChatMessageResponse]), total, page, page_size, total_pages

Network Schemas:

  • NetworkCreate: name (1-100 chars), description, is_public=True
  • NetworkUpdate: name, description, is_public (optional)
  • NetworkMemberCreate: user_id, role=NetworkMemberRole.MEMBER
  • NetworkResponse: id, name, description, creator_id, is_public, created_at, member_count=0, is_member=False (from_attributes)
  • NetworkListResponse: networks (List[NetworkResponse]), total

Preference Schemas:

  • UserPreferenceUpdate: theme, notifications_enabled, email_notifications, auto_save_decks, default_format, language (all optional)
  • UserPreferenceResponse: user_id, theme, notifications_enabled, email_notifications, auto_save_decks, default_format, language, updated_at (from_attributes)

Activity Log Schemas:

  • ActivityLogEntry: id, user_id, activity_type, activity_data, ip_address, created_at (from_attributes)
  • ActivityLogListResponse: entries (List[ActivityLogEntry]), total, page, page_size, total_pages

Generic Schemas:

  • MessageResponse: message (str)
  • CountResponse: count (int)
  • ErrorDetail: error (str), detail (str)

2.3 User Deck Schemas (user_deck_schemas.py)

Enums:

  • DeckStatus: DRAFT, FINAL
  • DeckZone: MAIN, SIDEBOARD
  • SuggestionType: SIMILAR, PAIRING, ALTERNATIVE

Deck Schemas:

  • UserDeckCreate: name (1-255 chars), folder_id, format="standard" (max 50), notes, is_precedent=False, precedent_name
  • UserDeckUpdate: name, folder_id, format, notes, status, is_precedent, precedent_name (all optional)
  • UserDeckResponse: id, user_id, name, status, folder_id, format, notes, is_precedent, precedent_name, created_at, updated_at, card_count=0, is_owner=False (from_attributes)
  • UserDeckListResponse: decks (List[UserDeckResponse]), total, page, page_size, total_pages

Deck Card Schemas:

  • DeckCardCreate: card_id, quantity=1 (ge=1), zone=DeckZone.MAIN, position
  • DeckCardUpdate: quantity, zone, position (optional)
  • DeckCardResponse: id, deck_id, card_id, quantity, zone, position, created_at (from_attributes)
  • DeckCardWithDetailsResponse: extends DeckCardResponse, card_name="", card_type_line="", card_image
  • DeckCardListResponse: cards (List[DeckCardWithDetailsResponse]), total

Deck Precedent Schemas:

  • PrecedentCreate: name (1-255 chars), description, format="standard" (max 50), is_public=True
  • PrecedentUpdate: name, description, format, is_public (optional)
  • PrecedentResponse: id, name, description, format, is_public, created_by, created_at, updated_at, card_count=0 (from_attributes)
  • PrecedentListResponse: precedents (List[PrecedentResponse]), total, page, page_size, total_pages

Card Suggestion Schemas:

  • SuggestionCreate: card_id, source_card_id, suggestion_type=SuggestionType.SIMILAR, confidence (0.0-1.0), notes
  • SuggestionResponse: id, deck_id, card_id, source_card_id, suggestion_type, confidence, notes, created_at, card_name="" (from_attributes)
  • SuggestionListResponse: suggestions (List[SuggestionResponse]), total

Deck Action Schemas:

  • DeckFinalizeRequest: status=DeckStatus.FINAL
  • DeckFinalizeResponse: deck_id, status, message
  • DeckDeleteResponse: deck_id, message

Search Schemas:

  • CardSearchRequest: query (1-100 chars), limit=50 (1-200), offset=0 (ge=0)
  • CardSearchResponse: cards (List[Dict]), total, page, page_size, total_pages

Generic Schemas:

  • MessageResponse: message (str)
  • CountResponse: count (int)

2.4 Card Import Schemas (card_import_schemas.py)

  • CardImportRequest: card_names (List[str], 1-10000 items)
  • CardImportResponse: message, card_count, card_names, imported_at
  • CardImportStatusResponse: has_import, card_count, card_names, last_imported
  • CardMatchResult: card_id, card_name, matched_name, match_type (exact|fuzzy|partial), confidence (0.0-1.0)
  • CardImportSummary: total_cards, matched_cards (List[CardMatchResult]), unmatched_cards (List[str]), import_id
  • MessageResponse: message (str)
  • CountResponse: count (int)
  • ErrorResponse: detail (str), error_code

2.5 Card Search Schemas (card_search_schemas.py)

  • CardResponse: id, name, mana_cost, type_line, oracle_text, power, toughness, rarity, layout, colors, set_code, set_name, identifiers (Dict), images (Dict) (from_attributes)
  • SetResponse: id, name, code, release_date, card_count (from_attributes)
  • CardTypeResponse: type (str)
  • CardSearchResponse: cards (List[Dict]), total, page, page_size, total_pages
  • CardImportResponse: message, batch_id, card_count, status, imported_at
  • CardImportStatusResponse: has_import, batch_id, status, card_count, matched_count, unmatched_count, last_imported, error_message
  • CardMatchResult: card_id, card_name, matched_name, match_type (exact|high_confidence|low_confidence), confidence (0.0-1.0)
  • CardImportSummary: total_cards, matched_cards (List[CardMatchResult]), unmatched_cards (List[str]), import_id
  • MessageResponse: message (str)
  • CountResponse: count (int)
  • ErrorResponse: detail (str), error_code

2.6 Protocol Schemas (proto_messages.py)

Base:

  • ProtoMessageBase: message_type, timestamp

Commands:

  • SessionCommand: message_type="SessionCommand", cmd_type (int), cmd_id=0, data (Dict)
  • GameCommand: message_type="GameCommand", cmd_type (int), cmd_id=0, player_id, data (Dict)
  • GameEvent: message_type="GameEvent", event_type (int), player_id, data (Dict)
  • Response: message_type="Response", cmd_id, response_code (int), data (Dict)

Server Info:

  • ServerInfoUser: id, name, user_level=0, address, real_name, country, avatar_bmp (bytes), server_id, session_id, accountage_secs, email, privlevel
  • ServerInfoDeckStorageFile: creation_time
  • ServerInfoDeckStorageFolder: items (List[Dict])
  • ServerInfoDeckStorageTreeItem: id, name, file, folder
  • ServerInfoCard: id, name, x, y, face_down=False, tapped=False, attacking=False, color, pt, annotation, destroy_on_zone_change=False, doesnt_untap=False, counter_list (List[Dict]), attach_player_id, attach_zone, attach_card_id, provider_id
  • ServerInfoZone: name, zone_type=0, with_coords=False, card_count=0, card_list (List[ServerInfoCard]), always_reveal_top_card=False, always_look_at_top_card=False
  • ServerInfoGame: server_id, room_id, game_id, description, with_password=False, max_players=4, game_types (List[int]), creator_info, only_buddies=False, only_registered=False, spectators_allowed=False, spectators_need_password=False, spectators_can_chat=False, spectators_omniscient=False, share_decklists_on_load=False, player_count=0, spectators_count=0, started=False, start_time, closed=False

2.7 Protocol Constants (protocol_constants.py)

Enums:

  • SessionCommandType (IntEnum): PING=1000, LOGIN=1001, MESSAGE=1002, LIST_USERS=1003, GET_GAMES_OF_USER=1004, GET_USER_INFO=1005, ADD_TO_LIST=1006, REMOVE_FROM_LIST=1007, DECK_LIST=1008, DECK_NEW_DIR=1009, DECK_DEL_DIR=1010, DECK_DEL=1011, DECK_DOWNLOAD=1012, DECK_UPLOAD=1013, LIST_ROOMS=1014, JOIN_ROOM=1015, REGISTER=1016, ACTIVATE=1017, ACCOUNT_EDIT=1018, ACCOUNT_IMAGE=1019, ACCOUNT_PASSWORD=1020, FORGOT_PASSWORD_REQUEST=1021, FORGOT_PASSWORD_RESET=1022, FORGOT_PASSWORD_CHALLENGE=1023, REQUEST_PASSWORD_SALT=1024, SET_CARD_ART_PARAMS=1025, REPLAY_LIST=1100, REPLAY_DOWNLOAD=1101, REPLAY_MODIFY_MATCH=1102, REPLAY_DELETE_MATCH=1103, REPLAY_GET_CODE=1104, REPLAY_SUBMIT_CODE=1105

  • GameCommandType (IntEnum): KICK_FROM_GAME=1000, LEAVE_GAME=1001, GAME_SAY=1002, SHUFFLE=1003, MULLIGAN=1004, ROLL_DIE=1005, DRAW_CARDS=1006, UNDO_DRAW=1007, FLIP_CARD=1008, ATTACH_CARD=1009, CREATE_TOKEN=1010, CREATE_ARROW=1011, DELETE_ARROW=1012, SET_CARD_ATTR=1013, SET_CARD_COUNTER=1014, INC_CARD_COUNTER=1015, READY_START=1016, CONCEDE=1017, INC_COUNTER=1018, CREATE_COUNTER=1019, SET_COUNTER=1020, DEL_COUNTER=1021, NEXT_TURN=1022, SET_ACTIVE_PHASE=1023, DUMP_ZONE=1024, REVEAL_CARDS=1026, MOVE_CARD=1027, SET_SIDEBOARD_PLAN=1028, DECK_SELECT=1029, SET_SIDEBOARD_LOCK=1030, CHANGE_ZONE_PROPERTIES=1031, UNCONCEDE=1032, JUDGE=1033, REVERSE_TURN=1034

  • GameEventType (IntEnum): JOIN=1000, LEAVE=1001, GAME_CLOSED=1002, GAME_HOST_CHANGED=1003, KICKED=1004, GAME_STATE_CHANGED=1005, PLAYER_PROPERTIES_CHANGED=1007, GAME_SAY=1009, CREATE_ARROW=2000, DELETE_ARROW=2001, CREATE_COUNTER=2002, SET_COUNTER=2003, DEL_COUNTER=2004, DRAW_CARDS=2005, REVEAL_CARDS=2006, SHUFFLE=2007, ROLL_DIE=2008, MOVE_CARD=2009, FLIP_CARD=2010, DESTROY_CARD=2011, ATTACH_CARD=2012, CREATE_TOKEN=2013, SET_CARD_ATTR=2014, SET_CARD_COUNTER=2015, SET_ACTIVE_PLAYER=2016, SET_ACTIVE_PHASE=2017, DUMP_ZONE=2018, CHANGE_ZONE_PROPERTIES=2020, REVERSE_TURN=2021, GAME_LOG_NOTICE=2022

  • ResponseCode (IntEnum): RespNotConnected=-1, RespNothing=0, RespOk=1, RespNotInRoom=2, RespInternalError=3, RespInvalidCommand=4, RespInvalidData=5, RespNameNotFound=6, RespLoginNeeded=7, RespFunctionNotAllowed=8, RespGameNotStarted=9, RespGameFull=10, RespContextError=11, RespWrongPassword=12, RespSpectatorsNotAllowed=13, RespOnlyBuddies=14, RespUserLevelTooLow=15, RespInIgnoreList=16, RespWouldOverwriteOldSession=17, RespChatFlood=18, RespUserIsBanned=19, RespAccessDenied=20, RespUsernameInvalid=21, RespRegistrationRequired=22, RespRegistrationAccepted=23, RespUserAlreadyExists=24, RespEmailRequiredToRegister=25, RespTooManyRequests=26, RespPasswordTooShort=27, RespAccountNotActivated=28, RespRegistrationDisabled=29, RespRegistrationFailed=30, RespActivationAccepted=31, RespActivationFailed=32, RespRegistrationAcceptedNeedsActivation=33, RespClientIdRequired=34, RespClientUpdateRequired=35, RespServerFull=36, RespEmailBlackListed=37

  • ZoneType (IntEnum): PrivateZone=0, PublicZone=1, HiddenZone=2

  • UserLevelFlag (IntFlag): IsNothing=0, IsUser=1, IsRegistered=2, IsModerator=4, IsAdmin=8, IsJudge=16


2.8 User Card Collection Schemas (user_card_collection.py)

Enums:

  • CardCondition: NEAR_MINT, LIGHTLY_PLAYED, MODERATELY_PLAYED, HEAVILY_PLAYED, DAMAGED
  • AcquisitionMethod: PACK_OPENING, TRADE, PURCHASE, GIFT, CONTEST, OTHER

Card Collection Schemas:

  • CardCollectionCreate: card_id, quantity=1 (ge=1), condition=CardCondition.NEAR_MINT, language="EN" (max 5), is_foil=False, is_alt_art=False, acquired_date, acquisition_method, notes
  • CardCollectionUpdate: quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes (optional)
  • CardCollectionResponse: id, user_id, card_id, quantity, condition, language, is_foil, is_alt_art, acquired_date, acquisition_method, notes, created_at, updated_at (from_attributes)
  • CardCollectionListResponse: cards (List[CardCollectionResponse]), total, page, page_size, total_pages

Wishlist Schemas:

  • WishlistCreate: card_id, max_price, notes
  • WishlistUpdate: max_price, notes (optional)
  • WishlistResponse: id, user_id, card_id, max_price, notes, created_at (from_attributes)
  • WishlistListResponse: items (List[WishlistResponse]), total

Collection Statistics Schemas:

  • CollectionStatistics: total_cards, unique_cards, total_quantity, foil_count, alt_art_count, condition_breakdown (Dict), language_breakdown (Dict), acquisition_breakdown (Dict)
  • CollectionSummaryResponse: statistics (CollectionStatistics), recent_acquisitions (List[CardCollectionResponse]), top_cards (List[CardCollectionResponse])

Generic Schemas:

  • MessageResponse: message (str)
  • CountResponse: count (int)
  • ErrorDetail: error (str), detail (str)

3. Routers (app/routers/)

3.1 Auth Router (auth.py)

Endpoints:

  • POST /login - Authenticate user, return JWT tokens

    • Request: LoginRequest
    • Response: LoginResponse
    • Dependencies: get_db (AsyncSession)
    • Logic: Verify credentials, check account status, generate access/refresh tokens
  • POST /refresh - Refresh access token

    • Request: RefreshTokenRequest
    • Response: TokenResponse
    • Dependencies: get_db (AsyncSession)
    • Logic: Validate refresh token, verify user still active, generate new access token
  • POST /register - Register new user

    • Request: UserCreate
    • Response: UserResponse
    • Dependencies: get_db (AsyncSession)
    • Logic: Check username/email uniqueness, hash password, create user
  • GET /me - Get current authenticated user

    • Query: token (str)
    • Response: UserResponse
    • Dependencies: get_db (AsyncSession)
    • Logic: Decode access token, fetch user

Imports:

  • From app.core.security: verify_password, hash_password, create_access_token, create_refresh_token, decode_token
  • From app.models.models: User
  • From app.schemas.schemas: LoginRequest, LoginResponse, RefreshTokenRequest, TokenResponse, UserCreate, UserResponse

3.2 Users Router (users.py)

Endpoints:

  • GET /{user_id} - Get user by ID

    • Response: UserResponse
    • Dependencies: get_db, get_current_user
    • Logic: Fetch user, return response
  • PATCH /{user_id} - Update user profile

    • Request: UserUpdate
    • Response: UserResponse
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, update fields, hash new password if provided
  • POST /{user_id}/ban - Ban user (admin only)

    • Query: reason (str), expiration_time (optional str)
    • Response: dict with message
    • Dependencies: get_db, get_current_user
    • Logic: Check admin privileges, update ban status
  • POST /{user_id}/unban - Unban user (admin only)

    • Response: dict with message
    • Dependencies: get_db, get_current_user
    • Logic: Check admin privileges, clear ban status

Imports:

  • From app.core.security: get_current_user, hash_password
  • From app.models.models: User
  • From app.schemas.schemas: UserUpdate, UserResponse

3.3 Decks Router (decks.py)

Deck CRUD:

  • GET / - List user's decks with filtering

    • Query: status_filter, folder_id, is_precedent, page, page_size
    • Response: UserDeckListResponse
    • Dependencies: get_db, get_current_user
    • Logic: Filter decks, count cards, paginate
  • POST / - Create new user deck (DRAFT)

    • Request: UserDeckCreate
    • Response: UserDeckResponse (201)
    • Dependencies: get_db, get_current_user
    • Logic: Verify folder, create deck
  • GET /{deck_id} - Get specific deck

    • Response: UserDeckResponse
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, count cards
  • PATCH /{deck_id} - Update deck

    • Request: UserDeckUpdate
    • Response: UserDeckResponse
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, check not FINAL, update fields
  • DELETE /{deck_id} - Delete deck

    • Response: MessageResponse
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, delete

Deck Finalize:

  • POST /{deck_id}/finalize - Transition DRAFT to FINAL
    • Response: DeckFinalizeResponse
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, check not already FINAL, verify has cards, update status

Deck Card Management:

  • POST /{deck_id}/cards - Add card to deck

    • Request: DeckCardCreate
    • Response: DeckCardResponse (201)
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, check not FINAL, verify card exists, handle duplicates (update quantity or create new)
  • GET /{deck_id}/cards - Get all cards in deck

    • Query: zone (optional)
    • Response: DeckCardListResponse
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, fetch cards with details from mirror
  • PATCH /{deck_id}/cards/{card_id} - Update card in deck

    • Request: DeckCardUpdate
    • Response: DeckCardResponse
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, check not FINAL, update quantity/zone/position
  • DELETE /{deck_id}/cards/{card_id} - Remove card from deck

    • Response: MessageResponse
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, check not FINAL, delete card entry

Deck Precedents:

  • GET /precedents - List available precedents

    • Query: page, page_size, format_filter
    • Response: PrecedentListResponse
    • Dependencies: get_db, get_current_user
    • Logic: Filter public precedents, count cards, paginate
  • POST /precedents - Create precedent (template)

    • Request: PrecedentCreate
    • Response: PrecedentResponse (201)
    • Dependencies: get_db, get_current_user
    • Logic: Create precedent with creator
  • GET /precedents/{precedent_id} - Get specific precedent

    • Response: PrecedentResponse
    • Dependencies: get_db, get_current_user
    • Logic: Fetch precedent, count cards
  • POST /precedents/{precedent_id}/use - Clone precedent to new deck

    • Response: dict with deck_id, deck_name, card_count
    • Dependencies: get_db, get_current_user
    • Logic: Fetch precedent, create new deck, copy cards

Card Search:

  • POST /search/cards - Search MTG cards
    • Request: CardSearchRequest
    • Response: CardSearchResponse
    • Dependencies: get_db, get_current_user
    • Logic: Search local mirror by name/type_line/mana_cost with ILIKE

Card Suggestions:

  • GET /{deck_id}/suggestions - Get card suggestions

    • Query: suggestion_type (optional)
    • Response: SuggestionListResponse
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, fetch suggestions with card names
  • POST /{deck_id}/suggestions - Add suggestion

    • Request: SuggestionCreate
    • Response: SuggestionResponse (201)
    • Dependencies: get_db, get_current_user
    • Logic: Verify ownership, check not FINAL, create suggestion

Imports:

  • From app.core.database: get_db, mtg_get_db
  • From app.core.security: get_current_user
  • From app.models.models: User, DecklistFolder, MtgonlineCard
  • From app.models.mtg_models: MtgCard, MtgSet
  • From app.models.user_deck: UserDeck, UserDeckCard, DeckPrecedent, DeckPrecedentCard, CardSuggestion
  • From app.schemas.user_deck_schemas: All deck-related schemas

3.4 Other Routers

Rooms Router (rooms.py)

  • File exists but content not fully read

Games Router (games/)

  • Directory exists (sub-routers)

Admin Router (admin.py)

  • File exists but content not fully read

Card Router (card_router.py)

  • File exists but content not fully read

Interactions Router (interactions.py)

  • File exists but content not fully read

Refresh Router (refresh.py)

  • File exists but content not fully read

Card Import Router (card_import.py)

  • File exists but content not fully read

Users Router (users.py)

  • Already documented above

WS Router (ws.py)

  • File exists but content not fully read

4. Services (app/services/)

4.1 Card Database Service (card_database.py)

  • File exists but content not fully read

4.2 Card Mirror Service (card_mirror_service.py)

  • File exists but content not fully read

4.3 Card Search Service (card_search_service.py)

  • File exists but content not fully read

4.4 Deck Manager Service (deck_manager.py)

  • File exists but content not fully read

4.5 Deck Parser Service (deck_parser.py)

  • File exists but content not fully read

4.6 Deck Suggestion Service (deck_suggestion_service.py)

  • File exists but content not fully read

4.7 File Parser Service (file_parser.py)

  • File exists but content not fully read

4.8 Fuzzy Card Matcher Service (fuzzy_card_matcher.py)

  • File exists but content not fully read

4.9 Game Server Service (game_server.py)

  • File exists but content not fully read

4.10 Import Batch Processor Service (import_batch_processor.py)

  • File exists but content not fully read

4.11 MTGJSON Downloader Service (mtgjson_downloader.py)

  • File exists but content not fully read

4.12 MTGJSON Loader Service (mtgjson_loader.py)

  • File exists but content not fully read

4.13 MTGJSON Manager Service (mtgjson_manager.py)

  • File exists but content not fully read

4.14 MTGJSON Uploader Service (mtgjson_uploader.py)

  • File exists but content not fully read

5. Core Configuration (app/core/)

5.1 Database (database.py)

  • File exists but content not fully read

5.2 Redis Client (redis_client.py)

  • File exists but content not fully read

5.3 Security (security.py)

  • File exists but content not fully read

5.4 Settings (settings.py)

  • File exists but content not fully read

6. Main Application (app/main.py)

  • File exists but content not fully read

7. Alembic Migrations (alembic/)

  • Migration directory exists
  • Migration files present but not fully documented

8. Test Structure (tests/)

  • Test directory exists
  • Test files present but not fully documented

Summary

The MTG Online backend is a comprehensive FastAPI application with:

Data Layer:

  • 20+ SQLAlchemy models across 6 model files
  • Core user/deck/room management
  • MTG card database integration (mtgjson.com)
  • Card mirroring for performance
  • User deck building with precedents
  • Game replay and statistics
  • Social features (groups, networks)
  • Card collection and wishlist

API Layer:

  • 3+ fully documented routers (auth, users, decks)
  • 7+ additional routers (rooms, games, admin, card_router, interactions, refresh, card_import, ws)
  • Comprehensive Pydantic schemas (8 schema files)
  • Protocol buffer message definitions
  • Protocol constants for MTG Online client

Service Layer:

  • 14 service files for business logic
  • Card database management
  • Deck parsing and management
  • Card search and fuzzy matching
  • MTGJSON data loading and synchronization
  • File import processing

Infrastructure:

  • Async SQLAlchemy with PostgreSQL
  • Redis for caching/sessions
  • JWT authentication
  • Alembic migrations
  • Docker deployment

Note: This documentation covers the structure that was explicitly read. Several files (services, core configs, additional routers) exist but their detailed contents were not fully read in this session.