Backend / DevOps / Architect
Brit by birth,
located worldwide

All content © Alex Shepherd 2008-2024
unless otherwise noted

Building a YAML router for Falcon Framework

Published
2 min read
image
Image Credit: Peter K Burian

Today, we're going to go over how to build a YAML router for the Python Framework "Falcon".

Introduction

Falcon is a lightweight, unopinionated web framework for Python. I've used it for a number of personal and freelance projects recently, and have been extremely impressed with its performance, scalability and simplicity. It's designed primarily for RESTful APIs, as one of the most important characteristics of any web API is extremely good performance, both under high and low levels of traffic.

In fact, it's so lightweight and unopinionated, it doesn't require any libraries other than six for Python 2/3 compatibility and python-mimeparse for correctly parsing content types. This means request routing is all hard coded, which gets pretty messy and hard to read for larger projects. My solution for this is to allow route configuration via YAML.

Building the router

How routing works out of the box

By default, you add routes to your application manually in code by calling falcon.API's "add_route" method, passing it a URI template and a "resource". A resource in Falcon is any class which provides responder methods which map to HTTP request verbs.

Initialising our application

First of all, we need to install a version of Python supported by Falcon. For this example, I used 3.6.1 - the latest release of Python 3 - although any Falcon-supported Python 3 should work identically for our purposes. We'll use PyEnv, which provides a way to easily manage any number of Python versions and VirtualEnvs based on them.

Below, we're using packages provided by Debian Jessie. If you're using a different Linux distribution, or Windows the top part of the instructions will differ.

$ curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | bash
... snip ...

$ echo 'export PATH="~/.pyenv/bin:$PATH"
eval "$(pyenv init -)"
eval "$(pyenv virtualenv-init -)"' >> ~/.bashrc

$ sudo apt-get install -y build-essential libssl-dev libreadline-dev libsqlite3-dev libbz2-dev libyaml-dev zlib1g-dev
... snip ...

$ pyenv install 3.6.1
... snip ...

$ pyenv virtualenv 3.6.1 falcon-yaml-router
... snip ...

We'll now go and set up our project's VCS with some sensible defaults for a Python project.

$ mkdir -p /path/to/project

$ cd /path/to/project

$ git init
Initialised empty Git repository in /path/to/project

$ wget -O .gitignore "https://raw.githubusercontent.com/github/gitignore/master/Python.gitignore"

$ pyenv local falcon-yaml-router

(falcon-yaml-router) $ pip install -U pip
... snip ...

(falcon-yaml-router) $ pip install falcon
... snip ...

(falcon-yaml-router) $ pip install PyYaml
... snip ...

(falcon-yaml-router) $ pip freeze > requirements.txt

(falcon-yaml-router) $ git add -A

(falcon-yaml-router) $ git commit -m "Initial commit"

Now we're ready to start coding our project.

Writing resources to apply to our router

Let's build a couple of very simple resources we can use to test our application. Create a file called resources.py in the project folder and enter the following:

import json

class QuoteResource:

    quotes = {
        'arthur-c-clarke': [
            ('Any sufficiently advanced technology '
             'is indistinguishable from magic.'),
        ]
    }

    def on_get(self, req, resp, person, num):
        """
        Respond to GET requests

        Send a quote back to the user
        """
        resp.body = json.dumps({
            'quote': self.quotes[person][int(num)]
        })

class EchoResource:

    def on_post(self, req, resp):
        """
        Respond to POST requests

        Decode incoming JSON data then dump it back to the user
        """
        data = json.loads(req.stream.read())
        resp.body = json.dumps(data)

These are both very simple resources, and could easily have been combined into a single resource, but we've split it up so we can show the use of multiple routes.

Mapping routes from a YAML hash

Let's create a file called routing.py:

import yaml
import importlib

from falcon import routing

class YamlRouter(routing.CompiledRouter):

    """
    Falcon CompiledRouter extension to automatically load routes from a YAML
    configuration file.
    """

    def __init__(self, routing_file):
        """
        Constructor - load routes from YAML
        """
        super().__init__()
        self._routing_file = routing_file
        self._load_routes()

    def _load_routes(self):
        """
        Load the routes from the configured file

        Here's where the magic happens. We use importlib to import a module
        from its string representation, and then load the resource by fetching
        the class as an attribute from the module.
        """
        with open(self._routing_file, 'r') as f:
            routes = yaml.load(f)

        for uri_tpl, resource in routes.items():
            module_parts = resource.split('.')
            module_name = '.'.join(module_parts[:-1])

            module = importlib.import_module(module_name)
            Resource = getattr(module, module_parts[-1])

            resource_instance = Resource()
            method_map = routing.create_http_method_map(resource_instance)
            self.add_route(uri_tpl, method_map, resource_instance)

Configure routing

Now that we've got our router in place, let's set up the routing configuration file. Let's call it routing.yml in the project root:

---
/quote/{person}/{num}: resources.QuoteResource
/echo: resources.EchoResource

Set up and run the application

Now let's set up a very simple WSGI application to run via Gunicorn. Start by creating a file called application.py in the project root:

import falcon

from routing import YamlRouter

application = falcon.api.API(router=YamlRouter('routing.yml'))

That's all we should need to get things working!

To run the application, let's first install gunicorn:

(falcon-yaml-router) $ pip install gunicorn

And finally run the application:

(falcon-yaml-router) $ gunicorn application:application --log-level=debug

Test the application

Let's issue some HTTP requests using cURL to test that our router works:

$ curl -XGET "http://localhost:8000/quote/arthur-c-clarke/0" | python -mjson.tool
{
    "quote": "Any sufficiently advanced technology is indistinguishable from magic."
}

$ curl -XPOST --data '{"random-key":"StuffAndThings"}' "http://localhost:8000/echo" | python -mjson.tool
{
    "random-key": "StuffAndThings"
}

As we can see, QuoteResource returns an item from the self.quotes dictionary, and EchoResource decodes then re-encodes the user's JSON input. Now we can commit our code to the repository:

(falcon-yaml-router) $ git add -A
(falcon-yaml-router) $ git commit -m "Implemented YAML router"

This is an extremely simple example, but extending this to support almost anything is possible. To add further configurable behaviours just change the mappings in routing.yml to point each URI template to a YAML hash instead of just a string representation of the class.

That's all we have for today, speak you all again with my next post!