138 lines
4.4 KiB
Python
138 lines
4.4 KiB
Python
"""Models for declarative Beaver plugin packages."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PluginSkillDeclaration:
|
|
name: str
|
|
relative_path: str
|
|
root: Path
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PluginManifest:
|
|
schema_version: int
|
|
plugin_id: str
|
|
name: str
|
|
version: str
|
|
root: Path
|
|
manifest_path: Path
|
|
display_path: str
|
|
skills: tuple[PluginSkillDeclaration, ...]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PluginSkillFileDigest:
|
|
path: str
|
|
size: int
|
|
executable: bool
|
|
content_hash: str
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PluginSkillTreeDigest:
|
|
skill_content_hash: str
|
|
skill_tree_hash: str
|
|
files: tuple[PluginSkillFileDigest, ...]
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PluginDiscoveryError:
|
|
path: Path
|
|
display_path: str
|
|
message: str
|
|
plugin_id: str | None = None
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class PluginDiscoveryResult:
|
|
manifests: dict[str, PluginManifest]
|
|
errors: list[PluginDiscoveryError]
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class PluginSkillBinding:
|
|
accepted_upstream_tree_hash: str | None = None
|
|
observed_upstream_tree_hash: str | None = None
|
|
accepted_beaver_version: str | None = None
|
|
current_beaver_version: str | None = None
|
|
pending_candidate_id: str | None = None
|
|
status: str = "discovered"
|
|
last_error: str | None = None
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"accepted_upstream_tree_hash": self.accepted_upstream_tree_hash,
|
|
"observed_upstream_tree_hash": self.observed_upstream_tree_hash,
|
|
"accepted_beaver_version": self.accepted_beaver_version,
|
|
"current_beaver_version": self.current_beaver_version,
|
|
"pending_candidate_id": self.pending_candidate_id,
|
|
"status": self.status,
|
|
"last_error": self.last_error,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, payload: dict[str, Any] | None) -> "PluginSkillBinding":
|
|
data = payload if isinstance(payload, dict) else {}
|
|
return cls(
|
|
accepted_upstream_tree_hash=_optional_str(data.get("accepted_upstream_tree_hash")),
|
|
observed_upstream_tree_hash=_optional_str(data.get("observed_upstream_tree_hash")),
|
|
accepted_beaver_version=_optional_str(data.get("accepted_beaver_version")),
|
|
current_beaver_version=_optional_str(data.get("current_beaver_version")),
|
|
pending_candidate_id=_optional_str(data.get("pending_candidate_id")),
|
|
status=str(data.get("status") or "discovered"),
|
|
last_error=_optional_str(data.get("last_error")),
|
|
)
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class PluginState:
|
|
plugin_id: str
|
|
enabled: bool = False
|
|
updates_paused: bool = False
|
|
installed_version: str | None = None
|
|
manifest_path: str | None = None
|
|
status: str = "discovered"
|
|
last_error: str | None = None
|
|
skills: dict[str, PluginSkillBinding] = field(default_factory=dict)
|
|
|
|
def to_dict(self) -> dict[str, Any]:
|
|
return {
|
|
"enabled": self.enabled,
|
|
"updates_paused": self.updates_paused,
|
|
"installed_version": self.installed_version,
|
|
"manifest_path": self.manifest_path,
|
|
"status": self.status,
|
|
"last_error": self.last_error,
|
|
"skills": {name: binding.to_dict() for name, binding in sorted(self.skills.items())},
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, plugin_id: str, payload: dict[str, Any] | None) -> "PluginState":
|
|
data = payload if isinstance(payload, dict) else {}
|
|
raw_skills = data.get("skills") if isinstance(data.get("skills"), dict) else {}
|
|
return cls(
|
|
plugin_id=plugin_id,
|
|
enabled=bool(data.get("enabled", False)),
|
|
updates_paused=bool(data.get("updates_paused", False)),
|
|
installed_version=_optional_str(data.get("installed_version")),
|
|
manifest_path=_optional_str(data.get("manifest_path")),
|
|
status=str(data.get("status") or "discovered"),
|
|
last_error=_optional_str(data.get("last_error")),
|
|
skills={
|
|
str(name): PluginSkillBinding.from_dict(binding if isinstance(binding, dict) else {})
|
|
for name, binding in raw_skills.items()
|
|
},
|
|
)
|
|
|
|
|
|
def _optional_str(value: Any) -> str | None:
|
|
if value in (None, ""):
|
|
return None
|
|
return str(value)
|