"""Plugin manifest schema (Pydantic v2).""" from __future__ import annotations from pydantic import BaseModel, Field, field_validator class PluginRouteDef(BaseModel): """Route definition within a plugin manifest.""" path: str = Field(..., description="URL path prefix, e.g. /api/v1/plugin-mail") module: str = Field(..., description="Dotted path to the module containing the APIRouter") router_attr: str = Field(default="router", description="Attribute name of the APIRouter in the module") class PluginManifest(BaseModel): """Manifest describing a plugin's metadata, dependencies, and capabilities.""" name: str = Field(..., min_length=1, max_length=80, description="Unique plugin identifier (snake_case)") version: str = Field(..., min_length=1, max_length=40, description="Semantic version string") display_name: str = Field(..., min_length=1, max_length=120) description: str = Field(default="", max_length=500) dependencies: list[str] = Field(default_factory=list, description="Other plugin names this plugin depends on") routes: list[PluginRouteDef] = Field(default_factory=list, description="Route definitions to register on activation") events: list[str] = Field(default_factory=list, description="Event names this plugin listens to") migrations: list[str] = Field(default_factory=list, description="Migration file names (ordered, e.g. 0001_initial.sql)") permissions: list[str] = Field(default_factory=list, description="Required permissions for this plugin") @field_validator("name") @classmethod def validate_name(cls, v: str) -> str: if not v.replace("_", "").isalnum(): raise ValueError("Plugin name must be alphanumeric with underscores only") return v.lower() model_config = {"extra": "forbid"} class ManifestSchemaResponse(BaseModel): """Response model describing the manifest schema for API consumers.""" fields: dict[str, dict[str, str]] example: PluginManifest # Pre-built schema documentation for GET /api/v1/plugins/manifest endpoint MANIFEST_SCHEMA_DOC = ManifestSchemaResponse( fields={ "name": {"type": "str", "required": "true", "description": "Unique plugin identifier (snake_case, max 80 chars)"}, "version": {"type": "str", "required": "true", "description": "Semantic version string"}, "display_name": {"type": "str", "required": "true", "description": "Human-readable plugin name"}, "description": {"type": "str", "required": "false", "description": "Plugin description (max 500 chars)"}, "dependencies": {"type": "list[str]", "required": "false", "description": "Other plugin names required"}, "routes": {"type": "list[PluginRouteDef]", "required": "false", "description": "Route definitions to register"}, "events": {"type": "list[str]", "required": "false", "description": "Event names to listen to"}, "migrations": {"type": "list[str]", "required": "false", "description": "Migration file names (ordered)"}, "permissions": {"type": "list[str]", "required": "false", "description": "Required permissions"}, }, example=PluginManifest( name="example_plugin", version="1.0.0", display_name="Example Plugin", description="An example plugin demonstrating the manifest schema.", dependencies=[], routes=[], events=["company.created", "contact.created"], migrations=["0001_initial.sql"], permissions=["companies.read"], ), )