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.
This commit is contained in:
280
backend/app/core/exceptions.py
Normal file
280
backend/app/core/exceptions.py
Normal file
@@ -0,0 +1,280 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user