Files
shopdb-flask/plugins/knowledgebase/plugin.py
cproudlock a6f6c3f51f Extract Knowledge Base into a plugin (reference non-asset plugin)
First feature extracted from core into a plugin per "plugin is the product",
mirroring the notifications plugin. KB is a NON-asset plugin: it contributes a
model + blueprint + nav item but registers no AssetType.

- plugins/knowledgebase/: manifest.json (api_prefix /api/knowledgebase, no deps),
  models/ (KnowledgeBase, contract-pure imports via shopdb.api), api/ (the
  blueprint, same routes/prefix so the frontend is unchanged), plugin.py
  (get_blueprint + get_models + get_navigation_items).
- De-cored: removed shopdb/core/models/knowledgebase.py + api/knowledgebase.py,
  their __init__ exports, and 'knowledgebase' from CORE_BLUEPRINT_NAMES; dropped
  the hardcoded KB nav item from dashboard.py (now via the plugin nav hook).
- search.py and reports.py lazy-import KnowledgeBase from the plugin and degrade
  gracefully (search skips via _require_enabled when disabled; kb-popularity
  report returns 503 if the plugin is absent).
- Registered in instance/plugins.json (enabled).

The knowledgebase table stays in the core Alembic chain (bundled-plugin schema
folded into core, ADR-004); the model just maps it. KB was never in the
shopdb.api contract surface, so no __contract_version__ bump.

Pinned with characterization tests first (test_knowledgebase.py); they pass
unchanged against the plugin blueprint. 163 tests pass, naming green, app boots
7 bundled plugins, KB endpoint/nav/search verified live.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 20:32:37 -04:00

75 lines
2.4 KiB
Python

"""Knowledge Base plugin main class.
Reference NON-asset plugin: contributes a model + blueprint + nav item but no
AssetType (it is not an asset). Mirrors the notifications plugin.
"""
import json
import logging
from pathlib import Path
from typing import List, Dict, Optional, Type
from flask import Flask, Blueprint
from shopdb.plugins.base import BasePlugin, PluginMeta
from .models import KnowledgeBase
from .api import knowledgebase_bp
logger = logging.getLogger(__name__)
class KnowledgeBasePlugin(BasePlugin):
"""Knowledge Base plugin - articles linking to external resources."""
def __init__(self):
self._manifest = self._load_manifest()
def _load_manifest(self) -> Dict:
"""Load plugin manifest from JSON file."""
manifest_path = Path(__file__).parent / 'manifest.json'
if manifest_path.exists():
with open(manifest_path, 'r') as f:
return json.load(f)
return {}
@property
def meta(self) -> PluginMeta:
"""Return plugin metadata."""
return PluginMeta(
name=self._manifest.get('name', 'knowledgebase'),
version=self._manifest.get('version', '1.0.0'),
description=self._manifest.get(
'description',
'Knowledge Base articles linking to external resources'
),
author=self._manifest.get('author', 'ShopDB Team'),
dependencies=self._manifest.get('dependencies', []),
core_version=self._manifest.get('core_version', '>=0.1.0,<1.0.0'),
api_prefix=self._manifest.get('api_prefix', '/api/knowledgebase'),
)
def get_blueprint(self) -> Optional[Blueprint]:
"""Return Flask Blueprint with API routes."""
return knowledgebase_bp
def get_models(self) -> List[Type]:
"""Return list of SQLAlchemy model classes."""
return [KnowledgeBase]
def init_app(self, app: Flask, db_instance) -> None:
"""Initialize plugin with Flask app."""
logger.info(f"Knowledge Base plugin initialized (v{self.meta.version})")
def get_navigation_items(self) -> List[Dict]:
"""Return navigation menu items."""
return [
{
'name': 'Knowledge Base',
'icon': 'book-open',
'route': '/knowledgebase',
'position': 35,
'section': 'information',
},
]