mirror of
https://github.com/agentscope-ai/ReMe.git
synced 2026-08-28 05:25:04 +00:00
Some checks are pending
CI / Documentation / Test and build documentation (push) Waiting to run
CI / Python quality / Pre-commit (push) Waiting to run
CI / Python tests / Unit Tests - py3.11 (push) Waiting to run
CI / Python tests / Unit Tests - py3.12 (push) Waiting to run
CI / Python tests / Unit Tests - py3.13 (push) Waiting to run
CI / Windows / CLI smoke - py3.11 (push) Waiting to run
Deploy / Documentation / Build documentation (push) Waiting to run
Deploy / Documentation / deploy (push) Blocked by required conditions
Security / CodeQL / Analyze javascript-typescript (push) Waiting to run
Security / CodeQL / Analyze python (push) Waiting to run
* feat: simplify plugin setup and add management CLI * fix: isolate plugin CLI import side effects * refactor: streamline plugin validation * fix: route plugin CLI arguments independently * fix: support standard plugin source layouts
56 lines
2.4 KiB
Python
56 lines
2.4 KiB
Python
"""Parse the small, declarative contract shared by plugin runtime and CLI."""
|
|
|
|
from dataclasses import dataclass
|
|
from importlib import resources
|
|
from typing import Any
|
|
|
|
import yaml
|
|
|
|
PLUGIN_MANIFEST = "plugin.yaml"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PluginManifest:
|
|
"""The two contributions an installed plugin may declare."""
|
|
|
|
backends: dict[str, str]
|
|
application_defaults: dict[str, Any]
|
|
|
|
|
|
def parse_plugin_manifest(text: str, *, plugin_name: str) -> PluginManifest:
|
|
"""Parse and validate plugin.yaml without importing backend modules."""
|
|
try:
|
|
value = yaml.safe_load(text)
|
|
except yaml.YAMLError as exc:
|
|
raise ValueError(f"Plugin '{plugin_name}' manifest is invalid YAML") from exc
|
|
if not isinstance(value, dict):
|
|
raise ValueError(f"Plugin '{plugin_name}' manifest root must be a mapping")
|
|
|
|
unknown = set(value) - {"backends", "application_defaults"}
|
|
if unknown:
|
|
raise ValueError(f"Plugin '{plugin_name}' manifest has unknown keys: {', '.join(sorted(unknown))}")
|
|
|
|
backends = value.get("backends")
|
|
application_defaults = value.get("application_defaults")
|
|
backends = {} if backends is None else backends
|
|
application_defaults = {} if application_defaults is None else application_defaults
|
|
if not isinstance(backends, dict):
|
|
raise TypeError(f"Plugin '{plugin_name}' manifest 'backends' must be a mapping")
|
|
if not isinstance(application_defaults, dict):
|
|
raise TypeError(f"Plugin '{plugin_name}' manifest 'application_defaults' must be a mapping")
|
|
for name, target in backends.items():
|
|
if not isinstance(name, str) or not name:
|
|
raise TypeError(f"Plugin '{plugin_name}' backend names must be non-empty strings")
|
|
if not isinstance(target, str) or not target:
|
|
raise TypeError(f"Plugin '{plugin_name}' backend target for '{name}' must be a non-empty string")
|
|
return PluginManifest(backends=dict(backends), application_defaults=dict(application_defaults))
|
|
|
|
|
|
def load_package_manifest(package: str, *, plugin_name: str) -> PluginManifest:
|
|
"""Read plugin.yaml from an importable package."""
|
|
try:
|
|
path = resources.files(package).joinpath(PLUGIN_MANIFEST)
|
|
text = path.read_text(encoding="utf-8")
|
|
except (ModuleNotFoundError, FileNotFoundError, TypeError) as exc:
|
|
raise ValueError(f"Plugin '{plugin_name}' does not provide {package}/{PLUGIN_MANIFEST}") from exc
|
|
return parse_plugin_manifest(text, plugin_name=plugin_name)
|