Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Flask

pip install "dynamic-config-py[flask]"
from flask import Flask
from dynamic_config import DynamicConfig
from dynamic_config_web import token_guard
from dynamic_config_web.flask import DynamicConfigExtension, snapshot

config = DynamicConfig(Database, key="db").file("config.toml").env("APP_")
app = Flask(__name__)

DynamicConfigExtension(
    app, config, guard=token_guard(os.environ.get("CONFIG_TOKEN", ""))
)


@app.get("/")
def index():
    db = snapshot()
    return {"host": db.host, "pool": db.pool.max_size}

examples/03_flask.py runs all of it.

The habit this replaces

# Don't.
app.config.update(DATABASE_HOST=config.current().host)

A value copied into app.config at startup is frozen at the moment it was copied. The file reloads, the engine installs a new model, and app.config keeps answering with the old one — forever, and silently.

The extension writes nothing into app.config. snapshot() is the read, and it answers the model this request began with.

What the extension does

before_requestopens the request scope, and arms the watcher on the first request this process serves.
teardown_appcontextcloses the scope, whatever the view did.
Blueprint/healthz, /readyz, /metrics and the guarded pair, registered on the application.

The factory shape works too:

configuration = DynamicConfigExtension(target=config)


def create_app():
    app = Flask(__name__)
    configuration.init_app(app)

    return app

When the watcher starts

WSGI has no lifespan. There is no moment that is after the fork and before the first request for a library to hook — so the extension arms on the first request in each process, which is after the fork by construction.

That is what makes it correct under gunicorn --preload, uWSGI without lazy-apps, and flask run alike, with nothing to configure. The cost is one attribute read per request after the first.

Two other choices, for deployments that know better:

DynamicConfigExtension(app, config, start="eager")   # single process only
DynamicConfigExtension(app, config, start="manual")  # you call .start()

"eager" arms in init_app, which is wrong under --preload — the master would be the one watching, and the workers would inherit a dead handle. "manual" is for the gunicorn post_fork hook that would rather be explicit:

# gunicorn.conf.py
def post_fork(server, worker):
    from myapp import configuration

    configuration.start()

Reading in a template, a form, a helper

snapshot() needs no request argument — it reads through contextvars, so it works the same three frames down, in a Jinja global, or inside a function a view called. snapshot("cache") names one configuration of a group.

Outside a request it raises OutsideRequestScopeError. A CLI command, a Celery task or a startup hook wants dynamic_config_web.latest(config), which says that is what it meant.