Building RESTful Web Services with CherryPy
Joseph Tate
Joseph.Tate@palemountain.com
http://bit.ly/OTRvDd
Background image © 2006 by Cosmic Kitty. Used under Creative Commons license.
Some Definitions: REST
REST - REpresentational State Transfer
Definitions: REST Examples
GET /items/
200 OK
A list of items available.
POST /items/
201 CREATED
Create a new item, and generate an ID for it.
GET /item/7/
200 OK
Retrieve a single item listed above.
PUT /item/7/
204 No Content
Write changes made on the client to the server.
DELETE /item/7/
204 No Content OR 202 Accepted
Delete the item on the server. (204 = deleted, 202 = marked for deletion)
Definitions: REST Cont'd.
What do you notice?
Some Definitions: RWS
RWS: RESTful Web Service
A web accessible API that uses REST principles.
Some Definitions: SOA
SOA: Service Oriented Architecture
An application architecture which breaks a monolithic application into small, discrete, and reusable pieces.
Why CherryPy?
Zen: http://bit.ly/OAN0dC
CherryPy Key Architecture
cherrypy.engine: Controls process startup/teardown and event handling.
cherrypy.server: Configures and controls the WSGI or HTTP server.
cherrypy.tools: A toolbox of utilities that are orthogonal to processing an HTTP request.
CherryPy Hello World
import cherrypy
class HelloWorld(object):
def index(self):
return "Hello World!"
index.exposed = True
cherrypy.quickstart(HelloWorld())
CherryPy Config
Confusing at first, but powerful.
CherryPy Config Example
# Enable JSON processing on input
class Root(object):
@cherrypy.expose
@cherrypy.tools.json_in()
def index(self, urlparm1=None):
data = cherrypy.request.json
# etc.
# Alternative configuration
index.expose = True
index._cp_config = {
'cherrypy.tools.json_in.on': True
}
CherryPy Engine Plugins
cherrypy.engine is actually a publisher/subscriber bus.
Plugins can run custom code at application startup, teardown, exit, or at "engine intervals" by subscribing to the appropriate events.
Example:
Create a scratch DB at server startup that is destroyed at exit.
Engine Plugin Example
class ScratchDB(plugins.SimplePlugin):
def start(self):
self.fname = 'myapp_%d.db' % os.getpid()
self.db = sqlite.connect(database=self.fname)
start.priority = 80
def stop(self):
self.db.close()
os.remove(self.fname)
cherrypy.engine.scratchdb = ScratchDB(cherrypy.engine)
CherryPy Tools
Most Python frameworks use decorators to enable features and dispatching
YUCK!
CherryPy Tools Cont'd
CherryPy uses config to change application configuration by enabling/disabling tools.
Tool Example
def authorize_all():
cherrypy.request.authorized = 'authorize_all'
cherrypy.tools.authorize_all = cherrypy.Tool('before_handler', authorize_all, priority=11)
def is_authorized():
if not cherrypy.request.authorized:
raise cherrypy.HTTPError("403 Forbidden", ','.join(cherrypy.request.unauthorized_reasons))
cherrypy.tools.is_authorized = cherrypy.Tool('before_handler', is_authorized, priority = 49)
cherrypy.config.update({
'tools.is_authorized.on': True,
'tools.authorize_all.on': True
})
Parts of a RESTful Web Service
REST
Usually when thinking about REST you think about CRUD+i (create, retrieve, update, delete, plus index)
In CherryPy REST is handled via a paired class setup
REST Cont'd
Optimizations
CherryPy REST (Class 1)
class ItemIndexREST(object):
exposed = True
@cherrypy.tools.json_out()
def GET(self, dsid=None):
# Return an index of items (DON'T Actually do this)
return []
@cherrypy.tools.json_in()
@cherrypy.tools.authorize_all() # A registration method
def POST(self, login=False):
# Create the item and generate a URL to identify it
cherrypy.response.headers['Location'] = \
self._entity_url(actor)
cherrypy.response.status = 201
CherryPy REST (Class 2)
class ItemREST(object):
exposed = True
@cherrypy.tools.json_out()
@cherrypy.tools.authorize_self()
def GET(self, *vpath):
item = retrieve(vpath[0])
return item.asDict()
@cherrypy.tools.json_in()
@cherrypy.tools.authorize_self()
def PUT(self, *vpath):
# Do work to save the current state
cherrypy.response.headers['Location'] = \
path_to_object(item)
cherrypy.response.status = 204
CherryPy REST (Assembly)
RESTopts = {
'tools.SASessionTool.on': True,
'tools.SASessionTool.engine': model.engine,
'tools.SASessionTool.scoped_session': model.DBSession,
'tools.authenticate.on': True,
'tools.is_authorized.on': True,
'tools.authorize_admin.on': True,
'tools.json_out.handler': json.json_handler,
'tools.json_in.processor': json.json_processor,
'request.dispatch': cherrypy.dispatch.MethodDispatcher()
}
app = cherrypy.tree.mount(actor.ItemREST(), '/item', {'/': RESTopts})
app = cherrypy.tree.mount(actor.ItemIndexREST(), '/items', {'/': RESTopts})
app.merge(cfile)
Identification
Unless you're providing an anonymous service, it's important to know WHO or WHAT is accessing your service.
Build tools to handle each authentication method, e.g., OpenID, tokens, Basic Auth, cookies, etc..
Lots of free tools at http://tools.cherrypy.org/ (Defunct)
Authn Tool Examples
def authenticate():
if not hasattr(cherrypy.request, 'user') or cherrypy.request.user is None:
# < Do stuff to look up your users >
cherrypy.request.authorized = False # This only authenticates. Authz must be handled separately.
cherrypy.request.unauthorized_reasons = []
cherrypy.request.authorization_queries = []
cherrypy.tools.authenticate = \
cherrypy.Tool('before_handler', authenticate, priority=10)
Authorization
Authz Example
def authorize_all():
cherrypy.request.authorized = 'authorize_all'
cherrypy.tools.authorize_all = cherrypy.Tool('before_handler', authorize_all, priority=11)
def is_authorized():
if not cherrypy.request.authorized:
raise cherrypy.HTTPError("403 Forbidden", ','.join(cherrypy.request.unauthorized_reasons))
cherrypy.tools.is_authorized = cherrypy.Tool('before_handler', is_authorized, priority = 49)
cherrypy.config.update({
'tools.is_authorized.on': True,
'tools.authorize_all.on': True
})
Structure
Spend time mapping out your URL tree.
Can you auto discover the API?
Does it make sense?
Are your URLs really universal?
Encapsulation
Typical choices are XML, and increasingly JSON
Encapsulation Cont'd
Generic Envelopes make for intuitive APIs.
Encapsulation Cont'd
Think about CRUD+i
What needs encapsulating
Encapsulation via Shoji
http://www.aminus.org/rbre/shoji/shoji-draft-02.txt
Draft JSON encapsulation format mimicking the ATOM XML protocol.
Shoji defines three types of envelopes.
Shoji Catalogs
Indexing is handled by catalogs.
{
"element": "shoji:catalog",
"self": "http://example.org/users",
"entities": ["1"]
}
Shoji Catalogs Cont'd
Catalogs can have child catalogs, entities, and values
{"element": "shoji:catalog",
"self": "http://example.org/users",
"title": "Users Catalog",
"description": "The set of user entities for this application.",
"updated": "#2003-12-13T18:30:02Z#",
"catalogs": {"bills": "bills",
"sellers": "sellers",
"sellers by sold count": "sellers{?sold_count}"
},
"entities": ["1", "2", "88374", "9843"],
"views": {"Sold Counts": "sold_counts"},
}
Shoji Entities
Entities are the individual item envelopes
{
"element": "shoji:entity",
"self": "http://example.org/users/1",
"body": {
"last_modified": "2003-12-13 18:30:02Z",
"first_name": "Katsuhiro",
"last_name": "Shoji",
"sold_count": 387
}
}
Shoji Views
Item members are presented as views.
Error Handling
Error Handling Cont'd
What about validation errors?
Database errors?
Other application errors?
HTTP 500. Return a response body!
Error Handling Example
import cherrypy
import json
def error_page_default(status, message, traceback, version):
ret = {
'status': status,
'version': version,
'message': [message],
'traceback': traceback
}
return json.dumps(ret)
class Root:
_cp_config = {'error_page.default': error_page_default}
@cherrypy.expose
def index(self):
raise cherrypy.HTTPError(500, "This is an error")
cherrypy.quickstart(Root())
Other considerations?
Conclusions