Creating a RESTful Python API with Bottle

python, rest
This post is old, and probably obsolete

I used Bottle, a Python framework, to create a quick and dirty RESTful service to allow simple CRUD operations on a bunch of XML files.

About the task

This is part of an larger, abondoned project, a simple recipe manager. Previous steps had left me with a set of XML files I can visualize in a browser by applying XSLT to them. I now need a quick and dirty way to manage those files directly from the (yet to be built) front end.

I decided to use Python for the backend because I am trying to learn it. I decided to give Bottle a shot.

Some artefacts available on GitHub.

Installing Bottle

I couldn't use curl or wget for some reason, so I simply pointed my browser to http://bottlepy.org/bottle.py and hit "Save as..." to download the single Python file to a folder called bottle. To test it run, I created the first iteration of recipes-api.py script

from bottle import route, run

@route('/')
def hello():
    return "Hello World!"

run(host='localhost', port=8080, debug=True)

and started the server with python recipes-api.py. I then checked the server was up and running by pointing a browser to http://localhost:8080/. I got the Hello World string

Now I was ready to start building the API.

Testing Bottle installation

This server is going to perform the most basic CRUD operation without checking input, using HTTP verbs. That would be PUT for Create / Update (the only difference will be whether there is already a file by that name or not); DELETE for Delete; GET for Retrieve, plus another GET at the root to get the list of available files. Nothing fancier than that.

The first step is to create a script that proves all the calls I want to make work. I will test this manually with curl with these commands

curl -X GET http://localhost:8080/recipes
curl -X DELETE http://localhost:8080/recipes/name
curl -X GET http://localhost:8080/recipes/name
curl -X PUT http://localhost:8080/recipes/name

Implementing a server that fulfils those requests is pretty simple.

from bottle import route, run

@route('/recipes/')
def recipes_list():
    return "LIST"

@route('/recipes/<name>', method='GET')
def recipe_show( name="Mystery Recipe" ):
    return "SHOW RECIPE " + name

@route('/recipes/<name>', method='DELETE' )
def recipe_delete( name="Mystery Recipe" ):
    return "DELETE RECIPE " + name

@route('/recipes/<name>', method='PUT')
def recipe_save( name="Mystery Recipe" ):
    return "SAVE RECIPE " + name

run(host='localhost', port=8080, debug=True)

I run python recipes-api.py again, run the curl requests, and for each of them I got the expected response. Blimey - this is way too easy.

Returning JSON data with Bottle

Next returning JSON responses in the right "shape", but with dummy data:

from bottle import route, run

@route('/recipes/')
def recipes_list():
    return { "success" : False, "paths" : [], "error" : "list not implemented yet" }

@route('/recipes/<name>', method='GET')
def recipe_show( name="Mystery Recipe" ):
    return { "success" : False, "path" : "/PTH/TO/XML/"+name+".xml", "error" : "show not implemented yet" }

@route('/recipes/<name>', method='DELETE' )
def recipe_delete( name="Mystery Recipe" ):
    return { "success" : False, "error" : "delete not implemented yet" }

@route('/recipes/<name>', method='PUT')
def recipe_save( name="Mystery Recipe" ):
    return { "success" : False, "path" : "/PTH/TO/XML/"+name+".xml", "error" : "save not implemented yet" }

run(host='localhost', port=8080, debug=True)

Reading config data

Finally, I will set up a config files for the app to read config data from - always good practice. Nothing really too exciting here.

import json
from bottle import route, run

config_file = open('config.json')
config_data = json.load(config_file)
pth_xml = config_data["paths"]["xml"]

@route('/recipes/')
... etc

I run it and got the expected results.

Creating RESTful responses

Now it's simply a matter of filling in the blanks, and writing those methods.

Returning a JSON formatted directory listing with Bottle

First, let's get list of files. This is a readdir job, very simple with python.

import json
import os
from bottle import route, run

config_file = open( 'config.json' )
config_data = json.load( config_file )
pth_xml = config_data["paths"]["xml"]

@route('/recipes/')
def recipes_list():
    paths = []
    ls = os.listdir( pth_xml )
    for entry in ls:
        if ".xml" == os.path.splitext( entry )[1]:
            paths.append( entry )
    return { "success" : True, "paths" : paths }

Err... that's it. A JSON response. In the browser.

Echoing XML files with Bottle

Now, let's look at one of these files in the list. This is ridiculously easy using Bottle's static_file. Note that Bottle takes case of slashes, so I won't need to watch out for someone passing ../../../../../etc/passwd as name and showing sensitive data.

import json
import os
from bottle import route, run, static_file

config_file = open( 'config.json' )
config_data = json.load( config_file )
pth_xml = config_data["paths"]["xml"]

@route('/recipes/<name>', method='GET')
def recipe_show( name="" ):
    if "" != name:
        return static_file( name, pth_xml  )
    else:
        return { "success" : False, "error" : "show called without a filename" }

Embarassingly simple.

Creating and updating XML files with Bottle

The next obvious step is to add files to the set. The same script will be used for create or update - any exisiting files will simply be overwritten, without checking. And there won't be any schema checks either

This is also fairly simple, as long as one knows where to get the PUT data from - request.forms, which requires the request module.

import json
import os
from bottle import route, run, request

config_file = open( 'config.json' )
config_data = json.load( config_file )
pth_xml = config_data["paths"]["xml"]

@route('/recipes/<name>', method='PUT')
def recipe_save( name="" ):
    xml = request.forms.get( "xml" )
    if "" != name and "" != xml:
        with open( os.path.join( pth_xml, name + ".xml" ), "w" ) as f:
            f.write( xml )
        return { "success" : True, "path" : name }
    else:
        return { "success" : False, "error" : "save called without a filename or content" }

In order to test this I needed to send postdata with curl:

curl -X PUT --data-urlencode "xml=new file" http://localhost:8080/recipes/f
curl -X PUT --data-urlencode "xml=new file" http://localhost:8080/recipes/a

Everything worked as expected.

Deleting files with Bottle

The last piece of the puzzle, also fairly simple. A try / except is used to avoid errors if the files doesn't exist.

import json
import os
from bottle import route, run, , request

config_file = open( 'config.json' )
config_data = json.load( config_file )
pth_xml = config_data["paths"]["xml"]

@route('/recipes/<name>', method='DELETE' )
def recipe_delete( name="" ):
    if "" != name:
        try:
            os.remove( os.path.join( pth_xml, name + ".xml" ) )
            return { "success" : True  }
        except:
            return { "success" : False  }

So there

This is quick and dirty work - no testing, and running off a local dev server which wouldn't be suited for production. But I am still amazed but how easy it all was. Bottle really does rock.

The scripts are available on GitHub.