Skip to content

Settings Reference

Configuration reference for cosalette applications. Settings are managed by pydantic-settings and can be set via constructor arguments, environment variables, or .env files.

Getting started with configuration

See the Configuration guide for practical examples and the Configuration concept for architectural context.

Root Settings

cosalette.Settings

Settings(**data: Any)

Bases: BaseSettings

Root framework settings for cosalette applications.

Loaded from environment variables with the nested delimiter __ and an optional .env file in the working directory.

No env_prefix is set at the framework level — each application subclasses Settings and adds its own prefix (e.g. env_prefix="MYAPP_").

Warning

The root field names MQTT, LOGGING and SCHEMA are reserved environment namespaces: without an env_prefix, a bare environment variable of that name is parsed as JSON for the whole submodel (e.g. MQTT='{"host":"…"}'), and a non-JSON value (e.g. SCHEMA=public, as set by some CI/DB tooling) fails startup with an actionable error. Prefer setting env_prefix in your subclass for shared-host deployments.

Config-file support: set config_file="/path/to/app.toml" in the subclass model_config (or pass _config_file=<path> at construction time) to load settings from a TOML, YAML, or JSON file. Precedence is env > dotenv > config_file > defaults.

Example .env::

MQTT__HOST=broker.local
MQTT__PORT=1883
MQTT__USERNAME=user
MQTT__PASSWORD=secret
LOGGING__LEVEL=DEBUG
LOGGING__FORMAT=text

Example with an application prefix (subclass)::

class MyAppSettings(Settings):
    model_config = SettingsConfigDict(
        env_prefix="MYAPP_",
        env_nested_delimiter="__",
        env_file=".env",
        env_file_encoding="utf-8",
    )
Source code in packages/src/cosalette/_settings/__init__.py
def __init__(self, **data: Any) -> None:
    # Pop before super().__init__ so pydantic doesn't treat it as a field value
    config_file = data.pop("_config_file", _UNSET)
    token = _config_file_override.set(config_file)
    try:
        super().__init__(**data)
    except SettingsError as exc:
        # Without an env_prefix the reserved root names (MQTT, LOGGING,
        # SCHEMA) are read from the whole process environment; a bare
        # non-JSON variable of that name (e.g. SCHEMA=public set by CI or
        # DB tooling) makes complex-field parsing fail with an opaque
        # error. Translate it into actionable guidance (CWE-15) only when
        # the error actually originates from those reserved fields.
        _RESERVED = ("mqtt", "logging", "schema")
        if any(name in str(exc).lower() for name in _RESERVED):
            raise SettingsError(
                f"{exc} [hint: cosalette reads the reserved root environment "
                "variables MQTT, LOGGING and SCHEMA (complex fields expecting "
                "JSON, plus nested MQTT__*/LOGGING__*/SCHEMA__* names). A "
                "same-named non-JSON variable in your environment collides "
                "with them — unset/rename it, or set env_prefix in your "
                "Settings subclass.]"
            ) from exc
        raise
    finally:
        _config_file_override.reset(token)

model_config class-attribute instance-attribute

model_config = SettingsConfigDict(
    env_nested_delimiter="__",
    env_file=".env",
    env_file_encoding="utf-8",
    extra="ignore",
)

Settings uses extra="ignore" because the base class sets no env_prefix. Without a prefix, pydantic-settings reads every environment variable; the BaseSettings default of extra="forbid" would then reject unrelated variables (GH_TOKEN, PATH, etc.) as validation errors.

Subclasses that set env_prefix only see prefixed variables and may safely tighten this to extra="forbid" for strict validation.

MQTT Settings

cosalette.MqttSettings

Bases: BaseModel

MQTT broker connection and topic configuration.

Environment variables (with __ nesting)::

MQTT__HOST=broker.local
MQTT__PORT=1883
MQTT__USERNAME=user
MQTT__PASSWORD=secret
MQTT__TOPIC_PREFIX=myapp

Logging Settings

cosalette.LoggingSettings

Bases: BaseModel

Logging configuration.

When file is set, logs are also written to a rotating file (size-based rotation, backup_count generations kept). When None, logs go to stderr only.

The format field selects the output format:

  • "json" (default) — structured JSON lines for container log aggregators (Loki, Elasticsearch, CloudWatch). Each line is a complete JSON object with correlation metadata.
  • "text" — human-readable timestamped format for local development and direct terminal use.

Environment Variables

All settings can be overridden via environment variables using the nested __ separator convention from pydantic-settings.

Nested Delimiter Convention

Pydantic-settings uses __ (double underscore) as the nested delimiter: MQTT__HOST maps to settings.mqtt.host. When a subclass adds env_prefix="MYAPP_", the prefix prepends the entire path: MYAPP_MQTT__HOSTsettings.mqtt.host.

# Base variables (no prefix, base Settings class)
export MQTT__HOST=broker.local
export MQTT__PORT=1883
export LOGGING__LEVEL=DEBUG

# Prefixed variables (subclass with env_prefix="GAS2MQTT_")
export GAS2MQTT_MQTT__HOST=broker.local
export GAS2MQTT_LOGGING__LEVEL=INFO

.env File Loading

A .env file in the working directory is loaded automatically by pydantic-settings when env_file=".env" appears in model_config. The file path can be overridden at runtime with the --env-file CLI flag:

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.

MQTT

Variable Type Default Description
MQTT__HOST str "localhost" MQTT broker hostname or IP address
MQTT__PORT int 1883 MQTT broker port (1–65535)
MQTT__USERNAME str \| None None MQTT authentication username
MQTT__PASSWORD SecretStr \| None None MQTT authentication password (masked in logs)
MQTT__TLS bool true Enable TLS client connection. Defaults to true since 0.7.0 (ADR-062, F-CU1); brokers without TLS support require an explicit MQTT__TLS=false
MQTT__TLS_CA_FILE str \| None None CA bundle for broker certificate validation
MQTT__TLS_CERT_FILE str \| None None Client certificate for mutual TLS
MQTT__TLS_KEY_FILE str \| None None Client private key for mutual TLS
MQTT__CLIENT_ID str "" MQTT client identifier. Empty = auto-generated as {name}-{hex8} at startup
MQTT__RECONNECT_INTERVAL float 5.0 Initial seconds before reconnecting (doubles with jitter on each failure, up to max)
MQTT__RECONNECT_MAX_INTERVAL float 300.0 Upper bound (seconds) for exponential reconnect backoff
MQTT__TOPIC_PREFIX str "" Root prefix for all MQTT topics. Empty = uses App(name=...). Set to override (e.g. staging)

Logging

Variable Type Default Description
LOGGING__LEVEL str "INFO" Root log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
LOGGING__FORMAT str "json" Log output format (json or text)
LOGGING__FILE str | None None Optional log file path — None means stderr only
LOGGING__MAX_FILE_SIZE_MB int 10 Maximum log file size in megabytes before rotation. Only has effect when file is set
LOGGING__BACKUP_COUNT int 3 Number of rotated log files to keep

Application prefix

The base Settings class has no env_prefix. When you subclass Settings for your project, you can add one (e.g. env_prefix="MYAPP_") — all variables above would then require that prefix: MYAPP_MQTT__HOST, MYAPP_LOGGING__LEVEL, etc.

The app.settings Property

App.__init__ eagerly instantiates the settings_class passed to the constructor. The resulting instance is exposed as the read-only app.settings property.

Because the instance is created at construction time — before any decorators run — you can use it in decorator arguments:

app = cosalette.App(
    name="gas2mqtt",
    version="1.0.0",
    settings_class=Gas2MqttSettings,
)

@app.telemetry("counter", interval=app.settings.poll_interval)
async def counter() -> dict[str, object]:
    return {"impulses": 42}

Environment variables and .env files are read when settings_class() is called inside App.__init__, so app.settings.poll_interval reflects the actual runtime value.

CLI re-instantiation

The CLI entrypoint (app.run() / app.cli()) re-instantiates settings with --env-file support. That new instance is used internally during _run_async and is the one injected into DeviceContext. The app.settings property returns the original construction-time instance — which is correct for decorator arguments since those are evaluated at import time.

The extra="ignore" Behaviour

The base Settings class sets extra="ignore" in its model_config:

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

Why: The base class has no env_prefix, so pydantic-settings reads every environment variable in the process. The BaseSettings default of extra="forbid" would reject unrelated variables (PATH, HOME, GH_TOKEN, etc.) as validation errors.

extra="ignore" silently discards unknown variables, making the base class work in any environment.

Strict validation in subclasses

Subclasses that set env_prefix (e.g. env_prefix="GAS2MQTT_") only see prefixed variables. They may safely tighten this to extra="forbid" for strict validation if desired.

Settings Injection

Settings are automatically injected into device handlers and adapter factory callables that declare a parameter annotated with Settings (or a subclass).

Context How to access
Decorator arguments app.settings.field_name
Device handlers Declare a Settings-typed parameter
Adapter factory callables Declare a Settings-typed parameter
Lifespan hook ctx.settings

See the Adapters guide for examples of settings injection in factory callables.