Flask with Rest

This post continues the development of an application using Flask. Today we will add some Rest endpoints to the anonymous estimator.

Flask with Rest

This post continues the development of an application using Flask. Today we will add some Rest endpoints to the anonymous estimator.

First a disclaimer. I’m new to both Python and the Flask framework, so this series of posts charts my learning. Consequently I may take a few wrong turns along the way. If you spot any issues, or have any comments, please let me know at [email protected].

Clean up

In the Introduction to Flask post I began the application, but I was not happy with how the configuration of the app was left in the file controllers.py. This will cause issues when I begin to add different controller classes, for example, to handle Rest calls.

The fix for this was straightforward. The first step was to move the definition from the controllers class into the __init__.py file. Add an import to this to pull in the controllers:

from flask import Flask
from flask_bootstrap import Bootstrap

app = Flask(__name__)
bootstrap = Bootstrap(app)

from estimator import controllers

Both the run.py and controllers.py need to change slightly, including importing app. The full change can be seen in this commit message on Github.

Test Fixtures

In this post I’ll be laying the groundwork for responding to Rest API calls. I am going to organise the calls separately into a controller for normal web requests and another, rest_controller.py for the rest calls. Consequently there will be two separate test files.

If I leave things as they are this would mean each test would create the app instance individually. Rather than do this, I want to create a fixture that can be injected into each test and used in any of the test files that are created. This will keep the setup of the test code consistent, only requiring a change in one place.

To do this I will place the code into a file called conftest.py; the filename is dictate by Pytest, which finds this in the module and runs before the test. The code should look familiar.

from estimator.controllers import app
import pytest

@pytest.fixture
def client():
	"Create the test client as a fixture."
	client = app.test_client()
	client.testing = True
	return client

Pytest creates some fixtures automatically. You can see them all including any created yourself by running pytest --fixtures. Note that I added a doc string to the above definition to improve the message it gives.

Now each test can accept a client object in its method definition. Make sure that the name of the fixture method definition is exactly the same as the object in the test method. For example, here is one of the tests already that we created earlier with the fixture added:

def test_page_not_found(client):
	response = client.get('/not_there')
	assert response.status == '404 NOT FOUND'

Adding Rest

When talking about Rest calls, the advice is to think in terms of the action being taken. This app will allow a scrum master on an Agile team to create an estimation group and add issues. Team members can join the group, see issues, and estimate on the issues. When all the estimates are in, then results will be shown anonymously, allowing the team to discuss the result, accept a consensus, or estimate again.

Here are some of the actions we need to consider:

  • Create a group
  • Query a group
  • Create a nickname
  • Join a group
  • Show the groups I’ve joined
  • Create an issue (within a group)
  • Make an estimate for an issue
  • See the results

This project will use Json to transmit data. It appears to be the consensus format of choice, being much easier to read and parse than XML. At first I will only create a few messages manually. The first actions to consider are querying or creating a group.

My approach to defining rest calls is to give it a structure like the following: <verb>: <app>/rest/<version>/<entity>/<id>?<param>=<value>. The keyword “rest” simply separates the API calls from normal web pages. The version will allow older clients to exist and function with newer ones, should the API change or evolve into a new version. This might allow a new feature to be tried out on a limited number of users before being turned on for everyone.

What is done to the enitity identified is determined by the verb used. For example, POST will attempt to create an entity with the id suggested, while GET will return information about it.

Let’s start by writing a test. The url to query or create a group called TestGroup will look like this, with GET querying and POST creating the group:

/rest/v1/group/TestGroup

The caller will use POST to create a group or GET to query the group. If the group is created successfully, then the response code should be 201 Created otherwise it should indicate an error. Querying the group should return 200 OK. Let’s not worry about what’s in the response yet. Querying a group for a non-accessible group will return a 404 Not Found.

def test_create_group(client):
	# TODO: Mock the response the service returns to the controller
	response = client.post('/rest/v1/group/TestGroup')
	assert response.status == '201 CREATED'
    # TODO: check the content
	# TODO: verify that the group was created

def test_create_group_name_extsts(client):
	# TODO: Mock the response - duplicate group
	response = client.post('/rest/v1/group/TestGroup')
	assert response.status == '400 BAD REQUEST'
    # TODO: check the content
	# TODO: verify that the group was not created

def test_query_group(client):
	response = client.get('/rest/v1/group/TestGroup')
	assert response.status == '200 OK'
    # TODO: check the content

def test_query_group_not_found(client):
	response = client.get('/rest/v1/group/UnknownGroup')
	assert response.status == '404 NOT FOUND'

Great, we have a failing tests with Not Found (one is passing because it expects the correct result for now). Time to create a controller that can accept GET and POST requests. I’ve separated the two types of message into separate methods rather than add a conditional block into a shared method.

While I am not going to complete the implementation of the rest controller in this post, I do want to return a HTTP response code so that the tests pass. Lots of “TODO” comments, I know, but we will get back to them.

from flask import Response
from estimator import app

@app.route('/rest/v1/group/<groupname>', methods = ['POST'])
def create_group(groupname):
	# TODO: implement
	if groupname == 'NewGroup':
		body = '{ "message" : "Group created" }'
		code = 201
	else:
		body = '{ "message" : "Group exists" }'
		code = 400
	return Response(body, status=code, mimetype='application/json')

@app.route('/rest/v1/group/<groupname>', methods = ['GET'])
def query_group(groupname):
	# TODO: implement
	if groupname == 'TestGroup':
		return '{ "groupname" : "' + groupname + '" }'
	else:
		return Response('{ "message" : "group not found"', 404, mimetype='application/json')

We also need to add an import to the end of the estimator package initialisation file, the second line is newly added. Now all the tests will pass.

from estimator import controllers
from estimator import rest_controller

JSON Response

The last step I want to do is to properly return a JSON object, rather than the raw text that looks like JSON. Python objects translate nicely into this notation using the json package. This is a lot better than trying to construct JSON manually; can you spot the mistake in the query_group method above?

Flask has its own extension of this so there is no need to install another dependency. We can access it with:

from flask import json

Then all that needs to be done is to use the json.dumps method to produce and json.loads method to read in JSON objects.

For example, to fix the mistake mentioned above (it was a missing trailing chain bracket) I would change:

return Response('{ "message" : "group not found"', 404, mimetype='application/json')

to this (note that by using a Python map the interpreter will spot that the closing bracket was missing):

return Response(json.dumps({ "message" : "Group not found"}), 404, mimetype='application/json')

Note that there is no change to the tests for this since I have not added any checking on the response yet. The only thing remaining is to take a look to see what this looks like when we use Postman to connect to the endpoints. Run the Flask app in development mode with FLASK_ENV=development python run.py and send a post to the endpoint. Here is a POST to create a new group (with the name that I know is hardcoded to return):