Bugfix: Fix AttributeError in /api/plugins/settings-routes endpoint - Changed get_enabled_plugin_instances() to get_all_plugins() - Added explicit enabled check with pm.registry.is_enabled() - Test failure: test_no_route_answers_a_nonsense_filter_with_a_server_error Optimization: Enable parallel test execution (Phase 1 of pytest speedup) - Added pytest-xdist configuration to pytest.ini - Enabled -n auto (auto-detect CPU cores, typically 8 workers) - Added slow marker for future test categorization - Expected speedup: 4-8x (20 min -> 2.5-5 min) Root cause: 2857 tests recreate entire schema per test (0.4-0.6s overhead each) - Phase 2 (savepoint isolation) will achieve 80x total speedup - See docs/PYTEST-OPTIMIZATION-PLAN.md for full analysis Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
103 lines
3.6 KiB
Python
103 lines
3.6 KiB
Python
"""Plugin introspection + enable/disable API."""
|
|
|
|
from flask import Blueprint, current_app, request
|
|
from flask_jwt_extended import jwt_required
|
|
|
|
from shopdb.utils.responses import success_response, error_response, ErrorCodes
|
|
|
|
from shopdb.utils.authz import require_role
|
|
from shopdb.utils.authz import require_permission
|
|
|
|
plugins_bp = Blueprint('plugins', __name__)
|
|
|
|
|
|
@plugins_bp.route('', methods=['GET'])
|
|
@require_permission('settings.view')
|
|
def list_plugins():
|
|
"""List all discovered plugins (enabled or not) with their metadata and
|
|
the framework contract version."""
|
|
from shopdb import __contract_version__
|
|
pm = current_app.extensions.get('plugin_manager')
|
|
plugins = pm.discover_available() if pm else []
|
|
plugins.sort(key=lambda p: p['name'])
|
|
return success_response({
|
|
'contract_version': __contract_version__,
|
|
'count': len(plugins),
|
|
'plugins': plugins,
|
|
})
|
|
|
|
|
|
@plugins_bp.route('/enabled', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
def list_enabled_plugins():
|
|
"""Return just the names of enabled plugins as a flat array.
|
|
|
|
Cheap registry read (no DB). Exposed to anonymous callers on purpose:
|
|
the navigation endpoint already leaks the same enabled/disabled signal,
|
|
and the frontend needs it (including unauthenticated kiosk routes like
|
|
/tv) to gate plugin-owned routes. No metadata beyond the names.
|
|
"""
|
|
pm = current_app.extensions.get('plugin_manager')
|
|
names = pm.registry.get_enabled_plugins() if pm else []
|
|
return success_response(sorted(names))
|
|
|
|
|
|
@plugins_bp.route('/settings-routes', methods=['GET'])
|
|
@jwt_required(optional=True)
|
|
def get_plugin_settings_routes():
|
|
"""Return settings routes contributed by all enabled plugins.
|
|
|
|
Plugins can register routes to add tabs/pages to settings hubs like
|
|
Model Catalog, Asset Config, etc. Frontend router merges these dynamically.
|
|
|
|
Returns: { routes: [{hub, path, name, tab, component}, ...] }
|
|
"""
|
|
pm = current_app.extensions.get('plugin_manager')
|
|
if not pm:
|
|
return success_response({'routes': []})
|
|
|
|
routes = []
|
|
for plugin_name, plugin in pm.get_all_plugins().items():
|
|
# Only include routes from enabled plugins
|
|
if not pm.registry.is_enabled(plugin_name):
|
|
continue
|
|
if hasattr(plugin, 'get_settings_routes'):
|
|
plugin_routes = plugin.get_settings_routes()
|
|
if plugin_routes:
|
|
routes.extend(plugin_routes)
|
|
|
|
return success_response({'routes': routes})
|
|
|
|
|
|
@plugins_bp.route('/<name>', methods=['PUT'])
|
|
@jwt_required()
|
|
@require_role('admin')
|
|
def set_plugin_enabled(name: str):
|
|
"""Enable or disable a plugin. Takes effect on the next app restart for
|
|
route/navigation changes."""
|
|
data = request.get_json() or {}
|
|
if 'enabled' not in data:
|
|
return error_response(ErrorCodes.VALIDATION_ERROR, 'enabled is required')
|
|
|
|
pm = current_app.extensions.get('plugin_manager')
|
|
if not pm:
|
|
return error_response(ErrorCodes.INTERNAL_ERROR,
|
|
'Plugin manager unavailable', http_code=500)
|
|
|
|
want = bool(data['enabled'])
|
|
ok = pm.enable_plugin(name) if want else pm.disable_plugin(name)
|
|
if not ok:
|
|
# enable/disable refused (unknown plugin, or a dependency conflict)
|
|
return error_response(
|
|
ErrorCodes.CONFLICT,
|
|
f"Could not {'enable' if want else 'disable'} '{name}' "
|
|
f"(check it exists and dependencies allow it)",
|
|
http_code=409
|
|
)
|
|
|
|
return success_response(
|
|
{'name': name, 'enabled': want},
|
|
message=f"Plugin {'enabled' if want else 'disabled'} "
|
|
f"(restart to apply route changes)"
|
|
)
|