# -*- coding: utf-8 -*- """ Plugin-Manifest für Metadaten und Konfiguration. Definiert das Standard-Manifest-Format für Plugins mit Abhängigkeiten, Capabilities und Konfigurationsschema. """ from __future__ import annotations import json from dataclasses import dataclass, field from enum import Enum, auto from pathlib import Path from typing import Any class ManifestError(Exception): """Fehler beim Verarbeiten eines Manifests.""" pass class PluginCapability(Enum): """Vordefinierte Plugin-Fähigkeiten.""" AUDIO_INPUT = auto() # Kann Audio empfangen AUDIO_OUTPUT = auto() # Kann Audio ausgeben SPEECH_TO_TEXT = auto() # Spracherkennung TEXT_TO_SPEECH = auto() # Sprachsynthese INTENT_HANDLER = auto() # Verarbeitet Intents SATELLITE_API = auto() # Zugriff auf Satellite-API NETWORK_ACCESS = auto() # Netzwerkzugriff FILE_ACCESS = auto() # Dateisystemzugriff SCHEDULER = auto() # Zeitplanung UI_COMPONENT = auto() # Stellt UI-Komponenten bereit CUSTOM = auto() # Benutzerdefiniert @dataclass class PluginManifest: """ Plugin-Manifest mit allen Metadaten. Ein Manifest beschreibt ein Plugin vollständig: - Identifikation (Name, Version, Autor) - Abhängigkeiten - Fähigkeiten und Berechtigungen - Konfigurationsschema - Ressourcen und Assets Example: manifest = PluginManifest( name="my_plugin", version="1.0.0", description="Ein Beispiel-Plugin", author="Entwickler", dependencies={"core": "^2.0.0"}, capabilities=[PluginCapability.INTENT_HANDLER], ) # Speichern save_manifest(manifest, Path("plugins/my_plugin/manifest.json")) # Laden manifest = load_manifest(Path("plugins/my_plugin/manifest.json")) """ # Identifikation name: str version: str = "1.0.0" description: str = "" author: str = "" email: str = "" url: str = "" license: str = "MIT" # Abhängigkeiten dependencies: dict[str, str] = field(default_factory=dict) optional_dependencies: dict[str, str] = field(default_factory=dict) python_requires: str = ">=3.10" trixy_requires: str = ">=1.0.0" # Fähigkeiten capabilities: list[PluginCapability] = field(default_factory=list) custom_capabilities: list[str] = field(default_factory=list) permissions: list[str] = field(default_factory=list) # Konfiguration config_schema: dict[str, Any] = field(default_factory=dict) default_config: dict[str, Any] = field(default_factory=dict) # Ressourcen assets: list[str] = field(default_factory=list) entry_point: str = "main.py" class_name: str = "" # Leer = automatische Erkennung # Metadaten tags: list[str] = field(default_factory=list) category: str = "general" min_priority: int = 0 # Mindest-Ladepriorität keywords: list[str] = field(default_factory=list) # Zusätzliche Daten extra: dict[str, Any] = field(default_factory=dict) def __post_init__(self) -> None: """Validiert das Manifest.""" if not self.name: raise ManifestError("Plugin-Name ist erforderlich") if not self.name.replace("_", "").replace("-", "").isalnum(): raise ManifestError( f"Ungültiger Plugin-Name: {self.name} " "(nur Buchstaben, Zahlen, _ und - erlaubt)" ) def has_capability(self, capability: PluginCapability | str) -> bool: """ Prüft, ob das Plugin eine Fähigkeit hat. Args: capability: Die zu prüfende Fähigkeit Returns: True wenn vorhanden """ if isinstance(capability, PluginCapability): return capability in self.capabilities return capability in self.custom_capabilities def requires_permission(self, permission: str) -> bool: """Prüft, ob eine Berechtigung benötigt wird.""" return permission in self.permissions def get_all_dependencies(self) -> dict[str, str]: """Gibt alle Abhängigkeiten zurück (required + optional).""" result = self.dependencies.copy() result.update(self.optional_dependencies) return result def to_dict(self) -> dict[str, Any]: """Konvertiert zu Dictionary.""" return { "name": self.name, "version": self.version, "description": self.description, "author": self.author, "email": self.email, "url": self.url, "license": self.license, "dependencies": self.dependencies, "optional_dependencies": self.optional_dependencies, "python_requires": self.python_requires, "trixy_requires": self.trixy_requires, "capabilities": [c.name for c in self.capabilities], "custom_capabilities": self.custom_capabilities, "permissions": self.permissions, "config_schema": self.config_schema, "default_config": self.default_config, "assets": self.assets, "entry_point": self.entry_point, "class_name": self.class_name, "tags": self.tags, "category": self.category, "min_priority": self.min_priority, "keywords": self.keywords, "extra": self.extra, } @classmethod def from_dict(cls, data: dict[str, Any]) -> PluginManifest: """ Erstellt ein Manifest aus einem Dictionary. Args: data: Dictionary mit Manifest-Daten Returns: PluginManifest Raises: ManifestError: Bei ungültigen Daten """ try: # Capabilities parsen capabilities = [] for cap_name in data.get("capabilities", []): try: capabilities.append(PluginCapability[cap_name]) except KeyError: # Unbekannte Capability -> als custom behandeln if "custom_capabilities" not in data: data["custom_capabilities"] = [] data["custom_capabilities"].append(cap_name) return cls( name=data["name"], version=data.get("version", "1.0.0"), description=data.get("description", ""), author=data.get("author", ""), email=data.get("email", ""), url=data.get("url", ""), license=data.get("license", "MIT"), dependencies=data.get("dependencies", {}), optional_dependencies=data.get("optional_dependencies", {}), python_requires=data.get("python_requires", ">=3.10"), trixy_requires=data.get("trixy_requires", ">=1.0.0"), capabilities=capabilities, custom_capabilities=data.get("custom_capabilities", []), permissions=data.get("permissions", []), config_schema=data.get("config_schema", {}), default_config=data.get("default_config", {}), assets=data.get("assets", []), entry_point=data.get("entry_point", "main.py"), class_name=data.get("class_name", ""), tags=data.get("tags", []), category=data.get("category", "general"), min_priority=data.get("min_priority", 0), keywords=data.get("keywords", []), extra=data.get("extra", {}), ) except KeyError as e: raise ManifestError(f"Fehlendes Feld: {e}") except Exception as e: raise ManifestError(f"Ungültiges Manifest: {e}") def validate(self) -> list[str]: """ Validiert das Manifest und gibt Fehler zurück. Returns: Liste der Fehler (leer wenn valide) """ errors = [] if not self.name: errors.append("Name ist erforderlich") if not self.version: errors.append("Version ist erforderlich") # Version validieren try: from trixy_core.plugins.versioning.semver import parse_version parse_version(self.version) except ValueError as e: errors.append(f"Ungültige Version: {e}") # Abhängigkeiten validieren for dep_name, requirement in self.dependencies.items(): if not dep_name: errors.append("Abhängigkeitsname darf nicht leer sein") return errors def merge_config(self, config: dict[str, Any]) -> dict[str, Any]: """ Merged Konfiguration mit Default-Werten. Args: config: Benutzerkonfiguration Returns: Gemergte Konfiguration """ result = self.default_config.copy() result.update(config) return result def load_manifest(path: Path) -> PluginManifest: """ Lädt ein Manifest aus einer Datei. Args: path: Pfad zur Manifest-Datei Returns: PluginManifest Raises: ManifestError: Bei Fehler """ if not path.exists(): raise ManifestError(f"Manifest nicht gefunden: {path}") try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) return PluginManifest.from_dict(data) except json.JSONDecodeError as e: raise ManifestError(f"Ungültiges JSON in {path}: {e}") except Exception as e: raise ManifestError(f"Fehler beim Laden von {path}: {e}") def save_manifest(manifest: PluginManifest, path: Path) -> None: """ Speichert ein Manifest in eine Datei. Args: manifest: Das zu speichernde Manifest path: Zielpfad Raises: ManifestError: Bei Fehler """ try: with open(path, "w", encoding="utf-8") as f: json.dump(manifest.to_dict(), f, indent=2, ensure_ascii=False) except Exception as e: raise ManifestError(f"Fehler beim Speichern: {e}")