marketplace.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482
  1. # -*- coding: utf-8 -*-
  2. """
  3. Plugin-Registry und Marketplace.
  4. Zentrale Verwaltung von Plugin-Informationen und -Versionen.
  5. """
  6. from __future__ import annotations
  7. from dataclasses import dataclass, field
  8. from datetime import datetime
  9. from pathlib import Path
  10. from typing import Any, Iterator
  11. import json
  12. from trixy_core.plugins.registry.manifest import PluginManifest
  13. @dataclass
  14. class PluginInfo:
  15. """
  16. Kurzinformationen zu einem Plugin.
  17. Attributes:
  18. name: Plugin-Name
  19. version: Aktuelle Version
  20. description: Beschreibung
  21. author: Autor
  22. installed: Ist installiert
  23. enabled: Ist aktiviert
  24. path: Installationspfad
  25. """
  26. name: str
  27. version: str
  28. description: str = ""
  29. author: str = ""
  30. installed: bool = False
  31. enabled: bool = False
  32. path: Path | None = None
  33. def to_dict(self) -> dict[str, Any]:
  34. """Konvertiert zu Dictionary."""
  35. return {
  36. "name": self.name,
  37. "version": self.version,
  38. "description": self.description,
  39. "author": self.author,
  40. "installed": self.installed,
  41. "enabled": self.enabled,
  42. "path": str(self.path) if self.path else None,
  43. }
  44. @dataclass
  45. class RegistryEntry:
  46. """
  47. Vollständiger Registry-Eintrag für ein Plugin.
  48. Attributes:
  49. manifest: Plugin-Manifest
  50. available_versions: Verfügbare Versionen
  51. installed_version: Installierte Version
  52. install_path: Installationspfad
  53. installed_at: Installationszeitpunkt
  54. updated_at: Letztes Update
  55. enabled: Ist aktiviert
  56. download_count: Download-Zähler
  57. rating: Bewertung (0-5)
  58. tags: Tags
  59. """
  60. manifest: PluginManifest
  61. available_versions: list[str] = field(default_factory=list)
  62. installed_version: str | None = None
  63. install_path: Path | None = None
  64. installed_at: datetime | None = None
  65. updated_at: datetime | None = None
  66. enabled: bool = True
  67. download_count: int = 0
  68. rating: float = 0.0
  69. tags: list[str] = field(default_factory=list)
  70. @property
  71. def name(self) -> str:
  72. """Plugin-Name."""
  73. return self.manifest.name
  74. @property
  75. def is_installed(self) -> bool:
  76. """Ist das Plugin installiert?"""
  77. return self.installed_version is not None
  78. @property
  79. def has_update(self) -> bool:
  80. """Ist ein Update verfügbar?"""
  81. if not self.installed_version or not self.available_versions:
  82. return False
  83. from trixy_core.plugins.versioning.semver import parse_version
  84. try:
  85. installed = parse_version(self.installed_version)
  86. latest = parse_version(self.available_versions[-1])
  87. return latest > installed
  88. except ValueError:
  89. return False
  90. def to_info(self) -> PluginInfo:
  91. """Konvertiert zu PluginInfo."""
  92. return PluginInfo(
  93. name=self.manifest.name,
  94. version=self.installed_version or self.manifest.version,
  95. description=self.manifest.description,
  96. author=self.manifest.author,
  97. installed=self.is_installed,
  98. enabled=self.enabled,
  99. path=self.install_path,
  100. )
  101. def to_dict(self) -> dict[str, Any]:
  102. """Konvertiert zu Dictionary."""
  103. return {
  104. "manifest": self.manifest.to_dict(),
  105. "available_versions": self.available_versions,
  106. "installed_version": self.installed_version,
  107. "install_path": str(self.install_path) if self.install_path else None,
  108. "installed_at": self.installed_at.isoformat() if self.installed_at else None,
  109. "updated_at": self.updated_at.isoformat() if self.updated_at else None,
  110. "enabled": self.enabled,
  111. "download_count": self.download_count,
  112. "rating": self.rating,
  113. "tags": self.tags,
  114. }
  115. class PluginRegistry:
  116. """
  117. Zentrale Plugin-Registry.
  118. Verwaltet alle bekannten Plugins, deren Versionen und
  119. Installationsstatus.
  120. Example:
  121. registry = PluginRegistry()
  122. # Plugin registrieren
  123. registry.register(manifest)
  124. # Als installiert markieren
  125. registry.mark_installed("my_plugin", "1.0.0", Path("plugins/my_plugin"))
  126. # Suchen
  127. results = registry.search("audio")
  128. # Nach Kategorie filtern
  129. audio_plugins = registry.get_by_category("audio")
  130. # Updates prüfen
  131. updates = registry.get_available_updates()
  132. # Persistenz
  133. registry.save(Path("registry.json"))
  134. registry.load(Path("registry.json"))
  135. """
  136. def __init__(self) -> None:
  137. """Initialisiert die Registry."""
  138. self._entries: dict[str, RegistryEntry] = {}
  139. @property
  140. def count(self) -> int:
  141. """Anzahl registrierter Plugins."""
  142. return len(self._entries)
  143. @property
  144. def installed_count(self) -> int:
  145. """Anzahl installierter Plugins."""
  146. return sum(1 for e in self._entries.values() if e.is_installed)
  147. def register(
  148. self,
  149. manifest: PluginManifest,
  150. versions: list[str] | None = None,
  151. ) -> RegistryEntry:
  152. """
  153. Registriert ein Plugin.
  154. Args:
  155. manifest: Plugin-Manifest
  156. versions: Verfügbare Versionen
  157. Returns:
  158. Der Registry-Eintrag
  159. """
  160. versions = versions or [manifest.version]
  161. entry = RegistryEntry(
  162. manifest=manifest,
  163. available_versions=sorted(versions),
  164. tags=manifest.tags.copy(),
  165. )
  166. self._entries[manifest.name] = entry
  167. return entry
  168. def unregister(self, name: str) -> RegistryEntry | None:
  169. """
  170. Entfernt ein Plugin aus der Registry.
  171. Args:
  172. name: Plugin-Name
  173. Returns:
  174. Der entfernte Eintrag oder None
  175. """
  176. return self._entries.pop(name, None)
  177. def get(self, name: str) -> RegistryEntry | None:
  178. """Gibt einen Eintrag zurück."""
  179. return self._entries.get(name)
  180. def get_info(self, name: str) -> PluginInfo | None:
  181. """Gibt Plugin-Info zurück."""
  182. entry = self._entries.get(name)
  183. return entry.to_info() if entry else None
  184. def get_all(self) -> list[RegistryEntry]:
  185. """Gibt alle Einträge zurück."""
  186. return list(self._entries.values())
  187. def get_installed(self) -> list[RegistryEntry]:
  188. """Gibt alle installierten Plugins zurück."""
  189. return [e for e in self._entries.values() if e.is_installed]
  190. def get_enabled(self) -> list[RegistryEntry]:
  191. """Gibt alle aktivierten Plugins zurück."""
  192. return [e for e in self._entries.values() if e.is_installed and e.enabled]
  193. def mark_installed(
  194. self,
  195. name: str,
  196. version: str,
  197. path: Path,
  198. ) -> bool:
  199. """
  200. Markiert ein Plugin als installiert.
  201. Args:
  202. name: Plugin-Name
  203. version: Installierte Version
  204. path: Installationspfad
  205. Returns:
  206. True bei Erfolg
  207. """
  208. entry = self._entries.get(name)
  209. if entry is None:
  210. return False
  211. entry.installed_version = version
  212. entry.install_path = path
  213. entry.installed_at = datetime.now()
  214. entry.updated_at = datetime.now()
  215. return True
  216. def mark_uninstalled(self, name: str) -> bool:
  217. """
  218. Markiert ein Plugin als deinstalliert.
  219. Args:
  220. name: Plugin-Name
  221. Returns:
  222. True bei Erfolg
  223. """
  224. entry = self._entries.get(name)
  225. if entry is None:
  226. return False
  227. entry.installed_version = None
  228. entry.install_path = None
  229. entry.enabled = False
  230. return True
  231. def set_enabled(self, name: str, enabled: bool) -> bool:
  232. """
  233. Aktiviert/Deaktiviert ein Plugin.
  234. Args:
  235. name: Plugin-Name
  236. enabled: Aktivieren oder Deaktivieren
  237. Returns:
  238. True bei Erfolg
  239. """
  240. entry = self._entries.get(name)
  241. if entry is None or not entry.is_installed:
  242. return False
  243. entry.enabled = enabled
  244. return True
  245. def add_version(self, name: str, version: str) -> bool:
  246. """
  247. Fügt eine verfügbare Version hinzu.
  248. Args:
  249. name: Plugin-Name
  250. version: Neue Version
  251. Returns:
  252. True bei Erfolg
  253. """
  254. entry = self._entries.get(name)
  255. if entry is None:
  256. return False
  257. if version not in entry.available_versions:
  258. entry.available_versions.append(version)
  259. entry.available_versions.sort()
  260. return True
  261. def search(
  262. self,
  263. query: str,
  264. installed_only: bool = False,
  265. ) -> list[RegistryEntry]:
  266. """
  267. Sucht Plugins nach Name, Beschreibung oder Tags.
  268. Args:
  269. query: Suchbegriff
  270. installed_only: Nur installierte
  271. Returns:
  272. Liste passender Einträge
  273. """
  274. query = query.lower()
  275. results = []
  276. for entry in self._entries.values():
  277. if installed_only and not entry.is_installed:
  278. continue
  279. # Im Namen suchen
  280. if query in entry.manifest.name.lower():
  281. results.append(entry)
  282. continue
  283. # In Beschreibung suchen
  284. if query in entry.manifest.description.lower():
  285. results.append(entry)
  286. continue
  287. # In Tags suchen
  288. if any(query in tag.lower() for tag in entry.tags):
  289. results.append(entry)
  290. continue
  291. # In Keywords suchen
  292. if any(query in kw.lower() for kw in entry.manifest.keywords):
  293. results.append(entry)
  294. return results
  295. def get_by_category(self, category: str) -> list[RegistryEntry]:
  296. """Filtert nach Kategorie."""
  297. return [
  298. e for e in self._entries.values()
  299. if e.manifest.category == category
  300. ]
  301. def get_by_tag(self, tag: str) -> list[RegistryEntry]:
  302. """Filtert nach Tag."""
  303. return [e for e in self._entries.values() if tag in e.tags]
  304. def get_available_updates(self) -> list[RegistryEntry]:
  305. """Gibt Plugins mit verfügbaren Updates zurück."""
  306. return [e for e in self._entries.values() if e.has_update]
  307. def get_dependencies_of(self, name: str) -> list[str]:
  308. """
  309. Gibt die Abhängigkeiten eines Plugins zurück.
  310. Args:
  311. name: Plugin-Name
  312. Returns:
  313. Liste der Abhängigkeitsnamen
  314. """
  315. entry = self._entries.get(name)
  316. if entry is None:
  317. return []
  318. return list(entry.manifest.dependencies.keys())
  319. def get_dependents_of(self, name: str) -> list[str]:
  320. """
  321. Gibt Plugins zurück, die von diesem abhängen.
  322. Args:
  323. name: Plugin-Name
  324. Returns:
  325. Liste der abhängigen Plugin-Namen
  326. """
  327. dependents = []
  328. for entry in self._entries.values():
  329. if name in entry.manifest.dependencies:
  330. dependents.append(entry.name)
  331. return dependents
  332. def save(self, path: Path) -> None:
  333. """
  334. Speichert die Registry in eine Datei.
  335. Args:
  336. path: Zielpfad
  337. """
  338. data = {
  339. "version": "1.0",
  340. "entries": {
  341. name: entry.to_dict()
  342. for name, entry in self._entries.items()
  343. }
  344. }
  345. with open(path, "w", encoding="utf-8") as f:
  346. json.dump(data, f, indent=2, ensure_ascii=False)
  347. def load(self, path: Path) -> int:
  348. """
  349. Lädt die Registry aus einer Datei.
  350. Args:
  351. path: Quellpfad
  352. Returns:
  353. Anzahl geladener Einträge
  354. """
  355. if not path.exists():
  356. return 0
  357. with open(path, "r", encoding="utf-8") as f:
  358. data = json.load(f)
  359. for name, entry_data in data.get("entries", {}).items():
  360. try:
  361. manifest = PluginManifest.from_dict(entry_data["manifest"])
  362. entry = RegistryEntry(
  363. manifest=manifest,
  364. available_versions=entry_data.get("available_versions", []),
  365. installed_version=entry_data.get("installed_version"),
  366. install_path=Path(entry_data["install_path"]) if entry_data.get("install_path") else None,
  367. enabled=entry_data.get("enabled", True),
  368. download_count=entry_data.get("download_count", 0),
  369. rating=entry_data.get("rating", 0.0),
  370. tags=entry_data.get("tags", []),
  371. )
  372. if entry_data.get("installed_at"):
  373. entry.installed_at = datetime.fromisoformat(entry_data["installed_at"])
  374. if entry_data.get("updated_at"):
  375. entry.updated_at = datetime.fromisoformat(entry_data["updated_at"])
  376. self._entries[name] = entry
  377. except Exception:
  378. continue
  379. return len(self._entries)
  380. def __contains__(self, name: str) -> bool:
  381. return name in self._entries
  382. def __iter__(self) -> Iterator[RegistryEntry]:
  383. return iter(self._entries.values())
  384. def __len__(self) -> int:
  385. return len(self._entries)