Quart's built-in features
The previous section gave us a good understanding of how Quart processes a request, and that's good enough to get you started. There are more helpers that will prove useful. We'll discover the following main ones in this section:
- The
session
object: Cookie-based data - Globals: Storing data in the
request
context - Signals: Sending and intercepting events
- Extensions and middleware: Adding features
- Templates: Building text-based content
- Configuring: Grouping your running options in a
config
file - Blueprints: Organizing your code in namespaces
- Error handling and debugging: Dealing with errors in your app
The session object
Like the request
object, Quart creates a session
object, which is unique to the request
context. It's a dict-like object, which Quart serializes into a cookie on the user side. The data contained in the session mapping is dumped into a JSON mapping, then compressed using zlib
to make it smaller, and finally encoded in base64.
When the session
gets serialized, the itsdangerous (https://pythonhosted.org/itsdangerous/) library signs the content using a secret_key
value defined in the application. The signing uses HMAC (https://en.wikipedia.org/wiki/Hash-based_message_authentication_code) and SHA1.
This signature, which is added to the data as a suffix, ensures that the client cannot tamper with the data that is stored in a cookie unless they know the secret key to sign the session value. Note that the data itself is not encrypted. Quart will let you customize the signing algorithm to use, but HMAC + SHA1 is good enough when you need to store data in cookies.
However, when you're building microservices that are not producing HTML, you rarely rely on cookies as they are specific to web browsers. However, the idea of keeping a volatile key-value storage for each user can be extremely useful for speeding up some of the server-side work. For instance, if you need to perform some database look-ups to get some information pertaining to a user every time they connect, caching this information in a session
-like object on the server side and retrieving the values based on their authentication details makes a lot of sense.
Globals
As discussed earlier in this chapter, Quart provides a mechanism for storing global variables that are unique to a particular request
context. That is used for request
and session
, but is also available to store any custom object.
The quart.g
variable contains all globals, and you can set whatever attributes you want on it. In Quart, the @app.before_request
decorator can be used to point to a function that the app will call every time a request is made, just before it dispatches the request
to a view.
It's a typical pattern in Quart to use before_request
to set values in the globals. That way, all the functions that are called within the request context can interact with the special global variable called g
and get the data. In the following example, we copy the username
provided when the client performs an HTTP Basic Authentication in the user
attribute:
# globals.py
from quart import Quart, g, request
app = Quart(__name__)
@app.before_request
def authenticate():
if request.authorization:
g.user = request.authorization["username"]
else:
g.user = "Anonymous"
@app.route("/api")
def my_microservice():
return {"Hello": g.user}
if __name__ == "__main__":
app.run()
When a client requests the /api
view, the authenticate
function will set g.user
depending on the headers provided:
$ curl http://localhost:5000/api
{
"Hello": "Anonymous"
}
$ curl http://localhost:5000/api --user alice:password
{
"Hello": "alice"
}
Any data you may think of that's specific to a request
context, and that would be usefully shared throughout your code, can be added to quart.g
.
Signals
Sometimes in an application, we want to send a message from one place to another, when components are not directly connected. One way in which we can send such messages is to use signals. Quart integrates with Blinker
(https://pythonhosted.org/blinker/), which is a signal library that lets you subscribe a function to an event.
Events are instances of the AsyncNamedSignal
class, which is based on the blinker.base.NamedSignal
class. It is created with a unique label, and Quart instantiates 10 of them in version 0.13. Quart triggers signals at critical moments during the processing of a request. Since Quart
and Flask
use the same system, we can refer to the following full list: http://flask.pocoo.org/docs/latest/api/#core-signals-list.
Registering to a particular event is done by calling the signal's connect
method. Signals are triggered when some code calls the signal's send
method. The send
method accepts extra arguments to pass data to all the registered functions.
In the following example, we register the finished function to the request_finished
signal. That function will receive the response
object:
# signals.py
from quart import Quart, g, request_finished
from quart.signals import signals_available
app = Quart(__name__)
def finished(sender, response, **extra):
print("About to send a Response")
print(response)
request_finished.connect(finished)
@app.route("/api")
async def my_microservice():
return {"Hello": "World"}
if __name__ == "__main__":
app.run()
The signal
feature is provided by Blinker
, which is installed by default as a dependency when you install Quart
.
Some signals implemented in Quart are not useful in microservices, such as the ones occurring when the framework renders a template. However, there are some interesting signals that Quart triggers throughout the request
life, which can be used to log what's going on. For instance, the got_request_exception
signal is triggered when an exception occurs before the framework does something with it. That's how Sentry's (https://sentry.io) Python client hooks itself in to log exceptions.
It can also be interesting to implement custom signals in your apps when you want to trigger some of your features with events and decouple the code. For example, if your microservice produces PDF reports, and you want to have the reports cryptographically signed, you could trigger a report_ready
signal, and have a signer register to that event.
One important aspect of the signals implementation is that the registered functions are not called in any particular order, and so if there are dependencies between the functions that get called, this may cause trouble. If you need to do more complex or time-consuming work, then consider using a queue
such as RabbitMQ (https://www.rabbitmq.com/) or one provided by a cloud platform such as Amazon Simple Queue Service or Google PubSub to send a message to another service. These message queues offer far more options than a basic signal and allow two components to communicate easily without even necessarily being on the same computer. We will cover an example of message queues in Chapter 6, Interacting with Other Services.
Extensions and middleware
Quart extensions are simply Python projects that, once installed, provide a package or a module named quart_something
. They can be useful for avoiding having to reinvent anything when wanting to do things such as authentication or sending an email.
Because Quart can support some of the extensions available to Flask
, you can often find something to help in Flask's list of extensions: Search for Framework::Flask
in the Python package index at https://pypi.org/. To use Flask
extensions, you must first import a patch
module to ensure that it will work. For example, to import Flask's login
extension, use the following commands:
import quart.flask_patch
import flask_login
The most up-to-date list of Flask extensions that are known to work with Quart will be at the address below. This is a good place to start looking when searching for extra features that your microservice needs: http://pgjones.gitlab.io/quart/how_to_guides/flask_extensions.html.
The other mechanism for extending Quart is to use ASGI or WSGI middleware. These extend the application by wrapping themselves around an endpoint and changing the data that goes in and comes out again.
In the example that follows, the middleware fakes an X-Forwarded-For
header, so the Quart application thinks it's behind a proxy such as nginx
. This is useful in a testing environment when you want to make sure your application behaves properly when it tries to get the remote IP address, since the remote_addr
attribute will get the IP of the proxy, and not the real client. In this example, we have to create a new Headers
object, as the existing one is immutable:
# middleware.py
from quart import Quart, request
from werkzeug.datastructures import Headers
class XFFMiddleware:
def __init__(self, app, real_ip="10.1.1.1"):
self.app = app
self.real_ip = real_ip
async def __call__(self, scope, receive, send):
if "headers" in scope and "HTTP_X_FORWARDED_FOR" not in scope["headers"]:
new_headers = scope["headers"].raw_items() + [
(
b"X-Forwarded-For",
f"{self.real_ip}, 10.3.4.5, 127.0.0.1".encode(),
)
]
scope["headers"] = Headers(new_headers)
return await self.app(scope, receive, send)
app = Quart(__name__)
app.asgi_app = XFFMiddleware(app.asgi_app)
@app.route("/api")
def my_microservice():
if "X-Forwarded-For" in request.headers:
ips = [ip.strip() for ip in request.headers["X-Forwarded-For"].split(",")]
ip = ips[0]
else:
ip = request.remote_addr
return {"Hello": ip}
if __name__ == "__main__":
app.run()
Notice that we use app.asgi_app
here to wrap the ASGI application. app.asgi_app
is where the application is stored to let people wrap it in this way. The send
and receive
parameters are channels through which we can communicate. It's worth remembering that if the middleware returns a response to the client, then the rest of the Quart
app will never see the request!
In most situations, we won't have to write our own middleware, and it will be enough to include an extension to add a feature that someone else has produced.
Templates
Sending back JSON or YAML documents is easy enough, as we have seen in the examples so far. It's also true that most microservices produce machine-readable data and if a human needs to read it, the frontend must format it properly, using, for example, JavaScript on a web page. In some cases, though, we might need to create documents with some layout, whether it's an HTML page, a PDF report, or an email.
For anything that's text-based, Quart integrates a template engine called Jinja (https://jinja.palletsprojects.com/). You will often find examples showing Jinja being used to create HTML documents, but it works with any text-based document. Configuration management tools such as Ansible use Jinja to create configuration files from a template so that a computer's settings can be kept up to date automatically.
Most of the time, Quart will use Jinja to produce HTML documents, email messages, or some other piece of communication meant for a human—such as an SMS message or a bot that talks to people on tools such as Slack or Discord. Quart provides helpers such as render_template
, which generate responses by picking a Jinja template, and provides the output given some data.
For example, if your microservice sends emails instead of relying on the standard library's email package to produce the email content, which can be cumbersome, you could use Jinja. The following example email template should be saved as email_template.j2
in order for the later code examples to work:
Date: {{date}}
From: {{from}}
Subject: {{subject}}
To: {{to}}
Content-Type: text/plain
Hello {{name}},
We have received your payment!
Below is the list of items we will deliver for lunch:
{% for item in items %}- {{item['name']}} ({{item['price']}} Euros)
{% endfor %}
Thank you for your business!
--
My Fictional Burger Place
Jinja uses double brackets for marking variables that will be replaced by a value. Variables can be anything that is passed to Jinja at execution time. You can also use Python's if
and for
blocks directly in your templates with the {% for x in y % }... {% endfor %}
and {% if x %}...{% endif %}
notations.
The following is a Python script that uses the email template to produce an entirely valid RFC 822
message, which you can send via SMTP:
# email_render.py
from datetime import datetime
from jinja2 import Template
from email.utils import format_datetime
def render_email(**data):
with open("email_template.j2") as f:
template = Template(f.read())
return template.render(**data)
data = {
"date": format_datetime(datetime.now()),
"to": "[email protected]",
"from": "[email protected]",
"subject": "Your Burger order",
"name": "Bob",
"items": [
{"name": "Cheeseburger", "price": 4.5},
{"name": "Fries", "price": 2.0},
{"name": "Root Beer", "price": 3.0},
],
}
print(render_email(**data))
The render_email
function uses the Template
class to generate the email using the data provided.
Jinja is a powerful tool and comes with many features that would take too much space to describe here. If you need to do some templating work in your microservices, it is a good choice, also being present in Quart. Check out the following for full documentation on Jinja's features: https://jinja.palletsprojects.com/.
Configuration
When building applications, you will need to expose options to run them, such as the information needed to connect to a database, the contact email address to use, or any other variable that is specific to a deployment.
Quart uses a mechanism similar to Django in its configuration approach. The Quart
object comes with an object called config
, which contains some built-in variables, and which can be updated when you start your Quart
app via your configuration objects. For example, you can define a Config
class in a Python-format file as follows:
# prod_settings.py
class Config:
DEBUG = False
SQLURI = "postgres://username:xxx@localhost/db"
It can then be loaded from your app
object using app.config.from_object
:
>>> from quart import Quart
>>> import pprint
>>> pp = pprint.PrettyPrinter(indent=4)
>>> app = Quart(__name__)
>>> app.config.from_object('prod_settings.Config')
>>> pp.pprint(app.config)
{ 'APPLICATION_ROOT': None,
'BODY_TIMEOUT': 60,
'DEBUG': False,
'ENV': 'production',
'JSONIFY_MIMETYPE': 'application/json',
'JSONIFY_PRETTYPRINT_REGULAR': False,
'JSON_AS_ASCII': True,
'JSON_SORT_KEYS': True,
'MAX_CONTENT_LENGTH': 16777216,
'PERMANENT_SESSION_LIFETIME': datetime.timedelta(days=31),
'PREFER_SECURE_URLS': False,
'PROPAGATE_EXCEPTIONS': None,
'RESPONSE_TIMEOUT': 60,
'SECRET_KEY': None,
'SEND_FILE_MAX_AGE_DEFAULT': datetime.timedelta(seconds=43200),
'SERVER_NAME': None,
'SESSION_COOKIE_DOMAIN': None,
'SESSION_COOKIE_HTTPONLY': True,
'SESSION_COOKIE_NAME': 'session',
'SESSION_COOKIE_PATH': None,
'SESSION_COOKIE_SAMESITE': None,
'SESSION_COOKIE_SECURE': False,
'SESSION_REFRESH_EACH_REQUEST': True,
'SQLURI': 'postgres://username:xxx@localhost/db',
'TEMPLATES_AUTO_RELOAD': None,
'TESTING': False,
'TRAP_HTTP_EXCEPTIONS': False}
However, there are two significant drawbacks when using Python modules as configuration files. Firstly, since these configuration modules are Python files, it can be tempting to add code to them as well as simple values. By doing so, you will have to treat those modules like the rest of the application code; this can be a complicated way to ensure that it always produces the right value, especially if the configuration is produced with a template! Usually, when an application is deployed, the configuration is managed separately from the code.
Secondly, if another team is in charge of managing the configuration file of your application, they will need to edit the Python code to do so. While this is usually fine, it makes it increase the chance that some problems will be introduced, as it assumes that the other people are familiar with Python and how your application is structured. It is often good practice to make sure that someone who just needs to change the configuration doesn't also need to know how the code works.
Since Quart exposes its configuration via app.config
, it is quite simple to load additional options from a JSON, YAML, or other popular text-based configuration formats. All of the following examples are equivalent:
>>> from quart import Quart
>>> import yaml
>>> from pathlib import Path
>>> app = Quart(__name__)
>>> print(Path("prod_settings.json").read_text())
{
"DEBUG": false,
"SQLURI":"postgres://username:xxx@localhost/db"
}
>>> app.config.from_json("prod_settings.json")
>>> app.config["SQLURI"]
'postgres://username:xxx@localhost/db'
>>> print(Path("prod_settings.yml").read_text())
---
DEBUG: False
SQLURI: "postgres://username:xxx@localhost/db"
>>> app.config.from_file("prod_settings.yml", yaml.safe_load)
You can give from_file
a function to use to understand the data, such as yaml.safe_load
, toml.load
, and json.load
. If you prefer the INI format with [sections]
along with name = value
, then many extensions exist to help, and the standard library's ConfigParser
is also straightforward.
Blueprints
When you write microservices that have more than a single endpoint, you will end up with a number of different decorated functions—remember those are functions with a decorator above, such as @app.route
. The first logical step to organize your code is to have one module per endpoint, and when you create your app instance, make sure they get imported so that Quart registers the views.
For example, if your microservice manages a company's employees database, you could have one endpoint to interact with all employees, and one with teams. You could organize your application into these three modules:
app.py
: To contain theQuart
app object, and to run the appemployees.py
: To provide all the views related to employeesteams.py
: To provide all the views related to teams
From there, employees and teams can be seen as a subset of the app, and might have a few specific utilities and configurations. This is a standard way of structuring any Python application.
Blueprints take this logic a step further by providing a way to group your views into namespaces, making the structure used in separate files and giving it some special framework assistance. You can create a Blueprint
object that looks like a Quart
app object, and then use it to arrange some views. The initialization process can then register blueprints with app.register_blueprint
to make sure that all the views defined in the blueprint are part of the app. A possible implementation of the employee's blueprint could be as follows:
# blueprints.py
from quart import Blueprint
teams = Blueprint("teams", __name__)
_DEVS = ["Alice", "Bob"]
_OPS = ["Charles"]
_TEAMS = {1: _DEVS, 2: _OPS}
@teams.route("/teams")
def get_all():
return _TEAMS
@teams.route("/teams/<int:team_id>")
def get_team(team_id):
return _TEAMS[team_id]
The main module (app.py
) can then import this file, and register its blueprint with app.register_blueprint(teams)
. This mechanism is also interesting when you want to reuse a generic set of views in another application or several times in the same application—it's easy to imagine a situation where, for example, both the inventory management area and a sales area might want to have the same ability to look at current stock levels.
Error handling
When something goes wrong in your application, it is important to be able to control what responses the clients will receive. In HTML web apps, you usually get specific HTML pages when you encounter a 404
(Resource not found) or 5xx
(Server error), and that's how Quart works out of the box. But when building microservices, you need to have more control of what should be sent back to the client—that's where custom error handlers are useful.
The other important feature is the ability to debug your code when an unexpected error occurs; Quart comes with a built-in debugger, which can be activated when your app runs in debug mode.
Custom error handler
When your code does not handle an exception, Quart returns an HTTP 500
response without providing any specific information, like the traceback. Producing a generic error is a safe default behavior to avoid leaking any private information to users in the body of the error. The default 500
response is a simple HTML page along with the right status code:
$ curl http://localhost:5000/api
<!doctype html>
<title>500 Internal Server Error</title>
<h1>Internal Server Error</h1>
Server got itself in trouble
When implementing microservices using JSON, it is good practice to make sure that every response sent to clients, including any exception, is JSON-formatted. Consumers of your microservice will expect every response to be machine-parseable. It's far better to tell a client that you had an error and have it set up to process that message and show it to a human than to give a client something it doesn't understand and have it raise its own errors.
Quart lets you customize the app error handling via a couple of functions. The first one is the @app.errorhandler
decorator, which works like @app.route
. But instead of providing an endpoint, the decorator links a function to a specific error code.
In the following example, we use it to connect a function that will return a JSON-formatted error when Quart returns a 500
server response (any code exception):
# error_handler.py
from quart import Quart
app = Quart(__name__)
@app.errorhandler(500)
def error_handling(error):
return {"Error": str(error)}, 500
@app.route("/api")
def my_microservice():
raise TypeError("Some Exception")
if __name__ == "__main__":
app.run()
Quart will call this error view no matter what exception the code raises. However, in case your application issues an HTTP 404
or any other 4xx
or 5xx
response, you will be back to the default HTML responses that Quart sends. To make sure your app sends JSON for every 4xx
and 5xx
response, we need to register that function to each error code.
One place where you can find the list of errors is in the abort.mapping
dict. In the following code snippet, we register the error_handling
function to every error using app.register_error_handler
, which is similar to the @app.errorhandler
decorator:
# catch_all_errors.py
from quart import Quart, jsonify, abort
from werkzeug.exceptions import HTTPException, default_exceptions
def jsonify_errors(app):
def error_handling(error):
if isinstance(error, HTTPException):
result = {
"code": error.code,
"description": error.description,
"message": str(error),
}
else:
description = abort.mapping[ error.code].description
result = {"code": error.code, "description": description, "message": str(error)}
resp = jsonify(result)
resp.status_code = result["code"]
return resp
for code in default_exceptions.keys():
app.register_error_handler(code, error_handling)
return app
app = Quart(__name__)
app = jsonify_errors(app)
@app.route("/api")
def my_microservice():
raise TypeError("Some Exception")
if __name__ == "__main__":
app.run()
The jsonify_errors
function modifies a Quart
app instance and sets up the custom JSON error handler for every 4xx
and 5xx
error that might occur.