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>
This commit is contained in:
cproudlock
2026-06-26 16:45:06 -04:00
parent 37ffb4add5
commit f663cc5bbe
29 changed files with 2152 additions and 2107 deletions

View File

@@ -9,9 +9,7 @@ from flask import Flask, Blueprint
import click
from shopdb.plugins.base import BasePlugin, PluginMeta
from shopdb.extensions import db
from shopdb.core.models.machine import MachineType
from shopdb.core.models import AssetType
from shopdb.api import db, AssetType
from .models import Printer, PrinterType, ModelSupply
from .api import printers_asset_bp
@@ -104,7 +102,6 @@ class PrintersPlugin(BasePlugin):
with app.app_context():
self._ensure_asset_type()
self._ensure_printer_types()
self._ensure_legacy_machine_types()
logger.info("Printers plugin installed")
def _ensure_asset_type(self) -> None:
@@ -149,30 +146,6 @@ class PrintersPlugin(BasePlugin):
db.session.commit()
def _ensure_legacy_machine_types(self) -> None:
"""Ensure basic printer machine types exist (legacy architecture)."""
printertypes = [
('Laser Printer', 'Printer', 'Standard laser printer'),
('Inkjet Printer', 'Printer', 'Inkjet printer'),
('Label Printer', 'Printer', 'Label/barcode printer'),
('Multifunction Printer', 'Printer', 'MFP with scan/copy/fax'),
('Plotter', 'Printer', 'Large format plotter'),
]
for name, category, description in printertypes:
existing = MachineType.query.filter_by(machinetype=name).first()
if not existing:
mt = MachineType(
machinetype=name,
category=category,
description=description,
icon='printer'
)
db.session.add(mt)
logger.debug(f"Created machine type: {name}")
db.session.commit()
def on_uninstall(self, app: Flask) -> None:
"""Called when plugin is uninstalled."""
logger.info("Printers plugin uninstalled")