""" Machine service layer for business logic and database operations. This module handles all database operations for machines, 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.machine import Machine from api.schemas.machine import MachineCreate, MachineUpdate def get_machines(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[Machine], int]: """ Retrieve a paginated list of machines. Args: db: Database session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of machines, total count) Example: ```python machines, total = get_machines(db, skip=0, limit=50) print(f"Retrieved {len(machines)} of {total} machines") ``` """ # Get total count total = db.query(Machine).count() # Get paginated results, ordered by created_at descending (newest first) machines = ( db.query(Machine) .order_by(Machine.created_at.desc()) .offset(skip) .limit(limit) .all() ) return machines, total def get_machine_by_id(db: Session, machine_id: UUID) -> Machine: """ Retrieve a single machine by its ID. Args: db: Database session machine_id: UUID of the machine to retrieve Returns: Machine: The machine object Raises: HTTPException: 404 if machine not found Example: ```python machine = get_machine_by_id(db, machine_id) print(f"Found machine: {machine.hostname}") ``` """ machine = db.query(Machine).filter(Machine.id == str(machine_id)).first() if not machine: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail=f"Machine with ID {machine_id} not found" ) return machine def get_machine_by_hostname(db: Session, hostname: str) -> Optional[Machine]: """ Retrieve a machine by its hostname. Args: db: Database session hostname: Hostname to search for Returns: Optional[Machine]: The machine if found, None otherwise Example: ```python machine = get_machine_by_hostname(db, "laptop-dev-01") if machine: print(f"Found machine: {machine.friendly_name}") ``` """ return db.query(Machine).filter(Machine.hostname == hostname).first() def create_machine(db: Session, machine_data: MachineCreate) -> Machine: """ Create a new machine. Args: db: Database session machine_data: Machine creation data Returns: Machine: The created machine object Raises: HTTPException: 409 if machine with hostname already exists HTTPException: 500 if database error occurs Example: ```python machine_data = MachineCreate( hostname="laptop-dev-01", friendly_name="Development Laptop", platform="win32" ) machine = create_machine(db, machine_data) print(f"Created machine: {machine.id}") ``` """ # Check if machine with hostname already exists existing_machine = get_machine_by_hostname(db, machine_data.hostname) if existing_machine: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Machine with hostname '{machine_data.hostname}' already exists" ) try: # Create new machine instance db_machine = Machine(**machine_data.model_dump()) # Add to database db.add(db_machine) db.commit() db.refresh(db_machine) return db_machine except IntegrityError as e: db.rollback() # Handle unique constraint violations if "hostname" in str(e.orig): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Machine with hostname '{machine_data.hostname}' already exists" ) elif "machine_fingerprint" in str(e.orig): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Machine with this fingerprint 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 machine: {str(e)}" ) def update_machine(db: Session, machine_id: UUID, machine_data: MachineUpdate) -> Machine: """ Update an existing machine. Args: db: Database session machine_id: UUID of the machine to update machine_data: Machine update data (only provided fields will be updated) Returns: Machine: The updated machine object Raises: HTTPException: 404 if machine not found HTTPException: 409 if update would violate unique constraints HTTPException: 500 if database error occurs Example: ```python update_data = MachineUpdate( friendly_name="Updated Laptop Name", is_active=False ) machine = update_machine(db, machine_id, update_data) print(f"Updated machine: {machine.friendly_name}") ``` """ # Get existing machine machine = get_machine_by_id(db, machine_id) try: # Update only provided fields update_data = machine_data.model_dump(exclude_unset=True) # If updating hostname, check if new hostname is already taken if "hostname" in update_data and update_data["hostname"] != machine.hostname: existing = get_machine_by_hostname(db, update_data["hostname"]) if existing: raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail=f"Machine with hostname '{update_data['hostname']}' already exists" ) # Apply updates for field, value in update_data.items(): setattr(machine, field, value) db.commit() db.refresh(machine) return machine except HTTPException: db.rollback() raise except IntegrityError as e: db.rollback() if "hostname" in str(e.orig): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Machine with this hostname already exists" ) elif "machine_fingerprint" in str(e.orig): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail="Machine with this fingerprint 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 update machine: {str(e)}" ) def delete_machine(db: Session, machine_id: UUID) -> dict: """ Delete a machine by its ID. Args: db: Database session machine_id: UUID of the machine to delete Returns: dict: Success message Raises: HTTPException: 404 if machine not found HTTPException: 500 if database error occurs Example: ```python result = delete_machine(db, machine_id) print(result["message"]) # "Machine deleted successfully" ``` """ # Get existing machine (raises 404 if not found) machine = get_machine_by_id(db, machine_id) try: db.delete(machine) db.commit() return { "message": "Machine deleted successfully", "machine_id": str(machine_id) } except Exception as e: db.rollback() raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Failed to delete machine: {str(e)}" ) def get_active_machines(db: Session, skip: int = 0, limit: int = 100) -> tuple[list[Machine], int]: """ Retrieve a paginated list of active machines only. Args: db: Database session skip: Number of records to skip (for pagination) limit: Maximum number of records to return Returns: tuple: (list of active machines, total count) Example: ```python machines, total = get_active_machines(db, skip=0, limit=50) print(f"Retrieved {len(machines)} of {total} active machines") ``` """ # Get total count of active machines total = db.query(Machine).filter(Machine.is_active == True).count() # Get paginated results machines = ( db.query(Machine) .filter(Machine.is_active == True) .order_by(Machine.created_at.desc()) .offset(skip) .limit(limit) .all() ) return machines, total def get_primary_machine(db: Session) -> Optional[Machine]: """ Retrieve the primary machine. Args: db: Database session Returns: Optional[Machine]: The primary machine if one exists, None otherwise Example: ```python primary = get_primary_machine(db) if primary: print(f"Primary machine: {primary.hostname}") ``` """ return db.query(Machine).filter(Machine.is_primary == True).first()