""" Metro Warden Base Plugin — abstract base class for all plugins. Every plugin must subclass :class:`BasePlugin` and define the class-level attributes ``name``, ``version``, and ``description``. Lifecycle hooks ``on_load``, ``on_unload``, and ``on_event`` can be overridden as needed. """ from __future__ import annotations import abc import asyncio import logging from typing import Any, Optional log = logging.getLogger(__name__) class BasePlugin(abc.ABC): """ Abstract base class for Metro Warden plugins. Subclasses must declare: name = "my-plugin" # unique identifier version = "1.0.0" description = "Does something useful" tags = ["category"] # optional Lifecycle:: on_load() — called once after instantiation on_unload() — called before the plugin is removed on_event(topic, data) — called for bus events the plugin subscribes to """ # Class-level attributes — subclasses MUST override these name: str = "" version: str = "0.0.0" description: str = "" tags: list = [] def __init__(self, bus=None, state=None) -> None: if not self.name: raise ValueError(f"{type(self).__name__} must define a 'name' attribute") self._bus = bus self._state = state self._sub_ids: list[str] = [] self._log = logging.getLogger(f"plugin.{self.name}") # ------------------------------------------------------------------ # Lifecycle hooks # ------------------------------------------------------------------ def on_load(self) -> None: """Called once when the plugin is loaded by the registry.""" self._log.info("plugin %r loaded (v%s)", self.name, self.version) def on_unload(self) -> None: """Called when the plugin is being unloaded. Clean up resources here.""" self._unsubscribe_all() self._log.info("plugin %r unloaded", self.name) def on_event(self, topic: str, data: Any) -> None: """ Called for every bus event whose topic this plugin has subscribed to. Override in subclasses to handle specific events. """ # ------------------------------------------------------------------ # Helper utilities # ------------------------------------------------------------------ def subscribe(self, topic_pattern: str) -> Optional[str]: """Subscribe this plugin's on_event handler to *topic_pattern*.""" if self._bus is None: self._log.warning("no bus — cannot subscribe to %r", topic_pattern) return None sub_id = self._bus.subscribe(topic_pattern, self.on_event) self._sub_ids.append(sub_id) return sub_id def publish(self, topic: str, data: Any = None) -> None: """Publish an event to the bus.""" if self._bus is None: return self._bus.publish_sync(topic, data) def state_get(self, key: str, default: Any = None) -> Any: if self._state is None: return default return self._state.get(key, default) def state_set(self, key: str, value: Any) -> None: if self._state is not None: self._state.set(key, value) def _unsubscribe_all(self) -> None: if self._bus is None: return for sid in self._sub_ids: self._bus.unsubscribe(sid) self._sub_ids.clear() def __repr__(self) -> str: return f"<{type(self).__name__} name={self.name!r} v{self.version}>"