GitHub

Config

Read typed configuration from environment variables, .env files and more.

A Config[A] says which keys to read and how to decode them. Configs are effects that fail with ConfigError, so they compose with everything else.

read.py
import pyfect as fx
from pyfect import config, config_provider


@config.gen
def server():
    host = yield from config.string("HOST").with_default("localhost")
    port = yield from config.port("PORT")
    debug = yield from config.boolean("DEBUG").with_default(False)
    return (host, port, debug)


# Configs read from CurrentConfigProvider: environment variables unless a layer
# installs another source (a .env file, a directory, nested dicts).
env = config_provider.layer(config_provider.from_env(env={"PORT": "8080", "DEBUG": "true"}))

# server is a Config[tuple[str, int, bool]] and an Effect failing with ConfigError.
# ConfigError is still in the type, so run_sync (which takes only effects that
# cannot fail) would be a type error: run_sync_exit returns the Exit instead.
assert fx.run_sync_exit(server.pipe(fx.provide(env))) == fx.Success(("localhost", 8080, True))

# Invalid data fails with a readable message, not a default.
bad = config_provider.layer(config_provider.from_env(env={"PORT": "99999"}))
exit = fx.run_sync_exit(server.pipe(fx.provide(bad)))
assert isinstance(exit, fx.Failure)
assert "Expected a value between 1 and 65535" in str(exit.cause.squash())

Missing data is absent: with_default and config.option recover it. Data that is present but invalid fails, so a typo in PORT is an error, not a silent default.

Sources and secrets

A ConfigProvider is where values come from: from_env (the default), from_dot_env, from_dir, from_unknown (nested dicts) or your own lookup. Install one with config_provider.layer. nested reads a config under a prefix, and config.redacted keeps a secret out of logs and error messages.

secrets.py
import pyfect as fx
from pyfect import config, config_provider, redacted

provider = config_provider.from_unknown({"db": {"url": "postgres://db", "password": "hunter2"}})


@config.gen
def database():
    url = yield from config.string("url")
    password = yield from config.redacted("password")  # a Redacted[str]
    return url, password


# or_die: a config error at startup is fatal (a defect, out of the error type).
url, password = fx.run_sync(
    database.nested("db").pipe(fx.provide(config_provider.layer(provider))).or_die()
)
assert url == "postgres://db"
assert str(password) == "<redacted>"  # never printed by str, repr or loggers
assert redacted.value(password) == "hunter2"

Constructors: string, integer, number, boolean, port, url, duration, literal, log_level, byte_size, date_time, redacted, list_of, dict_of; combine with config.all, all_dict or @config.gen.

Reference: pyfect.config, pyfect.config_provider, pyfect.redacted.