Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Check plugin settings #230

Merged
merged 9 commits into from Nov 10, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
31 changes: 19 additions & 12 deletions documentation/dev/plugins.rst
Expand Up @@ -153,21 +153,28 @@ This will find `css/styles.css` if you add that folder and file to your Blueprin
Adding config settings
^^^^^^^^^^^^^^^^^^^^^^^^

FlexMeasures can be made to check for any custom config settings your plugin is using.
Required and optional config options can be registered by setting the ``__settings__`` attribute on your plugin module:
FlexMeasures can automatically check for you if any custom config settings, which your plugin is using, are present.
This can be very useful in maintaining installations of FlexMeasures with plugins.
Config settings can be registered by setting the (optional) ``__settings__`` attribute on your plugin module:

.. code-block:: python

__settings__ = {
"required": (
"MY_PLUGIN_SETTING_A",
"MY_PLUGIN_SETTING_B",
),
"optional": (
"MY_PLUGIN_SETTING_C",
"MY_PLUGIN_SETTING_D",
),
}
__settings__ = [
"MY_PLUGIN_URL": {
"description": "URL used by my plugin for x.",
"level": "error",
},
"MY_PLUGIN_TOKEN": {
"description": "Token used by my plugin for y.",
"level": "warning",
"message_if_missing": "Without this token, my plugin will not do y.",
"parse_as": str,
},
"MY_PLUGIN_COLOR": {
"description": "Color used to override the default plugin color.",
"level": "info",
},
]

You might want to override some FlexMeasures configuration settings from within your plugin. Some examples for possible settings are named on this page, e.g. the custom style (see above) or custom logo (see below). There is a `record_once` function on Blueprints which can help with this. Example:

Expand Down
103 changes: 73 additions & 30 deletions flexmeasures/utils/plugin_utils.py
@@ -1,21 +1,13 @@
import importlib.util
import os
import sys
import warnings
from importlib.abc import Loader
from typing import Any, Dict

import sentry_sdk
from flask import Flask, Blueprint


class ConfigurationWarning(Warning):
pass


class ConfigurationError(ValueError):
pass


def register_plugins(app: Flask):
"""
Register FlexMeasures plugins as Blueprints.
Expand Down Expand Up @@ -108,24 +100,75 @@ def register_plugins(app: Flask):
sentry_sdk.set_context("plugins", app.config.get("LOADED_PLUGINS", {}))


def check_config_settings(app, settings: dict):
"""Make sure expected config settings exist."""
missing_optional_config_settings = []
if "optional" in settings:
for setting in settings["optional"]:
if app.config.get(setting) is None:
missing_optional_config_settings.append(setting)
if missing_optional_config_settings:
warnings.warn(
f"Missing optional config setting(s): {missing_optional_config_settings}",
category=ConfigurationWarning,
)
if "required" in settings:
missing_required_config_settings = []
for setting in settings["required"]:
if app.config.get(setting) is None:
missing_required_config_settings.append(setting)
if missing_required_config_settings:
raise ConfigurationError(
f"Missing required config setting(s): {missing_required_config_settings}"
)
def check_config_settings(app, settings: Dict[str, dict]):
"""Make sure expected config settings exist.

For example:

settings = {
"MY_PLUGIN_URL": {
"description": "URL used by my plugin for x.",
"level": "error",
},
"MY_PLUGIN_TOKEN": {
"description": "Token used by my plugin for y.",
"level": "warning",
"message": "Without this token, my plugin will not do y.",
"parse_as": str,
},
"MY_PLUGIN_COLOR": {
"description": "Color used to override the default plugin color.",
"level": "info",
},
}

"""
assert isinstance(settings, dict), f"{settings} should be a dict"
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
for setting_name, setting_fields in settings.items():
assert isinstance(setting_fields, dict), f"{setting_name} should be a dict"

missing_config_settings = []
config_settings_with_wrong_type = []
for setting_name, setting_fields in settings.items():
setting = app.config.get(setting_name)
if setting is None:
missing_config_settings.append(setting_name)
elif "parse_as" in setting_fields and not isinstance(
setting, setting_fields["parse_as"]
):
config_settings_with_wrong_type.append((setting_name, setting))
for setting_name, setting in config_settings_with_wrong_type:
log_wrong_type_for_config_setting(
app, setting_name, settings[setting_name], setting
)
for setting_name in missing_config_settings:
log_missing_config_setting(app, setting_name, settings[setting_name])


def log_wrong_type_for_config_setting(
app, setting_name: str, setting_fields: dict, setting: Any
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
):
"""Log a message for this config setting that has the wrong type."""
app.logger.warning(
f"Config setting '{setting_name}' is a {type(setting)} whereas a {setting_fields['parse_as']} was expected."
)


def log_missing_config_setting(app, setting_name: str, setting_fields: dict):
"""Log a message for this missing config setting.

The logging level is taken from the 'level' key. If missing, we default to error.
If present, we also log the 'description' and the 'message_if_missing' keys.
"""
message_if_missing = (
f" {setting_fields['message_if_missing']}"
if "message_if_missing" in setting_fields
else ""
)
description = (
f" ({setting_fields['description']})" if "description" in setting_fields else ""
)
level = setting_fields["level"] if "level" in setting_fields else "error"
getattr(app.logger, level)(
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
f"Missing config setting '{setting_name}'{description}.{message_if_missing}",
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
)