from pathlib import Path
from typing import Any, Optional, cast
import yaml
from restapi.utilities import print_and_exit
from restapi.utilities.logs import log
PROJECTS_DEFAULTS_FILE = Path("projects_defaults.yaml")
PROJECT_CONF_FILENAME = Path("project_configuration.yaml")
ConfigurationType = dict[str, Any]
[docs]
def read_configuration(
default_file_path: Path,
base_project_path: Path,
projects_path: Path,
submodules_path: Path,
) -> tuple[ConfigurationType, Optional[str], Optional[Path]]:
"""
Read default configuration
"""
custom_configuration = load_yaml_file(
base_project_path.joinpath(PROJECT_CONF_FILENAME)
)
# Verify custom project configuration
project = custom_configuration.get("project")
# Can't be tested because it is included in default configuration
if project is None: # pragma: no cover
raise AttributeError("Missing project configuration")
base_configuration = load_yaml_file(
default_file_path.joinpath(PROJECTS_DEFAULTS_FILE)
)
extended_project = project.get("extends")
if extended_project is None:
# Mix default and custom configuration
return mix(base_configuration, custom_configuration), None, None
extends_from = project.get("extends_from", "projects")
if extends_from == "projects":
extend_path = projects_path
elif extends_from.startswith("submodules/"): # pragma: no cover
repository_name = (extends_from.split("/")[1]).strip()
if repository_name == "":
print_and_exit("Invalid repository name in extends_from, name is empty")
extend_path = submodules_path
else: # pragma: no cover
suggest = "Expected values: 'projects' or 'submodules/${REPOSITORY_NAME}'"
print_and_exit("Invalid extends_from parameter: {}.\n{}", extends_from, suggest)
if not extend_path.exists(): # pragma: no cover
print_and_exit("From project not found: {}", str(extend_path))
extend_file = Path(f"extended_{PROJECT_CONF_FILENAME}")
extended_configuration = load_yaml_file(extend_path.joinpath(extend_file))
m1 = mix(base_configuration, extended_configuration)
return mix(m1, custom_configuration), extended_project, extend_path
[docs]
def mix(base: ConfigurationType, custom: ConfigurationType) -> ConfigurationType:
for key, elements in custom.items():
if key not in base:
base[key] = custom[key]
continue
if elements is None:
if isinstance(base[key], dict):
log.warning("Cannot replace {} with empty list", key)
continue
if isinstance(elements, dict):
mix(base[key], custom[key])
elif isinstance(elements, list):
for e in elements:
base[key].append(e)
else:
base[key] = elements
return base
[docs]
def load_yaml_file(filepath: Path) -> ConfigurationType:
if not filepath.exists():
raise AttributeError(f"YAML file does not exist: {filepath}")
with open(filepath) as fh:
try:
docs = list(yaml.safe_load_all(fh))
if not docs:
raise AttributeError(f"YAML file is empty: {filepath}")
return cast(ConfigurationType, docs[0])
except Exception as e:
# # IF dealing with a strange exception string (escaped)
# import codecs
# error, _ = codecs.getdecoder("unicode_escape")(str(error))
raise AttributeError(f"Failed to read file {filepath}: {e}") from e