Skip to content

Configuration

Cosalette uses pydantic-settings for type-safe, layered configuration. Environment variables, .env files, and CLI flags are merged into a single validated Settings object before the application starts.

Configuration Hierarchy

Settings are resolved with the following precedence (highest wins):

CLI flags  >  Environment variables  >  .env file  >  Model defaults
graph TB
    A["Model defaults"] --> B[".env file"]
    B --> C["Environment variables"]
    C --> D["CLI flags (--log-level, etc.)"]
    D --> E["Final Settings object"]

    style E fill:#e8f5e9,stroke:#2FB170

Settings Schema

The root Settings class composes two sub-models:

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_nested_delimiter="__",
        env_file=".env",
        env_file_encoding="utf-8",
    )

    mqtt: MqttSettings = Field(default_factory=MqttSettings)
    logging: LoggingSettings = Field(default_factory=LoggingSettings)

MqttSettings

Field Type Default Description
host str "localhost" Broker hostname or IP
port int (1–65535) 1883 Broker port
username str | None None Authentication username
password SecretStr | None None Authentication password (masked)
client_id str "" MQTT client ID (auto-set by App)
reconnect_interval float (> 0) 5.0 Initial seconds before reconnection (base for backoff)
reconnect_max_interval float (> 0) 300.0 Upper bound (seconds) for exponential backoff
topic_prefix str "" Root topic prefix (auto-set by App)

Reconnect backoff algorithm

When the MQTT connection drops, cosalette waits reconnect_interval seconds before the first retry. On each consecutive failure the delay doubles (exponential backoff) and a ±20 % jitter is applied to prevent thundering-herd reconnections across multiple instances. The delay never exceeds reconnect_max_interval. On successful reconnection the delay resets to reconnect_interval.

LoggingSettings

Field Type Default Description
level "DEBUG" | "INFO" | "WARNING" | "ERROR" | "CRITICAL" "INFO" Root log level
format "json" | "text" "json" Output format (JSON lines or text)
file str | None None Optional log file path
max_file_size_mb int (≥ 1) 10 Max log file size (MB) before rotation
backup_count int (≥ 0) 3 Rotated log file generations

Nested Delimiter

Pydantic-settings uses __ (double underscore) as the nested delimiter. Nested model fields map to environment variables with __ separating levels:

# Environment variables
export MQTT__HOST=broker.local
export MQTT__PORT=1883
export MQTT__USERNAME=admin
export MQTT__PASSWORD=secret
export LOGGING__LEVEL=DEBUG
export LOGGING__FORMAT=text

These map directly to the Python object hierarchy:

settings.mqtt.host        # "broker.local"
settings.mqtt.port        # 1883
settings.logging.level    # "DEBUG"
settings.logging.format   # "text"

.env File Support

A .env file in the working directory is loaded automatically. The file path can be overridden via the --env-file CLI flag:

.env
MQTT__HOST=broker.local
MQTT__PORT=1883
MQTT__USERNAME=user
MQTT__PASSWORD=s3cret
LOGGING__LEVEL=INFO
# Use a different env file
myapp --env-file /etc/myapp/production.env

.env files are optional

If no .env file exists, pydantic-settings silently continues with environment variables and model defaults. This is the expected case in container deployments where all config comes from environment variables.

Application Extension Pattern

Framework consumers subclass Settings to add application-specific fields and an env_prefix:

from cosalette._settings import Settings, MqttSettings
from pydantic import Field
from pydantic_settings import SettingsConfigDict

class VeluxSettings(Settings):
    model_config = SettingsConfigDict(
        env_prefix="VELUX_",  # (1)!
        env_nested_delimiter="__",
        env_file=".env",
        env_file_encoding="utf-8",
    )

    serial_port: str = Field(
        default="/dev/ttyUSB0",
        description="Serial port for KLF200 gateway",
    )
    poll_interval: float = Field(
        default=30.0,
        description="Seconds between status polls",
    )
  1. With env_prefix="VELUX_", environment variables become VELUX_MQTT__HOST, VELUX_SERIAL_PORT, etc.

Sub-models use BaseModel, not BaseSettings

MqttSettings and LoggingSettings are pydantic.BaseModel subclasses composed into the root BaseSettings. Only the root class participates in environment variable loading — sub-models exist for structural organisation.

CLI Override Integration

The Typer-based CLI exposes framework-level flags that take precedence over all other sources:

myapp --log-level DEBUG --log-format text --dry-run --env-file prod.env

These overrides are applied after settings are loaded from the environment:

if log_level is not None:
    settings.logging = settings.logging.model_copy(
        update={"level": log_level.upper()},
    )

See Also