Phase 3 (part 1): manifest-first loader, shopdb.api namespace, auto-register
Hardens the plugin framework so sister-site adoption is safe. Loader rewrite (shopdb/plugins/loader.py): - Reads manifest.json directly. Dependency sort and version checks no longer instantiate plugin classes (avoids __init__ side effects). - Fail-loud policy: in dev/test (DEBUG or TESTING true), plugin errors re-raise. In production, errors log with full context and the plugin is excluded from registration. Framework keeps booting. - Contract-version range check via packaging.SpecifierSet. Plugin's manifest.core_version must include the framework's __contract_version__ or load fails per the policy above. - Manifest validation: required fields (name, version, description), name matches directory, JSON parseable. Exceptions (shopdb/exceptions.py): - PluginNotFoundError, PluginContractError, PluginVersionError, PluginDependencyError. Specific types replace generic Exception swallowing. Auto-register core blueprints (shopdb/__init__.py): - CORE_BLUEPRINT_NAMES tuple drives registration. Adding a core resource is one entry, not three lines (import + register call). - Replaces 27 hand-coded register_blueprint calls. - Asserts each blueprint is exported by shopdb.core.api at boot. Public API namespace (shopdb/api/__init__.py): - audit_log: thin wrapper over AuditLog.log() with stable signature. - resolve_asset_position: implements ADR-001 position resolution (asset > related > location). Asset.mapx/mapy and AssetRelationship.inheritsposition columns are part of the locked contract surface but not yet in models; helper degrades gracefully to location-only fallback until the migration lands. BasePlugin helpers (shopdb/plugins/base.py): - get_setting(key, default), set_setting(key, value, ...). Settings namespaced as plugin.<pluginname>.<key> so two plugins can use the same key without colliding. Manifest version compatibility (plugins/*/manifest.json): - Bumped core_version from ">=1.0.0" to ">=0.1.0,<1.0.0" so all bundled plugins satisfy the new range check. Contract version bump (shopdb/__init__.py): - 0.1.0 -> 0.2.0. Additive surface change (Setting helpers, shopdb.api namespace) per ADR-002 minor-bump rules. Tests (tests/test_plugin_loader.py, tests/test_api_namespace.py): - 13 loader tests: manifest validation failures, version range checks, plugin.py import errors, strict-vs-isolate behavior under TESTING vs production-like config, manifest-first dependency sort. - 8 api-namespace tests: audit_log roundtrip, resolve position fallback chain, plugin.get_setting/set_setting roundtrip with per-plugin namespacing. Test count: 66 -> 87 passing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -12,7 +12,7 @@ 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.1.0'
|
||||
__contract_version__ = '0.2.0'
|
||||
|
||||
|
||||
def create_app(config_name: str = None) -> Flask:
|
||||
@@ -76,57 +76,52 @@ def create_app(config_name: str = None) -> Flask:
|
||||
return app
|
||||
|
||||
|
||||
CORE_BLUEPRINT_NAMES = (
|
||||
'auth',
|
||||
'assets',
|
||||
'machines',
|
||||
'machinetypes',
|
||||
'pctypes',
|
||||
'statuses',
|
||||
'vendors',
|
||||
'models',
|
||||
'businessunits',
|
||||
'locations',
|
||||
'operatingsystems',
|
||||
'dashboard',
|
||||
'applications',
|
||||
'knowledgebase',
|
||||
'search',
|
||||
'reports',
|
||||
'collector',
|
||||
'employees',
|
||||
'slides',
|
||||
'settings',
|
||||
'auditlogs',
|
||||
'users',
|
||||
)
|
||||
|
||||
|
||||
def register_blueprints(app: Flask):
|
||||
"""Register core API blueprints."""
|
||||
from .core.api import (
|
||||
auth_bp,
|
||||
assets_bp,
|
||||
machines_bp,
|
||||
machinetypes_bp,
|
||||
pctypes_bp,
|
||||
statuses_bp,
|
||||
vendors_bp,
|
||||
models_bp,
|
||||
businessunits_bp,
|
||||
locations_bp,
|
||||
operatingsystems_bp,
|
||||
dashboard_bp,
|
||||
applications_bp,
|
||||
knowledgebase_bp,
|
||||
search_bp,
|
||||
reports_bp,
|
||||
collector_bp,
|
||||
employees_bp,
|
||||
slides_bp,
|
||||
settings_bp,
|
||||
auditlogs_bp,
|
||||
users_bp,
|
||||
)
|
||||
"""Register core API blueprints from CORE_BLUEPRINT_NAMES.
|
||||
|
||||
Each entry maps to an attribute `<name>_bp` exported by
|
||||
`shopdb.core.api` and a URL prefix `/api/<name>`. Adding a new
|
||||
core resource is one entry in CORE_BLUEPRINT_NAMES, not a 3-line
|
||||
edit in this function.
|
||||
"""
|
||||
from .core import api as api_module
|
||||
|
||||
api_prefix = '/api'
|
||||
|
||||
app.register_blueprint(auth_bp, url_prefix=f'{api_prefix}/auth')
|
||||
app.register_blueprint(assets_bp, url_prefix=f'{api_prefix}/assets')
|
||||
app.register_blueprint(machines_bp, url_prefix=f'{api_prefix}/machines')
|
||||
app.register_blueprint(machinetypes_bp, url_prefix=f'{api_prefix}/machinetypes')
|
||||
app.register_blueprint(pctypes_bp, url_prefix=f'{api_prefix}/pctypes')
|
||||
app.register_blueprint(statuses_bp, url_prefix=f'{api_prefix}/statuses')
|
||||
app.register_blueprint(vendors_bp, url_prefix=f'{api_prefix}/vendors')
|
||||
app.register_blueprint(models_bp, url_prefix=f'{api_prefix}/models')
|
||||
app.register_blueprint(businessunits_bp, url_prefix=f'{api_prefix}/businessunits')
|
||||
app.register_blueprint(locations_bp, url_prefix=f'{api_prefix}/locations')
|
||||
app.register_blueprint(operatingsystems_bp, url_prefix=f'{api_prefix}/operatingsystems')
|
||||
app.register_blueprint(dashboard_bp, url_prefix=f'{api_prefix}/dashboard')
|
||||
app.register_blueprint(applications_bp, url_prefix=f'{api_prefix}/applications')
|
||||
app.register_blueprint(knowledgebase_bp, url_prefix=f'{api_prefix}/knowledgebase')
|
||||
app.register_blueprint(search_bp, url_prefix=f'{api_prefix}/search')
|
||||
app.register_blueprint(reports_bp, url_prefix=f'{api_prefix}/reports')
|
||||
app.register_blueprint(collector_bp, url_prefix=f'{api_prefix}/collector')
|
||||
app.register_blueprint(employees_bp, url_prefix=f'{api_prefix}/employees')
|
||||
app.register_blueprint(slides_bp, url_prefix=f'{api_prefix}/slides')
|
||||
app.register_blueprint(settings_bp, url_prefix=f'{api_prefix}/settings')
|
||||
app.register_blueprint(auditlogs_bp, url_prefix=f'{api_prefix}/auditlogs')
|
||||
app.register_blueprint(users_bp, url_prefix=f'{api_prefix}/users')
|
||||
for name in CORE_BLUEPRINT_NAMES:
|
||||
attr_name = f'{name}_bp'
|
||||
if not hasattr(api_module, attr_name):
|
||||
raise RuntimeError(
|
||||
f'Core blueprint "{attr_name}" missing from shopdb.core.api. '
|
||||
f'Either add it or remove "{name}" from CORE_BLUEPRINT_NAMES.'
|
||||
)
|
||||
bp = getattr(api_module, attr_name)
|
||||
app.register_blueprint(bp, url_prefix=f'{api_prefix}/{name}')
|
||||
|
||||
|
||||
def register_cli_commands(app: Flask):
|
||||
|
||||
Reference in New Issue
Block a user