Skip to content

core_config

Configuration management for Genres Autoupdater v2.0.

ConfigurationError

ConfigurationError(message, config_path=None)

Bases: Exception

Raised when configuration loading or parsing fails.

Initialize the configuration error.

Parameters:

Name Type Description Default
message str

Error description

required
config_path str | None

Path to the config file that caused the error

None
Source code in src/core/core_config.py
def __init__(self, message: str, config_path: str | None = None) -> None:
    """Initialize the configuration error.

    Args:
        message: Error description
        config_path: Path to the config file that caused the error

    """
    super().__init__(message)
    self.config_path = config_path

resolve_env_vars

resolve_env_vars(config)

Recursively resolve environment variables in config values.

Parameters:

Name Type Description Default
config ConfigValue

Configuration value (dict, list, or primitive).

required

Returns:

Type Description
ConfigValue

Config with environment variables resolved.

Source code in src/core/core_config.py
def resolve_env_vars(config: ConfigValue) -> ConfigValue:
    """Recursively resolve environment variables in config values.

    Args:
        config: Configuration value (dict, list, or primitive).

    Returns:
        Config with environment variables resolved.

    """
    if isinstance(config, dict):
        return {str(k): resolve_env_vars(v) for k, v in config.items()}
    if isinstance(config, list):
        return [resolve_env_vars(item) for item in config]
    if isinstance(config, str):
        return _expand_string_env_var(config)
    return config

validate_required_env_vars

validate_required_env_vars()

Validate required environment variables.

Returns:

Type Description
list[str]

list[str]: List of missing required environment variables.

Source code in src/core/core_config.py
def validate_required_env_vars() -> list[str]:
    """Validate required environment variables.

    Returns:
        list[str]: List of missing required environment variables.

    """
    missing: list[str] = []
    for var in REQUIRED_ENV_VARS:
        value = os.getenv(var)
        if not value or value.startswith("$"):
            missing.append(var)
    return missing

load_config

load_config(config_path)

Load the configuration from a YAML file, resolve environment variables, and validate it.

Parameters:

Name Type Description Default
config_path str

Path to the configuration YAML file.

required

Returns:

Type Description
AppConfig

Validated AppConfig Pydantic model with resolved env vars.

Raises:

Type Description
FileNotFoundError

If the config file does not exist.

ValueError

If the configuration is invalid, the path is insecure, or env vars are missing.

PermissionError

If the config file cannot be read.

YAMLError

If there is an error parsing the YAML file.

RuntimeError

For unexpected errors during additional validation steps.

Source code in src/core/core_config.py
def load_config(config_path: str) -> AppConfig:
    """Load the configuration from a YAML file, resolve environment variables, and validate it.

    Args:
        config_path: Path to the configuration YAML file.

    Returns:
        Validated AppConfig Pydantic model with resolved env vars.

    Raises:
        FileNotFoundError: If the config file does not exist.
        ValueError: If the configuration is invalid, the path is insecure, or env vars are missing.
        PermissionError: If the config file cannot be read.
        YAMLError: If there is an error parsing the YAML file.
        RuntimeError: For unexpected errors during additional validation steps.

    """
    env_loaded = load_dotenv()
    logger.info(".env file %s", "found and loaded" if env_loaded else "not found, using system environment variables")

    if missing_vars := validate_required_env_vars():
        error_msg = f"Missing required environment variables: {', '.join(missing_vars)}"
        logger.critical(error_msg)
        raise ValueError(error_msg)

    try:
        validated_path = _validate_config_path(config_path)
        config_data = _read_and_parse_config(validated_path)

        logger.debug(
            "[CONFIG] Raw config (before env var resolution):\n%s",
            yaml.dump(config_data),
        )
        config_data = resolve_env_vars(config_data)
        logger.debug(
            "[CONFIG] Resolved config (after env var resolution):\n%s",
            yaml.dump(config_data),
        )

        config_data = _validate_config_data_type(config_data)

        # Validate with Pydantic
        try:
            config_model = AppConfig(**config_data)
        except ValidationError as e:
            error_details = format_pydantic_errors(e)
            msg = f"Configuration validation failed:\n{error_details}"
            raise ValueError(msg) from e

        logger.info("Configuration successfully loaded and validated.")
        return config_model

    except (FileNotFoundError, PermissionError, ValueError, yaml.YAMLError) as e:
        logger.critical("Configuration loading failed: %s", e)
        raise
    except Exception as e:
        logger.critical("An unexpected error occurred during config loading: %s", e, exc_info=True)
        msg = f"An unexpected error occurred during config loading: {e}"
        raise RuntimeError(msg) from e

format_pydantic_errors

format_pydantic_errors(error)

Format Pydantic validation errors into a readable string.

Parameters:

Name Type Description Default
error ValidationError

Pydantic ValidationError instance.

required

Returns:

Type Description
str

Formatted error message string.

Source code in src/core/core_config.py
def format_pydantic_errors(error: ValidationError) -> str:
    """Format Pydantic validation errors into a readable string.

    Args:
        error: Pydantic ValidationError instance.

    Returns:
        Formatted error message string.

    """
    error_messages: list[str] = []

    for err in error.errors():
        loc_path = ".".join(str(loc) for loc in err["loc"])
        msg = err["msg"]
        error_type = err["type"]

        if error_type == "missing":
            error_messages.append(f"{loc_path}: Missing required field")
        elif error_type in ("type_error", "value_error", "assertion_error"):
            error_messages.append(f"{loc_path}: {msg}")
        else:
            error_messages.append(f"{loc_path}: {msg} (type: {error_type})")

    return "\n".join(error_messages)

validate_api_auth

validate_api_auth(api_auth)

Validate that API authentication fields are non-empty after env var resolution.

Pydantic ensures ApiAuthConfig fields exist, but they may resolve to empty strings when the corresponding environment variables are unset.

Parameters:

Name Type Description Default
api_auth ApiAuthConfig

Typed API authentication configuration.

required

Raises:

Type Description
ValueError

If required credentials are empty.

Source code in src/core/core_config.py
def validate_api_auth(api_auth: ApiAuthConfig) -> None:
    """Validate that API authentication fields are non-empty after env var resolution.

    Pydantic ensures ``ApiAuthConfig`` fields exist, but they may resolve
    to empty strings when the corresponding environment variables are unset.

    Args:
        api_auth: Typed API authentication configuration.

    Raises:
        ValueError: If required credentials are empty.

    """
    missing_fields: list[str] = []
    if not api_auth.discogs_token:
        missing_fields.append("DISCOGS_TOKEN")
    if not api_auth.contact_email:
        missing_fields.append("CONTACT_EMAIL")

    for field in missing_fields:
        logger.warning("%s is not set in environment", field)
    if missing_fields:
        msg = f"API authentication config incomplete: {', '.join(missing_fields)}"
        raise ValueError(msg)