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:
2026-08-26 15:15:36 +00:00
parent 03f5433583
commit 900f51af3a
45 changed files with 5823 additions and 5460 deletions
+487
View File
@@ -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)