"""Project and Frame CRUD endpoints.""" import logging from typing import List from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy.orm import Session from database import get_db from models import Project, Frame, User from routers.auth import get_current_user, require_editor from schemas import ProjectCreate, ProjectOut, ProjectUpdate, FrameCreate, FrameOut from minio_client import get_presigned_url logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/projects", tags=["Projects"]) # --------------------------------------------------------------------------- # Projects # --------------------------------------------------------------------------- @router.post( "", response_model=ProjectOut, status_code=status.HTTP_201_CREATED, summary="Create a new project", ) def create_project( payload: ProjectCreate, db: Session = Depends(get_db), current_user: User = Depends(require_editor), ) -> Project: """Create a new segmentation project.""" project = Project(**payload.model_dump(), owner_user_id=current_user.id) db.add(project) db.commit() db.refresh(project) logger.info("Created project id=%s name=%s", project.id, project.name) return project @router.get( "", response_model=List[ProjectOut], summary="List all projects", ) def list_projects( skip: int = 0, limit: int = 100, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> List[Project]: """Retrieve a paginated list of projects.""" projects = ( db.query(Project) .filter(Project.owner_user_id == current_user.id) .offset(skip) .limit(limit) .all() ) for p in projects: p.frame_count = len(p.frames) if p.thumbnail_url: p.thumbnail_url = get_presigned_url(p.thumbnail_url, expires=3600) return projects @router.get( "/{project_id}", response_model=ProjectOut, summary="Get a single project", ) def get_project( project_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Project: """Retrieve a project by its ID.""" project = db.query(Project).filter( Project.id == project_id, Project.owner_user_id == current_user.id, ).first() if not project: raise HTTPException(status_code=404, detail="Project not found") project.frame_count = len(project.frames) if project.thumbnail_url: project.thumbnail_url = get_presigned_url(project.thumbnail_url, expires=3600) return project @router.patch( "/{project_id}", response_model=ProjectOut, summary="Update a project", ) def update_project( project_id: int, payload: ProjectUpdate, db: Session = Depends(get_db), current_user: User = Depends(require_editor), ) -> Project: """Update project fields partially.""" project = db.query(Project).filter( Project.id == project_id, Project.owner_user_id == current_user.id, ).first() if not project: raise HTTPException(status_code=404, detail="Project not found") for key, value in payload.model_dump(exclude_unset=True).items(): setattr(project, key, value) db.commit() db.refresh(project) logger.info("Updated project id=%s", project_id) return project @router.delete( "/{project_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Delete a project", ) def delete_project( project_id: int, db: Session = Depends(get_db), current_user: User = Depends(require_editor), ) -> None: """Delete a project and all related frames and annotations.""" project = db.query(Project).filter( Project.id == project_id, Project.owner_user_id == current_user.id, ).first() if not project: raise HTTPException(status_code=404, detail="Project not found") db.delete(project) db.commit() logger.info("Deleted project id=%s", project_id) # --------------------------------------------------------------------------- # Frames # --------------------------------------------------------------------------- @router.post( "/{project_id}/frames", response_model=FrameOut, status_code=status.HTTP_201_CREATED, summary="Add a frame to a project", ) def create_frame( project_id: int, payload: FrameCreate, db: Session = Depends(get_db), current_user: User = Depends(require_editor), ) -> Frame: """Register a new frame under a project.""" project = db.query(Project).filter( Project.id == project_id, Project.owner_user_id == current_user.id, ).first() if not project: raise HTTPException(status_code=404, detail="Project not found") frame = Frame(project_id=project_id, **payload.model_dump(exclude={"project_id"})) db.add(frame) db.commit() db.refresh(frame) return frame @router.get( "/{project_id}/frames", response_model=List[FrameOut], summary="List frames for a project", ) def list_frames( project_id: int, skip: int = 0, limit: int = 1000, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> List[Frame]: """Retrieve all frames belonging to a project.""" project = db.query(Project).filter( Project.id == project_id, Project.owner_user_id == current_user.id, ).first() if not project: raise HTTPException(status_code=404, detail="Project not found") frames = ( db.query(Frame) .filter(Frame.project_id == project_id) .order_by(Frame.frame_index) .offset(skip) .limit(limit) .all() ) for frame in frames: frame.image_url = get_presigned_url(frame.image_url, expires=3600) return frames @router.get( "/{project_id}/frames/{frame_id}", response_model=FrameOut, summary="Get a single frame", ) def get_frame( project_id: int, frame_id: int, db: Session = Depends(get_db), current_user: User = Depends(get_current_user), ) -> Frame: """Retrieve a specific frame by ID.""" frame = ( db.query(Frame) .join(Project, Project.id == Frame.project_id) .filter( Frame.project_id == project_id, Frame.id == frame_id, Project.owner_user_id == current_user.id, ) .first() ) if not frame: raise HTTPException(status_code=404, detail="Frame not found") return frame