KatetoSpace / space /plugin.py
Chaos
fix(space): make Hugging Face Space self-contained
35a216d
Raw
History Blame Contribute Delete
2.89 kB
from __future__ import annotations
import asyncio
import inspect
from collections.abc import Awaitable, Callable
from typing import Protocol
from pydantic import BaseModel
from .event import EventEnvelope
EventHandler = Callable[[BaseModel], Awaitable[None]]
QueuedEvent = tuple[EventEnvelope[BaseModel], EventHandler]
class PluginManagerProtocol(Protocol):
def register_event(self, name: str, contract: type[BaseModel]) -> None: ...
async def emit(
self,
name: str,
data: BaseModel,
*,
source: str = "space",
) -> EventEnvelope[BaseModel]: ...
async def wait_for_idle(self, *, timeout: float = 5) -> None: ...
async def report_plugin_error(
self,
plugin: Plugin,
envelope: EventEnvelope[BaseModel],
error: Exception,
) -> None: ...
class Plugin:
def __init__(self, name: str, *, receive_self_events: bool = False) -> None:
if not name:
raise ValueError("plugin name must not be empty")
self.name: str = name
self.capabilities: tuple[str, ...] = ()
self.receive_self_events: bool = receive_self_events
self.manager: PluginManagerProtocol | None = None
self.enabled: bool = False
self.queue: asyncio.Queue[QueuedEvent] = asyncio.Queue()
self._initialized: bool = False
self._worker: asyncio.Task[None] | None = None
async def initialize(self) -> None:
return None
def iter_event_handlers(self) -> dict[str, EventHandler]:
return {
name.removeprefix("on_"): getattr(self, name)
for name in dir(self)
if name.startswith("on_") and inspect.iscoroutinefunction(getattr(self, name))
}
async def _enqueue(self, envelope: EventEnvelope[BaseModel], handler: EventHandler) -> None:
if self.enabled:
await self.queue.put((envelope, handler))
def _start_worker(self) -> None:
self._worker = asyncio.create_task(self._run(), name=f"space-plugin-{self.name}")
async def _stop_worker(self) -> None:
worker = self._worker
self._worker = None
if worker is not None:
worker.cancel()
try:
await worker
except asyncio.CancelledError:
pass
self.clear_queue()
def clear_queue(self) -> None:
while not self.queue.empty():
self.queue.get_nowait()
self.queue.task_done()
async def _run(self) -> None:
while True:
envelope, handler = await self.queue.get()
try:
await handler(envelope.data)
except Exception as error:
if self.manager is not None:
await self.manager.report_plugin_error(self, envelope, error)
finally:
self.queue.task_done()