Files
beaver_project/app-instance/backend/beaver/plugins/models.py

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)