- Added MTG card ORM models (mtg_cards, mtg_sets tables) - Created card_database service with search, get_by_name, get_by_set - Added Redis client with caching layer (3600s TTL default) - Created card router with caching on all endpoints: - Search cards (5min cache) - Get card by name (10min cache) - Get cards by set (15min cache) - Get card types/rarities (30min cache) - Get sets (1hr cache) - Get statistics (1hr cache) - Updated settings.py: - Added JWT_SECRET_KEY field - Added DB_CONFIG and REDIS_CONFIG dictionaries - Updated security.py to use JWT_SECRET_KEY with fallback - Updated auth.py to use timezone-aware datetimes - Updated refresh_mtg.py to use settings instead of os.environ - Updated mtg_monitor.py to use settings for connections - Added services package with __init__.py All 20 tests passing.
266 lines
7.5 KiB
Python
266 lines
7.5 KiB
Python
"""Deck management router endpoints."""
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select, delete, update
|
|
from typing import Optional, List
|
|
|
|
from app.core.database import get_db
|
|
from app.core.security import get_current_user
|
|
from app.models.models import DecklistFile, DecklistFolder
|
|
from app.schemas.schemas import DeckCreate, DeckUpdate, DeckResponse, FolderCreate, FolderResponse
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
@router.get("/", response_model=List[DeckResponse])
|
|
async def list_decks(
|
|
folder_id: Optional[int] = None,
|
|
page: int = 1,
|
|
page_size: int = 50,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""List decks for current user."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
if folder_id:
|
|
stmt = (
|
|
select(DecklistFile)
|
|
.where(
|
|
DecklistFile.owner_id == user_id,
|
|
DecklistFile.folder_id == folder_id,
|
|
)
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
)
|
|
else:
|
|
stmt = (
|
|
select(DecklistFile)
|
|
.where(DecklistFile.owner_id == user_id)
|
|
.offset((page - 1) * page_size)
|
|
.limit(page_size)
|
|
)
|
|
|
|
result = await db.execute(stmt)
|
|
decks = result.scalars().all()
|
|
|
|
return [DeckResponse.model_validate(deck) for deck in decks]
|
|
|
|
|
|
@router.post("/", response_model=DeckResponse)
|
|
async def create_deck(
|
|
request: DeckCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a new deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify folder exists if specified
|
|
if request.folder_id:
|
|
stmt = select(DecklistFolder).where(
|
|
DecklistFolder.id == request.folder_id,
|
|
DecklistFolder.owner_id == user_id,
|
|
)
|
|
result = await db.execute(stmt)
|
|
folder = result.scalar_one_or_none()
|
|
if not folder:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Folder not found",
|
|
)
|
|
|
|
new_deck = DecklistFile(
|
|
owner_id=user_id,
|
|
folder_id=request.folder_id,
|
|
name=request.name,
|
|
content=request.content,
|
|
format=request.format,
|
|
)
|
|
db.add(new_deck)
|
|
await db.flush()
|
|
|
|
return DeckResponse.model_validate(new_deck)
|
|
|
|
|
|
@router.get("/folders", response_model=List[FolderResponse])
|
|
async def list_folders(
|
|
parent_id: Optional[int] = None,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""List folders for current user."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
if parent_id:
|
|
stmt = select(DecklistFolder).where(
|
|
DecklistFolder.parent_id == parent_id,
|
|
DecklistFolder.owner_id == user_id,
|
|
)
|
|
else:
|
|
stmt = select(DecklistFolder).where(
|
|
DecklistFolder.parent_id == None, # Top-level folders
|
|
DecklistFolder.owner_id == user_id,
|
|
)
|
|
|
|
result = await db.execute(stmt)
|
|
folders = result.scalars().all()
|
|
|
|
return [FolderResponse.model_validate(folder) for folder in folders]
|
|
|
|
|
|
@router.post("/folders", response_model=FolderResponse)
|
|
async def create_folder(
|
|
request: FolderCreate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Create a new folder."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
# Verify parent folder exists if specified
|
|
if request.parent_id:
|
|
stmt = select(DecklistFolder).where(
|
|
DecklistFolder.id == request.parent_id,
|
|
DecklistFolder.owner_id == user_id,
|
|
)
|
|
result = await db.execute(stmt)
|
|
parent_folder = result.scalar_one_or_none()
|
|
if not parent_folder:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Parent folder not found",
|
|
)
|
|
|
|
new_folder = DecklistFolder(
|
|
owner_id=user_id,
|
|
name=request.name,
|
|
parent_id=request.parent_id,
|
|
)
|
|
db.add(new_folder)
|
|
await db.flush()
|
|
|
|
return FolderResponse.model_validate(new_folder)
|
|
|
|
|
|
@router.delete("/folders/{folder_id}")
|
|
async def delete_folder(
|
|
folder_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete folder and all its contents."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(DecklistFolder).where(
|
|
DecklistFolder.id == folder_id,
|
|
DecklistFolder.owner_id == user_id,
|
|
)
|
|
result = await db.execute(stmt)
|
|
folder = result.scalar_one_or_none()
|
|
|
|
if not folder:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Folder not found",
|
|
)
|
|
|
|
# Delete folder and all decks (cascading delete)
|
|
await db.execute(delete(DecklistFolder).where(DecklistFolder.id == folder_id))
|
|
|
|
return {"message": "Folder deleted successfully"}
|
|
|
|
|
|
@router.get("/{deck_id}", response_model=DeckResponse)
|
|
async def get_deck(
|
|
deck_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Get deck by ID."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(DecklistFile).where(
|
|
DecklistFile.id == deck_id,
|
|
DecklistFile.owner_id == user_id,
|
|
)
|
|
result = await db.execute(stmt)
|
|
deck = result.scalar_one_or_none()
|
|
|
|
if not deck:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Deck not found",
|
|
)
|
|
|
|
return DeckResponse.model_validate(deck)
|
|
|
|
|
|
@router.patch("/{deck_id}", response_model=DeckResponse)
|
|
async def update_deck(
|
|
deck_id: int,
|
|
request: DeckUpdate,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Update deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(DecklistFile).where(
|
|
DecklistFile.id == deck_id,
|
|
DecklistFile.owner_id == user_id,
|
|
)
|
|
result = await db.execute(stmt)
|
|
deck = result.scalar_one_or_none()
|
|
|
|
if not deck:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Deck not found",
|
|
)
|
|
|
|
# Update fields
|
|
update_data = request.model_dump(exclude_unset=True)
|
|
|
|
stmt = (
|
|
update(DecklistFile)
|
|
.where(DecklistFile.id == deck_id)
|
|
.values(**update_data)
|
|
)
|
|
await db.execute(stmt)
|
|
await db.flush()
|
|
|
|
# Fetch updated deck
|
|
stmt = select(DecklistFile).where(DecklistFile.id == deck_id)
|
|
result = await db.execute(stmt)
|
|
updated_deck = result.scalar_one_or_none()
|
|
|
|
return DeckResponse.model_validate(updated_deck)
|
|
|
|
|
|
@router.delete("/{deck_id}")
|
|
async def delete_deck(
|
|
deck_id: int,
|
|
db: AsyncSession = Depends(get_db),
|
|
current_user: dict = Depends(get_current_user),
|
|
):
|
|
"""Delete deck."""
|
|
user_id = int(current_user["user_id"])
|
|
|
|
stmt = select(DecklistFile).where(
|
|
DecklistFile.id == deck_id,
|
|
DecklistFile.owner_id == user_id,
|
|
)
|
|
result = await db.execute(stmt)
|
|
deck = result.scalar_one_or_none()
|
|
|
|
if not deck:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="Deck not found",
|
|
)
|
|
|
|
await db.execute(delete(DecklistFile).where(DecklistFile.id == deck_id))
|
|
|
|
return {"message": "Deck deleted successfully"}
|