flexmeasures.utils.plugin_utils

Utils for registering FlexMeasures plugins

Functions

flexmeasures.utils.plugin_utils.check_config_settings(app, settings: dict[str, dict])

Make sure expected config settings exist.

Plugin settings that are not in the app config yet are looked up in the environment, so a plugin setting can be set the same way a FlexMeasures setting can. Settings that are still missing afterwards are logged, and are set to the “default” that the plugin declared for them, if any. Whatever a setting ends up holding, a declared default included, is checked against its “parse_as” type.

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_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”, “default”: “blue”,

},

}

flexmeasures.utils.plugin_utils.find_importable_module(pkg_name: str) ModuleSpec | None

Find the spec of an importable module, if there is one.

A single-file module counts, just like a package: register_plugins imports either by name. Namespace packages do not. A folder without an __init__.py is importable, but accepting it here would shadow the clearer error that the file path branch of register_plugins reports for such a folder.

flexmeasures.utils.plugin_utils.is_written_as_path(plugin: str) bool

Whether this FLEXMEASURES_PLUGINS entry is spelled out as a file path.

A bare name like my_plugin is not: it may well name an installed package.

flexmeasures.utils.plugin_utils.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.

We close with whether the setting falls back to the ‘default’ the plugin declared, or stays unset, so that a ‘message_if_missing’ promising a fallback cannot leave the impression that a setting without one is optional.

flexmeasures.utils.plugin_utils.log_wrong_type_for_config_setting(app, setting_name: str, setting_fields: dict, setting_type: type)

Log a message for this config setting that has the wrong type.

flexmeasures.utils.plugin_utils.parse_setting_from_env(app: Flask, setting_name: str, value: str, parse_as: type | None)

Interpret an environment variable as the type the plugin declared for it.

Environment variables are always strings, so a setting that should be, say, an int is converted here. Lists and dicts are expected to be JSON-encoded. A value we cannot convert is passed on unconverted, which lets the type check in check_config_settings report it to the plugin author.

flexmeasures.utils.plugin_utils.read_plugin_settings_from_env(app: Flask, settings: dict[str, dict])

Fill in plugin-declared settings that are still unset from the environment.

Plugins are registered after the config file has been read, so a setting that already has a value keeps it, and the environment only fills the gaps. That is the same precedence that FlexMeasures’ own settings get, where the environment is read first and the config file may then override it.

Like FlexMeasures’ own settings, plugin settings are not read from the environment while testing or while building the documentation, which both run on defaults.

flexmeasures.utils.plugin_utils.register_plugins(app: Flask)

Register FlexMeasures plugins as Blueprints. This is configured by the config setting FLEXMEASURES_PLUGINS.

Assumptions: - a setting EITHER points to a plugin folder containing an __init__.py file

OR it is the name of an installed module, which can be imported.

  • each plugin defines at least one Blueprint object. These will be registered with the Flask app, so their functionality (e.g. routes) becomes available.

If you load a plugin via a file path, we’ll refer to the plugin with the name of your plugin folder (last part of the path).

An entry that is not spelled out as a file path is imported by name, so that normal import resolution along sys.path applies, rather than loaded from the folder of that name in the working directory. An installed plugin therefore wins from such a folder, unless the working directory itself comes first on sys.path. To load a folder on purpose, spell out its path (e.g. ./my_plugin).