manifest.py 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308
  1. # -*- coding: utf-8 -*-
  2. """
  3. Plugin-Manifest für Metadaten und Konfiguration.
  4. Definiert das Standard-Manifest-Format für Plugins mit
  5. Abhängigkeiten, Capabilities und Konfigurationsschema.
  6. """
  7. from __future__ import annotations
  8. import json
  9. from dataclasses import dataclass, field
  10. from enum import Enum, auto
  11. from pathlib import Path
  12. from typing import Any
  13. class ManifestError(Exception):
  14. """Fehler beim Verarbeiten eines Manifests."""
  15. pass
  16. class PluginCapability(Enum):
  17. """Vordefinierte Plugin-Fähigkeiten."""
  18. AUDIO_INPUT = auto() # Kann Audio empfangen
  19. AUDIO_OUTPUT = auto() # Kann Audio ausgeben
  20. SPEECH_TO_TEXT = auto() # Spracherkennung
  21. TEXT_TO_SPEECH = auto() # Sprachsynthese
  22. INTENT_HANDLER = auto() # Verarbeitet Intents
  23. SATELLITE_API = auto() # Zugriff auf Satellite-API
  24. NETWORK_ACCESS = auto() # Netzwerkzugriff
  25. FILE_ACCESS = auto() # Dateisystemzugriff
  26. SCHEDULER = auto() # Zeitplanung
  27. UI_COMPONENT = auto() # Stellt UI-Komponenten bereit
  28. CUSTOM = auto() # Benutzerdefiniert
  29. @dataclass
  30. class PluginManifest:
  31. """
  32. Plugin-Manifest mit allen Metadaten.
  33. Ein Manifest beschreibt ein Plugin vollständig:
  34. - Identifikation (Name, Version, Autor)
  35. - Abhängigkeiten
  36. - Fähigkeiten und Berechtigungen
  37. - Konfigurationsschema
  38. - Ressourcen und Assets
  39. Example:
  40. manifest = PluginManifest(
  41. name="my_plugin",
  42. version="1.0.0",
  43. description="Ein Beispiel-Plugin",
  44. author="Entwickler",
  45. dependencies={"core": "^2.0.0"},
  46. capabilities=[PluginCapability.INTENT_HANDLER],
  47. )
  48. # Speichern
  49. save_manifest(manifest, Path("plugins/my_plugin/manifest.json"))
  50. # Laden
  51. manifest = load_manifest(Path("plugins/my_plugin/manifest.json"))
  52. """
  53. # Identifikation
  54. name: str
  55. version: str = "1.0.0"
  56. description: str = ""
  57. author: str = ""
  58. email: str = ""
  59. url: str = ""
  60. license: str = "MIT"
  61. # Abhängigkeiten
  62. dependencies: dict[str, str] = field(default_factory=dict)
  63. optional_dependencies: dict[str, str] = field(default_factory=dict)
  64. python_requires: str = ">=3.10"
  65. trixy_requires: str = ">=1.0.0"
  66. # Fähigkeiten
  67. capabilities: list[PluginCapability] = field(default_factory=list)
  68. custom_capabilities: list[str] = field(default_factory=list)
  69. permissions: list[str] = field(default_factory=list)
  70. # Konfiguration
  71. config_schema: dict[str, Any] = field(default_factory=dict)
  72. default_config: dict[str, Any] = field(default_factory=dict)
  73. # Ressourcen
  74. assets: list[str] = field(default_factory=list)
  75. entry_point: str = "main.py"
  76. class_name: str = "" # Leer = automatische Erkennung
  77. # Metadaten
  78. tags: list[str] = field(default_factory=list)
  79. category: str = "general"
  80. min_priority: int = 0 # Mindest-Ladepriorität
  81. keywords: list[str] = field(default_factory=list)
  82. # Zusätzliche Daten
  83. extra: dict[str, Any] = field(default_factory=dict)
  84. def __post_init__(self) -> None:
  85. """Validiert das Manifest."""
  86. if not self.name:
  87. raise ManifestError("Plugin-Name ist erforderlich")
  88. if not self.name.replace("_", "").replace("-", "").isalnum():
  89. raise ManifestError(
  90. f"Ungültiger Plugin-Name: {self.name} "
  91. "(nur Buchstaben, Zahlen, _ und - erlaubt)"
  92. )
  93. def has_capability(self, capability: PluginCapability | str) -> bool:
  94. """
  95. Prüft, ob das Plugin eine Fähigkeit hat.
  96. Args:
  97. capability: Die zu prüfende Fähigkeit
  98. Returns:
  99. True wenn vorhanden
  100. """
  101. if isinstance(capability, PluginCapability):
  102. return capability in self.capabilities
  103. return capability in self.custom_capabilities
  104. def requires_permission(self, permission: str) -> bool:
  105. """Prüft, ob eine Berechtigung benötigt wird."""
  106. return permission in self.permissions
  107. def get_all_dependencies(self) -> dict[str, str]:
  108. """Gibt alle Abhängigkeiten zurück (required + optional)."""
  109. result = self.dependencies.copy()
  110. result.update(self.optional_dependencies)
  111. return result
  112. def to_dict(self) -> dict[str, Any]:
  113. """Konvertiert zu Dictionary."""
  114. return {
  115. "name": self.name,
  116. "version": self.version,
  117. "description": self.description,
  118. "author": self.author,
  119. "email": self.email,
  120. "url": self.url,
  121. "license": self.license,
  122. "dependencies": self.dependencies,
  123. "optional_dependencies": self.optional_dependencies,
  124. "python_requires": self.python_requires,
  125. "trixy_requires": self.trixy_requires,
  126. "capabilities": [c.name for c in self.capabilities],
  127. "custom_capabilities": self.custom_capabilities,
  128. "permissions": self.permissions,
  129. "config_schema": self.config_schema,
  130. "default_config": self.default_config,
  131. "assets": self.assets,
  132. "entry_point": self.entry_point,
  133. "class_name": self.class_name,
  134. "tags": self.tags,
  135. "category": self.category,
  136. "min_priority": self.min_priority,
  137. "keywords": self.keywords,
  138. "extra": self.extra,
  139. }
  140. @classmethod
  141. def from_dict(cls, data: dict[str, Any]) -> PluginManifest:
  142. """
  143. Erstellt ein Manifest aus einem Dictionary.
  144. Args:
  145. data: Dictionary mit Manifest-Daten
  146. Returns:
  147. PluginManifest
  148. Raises:
  149. ManifestError: Bei ungültigen Daten
  150. """
  151. try:
  152. # Capabilities parsen
  153. capabilities = []
  154. for cap_name in data.get("capabilities", []):
  155. try:
  156. capabilities.append(PluginCapability[cap_name])
  157. except KeyError:
  158. # Unbekannte Capability -> als custom behandeln
  159. if "custom_capabilities" not in data:
  160. data["custom_capabilities"] = []
  161. data["custom_capabilities"].append(cap_name)
  162. return cls(
  163. name=data["name"],
  164. version=data.get("version", "1.0.0"),
  165. description=data.get("description", ""),
  166. author=data.get("author", ""),
  167. email=data.get("email", ""),
  168. url=data.get("url", ""),
  169. license=data.get("license", "MIT"),
  170. dependencies=data.get("dependencies", {}),
  171. optional_dependencies=data.get("optional_dependencies", {}),
  172. python_requires=data.get("python_requires", ">=3.10"),
  173. trixy_requires=data.get("trixy_requires", ">=1.0.0"),
  174. capabilities=capabilities,
  175. custom_capabilities=data.get("custom_capabilities", []),
  176. permissions=data.get("permissions", []),
  177. config_schema=data.get("config_schema", {}),
  178. default_config=data.get("default_config", {}),
  179. assets=data.get("assets", []),
  180. entry_point=data.get("entry_point", "main.py"),
  181. class_name=data.get("class_name", ""),
  182. tags=data.get("tags", []),
  183. category=data.get("category", "general"),
  184. min_priority=data.get("min_priority", 0),
  185. keywords=data.get("keywords", []),
  186. extra=data.get("extra", {}),
  187. )
  188. except KeyError as e:
  189. raise ManifestError(f"Fehlendes Feld: {e}")
  190. except Exception as e:
  191. raise ManifestError(f"Ungültiges Manifest: {e}")
  192. def validate(self) -> list[str]:
  193. """
  194. Validiert das Manifest und gibt Fehler zurück.
  195. Returns:
  196. Liste der Fehler (leer wenn valide)
  197. """
  198. errors = []
  199. if not self.name:
  200. errors.append("Name ist erforderlich")
  201. if not self.version:
  202. errors.append("Version ist erforderlich")
  203. # Version validieren
  204. try:
  205. from trixy_core.plugins.versioning.semver import parse_version
  206. parse_version(self.version)
  207. except ValueError as e:
  208. errors.append(f"Ungültige Version: {e}")
  209. # Abhängigkeiten validieren
  210. for dep_name, requirement in self.dependencies.items():
  211. if not dep_name:
  212. errors.append("Abhängigkeitsname darf nicht leer sein")
  213. return errors
  214. def merge_config(self, config: dict[str, Any]) -> dict[str, Any]:
  215. """
  216. Merged Konfiguration mit Default-Werten.
  217. Args:
  218. config: Benutzerkonfiguration
  219. Returns:
  220. Gemergte Konfiguration
  221. """
  222. result = self.default_config.copy()
  223. result.update(config)
  224. return result
  225. def load_manifest(path: Path) -> PluginManifest:
  226. """
  227. Lädt ein Manifest aus einer Datei.
  228. Args:
  229. path: Pfad zur Manifest-Datei
  230. Returns:
  231. PluginManifest
  232. Raises:
  233. ManifestError: Bei Fehler
  234. """
  235. if not path.exists():
  236. raise ManifestError(f"Manifest nicht gefunden: {path}")
  237. try:
  238. with open(path, "r", encoding="utf-8") as f:
  239. data = json.load(f)
  240. return PluginManifest.from_dict(data)
  241. except json.JSONDecodeError as e:
  242. raise ManifestError(f"Ungültiges JSON in {path}: {e}")
  243. except Exception as e:
  244. raise ManifestError(f"Fehler beim Laden von {path}: {e}")
  245. def save_manifest(manifest: PluginManifest, path: Path) -> None:
  246. """
  247. Speichert ein Manifest in eine Datei.
  248. Args:
  249. manifest: Das zu speichernde Manifest
  250. path: Zielpfad
  251. Raises:
  252. ManifestError: Bei Fehler
  253. """
  254. try:
  255. with open(path, "w", encoding="utf-8") as f:
  256. json.dump(manifest.to_dict(), f, indent=2, ensure_ascii=False)
  257. except Exception as e:
  258. raise ManifestError(f"Fehler beim Speichern: {e}")