import sys from configparser import ConfigParser from os import environ from pathlib import Path from typing import Any, Dict, Optional, Union from urllib.parse import urlunsplit from loguru import logger # pylint: disable=no-name-in-module from pydantic import BaseModel, ValidationError from pydantic.fields import ModelField from pydantic.utils import deep_update from yaml import YAMLError, safe_load DEFAULT_ENV_PREFIX = "LIBRETIME" DEFAULT_CONFIG_FILEPATH = Path("/etc/libretime/config.yml") # pylint: disable=too-few-public-methods class BaseConfig(BaseModel): """ Read and validate the configuration from 'filepath' and os environment. :param filepath: yaml configuration file to read from :param env_prefix: prefix for the environment variable names :param env_delimiter: delimiter for the environment variable names :returns: configuration class """ def __init__( self, *, env_prefix: str = DEFAULT_ENV_PREFIX, env_delimiter: str = "_", filepath: Optional[Union[Path, str]] = None, ) -> None: if filepath is not None: filepath = Path(filepath) file_values = self._load_file_values(filepath) env_values = self._load_env_values(env_prefix, env_delimiter) try: super().__init__(**deep_update(file_values, env_values)) except ValidationError as error: logger.critical(error) sys.exit(1) def _load_env_values(self, env_prefix: str, env_delimiter: str) -> Dict[str, Any]: return self._get_fields_from_env(env_prefix, env_delimiter, self.__fields__) def _get_fields_from_env( self, env_prefix: str, env_delimiter: str, fields: Dict[str, ModelField], ) -> Dict[str, Any]: result: Dict[str, Any] = {} if env_prefix != "": env_prefix += env_delimiter for field in fields.values(): env_name = (env_prefix + field.name).upper() if field.is_complex(): children = self._get_fields_from_env( env_name, env_delimiter, field.type_.__fields__, ) if len(children) != 0: result[field.name] = children else: if env_name in environ: result[field.name] = environ[env_name] return result # pylint: disable=no-self-use def _load_file_values( self, filepath: Optional[Path] = None, ) -> Dict[str, Any]: if filepath is None: logger.debug("no config filepath is provided") return {} if not filepath.is_file(): logger.warning(f"provided config filepath '{filepath}' is not a file") return {} # pylint: disable=fixme # TODO: Remove ability to load ini files once yaml if fully supported. if filepath.suffix == ".conf": config = ConfigParser() config.read_string(filepath.read_text(encoding="utf-8")) return {s: dict(config.items(s)) for s in config.sections()} try: return safe_load(filepath.read_text(encoding="utf-8")) except YAMLError as error: logger.error(f"config file '{filepath}' is not a valid yaml file: {error}") return {} # pylint: disable=too-few-public-methods class GeneralConfig(BaseModel): api_key: str protocol: str = "http" base_url: str = "localhost" base_port: Optional[int] base_dir: str = "/" force_ssl: bool = False @property def public_url(self) -> str: scheme = "https" if self.force_ssl else self.protocol location = self.base_url if self.base_port is not None: location += f":{self.base_port}" path = self.base_dir.rstrip("/") return urlunsplit((scheme, location, path, None, None)) # pylint: disable=too-few-public-methods class DatabaseConfig(BaseModel): host: str = "localhost" port: int = 5432 name: str = "libretime" user: str = "libretime" password: str = "libretime" @property def url(self) -> str: return ( f"postgresql://{self.user}:{self.password}" f"@{self.host}:{self.port}/{self.name}" ) # pylint: disable=too-few-public-methods class RabbitMQConfig(BaseModel): host: str = "localhost" port: int = 5672 name: str = "libretime" user: str = "libretime" password: str = "libretime" vhost: str = "/libretime" @property def url(self) -> str: return ( f"amqp://{self.user}:{self.password}" f"@{self.host}:{self.port}/{self.vhost}" )