""" ProjectState service layer for business logic and database operations. Handles all database operations for project states, tracking the current state of projects for quick context retrieval. """ from typing import Optional from uuid import UUID from fastapi import HTTPException, status from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session from api.models.project_state import ProjectState from api.schemas.project_state import ProjectStateCreate, ProjectStateUpdate from api.utils.context_compression import compress_project_state def get_project_states( db: Session, skip: int = 0, limit: int = 100 ) -> tuple[list[ProjectState], int]: """ Retrieve a paginated list of project states. Args: db: Database session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of project states, total count) """ # Get total count total = db.query(ProjectState).count() # Get paginated results, ordered by most recently updated states = ( db.query(ProjectState) .order_by(ProjectState.updated_at.desc()) .offset(skip) .limit(limit) .all() ) return states, total def get_project_state_by_id(db: Session, state_id: UUID) -> ProjectState: """ Retrieve a single project state by its ID. Args: db: Database session state_id: UUID of the project state to retrieve Returns: ProjectState: The project state object Raises: HTTPException: 404 if project state not found """ state = db.query(ProjectState).filter(ProjectState.id == str(state_id)).first() if not state: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"ProjectState with ID {state_id} not found" ) return state def get_project_state_by_project(db: Session, project_id: UUID) -> Optional[ProjectState]: """ Retrieve the project state for a specific project. Each project has exactly one project state (unique constraint). Args: db: Database session project_id: UUID of the project Returns: Optional[ProjectState]: The project state if found, None otherwise """ state = db.query(ProjectState).filter(ProjectState.project_id == str(project_id)).first() return state def create_project_state( db: Session, state_data: ProjectStateCreate ) -> ProjectState: """ Create a new project state. Args: db: Database session state_data: Project state creation data Returns: ProjectState: The created project state object Raises: HTTPException: 409 if project state already exists for this project HTTPException: 500 if database error occurs """ # Check if project state already exists for this project existing_state = get_project_state_by_project(db, state_data.project_id) if existing_state: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"ProjectState for project ID {state_data.project_id} already exists" ) try: # Create new project state instance db_state = ProjectState(**state_data.model_dump()) # Add to database db.add(db_state) db.commit() db.refresh(db_state) return db_state except IntegrityError as e: db.rollback() if "project_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"ProjectState for project ID {state_data.project_id} already exists" ) else: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to create project state: {str(e)}" ) def update_project_state( db: Session, state_id: UUID, state_data: ProjectStateUpdate ) -> ProjectState: """ Update an existing project state. Uses compression utilities when updating to maintain efficient storage. Args: db: Database session state_id: UUID of the project state to update state_data: Project state update data Returns: ProjectState: The updated project state object Raises: HTTPException: 404 if project state not found HTTPException: 500 if database error occurs """ # Get existing state state = get_project_state_by_id(db, state_id) try: # Update only provided fields update_data = state_data.model_dump(exclude_unset=True) # Apply updates for field, value in update_data.items(): setattr(state, field, value) db.commit() db.refresh(state) return state except HTTPException: db.rollback() raise except IntegrityError as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Database error: {str(e)}" ) except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to update project state: {str(e)}" ) def update_project_state_by_project( db: Session, project_id: UUID, state_data: ProjectStateUpdate ) -> ProjectState: """ Update project state by project ID (convenience method). If project state doesn't exist, creates a new one. Args: db: Database session project_id: UUID of the project state_data: Project state update data Returns: ProjectState: The updated or created project state object Raises: HTTPException: 500 if database error occurs """ # Try to get existing state state = get_project_state_by_project(db, project_id) if state: # Update existing state return update_project_state(db, UUID(state.id), state_data) else: # Create new state create_data = ProjectStateCreate( project_id=project_id, **state_data.model_dump(exclude_unset=True) ) return create_project_state(db, create_data) def delete_project_state(db: Session, state_id: UUID) -> dict: """ Delete a project state by its ID. Args: db: Database session state_id: UUID of the project state to delete Returns: dict: Success message Raises: HTTPException: 404 if project state not found HTTPException: 500 if database error occurs """ # Get existing state (raises 404 if not found) state = get_project_state_by_id(db, state_id) try: db.delete(state) db.commit() return { "message": "ProjectState deleted successfully", "state_id": str(state_id) } except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete project state: {str(e)}" )