Complete architecture simplification: 54+ files → 37 files
- Removed 11 duplicate router files (auth.py, users.py, decks.py, card_router.py, interactions.py, user_data.py, rooms.py, games/router.py, refresh.py, card_import.py, ws.py) - Removed duplicate core files (logging_config.py, settings.py) - Removed unused services (card_mirror_service.py, mtgjson_loader.py, mtgjson_uploader.py) - Removed unused schemas (proto_messages.py, protocol_constants.py) - Created run.py startup script for simple execution - Created app/core/config.py for unified configuration management - Created app/core/logging.py for logging configuration - Consolidated routers into 6 focused files (auth_router.py, users_router.py, decks_router.py, cards_router.py, user_data_router.py, games_router.py) - Updated all imports to use consolidated model paths - Added backward compatibility aliases in database.py - Created SIMPLIFICATION_SUMMARY.md documentation Architecture improvements: - Single unified database engine with auto-detection (SQLite/PostgreSQL) - Consolidated models into 2 files (models.py, mtg_models.py) - Clean router structure with one file per domain - Simplified configuration with sensible defaults - SQLite by default for development, PostgreSQL for production Testing: - All imports verified working - App loads successfully - Ready for development and testing
This commit is contained in:
@@ -0,0 +1,487 @@
|
||||
"""
|
||||
Games Router - Consolidated
|
||||
|
||||
This module consolidates room management, game operations, MTGJSON data refresh,
|
||||
and WebSocket connections into a unified router structure.
|
||||
|
||||
Endpoints:
|
||||
/rooms/* - Room CRUD operations (Admin/Judge only for write ops)
|
||||
/games/* - Game management stubs
|
||||
/mtgjson/* - MTGJSON dataset refresh and verification (Admin only)
|
||||
/ws/game/{game_id} - WebSocket connection for game sessions
|
||||
|
||||
Architecture:
|
||||
- Uses FastAPI sub-routers with different prefixes for logical grouping
|
||||
- WebSocket endpoint for real-time game communication
|
||||
- All endpoints require authentication via get_current_user dependency
|
||||
- Admin/Judge privilege checks on sensitive operations
|
||||
"""
|
||||
|
||||
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, Query, WebSocket, status
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy import select, insert, update, delete
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
|
||||
from app.core.database import get_db
|
||||
from app.core.security import get_current_user
|
||||
from app.models.models import Room, User
|
||||
from app.schemas.schemas import RoomResponse, GameCreate, GameResponse
|
||||
from app.services.mtgjson_manager import MTGJSONManager
|
||||
from app.services.game_server import game_websocket_endpoint
|
||||
|
||||
# Main router
|
||||
router = APIRouter()
|
||||
|
||||
# =============================================================================
|
||||
# Room Management Sub-Router (prefix: /rooms)
|
||||
# =============================================================================
|
||||
|
||||
rooms_router = APIRouter(prefix="/rooms", tags=["Rooms"])
|
||||
|
||||
|
||||
@rooms_router.get("/", response_model=List[RoomResponse])
|
||||
async def list_rooms(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> List[RoomResponse]:
|
||||
"""List all available rooms.
|
||||
|
||||
Returns a list of all rooms in the system, ordered by ID.
|
||||
Requires authentication.
|
||||
"""
|
||||
stmt = select(Room).order_by(Room.id)
|
||||
result = await db.execute(stmt)
|
||||
rooms = result.scalars().all()
|
||||
|
||||
return [RoomResponse.model_validate(room) for room in rooms]
|
||||
|
||||
|
||||
@rooms_router.get("/{room_id}", response_model=RoomResponse)
|
||||
async def get_room(
|
||||
room_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> RoomResponse:
|
||||
"""Get room by ID.
|
||||
|
||||
Returns detailed information about a specific room.
|
||||
Raises 404 if room not found.
|
||||
"""
|
||||
stmt = select(Room).where(Room.id == room_id)
|
||||
result = await db.execute(stmt)
|
||||
room = result.scalar_one_or_none()
|
||||
|
||||
if not room:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Room not found",
|
||||
)
|
||||
|
||||
return RoomResponse.model_validate(room)
|
||||
|
||||
|
||||
@rooms_router.post("/", status_code=status.HTTP_201_CREATED)
|
||||
async def create_room(
|
||||
name: str,
|
||||
description: Optional[str] = None,
|
||||
is_password_protected: bool = False,
|
||||
password_hash: Optional[str] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Create a new room (Admin/Judge only).
|
||||
|
||||
Creates a new game room with the specified configuration.
|
||||
Only users with Admin or Judge privileges can create rooms.
|
||||
Raises 409 if room name already exists.
|
||||
"""
|
||||
# Check if current user is admin
|
||||
if current_user.get("privlevel") not in ["Admin", "Judge"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required",
|
||||
)
|
||||
|
||||
# Check if room name already exists
|
||||
stmt = select(Room).where(Room.name == name)
|
||||
result = await db.execute(stmt)
|
||||
if result.scalar_one_or_none():
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT,
|
||||
detail="Room name already exists",
|
||||
)
|
||||
|
||||
new_room = Room(
|
||||
name=name,
|
||||
description=description,
|
||||
is_password_protected=is_password_protected,
|
||||
password_hash=password_hash,
|
||||
)
|
||||
db.add(new_room)
|
||||
await db.flush()
|
||||
|
||||
return {"message": f"Room '{name}' created successfully", "room_id": new_room.id}
|
||||
|
||||
|
||||
@rooms_router.patch("/{room_id}")
|
||||
async def update_room(
|
||||
room_id: int,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
is_password_protected: Optional[bool] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Update room (Admin/Judge only).
|
||||
|
||||
Updates the specified fields of a room.
|
||||
Only users with Admin or Judge privileges can update rooms.
|
||||
"""
|
||||
# Check if current user is admin
|
||||
if current_user.get("privlevel") not in ["Admin", "Judge"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required",
|
||||
)
|
||||
|
||||
stmt = select(Room).where(Room.id == room_id)
|
||||
result = await db.execute(stmt)
|
||||
room = result.scalar_one_or_none()
|
||||
|
||||
if not room:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Room not found",
|
||||
)
|
||||
|
||||
# Update fields
|
||||
update_data = {}
|
||||
if name:
|
||||
update_data["name"] = name
|
||||
if description is not None:
|
||||
update_data["description"] = description
|
||||
if is_password_protected is not None:
|
||||
update_data["is_password_protected"] = is_password_protected
|
||||
|
||||
stmt = (
|
||||
update(Room)
|
||||
.where(Room.id == room_id)
|
||||
.values(**update_data)
|
||||
)
|
||||
await db.execute(stmt)
|
||||
|
||||
return {"message": f"Room '{name or room.name}' updated successfully"}
|
||||
|
||||
|
||||
@rooms_router.delete("/{room_id}")
|
||||
async def delete_room(
|
||||
room_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Delete room (Admin/Judge only).
|
||||
|
||||
Permanently deletes a room from the system.
|
||||
Only users with Admin or Judge privileges can delete rooms.
|
||||
"""
|
||||
# Check if current user is admin
|
||||
if current_user.get("privlevel") not in ["Admin", "Judge"]:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required",
|
||||
)
|
||||
|
||||
stmt = select(Room).where(Room.id == room_id)
|
||||
result = await db.execute(stmt)
|
||||
room = result.scalar_one_or_none()
|
||||
|
||||
if not room:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Room not found",
|
||||
)
|
||||
|
||||
await db.execute(delete(Room).where(Room.id == room_id))
|
||||
|
||||
return {"message": f"Room '{room.name}' deleted successfully"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Game Management Sub-Router (prefix: /games)
|
||||
# =============================================================================
|
||||
|
||||
games_router = APIRouter(prefix="/games", tags=["Games"])
|
||||
|
||||
|
||||
@games_router.get("/", response_model=List[GameResponse])
|
||||
async def list_games(
|
||||
room_id: Optional[int] = None,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> List[GameResponse]:
|
||||
"""List active games.
|
||||
|
||||
Returns a list of currently active games.
|
||||
Note: This is a stub implementation - returns empty list.
|
||||
"""
|
||||
# This would query a games table - simplified for now
|
||||
# In production, you'd have a MTG OnlineGames model
|
||||
return []
|
||||
|
||||
|
||||
@games_router.post("/", response_model=GameResponse, status_code=status.HTTP_201_CREATED)
|
||||
async def create_game(
|
||||
request: GameCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> GameResponse:
|
||||
"""Create a new game (mock implementation).
|
||||
|
||||
Creates a new game session.
|
||||
Note: This is a stub - returns mock response.
|
||||
"""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# Verify user exists
|
||||
stmt = select(User).where(User.id == user_id)
|
||||
result = await db.execute(stmt)
|
||||
user = result.scalar_one_or_none()
|
||||
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="User not found",
|
||||
)
|
||||
|
||||
# In production, you'd create a game in the game database
|
||||
# For now, return a mock response
|
||||
game_data = {
|
||||
"id": 1, # Mock ID
|
||||
"room_id": request.room_id,
|
||||
"game_type": request.game_type,
|
||||
"description": request.description,
|
||||
"with_password": bool(request.password),
|
||||
"max_players": 4,
|
||||
"player_count": 1,
|
||||
"started": False,
|
||||
"creation_date": datetime.now(),
|
||||
}
|
||||
|
||||
return GameResponse(**game_data)
|
||||
|
||||
|
||||
@games_router.get("/{game_id}", response_model=GameResponse)
|
||||
async def get_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> GameResponse:
|
||||
"""Get game by ID (mock implementation).
|
||||
|
||||
Returns details about a specific game.
|
||||
Note: This is a stub - returns mock response.
|
||||
"""
|
||||
# In production, query the games table
|
||||
return GameResponse(
|
||||
id=game_id,
|
||||
room_id=1,
|
||||
game_type="Casual",
|
||||
description="Test game",
|
||||
with_password=False,
|
||||
max_players=4,
|
||||
player_count=0,
|
||||
started=False,
|
||||
creation_date=datetime.now(),
|
||||
)
|
||||
|
||||
|
||||
@games_router.post("/{game_id}/join")
|
||||
async def join_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Join a game (stub implementation).
|
||||
|
||||
Adds the current user to a game session.
|
||||
Note: This is a stub - returns mock response.
|
||||
"""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# In production, add user to game players table
|
||||
return {"message": f"User {user_id} joined game {game_id}"}
|
||||
|
||||
|
||||
@games_router.post("/{game_id}/leave")
|
||||
async def leave_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Leave a game (stub implementation).
|
||||
|
||||
Removes the current user from a game session.
|
||||
Note: This is a stub - returns mock response.
|
||||
"""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# In production, remove user from game players table
|
||||
return {"message": f"User {user_id} left game {game_id}"}
|
||||
|
||||
|
||||
@games_router.post("/{game_id}/start")
|
||||
async def start_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Start a game (host only, stub implementation).
|
||||
|
||||
Initiates a game session.
|
||||
Note: This is a stub - returns mock response.
|
||||
"""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# In production, update game started status
|
||||
return {"message": f"Game {game_id} started"}
|
||||
|
||||
|
||||
@games_router.post("/{game_id}/end")
|
||||
async def end_game(
|
||||
game_id: int,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""End a game (stub implementation).
|
||||
|
||||
Terminates a game session.
|
||||
Note: This is a stub - returns mock response.
|
||||
"""
|
||||
user_id = int(current_user["user_id"])
|
||||
|
||||
# In production, update game closed status
|
||||
return {"message": f"Game {game_id} ended"}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# MTGJSON Data Refresh Sub-Router (prefix: /mtgjson)
|
||||
# =============================================================================
|
||||
|
||||
mtgjson_router = APIRouter(prefix="/mtgjson", tags=["MTGJSON Data"])
|
||||
|
||||
|
||||
@mtgjson_router.post("/refresh")
|
||||
async def trigger_refresh(
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Trigger a refresh of MTGJSON datasets (Admin only).
|
||||
|
||||
Downloads files from MTGJSON API and upserts data into PostgreSQL.
|
||||
The refresh runs in the background and may take several minutes.
|
||||
Only Admin users can trigger refreshes.
|
||||
"""
|
||||
# Check if current user is admin
|
||||
if current_user.get("privlevel") != "Admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required",
|
||||
)
|
||||
|
||||
# Create manager and trigger refresh in background
|
||||
manager = MTGJSONManager()
|
||||
background_tasks.add_task(manager.download_and_refresh, force=False)
|
||||
|
||||
return {
|
||||
"message": "Refresh initiated in background",
|
||||
"status": "started",
|
||||
}
|
||||
|
||||
|
||||
@mtgjson_router.get("/status")
|
||||
async def get_refresh_status(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Get current refresh status and data health (Admin only).
|
||||
|
||||
Returns information about:
|
||||
- Last successful refresh timestamp
|
||||
- Current data counts
|
||||
- File status
|
||||
- Required files status
|
||||
|
||||
Only Admin users can access this endpoint.
|
||||
"""
|
||||
# Check if current user is admin
|
||||
if current_user.get("privlevel") != "Admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required",
|
||||
)
|
||||
|
||||
manager = MTGJSONManager()
|
||||
return await manager.get_health_status()
|
||||
|
||||
|
||||
@mtgjson_router.post("/verify")
|
||||
async def verify_files(
|
||||
current_user: dict = Depends(get_current_user),
|
||||
) -> dict:
|
||||
"""Verify the integrity of downloaded MTGJSON files (Admin only).
|
||||
|
||||
Checks that all required files exist and are valid.
|
||||
Only Admin users can verify files.
|
||||
"""
|
||||
# Check if current user is admin
|
||||
if current_user.get("privlevel") != "Admin":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="Admin privileges required",
|
||||
)
|
||||
|
||||
manager = MTGJSONManager()
|
||||
all_valid, errors = await manager.verify_files()
|
||||
|
||||
if not all_valid:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail={
|
||||
"valid": False,
|
||||
"errors": errors,
|
||||
"message": "Some files failed verification",
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
"valid": True,
|
||||
"message": "All files verified successfully",
|
||||
}
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# WebSocket Endpoint
|
||||
# =============================================================================
|
||||
|
||||
@router.websocket("/ws/game/{game_id}")
|
||||
async def game_websocket(
|
||||
websocket: WebSocket,
|
||||
game_id: int,
|
||||
player_id: Optional[int] = Query(None),
|
||||
) -> None:
|
||||
"""WebSocket endpoint for game connections.
|
||||
|
||||
Handles real-time game state updates for connected players.
|
||||
Establishes a persistent connection for live game communication.
|
||||
"""
|
||||
await websocket.accept()
|
||||
await game_websocket_endpoint(websocket, game_id)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Include Sub-Routers
|
||||
# =============================================================================
|
||||
|
||||
router.include_router(rooms_router)
|
||||
router.include_router(games_router)
|
||||
router.include_router(mtgjson_router)
|
||||
Reference in New Issue
Block a user