| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308 |
- # -*- 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}")
|