Files
alpinebits_python/src/alpine_bits_python/config_loader.py
2025-10-15 14:27:30 +02:00

218 lines
6.1 KiB
Python

import os
from pathlib import Path
from annotatedyaml.loader import Secrets
from annotatedyaml.loader import load_yaml as load_annotated_yaml
from voluptuous import (
PREVENT_EXTRA,
All,
Boolean,
In,
Length,
MultipleInvalid,
Optional,
Range,
Required,
Schema,
)
from alpine_bits_python.const import (
CONF_ALPINE_BITS_AUTH,
CONF_DATABASE,
CONF_HOTEL_ID,
CONF_HOTEL_NAME,
CONF_LOGGING,
CONF_LOGGING_FILE,
CONF_LOGGING_LEVEL,
CONF_PASSWORD,
CONF_PUSH_ENDPOINT,
CONF_PUSH_TOKEN,
CONF_PUSH_URL,
CONF_PUSH_USERNAME,
CONF_SERVER,
CONF_SERVER_CODE,
CONF_SERVER_CODECONTEXT,
CONF_SERVER_COMPANYNAME,
CONF_SERVER_RES_ID_SOURCE_CONTEXT,
CONF_USERNAME,
ENV_ALPINE_BITS_CONFIG_PATH,
)
# --- Voluptuous schemas ---
database_schema = Schema({Required("url"): str}, extra=PREVENT_EXTRA)
logger_schema = Schema(
{
Required(CONF_LOGGING_LEVEL, default="INFO"): str,
Optional(CONF_LOGGING_FILE): str, # If not provided, log to console
},
extra=PREVENT_EXTRA,
)
def ensure_string(value):
"""Ensure the value is a string."""
if isinstance(value, str):
return value
return str(value)
server_info = Schema(
{
Required(CONF_SERVER_CODECONTEXT, default="ADVERTISING"): ensure_string,
Required(CONF_SERVER_CODE, default="70597314"): ensure_string,
Required(CONF_SERVER_COMPANYNAME, default="99tales Gmbh"): ensure_string,
Required(CONF_SERVER_RES_ID_SOURCE_CONTEXT, default="99tales"): ensure_string,
}
)
hotel_auth_schema = Schema(
{
Required(CONF_HOTEL_ID): ensure_string,
Required(CONF_HOTEL_NAME): str,
Required(CONF_USERNAME): str,
Required(CONF_PASSWORD): str,
Optional(CONF_PUSH_ENDPOINT): {
Required(CONF_PUSH_URL): str,
Required(CONF_PUSH_TOKEN): str,
Optional(CONF_PUSH_USERNAME): str,
},
},
extra=PREVENT_EXTRA,
)
basic_auth_schema = Schema(All([hotel_auth_schema], Length(min=1)))
# Email SMTP configuration schema
smtp_schema = Schema(
{
Required("host", default="localhost"): str,
Required("port", default=587): Range(min=1, max=65535),
Optional("username"): str,
Optional("password"): str,
Required("use_tls", default=True): Boolean(),
Required("use_ssl", default=False): Boolean(),
},
extra=PREVENT_EXTRA,
)
# Email daily report configuration schema
daily_report_schema = Schema(
{
Required("enabled", default=False): Boolean(),
Optional("recipients", default=[]): [str],
Required("send_time", default="08:00"): str,
Required("include_stats", default=True): Boolean(),
Required("include_errors", default=True): Boolean(),
},
extra=PREVENT_EXTRA,
)
# Email error alerts configuration schema
error_alerts_schema = Schema(
{
Required("enabled", default=False): Boolean(),
Optional("recipients", default=[]): [str],
Required("error_threshold", default=5): Range(min=1),
Required("buffer_minutes", default=15): Range(min=1),
Required("cooldown_minutes", default=15): Range(min=0),
Required("log_levels", default=["ERROR", "CRITICAL"]): [
In(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])
],
},
extra=PREVENT_EXTRA,
)
# Email monitoring configuration schema
monitoring_schema = Schema(
{
Optional("daily_report", default={}): daily_report_schema,
Optional("error_alerts", default={}): error_alerts_schema,
},
extra=PREVENT_EXTRA,
)
# Complete email configuration schema
email_schema = Schema(
{
Optional("smtp", default={}): smtp_schema,
Required("from_address", default="noreply@example.com"): str,
Required("from_name", default="AlpineBits Server"): str,
Optional("timeout", default=10): Range(min=1, max=300),
Optional("monitoring", default={}): monitoring_schema,
},
extra=PREVENT_EXTRA,
)
config_schema = Schema(
{
Required(CONF_DATABASE): database_schema,
Required(CONF_ALPINE_BITS_AUTH): basic_auth_schema,
Required(CONF_SERVER): server_info,
Required(CONF_LOGGING): logger_schema,
Optional("email"): email_schema, # Email is optional
Optional("api_tokens", default=[]): [str], # API tokens for bearer auth
},
extra=PREVENT_EXTRA,
)
DEFAULT_CONFIG_FILE = "config.yaml"
class Config:
"""Class to load and hold the configuration."""
def __init__(
self,
config_folder: str | Path | None = None,
config_name: str = DEFAULT_CONFIG_FILE,
testing_mode: bool = False,
):
if config_folder is None:
config_folder = os.environ.get(ENV_ALPINE_BITS_CONFIG_PATH)
if not config_folder:
config_folder = Path(__file__).parent.joinpath("../../config").resolve()
if isinstance(config_folder, str):
config_folder = Path(config_folder)
self.config_folder = config_folder
self.config_path = config_folder / config_name
self.secrets = Secrets(config_folder)
self.testing_mode = testing_mode
self._load_config()
def _load_config(self):
stuff = load_annotated_yaml(self.config_path, secrets=self.secrets)
try:
validated = config_schema(stuff)
except MultipleInvalid as e:
raise ValueError(f"Config validation error: {e}")
self.database = validated["database"]
self.basic_auth = validated["alpine_bits_auth"]
self.config = validated
def get(self, key, default=None):
return self.config.get(key, default)
@property
def db_url(self) -> str:
return self.database["url"]
@property
def hotel_id(self) -> str:
return self.basic_auth["hotel_id"]
@property
def hotel_name(self) -> str:
return self.basic_auth["hotel_name"]
@property
def users(self) -> list[dict[str, str]]:
return self.basic_auth["users"]
# For backward compatibility
def load_config():
return Config().config