A Flask "g" object for FastAPI

tl; dr

Those who are familiar with Flask are familiar with the g object that's convenient to have a "request lifecycle pseudo global" where programmers can store / retrieve arbitrary attributes.

Here is the doc for that g object. Tons of examples of various usages are available at stackoverflow or blog posts (storing a JWT token, the required API version, ...).

From now, I use FastAPI in place of flask for pure restful API servers. And unfortunately, FastAPI has not a similar feature. The FastAPI documentation says to use the request.state object. But this requires to pass the request object along in the depth to all callables from the route top function to every function that needs it.

Good news, this recipe that leverages both the types.SimpleNamespace and the new stdlib module from Python 3.7+ contextvars provides a flask.g like feature.

The big picture

In short, the contextvars module provides to programmers a convenient way to have "pseudo globals" that are shared by coroutines participating to the same asynchronous execution cycle. You can read better explanations than mmine in the official doc (see the link above) and tons of good examples from here.

The types.SimpleNamespace is an arbitrary personal choice for an easy to use attributes container. Once you'll get the enlightenment of this recipe, you may use a custom object that's better suited to what you need.

In short, this recipe shows how to:

  • Create the request lifecycle shared object.
  • (Re)initialize the request lifecycle shared object at each request in a middleware.
  • Make a basic usage of that request lifecycle shared object.

If you want to rebuild the demo at home, just create and activate a new Python 3.7+ virtualenv with whatever tool you prefer (virtualenv, venv, pew, - name yours) and issue:

pip install fastapi uvicorn requests

Note that requests is here just to make the demo client easier. You would not need it otherwise.

Now the files...

requestvars.py

The requestvars module provides the contextvars bootstrap and the public API for your route handlers (and app business logic).

Disclaimer

I know! "My" g is a function when the Flask g is just a strange object that does not need to be called. Any help to fill the gap without a monster machinery is welcome.

asgi.py

Just provides a function that creates our FastAPI app object. Nothing special. Just notice the init_requestvars dedicated middleware.

It re-initiallizes the content of our contextvar to an empty types.SimpleNamespace object. Of course, you may customize this with a pre-populated namespace with data required by your business logic, or choose something lese than a types.SimpleNamespace as free attributes container.

routes.py

Just a simple GET handler at /foo that requires a q parameter and returns that parameters twice. Stupid and useless, its only usage is the use of the requestvars.g function that provides the request lifecycle pseudo-global.

Line 3:
the usual import as for a global function.
Line 10:
we add the arbitrary attribute blah to the request lifecycle pseudo-global which value is the q parameter of the request.
Line 11:
we call the double function with no parameter
Line 17:
the double async function grabs the blah attribute of our request lifecycle pseudo-global and returns it twice.

The other lines do not need comments event to FastAPI noobs.

server.py

Is just an ordinary minimal uvicorn server which serves our stupid API on http://localhost:8000/foo?q=whatever. Does not need comment.

client.py

Is just a demo client that consumes our stupid API in an infinite loop, providing as q parameter whatever string provided as first shell line argument. Example:

python client.py whatever

Let's run the demo

Okay now open 3 or more terminals. In each terminal, cd to the demo directory where you grabbed the above files, and activate the virtual env.

In the first terminal, run the server:

python server.py

In the second terminal, run a client with parameter "hop":

python client.py hop

You should see...

{'result': 'hophop'}
{'result': 'hophop'}
... And so on each second ...

In the second terminal, run a client with parameter "schtroumpf":

python client.py schtroumpf

You should see...

{'result': 'schtroumpfschtroumpf'}
{'result': 'schtroumpfschtroumpf'}
... And so on each second ...

You may add as many terminals you want and ontinue on with other custom and unique paraméter, and notice what you can notice with the first two client terminals:

Each request lifecycle has its own values that are propagated through the g() attributes, that don't mess with g() attributes from other requests lifecycles.

Any suggestion to improve this recipe is welcome in comments below.

Comments !

links

social