Files
shopdb-flask/shopdb/plugins/templates/plugin.py.tmpl
cproudlock f663cc5bbe Enforce plugin contract purity: single import surface via shopdb.api
Plugins were reaching into internal core paths (shopdb.core.models.*,
shopdb.extensions, shopdb.utils.*), coupling them to core's file layout and
violating the ADR-001 contract. Consolidate onto one versioned surface.

- shopdb.api: expand from 2 helpers to the full plugin import surface -
  db, cache; BaseModel, AuditMixin; core models (Asset, AssetType,
  AssetStatus, Vendor, Model, Communication, CommunicationType, Location,
  Setting, AuditLog, Application, AppVersion, OperatingSystem); response +
  pagination helpers; employee_connection. Documented in PLUGIN-HOOKS.md.
- Migrate all 22 plugin source files to import only from shopdb.api (plus
  shopdb.plugins.base for the ABC).
- Drop the printers plugin's legacy MachineType dependency: remove
  _ensure_legacy_machine_types and the seed_supplies machinetypeid lookup
  (Model.machinetypeid is nullable; printers carry type via PrinterType).
- Guard test test_plugins_only_import_contract_surface scans plugin source
  and fails on any core import outside shopdb.api / shopdb.plugins.base.
- Scaffold templates updated so generated plugins are contract-pure.
- Bump __contract_version__ 0.2.0 -> 0.3.0 (additive surface expansion;
  manifests pin <1.0.0 so they still satisfy).

145 tests pass, naming/style green, app factory boots all 6 plugins.

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

71 lines
2.0 KiB
Cheetah

"""$Name plugin main class.
$description
"""
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 shopdb.api import db, AssetType
from .models import $Name
from .api import ${name}_bp
logger = logging.getLogger(__name__)
class ${Name}Plugin(BasePlugin):
"""$Name plugin.
$description
"""
def __init__(self):
manifest_path = Path(__file__).parent / 'manifest.json'
with open(manifest_path) as f:
self._manifest = json.load(f)
@property
def meta(self) -> PluginMeta:
return PluginMeta(
name=self._manifest['name'],
version=self._manifest['version'],
description=self._manifest['description'],
author=self._manifest.get('author', ''),
dependencies=self._manifest.get('dependencies', []),
core_version=self._manifest.get('core_version', '>=0.1.0'),
api_prefix=self._manifest.get('api_prefix'),
)
def get_blueprint(self) -> Optional[Blueprint]:
return ${name}_bp
def get_models(self) -> List[Type]:
return [$Name]
def init_app(self, app: Flask, db_instance) -> None:
logger.info(f'$Name plugin initialized (v{self.meta.version})')
def on_install(self, app: Flask) -> None:
with app.app_context():
self._ensure_asset_type()
logger.info('$Name plugin installed')
def _ensure_asset_type(self) -> None:
existing = AssetType.query.filter_by(assettype='$name').first()
if not existing:
asset_type = AssetType(
assettype='$name',
pluginname='$name',
tablename='$name',
description=self.meta.description,
)
db.session.add(asset_type)
db.session.commit()
logger.debug('Created asset type: $name')