Python Microservices Development - Second Edition

By Simon Fraser , Tarek Ziadé
    What do you get with a Packt Subscription?

  • Instant access to this title and 7,500+ eBooks & Videos
  • Constantly updated with 100+ new titles each month
  • Breadth and depth in over 1,000+ technologies
  1. Free Chapter
    Discovering Quart
About this book

The small scope and self-contained nature of microservices make them faster, cleaner, and more scalable than code-heavy monolithic applications. However, building microservices architecture that is efficient as well as lightweight into your applications can be challenging due to the complexity of all the interacting pieces.

Python Microservices Development, Second Edition will teach you how to overcome these issues and craft applications that are built as small standard units using proven best practices and avoiding common pitfalls. Through hands-on examples, this book will help you to build efficient microservices using Quart, SQLAlchemy, and other modern Python tools

In this updated edition, you will learn how to secure connections between services and how to script Nginx using Lua to build web application firewall features such as rate limiting. Python Microservices Development, Second Edition describes how to use containers and AWS to deploy your services. By the end of the book, you’ll have created a complete Python application based on microservices.

Publication date:
September 2021
Publisher
Packt
Pages
310
ISBN
9781801076302

 

Discovering Quart

Quart was started in 2017 as an evolution of the popular Flask framework. Quart shares many of the same design decisions as Flask, and so a lot of the advice for one will work with the other. This book will focus on Quart to allow us to support asynchronous operations and to explore features such as WebSockets and HTTP/2 support.

Quart and Flask are not the only Python frameworks. There is a long history of projects aimed at providing services on the web, such as Bottle, cherrypy, and Django. All of these tools are used around the web, and they all share a similar goal: to offer the Python community simple tools for building web applications quickly.

The smaller frameworks, such as Quart and Bottle, are often called microframeworks; however, the term can be a bit misleading. It does not mean you can only create micro-applications. Using those tools, you can build any application, large or small. The prefix "micro" means that the framework tries to make as few decisions as possible. It lets you freely organize your application code and use whichever libraries you want.

A microframework acts as the glue code that delivers requests to your system and sends back responses. It does not enforce any particular paradigm on your project.

A typical example of this philosophy is when you need to interact with a SQL database. A framework such as Django is batteries-included and provides everything you need to build your web app, including an Object-Relational Mapper (ORM) to bind objects with database query results.

If you want to use an alternative ORM such as SQLAlchemy in Django to benefit from some of its great features, you'd be choosing a difficult path that would involve rewriting a lot of the Django library you are hoping to make use of, because of the tight integration Django has with the ORM it comes with. For certain applications, that's a good thing, but not necessarily for producing a microservice.

Quart, on the other hand, does not have a built-in library to interact with your data, leaving you free to choose your own. The framework will only attempt to make sure it has enough hooks to be extended by external libraries to provide various kinds of features. In other words, using an ORM in Quart, and making sure you're doing the right thing with SQL sessions and transactions, will mostly consist of adding a package such as SQLAlchemy to your project. If you don't like how a particular library integrates, you're free to use another one or to build your own integration. Quart can also make use of the more common Flask extensions, although there is a performance risk there as they are unlikely to be asynchronous and could block your application's work.

Of course, that's not a silver bullet. Being completely free in your choices also means that it is easier to make poor decisions and build an application that relies on defective libraries, or one that is not well designed. But fear not! This chapter will make sure you know what Quart has to offer, and how to organize your code for building microservices.

This chapter covers the following topics:

  • Making sure we have Python
  • How Quart handles requests
  • Quart's built-in features
  • A microservice skeleton

The goal of this chapter is to give you all the information needed to build microservices with Quart. By doing so, it inevitably duplicates some of the information you can find in Quart's official documentation, but focuses on providing interesting details and anything relevant when building microservices. Quart and Flask have good online documentation.

Make sure you take a look at Quart's and Flask's documentation, listed respectively:

Both should serve as a great complement to this chapter. The source code is located at https://gitlab.com/pgjones/quart.

This is worth being aware of, as the source code is always the ultimate truth when you need to understand how the software works.

 

Making sure we have Python

Before we start digging into its features, we should make sure that we have Python installed and working!

You might see some documentation or posts online that mention Python version 2. There was a long transition from Python 2 to Python 3, and had this book been written a few years earlier, we would be discussing the merits of each. However, Python 3 is fully capable of everything the majority of people need to do, and Python 2 stopped being supported by the core Python team in 2020. This book uses the latest Python 3.9 stable release for all its code examples, but they are likely to work on Python 3.7 or later, as that's the minimum version that Quart requires in order to work.

If your computer does not have at least Python 3.7, you can download a new version from Python's own website, where installation instructions are provided: https://www.python.org/downloads/.

You will find it easier if all the code examples in this book are run in a virtual environment, or virtualenv (https://docs.python.org/3/library/venv.html). A virtual environment is Python's way of keeping each project separate, as it means you can install Quart and any other libraries you need; it will only affect the application you are currently working on. Other applications and projects can have different libraries, or different versions of the same library, without them getting in the way of each other. Using a virtualenv also means that you can easily recreate your project's dependencies somewhere else, which will be very useful when we deploy a microservice in a later chapter.

Some code editors, such as PyCharm or Visual Studio, may manage a virtual environment for you. Every code example in the book runs in a terminal, and so we will use a terminal to create our virtualenv. This also shows how things work in more detail than viewing a program's output on the web, or in log files, and will be helpful when fixing any problems in the future.

In a terminal, such as a macOS Terminal application, or a Windows Subsystem for Linux, change to the directory you would like to work in and run the following command:

python -m venv my-venv

Depending on how you installed Python, you may need to use python3 to create the virtual environment.

This creates a new virtual environment called my-venv in the current directory. You could give it another path if you like, but it's important to remember where it is. To use the virtual environment, you must activate it:

source my-venv/bin/activate

For most of the command-line examples in this book, we assume you are running on Linux, as that is what most services online use, so it is good to be familiar with it. This means that most of the commands will also work on macOS or on Windows using the Windows Subsystem for Linux. It's also possible to run Docker containers on all these systems, and we will describe containers later on when we discuss deploying your microservice.

Now, let's install Quart so that we can run our example code:

pip install quart

To stop using the virtual environment without closing the terminal, you can type deactivate. For now, though, let's keep the virtualenv active and look at how Quart will work.

 

How Quart handles requests

The framework entry point is the Quart class in the quart.app module. Running a Quart application means running one single instance of this class, which will take care of handling incoming Asynchronous Server Gateway Interface (ASGI) and Web Server Gateway Interface (WSGI) requests, dispatch them to the right code, and then return a response. Remember that in Chapter 1, Understanding Microservices, we discussed ASGI and WSGI, and how they define the interface between a web server and a Python application.

The Quart class offers a route method, which can decorate your functions. When you decorate a function this way, it becomes a view and is registered in the routing system.

When a request arrives, it will be to a specific endpoint—usually a web address (such as https://duckduckgo.com/?q=quart) or part of an address, such as /api. The routing system is how Quart connects an endpoint to the view—the bit of code that will run to process the request.

Here's a very basic example of a fully functional Quart application:

# quart_basic.py
from quart import Quart
app = Quart(__name__)
@app.route("/api")
def my_microservice():
    return {"Hello": "World!"}
if __name__ == "__main__":
    app.run()

All the code samples are available on GitHub at https://github.com/PacktPublishing/Python-Microservices-Development-2nd-Edition/tree/main/CodeSamples.

We see that our function returns a dictionary, and Quart knows that this should be encoded as a JSON object to be transferred. However, only querying the /api endpoint returns the value. Every other endpoint would return a 404 Error, indicating that it can't find the resource you requested because we haven't told it about any!

The __name__ variable, whose value will be __main__ when you run that single Python module, is the name of the application package. It's used by Quart to create a new logger with that name to format all the log messages, and to find where the file is located on the disk. Quart will use the directory as the root for helpers, such as the configuration that is associated with your app, and to determine default locations for the static and templates directories, which we will discuss later.

If you run that module in a terminal, the Quart app will run its own development web server, and start listening to incoming connections on port 5000. Here, we assume that you are still in the virtual environment created earlier and that the code above is in a file called quart_basic.py:

$ python quart_basic.py 
 * Serving Quart app 'quart_basic'
 * Environment: production
 * Please use an ASGI server (e.g. Hypercorn) directly in production
 * Debug mode: False
 * Running on http://localhost:5000 (CTRL + C to quit)
[2020-12-10 14:05:18,948] Running on http://localhost:5000 (CTRL + C to quit)

Visiting http://localhost:5000/api in your browser or with the curl command will return a valid JSON response with the right headers:

$ curl -v http://localhost:5000/api 
*   Trying localhost...
...
< HTTP/1.1 200
< content-type: application/json
< content-length: 18
< date: Wed, 02 Dec 2020 20:29:19 GMT
< server: hypercorn-h11
<
* Connection #0 to host localhost left intact
{"Hello":"World!"}* Closing connection 0

The curl command is going to be used a lot in this book. If you are under Linux or macOS, it should be pre-installed; refer to https://curl.haxx.se/.

If you are not developing your application on the same computer as the one that you are testing it on, you may need to adjust some of the settings, such as which IP addresses it should use to listen for connections. When we discuss deploying a microservice, we will cover some of the better ways of changing its configuration, but for now, the app.run line can be changed to use a different host and port:

app.run(host="0.0.0.0", port=8000)

While many web frameworks explicitly pass a request object to your code, Quart provides a global request variable, which points to the current request object it built for the incoming HTTP request.

This design decision makes the code for the simpler views very concise. As in our example, if you don't have to look at the request content to reply, there is no need to have it around. As long as your view returns what the client should get and Quart can serialize it, everything happens as you would hope. For other views, they can just import that variable and use it.

The request variable is global, but it is unique to each incoming request and is thread-safe. Let's add some print method calls here and there so that we can see what's happening under the hood. We will also explicitly make a Response object using jsonify, instead of letting Quart do that for us, so that we can examine it:

# quart_details.py
from quart import Quart, request, jsonify
app = Quart(__name__)
@app.route("/api", provide_automatic_options=False)
async def my_microservice():
    print(dir(request))
    response = jsonify({"Hello": "World!"})
    print(response)
    print(await response.get_data())
    return response
if __name__ == "__main__":
    print(app.url_map)
    app.run()

Running that new version in conjunction with the curl command in another terminal, you get a lot of details, including the following:

$ python quart_details.py 
QuartMap([<QuartRule '/api' (HEAD, GET, OPTIONS) -> my_microservice>,
 <QuartRule '/static/<filename>' (HEAD, GET, OPTIONS) -> static>])
Running on http://localhost:5000 (CTRL + C to quit)
  
[… '_load_field_storage', '_load_form_data', '_load_json_data', '_send_push_promise', 'accept_charsets', 'accept_encodings', 'accept_languages', 'accept_mimetypes', 'access_control_request_headers', 'access_control_request_method', 'access_route', 'args', 'authorization', 'base_url', 'blueprint', 'body', 'body_class', 'body_timeout', 'cache_control', 'charset', 'content_encoding', 'content_length', 'content_md5', 'content_type', 'cookies', 'data', 'date', 'dict_storage_class', 'encoding_errors', 'endpoint', 'files', 'form', 'full_path', 'get_data', 'get_json', 'headers', 'host', 'host_url', 'http_version', 'if_match', 'if_modified_since', 'if_none_match', 'if_range', 'if_unmodified_since', 'is_json', 'is_secure', 'json', 'list_storage_class', 'max_forwards', 'method', 'mimetype', 'mimetype_params', 'on_json_loading_failed', 'origin', 'parameter_storage_class', 'path', 'pragma', 'query_string', 'range', 'referrer', 'remote_addr', 'root_path', 'routing_exception', 'scheme', 'scope', 'send_push_promise', 'url', 'url_charset', 'url_root', 'url_rule', 'values', 'view_args']
Response(200)
b'{"Hello":"World!"}'

Let's explore what's happening here:

  • Routing: When the service starts, Quart creates the QuartMap object, and we can see here what it knows about endpoints and the associated views.
  • Request: Quart creates a Request object and my_microservice is showing us that it is a GET request to /api.
  • dir() shows us which methods and variables are in a class, such as get_data() to retrieve any data that was sent with the request.
  • Response: A Response object to be sent back to the client; in this case, curl. It has an HTTP response code of 200, indicating that everything is fine, and its data is the 'Hello world' dictionary we told it to send.

Routing

Routing happens in app.url_map, which is an instance of the QuartMap class that uses a library called Werkzeug. That class uses regular expressions to determine whether a function decorated by @app.route matches the incoming request. The routing only looks at the path you provided in the route call to see whether it matches the client's request.

By default, the mapper will only accept GET, OPTIONS, and HEAD methods on a declared route. Sending an HTTP request to a valid endpoint with an unsupported method will return a 405 Method Not Allowed response together with a list of supported methods in the allow header:

$ curl -v -XDELETE  http://localhost:5000/api
**   Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 5000 (#0)
> DELETE /api HTTP/1.1
> Host: localhost:5000
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 405
< content-type: text/html
< allow: GET, OPTIONS, HEAD
< content-length: 137
< date: Wed, 02 Dec 2020 21:14:36 GMT
< server: hypercorn-h11
<
<!doctype html>
<title>405 Method Not Allowed</title>
<h1>Method Not Allowed</h1>
Specified method is invalid for this resource
* Connection #0 to host 127.0.0.1 left intact
    * Closing connection 0

If you want to support specific methods allowing you to POST to an endpoint or DELETE some data, you can pass them to the route decorator with the methods argument, as follows:

@app.route('/api', methods=['POST', 'DELETE', 'GET']) 
def my_microservice(): 
    return {'Hello': 'World!'}

Note that the OPTIONS and HEAD methods are implicitly added in all rules since it is automatically managed by the request handler. You can deactivate this behavior by giving the provide_automatic_options=False argument to the route function. This can be useful when you want to add custom headers to the response when OPTIONS is called, such as when dealing with Cross-Origin Resource Sharing (CORS), in which you need to add several Access-Control-Allow-* headers.

For more information regarding HTTP request methods, a good resource is the Mozilla Developer Network: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods.

Variables and converters

A common requirement for an API is the ability to specify exactly which data we want to request. For example, if you have a system where each person has a unique number to identify them, you might want to create a function that handles all requests sent to the /person/N endpoint, so that /person/3 only deals with ID number 3, and /person/412 only affects the person with ID 412.

You can do this with variables in the route, using the <VARIABLE_NAME> syntax. This notation is pretty standard (Bottle also uses it), and allows you to describe endpoints with dynamic values. If we create a route such as /person/<person_id>, then, when Quart calls your function, it converts the value it finds in the URL to a function argument with the same name:

@app.route('/person/<person_id>') 
def person(person_id): 
    return {'Hello': person_id}
 
$ curl localhost:5000/person/3 
{"Hello": "3"} 

If you have several routes that match the same URL, the mapper uses a particular set of rules to determine which one it calls. Quart and Flask both use Werkzeug to organize their routing; this is the implementation description taken from Werkzeug's routing module:

  1. Rules without any arguments come first for performance. This is because we expect them to match faster and some common rules usually don't have any arguments (index pages, and so on).
  2. The more complex rules come first, so the second argument is the negative length of the number of weights.
  3. Lastly, we order by the actual weights.

Werkzeug's rules have, therefore, weights that are used to sort them, and this is not used or made visible in Quart. So, it boils down to picking views with more variables first, and then the others, in order of appearance, when Python imports the different modules. The rule of thumb is to make sure that every declared route in your app is unique, otherwise tracking which one gets picked will give you a headache.

This also means that our new route will not respond to queries sent to /person, or /person/3/help, or any other variation—only to /person/ followed by some set of characters. Characters include letters and punctuation, though, and we have already decided that /api/apiperson_id is a number! This is where converters are useful.

We can tell the route that a variable has a specific type. Since /api/apiperson_id is an integer, we can use <int:person_id>, as in the previous example, so that our code only responds when we give a number, and not when we give a name. You can also see that instead of the string "3", person_id is a number, with no quotes:

@app.route('/person/<int:person_id>') 
def person(person_id): 
    return {'Hello': person_id}
$ curl localhost:5000/person/3 
{ 
  "Hello": 3 
} 
$ curl localhost:5000/person/simon
<!doctype html>
<title>404 Not Found</title>
<h1>Not Found</h1>
Nothing matches the given URI

If we had two routes, one for /person/<int:person_id> and one for /person/<person_id> (with different function names!), then the more specific one, which needs an integer, would get all the requests that had a number in the right place, and the other function would get the remaining requests.

Built-in converters are string (the default is a Unicode string), int, float, path, any, and uuid.

The path converter is like the default converter, but includes forward slashes, so that a request to a URL, /api/some/path/like/this, would match the route /api/<path:my_path>, and the function would get an argument called my_path containing some/path/like/this. If you are familiar with regular expressions, it's similar to matching [^/].*?.

int and float are for integers and floating-point—decimal—numbers. The any converter allows you to combine several values. It can be a bit confusing to use at first, but it might be useful if you need to route several specific strings to the same place. A route of /<any(about, help, contact):page_name> will match requests to /about, /help, or /contact, and which one was chosen will be in the page_name variable passed to the function.

The uuid converter matches the UUID strings, such as those that you get from Python's uuid module, providing unique identifiers. Examples of all these converters in action are also in the code samples for this chapter on GitHub.

It's quite easy to create your custom converter. For example, if you want to match user IDs with usernames, you could create a converter that looks up a database and converts the integer into a username. To do this, you need to create a class derived from the BaseConverter class, which implements two methods: the to_python() method to convert the value to a Python object for the view, and the to_url() method to go the other way (used by url_for(), which is described in the next section):

# quart_converter.py
from quart import Quart, request 
from werkzeug.routing import BaseConverter, ValidationError
_USERS = {"1": "Alice", "2": "Bob"}
_IDS = {val: user_id for user_id, val in _USERS.items()}
class RegisteredUser(BaseConverter):
    def to_python(self, value):
        if value in _USERS:
            return _USERS[value]
        raise ValidationError()
    def to_url(self, value):
        return _IDS[value]
app = Quart(__name__)
app.url_map.converters["registered"] = RegisteredUser
@app.route("/api/person/<registered:name>")
def person(name):
    return {"Hello": name}
if __name__ == "__main__":
    app.run()

The ValidationError method is raised in case the conversion fails, and the mapper will consider that the route simply does not match that request. Let's try a few calls to see how that works in practice:

$ curl localhost:5000/api/person/1 
{ 
  "Hello hey": "Alice" 
}
 
$ curl localhost:5000/api/person/2 
{ 
  "Hello hey": "Bob" 
}
 
$ curl localhost:5000/api/person/3 
 
<!doctype html>
<title>404 Not Found</title>
<h1>Not Found</h1>
Nothing matches the given URI

Be aware that the above is just an example of demonstrating the power of converters—an API that handles personal information in this way could give a lot of information away to malicious people. It can also be painful to change all the routes when the code evolves, so it is best to only use this sort of technique when necessary.

The best practice for routing is to keep it as static and straightforward as possible. This is especially true as moving all the endpoints requires changing all of the software that connects to them! It is often a good idea to include a version in the URL for an endpoint so that it is immediately clear that the behavior will be different between, for example, /v1/person and /v2/person.

The url_for function

The last interesting feature of Quart's routing system is the url_for() function. Given any view, it will return its actual URL. Here's an example of using Python interactively:

>>> from quart_converter import app 
>>> from quart import url_for 
>>> import asyncio
>>> async def run_url_for():
...     async with app.test_request_context("/", method="GET"):
...         print(url_for('person', name='Alice')) 
... 
>>> loop = asyncio.get_event_loop()
>>> loop.run_until_complete(run_url_for())
/api/person/1  

The previous example uses the Read-Eval-Print Loop (REPL), which you can get by running the Python executable directly. There is also some extra code there to set up an asynchronous program because here, Quart is not doing that for us.

The url_for feature is quite useful in templates when you want to display the URLs of some views—depending on the execution context. Instead of hardcoding some links, you can just point the function name to url_for to get it.

Request

When a request comes in, Quart calls the view and uses a Request Context to make sure that each request has an isolated environment, specific to that request. We saw an example of that in the code above, where we were testing things using the helper method, test_request_context(). In other words, when you access the global request object in your view, you are guaranteed that it is unique to the handling of your specific request.

As we saw earlier when calling dir(request), the Request object contains a lot of methods when it comes to getting information about what is happening, such as the address of the computer making the request, what sort of request it is, and other information such as authorization headers. Feel free to experiment with some of these request methods using the example code as a starting point.

In the following example, an HTTP Basic Authentication request that is sent by the client is always converted to a base64 form when sent to the server. Quart will detect the Basic prefix and will parse it into username and password fields in the request.authorization attribute:

# quart_auth.py
from quart import Quart, request
app = Quart(__name__)
@app.route("/")
def auth():
    print("Quart's Authorization information")
    print(request.authorization)
    return ""
if __name__ == "__main__":
    app.run()
$ python quart_auth.py 
* Running on http://localhost:5000/ (Press CTRL+C to quit) 
Quart's Authorization information
{'username': 'alice', 'password': 'password'} 
[2020-12-03 18:34:50,387] 127.0.0.1:55615 GET / 1.1 200 0 3066
$ curl http://localhost:5000/ --user alice:password

This behavior makes it easy to implement a pluggable authentication system on top of the request object. Other common request elements, such as cookies and files, are all accessible via other attributes, as we will discover throughout this book.

Response

In many of the previous examples, we have simply returned a Python dictionary and left Quart to produce a response for us that the client will understand. Sometimes, we have called jsonify() to ensure that the result is a JSON object.

There are other ways to make a response for our web application, along with some other values that are automatically converted to the proper object for us. We could return any of the following, and Quart would do the right thing:

  • Response(): Creates a Response object manually.
  • str: A string will be encoded as a text/html object in the response. This is especially useful for HTML pages.
  • dict: A dictionary will be encoded as application/json using jsonify().
  • A generator or asynchronous generator object can be returned so that data can be streamed to the client.
  • A (response, status) tuple: The response will be converted to a response object if it matches one of the preceding data types, and the status will be the HTTP response code used.
  • A (response, status, headers) tuple: The response will be converted, and the response object will use a dictionary provided as headers that should be added to the response.

In most cases, a microservice will be returning data that some other software will interpret and choose how to display, and so we will be returning Python dictionaries or using jsonify() if we want to return a list or other object that can be serialized as JSON.

Here's an example with YAML, another popular way of representing data: the yamlify() function will return a (response, status, headers) tuple, which will be converted by Quart into a proper Response object:

# yamlify.py
from quart import Quart
import yaml  # requires PyYAML
app = Quart(__name__)
def yamlify(data, status=200, headers=None):
    _headers = {"Content-Type": "application/x-yaml"}
    if headers is not None:
        _headers.update(headers)
    return yaml.safe_dump(data), status, _headers
@app.route("/api")
def my_microservice():
    return yamlify(["Hello", "YAML", "World!"])
if __name__ == "__main__":
    app.run()

The way Quart handles requests can be summarized as follows:

  1. When the application starts, any function decorated with @app.route() is registered as a view and stored in app.url_map.
  2. A call is dispatched to the right view depending on its endpoint and method.
  3. A Request object is created in a local, isolated execution context.
  4. A Response object wraps the content to send back.

These four steps are roughly all you need to know to start building apps using Quart. The next section will summarize the most important built-in features that Quart offers, alongside this request-response mechanism.

 

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": "bob@example.com",
    "from": "shopping@example-shop.com",
    "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 the Quart app object, and to run the app
  • employees.py: To provide all the views related to employees
  • teams.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.

 

A microservice skeleton

So far in this chapter, we have looked at how Quart works, and at most of the built-in features it provides—all of which we will be using throughout this book. One topic we have not yet covered is how to organize the code in your projects, and how to instantiate your Quart app. Every example so far has used a single Python module and the app.run() call to run the service.

Having everything in a module is possible, but will create a lot of headaches unless your code is just a few lines. Since we will want to release and deploy the code, it's better to have it inside a Python package so that we can use standard packaging tools such as pip and setuptools.

It is also a good idea to organize views into blueprints, and have one module per blueprint. This lets us keep better track of what each bit of code does, and re-use code whenever possible.

Lastly, the run() call can be removed from the code since Quart provides a generic run command that looks for an application using information from the QUART_APP environment variable. Using that runner offers extra options, such as the ability to configure the host and port that will be used to run the app without going into the settings each time.

The microservice project on GitHub was created for this book and is a generic Quart project that you can use to start a microservice. It implements a simple layout, which works well for building microservices. You can install and run, and then modify it. The project can be found at https://github.com/PythonMicroservices/microservice-skeleton.

The microservice project skeleton contains the following structure:

  • setup.py: Distutils' setup file, which is used to install and release the project.
  • Makefile: A Makefile that contains a few useful targets to make, build, and run the project.
  • settings.yml: The application default settings in a YAML file.
  • requirements.txt: The project dependencies following the pip format produced by pip freeze.
  • myservices/: The actual package
    • __init__.py
    • app.py: The app module, which contains the app itself
    • views/: A directory containing the views organized in blueprints
      • __init__.py
      • home.py: The home blueprint, which serves the root endpoint
    • tests/: The directory containing all the tests
      • __init__.py
      • test_home.py: Tests for the home blueprint views

In the following code, the app.py file instantiates a Quart app using a helper function called create_app to register the blueprints and update the settings:

import os
from myservice.views import blueprints
from quart import Quart
_HERE = os.path.dirname(__file__)
_SETTINGS = os.path.join(_HERE, "settings.ini")
def create_app(name=__name__, blueprints=None, settings=None):
    app = Quart(name)
    # load configuration
    settings = os.environ.get("QUART_SETTINGS", settings)
    if settings is not None:
        app.config.from_pyfile(settings)
    # register blueprints
    if blueprints is not None:
        for bp in blueprints:
            app.register_blueprint(bp)
    return app
app = create_app(blueprints=blueprints, settings=_SETTINGS)

The home.py view uses a blueprint to create a simple route that doesn't return anything:

from quart import Blueprint
home = Blueprint("home", __name__)
@home.route("/")
def index():
    """Home view.
    This view will return an empty JSON mapping.
    """
    return {}

This example application can run via Quart's built-in command line, using the package name:

$ QUART_APP=myservice quart run
 * Serving Quart app 'myservice.app'
 * Environment: production
 * Please use an ASGI server (e.g. Hypercorn) directly in production
 * Debug mode: False
 * Running on http://localhost:5000 (CTRL + C to quit)
[2020-12-06 20:17:28,203] Running on http://127.0.0.1:5000 (CTRL + C to quit)

From there, building JSON views for your microservice consists of adding modules to microservices/views, and their corresponding tests.

 

Summary

This chapter gave us a detailed overview of the Quart framework and how it can be used to build microservices. The main things to remember are:

  • Quart wraps a simple request-response mechanism around the ASGI protocol, which lets you write your applications in almost vanilla Python.
  • Quart is easy to extend and can use Flask extensions if required.
  • Quart comes with some useful built-in features: blueprints, globals, signals, a template engine, and error handlers.
  • The microservice project is a Quart skeleton, which will be used to write microservices throughout this book.

The next chapter will focus on development methodology: how to continuously code, test, and document your microservices.

About the Authors
  • Simon Fraser

    Simon Fraser works as a Lead Site Reliability Engineer at Cisco Meraki. Previously, he worked at Mozilla as a Senior Release Engineer and Systems Administrator in a high-performance computing environment, which is where he started using Python to automate systems administration tasks. He has more than 20 years of experience in writing software tools, automating and deploying large-scale systems, and using data to drive the flow of work.

    Browse publications by this author
  • Tarek Ziadé

    Tarek Ziadé is a Software Engineer, located in Burgundy, France. He works at Elastic, building tools for developers. Before Elastic, he worked at Mozilla for 10 years, and he founded a French Python User group called Afpy. Tarek has written several articles on the Python language for various magazines, and also a few books in French and English.

    Browse publications by this author
Python Microservices Development - Second Edition
Unlock this book and the full library FREE for 7 days
Start now