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!