Source code for configuration.configurator

"""Application configuration."""

import argparse
import logging
import os
import sys
import uuid

import astropy.units as u
from dotenv import load_dotenv

import simtools.configuration.commandline_parser as argparser
from simtools.io_operations import io_handler
from simtools.utils import general as gen

__all__ = [
    "Configurator",
    "InvalidConfigurationParameterError",
]


[docs] class InvalidConfigurationParameterError(Exception): """Exception for Invalid configuration parameter."""
[docs] class Configurator: """ Application configuration. Allow to set configuration parameters by - command line arguments - configuration file (yml file) - configuration dict when calling the class - environmental variables Assigns unique ACTIVITY_ID to this configuration (uuid). Configuration parameter names are converted always to lower case. Parameters ---------- config: dict Configuration parameters as dict. label: str Class label. usage: str Application usage description. description: str Text displayed as description. epilog: str Text display after all arguments. """ def __init__(self, config=None, label=None, usage=None, description=None, epilog=None): """Initialize Configurator.""" self._logger = logging.getLogger(__name__) self._logger.debug("Init Configuration") self.config_class_init = config self.label = label self.config = {} self.parser = argparser.CommandLineParser( prog=self.label, usage=usage, description=description, epilog=epilog, formatter_class=argparse.ArgumentDefaultsHelpFormatter, )
[docs] def default_config(self, arg_list=None, add_db_config=False): """ Return dictionary of default configuration. Parameters ---------- arg_list: list List of arguments. add_db_config: bool Add DB configuration file. Returns ------- dict Configuration parameters as dict. """ self.parser.initialize_default_arguments() simulation_model = None if arg_list and "--site" in arg_list: simulation_model = ["site"] if arg_list and "--telescope" in arg_list: simulation_model = ["site", "telescope"] self.parser.initialize_simulation_model_arguments(simulation_model) if add_db_config: self.parser.initialize_db_config_arguments() self._fill_config(arg_list) return self.config
[docs] def initialize( self, require_command_line=True, paths=True, output=False, simulation_model=None, simulation_configuration=None, db_config=False, job_submission=False, ): """ Initialize application configuration. Configure from command line, configuration file, class config, or environmental variable. Priorities in parameter settings. 1. command line; 2. yaml file; 3. class init; 4. env variables. Conflicting configuration settings raise an Exception, with the exception of settings \ from environmental variables, which are only done when the configuration parameter \ is None. Parameters ---------- require_command_line: bool Require at least one command line argument. paths: bool Add path configuration to list of args. output: bool Add output file configuration to list of args. simulation_model: list List of simulation model configuration parameters to add to list of args simulation_configuration: dict Dict of simulation software configuration parameters to add to list of args. db_config: bool Add database configuration parameters to list of args. job_submission: bool Add job submission configuration to list of args. Returns ------- dict Configuration parameters as dict. dict Dictionary with DB parameters """ self.parser.initialize_default_arguments( paths=paths, output=output, simulation_model=simulation_model, simulation_configuration=simulation_configuration, db_config=db_config, job_submission=job_submission, ) self._fill_from_command_line(require_command_line=require_command_line) self._fill_from_config_file(self.config.get("config")) self._fill_from_config_dict(self.config_class_init) self._fill_from_environmental_variables() if self.config.get("activity_id", None) is None: self.config["activity_id"] = str(uuid.uuid4()) if self.config["label"] is None: self.config["label"] = self.label self._initialize_io_handler() if output: self._initialize_output() _db_dict = self._get_db_parameters() return self.config, _db_dict
def _fill_from_command_line(self, arg_list=None, require_command_line=True): """ Fill configuration parameters from command line arguments. Triggers a print of the help if no command line arguments are given and require_command_line is set. Parameters ---------- arg_list: list List of arguments. require_command_line: bool Require at least one command line argument. """ if arg_list is None: arg_list = sys.argv[1:] if require_command_line and len(arg_list) == 0: self._logger.debug("No command line arguments given, printing help.") arg_list = ["--help"] if "--config" in arg_list: self._reset_required_arguments() self._fill_config(arg_list) def _reset_required_arguments(self): """ Reset required parser arguments (i.e., arguments added with "required=True"). Includes also mutually exclusive groups. Access protected attributes of parser (no public method available). """ for group in self.parser._mutually_exclusive_groups: # pylint: disable=protected-access group.required = False for action in self.parser._actions: # pylint: disable=protected-access action.required = False def _fill_from_config_dict(self, input_dict, overwrite=False): """ Fill configuration parameters from dictionary. Enforce that configuration parameter names are lower case. Parameters ---------- input_dict: dict dictionary with configuration parameters. overwrite: bool overwrite existing configuration parameters. """ _tmp_config = {} try: for key, value in input_dict.items(): if not overwrite: self._check_parameter_configuration_status(key, value) _tmp_config[key.lower()] = value except AttributeError: pass self._fill_config(_tmp_config) def _check_parameter_configuration_status(self, key, value): """ Check if a parameter is already configured and not still set to the default value. Allow configuration with None values. Parameters ---------- key, value parameter key, value to be checked Raises ------ InvalidConfigurationParameterError if parameter has already been defined with a different value. """ # parameter not changed or None if self.parser.get_default(key) == self.config[key] or self.config[key] is None: return # parameter already set if key in self.config and self.config[key] != value: self._logger.error( f"Inconsistent configuration parameter ({key}) definition " f"({self.config[key]} vs {value})" ) raise InvalidConfigurationParameterError def _fill_from_config_file(self, config_file): """ Read and fill configuration parameters from yaml file. Parameters ---------- config file: str Name of configuration file name Raises ------ FileNotFoundError if configuration file has not been found. """ try: self._logger.debug(f"Reading configuration from {config_file}") _config_dict = gen.collect_data_from_file_or_dict( file_name=config_file, in_dict=None, allow_empty=True ) # yaml parser adds \n in multiline strings, remove them _config_dict = gen.remove_substring_recursively_from_dict(_config_dict, substring="\n") if "CTA_SIMPIPE" in _config_dict: try: self._fill_from_config_dict( input_dict=gen.change_dict_keys_case( _config_dict["CTA_SIMPIPE"]["CONFIGURATION"], ), overwrite=True, ) except KeyError: self._logger.info(f"No CTA_SIMPIPE:CONFIGURATION dict found in {config_file}.") else: self._fill_from_config_dict( input_dict=gen.change_dict_keys_case(_config_dict), overwrite=True ) # TypeError is raised for config_file=None except (TypeError, AttributeError): pass except FileNotFoundError: self._logger.error(f"Configuration file not found: {config_file}") raise def _fill_from_environmental_variables(self): """ Fill any configuration parameters from environmental variables or from file (e.g., ".env"). Only parameters which are not already configured are changed (i.e., parameter is None). """ _env_dict = {} try: load_dotenv(self.config["env_file"]) except KeyError: pass for key, value in self.config.items(): # environmental variables for simtools should always start with SIMTOOLS_ env_variable_to_read = f"SIMTOOLS_{key.upper()}" if value is None: env_value = os.environ.get(env_variable_to_read) if env_value is not None: env_value = env_value.split("#")[0].strip().replace('"', "").replace("'", "") _env_dict[key] = env_value self._fill_from_config_dict(_env_dict) def _initialize_io_handler(self): """Initialize IOHandler with input and output paths.""" _io_handler = io_handler.IOHandler() _io_handler.set_paths( output_path=self.config.get("output_path", None), use_plain_output_path=self.config.get("use_plain_output_path", False), data_path=self.config.get("data_path", None), model_path=self.config.get("model_path", None), ) def _initialize_output(self): """Initialize default output file names (in case output_file is not configured).""" if self.config.get("output_file", None) is None: self.config["output_file_from_default"] = True prefix = "TEST" label = extension = "" if not self.config.get("test", False): prefix = self.config["activity_id"] if self.config.get("label", "") and len(self.config.get("label", "")) > 0: label = f"-{self.config['label']}" if len(self.config.get("output_file_format", "")) > 0: extension = f".{self.config['output_file_format']}" self.config["output_file"] = f"{prefix}{label}{extension}" @staticmethod def _arglist_from_config(input_var): """ Convert input list of strings as needed by argparse. Special cases: - lists as arguments (using e.g., nargs="+") are expanded - boolean are expected to be handled as action="store_true" or "store_false" - None values or zero length values are ignored (this means setting a parameter to none or "" is not allowed). Ignore values which are None or of zero length. Parameters ---------- input_var: dict, list, None Dictionary/list of commands to convert to list. Returns ------- list Dict keys and values as dict. """ if isinstance(input_var, dict): _list_args = [] for key, value in input_var.items(): if isinstance(value, list): _list_args.append("--" + key) _list_args += value elif isinstance(value, u.Quantity) or ( not isinstance(value, bool) and value is not None and len(str(value)) > 0 ): _list_args.append("--" + key) _list_args.append(str(value)) elif value: _list_args.append("--" + key) return _list_args try: return [str(value) for value in list(input_var) if value != "None"] except TypeError: return [] @staticmethod def _convert_string_none_to_none(input_dict): """ Convert string type 'None' to type None (argparse returns None as str). Parameters ---------- input_dict Dictionary with values to be converted. """ for key, value in input_dict.items(): input_dict[key] = None if value == "None" else value return input_dict def _fill_config(self, input_container): """ Fill configuration dictionary. Parameters ---------- input_container List or dictionary with configuration updates. """ self.config = self._convert_string_none_to_none( vars( self.parser.parse_args( self._arglist_from_config(self.config) + self._arglist_from_config(input_container) ) ) ) def _get_db_parameters(self): """ Return parameters for DB configuration. Parameters ---------- dict Dictionary with DB parameters """ _db_dict = {} _db_para = ( "db_api_user", "db_api_pw", "db_api_port", "db_server", "db_simulation_model", "db_simulation_model_url", ) try: for _para in _db_para: _db_dict[_para] = self.config[_para] except KeyError: pass return _db_dict