Skip to content
11 changes: 1 addition & 10 deletions cea/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import cea.inputlocator
import cea.plugin
from cea.utilities import unique
from cea.utilities import unique, parse_string_to_list

__author__ = "Daren Thomas"
__copyright__ = "Copyright 2017, Architecture and Building Systems - ETH Zurich"
Expand Down Expand Up @@ -1192,15 +1192,6 @@ class ScenarioNameMultiChoiceParameter(MultiChoiceParameter, ScenarioNameParamet
pass


def parse_string_to_list(line):
"""Parse a line in the csv format into a list of strings"""
if line is None:
return []
line = line.replace('\n', ' ')
line = line.replace('\r', ' ')
return [str(field.strip()) for field in line.split(',') if field.strip()]


def parse_string_coordinate_list(string_tuples):
"""Parse a string of comma-separated coordinate tuples into a list of tuples"""
numerical = r'\d+(\.\d+)?'
Expand Down
4 changes: 2 additions & 2 deletions cea/interfaces/cli/cea_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
import sys

import cea.config
import cea.inputlocator
import cea.scripts
from cea.plugin import instantiate_plugin
from cea.utilities import parse_string_to_list

__author__ = "Daren Thomas"
__copyright__ = "Copyright 2017, Architecture and Building Systems - ETH Zurich"
Expand Down Expand Up @@ -46,7 +46,7 @@ def main(args=None):
elif script_name == "add-plugins":
plugins_fqname = "general:plugins"
parameter = config.get_parameter(plugins_fqname)
plugins = cea.config.parse_string_to_list(parameter.encode(parameter.get()))
plugins = parse_string_to_list(parameter.encode(parameter.get()))

new_plugins = [p for p in args if p not in plugins] # filter existing plugins
valid_plugins = [p for p in new_plugins if instantiate_plugin(p) is not None]
Expand Down
4 changes: 2 additions & 2 deletions cea/plots/cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,6 @@
import os
import time

import pandas as pd


class PlotCache(object):
"""A cache for plot data. Use the ``lookup`` method to retrieve data from the cache."""
Expand Down Expand Up @@ -122,6 +120,8 @@ def store_cached_value(self, data_path, parameters, producer):

def load_cached_value(self, data_path, parameters):
"""Load a Dataframe from disk"""
import pandas as pd

return pd.read_pickle(self._cached_data_file(data_path, parameters))


Expand Down
6 changes: 6 additions & 0 deletions cea/plugin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from .loader import instantiate_plugin, add_plugins

# CEA Plugin base class where user plugins import from
from .base import CeaPlugin

__all__ = ["instantiate_plugin", "add_plugins", "CeaPlugin"]
87 changes: 87 additions & 0 deletions cea/plugin/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
"""
A base class for creating CEA plugins. Subclass this class in your own namespace to become a CEA plugin.
"""

import configparser
import inspect
import os

import yaml


__author__ = "Daren Thomas"
__copyright__ = "Copyright 2020, Architecture and Building Systems - ETH Zurich"
__credits__ = ["Daren Thomas"]
__license__ = "MIT"
__version__ = "0.1"
__maintainer__ = "Daren Thomas"
__email__ = "[email protected]"
__status__ = "Production"


class CeaPlugin:
"""
A CEA Plugin defines a list of scripts and a list of plots - the CEA uses this to populate the GUI
and other interfaces. In addition, any input- and output files need to be defined.
"""

@property
def scripts(self):
"""Return the scripts.yml dictionary."""
scripts_yml = os.path.join(os.path.dirname(inspect.getmodule(self).__file__), "scripts.yml")
if not os.path.exists(scripts_yml):
return {}
with open(scripts_yml, "r") as scripts_yml_fp:
scripts = yaml.safe_load(scripts_yml_fp) or {}
return scripts

@property
def plot_categories(self):
"""
Return a list of :py:class`cea.plots.PlotCategory` instances to add to the GUI. The default implementation
uses the ``plots.yml`` file to create PluginPlotCategory instances that use PluginPlotBase.
"""
from .plot_category import PluginPlotCategory

plots_yml = os.path.join(os.path.dirname(inspect.getmodule(self).__file__), "plots.yml")
if not os.path.exists(plots_yml):
return []
with open(plots_yml, "r") as plots_yml_fp:
categories = yaml.safe_load(plots_yml_fp) or {}
return [PluginPlotCategory(category_label, categories[category_label], self) for category_label in
categories.keys()]

@property
def schemas(self):
"""Return the schemas dict for this plugin - it should be in the same format as ``cea/schemas.yml``

(You don't actually have to implement this for your own plugins - having a ``schemas.yml`` file in the same
folder as the plugin class will trigger the default behavior)
"""
schemas_yml = os.path.join(os.path.dirname(inspect.getmodule(self).__file__), "schemas.yml")
if not os.path.exists(schemas_yml):
return {}
with open(schemas_yml, "r") as schemas_yml_fp:
schemas = yaml.safe_load(schemas_yml_fp) or {}
return schemas

@property
def config(self):
"""
Return the configuration for this plugin - the `cea.config.Configuration` object will include these.

The format is expected to be the same format as `default.config` in the CEA.

:rtype: configparser.ConfigParser
"""

plugin_config = os.path.join(os.path.dirname(inspect.getmodule(self).__file__), "plugin.config")
parser = configparser.ConfigParser()
if not os.path.exists(plugin_config):
return parser
parser.read(plugin_config)
return parser

def __str__(self):
"""To enable encoding in cea.config.PluginListParameter, return the fqname of the class"""
return "{module}.{name}".format(module=self.__class__.__module__, name=self.__class__.__name__)
48 changes: 48 additions & 0 deletions cea/plugin/loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import importlib
import warnings

from cea.utilities import parse_string_to_list


def instantiate_plugin(plugin_fqname):
"""Return a new CeaPlugin based on it's fully qualified name - this is how the config object creates plugins"""
try:
plugin_path = plugin_fqname.split(".")
plugin_module = ".".join(plugin_path[:-1])
plugin_class = plugin_path[-1]
module = importlib.import_module(plugin_module)
instance = getattr(module, plugin_class)()
return instance
except Exception as ex:
warnings.warn(f"Could not instantiate plugin {plugin_fqname} ({ex})")
return None


def add_plugins(default_config, user_config):
"""
Patch in the plugin configurations during __init__ and __setstate__

:param configparser.ConfigParser default_config:
:param configparser.ConfigParser user_config:
:return: (modifies default_config and user_config in-place)
:rtype: None
"""
plugin_fqnames = parse_string_to_list(user_config.get("general", "plugins"))
for plugin in [instantiate_plugin(plugin_fqname) for plugin_fqname in plugin_fqnames]:
if plugin is None:
# plugin could not be instantiated
continue
for section_name in plugin.config.sections():
if section_name in default_config.sections():
raise ValueError("Plugin tried to redefine config section {section_name}".format(
section_name=section_name))
default_config.add_section(section_name)
if not user_config.has_section(section_name):
user_config.add_section(section_name)
for option_name in plugin.config.options(section_name):
if option_name in default_config.options(section_name):
raise ValueError("Plugin tried to redefine parameter {section_name}:{option_name}".format(
section_name=section_name, option_name=option_name))
default_config.set(section_name, option_name, plugin.config.get(section_name, option_name))
if "." not in option_name and not user_config.has_option(section_name, option_name):
user_config.set(section_name, option_name, default_config.get(section_name, option_name))
134 changes: 3 additions & 131 deletions cea/plugin.py → cea/plugin/plot_category.py
Original file line number Diff line number Diff line change
@@ -1,98 +1,14 @@
"""
A base class for creating CEA plugins. Subclass this class in your own namespace to become a CEA plugin.
"""

import configparser
import importlib
import inspect
import os
import warnings

import yaml

import cea.inputlocator
import cea.plots.categories
import cea.schemas

from cea.plots.base import PlotBase
from cea.plots.categories import PlotCategory
from cea.utilities import identifier

__author__ = "Daren Thomas"
__copyright__ = "Copyright 2020, Architecture and Building Systems - ETH Zurich"
__credits__ = ["Daren Thomas"]
__license__ = "MIT"
__version__ = "0.1"
__maintainer__ = "Daren Thomas"
__email__ = "[email protected]"
__status__ = "Production"


class CeaPlugin:
"""
A CEA Plugin defines a list of scripts and a list of plots - the CEA uses this to populate the GUI
and other interfaces. In addition, any input- and output files need to be defined.
"""

@property
def scripts(self):
"""Return the scripts.yml dictionary."""
scripts_yml = os.path.join(os.path.dirname(inspect.getmodule(self).__file__), "scripts.yml")
if not os.path.exists(scripts_yml):
return {}
with open(scripts_yml, "r") as scripts_yml_fp:
scripts = yaml.safe_load(scripts_yml_fp)
return scripts

@property
def plot_categories(self):
"""
Return a list of :py:class`cea.plots.PlotCategory` instances to add to the GUI. The default implementation
uses the ``plots.yml`` file to create PluginPlotCategory instances that use PluginPlotBase.
"""
plots_yml = os.path.join(os.path.dirname(inspect.getmodule(self).__file__), "plots.yml")
if not os.path.exists(plots_yml):
return {}
with open(plots_yml, "r") as plots_yml_fp:
categories = yaml.load(plots_yml_fp, Loader=yaml.CLoader)
return [PluginPlotCategory(category_label, categories[category_label], self) for category_label in
categories.keys()]

@property
def schemas(self):
"""Return the schemas dict for this plugin - it should be in the same format as ``cea/schemas.yml``

(You don't actually have to implement this for your own plugins - having a ``schemas.yml`` file in the same
folder as the plugin class will trigger the default behavior)
"""
schemas_yml = os.path.join(os.path.dirname(inspect.getmodule(self).__file__), "schemas.yml")
if not os.path.exists(schemas_yml):
return {}
with open(schemas_yml, "r") as schemas_yml_fp:
schemas = yaml.load(schemas_yml_fp, Loader=yaml.CLoader)
return schemas

@property
def config(self):
"""
Return the configuration for this plugin - the `cea.config.Configuration` object will include these.

The format is expected to be the same format as `default.config` in the CEA.

:rtype: configparser.ConfigParser
"""

plugin_config = os.path.join(os.path.dirname(inspect.getmodule(self).__file__), "plugin.config")
parser = configparser.ConfigParser()
if not os.path.exists(plugin_config):
return parser
parser.read(plugin_config)
return parser

def __str__(self):
"""To enable encoding in cea.config.PluginListParameter, return the fqname of the class"""
return "{module}.{name}".format(module=self.__class__.__module__, name=self.__class__.__name__)


class PluginPlotCategory(cea.plots.categories.PlotCategory):
class PluginPlotCategory(PlotCategory):
"""
Normally, a PlotCategory reads its plot classes by traversing a folder structure and importing all modules found
there. The PluginPlotCategory works just like a PlotCategory (i.e. compatible with the CEA GUI / Dashboard) but
Expand Down Expand Up @@ -226,47 +142,3 @@ def output_path(self):
return self.locator.get_timeseries_plots_file(file_name, self.category_path)




def instantiate_plugin(plugin_fqname):
"""Return a new CeaPlugin based on it's fully qualified name - this is how the config object creates plugins"""
try:
plugin_path = plugin_fqname.split(".")
plugin_module = ".".join(plugin_path[:-1])
plugin_class = plugin_path[-1]
module = importlib.import_module(plugin_module)
instance = getattr(module, plugin_class)()
return instance
except BaseException as ex:
warnings.warn(f"Could not instantiate plugin {plugin_fqname} ({ex})")
return None


def add_plugins(default_config, user_config):
"""
Patch in the plugin configurations during __init__ and __setstate__

:param configparser.ConfigParser default_config:
:param configparser.ConfigParser user_config:
:return: (modifies default_config and user_config in-place)
:rtype: None
"""
plugin_fqnames = cea.config.parse_string_to_list(user_config.get("general", "plugins"))
for plugin in [instantiate_plugin(plugin_fqname) for plugin_fqname in plugin_fqnames]:
if plugin is None:
# plugin could not be instantiated
continue
for section_name in plugin.config.sections():
if section_name in default_config.sections():
raise ValueError("Plugin tried to redefine config section {section_name}".format(
section_name=section_name))
default_config.add_section(section_name)
if not user_config.has_section(section_name):
user_config.add_section(section_name)
for option_name in plugin.config.options(section_name):
if option_name in default_config.options(section_name):
raise ValueError("Plugin tried to redefine parameter {section_name}:{option_name}".format(
section_name=section_name, option_name=option_name))
default_config.set(section_name, option_name, plugin.config.get(section_name, option_name))
if "." not in option_name and not user_config.has_option(section_name, option_name):
user_config.set(section_name, option_name, default_config.get(section_name, option_name))
Loading