extension_point.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642
  1. # -*- coding: utf-8 -*-
  2. """
  3. Extension-Point-System.
  4. Definiert Erweiterungspunkte für Core-Komponenten.
  5. """
  6. import asyncio
  7. import logging
  8. from dataclasses import dataclass, field
  9. from datetime import datetime
  10. from typing import Any, Callable, TypeVar, Generic
  11. from trixy_core.plugins.extensions.extension import Extension, ExtensionInfo, ExtensionState
  12. T = TypeVar("T")
  13. @dataclass
  14. class ExtensionPointInfo:
  15. """Informationen über einen Extension Point."""
  16. id: str # z.B. "music.source"
  17. name: str # z.B. "Musikquellen"
  18. description: str = ""
  19. category: str = "" # z.B. "music", "network"
  20. # Typ-Informationen
  21. interface_type: str = "" # z.B. "MusicSource"
  22. allows_multiple: bool = True # Mehrere Extensions erlaubt?
  23. # Callbacks
  24. on_register: str = "" # Event-Name bei Registrierung
  25. on_unregister: str = "" # Event-Name bei Entfernung
  26. # Metadaten
  27. created_at: datetime = field(default_factory=datetime.now)
  28. metadata: dict[str, Any] = field(default_factory=dict)
  29. class ExtensionPoint(Generic[T]):
  30. """
  31. Ein Erweiterungspunkt für eine Core-Komponente.
  32. Extensions können sich an diesem Punkt registrieren und werden
  33. dann von der Core-Komponente genutzt.
  34. Beispiel:
  35. # Im Music-Modul
  36. source_point = ExtensionPoint[MusicSource](
  37. "music.source",
  38. "Musikquellen",
  39. description="Erweiterung um neue Musikquellen"
  40. )
  41. # Im Plugin
  42. class SpotifyExtension(Extension[MusicSource]):
  43. async def create_instance(self, config):
  44. return SpotifySource(config)
  45. source_point.register(SpotifyExtension(...))
  46. # Nutzung
  47. for source in source_point.get_all():
  48. tracks = await source.search("query")
  49. """
  50. def __init__(
  51. self,
  52. id: str,
  53. name: str,
  54. description: str = "",
  55. category: str = "",
  56. allows_multiple: bool = True,
  57. ) -> None:
  58. """
  59. Erstellt einen Extension Point.
  60. Args:
  61. id: Eindeutige ID (z.B. "music.source")
  62. name: Anzeigename
  63. description: Beschreibung
  64. category: Kategorie
  65. allows_multiple: Mehrere Extensions erlaubt
  66. """
  67. self._info = ExtensionPointInfo(
  68. id=id,
  69. name=name,
  70. description=description,
  71. category=category,
  72. allows_multiple=allows_multiple,
  73. )
  74. self._extensions: dict[str, Extension[T]] = {}
  75. self._lock = asyncio.Lock()
  76. self.logger = logging.getLogger(__name__)
  77. # Callbacks
  78. self._on_register: list[Callable[[Extension[T]], None]] = []
  79. self._on_unregister: list[Callable[[str], None]] = []
  80. @property
  81. def id(self) -> str:
  82. """Extension Point ID."""
  83. return self._info.id
  84. @property
  85. def info(self) -> ExtensionPointInfo:
  86. """Extension Point Informationen."""
  87. return self._info
  88. @property
  89. def count(self) -> int:
  90. """Anzahl registrierter Extensions."""
  91. return len(self._extensions)
  92. @property
  93. def is_empty(self) -> bool:
  94. """Prüft ob keine Extensions registriert sind."""
  95. return len(self._extensions) == 0
  96. # ==========================================================================
  97. # Registrierung
  98. # ==========================================================================
  99. async def register(
  100. self,
  101. extension: Extension[T],
  102. config: dict[str, Any] | None = None,
  103. initialize: bool = True,
  104. ) -> bool:
  105. """
  106. Registriert eine Extension.
  107. Args:
  108. extension: Die Extension
  109. config: Optionale Konfiguration
  110. initialize: Extension initialisieren
  111. Returns:
  112. True wenn erfolgreich
  113. """
  114. async with self._lock:
  115. ext_id = extension.id
  116. # Prüfen ob bereits registriert
  117. if ext_id in self._extensions:
  118. self.logger.warning(f"Extension bereits registriert: {ext_id}")
  119. return False
  120. # Prüfen ob multiple erlaubt
  121. if not self._info.allows_multiple and self._extensions:
  122. self.logger.warning(
  123. f"Extension Point {self.id} erlaubt nur eine Extension"
  124. )
  125. return False
  126. # Extension Point setzen
  127. extension.info.extension_point = self.id
  128. # Initialisieren wenn gewünscht
  129. if initialize:
  130. cfg = config or extension.get_default_config()
  131. success = await extension.initialize(cfg)
  132. if not success:
  133. self.logger.error(
  134. f"Extension {ext_id} Initialisierung fehlgeschlagen: "
  135. f"{extension.info.error_message}"
  136. )
  137. return False
  138. # Registrieren
  139. self._extensions[ext_id] = extension
  140. self.logger.info(f"Extension registriert: {ext_id} @ {self.id}")
  141. # Callbacks
  142. for callback in self._on_register:
  143. try:
  144. callback(extension)
  145. except Exception as e:
  146. self.logger.error(f"Register-Callback Fehler: {e}")
  147. return True
  148. async def unregister(self, extension_id: str) -> bool:
  149. """
  150. Entfernt eine Extension.
  151. Args:
  152. extension_id: Extension-ID
  153. Returns:
  154. True wenn entfernt
  155. """
  156. async with self._lock:
  157. if extension_id not in self._extensions:
  158. return False
  159. extension = self._extensions.pop(extension_id)
  160. # Herunterfahren
  161. await extension.shutdown()
  162. self.logger.info(f"Extension entfernt: {extension_id} @ {self.id}")
  163. # Callbacks
  164. for callback in self._on_unregister:
  165. try:
  166. callback(extension_id)
  167. except Exception as e:
  168. self.logger.error(f"Unregister-Callback Fehler: {e}")
  169. return True
  170. async def unregister_by_plugin(self, plugin_name: str) -> int:
  171. """
  172. Entfernt alle Extensions eines Plugins.
  173. Args:
  174. plugin_name: Plugin-Name
  175. Returns:
  176. Anzahl entfernter Extensions
  177. """
  178. to_remove = [
  179. ext_id for ext_id, ext in self._extensions.items()
  180. if ext.info.plugin_name == plugin_name
  181. ]
  182. count = 0
  183. for ext_id in to_remove:
  184. if await self.unregister(ext_id):
  185. count += 1
  186. return count
  187. # ==========================================================================
  188. # Zugriff
  189. # ==========================================================================
  190. def get(self, extension_id: str) -> Extension[T] | None:
  191. """
  192. Holt Extension nach ID.
  193. Args:
  194. extension_id: Extension-ID
  195. Returns:
  196. Extension oder None
  197. """
  198. return self._extensions.get(extension_id)
  199. def get_instance(self, extension_id: str) -> T | None:
  200. """
  201. Holt Instanz einer Extension.
  202. Args:
  203. extension_id: Extension-ID
  204. Returns:
  205. Instanz oder None
  206. """
  207. ext = self._extensions.get(extension_id)
  208. return ext.instance if ext and ext.is_active else None
  209. def get_all(self) -> list[Extension[T]]:
  210. """
  211. Holt alle registrierten Extensions.
  212. Returns:
  213. Liste aller Extensions
  214. """
  215. return list(self._extensions.values())
  216. def get_all_active(self) -> list[Extension[T]]:
  217. """
  218. Holt alle aktiven Extensions.
  219. Returns:
  220. Liste aktiver Extensions
  221. """
  222. return [ext for ext in self._extensions.values() if ext.is_active]
  223. def get_all_instances(self) -> list[T]:
  224. """
  225. Holt alle aktiven Instanzen.
  226. Returns:
  227. Liste aktiver Instanzen
  228. """
  229. return [
  230. ext.instance for ext in self._extensions.values()
  231. if ext.is_active and ext.instance is not None
  232. ]
  233. def get_by_priority(self) -> list[Extension[T]]:
  234. """
  235. Holt Extensions sortiert nach Priorität (höchste zuerst).
  236. Returns:
  237. Sortierte Liste
  238. """
  239. return sorted(
  240. self._extensions.values(),
  241. key=lambda e: e.info.priority,
  242. reverse=True,
  243. )
  244. def get_first(self) -> Extension[T] | None:
  245. """
  246. Holt die erste (höchste Priorität) aktive Extension.
  247. Returns:
  248. Extension oder None
  249. """
  250. for ext in self.get_by_priority():
  251. if ext.is_active:
  252. return ext
  253. return None
  254. def get_first_instance(self) -> T | None:
  255. """
  256. Holt die Instanz der ersten aktiven Extension.
  257. Returns:
  258. Instanz oder None
  259. """
  260. ext = self.get_first()
  261. return ext.instance if ext else None
  262. # ==========================================================================
  263. # Callbacks
  264. # ==========================================================================
  265. def on_register(self, callback: Callable[[Extension[T]], None]) -> None:
  266. """Registriert Callback für Extension-Registrierung."""
  267. self._on_register.append(callback)
  268. def on_unregister(self, callback: Callable[[str], None]) -> None:
  269. """Registriert Callback für Extension-Entfernung."""
  270. self._on_unregister.append(callback)
  271. # ==========================================================================
  272. # Info
  273. # ==========================================================================
  274. def get_info(self) -> dict[str, Any]:
  275. """Liefert Informationen über diesen Extension Point."""
  276. return {
  277. "id": self._info.id,
  278. "name": self._info.name,
  279. "description": self._info.description,
  280. "category": self._info.category,
  281. "allows_multiple": self._info.allows_multiple,
  282. "extension_count": self.count,
  283. "extensions": [
  284. ext.info.to_dict() for ext in self._extensions.values()
  285. ],
  286. }
  287. def __contains__(self, extension_id: str) -> bool:
  288. return extension_id in self._extensions
  289. def __len__(self) -> int:
  290. return len(self._extensions)
  291. def __bool__(self) -> bool:
  292. """ExtensionPoint ist immer truthy, auch wenn leer."""
  293. return True
  294. def __iter__(self):
  295. return iter(self._extensions.values())
  296. def __repr__(self) -> str:
  297. return (
  298. f"ExtensionPoint(id={self.id!r}, "
  299. f"extensions={self.count})"
  300. )
  301. class ExtensionRegistry:
  302. """
  303. Zentrale Registry für alle Extension Points.
  304. Verwaltet alle Extension Points und ermöglicht globalen Zugriff.
  305. """
  306. def __init__(self) -> None:
  307. """Initialisiert die Registry."""
  308. self._points: dict[str, ExtensionPoint] = {}
  309. self._lock = asyncio.Lock()
  310. self.logger = logging.getLogger(__name__)
  311. @property
  312. def points(self) -> list[ExtensionPoint]:
  313. """Alle Extension Points."""
  314. return list(self._points.values())
  315. @property
  316. def point_count(self) -> int:
  317. """Anzahl der Extension Points."""
  318. return len(self._points)
  319. # ==========================================================================
  320. # Extension Points
  321. # ==========================================================================
  322. def create_point(
  323. self,
  324. id: str,
  325. name: str,
  326. description: str = "",
  327. category: str = "",
  328. allows_multiple: bool = True,
  329. ) -> ExtensionPoint:
  330. """
  331. Erstellt einen neuen Extension Point.
  332. Args:
  333. id: Eindeutige ID
  334. name: Anzeigename
  335. description: Beschreibung
  336. category: Kategorie
  337. allows_multiple: Mehrere Extensions erlaubt
  338. Returns:
  339. Der erstellte Extension Point
  340. """
  341. if id in self._points:
  342. self.logger.warning(f"Extension Point existiert bereits: {id}")
  343. return self._points[id]
  344. point = ExtensionPoint(
  345. id=id,
  346. name=name,
  347. description=description,
  348. category=category,
  349. allows_multiple=allows_multiple,
  350. )
  351. self._points[id] = point
  352. self.logger.debug(f"Extension Point erstellt: {id}")
  353. return point
  354. def get_point(self, point_id: str) -> ExtensionPoint | None:
  355. """
  356. Holt Extension Point nach ID.
  357. Args:
  358. point_id: Extension Point ID
  359. Returns:
  360. Extension Point oder None
  361. """
  362. return self._points.get(point_id)
  363. def get_or_create(
  364. self,
  365. id: str,
  366. name: str = "",
  367. **kwargs,
  368. ) -> ExtensionPoint:
  369. """
  370. Holt oder erstellt Extension Point.
  371. Args:
  372. id: Extension Point ID
  373. name: Anzeigename (falls Erstellung)
  374. **kwargs: Weitere Parameter für Erstellung
  375. Returns:
  376. Extension Point
  377. """
  378. if id in self._points:
  379. return self._points[id]
  380. return self.create_point(id, name or id, **kwargs)
  381. def remove_point(self, point_id: str) -> bool:
  382. """
  383. Entfernt Extension Point.
  384. Args:
  385. point_id: Extension Point ID
  386. Returns:
  387. True wenn entfernt
  388. """
  389. if point_id in self._points:
  390. del self._points[point_id]
  391. return True
  392. return False
  393. # ==========================================================================
  394. # Extension-Operationen
  395. # ==========================================================================
  396. async def register(
  397. self,
  398. point_id: str,
  399. extension: Extension,
  400. config: dict[str, Any] | None = None,
  401. ) -> bool:
  402. """
  403. Registriert Extension an einem Point.
  404. Args:
  405. point_id: Extension Point ID
  406. extension: Die Extension
  407. config: Optionale Konfiguration
  408. Returns:
  409. True wenn erfolgreich
  410. """
  411. point = self._points.get(point_id)
  412. if not point:
  413. self.logger.error(f"Extension Point nicht gefunden: {point_id}")
  414. return False
  415. return await point.register(extension, config)
  416. async def unregister(self, point_id: str, extension_id: str) -> bool:
  417. """
  418. Entfernt Extension von einem Point.
  419. Args:
  420. point_id: Extension Point ID
  421. extension_id: Extension ID
  422. Returns:
  423. True wenn entfernt
  424. """
  425. point = self._points.get(point_id)
  426. if not point:
  427. return False
  428. return await point.unregister(extension_id)
  429. async def unregister_plugin(self, plugin_name: str) -> int:
  430. """
  431. Entfernt alle Extensions eines Plugins.
  432. Args:
  433. plugin_name: Plugin-Name
  434. Returns:
  435. Anzahl entfernter Extensions
  436. """
  437. count = 0
  438. for point in self._points.values():
  439. count += await point.unregister_by_plugin(plugin_name)
  440. return count
  441. # ==========================================================================
  442. # Suche
  443. # ==========================================================================
  444. def get_by_category(self, category: str) -> list[ExtensionPoint]:
  445. """
  446. Holt alle Extension Points einer Kategorie.
  447. Args:
  448. category: Kategorie
  449. Returns:
  450. Liste der Extension Points
  451. """
  452. return [
  453. p for p in self._points.values()
  454. if p.info.category == category
  455. ]
  456. def get_all_extensions(self) -> list[Extension]:
  457. """
  458. Holt alle registrierten Extensions über alle Points.
  459. Returns:
  460. Liste aller Extensions
  461. """
  462. all_ext = []
  463. for point in self._points.values():
  464. all_ext.extend(point.get_all())
  465. return all_ext
  466. def find_extension(self, extension_id: str) -> tuple[ExtensionPoint, Extension] | None:
  467. """
  468. Findet Extension über alle Points.
  469. Args:
  470. extension_id: Extension ID
  471. Returns:
  472. Tuple (Point, Extension) oder None
  473. """
  474. for point in self._points.values():
  475. ext = point.get(extension_id)
  476. if ext:
  477. return (point, ext)
  478. return None
  479. # ==========================================================================
  480. # Info
  481. # ==========================================================================
  482. def get_status(self) -> dict[str, Any]:
  483. """Liefert Status der Registry."""
  484. return {
  485. "point_count": self.point_count,
  486. "total_extensions": sum(p.count for p in self._points.values()),
  487. "points": {
  488. point_id: point.get_info()
  489. for point_id, point in self._points.items()
  490. },
  491. }
  492. def __contains__(self, point_id: str) -> bool:
  493. return point_id in self._points
  494. def __getitem__(self, point_id: str) -> ExtensionPoint:
  495. return self._points[point_id]
  496. def __len__(self) -> int:
  497. return len(self._points)
  498. def __repr__(self) -> str:
  499. return f"ExtensionRegistry(points={self.point_count})"
  500. # ==========================================================================
  501. # Globale Registry
  502. # ==========================================================================
  503. _global_registry: ExtensionRegistry | None = None
  504. def get_global_registry() -> ExtensionRegistry:
  505. """Holt die globale Extension Registry."""
  506. global _global_registry
  507. if _global_registry is None:
  508. _global_registry = ExtensionRegistry()
  509. return _global_registry