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):
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:
MQTT__HOST=broker.local
MQTT__PORT=1883
MQTT__USERNAME=user
MQTT__PASSWORD=s3cret
LOGGING__LEVEL=INFO
.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",
)
- With
env_prefix="VELUX_", environment variables becomeVELUX_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:
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¶
- Architecture — how settings feed into the composition root
- Logging —
LoggingSettingsfields and their effects - MQTT Topics —
topic_prefixusage in topic layout - ADR-003 — Configuration System