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:
+2
-1
@@ -24,7 +24,7 @@ from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from app.core.settings import get_settings
|
||||
from app.core.database import engine, mtg_engine, async_session, mtg_async_session
|
||||
from app.routers import auth, users, decks, rooms, games, admin, card_router, interactions, refresh
|
||||
from app.routers import auth, users, decks, rooms, games, admin, card_router, interactions, refresh, user_data
|
||||
from app.services.mtgjson_manager import MTGJSONManager
|
||||
|
||||
|
||||
@@ -140,6 +140,7 @@ app.include_router(admin.router, prefix="/admin", tags=["Admin"])
|
||||
app.include_router(card_router.router, prefix="/api", tags=["MTG Cards"])
|
||||
app.include_router(interactions.router, tags=["Card Interactions"])
|
||||
app.include_router(refresh.router)
|
||||
app.include_router(user_data.router, prefix="/api/v1/user-data", tags=["User Data"])
|
||||
|
||||
|
||||
@app.get("/health", tags=["Health"])
|
||||
|
||||
@@ -2,6 +2,12 @@
|
||||
from app.models.models import User, DecklistFile, DecklistFolder, Room, RoomGameType, Ban, GameLog, AuditLog
|
||||
from app.models.mtg_models import MtgSet, MtgCard
|
||||
from app.models.mirror_models import MtgCardMirror, DeckCardLink
|
||||
from app.models.user_data import (
|
||||
UserSession, DeckVersion, GameReplay, ReplayPlayer,
|
||||
GameOutcome, UserStatistics, UserCardCollection, CardWishlist,
|
||||
UserGroup, GroupMember, GroupChatMessage, UserNetwork,
|
||||
NetworkMember, UserPreference, UserActivityLog
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"User",
|
||||
@@ -16,4 +22,19 @@ __all__ = [
|
||||
"MtgCard",
|
||||
"MtgCardMirror",
|
||||
"DeckCardLink",
|
||||
"UserSession",
|
||||
"DeckVersion",
|
||||
"GameReplay",
|
||||
"ReplayPlayer",
|
||||
"GameOutcome",
|
||||
"UserStatistics",
|
||||
"UserCardCollection",
|
||||
"CardWishlist",
|
||||
"UserGroup",
|
||||
"GroupMember",
|
||||
"GroupChatMessage",
|
||||
"UserNetwork",
|
||||
"NetworkMember",
|
||||
"UserPreference",
|
||||
"UserActivityLog",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,325 @@
|
||||
"""
|
||||
SQLAlchemy ORM models for user data features.
|
||||
|
||||
Includes sessions, decks, replays, cards, groups, and networks.
|
||||
"""
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, BigInteger, Boolean, DateTime, Text,
|
||||
ForeignKey, Index, UniqueConstraint, Float, JSON
|
||||
)
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.core.database import Base
|
||||
|
||||
|
||||
class UserSession(Base):
|
||||
"""User authentication session."""
|
||||
__tablename__ = "user_sessions"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
session_token_hash = Column(String(255), unique=True, nullable=False, index=True)
|
||||
ip_address = Column(String(45), nullable=True)
|
||||
user_agent = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
expires_at = Column(DateTime, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
|
||||
user = relationship("User", backref="sessions")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<UserSession user={self.user_id} expires={self.expires_at}>"
|
||||
|
||||
|
||||
class DeckVersion(Base):
|
||||
"""Deck version history."""
|
||||
__tablename__ = "deck_versions"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
deck_id = Column(Integer, ForeignKey("mtgonline_decklist_files.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
version_number = Column(Integer, nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
status = Column(String(20), server_default="DRAFT")
|
||||
comment = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
deck = relationship("DecklistFile", backref="versions")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<DeckVersion deck={self.deck_id} v={self.version_number}>"
|
||||
|
||||
|
||||
class GameReplay(Base):
|
||||
"""Game replay recording."""
|
||||
__tablename__ = "game_replays"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
game_uuid = Column(String(36), unique=True, nullable=False)
|
||||
room_id = Column(Integer, ForeignKey("mtgonline_rooms.id"), nullable=True, index=True)
|
||||
game_type = Column(String(50), nullable=True)
|
||||
format = Column(String(50), nullable=True)
|
||||
duration_seconds = Column(Integer, nullable=True)
|
||||
start_time = Column(DateTime, nullable=False)
|
||||
end_time = Column(DateTime, nullable=True)
|
||||
status = Column(String(20), server_default="IN_PROGRESS")
|
||||
replay_data = Column(JSON, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
players = relationship("ReplayPlayer", back_populates="replay", cascade="all, delete-orphan")
|
||||
outcomes = relationship("GameOutcome", back_populates="replay", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<GameReplay {self.game_uuid} status={self.status}>"
|
||||
|
||||
|
||||
class ReplayPlayer(Base):
|
||||
"""Player in a game replay."""
|
||||
__tablename__ = "replay_players"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
replay_id = Column(BigInteger, ForeignKey("game_replays.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
||||
position = Column(Integer, nullable=True)
|
||||
deck_id = Column(Integer, ForeignKey("mtgonline_decklist_files.id"), nullable=True)
|
||||
won = Column(Boolean, nullable=True)
|
||||
lost = Column(Boolean, nullable=True)
|
||||
concession = Column(Boolean, default=False)
|
||||
turn_one = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
replay = relationship("GameReplay", back_populates="players")
|
||||
user = relationship("User")
|
||||
deck = relationship("DecklistFile")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<ReplayPlayer replay={self.replay_id} user={self.user_id}>"
|
||||
|
||||
|
||||
class GameOutcome(Base):
|
||||
"""Game outcome record."""
|
||||
__tablename__ = "game_outcomes"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
||||
game_uuid = Column(String(36), ForeignKey("game_replays.game_uuid"), nullable=False, index=True)
|
||||
outcome = Column(String(20), nullable=False, index=True)
|
||||
opponent_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=True)
|
||||
format = Column(String(50), nullable=True)
|
||||
rating_before = Column(Integer, nullable=True)
|
||||
rating_after = Column(Integer, nullable=True)
|
||||
rating_change = Column(Integer, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
replay = relationship("GameReplay", back_populates="outcomes")
|
||||
user = relationship("User", foreign_keys=[user_id])
|
||||
opponent = relationship("User", foreign_keys=[opponent_id])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<GameOutcome user={self.user_id} {self.outcome}>"
|
||||
|
||||
|
||||
class UserStatistics(Base):
|
||||
"""User game statistics summary."""
|
||||
__tablename__ = "user_statistics"
|
||||
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), primary_key=True)
|
||||
total_games = Column(Integer, default=0)
|
||||
total_wins = Column(Integer, default=0)
|
||||
total_losses = Column(Integer, default=0)
|
||||
total_concessions = Column(Integer, default=0)
|
||||
win_rate = Column(Float, default=0.0)
|
||||
current_streak = Column(Integer, default=0)
|
||||
best_streak = Column(Integer, default=0)
|
||||
average_rating = Column(Float, default=0.0)
|
||||
last_game_date = Column(DateTime, nullable=True)
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
user = relationship("User")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<UserStatistics user={self.user_id} wins={self.total_wins} losses={self.total_losses}>"
|
||||
|
||||
|
||||
class UserCardCollection(Base):
|
||||
"""User card collection."""
|
||||
__tablename__ = "user_card_collection"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
card_id = Column(Integer, nullable=False, index=True)
|
||||
quantity = Column(Integer, default=1)
|
||||
condition = Column(String(20), server_default="NEAR_MINT")
|
||||
language = Column(String(5), server_default="EN")
|
||||
is_foil = Column(Boolean, default=False)
|
||||
is_alt_art = Column(Boolean, default=False)
|
||||
acquired_date = Column(DateTime, server_default=func.now())
|
||||
acquisition_method = Column(String(50), nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'card_id', 'is_foil', 'is_alt_art', name='uq_collection_unique'),
|
||||
Index('idx_collection_user_card', 'user_id', 'card_id'),
|
||||
)
|
||||
|
||||
user = relationship("User", backref="card_collection")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<UserCard user={self.user_id} card={self.card_id} qty={self.quantity}>"
|
||||
|
||||
|
||||
class CardWishlist(Base):
|
||||
"""User card wishlist."""
|
||||
__tablename__ = "card_wishlist"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id", ondelete="CASCADE"), nullable=False)
|
||||
card_id = Column(Integer, nullable=False)
|
||||
max_price = Column(Float, nullable=True)
|
||||
notes = Column(Text, nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('user_id', 'card_id', name='uq_wishlist_user_card'),
|
||||
)
|
||||
|
||||
user = relationship("User", backref="wishlist")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<CardWishlist user={self.user_id} card={self.card_id}>"
|
||||
|
||||
|
||||
class UserGroup(Base):
|
||||
"""User group."""
|
||||
__tablename__ = "user_groups"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
owner_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
||||
is_public = Column(Boolean, default=True)
|
||||
max_members = Column(Integer, default=50)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
owner = relationship("User", foreign_keys=[owner_id])
|
||||
members = relationship("GroupMember", back_populates="group", cascade="all, delete-orphan")
|
||||
messages = relationship("GroupChatMessage", back_populates="group", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<UserGroup {self.name} owner={self.owner_id}>"
|
||||
|
||||
|
||||
class GroupMember(Base):
|
||||
"""Group member."""
|
||||
__tablename__ = "group_members"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
group_id = Column(BigInteger, ForeignKey("user_groups.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
||||
role = Column(String(20), server_default="MEMBER")
|
||||
joined_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
group = relationship("UserGroup", back_populates="members")
|
||||
user = relationship("User")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('group_id', 'user_id', name='uq_group_member'),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<GroupMember group={self.group_id} user={self.user_id} role={self.role}>"
|
||||
|
||||
|
||||
class GroupChatMessage(Base):
|
||||
"""Group chat message."""
|
||||
__tablename__ = "group_chat_messages"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
group_id = Column(BigInteger, ForeignKey("user_groups.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
sender_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
||||
message = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, server_default=func.now(), index=True)
|
||||
|
||||
group = relationship("UserGroup", back_populates="messages")
|
||||
sender = relationship("User", foreign_keys=[sender_id])
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<GroupChatMessage group={self.group_id} sender={self.sender_id}>"
|
||||
|
||||
|
||||
class UserNetwork(Base):
|
||||
"""User network (extended social connection)."""
|
||||
__tablename__ = "user_networks"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
name = Column(String(100), nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
creator_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False)
|
||||
is_public = Column(Boolean, default=True)
|
||||
created_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
creator = relationship("User", foreign_keys=[creator_id])
|
||||
members = relationship("NetworkMember", back_populates="network", cascade="all, delete-orphan")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<UserNetwork {self.name} creator={self.creator_id}>"
|
||||
|
||||
|
||||
class NetworkMember(Base):
|
||||
"""Network member."""
|
||||
__tablename__ = "network_members"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
network_id = Column(BigInteger, ForeignKey("user_networks.id", ondelete="CASCADE"), nullable=False, index=True)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
||||
role = Column(String(20), server_default="MEMBER")
|
||||
joined_at = Column(DateTime, server_default=func.now())
|
||||
|
||||
network = relationship("UserNetwork", back_populates="members")
|
||||
user = relationship("User")
|
||||
|
||||
__table_args__ = (
|
||||
UniqueConstraint('network_id', 'user_id', name='uq_network_member'),
|
||||
)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<NetworkMember network={self.network_id} user={self.user_id} role={self.role}>"
|
||||
|
||||
|
||||
class UserPreference(Base):
|
||||
"""User preferences and settings."""
|
||||
__tablename__ = "user_preferences"
|
||||
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), primary_key=True)
|
||||
theme = Column(String(20), server_default="light")
|
||||
notifications_enabled = Column(Boolean, default=True)
|
||||
email_notifications = Column(Boolean, default=True)
|
||||
auto_save_decks = Column(Boolean, default=True)
|
||||
default_format = Column(String(50), server_default="standard")
|
||||
language = Column(String(5), server_default="EN")
|
||||
updated_at = Column(DateTime, server_default=func.now(), onupdate=func.now())
|
||||
|
||||
user = relationship("User")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<UserPreference user={self.user_id} theme={self.theme}>"
|
||||
|
||||
|
||||
class UserActivityLog(Base):
|
||||
"""User activity log."""
|
||||
__tablename__ = "user_activity_log"
|
||||
|
||||
id = Column(BigInteger, primary_key=True)
|
||||
user_id = Column(Integer, ForeignKey("mtgonline_users.id"), nullable=False, index=True)
|
||||
activity_type = Column(String(50), nullable=False, index=True)
|
||||
activity_data = Column(JSON, nullable=True)
|
||||
ip_address = Column(String(45), nullable=True)
|
||||
created_at = Column(DateTime, server_default=func.now(), index=True)
|
||||
|
||||
user = relationship("User", backref="activity_logs")
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"<UserActivity user={self.user_id} type={self.activity_type}>"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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