mirror of
https://github.com/samjage/metro-warden.git
synced 2026-06-06 01:20:42 +00:00
106 lines
3.5 KiB
Python
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}>"
|