This is part of an ongoing project, a simple recipe manager. Previous steps left me with a set of XML files I can visualize in a browser, one per recipe. I now need a quick and dirty way to manage those files directly from the (yet to be built) front end.
1234
Feature: Creating a quick RESTful API for recipes In order to develop a front end quickly As a developer I need to a RESTful API that performas basic CRUD operations
I decided to use Python for the backend because I am trying to learn it, and it also fits well with some tools that I intend to use further down the line. I haven’t decided on a framework yet, that will be part of the challenge. I will try Bottle to start with.
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 this script
and started the server with python test1.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 can 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 recipes. Nothing fancier than that.
The first step is to create a script that proves all the calls I want to make work.
12345678910111213141516171819
Scenario: Installing and setting up Bottle Given that I am running test2.pyWhen I access the <url> with <verb>Then the response should have status 200 and be <response>Examples: Basic Strings | url | verb | response | | http://localhost:8080/recipes | GET | LIST | | http://localhost:8080/recipes/name | PUT | SAVE RECIPE name | | http://localhost:8080/recipes/name | GET | SHOW RECIPE name | | http://localhost:8080/recipes/name | DELETE | DELETE RECIPE name |Examples: JSON Responses | url | verb | response | | http://localhost:8080/recipes | GET | { "success" : False, "paths" : [], "error" : "list not implemented yet" } | | http://localhost:8080/recipes/name | PUT | { "success" : False, "path" : "/PTH/TO/XML/name.xml", "error" : "save not implemented yet" } | | http://localhost:8080/recipes/name | GET | { "success" : False, "path" : "/PTH/TO/XML/name.xml", "error" : "show not implemented yet" } | | http://localhost:8080/recipes/name | DELETE | { "success" : False, "paths" : [], "error" : "delete not implemented yet" } |
I will test this manually with curl with this script
1234
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.
I run python test2.py, 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
So simple I can skip directly to the next iteration, return JSON responses in the right “shape”, but with dummy data:
bottle/test2.py
12345678910111213141516171819
frombottleimportroute,run@route('/recipes/')defrecipes_list():return{"success":False,"paths":[],"error":"list not implemented yet"}@route('/recipes/<name>',method='GET')defrecipe_show(name="Mystery Recipe"):return{"success":False,"path":"/PTH/TO/XML/"+name+".xml","error":"show not implemented yet"}@route('/recipes/<name>',method='DELETE')defrecipe_delete(name="Mystery Recipe"):return{"success":False,"error":"delete not implemented yet"}@route('/recipes/<name>',method='PUT')defrecipe_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.
12345678910
Scenario: Loading config data with ConfigParser Given that config.json contains""" "paths" : { "xml" : "/Users/ME/work/xml/" }"""And I am running test2.pyWhen I access http://localhost:8080/recipes/nameThen the app should return a JSON response { "success" : false, "path" : "/Users/ME/work/xml/name.xml" , "error" : "show not implemented yet }
Nothing really too exciting here.
bottle/test2.py
12345678910111213141516171819202122232425
importjsonfrombottleimportroute,runconfig_file=open('config.json')config_data=json.load(config_file)pth_xml=config_data["paths"]["xml"]@route('/recipes/')defrecipes_list():return{"success":False,"paths":[],"error":"list not implemented yet"}@route('/recipes/<name>',method='GET')defrecipe_show(name="Mystery Recipe"):return{"success":False,"path":pth_xml+name+" .xml","error":"show not implemented yet"}@route('/recipes/<name>',method='DELETE')defrecipe_delete(name="Mystery Recipe"):return{"success":False,"error":"delete not implemented yet"}@route('/recipes/<name>',method='PUT')defrecipe_save(name="Mystery Recipe"):return{"success":False,"path":pth_xml+name+" .xml","error":"save not implemented yet"}run(host='localhost',port=8080,debug=True)
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.
123456789
Scenario: Returning a JSON formatted directory listing with Bottle Given that config.json points to <PTH>And <PTH> contains <filelist>When I request http://localhost:8080/recipes<url> with <verb>Then the app should return <response>Examples: simple listing | PTH | filelist | url | verb | response | | test/data/1 | a.xml, b.xml, c.xml, d.txt | / | GET | { "success": true, "paths": [ "a.xml", "b.xml", "c.xml" ] } |
Now, let’s look at one of these files in the list.
123456789
Scenario: Echoing XML files with Bottle Given that config.json points to <PTH>And <PTH> contains <filelist>When I request http://localhost:8080/recipes<url> with <verb>Then the app should return <response>Examples: simple listing | PTH | filelist | url | verb | response | | test/data/1 | a.xml, b.xml, c.xml, d.txt | /a | GET |<content of a.xml> |
So now I am actually serving a file. It’s not JSON, it’s XML - that’s still OK, it’s all human readable data in the end.
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.
test3.py
1234567891011121314
importjsonimportosfrombottleimportroute,run,static_fileconfig_file=open('config.json')config_data=json.load(config_file)pth_xml=config_data["paths"]["xml"]@route('/recipes/<name>',method='GET')defrecipe_show(name=""):if""!=name:returnstatic_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 checks for the correct XML format either.
12345678910111213
Scenario: Creating and updating XML files with Bottle Given that config.json points to <PTH>And <PTH> contains <filelist>When I request http://localhost:8080/recipes<url> with <verb>And send <postdata>Then the app should return <response>And the <PTH> should contain <newfilelist>And <PTH><url> should contain <postdata>Examples: simple listing | PTH | filelist | url | verb | postdata | response | newfilelist | | test/data/2 | a.xml, b.xml | /f | PUT | new file | { "success": true, "path": [ "test/data/2/a.xml" ] } | a.xml, b.xml, f.xml | | test/data/2 | a.xml, b.xml | /a | PUT | new file | { "success": true, "path": [ "test/data/2/a.xml" ] } | a.xml, b.xml |
This is also fairly simple, as long as one knows where to get the PUT data from - request.forms, which requires the request module.
bottle/test3.py
1234567891011121314151617
importjsonimportosfrombottleimportroute,run,requestconfig_file=open('config.json')config_data=json.load(config_file)pth_xml=config_data["paths"]["xml"]@route('/recipes/<name>',method='PUT')defrecipe_save(name=""):xml=request.forms.get("xml")if""!=nameand""!=xml:withopen(os.path.join(pth_xml,name+".xml"),"w")asf: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:
12
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.
12345678910
Scenario: eleting files with Bottle Given that config.json points to <PTH>And <PTH> contains <filelist>When I request http://localhost:8080/recipes<url> with <verb>Then the app should return <response>And the <PTH> should contain <newfilelist>Examples: simple listing | PTH | filelist | url | verb | response | newfilelist | | test/data/2 | a.xml, b.xml, f.xml | /f | DELETE | { "success": true } | a.xml, b.xml |
Also fairly simple. A try / except is used to avoid errors if the files doesn’t exist.
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.