Source code for panoptes.utils.config.helpers
from contextlib import suppress
from pathlib import Path
from loguru import logger
from panoptes.utils import error
from panoptes.utils.serializers import from_yaml, to_yaml
from panoptes.utils.utils import listify
[docs]
def load_config(
config_files: str | Path | list | None = None, parse: bool = True, load_local: bool = True
) -> dict:
"""Loads configuration information from one or more YAML files.
This function is used by the config server; normal config usage should
be via a running config server.
If no options are passed to `config_files`, the default `$PANOPTES_CONFIG_FILE`
will be loaded. Multiple files can be specified and are loaded in order,
with later files overwriting values from earlier ones. Local versions of files
(named `<name>_local.yaml`) can override built-in versions if present.
Args:
config_files (str | Path | List | None, optional): A path or list of paths to config files.
If None, uses the default config file. Files are loaded in order.
parse (bool, optional): Whether to parse objects such as dates and astropy units.
Defaults to True.
load_local (bool, optional): Whether to load local override files (ending with `_local.yaml`)
if present. Defaults to True.
Returns:
dict: Dictionary of configuration items.
Raises:
ruamel.yaml.parser.ParserError: If a YAML file cannot be parsed.
IOError: If a config file cannot be read.
TypeError: If a config file contains invalid data types.
Notes:
Local files are automatically loaded if they exist alongside the specified config path.
Local files can be ignored by setting `load_local=False`.
"""
config = dict()
config_files = listify(config_files)
logger.debug(f"Loading config files: config_files={config_files!r}")
for config_file in config_files:
config_file = Path(config_file)
logger.debug(f"Adding config_file={config_file!r} to config dict")
_add_to_conf(config, config_file, parse=parse)
# Load local version of config
if load_local:
local_version = config_file.parent / Path(config_file.stem + "_local.yaml")
if local_version.exists():
_add_to_conf(config, local_version, parse=parse)
# parse_config_directories currently only corrects directory names.
if parse:
logger.trace(f"Parsing config={config!r}")
with suppress(KeyError):
config["directories"] = parse_config_directories(config["directories"])
logger.trace(f"Config directories parsed: config={config!r}")
return config
[docs]
def save_config(save_path: Path, config: dict, overwrite: bool = True) -> bool:
"""Save config to local yaml file.
Args:
save_path (str): Path to save, can be relative or absolute. See Notes in
``load_config``.
config (dict): Config to save.
overwrite (bool, optional): True if file should be updated, False
to generate a warning for existing config. Defaults to True
for updates.
Returns:
bool: If the save was successful.
Raises:
FileExistsError: If the local path already exists and ``overwrite=False``.
"""
# Make sure it's a path.
save_path = Path(save_path)
# Make sure ends with '_local.yaml'.
if save_path.stem.endswith("_local") is False:
save_path = save_path.with_name(save_path.stem + "_local.yaml")
if save_path.exists() and overwrite is False:
raise FileExistsError(f"Path exists and overwrite=False: {save_path}")
else:
# Create directory if it does not exist.
save_path.parent.mkdir(parents=True, exist_ok=True)
logger.info(f"Saving config to {save_path}")
with save_path.open("w") as fn:
to_yaml(config, stream=fn)
logger.success(f"Config info saved to {save_path}")
return True
[docs]
def parse_config_directories(directories: dict[str, str]) -> dict:
"""Parse the config dictionary for common objects.
Given a `base` entry that corresponds to the absolute path of a directory,
prepend the `base` to all other relative directory entries.
The `base` directory must exist or an exception is rasied.
If the `base` entry is not given the current working directory is used.
.. doctest::
>>> dirs_config = dict(base='/tmp', foo='bar', baz='bam', app='/app')
>>> parse_config_directories(dirs_config)
{'base': '/tmp', 'foo': '/tmp/bar', 'baz': '/tmp/bam', 'app': '/app'}
>>> # If base doesn't exist an exception is raised.
>>> dirs_config = dict(base='/panoptes', foo='bar', baz='bam', app='/app')
>>> parse_config_directories(dirs_config)
Traceback (most recent call last):
...
panoptes.utils.error.NotFound: NotFound: Base directory does not exist: /panoptes
Args:
directories (dict): The dictionary of directory information. Usually comes
from the "directories" entry in the config.
Returns:
dict: The same directory but with relative directories resolved.
Raises:
panoptes.utils.error.NotFound: if the 'base' entry is given but does not exist.
"""
resolved_dirs = directories.copy()
# Try to get the base directory first.
base_dir = Path(resolved_dirs.get("base", ".")).absolute()
# Warn if base directory does not exist.
if base_dir.is_dir() is False:
raise error.NotFound(f"Base directory does not exist: {base_dir}")
# Add back absolute path for base directory.
resolved_dirs["base"] = str(base_dir)
logger.trace(f"Using base_dir={base_dir!r} for setting config directories")
# Add the base directory to any relative dir.
for dir_name, dir_path in resolved_dirs.items():
if dir_path.startswith("/") is False and dir_name != "base":
sub_dir = (base_dir / dir_path).absolute()
if sub_dir.exists() is False:
logger.warning(f"{sub_dir!r} does not exist.")
logger.trace(f"Setting {dir_name} to {sub_dir}")
resolved_dirs[dir_name] = str(sub_dir)
return resolved_dirs
def _add_to_conf(config: dict, conf_fn: Path, parse: bool = False) -> None:
"""Add configuration from file to existing config dictionary.
Args:
config (dict): Configuration dictionary to update.
conf_fn (Path): Path to configuration file.
parse (bool, optional): Whether to parse YAML values. Defaults to False.
"""
with suppress(IOError, TypeError):
with conf_fn.open("r") as fn:
config.update(from_yaml(fn.read(), parse=parse))