Simple RESTful Flask service

Nov 2016
Sun 06
0
0
0

Have a REST and take a sip from your Flask...

As a follow on my previous post on sockets and HTTP, this post will demonstrate a simple REST service using Flask.

Firstly, what is REST or a RESTful service?

REST

In terms of communication between two processes, REST is the simplest concept. If you were coming up with a method of transferting data between two isolated processes, a first thought would probably be: "encode a version of the state of one process and send it to the other". Each process would act independently and only have information that was sent in the current message. This summaries the basis of communication using REST or REpresentational State Transfer.

Properties of a RESTful service

Of the properties and constraints of a RESTful service, a few are important to understand (at least in my opinion):

  1. Stateless - The server should hold no context or state of the connecting client
  2. Self-descriptive messages - Enough information should be sent with each message to allow interpretation by the client/server.
  3. Hypermedia as the engine of application state - A client should be able to interogate a RESTful service to extract reources (the API).
  4. Layered system - Communication may go through a middleware process, unbeknown to the connecting client. This is useful for processes such as load balancing of server resources.

REST over HTTP

To implement a RESTful service using HTTP, we make use of the HTTP methods GET, POST, PUT and DELETE. We expose resources using URLs from a common base URL. For example, if we had a service for interogeting the staff of a company, the base URL might be:

http://api.mycompnay.com/resources/

and the GET request:

http://api.myservice.com/resources/employees

would return a JSON encoded list of employees.

Flask

To demonstrate a RESTful service, we could build a HTTP server from the gound up using sockets (see the previous post). However this is more involved and, believe it or not, this has all been done before.

Flask is a web framework for developing apps for web deployment. It takes care of the nitty-gritty of HTTP (routing, encoding, authorisation etc) and also integrates with templating engines like Jinja2 (as used in this website). For our needs, we will use Flask to do all of the heavy lifting and focus on the implementation of a RESTful service.

So, the Flask server:

flask_server.py

from flask import Flask, jsonify, abort

app = Flask("MyRESTservice")

#in memory data store (in practice this is pulled from a DB)
class Address():
    def __init__(self, ID, number, line_1, line_2, post_code):
        self.ID = ID
        self.number = number
        self.line_1 = line_1
        self.line_2 = line_2
        self.post_code = post_code

add1 = Address(1, 2, "North Street", "Python City", "PY11 0PY")
add2 = Address(2, 21, "South Street", "Python City", "PY12 5PY")

addresses = [add1,add2]

@app.route('/address_book/api/v1.0/addresses', methods=['GET'])
def get_addressess():
    return jsonify({'addresses': [add.__dict__ for add in addresses ]})


if __name__ == '__main__':
    app.run(host="localhost", port=12345)

The key section to understand here is:

@app.route('/address_book/api/v1.0/addresses', methods=['GET'])
def get_addressess():
    return jsonify({'addresses': [add.__dict__ for add in addresses ]})

Here we are defining the routining for the GET request address_book/api/v1.0/addresses. The function decorated with the routing info returns a json string of addresses. This return string will be in the HTTP repsonse to the GET request.

To test out the server, we need a client. We could use a browser - navigate to localhost:12345/address_book/api/v1.0/addresses and see the json response.

Another option is to write a socket-based python client:

socket_client.py

import socket

clientsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
clientsocket.connect(("localhost", 12345))

clientsocket.send("GET /address_book/api/v1.0/addresses HTTP/1.0\r\n\r\n")

rec=""
msg=""
while (1):
    rec = clientsocket.recv(1024)
    if not rec:break
    msg+=rec

print "client received: "+msg

This method clearly shows the form of the underlying HTTP GET request. Running both python scripts (in separate processes) yeilds:

running server

python flask_server.py

running client

python socket_client.py
client received:2 HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 282
Server: Werkzeug/0.11.11 Python/2.7.8
Date: Sun, 06 Nov 2016 18:53:39 GMT

{
    "addresses": [
        {
        "ID": 1, 
        "line_1": "North Street", 
        "line_2": "Python City", 
        "number": 2, 
        "post_code": "PY11 0PY"
        }, 
        {
        "ID": 2, 
        "line_1": "South Street", 
        "line_2": "Python City", 
        "number": 21, 
        "post_code": "PY12 5PY"
        }
    ]
}

Here we can see the JSON in the HTTP response, along with information regarding the message (recall the requirements for REST - Self-descriptive messages)

An alternative to using sockets in the client, we can use the Python Requests library. This significantly reduces the number of lines of code:

requests_client.py

import requests

r = requests.get('http://localhost:12345/address_book/api/v1.0/addresses')
print r.json()

Now we have a RESTful service which provides a list of all addresses. Let's increase the API to provide specific addresses for a given ID:

flask_server.py

@app.route('/address_book/api/v1.0/addresses/<int:address_id>', methods=['GET'])
def get_address(address_id):
    selected = [selected for selected in addresses if selected.ID == address_id]
    if len(selected)==0:
        abort(404)
    return jsonify({'address': [add.__dict__ for add in selected ]})

Here we have introduced the syntax for including parameters in the request URL. The syntax <int:address_id> defines a variable address_id and converts it to an int. Without the converter, the parameter is assumed to be a string <address_id>.

Another important introduction is the abort(404) method. This is a method (provided by flask) to handle errors and return corresponding error information.

We can now request specific addresses:

r = requests.get('http://localhost:12345/address_book/api/v1.0/addresses/1')
print r.json()

But what about adding addresses? We can expand the API to include POST requests and use these to add addresses to the server-side list. Modifications to the server code:

flask_server.py

@app.route('/address_book/api/v1.0/addresses', methods=['POST'])
def add_address():
    print request.json
    if not request.json: 
        abort(400)
    reqs = ['ID', 'number', 'post_code']
    for req in reqs:
        if not req in request.json:
            abort(400)

    add = Address(request.json['ID'],
            request.json['number'], 
            request.json.get('line_1', ""),
            request.json.get('line_2', ""),
            request.json['post_code'], 
    )
    addresses.append(add)
    return jsonify({'added': add.__dict__}), 201

The above has two catches for errors, one if the request coming in is not json and another check for mandatory data; ID, number and post_code. The lines:

request.json.get('line_1', ""),
request.json.get('line_2', ""),

set default values to "" if not present in the request json. The return value for the response is the json representation of the added address.

The client script can be modified to make the POST request:

data = { "ID" : 3, "number": 11, "post_code": "PY13 5UP"}

r = requests.post('http://localhost:12345/address_book/api/v1.0/addresses',
json = data)

print r.json()

Running the server/client combo results in:

client output

{
    u'added': {
        u'line_1': u'',
        u'line_2': u'',
        u'ID': 3,
        u'post_code': u'PY135UP',
        u'number': 11
    }
}

Now our REST API has the following resources:

POST http://localhost:12345/address_book/api/v1.0/addresses
GET http://localhost:12345/address_book/api/v1.0/addresses
GET http://localhost:12345/address_book/api/v1.0/addresses/<id>

The API can be expanded to add request such as DELETE. There are also methods for authentication and tools for autodocumenting the API, but this will have to be the subject of a separate post.




Comments