"""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 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)) -> Project: """Create a new segmentation project.""" project = Project(**payload.model_dump()) 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)) -> List[Project]: """Retrieve a paginated list of projects.""" projects = db.query(Project).offset(skip).limit(limit).all() for p in projects: p.frame_count = len(p.frames) 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)) -> Project: """Retrieve a project by its ID.""" project = db.query(Project).filter(Project.id == project_id).first() if not project: raise HTTPException(status_code=404, detail="Project not found") project.frame_count = len(project.frames) 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), ) -> Project: """Update project fields partially.""" project = db.query(Project).filter(Project.id == project_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)) -> None: """Delete a project and all related frames and annotations.""" project = db.query(Project).filter(Project.id == project_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), ) -> Frame: """Register a new frame under a project.""" project = db.query(Project).filter(Project.id == project_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), ) -> List[Frame]: """Retrieve all frames belonging to a project.""" project = db.query(Project).filter(Project.id == project_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)) -> Frame: """Retrieve a specific frame by ID.""" frame = ( db.query(Frame) .filter(Frame.project_id == project_id, Frame.id == frame_id) .first() ) if not frame: raise HTTPException(status_code=404, detail="Frame not found") return frame