añadido los estados de reporte
This commit is contained in:
@@ -1,154 +1,154 @@
|
||||
"""File storage utilities for report images"""
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from fastapi import UploadFile, HTTPException, status
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from core.config import ConfSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageStorageManager:
|
||||
"""Maneja almacenamiento, compresión y eliminación de imágenes de reportes"""
|
||||
|
||||
def __init__(self):
|
||||
self.storage_path = Path(ConfSettings.storage_base_path) / ConfSettings.images_dir
|
||||
self.max_size_bytes = ConfSettings.images_max_size_mb * 1024 * 1024
|
||||
self.allowed_types = ConfSettings.images_allowed_types
|
||||
self.compression_quality = ConfSettings.images_compression_quality
|
||||
|
||||
# Crear directorio si no existe
|
||||
self.storage_path.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"ImageStorageManager initialized with path: {self.storage_path}")
|
||||
|
||||
def validate_and_save_image(self, file: UploadFile, report_id: str) -> str:
|
||||
"""
|
||||
Valida y guarda una imagen, comprimiendo a WebP.
|
||||
|
||||
Args:
|
||||
file: Archivo subido (UploadFile)
|
||||
report_id: ID del reporte para nombrado del archivo
|
||||
|
||||
Returns:
|
||||
Nombre del archivo guardado (sin ruta)
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay error en validación o guardado
|
||||
"""
|
||||
try:
|
||||
# Validar tipo MIME
|
||||
if file.content_type not in self.allowed_types:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Tipo de archivo no permitido. Permitidos: {', '.join(self.allowed_types)}"
|
||||
)
|
||||
|
||||
# Leer contenido
|
||||
content = file.file.read()
|
||||
|
||||
# Validar tamaño
|
||||
if len(content) > self.max_size_bytes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"Archivo demasiado grande. Máximo: {ConfSettings.images_max_size_mb}MB"
|
||||
)
|
||||
|
||||
# Abrir imagen con Pillow
|
||||
try:
|
||||
image = Image.open(BytesIO(content))
|
||||
image.verify() # Verificar que sea una imagen válida
|
||||
|
||||
# Reabrir después de verify() que la cierra
|
||||
image = Image.open(BytesIO(content))
|
||||
|
||||
# Convertir a RGB si tiene canal alpha (RGBA)
|
||||
if image.mode in ('RGBA', 'LA', 'P'):
|
||||
rgb_image = Image.new('RGB', image.size, (255, 255, 255))
|
||||
rgb_image.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
|
||||
image = rgb_image
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating image: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Archivo no es una imagen válida"
|
||||
)
|
||||
|
||||
# Guardar como WebP comprimido
|
||||
filename = f"{report_id}.webp"
|
||||
filepath = self.storage_path / filename
|
||||
|
||||
try:
|
||||
image.save(
|
||||
filepath,
|
||||
"WEBP",
|
||||
quality=self.compression_quality,
|
||||
method=6
|
||||
)
|
||||
logger.info(f"Image saved: {filename} ({len(open(filepath, 'rb').read())} bytes)")
|
||||
return filename
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving image to disk: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error al guardar la imagen"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error processing image: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error procesando la imagen"
|
||||
)
|
||||
|
||||
def get_image_path(self, filename: str) -> Path:
|
||||
"""Obtiene la ruta completa de una imagen"""
|
||||
return self.storage_path / filename
|
||||
|
||||
def image_exists(self, filename: str) -> bool:
|
||||
"""Verifica si una imagen existe"""
|
||||
if not filename:
|
||||
return False
|
||||
filepath = self.get_image_path(filename)
|
||||
return filepath.exists()
|
||||
|
||||
def delete_image(self, filename: str) -> bool:
|
||||
"""
|
||||
Elimina una imagen del almacenamiento.
|
||||
|
||||
Args:
|
||||
filename: Nombre del archivo a eliminar
|
||||
|
||||
Returns:
|
||||
True si se eliminó, False si no existía
|
||||
"""
|
||||
if not filename:
|
||||
return False
|
||||
|
||||
filepath = self.get_image_path(filename)
|
||||
try:
|
||||
if filepath.exists():
|
||||
filepath.unlink() # Eliminar archivo
|
||||
logger.info(f"Image deleted: {filename}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Image not found for deletion: {filename}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting image {filename}: {e}")
|
||||
return False
|
||||
|
||||
def get_image_url(self, filename: str) -> str:
|
||||
"""Genera la URL pública para una imagen"""
|
||||
if not filename:
|
||||
return None
|
||||
return f"/images/{filename}"
|
||||
|
||||
|
||||
# Instancia global
|
||||
image_storage = ImageStorageManager()
|
||||
"""File storage utilities for report images"""
|
||||
import os
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from fastapi import UploadFile, HTTPException, status
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from core.config import ConfSettings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ImageStorageManager:
|
||||
"""Maneja almacenamiento, compresión y eliminación de imágenes de reportes"""
|
||||
|
||||
def __init__(self):
|
||||
self.storage_path = Path(ConfSettings.storage_base_path) / ConfSettings.images_dir
|
||||
self.max_size_bytes = ConfSettings.images_max_size_mb * 1024 * 1024
|
||||
self.allowed_types = ConfSettings.images_allowed_types
|
||||
self.compression_quality = ConfSettings.images_compression_quality
|
||||
|
||||
# Crear directorio si no existe
|
||||
self.storage_path.mkdir(parents=True, exist_ok=True)
|
||||
logger.info(f"ImageStorageManager initialized with path: {self.storage_path}")
|
||||
|
||||
def validate_and_save_image(self, file: UploadFile, report_id: str) -> str:
|
||||
"""
|
||||
Valida y guarda una imagen, comprimiendo a WebP.
|
||||
|
||||
Args:
|
||||
file: Archivo subido (UploadFile)
|
||||
report_id: ID del reporte para nombrado del archivo
|
||||
|
||||
Returns:
|
||||
Nombre del archivo guardado (sin ruta)
|
||||
|
||||
Raises:
|
||||
HTTPException: Si hay error en validación o guardado
|
||||
"""
|
||||
try:
|
||||
# Validar tipo MIME
|
||||
if file.content_type not in self.allowed_types:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f"Tipo de archivo no permitido. Permitidos: {', '.join(self.allowed_types)}"
|
||||
)
|
||||
|
||||
# Leer contenido
|
||||
content = file.file.read()
|
||||
|
||||
# Validar tamaño
|
||||
if len(content) > self.max_size_bytes:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
|
||||
detail=f"Archivo demasiado grande. Máximo: {ConfSettings.images_max_size_mb}MB"
|
||||
)
|
||||
|
||||
# Abrir imagen con Pillow
|
||||
try:
|
||||
image = Image.open(BytesIO(content))
|
||||
image.verify() # Verificar que sea una imagen válida
|
||||
|
||||
# Reabrir después de verify() que la cierra
|
||||
image = Image.open(BytesIO(content))
|
||||
|
||||
# Convertir a RGB si tiene canal alpha (RGBA)
|
||||
if image.mode in ('RGBA', 'LA', 'P'):
|
||||
rgb_image = Image.new('RGB', image.size, (255, 255, 255))
|
||||
rgb_image.paste(image, mask=image.split()[-1] if image.mode == 'RGBA' else None)
|
||||
image = rgb_image
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error validating image: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="Archivo no es una imagen válida"
|
||||
)
|
||||
|
||||
# Guardar como WebP comprimido
|
||||
filename = f"{report_id}.webp"
|
||||
filepath = self.storage_path / filename
|
||||
|
||||
try:
|
||||
image.save(
|
||||
filepath,
|
||||
"WEBP",
|
||||
quality=self.compression_quality,
|
||||
method=6
|
||||
)
|
||||
logger.info(f"Image saved: {filename} ({len(open(filepath, 'rb').read())} bytes)")
|
||||
return filename
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error saving image to disk: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error al guardar la imagen"
|
||||
)
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(f"Unexpected error processing image: {e}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||||
detail="Error procesando la imagen"
|
||||
)
|
||||
|
||||
def get_image_path(self, filename: str) -> Path:
|
||||
"""Obtiene la ruta completa de una imagen"""
|
||||
return self.storage_path / filename
|
||||
|
||||
def image_exists(self, filename: str) -> bool:
|
||||
"""Verifica si una imagen existe"""
|
||||
if not filename:
|
||||
return False
|
||||
filepath = self.get_image_path(filename)
|
||||
return filepath.exists()
|
||||
|
||||
def delete_image(self, filename: str) -> bool:
|
||||
"""
|
||||
Elimina una imagen del almacenamiento.
|
||||
|
||||
Args:
|
||||
filename: Nombre del archivo a eliminar
|
||||
|
||||
Returns:
|
||||
True si se eliminó, False si no existía
|
||||
"""
|
||||
if not filename:
|
||||
return False
|
||||
|
||||
filepath = self.get_image_path(filename)
|
||||
try:
|
||||
if filepath.exists():
|
||||
filepath.unlink() # Eliminar archivo
|
||||
logger.info(f"Image deleted: {filename}")
|
||||
return True
|
||||
else:
|
||||
logger.warning(f"Image not found for deletion: {filename}")
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting image {filename}: {e}")
|
||||
return False
|
||||
|
||||
def get_image_url(self, filename: str) -> str:
|
||||
"""Genera la URL pública para una imagen"""
|
||||
if not filename:
|
||||
return None
|
||||
return f"/images/{filename}"
|
||||
|
||||
|
||||
# Instancia global
|
||||
image_storage = ImageStorageManager()
|
||||
|
||||
@@ -23,6 +23,7 @@ class ReportRepositoryMongo(ReportRepository):
|
||||
"lng": report.lng,
|
||||
"image_filename": report.image_filename,
|
||||
"visibilidad": report.visibilidad,
|
||||
"estado": report.estado,
|
||||
"fecha_creacion": report.fecha_creacion or datetime.utcnow()
|
||||
}
|
||||
result = self.collection.insert_one(report_dict)
|
||||
@@ -59,6 +60,13 @@ class ReportRepositoryMongo(ReportRepository):
|
||||
{"$set": {"visibilidad": new_visibility}}
|
||||
)
|
||||
|
||||
def update_estado(self, report_id: str, new_estado: str) -> None:
|
||||
"""Actualiza el estado de un reporte"""
|
||||
self.collection.update_one(
|
||||
{"id_reporte": report_id},
|
||||
{"$set": {"estado": new_estado}}
|
||||
)
|
||||
|
||||
def delete(self, report_id: str) -> bool:
|
||||
"""Elimina un reporte"""
|
||||
result = self.collection.delete_one({"id_reporte": report_id})
|
||||
@@ -83,5 +91,6 @@ class ReportRepositoryMongo(ReportRepository):
|
||||
lng=doc.get("lng"),
|
||||
image_filename=doc.get("image_filename"),
|
||||
visibilidad=doc.get("visibilidad"),
|
||||
estado=doc.get("estado", "en proceso"),
|
||||
fecha_creacion=doc.get("fecha_creacion")
|
||||
)
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""RabbitMQ adapters for message publishing and consuming"""
|
||||
"""RabbitMQ adapters for message publishing and consuming"""
|
||||
|
||||
@@ -1,58 +1,58 @@
|
||||
import pika
|
||||
import json
|
||||
from typing import Callable, Dict, Any
|
||||
import logging
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RabbitMQConsumer:
|
||||
def __init__(self, queue_name: str, host: str = 'localhost', port: int = 5672):
|
||||
self.queue_name = queue_name
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.callback = None
|
||||
|
||||
def set_callback(self, callback: Callable[[Dict[str, Any]], None]) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def start_consuming(self) -> None:
|
||||
try:
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host=self.host, port=self.port)
|
||||
)
|
||||
channel = connection.channel()
|
||||
channel.queue_declare(queue=self.queue_name, durable=True)
|
||||
|
||||
def callback_wrapper(ch, method, properties, body):
|
||||
try:
|
||||
message = json.loads(body.decode('utf-8'))
|
||||
logger.info(f"Received message from queue '{self.queue_name}': {message}")
|
||||
|
||||
if self.callback:
|
||||
self.callback(message)
|
||||
|
||||
ch.basic_ack(delivery_tag=method.delivery_tag)
|
||||
|
||||
except IntegrityError as e:
|
||||
# Error de negocio: no tiene sentido reintentar
|
||||
logger.warning(f"Business error, discarding message: {e}")
|
||||
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
|
||||
|
||||
except Exception as e:
|
||||
# Error transitorio (red, DB caída): sí puede resolverse solo
|
||||
logger.error(f"Transient error processing message: {e}")
|
||||
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
|
||||
|
||||
channel.basic_consume(
|
||||
queue=self.queue_name,
|
||||
on_message_callback=callback_wrapper,
|
||||
auto_ack=False
|
||||
)
|
||||
|
||||
logger.info(f"[*] Waiting for messages in queue '{self.queue_name}'. Ctrl+C to exit.")
|
||||
channel.start_consuming()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in consumer: {e}")
|
||||
import pika
|
||||
import json
|
||||
from typing import Callable, Dict, Any
|
||||
import logging
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
class RabbitMQConsumer:
|
||||
def __init__(self, queue_name: str, host: str = 'localhost', port: int = 5672):
|
||||
self.queue_name = queue_name
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.callback = None
|
||||
|
||||
def set_callback(self, callback: Callable[[Dict[str, Any]], None]) -> None:
|
||||
self.callback = callback
|
||||
|
||||
def start_consuming(self) -> None:
|
||||
try:
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host=self.host, port=self.port)
|
||||
)
|
||||
channel = connection.channel()
|
||||
channel.queue_declare(queue=self.queue_name, durable=True)
|
||||
|
||||
def callback_wrapper(ch, method, properties, body):
|
||||
try:
|
||||
message = json.loads(body.decode('utf-8'))
|
||||
logger.info(f"Received message from queue '{self.queue_name}': {message}")
|
||||
|
||||
if self.callback:
|
||||
self.callback(message)
|
||||
|
||||
ch.basic_ack(delivery_tag=method.delivery_tag)
|
||||
|
||||
except IntegrityError as e:
|
||||
# Error de negocio: no tiene sentido reintentar
|
||||
logger.warning(f"Business error, discarding message: {e}")
|
||||
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=False)
|
||||
|
||||
except Exception as e:
|
||||
# Error transitorio (red, DB caída): sí puede resolverse solo
|
||||
logger.error(f"Transient error processing message: {e}")
|
||||
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
|
||||
|
||||
channel.basic_consume(
|
||||
queue=self.queue_name,
|
||||
on_message_callback=callback_wrapper,
|
||||
auto_ack=False
|
||||
)
|
||||
|
||||
logger.info(f"[*] Waiting for messages in queue '{self.queue_name}'. Ctrl+C to exit.")
|
||||
channel.start_consuming()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error in consumer: {e}")
|
||||
raise
|
||||
@@ -1,85 +1,86 @@
|
||||
"""Message schemas for RabbitMQ communication"""
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
|
||||
class UserEventType(str, Enum):
|
||||
"""Types of user events"""
|
||||
CREATE = "user.create"
|
||||
UPDATE = "user.update"
|
||||
DELETE = "user.delete"
|
||||
|
||||
|
||||
class ReportEventType(str, Enum):
|
||||
"""Types of report events"""
|
||||
CREATE = "report.create"
|
||||
UPDATE_VISIBILITY = "report.update_visibility"
|
||||
DELETE = "report.delete"
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserMessage:
|
||||
"""Message for user events"""
|
||||
event_type: UserEventType
|
||||
user_id: Optional[int] = None
|
||||
nombre: Optional[str] = None
|
||||
apellido: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
fecha_nacimiento: Optional[str] = None # ISO format datetime string
|
||||
fecha_creacion: Optional[str] = None # ISO format datetime string
|
||||
calificacion: Optional[float] = None
|
||||
numero_reportes: Optional[int] = None
|
||||
url_foto_perfil: Optional[str] = None
|
||||
biografia: Optional[str] = None
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary"""
|
||||
data = asdict(self)
|
||||
data['event_type'] = self.event_type.value
|
||||
return data
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert to JSON string"""
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> 'UserMessage':
|
||||
"""Create from dictionary"""
|
||||
data['event_type'] = UserEventType(data['event_type'])
|
||||
return UserMessage(**data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReportMessage:
|
||||
"""Message for report events"""
|
||||
event_type: ReportEventType
|
||||
id_reporte: Optional[str] = None
|
||||
id_usuario: Optional[int] = None
|
||||
tipo_reporte: Optional[int] = None
|
||||
descripcion: Optional[str] = None
|
||||
ubicacion: Optional[str] = None
|
||||
lat: Optional[float] = None
|
||||
lng: Optional[float] = None
|
||||
image_filename: Optional[str] = None # Nombre del archivo de imagen guardado
|
||||
visibilidad: Optional[float] = None
|
||||
fecha_creacion: Optional[str] = None # ISO format datetime string
|
||||
penalize_author: Optional[bool] = None # For update_visibility event
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary"""
|
||||
data = asdict(self)
|
||||
data['event_type'] = self.event_type.value
|
||||
return data
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert to JSON string"""
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> 'ReportMessage':
|
||||
"""Create from dictionary"""
|
||||
data['event_type'] = ReportEventType(data['event_type'])
|
||||
return ReportMessage(**data)
|
||||
"""Message schemas for RabbitMQ communication"""
|
||||
from dataclasses import dataclass, asdict
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from enum import Enum
|
||||
import json
|
||||
|
||||
|
||||
class UserEventType(str, Enum):
|
||||
"""Types of user events"""
|
||||
CREATE = "user.create"
|
||||
UPDATE = "user.update"
|
||||
DELETE = "user.delete"
|
||||
|
||||
|
||||
class ReportEventType(str, Enum):
|
||||
"""Types of report events"""
|
||||
CREATE = "report.create"
|
||||
UPDATE_VISIBILITY = "report.update_visibility"
|
||||
DELETE = "report.delete"
|
||||
|
||||
|
||||
@dataclass
|
||||
class UserMessage:
|
||||
"""Message for user events"""
|
||||
event_type: UserEventType
|
||||
user_id: Optional[int] = None
|
||||
nombre: Optional[str] = None
|
||||
apellido: Optional[str] = None
|
||||
email: Optional[str] = None
|
||||
fecha_nacimiento: Optional[str] = None # ISO format datetime string
|
||||
fecha_creacion: Optional[str] = None # ISO format datetime string
|
||||
calificacion: Optional[float] = None
|
||||
numero_reportes: Optional[int] = None
|
||||
url_foto_perfil: Optional[str] = None
|
||||
biografia: Optional[str] = None
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary"""
|
||||
data = asdict(self)
|
||||
data['event_type'] = self.event_type.value
|
||||
return data
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert to JSON string"""
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> 'UserMessage':
|
||||
"""Create from dictionary"""
|
||||
data['event_type'] = UserEventType(data['event_type'])
|
||||
return UserMessage(**data)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReportMessage:
|
||||
"""Message for report events"""
|
||||
event_type: ReportEventType
|
||||
id_reporte: Optional[str] = None
|
||||
id_usuario: Optional[int] = None
|
||||
tipo_reporte: Optional[int] = None
|
||||
descripcion: Optional[str] = None
|
||||
ubicacion: Optional[str] = None
|
||||
lat: Optional[float] = None
|
||||
lng: Optional[float] = None
|
||||
image_filename: Optional[str] = None # Nombre del archivo de imagen guardado
|
||||
visibilidad: Optional[float] = None
|
||||
estado: Optional[str] = None # Estado del reporte: "en proceso", "no resuelto", "resuelto"
|
||||
fecha_creacion: Optional[str] = None # ISO format datetime string
|
||||
penalize_author: Optional[bool] = None # For update_visibility event
|
||||
|
||||
def to_dict(self):
|
||||
"""Convert to dictionary"""
|
||||
data = asdict(self)
|
||||
data['event_type'] = self.event_type.value
|
||||
return data
|
||||
|
||||
def to_json(self) -> str:
|
||||
"""Convert to JSON string"""
|
||||
return json.dumps(self.to_dict())
|
||||
|
||||
@staticmethod
|
||||
def from_dict(data: dict) -> 'ReportMessage':
|
||||
"""Create from dictionary"""
|
||||
data['event_type'] = ReportEventType(data['event_type'])
|
||||
return ReportMessage(**data)
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
"""RabbitMQ message sender"""
|
||||
import pika
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RabbitMQSender:
|
||||
"""Generic RabbitMQ sender for publishing messages to queues"""
|
||||
|
||||
def __init__(self, host: str = 'localhost', port: int = 5672):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def send_message(self, queue_name: str, message: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Sends a message to a RabbitMQ queue
|
||||
|
||||
Args:
|
||||
queue_name: Name of the queue to send to
|
||||
message: Dictionary containing the message data
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host=self.host, port=self.port)
|
||||
)
|
||||
channel = connection.channel()
|
||||
|
||||
# Declare queue to ensure it exists
|
||||
channel.queue_declare(queue=queue_name, durable=True)
|
||||
|
||||
# Convert message to JSON
|
||||
message_json = json.dumps(message)
|
||||
|
||||
# Publish the message
|
||||
channel.basic_publish(
|
||||
exchange='',
|
||||
routing_key=queue_name,
|
||||
body=message_json,
|
||||
properties=pika.BasicProperties(
|
||||
delivery_mode=pika.spec.PERSISTENT_DELIVERY_MODE
|
||||
)
|
||||
)
|
||||
|
||||
connection.close()
|
||||
logger.info(f"Message sent to queue '{queue_name}': {message_json}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message to RabbitMQ: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def send_to_queue(queue_name: str, message: Dict[str, Any],
|
||||
host: str = 'localhost', port: int = 5672) -> bool:
|
||||
"""
|
||||
Convenience function to send a message to RabbitMQ
|
||||
|
||||
Args:
|
||||
queue_name: Name of the queue
|
||||
message: Message dictionary
|
||||
host: RabbitMQ host
|
||||
port: RabbitMQ port
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
sender = RabbitMQSender(host=host, port=port)
|
||||
return sender.send_message(queue_name, message)
|
||||
"""RabbitMQ message sender"""
|
||||
import pika
|
||||
import json
|
||||
from typing import Any, Dict
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RabbitMQSender:
|
||||
"""Generic RabbitMQ sender for publishing messages to queues"""
|
||||
|
||||
def __init__(self, host: str = 'localhost', port: int = 5672):
|
||||
self.host = host
|
||||
self.port = port
|
||||
|
||||
def send_message(self, queue_name: str, message: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Sends a message to a RabbitMQ queue
|
||||
|
||||
Args:
|
||||
queue_name: Name of the queue to send to
|
||||
message: Dictionary containing the message data
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
try:
|
||||
connection = pika.BlockingConnection(
|
||||
pika.ConnectionParameters(host=self.host, port=self.port)
|
||||
)
|
||||
channel = connection.channel()
|
||||
|
||||
# Declare queue to ensure it exists
|
||||
channel.queue_declare(queue=queue_name, durable=True)
|
||||
|
||||
# Convert message to JSON
|
||||
message_json = json.dumps(message)
|
||||
|
||||
# Publish the message
|
||||
channel.basic_publish(
|
||||
exchange='',
|
||||
routing_key=queue_name,
|
||||
body=message_json,
|
||||
properties=pika.BasicProperties(
|
||||
delivery_mode=pika.spec.PERSISTENT_DELIVERY_MODE
|
||||
)
|
||||
)
|
||||
|
||||
connection.close()
|
||||
logger.info(f"Message sent to queue '{queue_name}': {message_json}")
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error sending message to RabbitMQ: {e}")
|
||||
return False
|
||||
|
||||
|
||||
def send_to_queue(queue_name: str, message: Dict[str, Any],
|
||||
host: str = 'localhost', port: int = 5672) -> bool:
|
||||
"""
|
||||
Convenience function to send a message to RabbitMQ
|
||||
|
||||
Args:
|
||||
queue_name: Name of the queue
|
||||
message: Message dictionary
|
||||
host: RabbitMQ host
|
||||
port: RabbitMQ port
|
||||
|
||||
Returns:
|
||||
True if successful, False otherwise
|
||||
"""
|
||||
sender = RabbitMQSender(host=host, port=port)
|
||||
return sender.send_message(queue_name, message)
|
||||
|
||||
Reference in New Issue
Block a user