- 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
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, indexedusername: String(64), unique, indexedpassword_hash: String(128), bcrypt hashsalt: String(128)email: String(255), indexedcountry: String(2)real_name: String(128)avatar_bmp: Text (Base64 encoded)privlevel: String(50), default "User"is_active: Boolean, default Trueis_banned: Boolean, default Falseban_reason: Textban_ends: DateTimevip_status: Integer (0=normal, 1=vip, 2=donator)vip_expiry: DateTimecreation_date: DateTime, server default now()last_login: DateTime- Relationships:
decklist_files,decklist_folders(both cascade delete)
MtonlineCard (mtgonline_cards)
id: Integer, primary key, indexedsource_id: Integer, indexed (references mtg_cards.id in mtgdata)name: String(255), indexedmana_cost: String(255)type_line: String(255)oracle_text: Textpower: String(50)toughness: String(50)rarity: String(50)layout: String(50)artist: String(255)flavor_text: Textnumbers: String(100)identifiers: Text (JSON string)images: Text (JSON string)image: Text (card image URL)set_code: String(10), indexedset_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, indexedowner_id: Integer, FK to mtgonline_users.idname: 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, indexedfolder_id: Integer, FK to mtgonline_decklist_folders.idowner_id: Integer, FK to mtgonline_users.idname: 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, indexedname: String(100), uniquedescription: Textis_password_protected: Booleanpassword_hash: String(128)creation_date: DateTime- Relationships:
game_types(cascade delete)
RoomGameType (mtgonline_rooms_gametypes)
id: Integer, primary key, indexedroom_id: Integer, FK to mtgonline_rooms.idname: String(100)description: Text- Relationships:
room
Ban (mtgonline_bans)
id: Integer, primary key, indexeduser_id: Integer, FK to mtgonline_users.idserver_id: Integerreason: Textmoderators: String(255)ip_address: String(45)expiration_time: DateTimeactive: Boolean, default Truecreation_date: DateTime- Relationships:
user
GameLog (mtgonline_log)
id: Integer, primary key, indexedroom_id: Integer, FK to mtgonline_rooms.idplayer_id: Integer, FK to mtgonline_users.idmessage: Texttimestamp: DateTime- Relationships:
room,player
AuditLog (mtgonline_audit)
id: Integer, primary key, indexedadmin_id: Integer, FK to mtgonline_users.idaction_type: String(50)target_user_id: Integer, FK to mtgonline_users.iddetails: Textip_address: String(45)timestamp: DateTime- Relationships:
admin,target_user
Indexes:
idx_decks_owneron DecklistFile.owner_ididx_decks_folderon DecklistFile.folder_ididx_bans_activeon Ban.activeidx_log_timestampon GameLog.timestamp
1.2 MTG Models (mtg_models.py)
MtgSet (mtg_sets)
id: Integer, primary key, indexedcode: String(10), unique, indexedname: String(255)type: String(100)release_date: DateTimebase_set_size: Integertotal_size: Integeris_foil_only: Integeris_non_foil_only: Integerdigital: Integericon_svg_url: Textparent_code: String(10)mtgo_code: String(10)image: Textupdated_at: DateTime- Relationships:
cards
MtgCard (mtg_cards)
id: Integer, primary key, indexedset_id: Integer, FK to mtg_sets.id, indexedname: String(255), indexedmana_cost: String(255), indexedtype_line: String(255), indexedoracle_text: Textpower: String(50)toughness: String(50)rarity: String(50), indexedlayout: String(50)artist: String(255)flavor_text: Textnumbers: String(100)identifiers: Text (JSON)images: Text (JSON)image: Textupdated_at: DateTime- Relationships:
set
Indexes:
idx_mtg_cards_name_seton MtgCard.name, MtgCard.set_ididx_mtg_cards_typeon MtgCard.type_lineidx_mtg_cards_rarityon MtgCard.rarity
1.3 Mirror Models (mirror_models.py)
MtgCardMirror (mtg_cards_mirror)
id: Integer, primary key, indexedsource_id: Integer, indexed (references mtg_cards.id)name: String(255), indexedmana_cost: String(255)type_line: String(255)oracle_text: Textpower: String(50)toughness: String(50)rarity: String(50)layout: String(50)artist: String(255)flavor_text: Textnumbers: String(100)identifiers: Text (JSON)images: Text (JSON)image: Textcard_parts: Textkeywords: Textlegalities: Text (JSON)set_code: String(10), indexedset_name: String(255)synced_at: DateTime, onupdate now()created_at: DateTime- Relationships:
deck_links(cascade delete)
DeckCardLink (deck_card_links)
id: Integer, primary key, indexeddeck_id: Integer, FK to mtgonline_decklist_files.id, CASCADE deletecard_id: Integer, FK to mtg_cards_mirror.idquantity: Integer, default 1zone: 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 keyuser_id: Integer, FK to mtgonline_users.id, CASCADE, indexedsession_token_hash: String(255), unique, indexedip_address: String(45)user_agent: Textcreated_at: DateTimeexpires_at: DateTimeis_active: Boolean, default True- Relationships:
user
DeckVersion (deck_versions)
id: BigInteger, primary keydeck_id: Integer, FK to mtgonline_decklist_files.id, CASCADE, indexedversion_number: Integercontent: Textstatus: String(20), default "DRAFT"comment: Textcreated_at: DateTime- Relationships:
deck
GameReplay (game_replays)
id: BigInteger, primary keygame_uuid: String(36), uniqueroom_id: Integer, FK to mtgonline_rooms.id, indexedgame_type: String(50)format: String(50)duration_seconds: Integerstart_time: DateTimeend_time: DateTimestatus: String(20), default "IN_PROGRESS"replay_data: JSONcreated_at: DateTimeupdated_at: DateTime- Relationships:
players(cascade),outcomes(cascade)
ReplayPlayer (replay_players)
id: BigInteger, primary keyreplay_id: BigInteger, FK to game_replays.id, CASCADE, indexeduser_id: Integer, FK to mtgonline_users.id, indexedposition: Integerdeck_id: Integer, FK to mtgonline_decklist_files.idwon: Booleanlost: Booleanconcession: Boolean, default Falseturn_one: Boolean, default Falsecreated_at: DateTime- Relationships:
replay,user,deck
GameOutcome (game_outcomes)
id: BigInteger, primary keyuser_id: Integer, FK to mtgonline_users.id, indexedgame_uuid: String(36), FK to game_replays.game_uuid, indexedoutcome: String(20), indexedopponent_id: Integer, FK to mtgonline_users.idformat: String(50)rating_before: Integerrating_after: Integerrating_change: Integercreated_at: DateTime- Relationships:
replay,user,opponent
UserStatistics (user_statistics)
user_id: Integer, FK to mtgonline_users.id, primary keytotal_games: Integer, default 0total_wins: Integer, default 0total_losses: Integer, default 0total_concessions: Integer, default 0win_rate: Float, default 0.0current_streak: Integer, default 0best_streak: Integer, default 0average_rating: Float, default 0.0last_game_date: DateTimeupdated_at: DateTime- Relationships:
user
UserCardCollection (user_card_collection)
id: BigInteger, primary keyuser_id: Integer, FK to mtgonline_users.id, CASCADE, indexedcard_id: Integer, indexedquantity: Integer, default 1condition: String(20), default "NEAR_MINT"language: String(5), default "EN"is_foil: Boolean, default Falseis_alt_art: Boolean, default Falseacquired_date: DateTimeacquisition_method: String(50)notes: Textcreated_at: DateTimeupdated_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 keyuser_id: Integer, FK to mtgonline_users.id, CASCADEcard_id: Integermax_price: Floatnotes: Textcreated_at: DateTime- Constraints: UniqueConstraint(user_id, card_id)
- Relationships:
user
UserGroup (user_groups)
id: BigInteger, primary keyname: String(100)description: Textowner_id: Integer, FK to mtgonline_users.id, indexedis_public: Boolean, default Truemax_members: Integer, default 50created_at: DateTimeupdated_at: DateTime- Relationships:
owner,members(cascade),messages(cascade)
GroupMember (group_members)
id: BigInteger, primary keygroup_id: BigInteger, FK to user_groups.id, CASCADE, indexeduser_id: Integer, FK to mtgonline_users.id, indexedrole: String(20), default "MEMBER"joined_at: DateTime- Constraints: UniqueConstraint(group_id, user_id)
- Relationships:
group,user
GroupChatMessage (group_chat_messages)
id: BigInteger, primary keygroup_id: BigInteger, FK to user_groups.id, CASCADE, indexedsender_id: Integer, FK to mtgonline_users.id, indexedmessage: Textcreated_at: DateTime, indexed- Relationships:
group,sender
UserNetwork (user_networks)
id: BigInteger, primary keyname: String(100)description: Textcreator_id: Integer, FK to mtgonline_users.idis_public: Boolean, default Truecreated_at: DateTime- Relationships:
creator,members(cascade)
NetworkMember (network_members)
id: BigInteger, primary keynetwork_id: BigInteger, FK to user_networks.id, CASCADE, indexeduser_id: Integer, FK to mtgonline_users.id, indexedrole: 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 keytheme: String(20), default "light"notifications_enabled: Boolean, default Trueemail_notifications: Boolean, default Trueauto_save_decks: Boolean, default Truedefault_format: String(50), default "standard"language: String(5), default "EN"updated_at: DateTime- Relationships:
user
UserActivityLog (user_activity_log)
id: BigInteger, primary keyuser_id: Integer, FK to mtgonline_users.id, indexedactivity_type: String(50), indexedactivity_data: JSONip_address: String(45)created_at: DateTime, indexed- Relationships:
user
1.5 User Deck Models (user_deck.py)
UserDeck (user_decks)
id: BigInteger, primary key, autoincrementuser_id: Integer, FK to mtgonline_users.id, CASCADE, indexedname: String(255), indexedstatus: String(20), default "DRAFT", indexedfolder_id: Integer, FK to mtgonline_decklist_folders.idformat: String(50), default "standard"notes: Textis_precedent: Boolean, default False, indexedprecedent_name: String(255)created_at: DateTimeupdated_at: DateTime- Relationships:
user,folder,cards(cascade, ordered by id)
UserDeckCard (user_deck_cards)
id: BigInteger, primary key, autoincrementdeck_id: BigInteger, FK to user_decks.id, CASCADE, indexedcard_id: Integer, FK to mtgonline_cards.id, indexedquantity: Integer, default 1zone: 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, autoincrementname: String(255), indexeddescription: Textformat: String(50), default "standard"is_public: Boolean, default True, indexedcreated_by: Integer, FK to mtgonline_users.idcreated_at: DateTimeupdated_at: DateTime- Relationships:
creator,cards(cascade)
DeckPrecedentCard (deck_precedent_cards)
id: BigInteger, primary key, autoincrementprecedent_id: BigInteger, FK to deck_precedents.id, CASCADE, indexedcard_id: Integer, indexedquantity: Integer, default 1zone: String(20), default "main"- Constraints: UniqueConstraint(precedent_id, card_id, zone)
- Relationships:
precedent
CardSuggestion (card_suggestions)
id: BigInteger, primary key, autoincrementdeck_id: BigInteger, FK to user_decks.id, CASCADE, indexedcard_id: Integer, indexedsource_card_id: Integersuggestion_type: String(50), default "SIMILAR"confidence: Floatnotes: Textcreated_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, autoincrementuser_id: Integer, FK to mtgonline_users.id, CASCADE, indexedfilename: 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 0matched_cards: Integer, default 0unmatched_cards: Integer, default 0match_results: JSONerror_message: Textcreated_at: DateTimeupdated_at: DateTime- Relationships:
user
UserCardImportRecord (user_card_imports_confirmed)
id: Integer, primary key, autoincrementuser_id: Integer, FK to mtgonline_users.id, CASCADE, indexedbatch_id: Integer, FK to card_import_batches.id, CASCADE, indexedis_confirmed: Boolean, default Trueconfirmed_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_tokenTokenResponse: 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, ARCHIVEDGameReplayStatus: IN_PROGRESS, COMPLETED, FAILED, CANCELLEDGameOutcomeType: WIN, LOSS, CONCESSION, DISCONNECTGroupMemberRole: OWNER, ADMIN, MEMBERNetworkMemberRole: OWNER, ADMIN, MEMBERUserPreferenceTheme: LIGHT, DARK, SYSTEMActivityType: 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_changeGameOutcomeResponse: 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, notesCardCollectionUpdate: 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, notesWishlistUpdate: 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.MEMBERGroupMemberUpdate: role=GroupMemberRoleGroupMemberRemove: user_idGroupResponse: 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]), totalGroupChatMessageCreate: 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=TrueNetworkUpdate: name, description, is_public (optional)NetworkMemberCreate: user_id, role=NetworkMemberRole.MEMBERNetworkResponse: 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, FINALDeckZone: MAIN, SIDEBOARDSuggestionType: SIMILAR, PAIRING, ALTERNATIVE
Deck Schemas:
UserDeckCreate: name (1-255 chars), folder_id, format="standard" (max 50), notes, is_precedent=False, precedent_nameUserDeckUpdate: 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, positionDeckCardUpdate: 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_imageDeckCardListResponse: cards (List[DeckCardWithDetailsResponse]), total
Deck Precedent Schemas:
PrecedentCreate: name (1-255 chars), description, format="standard" (max 50), is_public=TruePrecedentUpdate: 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), notesSuggestionResponse: 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.FINALDeckFinalizeResponse: deck_id, status, messageDeckDeleteResponse: 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_atCardImportStatusResponse: has_import, card_count, card_names, last_importedCardMatchResult: 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_idMessageResponse: 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_pagesCardImportResponse: message, batch_id, card_count, status, imported_atCardImportStatusResponse: has_import, batch_id, status, card_count, matched_count, unmatched_count, last_imported, error_messageCardMatchResult: 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_idMessageResponse: 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, privlevelServerInfoDeckStorageFile: creation_timeServerInfoDeckStorageFolder: items (List[Dict])ServerInfoDeckStorageTreeItem: id, name, file, folderServerInfoCard: 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_idServerInfoZone: name, zone_type=0, with_coords=False, card_count=0, card_list (List[ServerInfoCard]), always_reveal_top_card=False, always_look_at_top_card=FalseServerInfoGame: 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, DAMAGEDAcquisitionMethod: 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, notesCardCollectionUpdate: 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, notesWishlistUpdate: 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.