Files
mtgonline/backend/app/schemas/user_data_schemas.py
T
akadmin c42d7ca0e1 feat: implement user data schema and API endpoints
- Add Alembic migration setup with async configuration
- Create 16 user data models (users, decks, cards, replays, etc.)
- Implement comprehensive API endpoints with JWT auth
- Add replay, card collection, group, network, preferences, and activity log routers
- Include API documentation and migration test plan
- Update Dockerfile to run migrations on startup
2026-07-23 03:23:54 +00:00

527 lines
12 KiB
Python

"""
Pydantic schemas for user data features.
Covers sessions, decks, replays, cards, groups, networks, preferences, and activity logs.
"""
from pydantic import BaseModel, Field
from typing import Optional, List, Dict, Any
from datetime import datetime
from enum import Enum
# ===== Enum Types =====
class DeckVersionStatus(str, Enum):
DRAFT = "DRAFT"
FINAL = "FINAL"
ARCHIVED = "ARCHIVED"
class GameReplayStatus(str, Enum):
IN_PROGRESS = "IN_PROGRESS"
COMPLETED = "COMPLETED"
FAILED = "FAILED"
CANCELLED = "CANCELLED"
class GameOutcomeType(str, Enum):
WIN = "WIN"
LOSS = "LOSS"
CONCESSION = "CONCESSION"
DISCONNECT = "DISCONNECT"
class GroupMemberRole(str, Enum):
OWNER = "OWNER"
ADMIN = "ADMIN"
MEMBER = "MEMBER"
class NetworkMemberRole(str, Enum):
OWNER = "OWNER"
ADMIN = "ADMIN"
MEMBER = "MEMBER"
class UserPreferenceTheme(str, Enum):
LIGHT = "light"
DARK = "dark"
SYSTEM = "system"
class ActivityType(str, Enum):
LOGIN = "LOGIN"
LOGOUT = "LOGOUT"
DECK_EDIT = "DECK_EDIT"
GAME_PLAYED = "GAME_PLAYED"
CARD_ACQUIRED = "CARD_ACQUIRED"
CARD_TRADED = "CARD_TRADED"
GROUP_CREATED = "GROUP_CREATED"
GROUP_JOINED = "GROUP_JOINED"
# ===== Session Schemas =====
class SessionResponse(BaseModel):
"""User session response."""
id: int
user_id: int
ip_address: Optional[str] = None
user_agent: Optional[str] = None
created_at: datetime
expires_at: datetime
is_active: bool
class Config:
from_attributes = True
class SessionCleanupResponse(BaseModel):
"""Response after cleaning expired sessions."""
cleaned_count: int
message: str
# ===== Deck Version Schemas =====
class DeckVersionCreate(BaseModel):
"""Deck version creation request."""
content: str = Field(..., min_length=1)
status: DeckVersionStatus = DeckVersionStatus.DRAFT
comment: Optional[str] = None
class DeckVersionUpdate(BaseModel):
"""Deck version update request."""
content: Optional[str] = None
status: Optional[DeckVersionStatus] = None
comment: Optional[str] = None
class DeckVersionResponse(BaseModel):
"""Deck version response."""
id: int
deck_id: int
version_number: int
content: str
status: str
comment: Optional[str]
created_at: datetime
class Config:
from_attributes = True
class DeckVersionListResponse(BaseModel):
"""List of deck versions."""
versions: List[DeckVersionResponse]
total: int
# ===== Game Replay Schemas =====
class GameReplayCreate(BaseModel):
"""Game replay creation request."""
game_uuid: str = Field(..., min_length=36, max_length=36)
room_id: Optional[int] = None
game_type: Optional[str] = None
format: Optional[str] = None
duration_seconds: Optional[int] = None
start_time: datetime
end_time: Optional[datetime] = None
status: GameReplayStatus = GameReplayStatus.IN_PROGRESS
replay_data: Optional[Dict[str, Any]] = None
class GameReplayUpdate(BaseModel):
"""Game replay update request."""
room_id: Optional[int] = None
game_type: Optional[str] = None
format: Optional[str] = None
duration_seconds: Optional[int] = None
end_time: Optional[datetime] = None
status: Optional[GameReplayStatus] = None
replay_data: Optional[Dict[str, Any]] = None
class GameReplayResponse(BaseModel):
"""Game replay response."""
id: int
game_uuid: str
room_id: Optional[int]
game_type: Optional[str]
format: Optional[str]
duration_seconds: Optional[int]
start_time: datetime
end_time: Optional[datetime]
status: str
replay_data: Optional[Dict[str, Any]]
created_at: datetime
updated_at: datetime
players: List[Dict[str, Any]] = []
class Config:
from_attributes = True
class GameReplayListResponse(BaseModel):
"""List of game replays."""
replays: List[GameReplayResponse]
total: int
page: int
page_size: int
total_pages: int
# ===== Game Outcome Schemas =====
class GameOutcomeCreate(BaseModel):
"""Game outcome creation request."""
game_uuid: str
outcome: GameOutcomeType
opponent_id: Optional[int] = None
format: Optional[str] = None
rating_before: Optional[int] = None
rating_after: Optional[int] = None
rating_change: Optional[int] = None
class GameOutcomeResponse(BaseModel):
"""Game outcome response."""
id: int
user_id: int
game_uuid: str
outcome: str
opponent_id: Optional[int]
format: Optional[str]
rating_before: Optional[int]
rating_after: Optional[int]
rating_change: Optional[int]
created_at: datetime
class Config:
from_attributes = True
class GameOutcomeListResponse(BaseModel):
"""List of game outcomes."""
outcomes: List[GameOutcomeResponse]
total: int
# ===== User Statistics Schemas =====
class UserStatisticsResponse(BaseModel):
"""User statistics summary response."""
user_id: int
total_games: int
total_wins: int
total_losses: int
total_concessions: int
win_rate: float
current_streak: int
best_streak: int
average_rating: float
last_game_date: Optional[datetime]
updated_at: datetime
class Config:
from_attributes = True
class StatisticsUpdateResponse(BaseModel):
"""Response after updating statistics."""
user_id: int
total_games: int
total_wins: int
total_losses: int
win_rate: float
current_streak: int
updated_at: datetime
# ===== Card Collection Schemas =====
class CardCollectionCreate(BaseModel):
"""Card collection item creation request."""
card_id: int
quantity: int = Field(1, ge=1)
condition: str = Field("NEAR_MINT", max_length=20)
language: str = Field("EN", max_length=5)
is_foil: bool = False
is_alt_art: bool = False
acquired_date: Optional[datetime] = None
acquisition_method: Optional[str] = None
notes: Optional[str] = None
class CardCollectionUpdate(BaseModel):
"""Card collection item update request."""
quantity: Optional[int] = None
condition: Optional[str] = None
language: Optional[str] = None
is_foil: Optional[bool] = None
is_alt_art: Optional[bool] = None
acquired_date: Optional[datetime] = None
acquisition_method: Optional[str] = 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
# ===== Group Schemas =====
class GroupCreate(BaseModel):
"""User group creation request."""
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = None
is_public: bool = True
max_members: int = Field(50, ge=2, le=500)
class GroupUpdate(BaseModel):
"""User group update request."""
name: Optional[str] = None
description: Optional[str] = None
is_public: Optional[bool] = None
max_members: Optional[int] = None
class GroupMemberCreate(BaseModel):
"""Group member addition request."""
user_id: int
role: GroupMemberRole = GroupMemberRole.MEMBER
class GroupMemberUpdate(BaseModel):
"""Group member role update request."""
role: GroupMemberRole
class GroupMemberRemove(BaseModel):
"""Group member removal request."""
user_id: int
class GroupResponse(BaseModel):
"""User group response."""
id: int
name: str
description: Optional[str]
owner_id: int
is_public: bool
max_members: int
created_at: datetime
updated_at: datetime
member_count: int = 0
is_member: bool = False
class Config:
from_attributes = True
class GroupListResponse(BaseModel):
"""List of user groups."""
groups: List[GroupResponse]
total: int
class GroupChatMessageCreate(BaseModel):
"""Group chat message creation request."""
message: str = Field(..., min_length=1, max_length=2000)
class GroupChatMessageResponse(BaseModel):
"""Group chat message response."""
id: int
group_id: int
sender_id: int
sender_username: Optional[str] = None
message: str
created_at: datetime
class Config:
from_attributes = True
class GroupChatMessageListResponse(BaseModel):
"""List of group chat messages."""
messages: List[GroupChatMessageResponse]
total: int
page: int
page_size: int
total_pages: int
# ===== Network Schemas =====
class NetworkCreate(BaseModel):
"""User network creation request."""
name: str = Field(..., min_length=1, max_length=100)
description: Optional[str] = None
is_public: bool = True
class NetworkUpdate(BaseModel):
"""User network update request."""
name: Optional[str] = None
description: Optional[str] = None
is_public: Optional[bool] = None
class NetworkMemberCreate(BaseModel):
"""Network member addition request."""
user_id: int
role: NetworkMemberRole = NetworkMemberRole.MEMBER
class NetworkResponse(BaseModel):
"""User network response."""
id: int
name: str
description: Optional[str]
creator_id: int
is_public: bool
created_at: datetime
member_count: int = 0
is_member: bool = False
class Config:
from_attributes = True
class NetworkListResponse(BaseModel):
"""List of user networks."""
networks: List[NetworkResponse]
total: int
# ===== Preference Schemas =====
class UserPreferenceUpdate(BaseModel):
"""User preference update request."""
theme: Optional[UserPreferenceTheme] = None
notifications_enabled: Optional[bool] = None
email_notifications: Optional[bool] = None
auto_save_decks: Optional[bool] = None
default_format: Optional[str] = None
language: Optional[str] = None
class UserPreferenceResponse(BaseModel):
"""User preference response."""
user_id: int
theme: str
notifications_enabled: bool
email_notifications: bool
auto_save_decks: bool
default_format: str
language: str
updated_at: datetime
class Config:
from_attributes = True
# ===== Activity Log Schemas =====
class ActivityLogEntry(BaseModel):
"""Activity log entry."""
id: int
user_id: int
activity_type: str
activity_data: Optional[Dict[str, Any]]
ip_address: Optional[str]
created_at: datetime
class Config:
from_attributes = True
class ActivityLogListResponse(BaseModel):
"""List of activity log entries."""
entries: List[ActivityLogEntry]
total: int
page: int
page_size: int
total_pages: int
# ===== 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