| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642 |
- # -*- 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
|