"""Module for managing callback utility functions.""" import logging from collections.abc import Callable from typing import Generic, TypeVar _LOGGER = logging.getLogger(__name__) K = TypeVar("K") V = TypeVar("V") def safe_callback( callback: Callable[[V], None], logger: logging.Logger | logging.LoggerAdapter | None = None ) -> Callable[[V], None]: """Wrap a callback to catch and log exceptions. This is useful for ensuring that errors in callbacks do not propagate and cause unexpected behavior. Any failures during callback execution will be logged. """ if logger is None: logger = _LOGGER def wrapper(value: V) -> None: try: callback(value) except Exception as ex: # noqa: BLE001 logger.error("Uncaught error in callback '%s': %s", getattr(callback, "__name__", "Unknown"), ex) return wrapper class CallbackMap(Generic[K, V]): """A mapping of callbacks for specific keys. This allows for registering multiple callbacks for different keys and invoking them when a value is received for a specific key. """ def __init__(self, logger: logging.Logger | logging.LoggerAdapter | None = None) -> None: self._callbacks: dict[K, list[Callable[[V], None]]] = {} self._logger = logger or _LOGGER def keys(self) -> list[K]: """Get all keys in the callback map.""" return list(self._callbacks.keys()) def add_callback(self, key: K, callback: Callable[[V], None]) -> Callable[[], None]: """Add a callback for a specific key. Any failures during callback execution will be logged. Returns a callable that can be used to remove the callback. """ self._callbacks.setdefault(key, []).append(callback) def remove_callback() -> None: """Remove the callback for the specific key.""" if cb_list := self._callbacks.get(key): cb_list.remove(callback) if not cb_list: del self._callbacks[key] return remove_callback def get_callbacks(self, key: K) -> list[Callable[[V], None]]: """Get all callbacks for a specific key.""" return self._callbacks.get(key, []) def __call__(self, key: K, value: V) -> None: """Invoke all callbacks for a specific key.""" for callback in self.get_callbacks(key): safe_callback(callback, self._logger)(value) class CallbackList(Generic[V]): """A list of callbacks that can be invoked. This combines a list of callbacks into a single callable. Callers can add additional callbacks to the list at any time. """ def __init__(self, logger: logging.Logger | logging.LoggerAdapter | None = None) -> None: self._callbacks: list[Callable[[V], None]] = [] self._logger = logger or _LOGGER def add_callback(self, callback: Callable[[V], None]) -> Callable[[], None]: """Add a callback to the list. Any failures during callback execution will be logged. Returns a callable that can be used to remove the callback. """ self._callbacks.append(callback) return lambda: self._callbacks.remove(callback) def __call__(self, value: V) -> None: """Invoke all callbacks in the list.""" for callback in self._callbacks: safe_callback(callback, self._logger)(value) def decoder_callback( decoder: Callable[[K], list[V]], callback: Callable[[V], None], logger: logging.Logger | logging.LoggerAdapter | None = None, ) -> Callable[[K], None]: """Create a callback that decodes messages using a decoder and invokes a callback. The decoder converts a value into a list of values. The callback is then invoked for each value in the list. Any failures during decoding or invoking the callbacks will be logged. """ if logger is None: logger = _LOGGER safe_cb = safe_callback(callback, logger) def wrapper(data: K) -> None: if not (messages := decoder(data)): logger.debug("Failed to decode message: %s", data) return for message in messages: logger.debug("Decoded message: %s", message) safe_cb(message) return wrapper