Files
ATLAS/backend/app/core/exceptions.py
FlotillasGPS Developer 51d78bacf4 FlotillasGPS - Sistema completo de monitoreo de flotillas GPS
Sistema completo para monitoreo y gestion de flotas de vehiculos con:
- Backend FastAPI con PostgreSQL/TimescaleDB
- Frontend React con TypeScript y TailwindCSS
- App movil React Native con Expo
- Soporte para dispositivos GPS, Meshtastic y celulares
- Video streaming en vivo con MediaMTX
- Geocercas, alertas, viajes y reportes
- Autenticacion JWT y WebSockets en tiempo real

Documentacion completa y guias de usuario incluidas.
2026-01-21 08:18:00 +00:00

281 lines
8.2 KiB
Python

"""
Excepciones personalizadas para la aplicación.
Define excepciones específicas del dominio y handlers
para convertirlas en respuestas HTTP apropiadas.
"""
from typing import Any, Dict, Optional
from fastapi import HTTPException, Request, status
from fastapi.responses import JSONResponse
class AdanException(Exception):
"""Excepción base para todas las excepciones de la aplicación."""
def __init__(
self,
message: str,
code: str = "ADAN_ERROR",
details: Optional[Dict[str, Any]] = None,
):
self.message = message
self.code = code
self.details = details or {}
super().__init__(self.message)
class NotFoundError(AdanException):
"""Recurso no encontrado."""
def __init__(
self,
resource: str,
identifier: Any = None,
details: Optional[Dict[str, Any]] = None,
):
message = f"{resource} no encontrado"
if identifier:
message = f"{resource} con id '{identifier}' no encontrado"
super().__init__(message, "NOT_FOUND", details)
self.resource = resource
self.identifier = identifier
class AlreadyExistsError(AdanException):
"""El recurso ya existe."""
def __init__(
self,
resource: str,
field: str,
value: Any,
details: Optional[Dict[str, Any]] = None,
):
message = f"{resource} con {field}='{value}' ya existe"
super().__init__(message, "ALREADY_EXISTS", details)
self.resource = resource
self.field = field
self.value = value
class ValidationError(AdanException):
"""Error de validación de datos."""
def __init__(
self,
message: str,
field: Optional[str] = None,
details: Optional[Dict[str, Any]] = None,
):
super().__init__(message, "VALIDATION_ERROR", details)
self.field = field
class AuthenticationError(AdanException):
"""Error de autenticación."""
def __init__(
self,
message: str = "Credenciales inválidas",
details: Optional[Dict[str, Any]] = None,
):
super().__init__(message, "AUTHENTICATION_ERROR", details)
class AuthorizationError(AdanException):
"""Error de autorización (permisos insuficientes)."""
def __init__(
self,
message: str = "No tiene permisos para realizar esta acción",
details: Optional[Dict[str, Any]] = None,
):
super().__init__(message, "AUTHORIZATION_ERROR", details)
class ExternalServiceError(AdanException):
"""Error al comunicarse con un servicio externo."""
def __init__(
self,
service: str,
message: str,
details: Optional[Dict[str, Any]] = None,
):
full_message = f"Error en servicio {service}: {message}"
super().__init__(full_message, "EXTERNAL_SERVICE_ERROR", details)
self.service = service
class GeocercaViolationError(AdanException):
"""Violación de geocerca detectada."""
def __init__(
self,
geocerca_id: int,
geocerca_nombre: str,
tipo_violacion: str, # 'entrada' o 'salida'
vehiculo_id: int,
details: Optional[Dict[str, Any]] = None,
):
message = f"Vehículo {vehiculo_id} {tipo_violacion} de geocerca '{geocerca_nombre}'"
super().__init__(message, "GEOCERCA_VIOLATION", details)
self.geocerca_id = geocerca_id
self.geocerca_nombre = geocerca_nombre
self.tipo_violacion = tipo_violacion
self.vehiculo_id = vehiculo_id
class SpeedLimitExceededError(AdanException):
"""Límite de velocidad excedido."""
def __init__(
self,
vehiculo_id: int,
velocidad: float,
limite: float,
details: Optional[Dict[str, Any]] = None,
):
message = f"Vehículo {vehiculo_id} excedió límite de velocidad: {velocidad} km/h (límite: {limite} km/h)"
super().__init__(message, "SPEED_LIMIT_EXCEEDED", details)
self.vehiculo_id = vehiculo_id
self.velocidad = velocidad
self.limite = limite
class DeviceConnectionError(AdanException):
"""Error de conexión con dispositivo."""
def __init__(
self,
dispositivo_id: int,
message: str,
details: Optional[Dict[str, Any]] = None,
):
full_message = f"Error de conexión con dispositivo {dispositivo_id}: {message}"
super().__init__(full_message, "DEVICE_CONNECTION_ERROR", details)
self.dispositivo_id = dispositivo_id
class VideoStreamError(AdanException):
"""Error con stream de video."""
def __init__(
self,
camara_id: int,
message: str,
details: Optional[Dict[str, Any]] = None,
):
full_message = f"Error de video en cámara {camara_id}: {message}"
super().__init__(full_message, "VIDEO_STREAM_ERROR", details)
self.camara_id = camara_id
class MaintenanceRequiredError(AdanException):
"""Mantenimiento requerido para el vehículo."""
def __init__(
self,
vehiculo_id: int,
tipo_mantenimiento: str,
details: Optional[Dict[str, Any]] = None,
):
message = f"Vehículo {vehiculo_id} requiere mantenimiento: {tipo_mantenimiento}"
super().__init__(message, "MAINTENANCE_REQUIRED", details)
self.vehiculo_id = vehiculo_id
self.tipo_mantenimiento = tipo_mantenimiento
class DatabaseError(AdanException):
"""Error de base de datos."""
def __init__(
self,
operation: str,
message: str,
details: Optional[Dict[str, Any]] = None,
):
full_message = f"Error de base de datos en {operation}: {message}"
super().__init__(full_message, "DATABASE_ERROR", details)
self.operation = operation
# ============================================================================
# Exception Handlers para FastAPI
# ============================================================================
async def adan_exception_handler(request: Request, exc: AdanException) -> JSONResponse:
"""Handler para excepciones base de Adan."""
status_code = status.HTTP_500_INTERNAL_SERVER_ERROR
if isinstance(exc, NotFoundError):
status_code = status.HTTP_404_NOT_FOUND
elif isinstance(exc, AlreadyExistsError):
status_code = status.HTTP_409_CONFLICT
elif isinstance(exc, ValidationError):
status_code = status.HTTP_422_UNPROCESSABLE_ENTITY
elif isinstance(exc, AuthenticationError):
status_code = status.HTTP_401_UNAUTHORIZED
elif isinstance(exc, AuthorizationError):
status_code = status.HTTP_403_FORBIDDEN
elif isinstance(exc, ExternalServiceError):
status_code = status.HTTP_502_BAD_GATEWAY
return JSONResponse(
status_code=status_code,
content={
"error": {
"code": exc.code,
"message": exc.message,
"details": exc.details,
}
},
)
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
"""Handler para HTTPException estándar de FastAPI."""
return JSONResponse(
status_code=exc.status_code,
content={
"error": {
"code": "HTTP_ERROR",
"message": exc.detail,
"details": {},
}
},
)
async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse:
"""Handler para excepciones no capturadas."""
# En producción, loguear el error completo pero no exponerlo al cliente
import traceback
traceback.print_exc()
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={
"error": {
"code": "INTERNAL_ERROR",
"message": "Error interno del servidor",
"details": {},
}
},
)
def register_exception_handlers(app) -> None:
"""
Registra los handlers de excepciones en la aplicación FastAPI.
Args:
app: Instancia de FastAPI.
"""
app.add_exception_handler(AdanException, adan_exception_handler)
app.add_exception_handler(HTTPException, http_exception_handler)
app.add_exception_handler(Exception, general_exception_handler)