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
This commit is contained in:
@@ -0,0 +1,526 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user