{
"info": {
"author": "Stephan Richter and the Zope Community",
"author_email": "zope3-dev@zope.org",
"bugtrack_url": null,
"classifiers": [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Framework :: Zope3",
"Intended Audience :: Developers",
"License :: OSI Approved :: Zope Public License",
"Natural Language :: English",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Topic :: Internet :: WWW/HTTP"
],
"description": "This package provides a framework to build REST APIs on top of Zope 3.\n\n\nDetailed Documentation\n**********************\n\n===================================================\nA Framework for Building RESTive Services in Zope 3\n===================================================\n\nThis package implements several components that relate to building RESTive Web\nservices using the Zope publisher. Each set of components is documented in a\ncorresponding text file.\n\n* ``client.txt`` [must read]\n\n This package also provides a REST Web client, which can be used for testing\n or for accessing a RESTive API within an application.\n\n* ``null.txt`` [advanced user]\n\n In order to create new resources, the publisher must be able to traverse to\n resources/objects that do not yet exist. This file explains how those null\n resources work.\n\n* ``traverser.txt`` [advanced user]\n\n The ``traverser`` module contains several traversal helper components for\n common traversal scenarios, suhc as containers and null resources.\n\n* ``rest.txt`` [informative]\n\n This document introduces the hooks required to manage RESTive requests in\n the publisher. It also discusses hwo those components are used by the\n publisher.\n\n===========\nREST Client\n===========\n\nThe REST client provides a simple Python API to interact easily with RESTive\nWeb services. It was designed to have a similar API to Zope's test\nbrowser.\n\nLet's start by instantiating the the client. Of course we have a version of\nthe client that talks directly to the Zope publisher:\n\n >>> from z3c.rest import testing\n >>> client = testing.RESTClient()\n\nFor testing purposes, we have defined a simple REST API for folders. The\nsimplest call is to retrieve the contents of the root folder:\n\n >>> client.open('http://localhost/')\n >>> print client.contents\n \n \n \n \n \n \n \n \n\nYou can also instantiate the client providing a URL:\n\n >>> client = testing.RESTClient('http://localhost/')\n >>> print client.contents\n \n \n \n \n \n \n \n \n\nHTTPS URLs are also supported\n\n >>> client = testing.RESTClient('https://localhost/')\n Using SSL\n >>> print client.contents\n \n \n \n \n \n \n \n \n\n\nGetting Resources\n-----------------\n\nThe ``open()`` method implicitely uses the \"GET\" HTTP method. An alternative\nwould be to use this:\n\n >>> client.get('http://localhost/')\n >>> print client.contents\n \n \n \n \n \n \n \n \n\nThere are several other pieces of information of the response that are\navailable:\n\n >>> client.url\n 'http://localhost/'\n >>> client.status\n 200\n >>> client.reason\n 'Ok'\n >>> client.fullStatus\n '200 Ok'\n >>> client.headers\n [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'),\n ('Content-Length', '204'),\n ('Content-Type', 'text/xml;charset=utf-8')]\n\nIf we try to access a non-existent resource, no exception is raised, but the\nstatus is '404' (not found) of course:\n\n >>> client.get('http://localhost/unknown')\n >>> client.fullStatus\n '404 Not Found'\n >>> client.contents\n ''\n >>> client.headers\n [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'),\n ('Content-Length', '0')]\n\nAs in the original test browser, I can turn off the Zope error handling and\nthe Python exception will propagate through the publisher:\n\n >>> client.handleErrors = False\n >>> client.get('http://localhost/unknown')\n Traceback (most recent call last):\n ...\n NotFound: Object: , name: u'unknown'\n\n >>> client.handleErrors = True\n\nAs RESTive APIs often use query string key-value pairs to parameterize the\nrequest, this REST client has strong support for it. For example, you can\nsimply specify the parameters in the URL:\n\n >>> client.get('http://localhost/?noitems=1')\n >>> print client.contents\n \n \n \n \n \n \n\nYou can also specify the parameter via an argument:\n\n >>> client.get('http://localhost/', params={'noitems': 1})\n >>> print client.contents\n \n \n \n \n \n \n\nYou can even combine the two methods of specifying parameters:\n\n >>> client.get('http://localhost/?noitems=1', params={'notitle': 1})\n >>> print client.contents\n \n \n \n \n \n\nBut our little demo API can do more. Parameters can also be specified as a\nheader with a special prefix. Headers can be globally specified and are then\nused for every request:\n\n >>> client.requestHeaders['demo-noitems'] = 'on'\n >>> client.get('http://localhost/')\n >>> print client.contents\n \n \n \n \n \n \n\nThere is also a headers argument to the \"open\" methods that specify the header\nonce:\n\n >>> client.get('http://localhost/', headers={'demo-notitle': 1})\n >>> print client.contents\n \n \n \n \n \n\n >>> del client.requestHeaders['demo-noitems']\n\nFinally, when dealing with a real site, a socket error might occur. The error\nis propagated, but the error number and message are recorded:\n\n >>> from z3c.rest.client import RESTClient\n >>> realClient = RESTClient()\n >>> realClient.open('http://localhost:65000')\n Traceback (most recent call last):\n ...\n error: (61, 'Connection refused')\n\n >>> realClient.fullStatus\n '61 Connection refused'\n\n\nCreating new resources\n----------------------\n\nLet's now create a new resource in the server root. Our little sample\napplication will simply create another collection:\n\n >>> client.put(\n ... 'http://localhost/folder1',\n ... '''\n ... ''')\n\n >>> client.fullStatus\n '401 Unauthorized'\n\nAccessing the folder resource is available to everyone. But if you want to\nmodify any resource, you have to log in:\n\n >>> client.setCredentials('globalmgr', 'globalmgrpw')\n\nSo let's try this again:\n\n >>> client.put(\n ... 'http://localhost/folder1',\n ... '''\n ... ''')\n\n >>> client.fullStatus\n '201 Created'\n >>> client.headers\n [('X-Powered-By', 'Zope (www.zope.org), Python (www.python.org)'),\n ('Content-Length', '0'),\n ('Location', 'http://localhost/folder1')]\n\nWe can now look at the root container and see the item there:\n\n >>> client.get('http://localhost/')\n >>> print client.contents\n \n \n \n \n \n \n \n \n \n\nBy the way, you can now use a relative URL to access the `folder1` resource:\n\n >>> client.get('folder1')\n\n >>> client.url\n 'http://localhost/folder1'\n\n >>> print client.contents\n \n \n folder1\n \n \n \n \n \n\nWhen we try to create a resource on top of a non-existent resource, we get a\n404 error:\n\n >>> client.put(\n ... 'http://localhost/folder2/folder21',\n ... '''\n ... ''')\n\n >>> client.fullStatus\n '404 Not Found'\n\n\nModifying Resources\n-------------------\n\nModifying a given resource can be done via POST or PUT, but they have different\nsemantics. Let's have a look at POST first. We would now like to change the\ntitle of the folder; this can be done as follows:\n\n >>> client.post(\n ... 'http://localhost/folder1',\n ... '''\n ... \n ... My Folder 1\n ... ''')\n\n >>> client.fullStatus\n '200 Ok'\n\n >>> client.get()\n >>> print client.contents\n \n \n folder1\n My Folder 1\n \n \n \n \n\nAs mentioned above, it must also work for PUT:\n\n >>> client.put(\n ... 'http://localhost/folder1',\n ... '''\n ... \n ... Folder 1\n ... ''')\n\n >>> client.fullStatus\n '200 Ok'\n\n >>> client.get()\n >>> print client.contents\n \n \n folder1\n Folder 1\n \n \n \n \n\n\nDeleting Resources\n------------------\n\nDeleting a resource is as simple as all of the other methods. Let's delete our\n`folder1`:\n\n >>> client.delete('http://localhost/folder1')\n\n >>> client.fullStatus\n '200 Ok'\n\nSo the resource is really gone:\n\n >>> client.get()\n >>> client.fullStatus\n '404 Not Found'\n\nIt should not be possible to delete a non-existing resource:\n\n >>> client.delete('http://localhost/folder2')\n >>> client.fullStatus\n '404 Not Found'\n\nAlso, we cannot delete the root folder:\n\n >>> client.delete('http://localhost/')\n >>> client.fullStatus\n '405 Method Not Allowed'\n\n\nSearching the Response Data\n---------------------------\n\nWhile not required, most REST services are XML-based. Thus, the client\nsupports inspecting the result XML using XPath. Let's create a couple of\nfolders for this to be more interesting:\n\n >>> client.put(\n ... 'http://localhost/folder1',\n ... '''\n ... ''')\n\n >>> client.put(\n ... 'http://localhost/folder2',\n ... '''\n ... ''')\n\nNext we get the root folder resource:\n\n >>> client.get('http://localhost/')\n >>> print client.contents\n \n \n \n \n \n \n \n \n \n \n\nBut in general, inspecting the XML output on the string level is tedious. So\nlet's write a cool XPath expression that extracts the xlink title of all\nitems:\n\n >>> nsmap = {'xlink': \"http://www.w3.org/1999/xlink\"}\n >>> client.xpath('//folder/items/item/@xlink:title', nsmap)\n ['folder1', 'folder2']\n\nOftentimes, however, we specifically query for one result. In those cases we\ndo not want to receive a list:\n\n >>> client.xpath('//folder/items/item[@xlink:title=\"folder1\"]', nsmap, True)\n \n\nNow, if multiple matches are detected, even though we only expect one, then a\n``ValueError`` is raised:\n\n >>> client.xpath('//folder/items/item', nsmap, True)\n Traceback (most recent call last):\n ...\n ValueError: XPath expression returned more than one result.\n\n\nAccessing Links\n---------------\n\nSince we want the REST client to behave like a browser -- at least a little\nbit -- we can also use the ``getLink()`` method to access links:\n\n >>> client.getLink('folder1')\n \n\nBy default, the link is looked up by title. But you can also look it up by\nURL:\n\n >>> client.getLink(url='http://localhost/folder1')\n \n\nIf you forget to specify a title or URL, you receive a ``ValueError``:\n\n >>> client.getLink()\n Traceback (most recent call last):\n ...\n ValueError: You must specify a title or URL.\n\nLinks can also be relative, such as the one for ACL:\n\n >>> client.open('http://localhost/folder1')\n >>> client.getLink('ACL')\n \n\n >>> client.open('http://localhost/folder1/')\n >>> client.getLink('ACL')\n \n\nThe cool part about the link is that you can click it:\n\n >>> client.open('http://localhost/')\n >>> client.url\n 'http://localhost/'\n\n >>> client.getLink('folder1').click()\n\n >>> client.url\n 'http://localhost/folder1'\n\n\nMoving through time\n-------------------\n\nLike in a real browser, you can go back to a previous state. For example,\ncurrently we are looking at `folder1`, ...\n\n >>> client.url\n 'http://localhost/folder1'\n\nbut if I go back one step, I am back at the root folder:\n\n >>> client.goBack()\n\n >>> client.url\n 'http://localhost/'\n\n >>> print client.contents\n \n \n \n \n \n \n \n \n \n \n\nBut going back in history is only cool, if you can also reload. So let's\ndelete `folder2`:\n\n >>> client.getLink('folder2').click()\n >>> client.delete()\n\nNow we go back 2 steps:\n\n >>> client.goBack(2)\n\n >>> client.url\n 'http://localhost/'\n\n >>> print client.contents\n \n \n \n \n \n \n \n \n \n \n\nAs expected, the contents has not changed yet. So let's reload:\n\n >>> client.reload()\n\n >>> client.url\n 'http://localhost/'\n\n >>> print client.contents\n \n \n \n \n \n \n \n \n \n\nNote that going back zero steps does nothing:\n\n >>> client.url\n 'http://localhost/'\n\n >>> client.getLink('folder1').click()\n >>> client.goBack(0)\n\n >>> client.url\n 'http://localhost/folder1'\n\nAlso, if you try to go back beyond the beginning of time, a value error is\nraised:\n\n >>> client.goBack(1000)\n Traceback (most recent call last):\n ...\n ValueError: There is not enough history.\n\n\nAbsolute URLs\n-------------\n\nAs mentioned above, we allow specifying relative URLs, a call with an absolute\nURL has been made. A function called ``absoluteURL()`` is used to compute the\nnew absolute URL.\n\n >>> from z3c.rest.client import absoluteURL\n\nThe basic functionality is simple:\n\n >>> absoluteURL('http://localhost/folder1/', 'folder11')\n 'http://localhost/folder1/folder11'\n\nIt also detects, if the new part of the URL is absolute, in which case it\nreplaces the original base URL:\n\n >>> absoluteURL('http://einstein/folder1', 'http://localhost/folder11')\n 'http://localhost/folder11'\n\nIf the base URL does not have a trailing slash, it is added automatically:\n\n >>> absoluteURL('http://localhost/folder1', 'folder11')\n 'http://localhost/folder1/folder11'\n\nAny slashes at the end of the new URL are preserved:\n\n >>> absoluteURL('http://localhost/folder1', 'folder11/')\n 'http://localhost/folder1/folder11/'\n\n >>> absoluteURL('http://einstein/folder1', 'http://localhost/folder11/')\n 'http://localhost/folder11/'\n\nThe function also handles more complex URLs containing a query string\ncorrectly:\n\n >>> absoluteURL('http://localhost/folder1', 'folder11?max=1')\n 'http://localhost/folder1/folder11?max=1'\n\n >>> absoluteURL('http://localhost/folder1', 'folder11/?max=1')\n 'http://localhost/folder1/folder11/?max=1'\n\nIf the base URL contains a query string, the resulting URL will as well:\n\n >>> absoluteURL('http://localhost/folder1?max=1', 'folder11/')\n 'http://localhost/folder1/folder11/?max=1'\n\n >>> absoluteURL('http://localhost/folder1/?max=1', 'folder11/')\n 'http://localhost/folder1/folder11/?max=1'\n\n >>> absoluteURL('http://localhost/folder1?max=1', 'folder11')\n 'http://localhost/folder1/folder11?max=1'\n\n >>> absoluteURL('http://localhost/folder1/?max=1', 'folder11')\n 'http://localhost/folder1/folder11?max=1'\n\nIf both, the base and relative URL provide query strings, they are merged:\n\n >>> absoluteURL('http://localhost/folder1/?max=1', 'folder11?min=0')\n 'http://localhost/folder1/folder11?max=1&min=0'\n\n >>> absoluteURL('http://localhost/folder1/?max=1', 'folder11?min=0&start=0')\n 'http://localhost/folder1/folder11?max=1&min=0&start=0'\n\n >>> absoluteURL('http://localhost/folder1/?max=1&start=0', 'folder11?min=0')\n 'http://localhost/folder1/folder11?max=1&start=0&min=0'\n\n\n==============\nNull Resources\n==============\n\nIt is sometimes necessary to traverse to resources that do not yet exist. In\nparticular, this is needed when creating resources using \"PUT\" or \"POST\". It\nis the responsibility of the traverser to handle those cases correctly and\nproduce the null resources. This document only describes their behavior.\n\nA null resource is easily instantiated using the container and the name of the\nresource:\n\n >>> class Folder(object):\n ... __parent__ = __name__ = None\n ... child = None\n ...\n ... def __init__(self, name=''):\n ... self.name = self.__name__ = name\n ...\n ... def __repr__(self):\n ... return '' %self.name\n >>> folder = Folder()\n\n >>> from z3c.rest import null\n >>> resource = null.NullResource(folder, 'resource')\n\n >>> from zope.app.http.interfaces import INullResource\n >>> INullResource.providedBy(resource)\n True\n\nNull resources are locations, so security is available:\n\n >>> from zope.location.interfaces import ILocation\n >>> ILocation.providedBy(resource)\n True\n\nThe container is also the parent:\n\n >>> resource.container\n \n >>> resource.__parent__\n \n\nThe name of the resource is available at:\n\n >>> resource.name\n 'resource'\n >>> resource.__name__\n 'resource'\n\nThere is a special implementation of \"PUT\" for null resources. It works by\nlooking up a view called \"NullPUT\" for the container. This way, one null\nresource implementation can be used for all container implementations.\n\n >>> import StringIO\n >>> from z3c.rest import rest\n >>> request = rest.RESTRequest(StringIO.StringIO(), {})\n\n >>> nullPut = null.NullPUT(resource, request)\n >>> nullPut.PUT()\n\nSince no view called \"NullPUT\" exists for our `Folder` class, we get a 501\nreturn status:\n\n >>> request.response.getStatusString()\n '501 Not Implemented'\n\nLet's now register a simple NullPUT view:\n\n >>> class FolderAPI(rest.RESTView):\n ...\n ... def NullPUT(self, resource):\n ... self.context.child = Folder(resource.name)\n ... self.context.child.__parent__ = self.context\n ... return self.context.child\n\n >>> import zope.component\n >>> from z3c.rest import interfaces\n >>> zope.component.provideAdapter(\n ... FolderAPI, (Folder, interfaces.IRESTRequest), name='NullPUT')\n\nLet's make sure our location structure is correctly setup, so that absolute\nURL will work:\n\n >>> from zope.traversing.interfaces import IContainmentRoot\n >>> import zope.interface\n >>> zope.interface.alsoProvides(folder, IContainmentRoot)\n\nNow we are ready to PUT the new resource:\n\n >>> request = rest.RESTRequest(\n ... StringIO.StringIO(), {'SERVER_URL': 'http://localhost/'})\n\n >>> nullPut = null.NullPUT(resource, request)\n >>> nullPut.PUT()\n\n >>> request.response.getStatusString()\n '201 Created'\n >>> request.response.getHeader('Location')\n 'http://localhost/resource'\n\n >>> folder.child\n \n\n=========================\nREST Traverser Components\n=========================\n\nBeing able to control and extend traversal is essential to any RESTive\nAPI. This package uses the pluggable traverser implementation of the\n``z3c.traverser`` package to provide a flexible traversal mechanism.\n\n\nREST Pluggable Traverser\n------------------------\n\nThe REST pluggable traverser is registered for all types of components. Its\nimplementation is fully tested in the ``z3c.traverser`` package.\n\n >>> from z3c.rest import traverser\n\n >>> import StringIO\n >>> from z3c.rest import rest\n >>> request = rest.RESTRequest(StringIO.StringIO(), {})\n\n >>> pluggable = traverser.RESTPluggableTraverser(object(), request)\n >>> pluggable\n \n\n\nItem Container Traverser Plugin\n-------------------------------\n\nThe item mapping interface -- from which item container inherits -- is the\nmost minimal mapping interface in Python. Thus, once traversing through this\nitem container is implemented, it can be used by all other container\ninterfaces and implementations.\n\nLet's start by creating a very simple item container implementation:\n\n >>> import zope.interface\n >>> from zope.app.container.interfaces import IItemContainer\n\n >>> class SimpleContainer(dict):\n ... zope.interface.implements(IItemContainer)\n ... def __init__(self, name=''):\n ... self.name = name\n ... def __repr__(self):\n ... return '' %self.name\n\n >>> container = SimpleContainer()\n\n >>> container['sub1'] = SimpleContainer('sub1')\n >>> container['sub2'] = SimpleContainer('sub2')\n\nAfter creating a traverser plugin instance,\n\n >>> request = rest.RESTRequest(StringIO.StringIO(), {})\n\n >>> containerTraverser = traverser.ContainerItemTraverserPlugin(\n ... container, request)\n\nwe can traverse to a sub-object of that container:\n\n >>> containerTraverser.publishTraverse(request, 'sub1')\n \n\nIf no proper sub-item can be found, some interesting can happen. In a normal\ncase, ``NotFound`` is raised:\n\n >>> containerTraverser.publishTraverse(request, 'unknown')\n Traceback (most recent call last):\n ...\n NotFound: Object: , name: 'unknown'\n\nHowever, if the request is a PUT request, we must generate a null resource:\n\n >>> request.method = 'PUT'\n >>> containerTraverser.publishTraverse(request, 'unknown')\n \n\nHowever, a null resource is only created, if the current resource is the last\none in the traversal stack:\n\n >>> request.setTraversalStack(('sub11',))\n >>> containerTraverser.publishTraverse(request, 'unknown')\n Traceback (most recent call last):\n ...\n NotFound: Object: , name: 'unknown'\n\nAnd that's it.\n\n=================================\nPublisher Hooks for REST Requests\n=================================\n\nReading this document requires -- to some extend -- that the reader is\nfamiliar with the basic steps of the publication process.\n\n\nThe Publication Request Factory\n-------------------------------\n\nThe Zope publication process starts when a WSGI server sends the request\nenvironment and response initialization callable to the Zope WSGI Publisher\napplication _[1]. The WSGI publisher application is then responsible for\nprocessing the request in the publisher and stream out the result.\n\nIn order to process a request in the publisher, we have to create a valid\npublisher request object. The WSGI publisher application uses a request\nfactory for this purpose. This package implements this factory to ensure that\na special REST request (based on HTTP Request) is created at all times.\n\nThe request factory is instantiated using a ZODB database object:\n\n >>> from ZODB.DB import DB\n >>> from ZODB.DemoStorage import DemoStorage\n >>> db = DB(DemoStorage())\n\nLet's now create the factory:\n\n >>> from z3c.rest import rest\n >>> RequestFactory = rest.RESTPublicationRequestFactory(db)\n\nWhen a request comes in from the server, the request is created as follows:\n\n >>> import StringIO\n >>> inStream = StringIO.StringIO('Input stream')\n\n >>> env = {'HTTP_ACCEPT_LANGUAGE': 'en-US,en',\n ... 'SERVER_URL': 'http://localhost:8080/'}\n\n >>> request = RequestFactory(inStream, env)\n\nWe now got a valid request that we can send through the publisher:\n\n >>> request\n \n\nThe request, however, is only responsible for representing the network request\nin the publisher and has no direct knowledge of the application. But the\nrequest connects to an application-specific -- in this case Zope 3 --\ncomponent known as the publication.\n\n >>> request.publication\n \n\nSince we do not need a special REST publication, we are simply reusing the\nmore generic HTTP version. The publication will be the same for all\nrequests. It also contains the reference to the database:\n\n >>> request.publication.db\n \n\nUnfortunately, it takes a lot more setup to send the request through the\npublisher successfully. The publication requires many other aspects of\npublishing to be available, including traversal, security, and a properly\nconstructed database. However, we can still see a failure:\n\n >>> from zope.publisher import publish\n >>> publish.publish(request)\n \n >>> print request.response.consumeBody()\n \n \n ComponentLookupError\n (<InterfaceClass ...IAuthentication>, u'')\n \n\nLet' unwind a bit. Originally, we started with the desire to create a\nPublisher WSGI Application instance that internally uses a REST request. All\nthat you need to do is:\n\n >>> from zope.app import wsgi\n >>> app = wsgi.WSGIPublisherApplication(\n ... db, rest.RESTPublicationRequestFactory)\n >>> app\n \n\nWhen the WSGI server sends a request to the WSGI application, the following\nhappens:\n\n >>> status = None\n >>> headers = None\n >>> def start_response(s, h):\n ... global status\n ... global headers\n ... status, headers = s, h\n\n >>> wsgiEnv = {'wsgi.input': inStream}\n >>> wsgiEnv.update(env)\n\n >>> print '\\n'.join(app(wsgiEnv, start_response))\n \n \n ComponentLookupError\n (<InterfaceClass ...IAuthentication>, u'')\n \n\n\n.. [1]: ``zope.app.wsgi.WSGIPublisherApplication.__call__``\n\n\nThe REST Request\n----------------\n\nFor most parts, the REST request is identical to the HTTP request, so I won't\ngo into too much detail about the HTTP request API.\n\nThe REST request mainly extends the HTTP request in that it parses the query\nstring of the URL into a set of parameters. This happens during\n``processInputs()``.\n\nIf there is no query string, the paramaters mapping is empty:\n\n >>> request = RequestFactory(\n ... StringIO.StringIO(), {})\n >>> request.processInputs()\n >>> request.parameters\n {}\n\nSo let's now pass a few parameters:\n\n >>> request = RequestFactory(\n ... StringIO.StringIO(),\n ... {'QUERY_STRING': 'format=html&action=delete&item=1&item=3'})\n >>> request.processInputs()\n >>> pprint(request.parameters)\n {'action': 'delete',\n 'format': 'html',\n 'item': ['1',\n '3']}\n\nWe also override some of the request's mapping methods, so that the parameters\nand environment values are available as part of the request:\n\n >>> sorted(request.keys())\n ['QUERY_STRING', 'action', 'format', 'item']\n\n >>> request.get('QUERY_STRING')\n 'format=html&action=delete&item=1&item=3'\n >>> request.get('action')\n 'delete'\n >>> request.get('unknwon', 'default')\n 'default'\n\n\nThe REST Response\n-----------------\n\nThe REST Response is pretty much identical to the HTTP Response, except that\nits exception handler returns XML instead of HTML. This method, however, is\nonly used for classic and string exceptions.\n\nStarting with a response, ...\n\n >>> response = rest.RESTResponse()\n\n... we can now call the handler:\n\n >>> class MyException(Exception):\n ... pass\n\n >>> response.handleException((MyException, MyException(), None))\n >>> response._status\n 500\n >>> response._reason\n 'Internal Server Error'\n >>> print '\\n'.join(response._result)\n \n \n MyException\n \n \n\nLet's try a string exception too:\n\n >>> response.handleException(('StringException', 'Some details', None))\n >>> response._status\n 500\n >>> response._reason\n 'Internal Server Error'\n >>> print '\\n'.join(response._result)\n \n \n StringException\n Some details\n \n\nThe `Redirect` exception is special. It actuaually causes the request to be\nredirected.\n\n >>> response._request = rest.RESTRequest(None, {})\n\n >>> from zope.publisher.interfaces import Redirect\n >>> response.handleException(\n ... (Redirect, Redirect('http://localhost'), None), trusted=True)\n >>> response._status\n 302\n >>> response._reason\n 'Moved Temporarily'\n >>> response._headers['location']\n ['http://localhost']\n\n\nREST Views\n----------\n\nUnlike browser views, a REST view does *not* represent its own sub-resource\n(such as \"index.html\"). Instead it merely defines the behavior of the HTTP\nmethods for a particular content component.\n\nHere is an example:\n\n >>> class ObjectAPI(rest.RESTView):\n ...\n ... def GET(self):\n ... return str(self.context)\n\nThe ``RESTView`` base class provides a suitable constructor:\n\n >>> class Object(object):\n ... def __repr__(self):\n ... return '