"""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"], ), )