Handling REST representations with bottle.py

24/11/2012

Making data available via a REST architecture suppose to define resources. Another key point is that resources can/should have multiple representations. In order to deliver the good representation, expected by the client, the server has to make a content negotiation. Let's try with python & bottle.py.

Roy Fielding describe these principles in its chapter 5, §5.2.1.2 Representations

If the value set of a resource at a given time consists of multiple representations, content negotiation may be used to select the best representation for inclusion in a given message.

In my sample, I want my server to deliver 3 representations of the same resource:

  1. html : representation that can be displayed into a browser with an image ;
  2. json : representation that can be exchanged in a program ;
  3. plain text : representation that can be logged in the console.

The content negation is done according to the media type (defined by the mime type).

Using bottle to serve resources

I'm using bottle.py for months, because the summary found on the web page define bottle very well. From my point of view, it does work and it so simple, a great pleasure :

Bottle is a fast, simple and lightweight WSGI micro web-framework for Python. It is distributed as a single file module and has no dependencies other than the Python Standard Library.

BUT. There is no annotation to handle representations (as Jersey has, for instance). Bottle can deliver json, html, xml, and so one. But nothing is available to choose which representation the server should return according to the client choice.

There are several good tutorials on the internet explaining how to start with bottle.py, but I haven't seen any tutorial addressing these representation issue.

Some pointers:

That's why, I need a simple but understandable example.

Handle representation the “hard way”

A client can ask a representation of a resource by setting its choice in the HTTP headers. The Accept parameter have to be set for that purpose :

The text representation will be the default one.

Ask for the json representations with curl

curl -H "Accept: application/json" http://localhost:8080/book/0836221362

Ask for the plain text representation with curl

curl -H "Accept: text/plain" http://localhost:8080/book/0836221362

Ask for the plain text representation with curl

curl -H "Accept: text/html" http://localhost:8080/book/0836221362

On the server, the Accept metadata is set in the headers, so you can retrieve it with

	accept=request.headers.get("Accept")

Then, a very basic way to handle representations is to test the different supported mime type by searching them in the accept variable.

The @route is the same but the response will vary according to the Accept metadata set by the client.

3 functions prepare the representation. Each takes the same parameter, the data.

def html_repr(data):
	"""
	HTML Representation of my data
	data : my data in a dictionary
	"""
	html="""
	<html>
		<head><title>Book ISBN %s</title></head>
		<body><h1>%s</h1>
		 <h2>%s</h2><img src="%s"/>
		</body>
	</html>"""%(data["isbn"], data["title"], 
				data["author"], data["img"])
	
	return html.strip()

def json_repr(data):
	"""
	JSON Representation of my data
	data : my data in a dictionary
	"""
	return data

def txt_repr(data):
	"""
	TEXT Representation of my data
	data : my data in a dictionary
	"""
	return "[%s] %s - %s\n"%(data["isbn"], data["title"], data["author"])

	

The main function which delivers the result can be like this


# Define the route (URL) of my resource
@route('/book/:isbn')

def book_resource(isbn):
	book={'isbn':isbn}
	book.update(mydata[isbn])

	# get the Accept paramter from the HTTP Header
	accept=request.headers.get("Accept")

	# Choose the good representation according to the mimetype
	if MIME_HTML in accept:
		response.content_type=MIME_HTML
		return html_repr(book)
	elif MIME_JSON in accept:
		response.content_type=MIME_JSON
		return json_repr(book) 
	else:
		# default
		response.content_type=MIME_TEXT
		return txt_repr(book)

This is the simplest way to do that and not the best. In fact, the Accept metadata can have several parts and parameters. Using a library like mimeparse is a good idea.

For instance, a simple request with a browser set the metadata like this :

Accept : text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8

See §14.1 of HTTP for more details.

Handle representation with mimerender

Mimerender is a python package that brings a decorator to deal with the Accept metadata.

The documentation is well done, read http://mimerender.readthedocs.org/

We can clean the previous method by adding a the mimerender decorator

# Define the route (URL) of my resource
@route('/book/:isbn')

# Define wich representation will be used according to the MIME type asked
@mimerender(
    default = 'txt',
    html = lambda **args: html_repr(args),
    json = lambda **args: json_repr(args),
    txt  = lambda **args: txt_repr(args)
)
# Method that gets the ressource 
def book_ressource(isbn):
	book={'isbn':isbn}
	book.update(mydata[isbn])
	return book
	

Just return the data (a dictionary here), and the decorator does the job. The Accept metadata is analysed and the good representation is called.

Results

Html representation of http://localhost:8080/book/0836221362


fred:~ opikanoba$ curl -H "Accept: text/html" http://localhost:8080/book/0836221362
<html>
		<head><title>Book ISBN 0836221362</title></head>
		<body><h1>The Days Are Just Packed</h1>
		 <h2>Bill Watterson</h2><img src="http://isbn.abebooks.com/mz/57/83/0836217357.jpg"/>
		</body>

Plain text representation of http://localhost:8080/book/0836221362


fred:~ opikanoba$ curl -H "Accept: text/plain" http://localhost:8080/book/0836221362
[0836221362] The Days Are Just Packed - Bill Watterson

JSON representation of http://localhost:8080/book/0836221362


fred:~ opikanoba$ curl -H "Accept: application/json" http://localhost:8080/book/0836221362
{"title": "The Days Are Just Packed", "isbn": "0836221362", 
"img": "http://isbn.abebooks.com/mz/57/83/0836217357.jpg", "author": "Bill Watterson"}

Get the code

See the complete code on github : http://github.com/flrt/repr-bottle

Fork the code :

git clone git://github.com/flrt/repr-bottle