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:
@@ -233,6 +233,34 @@ These run when the plugin's installation state changes. All optional.
|
|||||||
| `on_enable(app)` | When the plugin is enabled at runtime | Subscribe to events, warm caches |
|
| `on_enable(app)` | When the plugin is enabled at runtime | Subscribe to events, warm caches |
|
||||||
| `on_disable(app)` | When the plugin is disabled at runtime | Unsubscribe, drain queues |
|
| `on_disable(app)` | When the plugin is disabled at runtime | Unsubscribe, drain queues |
|
||||||
|
|
||||||
|
## The import surface (`shopdb.api`)
|
||||||
|
|
||||||
|
`shopdb.api` is the ONLY core module a plugin may import from (besides
|
||||||
|
`shopdb.plugins.base` for `BasePlugin` / `PluginMeta`). Importing internal
|
||||||
|
paths like `shopdb.core.models.*`, `shopdb.extensions`, or `shopdb.utils.*`
|
||||||
|
is a contract violation and fails the test
|
||||||
|
`tests/test_plugin_contract.py::test_plugins_only_import_contract_surface`.
|
||||||
|
|
||||||
|
What `shopdb.api` exposes:
|
||||||
|
|
||||||
|
- Infrastructure: `db`, `cache`
|
||||||
|
- Model bases: `BaseModel`, `AuditMixin`
|
||||||
|
- Core models: `Asset`, `AssetType`, `AssetStatus`, `Vendor`, `Model`,
|
||||||
|
`Communication`, `CommunicationType`, `Location`, `Setting`, `AuditLog`,
|
||||||
|
`Application`, `AppVersion`, `OperatingSystem`
|
||||||
|
- Responses: `success_response`, `error_response`, `paginated_response`,
|
||||||
|
`ErrorCodes`
|
||||||
|
- Pagination: `get_pagination_params`, `paginate_query`
|
||||||
|
- Helpers: `audit_log`, `resolve_asset_position`
|
||||||
|
- Legacy employee directory: `employee_connection`
|
||||||
|
|
||||||
|
```python
|
||||||
|
from shopdb.api import db, Asset, AssetType, success_response, paginate_query
|
||||||
|
```
|
||||||
|
|
||||||
|
Adding a name to `shopdb.api` is an additive (minor) contract bump; removing
|
||||||
|
one is breaking (major). See ADR-002.
|
||||||
|
|
||||||
## Helpers exposed to plugins
|
## Helpers exposed to plugins
|
||||||
|
|
||||||
The framework provides helper APIs in `shopdb.api` (the public namespace).
|
The framework provides helper APIs in `shopdb.api` (the public namespace).
|
||||||
|
|||||||
@@ -3,18 +3,7 @@
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, Asset, AssetType, OperatingSystem, Application, AppVersion, AuditLog, Communication, CommunicationType, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||||
from shopdb.core.models import (
|
|
||||||
Asset, AssetType, OperatingSystem, Application, AppVersion, AuditLog,
|
|
||||||
Communication, CommunicationType,
|
|
||||||
)
|
|
||||||
from shopdb.utils.responses import (
|
|
||||||
success_response,
|
|
||||||
error_response,
|
|
||||||
paginated_response,
|
|
||||||
ErrorCodes
|
|
||||||
)
|
|
||||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
|
||||||
|
|
||||||
from ..models import Computer, ComputerType, ComputerInstalledApp
|
from ..models import Computer, ComputerType, ComputerInstalledApp
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Computer plugin models."""
|
"""Computer plugin models."""
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, BaseModel
|
||||||
from shopdb.core.models.base import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class ComputerType(BaseModel):
|
class ComputerType(BaseModel):
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ from flask import Flask, Blueprint
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, AssetType, AssetStatus
|
||||||
from shopdb.core.models import AssetType, AssetStatus
|
|
||||||
|
|
||||||
from .models import Computer, ComputerType, ComputerInstalledApp
|
from .models import Computer, ComputerType, ComputerInstalledApp
|
||||||
from .api import computers_bp
|
from .api import computers_bp
|
||||||
@@ -87,9 +86,7 @@ class ComputersPlugin(BasePlugin):
|
|||||||
def apply_collector_payload(self, payload: Dict) -> Dict:
|
def apply_collector_payload(self, payload: Dict) -> Dict:
|
||||||
"""Idempotent upsert of a PC from a collector payload (by hostname)."""
|
"""Idempotent upsert of a PC from a collector payload (by hostname)."""
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from shopdb.core.models import (
|
from shopdb.api import Asset, AssetType, Application, Communication, CommunicationType
|
||||||
Asset, AssetType, Application, Communication, CommunicationType,
|
|
||||||
)
|
|
||||||
|
|
||||||
warnings = []
|
warnings = []
|
||||||
hostname = (payload.get('hostname') or '').strip()
|
hostname = (payload.get('hostname') or '').strip()
|
||||||
@@ -242,7 +239,7 @@ class ComputersPlugin(BasePlugin):
|
|||||||
def stats():
|
def stats():
|
||||||
"""Show computer statistics."""
|
"""Show computer statistics."""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from shopdb.core.models import Asset
|
from shopdb.api import Asset
|
||||||
|
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
total = db.session.query(Computer).join(Asset).filter(
|
total = db.session.query(Computer).join(Asset).filter(
|
||||||
|
|||||||
@@ -3,15 +3,7 @@
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, Asset, AssetType, Vendor, Model, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||||
from shopdb.core.models import Asset, AssetType, Vendor, Model, AuditLog
|
|
||||||
from shopdb.utils.responses import (
|
|
||||||
success_response,
|
|
||||||
error_response,
|
|
||||||
paginated_response,
|
|
||||||
ErrorCodes
|
|
||||||
)
|
|
||||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
|
||||||
|
|
||||||
from ..models import Equipment, EquipmentType
|
from ..models import Equipment, EquipmentType
|
||||||
|
|
||||||
@@ -446,7 +438,7 @@ def dashboard_summary():
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Count by status
|
# Count by status
|
||||||
from shopdb.core.models import AssetStatus
|
from shopdb.api import AssetStatus
|
||||||
by_status = db.session.query(
|
by_status = db.session.query(
|
||||||
AssetStatus.status,
|
AssetStatus.status,
|
||||||
db.func.count(Equipment.equipmentid)
|
db.func.count(Equipment.equipmentid)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Equipment plugin models."""
|
"""Equipment plugin models."""
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, BaseModel
|
||||||
from shopdb.core.models.base import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class EquipmentType(BaseModel):
|
class EquipmentType(BaseModel):
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ from flask import Flask, Blueprint
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, AssetType, AssetStatus
|
||||||
from shopdb.core.models import AssetType, AssetStatus
|
|
||||||
|
|
||||||
from .models import Equipment, EquipmentType
|
from .models import Equipment, EquipmentType
|
||||||
from .api import equipment_bp
|
from .api import equipment_bp
|
||||||
@@ -170,7 +169,7 @@ class EquipmentPlugin(BasePlugin):
|
|||||||
def stats():
|
def stats():
|
||||||
"""Show equipment statistics."""
|
"""Show equipment statistics."""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from shopdb.core.models import Asset
|
from shopdb.api import Asset
|
||||||
|
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
total = db.session.query(Equipment).join(Asset).filter(
|
total = db.session.query(Equipment).join(Asset).filter(
|
||||||
|
|||||||
@@ -3,15 +3,7 @@
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, Asset, AssetType, Vendor, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||||
from shopdb.core.models import Asset, AssetType, Vendor, AuditLog
|
|
||||||
from shopdb.utils.responses import (
|
|
||||||
success_response,
|
|
||||||
error_response,
|
|
||||||
paginated_response,
|
|
||||||
ErrorCodes
|
|
||||||
)
|
|
||||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
|
||||||
|
|
||||||
from ..models import NetworkDevice, NetworkDeviceType, Subnet, VLAN
|
from ..models import NetworkDevice, NetworkDeviceType, Subnet, VLAN
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Network device plugin models."""
|
"""Network device plugin models."""
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, BaseModel
|
||||||
from shopdb.core.models.base import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class NetworkDeviceType(BaseModel):
|
class NetworkDeviceType(BaseModel):
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Subnet and VLAN models for network plugin."""
|
"""Subnet and VLAN models for network plugin."""
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, BaseModel
|
||||||
from shopdb.core.models.base import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class VLAN(BaseModel):
|
class VLAN(BaseModel):
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ from flask import Flask, Blueprint
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, AssetType
|
||||||
from shopdb.core.models import AssetType
|
|
||||||
|
|
||||||
from .models import NetworkDevice, NetworkDeviceType, Subnet, VLAN
|
from .models import NetworkDevice, NetworkDeviceType, Subnet, VLAN
|
||||||
from .api import network_bp
|
from .api import network_bp
|
||||||
@@ -146,7 +145,7 @@ class NetworkPlugin(BasePlugin):
|
|||||||
def stats():
|
def stats():
|
||||||
"""Show network device statistics."""
|
"""Show network device statistics."""
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from shopdb.core.models import Asset
|
from shopdb.api import Asset
|
||||||
|
|
||||||
with current_app.app_context():
|
with current_app.app_context():
|
||||||
total = db.session.query(NetworkDevice).join(Asset).filter(
|
total = db.session.query(NetworkDevice).join(Asset).filter(
|
||||||
|
|||||||
@@ -4,15 +4,7 @@ from datetime import datetime
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query, employee_connection
|
||||||
from shopdb.utils.responses import (
|
|
||||||
success_response,
|
|
||||||
error_response,
|
|
||||||
paginated_response,
|
|
||||||
ErrorCodes
|
|
||||||
)
|
|
||||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
|
||||||
from shopdb.utils.employee_db import employee_connection
|
|
||||||
|
|
||||||
from ..models import Notification, NotificationType
|
from ..models import Notification, NotificationType
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""Notifications plugin models - adapted to existing database schema."""
|
"""Notifications plugin models - adapted to existing database schema."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db
|
||||||
|
|
||||||
|
|
||||||
class NotificationType(db.Model):
|
class NotificationType(db.Model):
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from flask import Flask, Blueprint
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db
|
||||||
|
|
||||||
from .models import Notification, NotificationType
|
from .models import Notification, NotificationType
|
||||||
from .api import notifications_bp
|
from .api import notifications_bp
|
||||||
|
|||||||
@@ -5,15 +5,7 @@ import logging
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
|
||||||
from shopdb.extensions import db, cache
|
from shopdb.api import db, cache, Asset, AssetType, Vendor, Model, Communication, CommunicationType, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||||
from shopdb.core.models import Asset, AssetType, Vendor, Model, Communication, CommunicationType
|
|
||||||
from shopdb.utils.responses import (
|
|
||||||
success_response,
|
|
||||||
error_response,
|
|
||||||
paginated_response,
|
|
||||||
ErrorCodes
|
|
||||||
)
|
|
||||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
|
||||||
|
|
||||||
from ..models import Printer, PrinterType, ModelSupply
|
from ..models import Printer, PrinterType, ModelSupply
|
||||||
from ..models.model_supply import SUPPLY_TYPES, SUPPLY_COLORS, CAPACITY_TIERS
|
from ..models.model_supply import SUPPLY_TYPES, SUPPLY_COLORS, CAPACITY_TIERS
|
||||||
@@ -565,7 +557,7 @@ def _get_low_supplies_data():
|
|||||||
# location name for the report row
|
# location name for the report row
|
||||||
location_name = None
|
location_name = None
|
||||||
if asset.locationid:
|
if asset.locationid:
|
||||||
from shopdb.core.models import Location
|
from shopdb.api import Location
|
||||||
loc = Location.query.get(asset.locationid)
|
loc = Location.query.get(asset.locationid)
|
||||||
if loc:
|
if loc:
|
||||||
location_name = loc.locationname
|
location_name = loc.locationname
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ drum/waste/maintenance item). Lets new models and their toners be added
|
|||||||
through the API/UI without a code change.
|
through the API/UI without a code change.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, BaseModel
|
||||||
from shopdb.core.models.base import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
# allowed values, surfaced to the UI via the /supplies/meta endpoint
|
# allowed values, surfaced to the UI via the /supplies/meta endpoint
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""Printer plugin models - new Asset-based architecture."""
|
"""Printer plugin models - new Asset-based architecture."""
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, BaseModel
|
||||||
from shopdb.core.models.base import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class PrinterType(BaseModel):
|
class PrinterType(BaseModel):
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ from flask import Flask, Blueprint
|
|||||||
import click
|
import click
|
||||||
|
|
||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, AssetType
|
||||||
from shopdb.core.models.machine import MachineType
|
|
||||||
from shopdb.core.models import AssetType
|
|
||||||
|
|
||||||
from .models import Printer, PrinterType, ModelSupply
|
from .models import Printer, PrinterType, ModelSupply
|
||||||
from .api import printers_asset_bp
|
from .api import printers_asset_bp
|
||||||
@@ -104,7 +102,6 @@ class PrintersPlugin(BasePlugin):
|
|||||||
with app.app_context():
|
with app.app_context():
|
||||||
self._ensure_asset_type()
|
self._ensure_asset_type()
|
||||||
self._ensure_printer_types()
|
self._ensure_printer_types()
|
||||||
self._ensure_legacy_machine_types()
|
|
||||||
logger.info("Printers plugin installed")
|
logger.info("Printers plugin installed")
|
||||||
|
|
||||||
def _ensure_asset_type(self) -> None:
|
def _ensure_asset_type(self) -> None:
|
||||||
@@ -149,30 +146,6 @@ class PrintersPlugin(BasePlugin):
|
|||||||
|
|
||||||
db.session.commit()
|
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:
|
def on_uninstall(self, app: Flask) -> None:
|
||||||
"""Called when plugin is uninstalled."""
|
"""Called when plugin is uninstalled."""
|
||||||
logger.info("Printers plugin uninstalled")
|
logger.info("Printers plugin uninstalled")
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ Key facts encoded here:
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, Vendor, Model
|
||||||
from shopdb.core.models import Vendor, Model
|
|
||||||
from shopdb.core.models.machine import MachineType
|
|
||||||
|
|
||||||
from ..models import ModelSupply
|
from ..models import ModelSupply
|
||||||
|
|
||||||
@@ -311,9 +309,6 @@ def seedsupplies():
|
|||||||
model that matches a family's keys; if a family matches no existing model,
|
model that matches a family's keys; if a family matches no existing model,
|
||||||
creates a canonical model row so its toners are still available.
|
creates a canonical model row so its toners are still available.
|
||||||
"""
|
"""
|
||||||
printertype = MachineType.query.filter_by(category='Printer').first()
|
|
||||||
printertypeid = printertype.machinetypeid if printertype else None
|
|
||||||
|
|
||||||
models_touched = 0
|
models_touched = 0
|
||||||
supplies_added = 0
|
supplies_added = 0
|
||||||
|
|
||||||
@@ -322,10 +317,11 @@ def seedsupplies():
|
|||||||
targets = _matching_models(family['matchkeys'], vendor.vendorid)
|
targets = _matching_models(family['matchkeys'], vendor.vendorid)
|
||||||
|
|
||||||
if not targets:
|
if not targets:
|
||||||
|
# machinetypeid is a legacy Model column (nullable); printers are
|
||||||
|
# asset-based now and carry their type via PrinterType, not here.
|
||||||
model = Model(
|
model = Model(
|
||||||
modelnumber=family['canonical'],
|
modelnumber=family['canonical'],
|
||||||
vendorid=vendor.vendorid,
|
vendorid=vendor.vendorid,
|
||||||
machinetypeid=printertypeid,
|
|
||||||
)
|
)
|
||||||
db.session.add(model)
|
db.session.add(model)
|
||||||
db.session.flush()
|
db.session.flush()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ from typing import Dict, List, Optional
|
|||||||
import requests
|
import requests
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
from shopdb.extensions import cache
|
from shopdb.api import cache
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ class ZabbixService:
|
|||||||
@property
|
@property
|
||||||
def isenabled(self) -> bool:
|
def isenabled(self) -> bool:
|
||||||
"""Whether the integration is switched on."""
|
"""Whether the integration is switched on."""
|
||||||
from shopdb.core.models import Setting
|
from shopdb.api import Setting
|
||||||
db_enabled = Setting.get('zabbix_enabled')
|
db_enabled = Setting.get('zabbix_enabled')
|
||||||
if db_enabled is not None:
|
if db_enabled is not None:
|
||||||
return bool(db_enabled)
|
return bool(db_enabled)
|
||||||
@@ -66,7 +66,7 @@ class ZabbixService:
|
|||||||
"""Enabled, and a URL plus token are present."""
|
"""Enabled, and a URL plus token are present."""
|
||||||
if not self.isenabled:
|
if not self.isenabled:
|
||||||
return False
|
return False
|
||||||
from shopdb.core.models import Setting
|
from shopdb.api import Setting
|
||||||
self._url = Setting.get('zabbix_url') or current_app.config.get('ZABBIX_URL')
|
self._url = Setting.get('zabbix_url') or current_app.config.get('ZABBIX_URL')
|
||||||
self._token = Setting.get('zabbix_token') or current_app.config.get('ZABBIX_TOKEN')
|
self._token = Setting.get('zabbix_token') or current_app.config.get('ZABBIX_TOKEN')
|
||||||
return bool(self._url and self._token)
|
return bool(self._url and self._token)
|
||||||
|
|||||||
@@ -4,15 +4,7 @@ from flask import Blueprint, request
|
|||||||
from flask_jwt_extended import jwt_required, get_jwt_identity
|
from flask_jwt_extended import jwt_required, get_jwt_identity
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, AuditLog, success_response, error_response, paginated_response, ErrorCodes, get_pagination_params, paginate_query
|
||||||
from shopdb.core.models import AuditLog
|
|
||||||
from shopdb.utils.responses import (
|
|
||||||
success_response,
|
|
||||||
error_response,
|
|
||||||
paginated_response,
|
|
||||||
ErrorCodes
|
|
||||||
)
|
|
||||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
|
||||||
|
|
||||||
from ..models import USBDevice, USBDeviceType, USBCheckout
|
from ..models import USBDevice, USBDeviceType, USBCheckout
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"""USB device plugin models."""
|
"""USB device plugin models."""
|
||||||
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, BaseModel, AuditMixin
|
||||||
from shopdb.core.models.base import BaseModel, AuditMixin
|
|
||||||
|
|
||||||
|
|
||||||
class USBDeviceType(BaseModel):
|
class USBDeviceType(BaseModel):
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from typing import List, Dict, Optional, Type
|
|||||||
from flask import Flask, Blueprint
|
from flask import Flask, Blueprint
|
||||||
|
|
||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db
|
||||||
|
|
||||||
from .models import USBDevice, USBDeviceType, USBCheckout
|
from .models import USBDevice, USBDeviceType, USBCheckout
|
||||||
from .api import usb_bp
|
from .api import usb_bp
|
||||||
|
|||||||
@@ -12,7 +12,10 @@ from .plugins import plugin_manager
|
|||||||
# ADR-002 for the bump rules. Plugins declare a compatible range in
|
# 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
|
# their manifest.json `core_version` field. Pre-1.0 (0.x) means the
|
||||||
# contract is still settling; sister sites should pin tight ranges.
|
# 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:
|
def create_app(config_name: str = None) -> Flask:
|
||||||
|
|||||||
@@ -14,7 +14,47 @@ Setting helpers are exposed via BasePlugin instance methods
|
|||||||
|
|
||||||
from typing import Any, Dict, Optional
|
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(
|
def audit_log(
|
||||||
@@ -155,4 +195,37 @@ def resolve_asset_position(asset) -> Optional[Dict[str, Any]]:
|
|||||||
return None
|
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',
|
||||||
|
]
|
||||||
|
|||||||
@@ -3,13 +3,14 @@
|
|||||||
from flask import Blueprint, request
|
from flask import Blueprint, request
|
||||||
from flask_jwt_extended import jwt_required
|
from flask_jwt_extended import jwt_required
|
||||||
|
|
||||||
from shopdb.utils.responses import (
|
from shopdb.api import (
|
||||||
success_response,
|
success_response,
|
||||||
error_response,
|
error_response,
|
||||||
paginated_response,
|
paginated_response,
|
||||||
ErrorCodes,
|
ErrorCodes,
|
||||||
|
get_pagination_params,
|
||||||
|
paginate_query,
|
||||||
)
|
)
|
||||||
from shopdb.utils.pagination import get_pagination_params, paginate_query
|
|
||||||
|
|
||||||
from ..models import $Name
|
from ..models import $Name
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ this table holds the $name-specific fields. Replace the example fields
|
|||||||
below with your domain model.
|
below with your domain model.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from shopdb.extensions import db
|
from shopdb.api import db, BaseModel
|
||||||
from shopdb.core.models.base import BaseModel
|
|
||||||
|
|
||||||
|
|
||||||
class $Name(BaseModel):
|
class $Name(BaseModel):
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ from typing import List, Dict, Optional, Type
|
|||||||
from flask import Flask, Blueprint
|
from flask import Flask, Blueprint
|
||||||
|
|
||||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||||
from shopdb.core.models import AssetType
|
from shopdb.api import db, AssetType
|
||||||
from shopdb.extensions import db
|
|
||||||
|
|
||||||
from .models import $Name
|
from .models import $Name
|
||||||
from .api import ${name}_bp
|
from .api import ${name}_bp
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ plugin's plugin.py / manifest.json.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -143,3 +144,38 @@ def test_baseplugin_does_not_have_event_handlers_hook():
|
|||||||
def test_baseplugin_has_collector_schema_hook():
|
def test_baseplugin_has_collector_schema_hook():
|
||||||
"""The collector schema hook is on the contract surface."""
|
"""The collector schema hook is on the contract surface."""
|
||||||
assert hasattr(BasePlugin, 'get_collector_schema')
|
assert hasattr(BasePlugin, 'get_collector_schema')
|
||||||
|
|
||||||
|
|
||||||
|
# Imports a plugin may make from the core. shopdb.api is the contract surface;
|
||||||
|
# shopdb.plugins.base is the plugin ABC. Anything else (shopdb.core.*,
|
||||||
|
# shopdb.extensions, shopdb.utils.*) is a contract violation per ADR-001.
|
||||||
|
ALLOWED_CORE_IMPORTS = ('shopdb.api', 'shopdb.plugins.base')
|
||||||
|
|
||||||
|
_PLUGIN_IMPORT_RE = re.compile(
|
||||||
|
r'^\s*(?:from (shopdb[\w.]*) import|import (shopdb[\w.]*))', re.MULTILINE
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _plugin_source_files():
|
||||||
|
root = Path(__file__).resolve().parent.parent / 'plugins'
|
||||||
|
return [p for p in root.rglob('*.py') if '__pycache__' not in p.parts]
|
||||||
|
|
||||||
|
|
||||||
|
def test_plugins_only_import_contract_surface():
|
||||||
|
"""Plugins must import core code only via shopdb.api / shopdb.plugins.base."""
|
||||||
|
violations = []
|
||||||
|
for path in _plugin_source_files():
|
||||||
|
text = path.read_text()
|
||||||
|
for match in _PLUGIN_IMPORT_RE.finditer(text):
|
||||||
|
module = match.group(1) or match.group(2)
|
||||||
|
if not module.startswith('shopdb'):
|
||||||
|
continue
|
||||||
|
if any(module == a or module.startswith(a + '.')
|
||||||
|
for a in ALLOWED_CORE_IMPORTS):
|
||||||
|
continue
|
||||||
|
line = text[:match.start()].count('\n') + 1
|
||||||
|
violations.append(f'{path.name}:{line} imports {module}')
|
||||||
|
assert not violations, (
|
||||||
|
'Plugins must import core only via shopdb.api or shopdb.plugins.base. '
|
||||||
|
'Violations:\n' + '\n'.join(violations)
|
||||||
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user