Source code for oshconnect.events.handler
# =============================================================================
# Copyright (c) 2025 Botts Innovative Research Inc.
# Date: 2025/10/6
# Author: Ian Patterson
# Contact Email: ian@botts-inc.com
# =============================================================================
from __future__ import annotations
import logging
from collections import deque
from typing import Callable
from .core import DefaultEventTypes, Event
from .listeners import CallbackListener, IEventListener
[docs]
class EventHandler(object):
"""
Singleton event bus. Manages listener registration and event dispatch.
Listeners are filtered by type and topic before dispatch — a listener only
receives events whose type is in ``listener.types`` (empty = all types) AND
whose topic is in ``listener.topics`` (empty = all topics).
Usage — functional style (no subclassing)::
handler = EventHandler()
def on_obs(event: Event):
print(event.data)
listener = handler.subscribe(on_obs, types=[DefaultEventTypes.NEW_OBSERVATION])
# later: handler.unregister_listener(listener)
Usage — subclass style::
class MyListener(IEventListener):
def handle_events(self, event: Event):
...
handler.register_listener(MyListener(types=[DefaultEventTypes.ADD_SYSTEM]))
"""
listeners: list[IEventListener] = []
to_add: list[IEventListener] = []
to_remove: list[IEventListener] = []
event_queue: deque[Event] = deque()
publish_lock: bool = False
def __new__(cls):
if not hasattr(cls, "instance"):
cls.instance = super(EventHandler, cls).__new__(cls)
return cls.instance
# ------------------------------------------------------------------
# Registration
# ------------------------------------------------------------------
[docs]
def register_listener(self, listener: IEventListener):
if listener not in self.listeners:
if not self.publish_lock:
self.listeners.append(listener)
else:
self.to_add.append(listener)
[docs]
def unregister_listener(self, listener: IEventListener):
if not self.publish_lock:
if listener in self.listeners:
self.listeners.remove(listener)
else:
self.to_remove.append(listener)
[docs]
def subscribe(
self,
callback: Callable[[Event], None],
types: list[DefaultEventTypes] = None,
topics: list[str] = None,
) -> CallbackListener:
"""
Register a plain callable as a listener.
:param callback: Function to call when a matching event is published.
:param types: Event types to filter on. ``None`` / empty = all types.
:param topics: MQTT/event topics to filter on. ``None`` / empty = all topics.
:returns: The ``CallbackListener`` — keep a reference to unregister later.
"""
listener = CallbackListener(
topics=topics or [],
types=types or [],
callback=callback,
)
self.register_listener(listener)
return listener
# ------------------------------------------------------------------
# Publishing
# ------------------------------------------------------------------
def _matches(self, listener: IEventListener, evt: Event) -> bool:
"""Return True if *evt* passes the listener's type and topic filters."""
type_match = not listener.types or evt.type in listener.types
topic_match = not listener.topics or evt.topic in listener.topics
return type_match and topic_match
[docs]
def publish(self, evt: Event):
if self.publish_lock:
self.event_queue.append(evt)
return
self.publish_lock = True
try:
for listener in self.listeners:
if self._matches(listener, evt):
try:
listener.handle_events(evt)
except Exception as e:
logging.error("Error in event listener %s: %s", listener, e)
finally:
self.publish_lock = False
self.commit_changes()
# ------------------------------------------------------------------
# Deferred add/remove bookkeeping
# ------------------------------------------------------------------
[docs]
def commit_changes(self):
self.commit_removes()
self.commit_adds()
while self.event_queue:
self.publish(self.event_queue.popleft())
[docs]
def commit_adds(self):
for listener in self.to_add:
self.listeners.append(listener)
self.to_add.clear()
[docs]
def commit_removes(self):
for listener in self.to_remove:
if listener in self.listeners:
self.listeners.remove(listener)
self.to_remove.clear()
# ------------------------------------------------------------------
# Utilities
# ------------------------------------------------------------------
[docs]
def clear_listeners(self):
self.listeners.clear()
self.to_add.clear()
self.to_remove.clear()
[docs]
def get_num_listeners(self) -> int:
return len(self.listeners)