In my last post, I spoke about Richardson Maturity model and its useful to demarcate web services based on their implementation maturity. We also understood, how CRUD Web Services fall under Level2 in Maturity model. Let’s take an example of CRUD Web Service developed with Tornado.
In this example, we develop a ‘blogger’ Web service by using multiple logical resources (/blog, /blog/id, /blogs) and HTTP Verbs (GET, POST, PUT and DELETE) to perform various operations offered by the blogger service. Services offered are detailed below.
Resources | HTTP Verbs | Implementation |
http://localhost/blog/ | POST | Creates a blog |
http://localhost/blog/id | GET | Displays the blog |
http://localhost/blog/id | PUT | Updates the blog |
http://localhost/blog/id | DELETE | Deletes the blog |
http://localhost/blogs/ | GET | Lists all the blogs |
http://localhost/blogs/ | DELETE | Deletes all the blogs |
Code implementation:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tornado.ioloop | |
import tornado.web | |
from datetime import datetime | |
import urlparse | |
from bson.json_util import dumps | |
import pymongo | |
from pymongo import Connection | |
class Home(tornado.web.RequestHandler): | |
def initialize(self, connection): | |
self.conn = connection | |
self.db = self.conn['blogger'] | |
self.collection = self.db['blogs'] | |
## Blog's Welcome entry | |
timestamp = datetime.now() | |
blog = { | |
"_id": 1, | |
"title": "Welcome blog!", | |
"tags": ["hello world"], | |
"category": ["welcome"], | |
"timestamp": timestamp, | |
} | |
self.db.blogs.insert(blog) | |
class Blog(Home): | |
def get(self, blogid): | |
blog = self.db.blogs.find_one({"_id":int(blogid)}) | |
self.set_header('Content-Type', 'application/json') | |
self.write(dumps(blog)) | |
def post(self): | |
_id = self.db.blogs.count() + 1 | |
timestamp = datetime.now() | |
body = urlparse.parse_qs(self.request.body) | |
for key in body: | |
body[key] = body[key][0] | |
blog = { | |
"_id": _id, | |
"title": body['title'], | |
"tags": body['tags'], | |
"category": body['category'], | |
"timestamp": timestamp | |
} | |
self.db.blogs.insert(blog) | |
location = "/blog/"+ str(_id) | |
self.set_header('Content-Type', 'application/json') | |
self.set_header('Location', location) | |
self.set_status(201) | |
self.write(dumps(blog)) | |
def put(self, blogid): | |
## Convert unicode to int | |
_id = int(blogid) | |
timestamp = datetime.now() | |
body = urlparse.parse_qs(self.request.body) | |
for key in body: | |
body[key] = body[key][0] | |
blog = { | |
"title": body['title'], | |
"tags": body['tags'], | |
"category": body['category'], | |
"timestamp": timestamp | |
} | |
self.db.blogs.update({"_id":_id}, {"$set":blog}) | |
self.set_header('Content-Type', 'application/json') | |
self.write(dumps(blog)) | |
def delete(self,blogid): | |
## Convert unicode to int | |
_id = int(blogid) | |
blog = { | |
"title": None, | |
"tags": [], | |
"category": [], | |
"timestamp": None, | |
} | |
self.db.blogs.update({"_id":_id}, {"$set":blog}) | |
self.set_header('Content-Type', 'application/json') | |
self.write(dumps(blog)) | |
class Blogs(Home): | |
def get(self): | |
blogs = str(list(self.db.blogs.find())) | |
self.set_header('Content-Type', 'application/json') | |
self.write(dumps(blogs)) | |
def delete(self): | |
blogs = str(list(self.db.blogs.find())) | |
self.set_header('Content-Type', 'application/json') | |
self.db.blogs.drop() | |
self.write(dumps(blogs)) | |
application = tornado.web.Application([ | |
(r"/", Home), | |
(r"/blog/([0-9]+)", Blog, dict(connection = Connection()) ), | |
(r"/blog/", Blog, dict(connection = Connection()) ), | |
(r"/blogs/", Blogs, dict(connection = Connection()) ), | |
],debug=True) | |
if __name__ == "__main__": | |
application.listen(7777) | |
tornado.ioloop.IOLoop.instance().start() |
In the above code snippet:
- We have a base class Home that initializes (method initialize) the blogger service with a Welcome blog. (as it’s true with Google blog or WordPress).
- Class Blog inherits from class Home for the initialization and exports methods get(), put() and delete() for performing operations like reading, updating and deleting a blog (based on blog id).
- post() method doesn’t take any argument but it creates a new blog and assigns a blogid. It also creates a resource /blog/id and returns a HTTP status code of 201 along with Location in Http headers.
- Class Blogs export operations to be performed on all blogs and enables GET and DELETE Http operations to enable the user to view all his blogs or delete all blogs respectively.
- All the responses from the blogger web service are JSON based (application/json).
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import httplib2 | |
from urllib import urlencode | |
import tornado.escape | |
import tornado.httpclient | |
http = httplib2.Http() | |
print "\nView welcome blog – GET" | |
headers, content = http.request("http://localhost:7777/blog/1", "GET") | |
print "Response:", tornado.escape.json_decode(content) | |
print "Headers:", headers | |
print "\nCreate new blog – POST" | |
tags = ['python', 'web'] | |
category = ['www'] | |
data = {'title':'Tornado Web Server', 'tags': tags, 'category':category} | |
body = urlencode(data) | |
headers, content = http.request("http://127.0.0.1:7777/blog/", "POST", body=body) | |
print "Response:", tornado.escape.json_decode(content) | |
print "Headers:", headers | |
print "\nView all blogs – GET" | |
headers, content = http.request("http://localhost:7777/blogs/", "GET") | |
print "Response:", tornado.escape.json_decode(content) | |
print "Headers:", headers | |
print "\nUpdate blog – PUT" | |
title = "mongo" | |
tags = ['python', 'DB'] | |
category = ['db'] | |
data = {'title': title, 'tags': tags, 'category':category} | |
body = urlencode(data) | |
headers, content = http.request("http://127.0.0.1:7777/blog/2", "PUT", body=body) | |
print "Response:", tornado.escape.json_decode(content) | |
print "Headers:", headers | |
print "\nDeactivate blog – DELETE" | |
headers, content = http.request("http://127.0.0.1:7777/blog/2", "DELETE") | |
print "Response:", tornado.escape.json_decode(content) | |
print "Headers:", headers | |
print "\nView all blogs – GET" | |
headers, content = http.request("http://localhost:7777/blogs/", "GET") | |
print "Response:",tornado.escape.json_decode(content) | |
print "Headers:", headers | |
print "\nClear all blogs – DELETE" | |
http = httplib2.Http() | |
headers, content = http.request("http://127.0.0.1:7777/blogs/", "DELETE") | |
print "Response:", tornado.escape.json_decode(content) | |
print "Headers:", headers |
The client more or less uses all the capabilities of blogger web service and displays the JSON responses and Http headers as sent by the web service.