1   Introduction

This post describes a REST CRUD Web application built in Python on top of Flask and flask-restful.

For information on REST () see here: https://en.wikipedia.org/wiki/Representational_state_transfer.

For information on CRUD () see here: https://en.wikipedia.org/wiki/Create,_read,_update_and_delete.

This module uses the SQLObject ORM for its database store. For information about SQLObject, see: http://sqlobject.org/.

2   Flask and flask-restful

Flask is a micro Web framework used to build applications in Python.

flask-restful is an extension for Flask. It supports building REST Web applications in Python in a quick and intuitive way.

The best way to get started with flask-restful is with the "Quick start guide": https://flask-restful.readthedocs.io/en/0.3.5/quickstart.html. The flask-restful Quickstart" gives a minimal buy complete REST application. In this post well flesh that out a bit, explain it, and then turn it into a template that you can use to quickly create your own REST applications.

Information about Flask is here:

You can install flask and flask-restful with pip. Something like the following should get you set up:

$ cd /my/test/directory
$ virtualenv flaskenv
$ source flaskenv/bin/activate
$ pip install flask flask-restful

3   Testing

The following shell scripts can be used to test the application that we will build. They also define the operations that we want that application to support. Note that you will likely need to make modifications, e.g. to the port number.

get-one-task.sh:

#!/bin/bash
# get-one-task.sh
# Retrieve a single task from the database.
curl -v http://localhost:8001/gettask

list-tasks.sh:

#!/bin/bash
# list-tasks.sh
# Retrieve a list of all existing tasks.
curl -v http://localhost:8001/gettask/$1

add-task.sh:

#!/bin/bash
# add-task.sh
# Create a new task; add it to the database.
curl -v -X POST http://localhost:8001/addtask -d "task_name=$1&description=$2"

update-task.sh:

#!/bin/bash
# update-task.sh
# Update an existing task.
curl -v -X PUT http://localhost:8001/updatetask -d "task_name=$1&description=$2"

delete-task.sh:

#!/bin/bash -x
# delete-task.sh
# Delete an existing task; remove it from the database.
curl -v -X DELETE http://localhost:8001/delete/$1

Notes:

  • In some cases, we specify the HTTP command with the curl "-X" option. That command is used by flask-restful to route control to the correct method/handler, i.e to the get, post, put, and delete methods in our handler class.
  • In some cases, we pass parameters in the query part of the URL using the curl "-d" option. For example, in add-task.sh and update-task.sh, we pass the task name and the task description in the URL encoded query string.
  • In some cases, we pass a parameter as part of the URL path. For example, in delete-task.sh, we pass the task name as the last component of the path.

4   The application, the code

This is the flask-restful application that implements the operations corresponding to the above four scripts:

#!/usr/bin/env python

"""
synopsis:
    Run flask-rest app with SQLObject database.
    Implements REST CRUD access to a set of tasks (name, description).
usage:
    python flaskrest_app01.py <db-file-name>
example:
    python flaskrest_app01.py data01.db
"""

# We try to enable our app to run under both Python 2 and Python 3.
from __future__ import print_function
import sys
import os
from flask import Flask, request
from flask_restful import abort, Api, Resource
import sqlobject

app = Flask(__name__)
api = Api(app)

#
# The following can be used for configuration.
##app.config.update(
##    DEBUG=True,
##    #SERVER_NAME='localhost:8080',
##    #SERVER_NAME='localhost:5000',
##    SERVER_NAME='localhost:8001',
##)

# Define the host and post.
SERVER_HOST = 'localhost'
SERVER_PORT = 8001


#
# Define the objects to be stored in the SQLObject database.
class TaskDef(sqlobject.SQLObject):
    task_name = sqlobject.StringCol()
    description = sqlobject.StringCol()


def open_db(db_filename):
    """Open the database.  If it does not exist, create it."""
    create = False
    db_filename = os.path.abspath(db_filename)
    if not os.path.exists(db_filename):
        create = True
    connection_string = 'sqlite:' + db_filename
    connection = sqlobject.connectionForURI(connection_string)
    sqlobject.sqlhub.processConnection = connection
    if create:
        TaskDef.createTable()


def get_task_by_name(task_name):
    """Retrieve a task from the DB.  Abort if it does not exist."""
    results = TaskDef.select(TaskDef.q.task_name == task_name)
    if results.count() < 1:
        abort(404, message="Task {} doesn't exist".format(task_name))
    return results[0]


class TaskList(Resource):
    """Handle operations on the list of tasks.

    Enable a GET command to get a list of tasks or a single task.
    Enable a POST command to create and add a new task.
    Enable a PUT command to update a task.
    """
    def __init__(self, *args, **kwargs):
        super(TaskList, self).__init__(*args, **kwargs)

    def get(self, task_name=None):
        """Return a collection of existing tasks or a single task."""
        if task_name is None:
            collection = {}
            results = TaskDef.select()
            for task in results:
                collection[task.task_name] = task.description
            return collection
        else:
            task = get_task_by_name(task_name)
            content = {
                'task_name': task_name,
                'description': task.description,
            }
            return content

    def delete(self, task_name):
        """Delete a single task."""
        task = get_task_by_name(task_name)
        TaskDef.delete(task.id)
        return "<p>Deleted task: {}</p>".format(task_name)
        return '', 204

    def post(self):
        """Create and save a new task."""
        task_name = request.form['task_name']
        description = request.form['description']
        TaskDef(
            task_name=task_name,
            description=description)
        task_json = {
            'task_name': task_name,
            'description': description}
        return task_json

    def put(self):
        """Create and save a new task."""
        task_name = request.form['task_name']
        description = request.form['description']
        task = get_task_by_name(task_name)
        task.description = description
        task_json = {
            'task_name': task_name,
            'description': description}
        return task_json


#
# Set up the api resource routing.
#
api.add_resource(
    TaskList,
    # Get a list of tasks.
    '/gettask',
    # Get one task by name.
    '/gettask/<task_name>',
    # Add a task.  Form data: task_name, description.
    '/addtask',
    # Update a task.  Form data: task_name, description.
    '/updatetask',
    # Delete one task.`
    '/delete/<task_name>',
)


def main():
    #import pdb; pdb.set_trace()
    args = sys.argv[1:]
    if len(args) != 1:
        sys.exit(__doc__)
    db_filename = args[0]
    open_db(db_filename)
    app.run(debug=True, host=SERVER_HOST, port=SERVER_PORT)


if __name__ == '__main__':
    main()

5   Explanation and guidance

5.1   Handlers and routing

TaskList -- TaskList is our flask-restful handler class.

Routing -- The call to api.add_resource sets up our routing. Notice that there are several paths that send control to this same class. Also notice that we do not specify the method by which a given path is to be handled. That method will be selected based on the HTTP method or verb. A request using a specific HTTP verb will be routed to the Python method in our handler class with the same name as the verb but in lower case.

HTTP verb Python handler method
GET get
POST post
PUT put
DELETE delete

More info -- I found the following helpful in learning to think about REST verbs and paths and operations: https://en.wikipedia.org/wiki/Representational_state_transfer#Applied_to_Web_services

5.2   Parameters

Path components -- If the parameter is passed as a component in the URL path, then we capture that parameter value the parameter in the path, for example we use <parameter_name> as a component in the path. Important: When we specify a parameter as a component of the path, we must modify the signature of any method in the handler that is called when that path is matched. (I found that the traceback that occurred when I forgot to do this was not informative and was hard to track down.)

Form data -- If the parameter is passed as form data or content and as URL encoded data, then we can use request.form to access those values. For example, when we use the following curl command:

curl -v -X POST http://localhost:8001/addtask -d "task_name=t23&description=tash 23"

Then we can capture the values from the request with this code fragment:

task_name = request.form['task_name']
description = request.form['description']

See the post and put methods in our application code for an example of this.

5.3   Returning content

From the flask-restful "Quickstart" doc:

Flask-RESTful understands multiple kinds of return values from view methods. Similar to Flask, you can return any iterable and it will be converted into a response, including raw Flask response objects. Flask-RESTful also support setting the response code and response headers using multiple return values ...

You can return a Python dictionary or a Python list and it will be converted to JSON and returned with a Content-Type: application/json header.

You can also, optionally return an HTTP response status code as a second value in the return statement. Example:

content = {'code': 'error125', 'message': 'resource not found'}
return content, 404

5.4   Starting the application

In the application code above, I use the following to start up the application:

app.run(debug=True, host=SERVER_HOST, port=SERVER_PORT)

Notes:

- Dave Kuhlman