"""Base plugin class that all plugins must inherit from.""" from abc import ABC, abstractmethod from typing import List, Dict, Optional, Type from dataclasses import dataclass, field from flask import Flask, Blueprint @dataclass class PluginMeta: """Plugin metadata container.""" name: str version: str description: str author: str = "" dependencies: List[str] = field(default_factory=list) # Default to the current pre-1.0 contract range; tighten when 1.0 lands core_version: str = ">=0.2.0,<1.0.0" api_prefix: str = None def __post_init__(self): if self.api_prefix is None: self.api_prefix = f"/api/{self.name.replace('_', '-')}" class BasePlugin(ABC): """ Base class for all ShopDB plugins. Plugins must implement: - meta: PluginMeta instance - get_blueprint(): Return Flask Blueprint for API routes - get_models(): Return list of SQLAlchemy model classes Optionally implement: - init_app(app, db): Custom initialization - get_cli_commands(): Return Click commands - get_services(): Return service classes - on_install(): Called when plugin is installed - on_uninstall(): Called when plugin is uninstalled - on_enable(): Called when plugin is enabled - on_disable(): Called when plugin is disabled """ @property @abstractmethod def meta(self) -> PluginMeta: """Return plugin metadata.""" pass @abstractmethod def get_blueprint(self) -> Optional[Blueprint]: """Return Flask Blueprint with API routes.""" pass @abstractmethod def get_models(self) -> List[Type]: """Return list of SQLAlchemy model classes.""" pass def init_app(self, app: Flask, db) -> None: """ Initialize plugin with Flask app. Override for custom initialization. """ pass def get_cli_commands(self) -> List: """Return list of Click command groups/commands.""" return [] def get_services(self) -> Dict[str, Type]: """Return dict of service name -> service class.""" return {} def get_config_schema(self) -> List[Dict]: """Declare the config fields this plugin needs, for the setup wizard. Each field is a dict: key - the Setting key (non-secret) it maps to label - human label type - 'text' | 'number' | 'password' secret - True for credentials; these are NOT stored in the DB, the wizard emits an .env line for the operator to paste instead envvar - (secret only) the .env variable name to emit default - optional default shown as a placeholder help - optional hint Return [] (default) if the plugin needs no configuration. """ return [] def get_setting(self, key: str, default=None): """Read a plugin-scoped setting from the core Setting store. Settings are namespaced by plugin name to avoid collisions across plugins. The on-disk key is `plugin..`. Returns the typed value (string, integer, boolean, etc.) or the default if not set. """ from shopdb.core.models import Setting namespaced_key = f'plugin.{self.meta.name}.{key}' return Setting.get(namespaced_key, default) def set_setting(self, key: str, value, valuetype: str = 'string', description: str = None) -> None: """Write a plugin-scoped setting to the core Setting store. Settings are namespaced by plugin name. Persists immediately and survives restarts. """ from shopdb.core.models import Setting namespaced_key = f'plugin.{self.meta.name}.{key}' Setting.set( namespaced_key, value, valuetype=valuetype, category=f'plugin.{self.meta.name}', description=description, ) def get_collector_schema(self) -> Optional[Dict]: """Return JSON Schema describing the collector payload for this plugin. Return None if the plugin does not accept collector input. See ADR-006 for the contract. The schema must include: - 'identityfield': name of the field that uniquely identifies an asset across submissions (e.g., 'hostname' for PCs, 'macaddress' for network devices). Used for idempotent upsert. - 'fields': JSON Schema definitions for the rest of the payload. Plugins returning a non-None schema have an endpoint at /api/collector/ auto-registered by the loader. """ return None def apply_collector_payload(self, payload: Dict) -> Dict: """Idempotently upsert an asset from a validated collector payload. Called by the generic /api/collector/ endpoint after the payload passed schema validation. Plugins that return a schema from get_collector_schema must implement this. Return a dict with at least: - 'action': 'created' | 'updated' | 'noop' - 'assetid': the affected asset id (or None) - 'warnings': list[str] """ raise NotImplementedError( f"{self.meta.name} declares a collector schema but does not " f"implement apply_collector_payload" ) def on_install(self, app: Flask) -> None: """Called when plugin is installed via CLI.""" pass def on_uninstall(self, app: Flask) -> None: """Called when plugin is uninstalled via CLI.""" pass def on_enable(self, app: Flask) -> None: """Called when plugin is enabled.""" pass def on_disable(self, app: Flask) -> None: """Called when plugin is disabled.""" pass def get_dashboard_widgets(self) -> List[Dict]: """ Return dashboard widget definitions. Each widget: { 'name': str, 'component': str, # Frontend component name 'endpoint': str, # API endpoint for data 'size': str, # 'small', 'medium', 'large' 'position': int # Order on dashboard } """ return [] def get_navigation_items(self) -> List[Dict]: """ Return navigation menu items. Each item: { 'name': str, 'icon': str, 'route': str, 'position': int, 'children': [] } """ return []