""" Project service layer for business logic and database operations. This module handles all database operations for projects, providing a clean separation between the API routes and data access layer. """ 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 import Project from api.models.client import Client from api.schemas.project import ProjectCreate, ProjectUpdate def get_projects(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[Project], int]: """ Retrieve a paginated list of projects. Args: db: Database session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of projects, total count) Example: ```python projects, total = get_projects(db, skip=0, limit=50) print(f"Retrieved {len(projects)} of {total} projects") ``` """ # Get total count total = db.query(Project).count() # Get paginated results, ordered by created_at descending (newest first) projects = ( db.query(Project) .order_by(Project.created_at.desc()) .offset(skip) .limit(limit) .all() ) return projects, total def get_project_by_id(db: Session, project_id: UUID) -> Project: """ Retrieve a single project by its ID. Args: db: Database session project_id: UUID of the project to retrieve Returns: Project: The project object Raises: HTTPException: 404 if project not found Example: ```python project = get_project_by_id(db, project_id) print(f"Found project: {project.name}") ``` """ project = db.query(Project).filter(Project.id == str(project_id)).first() if not project: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Project with ID {project_id} not found" ) return project def get_project_by_slug(db: Session, slug: str) -> Optional[Project]: """ Retrieve a project by its slug. Args: db: Database session slug: Slug to search for Returns: Optional[Project]: The project if found, None otherwise Example: ```python project = get_project_by_slug(db, "dataforth-dos") if project: print(f"Found project: {project.name}") ``` """ return db.query(Project).filter(Project.slug == slug).first() def get_projects_by_client(db: Session, client_id: str, skip: int = 0, limit: int = 100) -> tuple[list[Project], int]: """ Retrieve projects for a specific client. Args: db: Database session client_id: Client UUID skip: Number of records to skip limit: Maximum number of records to return Returns: tuple: (list of projects, total count) Example: ```python projects, total = get_projects_by_client(db, client_id) print(f"Client has {total} projects") ``` """ total = db.query(Project).filter(Project.client_id == str(client_id)).count() projects = ( db.query(Project) .filter(Project.client_id == str(client_id)) .order_by(Project.created_at.desc()) .offset(skip) .limit(limit) .all() ) return projects, total def get_projects_by_status(db: Session, status_filter: str, skip: int = 0, limit: int = 100) -> tuple[list[Project], int]: """ Retrieve projects by status. Args: db: Database session status_filter: Status to filter by (complete, working, blocked, pending, critical, deferred) skip: Number of records to skip limit: Maximum number of records to return Returns: tuple: (list of projects, total count) Example: ```python projects, total = get_projects_by_status(db, "working") print(f"Found {total} working projects") ``` """ total = db.query(Project).filter(Project.status == status_filter).count() projects = ( db.query(Project) .filter(Project.status == status_filter) .order_by(Project.created_at.desc()) .offset(skip) .limit(limit) .all() ) return projects, total def validate_client_exists(db: Session, client_id: str) -> None: """ Validate that a client exists. Args: db: Database session client_id: Client UUID to validate Raises: HTTPException: 404 if client not found Example: ```python validate_client_exists(db, client_id) # Continues if client exists, raises HTTPException if not ``` """ client = db.query(Client).filter(Client.id == str(client_id)).first() if not client: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Client with ID {client_id} not found" ) def create_project(db: Session, project_data: ProjectCreate) -> Project: """ Create a new project. Args: db: Database session project_data: Project creation data Returns: Project: The created project object Raises: HTTPException: 404 if client not found HTTPException: 409 if project with slug already exists HTTPException: 500 if database error occurs Example: ```python project_data = ProjectCreate( client_id="123e4567-e89b-12d3-a456-426614174000", name="Client Website Redesign", status="working" ) project = create_project(db, project_data) print(f"Created project: {project.id}") ``` """ # Validate client exists validate_client_exists(db, project_data.client_id) # Check if project with slug already exists (if slug is provided) if project_data.slug: existing_project = get_project_by_slug(db, project_data.slug) if existing_project: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Project with slug '{project_data.slug}' already exists" ) try: # Create new project instance db_project = Project(**project_data.model_dump()) # Add to database db.add(db_project) db.commit() db.refresh(db_project) return db_project except IntegrityError as e: db.rollback() # Handle unique constraint violations if "slug" in str(e.orig): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Project with slug '{project_data.slug}' already exists" ) elif "client_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Client with ID {project_data.client_id} not found" ) 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: {str(e)}" ) def update_project(db: Session, project_id: UUID, project_data: ProjectUpdate) -> Project: """ Update an existing project. Args: db: Database session project_id: UUID of the project to update project_data: Project update data (only provided fields will be updated) Returns: Project: The updated project object Raises: HTTPException: 404 if project or client not found HTTPException: 409 if update would violate unique constraints HTTPException: 500 if database error occurs Example: ```python update_data = ProjectUpdate( status="completed", completed_date=date.today() ) project = update_project(db, project_id, update_data) print(f"Updated project: {project.name}") ``` """ # Get existing project project = get_project_by_id(db, project_id) try: # Update only provided fields update_data = project_data.model_dump(exclude_unset=True) # If updating client_id, validate client exists if "client_id" in update_data and update_data["client_id"] != project.client_id: validate_client_exists(db, update_data["client_id"]) # If updating slug, check if new slug is already taken if "slug" in update_data and update_data["slug"] and update_data["slug"] != project.slug: existing = get_project_by_slug(db, update_data["slug"]) if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Project with slug '{update_data['slug']}' already exists" ) # Apply updates for field, value in update_data.items(): setattr(project, field, value) db.commit() db.refresh(project) return project except HTTPException: db.rollback() raise except IntegrityError as e: db.rollback() if "slug" in str(e.orig): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Project with this slug already exists" ) elif "client_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Client not found" ) 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 update project: {str(e)}" ) def delete_project(db: Session, project_id: UUID) -> dict: """ Delete a project by its ID. Args: db: Database session project_id: UUID of the project to delete Returns: dict: Success message Raises: HTTPException: 404 if project not found HTTPException: 500 if database error occurs Example: ```python result = delete_project(db, project_id) print(result["message"]) # "Project deleted successfully" ``` """ # Get existing project (raises 404 if not found) project = get_project_by_id(db, project_id) try: db.delete(project) db.commit() return { "message": "Project deleted successfully", "project_id": str(project_id) } except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete project: {str(e)}" )