Dieser Commit ist enthalten in:
Claude Project Manager
2025-07-05 17:51:16 +02:00
Commit 0d7d888502
1594 geänderte Dateien mit 122839 neuen und 0 gelöschten Zeilen

Datei anzeigen

@ -0,0 +1 @@
# Events Module

Datei anzeigen

@ -0,0 +1,191 @@
import json
import logging
from typing import Dict, Any, Callable, List
from datetime import datetime
import pika
from pika.exceptions import AMQPConnectionError
import threading
from collections import defaultdict
logger = logging.getLogger(__name__)
class Event:
"""Base event class"""
def __init__(self, event_type: str, data: Dict[str, Any], source: str = "unknown"):
self.id = self._generate_id()
self.type = event_type
self.data = data
self.source = source
self.timestamp = datetime.utcnow().isoformat()
def _generate_id(self) -> str:
import uuid
return str(uuid.uuid4())
def to_dict(self) -> Dict[str, Any]:
return {
"id": self.id,
"type": self.type,
"data": self.data,
"source": self.source,
"timestamp": self.timestamp
}
def to_json(self) -> str:
return json.dumps(self.to_dict())
class EventBus:
"""Event bus for pub/sub pattern with RabbitMQ backend"""
def __init__(self, rabbitmq_url: str):
self.rabbitmq_url = rabbitmq_url
self.connection = None
self.channel = None
self.exchange_name = "license_events"
self.local_handlers: Dict[str, List[Callable]] = defaultdict(list)
self._connect()
def _connect(self):
"""Establish connection to RabbitMQ"""
try:
parameters = pika.URLParameters(self.rabbitmq_url)
self.connection = pika.BlockingConnection(parameters)
self.channel = self.connection.channel()
# Declare exchange
self.channel.exchange_declare(
exchange=self.exchange_name,
exchange_type='topic',
durable=True
)
logger.info("Connected to RabbitMQ")
except AMQPConnectionError as e:
logger.error(f"Failed to connect to RabbitMQ: {e}")
# Fallback to local-only event handling
self.connection = None
self.channel = None
def publish(self, event: Event):
"""Publish an event"""
try:
# Publish to RabbitMQ if connected
if self.channel and not self.channel.is_closed:
self.channel.basic_publish(
exchange=self.exchange_name,
routing_key=event.type,
body=event.to_json(),
properties=pika.BasicProperties(
delivery_mode=2, # Make message persistent
content_type='application/json'
)
)
logger.debug(f"Published event: {event.type}")
# Also handle local subscribers
self._handle_local_event(event)
except Exception as e:
logger.error(f"Error publishing event: {e}")
# Ensure local handlers still get called
self._handle_local_event(event)
def subscribe(self, event_type: str, handler: Callable):
"""Subscribe to an event type locally"""
self.local_handlers[event_type].append(handler)
logger.debug(f"Subscribed to {event_type}")
def subscribe_queue(self, event_types: List[str], queue_name: str, handler: Callable):
"""Subscribe to events via RabbitMQ queue"""
if not self.channel:
logger.warning("RabbitMQ not connected, falling back to local subscription")
for event_type in event_types:
self.subscribe(event_type, handler)
return
try:
# Declare queue
self.channel.queue_declare(queue=queue_name, durable=True)
# Bind queue to exchange for each event type
for event_type in event_types:
self.channel.queue_bind(
exchange=self.exchange_name,
queue=queue_name,
routing_key=event_type
)
# Set up consumer
def callback(ch, method, properties, body):
try:
event_data = json.loads(body)
event = Event(
event_type=event_data['type'],
data=event_data['data'],
source=event_data['source']
)
handler(event)
ch.basic_ack(delivery_tag=method.delivery_tag)
except Exception as e:
logger.error(f"Error handling event: {e}")
ch.basic_nack(delivery_tag=method.delivery_tag, requeue=True)
self.channel.basic_consume(queue=queue_name, on_message_callback=callback)
# Start consuming in a separate thread
consumer_thread = threading.Thread(target=self.channel.start_consuming)
consumer_thread.daemon = True
consumer_thread.start()
logger.info(f"Started consuming from queue: {queue_name}")
except Exception as e:
logger.error(f"Error setting up queue subscription: {e}")
def _handle_local_event(self, event: Event):
"""Handle event with local subscribers"""
handlers = self.local_handlers.get(event.type, [])
for handler in handlers:
try:
handler(event)
except Exception as e:
logger.error(f"Error in event handler: {e}")
def close(self):
"""Close RabbitMQ connection"""
if self.connection and not self.connection.is_closed:
self.connection.close()
logger.info("Closed RabbitMQ connection")
# Event types
class EventTypes:
"""Centralized event type definitions"""
# License events
LICENSE_VALIDATED = "license.validated"
LICENSE_VALIDATION_FAILED = "license.validation.failed"
LICENSE_ACTIVATED = "license.activated"
LICENSE_DEACTIVATED = "license.deactivated"
LICENSE_TRANSFERRED = "license.transferred"
LICENSE_EXPIRED = "license.expired"
LICENSE_CREATED = "license.created"
LICENSE_UPDATED = "license.updated"
# Device events
DEVICE_ADDED = "device.added"
DEVICE_REMOVED = "device.removed"
DEVICE_BLOCKED = "device.blocked"
DEVICE_DEACTIVATED = "device.deactivated"
# Anomaly events
ANOMALY_DETECTED = "anomaly.detected"
ANOMALY_RESOLVED = "anomaly.resolved"
# Session events
SESSION_STARTED = "session.started"
SESSION_ENDED = "session.ended"
SESSION_EXPIRED = "session.expired"
# System events
RATE_LIMIT_EXCEEDED = "system.rate_limit_exceeded"
API_ERROR = "system.api_error"