# -*- coding: utf-8 -*- """ Extension-Point-System. Definiert Erweiterungspunkte für Core-Komponenten. """ import asyncio import logging from dataclasses import dataclass, field from datetime import datetime from typing import Any, Callable, TypeVar, Generic from trixy_core.plugins.extensions.extension import Extension, ExtensionInfo, ExtensionState T = TypeVar("T") @dataclass class ExtensionPointInfo: """Informationen über einen Extension Point.""" id: str # z.B. "music.source" name: str # z.B. "Musikquellen" description: str = "" category: str = "" # z.B. "music", "network" # Typ-Informationen interface_type: str = "" # z.B. "MusicSource" allows_multiple: bool = True # Mehrere Extensions erlaubt? # Callbacks on_register: str = "" # Event-Name bei Registrierung on_unregister: str = "" # Event-Name bei Entfernung # Metadaten created_at: datetime = field(default_factory=datetime.now) metadata: dict[str, Any] = field(default_factory=dict) class ExtensionPoint(Generic[T]): """ Ein Erweiterungspunkt für eine Core-Komponente. Extensions können sich an diesem Punkt registrieren und werden dann von der Core-Komponente genutzt. Beispiel: # Im Music-Modul source_point = ExtensionPoint[MusicSource]( "music.source", "Musikquellen", description="Erweiterung um neue Musikquellen" ) # Im Plugin class SpotifyExtension(Extension[MusicSource]): async def create_instance(self, config): return SpotifySource(config) source_point.register(SpotifyExtension(...)) # Nutzung for source in source_point.get_all(): tracks = await source.search("query") """ def __init__( self, id: str, name: str, description: str = "", category: str = "", allows_multiple: bool = True, ) -> None: """ Erstellt einen Extension Point. Args: id: Eindeutige ID (z.B. "music.source") name: Anzeigename description: Beschreibung category: Kategorie allows_multiple: Mehrere Extensions erlaubt """ self._info = ExtensionPointInfo( id=id, name=name, description=description, category=category, allows_multiple=allows_multiple, ) self._extensions: dict[str, Extension[T]] = {} self._lock = asyncio.Lock() self.logger = logging.getLogger(__name__) # Callbacks self._on_register: list[Callable[[Extension[T]], None]] = [] self._on_unregister: list[Callable[[str], None]] = [] @property def id(self) -> str: """Extension Point ID.""" return self._info.id @property def info(self) -> ExtensionPointInfo: """Extension Point Informationen.""" return self._info @property def count(self) -> int: """Anzahl registrierter Extensions.""" return len(self._extensions) @property def is_empty(self) -> bool: """Prüft ob keine Extensions registriert sind.""" return len(self._extensions) == 0 # ========================================================================== # Registrierung # ========================================================================== async def register( self, extension: Extension[T], config: dict[str, Any] | None = None, initialize: bool = True, ) -> bool: """ Registriert eine Extension. Args: extension: Die Extension config: Optionale Konfiguration initialize: Extension initialisieren Returns: True wenn erfolgreich """ async with self._lock: ext_id = extension.id # Prüfen ob bereits registriert if ext_id in self._extensions: self.logger.warning(f"Extension bereits registriert: {ext_id}") return False # Prüfen ob multiple erlaubt if not self._info.allows_multiple and self._extensions: self.logger.warning( f"Extension Point {self.id} erlaubt nur eine Extension" ) return False # Extension Point setzen extension.info.extension_point = self.id # Initialisieren wenn gewünscht if initialize: cfg = config or extension.get_default_config() success = await extension.initialize(cfg) if not success: self.logger.error( f"Extension {ext_id} Initialisierung fehlgeschlagen: " f"{extension.info.error_message}" ) return False # Registrieren self._extensions[ext_id] = extension self.logger.info(f"Extension registriert: {ext_id} @ {self.id}") # Callbacks for callback in self._on_register: try: callback(extension) except Exception as e: self.logger.error(f"Register-Callback Fehler: {e}") return True async def unregister(self, extension_id: str) -> bool: """ Entfernt eine Extension. Args: extension_id: Extension-ID Returns: True wenn entfernt """ async with self._lock: if extension_id not in self._extensions: return False extension = self._extensions.pop(extension_id) # Herunterfahren await extension.shutdown() self.logger.info(f"Extension entfernt: {extension_id} @ {self.id}") # Callbacks for callback in self._on_unregister: try: callback(extension_id) except Exception as e: self.logger.error(f"Unregister-Callback Fehler: {e}") return True async def unregister_by_plugin(self, plugin_name: str) -> int: """ Entfernt alle Extensions eines Plugins. Args: plugin_name: Plugin-Name Returns: Anzahl entfernter Extensions """ to_remove = [ ext_id for ext_id, ext in self._extensions.items() if ext.info.plugin_name == plugin_name ] count = 0 for ext_id in to_remove: if await self.unregister(ext_id): count += 1 return count # ========================================================================== # Zugriff # ========================================================================== def get(self, extension_id: str) -> Extension[T] | None: """ Holt Extension nach ID. Args: extension_id: Extension-ID Returns: Extension oder None """ return self._extensions.get(extension_id) def get_instance(self, extension_id: str) -> T | None: """ Holt Instanz einer Extension. Args: extension_id: Extension-ID Returns: Instanz oder None """ ext = self._extensions.get(extension_id) return ext.instance if ext and ext.is_active else None def get_all(self) -> list[Extension[T]]: """ Holt alle registrierten Extensions. Returns: Liste aller Extensions """ return list(self._extensions.values()) def get_all_active(self) -> list[Extension[T]]: """ Holt alle aktiven Extensions. Returns: Liste aktiver Extensions """ return [ext for ext in self._extensions.values() if ext.is_active] def get_all_instances(self) -> list[T]: """ Holt alle aktiven Instanzen. Returns: Liste aktiver Instanzen """ return [ ext.instance for ext in self._extensions.values() if ext.is_active and ext.instance is not None ] def get_by_priority(self) -> list[Extension[T]]: """ Holt Extensions sortiert nach Priorität (höchste zuerst). Returns: Sortierte Liste """ return sorted( self._extensions.values(), key=lambda e: e.info.priority, reverse=True, ) def get_first(self) -> Extension[T] | None: """ Holt die erste (höchste Priorität) aktive Extension. Returns: Extension oder None """ for ext in self.get_by_priority(): if ext.is_active: return ext return None def get_first_instance(self) -> T | None: """ Holt die Instanz der ersten aktiven Extension. Returns: Instanz oder None """ ext = self.get_first() return ext.instance if ext else None # ========================================================================== # Callbacks # ========================================================================== def on_register(self, callback: Callable[[Extension[T]], None]) -> None: """Registriert Callback für Extension-Registrierung.""" self._on_register.append(callback) def on_unregister(self, callback: Callable[[str], None]) -> None: """Registriert Callback für Extension-Entfernung.""" self._on_unregister.append(callback) # ========================================================================== # Info # ========================================================================== def get_info(self) -> dict[str, Any]: """Liefert Informationen über diesen Extension Point.""" return { "id": self._info.id, "name": self._info.name, "description": self._info.description, "category": self._info.category, "allows_multiple": self._info.allows_multiple, "extension_count": self.count, "extensions": [ ext.info.to_dict() for ext in self._extensions.values() ], } def __contains__(self, extension_id: str) -> bool: return extension_id in self._extensions def __len__(self) -> int: return len(self._extensions) def __bool__(self) -> bool: """ExtensionPoint ist immer truthy, auch wenn leer.""" return True def __iter__(self): return iter(self._extensions.values()) def __repr__(self) -> str: return ( f"ExtensionPoint(id={self.id!r}, " f"extensions={self.count})" ) class ExtensionRegistry: """ Zentrale Registry für alle Extension Points. Verwaltet alle Extension Points und ermöglicht globalen Zugriff. """ def __init__(self) -> None: """Initialisiert die Registry.""" self._points: dict[str, ExtensionPoint] = {} self._lock = asyncio.Lock() self.logger = logging.getLogger(__name__) @property def points(self) -> list[ExtensionPoint]: """Alle Extension Points.""" return list(self._points.values()) @property def point_count(self) -> int: """Anzahl der Extension Points.""" return len(self._points) # ========================================================================== # Extension Points # ========================================================================== def create_point( self, id: str, name: str, description: str = "", category: str = "", allows_multiple: bool = True, ) -> ExtensionPoint: """ Erstellt einen neuen Extension Point. Args: id: Eindeutige ID name: Anzeigename description: Beschreibung category: Kategorie allows_multiple: Mehrere Extensions erlaubt Returns: Der erstellte Extension Point """ if id in self._points: self.logger.warning(f"Extension Point existiert bereits: {id}") return self._points[id] point = ExtensionPoint( id=id, name=name, description=description, category=category, allows_multiple=allows_multiple, ) self._points[id] = point self.logger.debug(f"Extension Point erstellt: {id}") return point def get_point(self, point_id: str) -> ExtensionPoint | None: """ Holt Extension Point nach ID. Args: point_id: Extension Point ID Returns: Extension Point oder None """ return self._points.get(point_id) def get_or_create( self, id: str, name: str = "", **kwargs, ) -> ExtensionPoint: """ Holt oder erstellt Extension Point. Args: id: Extension Point ID name: Anzeigename (falls Erstellung) **kwargs: Weitere Parameter für Erstellung Returns: Extension Point """ if id in self._points: return self._points[id] return self.create_point(id, name or id, **kwargs) def remove_point(self, point_id: str) -> bool: """ Entfernt Extension Point. Args: point_id: Extension Point ID Returns: True wenn entfernt """ if point_id in self._points: del self._points[point_id] return True return False # ========================================================================== # Extension-Operationen # ========================================================================== async def register( self, point_id: str, extension: Extension, config: dict[str, Any] | None = None, ) -> bool: """ Registriert Extension an einem Point. Args: point_id: Extension Point ID extension: Die Extension config: Optionale Konfiguration Returns: True wenn erfolgreich """ point = self._points.get(point_id) if not point: self.logger.error(f"Extension Point nicht gefunden: {point_id}") return False return await point.register(extension, config) async def unregister(self, point_id: str, extension_id: str) -> bool: """ Entfernt Extension von einem Point. Args: point_id: Extension Point ID extension_id: Extension ID Returns: True wenn entfernt """ point = self._points.get(point_id) if not point: return False return await point.unregister(extension_id) async def unregister_plugin(self, plugin_name: str) -> int: """ Entfernt alle Extensions eines Plugins. Args: plugin_name: Plugin-Name Returns: Anzahl entfernter Extensions """ count = 0 for point in self._points.values(): count += await point.unregister_by_plugin(plugin_name) return count # ========================================================================== # Suche # ========================================================================== def get_by_category(self, category: str) -> list[ExtensionPoint]: """ Holt alle Extension Points einer Kategorie. Args: category: Kategorie Returns: Liste der Extension Points """ return [ p for p in self._points.values() if p.info.category == category ] def get_all_extensions(self) -> list[Extension]: """ Holt alle registrierten Extensions über alle Points. Returns: Liste aller Extensions """ all_ext = [] for point in self._points.values(): all_ext.extend(point.get_all()) return all_ext def find_extension(self, extension_id: str) -> tuple[ExtensionPoint, Extension] | None: """ Findet Extension über alle Points. Args: extension_id: Extension ID Returns: Tuple (Point, Extension) oder None """ for point in self._points.values(): ext = point.get(extension_id) if ext: return (point, ext) return None # ========================================================================== # Info # ========================================================================== def get_status(self) -> dict[str, Any]: """Liefert Status der Registry.""" return { "point_count": self.point_count, "total_extensions": sum(p.count for p in self._points.values()), "points": { point_id: point.get_info() for point_id, point in self._points.items() }, } def __contains__(self, point_id: str) -> bool: return point_id in self._points def __getitem__(self, point_id: str) -> ExtensionPoint: return self._points[point_id] def __len__(self) -> int: return len(self._points) def __repr__(self) -> str: return f"ExtensionRegistry(points={self.point_count})" # ========================================================================== # Globale Registry # ========================================================================== _global_registry: ExtensionRegistry | None = None def get_global_registry() -> ExtensionRegistry: """Holt die globale Extension Registry.""" global _global_registry if _global_registry is None: _global_registry = ExtensionRegistry() return _global_registry