-
Notifications
You must be signed in to change notification settings - Fork 0
BasePlugin
BasePlugin is the abstract base class that all PluginForge plugins must inherit from.
| Attribute | Type | Default | Description |
|---|---|---|---|
name |
str |
(required) | Unique plugin identifier |
version |
str |
"0.1.0" |
Plugin version |
api_version |
str |
"1" |
Hook spec compatibility version |
description |
str |
"" |
Human-readable description |
author |
str |
"" |
Plugin author |
depends_on |
list[str] |
[] |
Plugin names this plugin depends on |
app_config |
dict |
{} |
Global app config (populated by init()) |
config |
dict |
{} |
Plugin-specific config (populated by init()) |
config_schema |
dict[str, type] | None |
None |
Optional config type validation schema |
Called when the plugin is loaded. Receives both the global application config and the plugin-specific config from YAML. The default implementation stores both:
def init(self, app_config: dict, plugin_config: dict) -> None:
self.app_config = app_config
self.config = plugin_configOverride to perform custom initialization:
def init(self, app_config: dict, plugin_config: dict) -> None:
super().init(app_config, plugin_config)
self.db_url = app_config.get("database", {}).get("url")
self.output_dir = self.config.get("output_dir", "/tmp")Called after init() when the plugin is activated. Use this to start services, open connections, or register resources.
def activate(self) -> None:
self.connection = create_connection(self.db_url)Called when the plugin is being shut down. Release resources here.
def deactivate(self) -> None:
if self.connection:
self.connection.close()Return a list of FastAPI APIRouter instances. Only relevant when using FastAPI integration.
from fastapi import APIRouter
def get_routes(self) -> list:
router = APIRouter()
@router.get("/hello")
def hello():
return {"message": self.config.get("greeting", "Hello")}
return [router]Return a dict describing frontend UI components. Useful for apps with plugin-driven UIs.
def get_frontend_manifest(self) -> dict | None:
return {
"components": ["ExportDialog", "FormatSelector"],
"css": "/static/plugins/export/style.css",
}Returns None by default.
Return plugin health status. Override to check external dependencies (APIs, databases, etc.).
def health(self) -> dict:
try:
self.api_client.ping()
return {"status": "ok", "latency_ms": 12}
except Exception as e:
return {"status": "error", "error": str(e)}Returns {"status": "ok"} by default.
Return the path to Alembic migration scripts. Only relevant when using Alembic integration.
def get_migrations_dir(self) -> str | None:
return str(Path(__file__).parent / "migrations")Plugins can declare expected config types via config_schema. Validation happens automatically during init():
class ExportPlugin(BasePlugin):
name = "export"
config_schema = {
"pandoc_path": str,
"toc_depth": int,
"default_format": str,
}If a config value has the wrong type, the plugin is skipped and the error is reported in pm.get_load_errors(). Missing keys are not an error - only present keys with wrong types are rejected.
import pluggy
from pluginforge import BasePlugin
hookimpl = pluggy.HookimplMarker("myapp")
class ExportPlugin(BasePlugin):
name = "export"
version = "1.0.0"
description = "Export documents to various formats"
author = "Asterios Raptis"
depends_on = ["storage"]
config_schema = {"formats": list, "pandoc_path": str}
def init(self, app_config: dict, plugin_config: dict) -> None:
super().init(app_config, plugin_config)
self.formats = self.config.get("formats", ["pdf"])
def activate(self) -> None:
self.engine = self._create_engine()
def deactivate(self) -> None:
self.engine = None
def health(self) -> dict:
return {"status": "ok", "formats": self.formats}
@hookimpl
def on_document_save(self, document: dict) -> None:
for fmt in self.formats:
self.engine.export(document, fmt)A plugin is both:
- A class inheriting
BasePlugin(lifecycle: init, activate, deactivate) - An object with
@hookimpl-decorated methods (pluggy hook system)
PluginForge manages the lifecycle around the plugin. pluggy manages the hooks. These are complementary, not competing.