RM / app /api /v1 /library.py
trretretret's picture
Initial commit: Add research assistant application
b708f13
# app/api/v1/library.py
import json
import logging
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query, status
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.api import deps
from app.models.user import User
from app.models.paper import Paper
from app.models.library import LibraryItem
from app.schemas.library import (
LibraryCreate,
LibraryResponse,
LibraryUpdate,
)
logger = logging.getLogger("rm_research.api.library")
router = APIRouter()
# ---------------------------------------------------------
# Save Paper
# ---------------------------------------------------------
@router.post(
"/",
response_model=LibraryResponse,
status_code=status.HTTP_201_CREATED,
summary="Save paper to library",
)
async def save_paper(
item_in: LibraryCreate,
db: AsyncSession = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_user),
) -> LibraryResponse:
"""Save a paper to the user's personal research library."""
# 1️⃣ Verify paper exists
paper_result = await db.execute(
select(Paper).where(Paper.id == item_in.paper_id)
)
paper = paper_result.scalar_one_or_none()
if paper is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Paper not found.",
)
# 2️⃣ Prevent duplicate saves
existing = await db.execute(
select(LibraryItem.id)
.where(LibraryItem.user_id == current_user.id)
.where(LibraryItem.paper_id == item_in.paper_id)
)
if existing.scalar_one_or_none():
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="Paper already exists in your library.",
)
# 3️⃣ Create library item (FIXED: Serializing tags to JSON)
library_item = LibraryItem(
user_id=current_user.id,
paper_id=paper.id,
tags=json.dumps(item_in.tags_list) if item_in.tags_list else "[]",
notes=item_in.notes,
)
db.add(library_item)
try:
await db.commit()
await db.refresh(library_item)
return library_item
except Exception:
await db.rollback()
logger.exception(
"Failed saving library item | user=%s paper=%s",
current_user.id,
item_in.paper_id,
)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database error while saving paper.",
)
# ---------------------------------------------------------
# Get User Library
# ---------------------------------------------------------
@router.get(
"/",
response_model=List[LibraryResponse],
summary="View saved library",
)
async def get_library(
limit: int = Query(50, ge=1, le=100),
offset: int = Query(0, ge=0),
db: AsyncSession = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_user),
) -> List[LibraryResponse]:
"""Retrieve saved papers from the user's library with pagination."""
result = await db.execute(
select(LibraryItem)
.where(LibraryItem.user_id == current_user.id)
.order_by(LibraryItem.created_at.desc())
.limit(limit)
.offset(offset)
)
return result.scalars().all()
# ---------------------------------------------------------
# Update Library Item
# ---------------------------------------------------------
@router.patch(
"/{library_id}",
response_model=LibraryResponse,
summary="Update library item",
)
async def update_library_item(
library_id: int,
item_update: LibraryUpdate,
db: AsyncSession = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_user),
) -> LibraryResponse:
"""Update notes or tags for a saved paper."""
result = await db.execute(
select(LibraryItem)
.where(LibraryItem.id == library_id)
.where(LibraryItem.user_id == current_user.id)
)
library_item = result.scalar_one_or_none()
if library_item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Library item not found.",
)
if item_update.notes is not None:
library_item.notes = item_update.notes
if item_update.tags_list is not None:
# FIXED: Serialize tags to JSON when updating
library_item.tags = json.dumps(item_update.tags_list)
try:
await db.commit()
await db.refresh(library_item)
return library_item
except Exception:
await db.rollback()
logger.exception("Failed updating library item | id=%s", library_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database error while updating item.",
)
# ---------------------------------------------------------
# Remove Paper From Library
# ---------------------------------------------------------
@router.delete(
"/{library_id}",
status_code=status.HTTP_204_NO_CONTENT,
summary="Remove paper from library",
)
async def delete_library_item(
library_id: int,
db: AsyncSession = Depends(deps.get_db),
current_user: User = Depends(deps.get_current_user),
):
"""Delete a saved paper from the user's library."""
result = await db.execute(
select(LibraryItem)
.where(LibraryItem.id == library_id)
.where(LibraryItem.user_id == current_user.id)
)
library_item = result.scalar_one_or_none()
if library_item is None:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Library item not found.",
)
try:
await db.delete(library_item)
await db.commit()
except Exception:
await db.rollback()
logger.exception("Failed deleting library item | id=%s", library_id)
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Database error while deleting item.",
)