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

@@ -12,7 +12,10 @@ from .plugins import plugin_manager
# ADR-002 for the bump rules. Plugins declare a compatible range in
# their manifest.json `core_version` field. Pre-1.0 (0.x) means the
# contract is still settling; sister sites should pin tight ranges.
__contract_version__ = '0.2.0'
# 0.3.0: shopdb.api expanded to the full plugin import surface (db, cache,
# model bases, core models, response + pagination helpers, employee_connection)
# so plugins no longer import internal core paths. Additive, hence minor bump.
__contract_version__ = '0.3.0'
def create_app(config_name: str = None) -> Flask:

View File

@@ -14,7 +14,47 @@ Setting helpers are exposed via BasePlugin instance methods
from typing import Any, Dict, Optional
from shopdb.core.models import AuditLog
# -- Plugin contract surface (ADR-001, versioned per ADR-002) ----------------
# Everything a plugin is allowed to import from the core lives here. Plugins
# import these from `shopdb.api`, never from internal paths like
# `shopdb.core.models.*` or `shopdb.extensions`. The contract test
# (tests/test_plugin_contract.py) enforces this. Adding a name here is an
# additive (minor) contract change; removing one is breaking (major).
# Infrastructure
from shopdb.extensions import db, cache
# Model base classes for declaring plugin tables
from shopdb.core.models.base import BaseModel, AuditMixin
# Core domain models plugins legitimately reference (the asset contract)
from shopdb.core.models import (
Asset,
AssetType,
AssetStatus,
Vendor,
Model,
Communication,
CommunicationType,
Location,
Setting,
AuditLog,
Application,
AppVersion,
OperatingSystem,
)
# Response + pagination helpers for plugin API blueprints
from shopdb.utils.responses import (
success_response,
error_response,
paginated_response,
ErrorCodes,
)
from shopdb.utils.pagination import get_pagination_params, paginate_query
# Legacy employee directory lookup (read-only) used by notifications
from shopdb.utils.employee_db import employee_connection
def audit_log(
@@ -155,4 +195,37 @@ def resolve_asset_position(asset) -> Optional[Dict[str, Any]]:
return None
__all__ = ['audit_log', 'resolve_asset_position']
__all__ = [
# Helpers
'audit_log',
'resolve_asset_position',
# Infrastructure
'db',
'cache',
# Model bases
'BaseModel',
'AuditMixin',
# Core models
'Asset',
'AssetType',
'AssetStatus',
'Vendor',
'Model',
'Communication',
'CommunicationType',
'Location',
'Setting',
'AuditLog',
'Application',
'AppVersion',
'OperatingSystem',
# Response + pagination helpers
'success_response',
'error_response',
'paginated_response',
'ErrorCodes',
'get_pagination_params',
'paginate_query',
# Legacy employee directory
'employee_connection',
]

View File

@@ -3,13 +3,14 @@
from flask import Blueprint, request
from flask_jwt_extended import jwt_required
from shopdb.utils.responses import (
from shopdb.api import (
success_response,
error_response,
paginated_response,
ErrorCodes,
get_pagination_params,
paginate_query,
)
from shopdb.utils.pagination import get_pagination_params, paginate_query
from ..models import $Name

View File

@@ -6,8 +6,7 @@ this table holds the $name-specific fields. Replace the example fields
below with your domain model.
"""
from shopdb.extensions import db
from shopdb.core.models.base import BaseModel
from shopdb.api import db, BaseModel
class $Name(BaseModel):

View File

@@ -11,8 +11,7 @@ from typing import List, Dict, Optional, Type
from flask import Flask, Blueprint
from shopdb.plugins.base import BasePlugin, PluginMeta
from shopdb.core.models import AssetType
from shopdb.extensions import db
from shopdb.api import db, AssetType
from .models import $Name
from .api import ${name}_bp