sintonia/shared/libretime_shared/config.py

163 lines
4.7 KiB
Python

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}"
)