""" Service service layer for business logic and database operations. This module handles all database operations for services, 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.service import Service from api.models.infrastructure import Infrastructure from api.schemas.service import ServiceCreate, ServiceUpdate def get_services(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[Service], int]: """ Retrieve a paginated list of services. Args: db: Database session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of services, total count) Example: ```python services, total = get_services(db, skip=0, limit=50) print(f"Retrieved {len(services)} of {total} services") ``` """ # Get total count total = db.query(Service).count() # Get paginated results, ordered by created_at descending (newest first) services = ( db.query(Service) .order_by(Service.created_at.desc()) .offset(skip) .limit(limit) .all() ) return services, total def get_service_by_id(db: Session, service_id: UUID) -> Service: """ Retrieve a single service by its ID. Args: db: Database session service_id: UUID of the service to retrieve Returns: Service: The service object Raises: HTTPException: 404 if service not found Example: ```python service = get_service_by_id(db, service_id) print(f"Found service: {service.service_name}") ``` """ service = db.query(Service).filter(Service.id == str(service_id)).first() if not service: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Service with ID {service_id} not found" ) return service def get_services_by_client(db: Session, client_id: str, skip: int = 0, limit: int = 100) -> tuple[list[Service], int]: """ Retrieve services for a specific client (via infrastructure). 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 services, total count) Example: ```python services, total = get_services_by_client(db, client_id) print(f"Client has {total} services") ``` """ # Join with Infrastructure to filter by client_id query = ( db.query(Service) .join(Infrastructure, Service.infrastructure_id == Infrastructure.id) .filter(Infrastructure.client_id == str(client_id)) ) total = query.count() services = ( query .order_by(Service.created_at.desc()) .offset(skip) .limit(limit) .all() ) return services, total def get_services_by_type(db: Session, service_type: str, skip: int = 0, limit: int = 100) -> tuple[list[Service], int]: """ Retrieve services by type. Args: db: Database session service_type: Service type to filter by (e.g., 'git_hosting', 'database') skip: Number of records to skip limit: Maximum number of records to return Returns: tuple: (list of services, total count) Example: ```python services, total = get_services_by_type(db, "database") print(f"Found {total} database services") ``` """ total = db.query(Service).filter(Service.service_type == service_type).count() services = ( db.query(Service) .filter(Service.service_type == service_type) .order_by(Service.created_at.desc()) .offset(skip) .limit(limit) .all() ) return services, total def get_services_by_status(db: Session, status_filter: str, skip: int = 0, limit: int = 100) -> tuple[list[Service], int]: """ Retrieve services by status. Args: db: Database session status_filter: Status to filter by (running, stopped, error, maintenance) skip: Number of records to skip limit: Maximum number of records to return Returns: tuple: (list of services, total count) Example: ```python services, total = get_services_by_status(db, "running") print(f"Found {total} running services") ``` """ total = db.query(Service).filter(Service.status == status_filter).count() services = ( db.query(Service) .filter(Service.status == status_filter) .order_by(Service.created_at.desc()) .offset(skip) .limit(limit) .all() ) return services, total def validate_infrastructure_exists(db: Session, infrastructure_id: str) -> None: """ Validate that infrastructure exists. Args: db: Database session infrastructure_id: Infrastructure UUID to validate Raises: HTTPException: 404 if infrastructure not found Example: ```python validate_infrastructure_exists(db, infrastructure_id) # Continues if infrastructure exists, raises HTTPException if not ``` """ infrastructure = db.query(Infrastructure).filter(Infrastructure.id == str(infrastructure_id)).first() if not infrastructure: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Infrastructure with ID {infrastructure_id} not found" ) def create_service(db: Session, service_data: ServiceCreate) -> Service: """ Create a new service. Args: db: Database session service_data: Service creation data Returns: Service: The created service object Raises: HTTPException: 404 if infrastructure not found HTTPException: 500 if database error occurs Example: ```python service_data = ServiceCreate( infrastructure_id="123e4567-e89b-12d3-a456-426614174000", service_name="Gitea", service_type="git_hosting", status="running" ) service = create_service(db, service_data) print(f"Created service: {service.id}") ``` """ # Validate infrastructure exists if provided if service_data.infrastructure_id: validate_infrastructure_exists(db, service_data.infrastructure_id) try: # Create new service instance db_service = Service(**service_data.model_dump()) # Add to database db.add(db_service) db.commit() db.refresh(db_service) return db_service except IntegrityError as e: db.rollback() # Handle foreign key constraint violations if "infrastructure_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Infrastructure with ID {service_data.infrastructure_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 service: {str(e)}" ) def update_service(db: Session, service_id: UUID, service_data: ServiceUpdate) -> Service: """ Update an existing service. Args: db: Database session service_id: UUID of the service to update service_data: Service update data (only provided fields will be updated) Returns: Service: The updated service object Raises: HTTPException: 404 if service or infrastructure not found HTTPException: 500 if database error occurs Example: ```python update_data = ServiceUpdate( status="stopped", notes="Service temporarily stopped for maintenance" ) service = update_service(db, service_id, update_data) print(f"Updated service: {service.service_name}") ``` """ # Get existing service service = get_service_by_id(db, service_id) try: # Update only provided fields update_data = service_data.model_dump(exclude_unset=True) # If updating infrastructure_id, validate infrastructure exists if "infrastructure_id" in update_data and update_data["infrastructure_id"] and update_data["infrastructure_id"] != service.infrastructure_id: validate_infrastructure_exists(db, update_data["infrastructure_id"]) # Apply updates for field, value in update_data.items(): setattr(service, field, value) db.commit() db.refresh(service) return service except HTTPException: db.rollback() raise except IntegrityError as e: db.rollback() if "infrastructure_id" in str(e.orig): raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Infrastructure 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 service: {str(e)}" ) def delete_service(db: Session, service_id: UUID) -> dict: """ Delete a service by its ID. Args: db: Database session service_id: UUID of the service to delete Returns: dict: Success message Raises: HTTPException: 404 if service not found HTTPException: 500 if database error occurs Example: ```python result = delete_service(db, service_id) print(result["message"]) # "Service deleted successfully" ``` """ # Get existing service (raises 404 if not found) service = get_service_by_id(db, service_id) try: db.delete(service) db.commit() return { "message": "Service deleted successfully", "service_id": str(service_id) } except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete service: {str(e)}" )