Files
metro-warden/plugins/base.py
T

106 lines
3.5 KiB
Python

"""
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}>"