Tutorial

What is amplee and what it isn't

amplee is an implementation of the Atom Publishing Protocol using the Python programming language. The goal of amplee is to provide a toolkit for developer wishing to profit from the benefit of that new protocol in their application.

amplee tries hard not to feel like a framework. Of course it cannot avoid to have some aspects of a framework but by decoupling the different packages of amplee, I hope I managed to demonstrate that they could be used independently from each other.

Unlike version 0.5.x which included the HTTP interface itself and was requesting that you plug callbacks at different points to build your web service, amplee 0.6.x only provides the store layer and leaves you the responsibility to call that layer from your HTTP handlers. This is much more flexible and avoids the "framework" feeling of previous versions. amplee now looks more like a library.

Requirements

Before starting with amplee you must ensure you have the basic requirements:

  • I will assume you run amplee 0.6.x
  • amara 1.2.x, the powerful XML toolkit by Uche Ogbuji
  • uuid which is a Python 2.5 built-in module

For further requirements, please report you here.

In addition, for this tutorial we will assume you run CherryPy 3 but this would be easily transposable to any other Python web framework. We will also consider you use httplib2 as the client of the HTTP server.

AtomPub store tutorial

The purpose of this tutorial is to demonstrate how to use amplee to create a basic AtomPub store that can handle any kind of media type. We will not generate any HTML frontend in order to focus on the AtomPub store part and that's why we will converse with the HTTP server using htpplib2 from the Python interpreter.

The source code of this example can be found in the amplee repository.

Step 1: Initialize the AtomPub application module

The first thing is to import amplee and some other modules in order to have all the tools at hand.

import os
import time

base_dir = os.getcwd()

import cherrypy

# Module used to load an !AtomPub service document into an amplee structure
from amplee.loader import !AtomPubLoader

# Base class of your member resources
from amplee.atompub.member import MemberResource

# Handler of the public and collection feeds
from amplee.atompub.collection import FeedHandler

# Error declaration
from amplee.error import ResourceOperationException

# Helpers
from amplee.utils.mediatype import get_best_mimetype
from amplee.utils import get_isodate, generate_uuid_uri, \
     compute_etag_from_feed, compute_etag_from_entry, \
     decode_slug, handle_if_match, safe_unquote, \
     extract_media_type, extract_url_trail, \
     safe_url_join, extract_url_path_info, qname

In addition we add a tool to the CherryPy? tools that specifies to CherryPy? not to process the request body itself. We do that because we want to create a generic CherryPy? handler that can deal with any kind of media type.

def _amplee_process_request_body():
    # We do not want CherryPy to handle the request body
    # as we will always simply read the content no matter
    # what. The following two lines achieve this.
    cherrypy.request.body = cherrypy.request.rfile
    cherrypy.request.process_request_body = False

# We set the previous function as a tool that we can enable for the Store 
cherrypy.tools.amplee_request_body = cherrypy.Tool('before_request_body',
                                                   _amplee_process_request_body)

Step 2: Create the indexers

Before initializing the AtomPub stores we are going to create indexers. This step is not compulsory per se but amplee provides simple and flexible indexers that help to browse the AtomPub store.

from amplee.indexer import *
def setup_index():
    index = Indexer()
    container = ShelveContainer(os.path.join(base_dir, 'index.p'))
    index.register(PublishedIndex('pi', container=container, granularity=DateIndex.day))
    index.register(UpdatedIndex('ui', container=container, granularity=DateIndex.minute))
    index.register(EditedIndex('ei', container=container, granularity=DateIndex.minute))
    index.register(AuthorIndex('ai', container=container, index_email=True, index_uri=True))
    index.register(CategoryIndex('ci', container=container))
        
    return index

In this case we use the shelve container named index.p and we register different indexers.

Step 3: AtomPub store application and loading the service document

The next step is to create a CherryPy? application that will take care of loading the AtomPub service document and mounting the different collections as addressable via HTTP.

class Application(object):
    def __init__(self, index):
        apl = AtomPubLoader(base_dir)
        self.servdoc, xmldoc = apl.load(os.path.join(base_dir, 'config.xml'),
                                        os.path.join(base_dir, 'service.xml'))

        self.complete_service_loading(xmldoc, index)
        self.create_serving_applications()

        cherrypy.log('AtomPub service application ready')
        
    def complete_service_loading(self, xmldoc, index):
        # When a service is loaded from a service document
        # some information must be set manually after the loading process
        # as they couldn't be guessed by amplee
        
        for collection in self.servdoc.get_collections():
            # Because the service loader doesn't have the notion
            # of an id for the collection, we first need to
            # create one. For this example, we use the path info
            # at which the collection can be located
            pi = extract_url_path_info(collection.get_base_edit_uri()).strip('/')
            collection.name_or_id = pi

            # Next we use that id to ensure the repository structure
            # is correctly created
            collection.store.storage.create_container(pi)
            collection.store.media_storage.create_container(pi)
            
            # Set the index (see step 2)
            collection.add_indexer(index)
            
            # We indicate what class needs to be used when loading members (see step 5)
            collection.set_member_class(ResourceWrapper)

            # Reload members so that the index is updated (in case you'd delete it ;))
            collection.reload_members()
            
            # We instantiate the feed handler
            collection.feed_handler = FeedHandler()
            # Reset the collection feed cache 
            collection.feed_handler.set(collection.feed)

            # We look for a link that would indicate the public URI of this collection
            query = '//app:collection[@href="%s"]/atom:link[@type="application/atom+xml;type=feed"]'

            # Make sure you use the base_edit_uri so that you get the exact value of
            # the href attribute, if you use the get_base_edit_uri() method you get
            # the extended value prefixed with any potential xml:base found in one
            # of the ancestor of the app:collection element
            query = query % collection.base_edit_uri

            result = xmldoc.xml_xpath(query)
            if result:
                link = result.pop()
                collection.base_uri = unicode(link.href)

    def create_serving_applications(self):
        # Mount the service handler that will serve the AtomPub service document
        self.service = ServiceHandler(self.servdoc.to_service().xml(indent=True))

        # Mount the blog AtomPub web service
        c = self.servdoc.get_collection_by_uri('http://localhost:8080/service/blog/')
        self.service.blog = CollectionHandler(c)
        self.service.blog.paging = CollectionPagingHandler(c)

As you can see the loader expects the AtomPub service document describing your store but unlike the old INI loader that could hold extra information about the store, there are some missing pieces here. That's the reason why once the service is loaded and an amplee structure is created we must have the second pass to fill the missing data.

Note that we eventually mount the applications that will serve the AtomPub service document as well as the collection handlers. We even mount a handler to support collection paging (at least some sort of).

Here is an example of service document:

<?xml version="1.0" encoding="UTF-8"?>
<app:service xmlns:atom="http://www.w3.org/2005/Atom" xmlns:app="http://www.w3.org/2007/app" xml:lang="en" xml:base="http://localhost:8080/">
  <app:workspace>
    <atom:title type="text">Blog</atom:title>
    <app:collection href="service/blog/">
      <atom:title type="text">My blog and related thoughts</atom:title>

      <!-- public URI of the blog -->
      <atom:link href="blog/" type="application/atom+xml;type=feed" rel="alternate"/>

      <app:accept>application/atom+xml;type=entry</app:accept>
      <app:accept>image/png</app:accept>
      <app:accept>application/x-www-form-urlencoded</app:accept>
      <app:categories>
        <atom:category term="me" scheme="http://people.net" label="Me me me" />
      </app:categories>
    </app:collection>
  </app:workspace>
</app:service>

And an example of the config.xml file that holds the information about the type of storages to use:

<config xmlns="http://purl.oclc.org/DEFUZE/amplee">
  <store>
    <lock />
    <storage type="filesystem" target="member">
      <encoding>utf-8</encoding>
      <basepath>repository</basepath>
      <!-- Uncomment the following line if you want to add memcached caching support -->
      <!--<cache ref="cache0" />-->
    </storage>
    <storage type="filesystem" target="media">
      <encoding>utf-8</encoding>
      <basepath>repository</basepath>
    </storage>
    <cache id="cache0">
      <servers>
        <server ip="127.0.0.1" port="11211" />
        <server ip="127.0.0.1" port="11212" />
      </servers>
    </cache>
  </store>
</config>

In this example we use a filesystem storage for both the members and the media resources. It also happens they both live in the same directory named repository that will be prefixed by the directory provided to the AtomPubLoader instance.

Note as well that you can enable memcached caching support of storages as shown above. This will wrap the storage instance of your choice into a amplee.storage.storememcache.StorageMemcache instance that will act as a proxy to the underlying storage. On write operations the content will be added/updated/deleted from both the memcached servers and the underlying storage. On retrieve operations it will return the memcached value and if it doesn't exist it will request the underlying storage.

Step 4: The AtomPub service HTTP handler

This is a basic CherryPy? application that allows for GET and HEAD requests to retrieve the service resource representation.

class ServiceHandler(object):
    exposed = True
    
    def __init__(self, servdoc):
        self.servdoc = servdoc

    def HEAD(self, *args, **kwargs):
        content = self.GET(*args, **kwargs)
        cherrypy.response.headers['Content-Length'] = len(content)

    def GET(self):
        cherrypy.response.headers['Content-Type'] = 'application/atomsvc+xml'
        return self.servdoc

Step 5: The member resource interface

First we sub-class the MemberResource class to override the generate_resource_id method used internally by amplee whenever it needs to get the identifier of the resource within the storage.

class ResourceWrapper(MemberResource):
    def generate_resource_id(self, entry=None, slug=None, info=None):
        if slug:
            return slug.replace(' ','_').decode('utf-8')
        else:
            # if not then we use the last segment of the
            # link as the id of the resource in the storage
            links = entry.xml_xpath('/atom:entry/atom:link[@rel="edit"]')
            if links:
                return extract_url_trail(links[0].href)

        # fallback
        return str(time.time())

The method takes at most three parameters:

  • the entry instance itself which is an amara instance.
  • the slug decoded from the HTTP request if it was present or None
  • the info object, a amplee.storage.StorageResourceInfo instance, representing the resource within the storage. None if the resource is newly created and therefore doesn't exist in the storage yet.

In the example above, we use the slug value if it was provided in the request. If not we try to extract a value from the Atom entry and if not there we simply generate a dummy value. Note of course that you may have special needs and generate the identifier of the resource within the storage in a complete other way. As long as you return a string that the chosen storage can handle it doesn't matter at all for amplee.

You could also override the generate_atom_id(self, entry=None, slug=None) method if you want to specify the way to generate the atom:id values. By default it creates a UUID.

Step 6: The collection web service

First we define the handler so that CherryPy? doesn't process the request body for URLs mapping to instances of this class. We also define a few helpers that we use in the rest of the class.

class CollectionHandler(object):
    exposed = True
    _cp_config = {'tools.amplee_request_body.on': True}
    
    def __init__(self, collection):
        self.collection = collection

    ##########################################
    # Helpers
    ##########################################
    def __check_content_type(self):
        ct = cherrypy.request.headers.get('Content-Type')
        if not ct:
            raise cherrypy.HTTPError(400, "Missing content type")

        mimetype = get_best_mimetype(ct, self.collection.editable_media_types,
                                     check_params=False, return_full=True)
        if not mimetype:
            raise cherrypy.HTTPError(415, 'Unsupported Media Type')

        # The problem with this media-type is that it also contains
        # the multipart boundary value within the media-type
        # and that value is per client so here we do force
        # the media-type to be stripped of its parameters
        if mimetype.startswith('multipart/form-data'):
            mimetype = 'multipart/form-data'

        return mimetype

    def __check_length(self):
        length = cherrypy.request.headers.get('Content-Length')
        if not length:
            raise cherrypy.HTTPError(411, 'Length Required')
        return int(length)

    def __get_member(self, id):
        id = safe_unquote(id)
        member_id, media_id = self.collection.convert_id(id)
        member = self.collection.get_member(member_id)
        if not member:
            raise cherrypy.NotFound()
        return member

Let's first see how we can implement the GET handler.

    def GET(self, id=None):
        # If no id was provided we understand the request was to serve the collection feed
        if id == None:
            collection_feed = self.collection.feed_handler.retrieve()
            cherrypy.response.headers['etag'] = compute_etag_from_feed(collection_feed)
            cherrypy.response.headers['content-type'] = 'application/atom+xml;type=feed'
            return self.collection.feed_handler.collection_xml()

        member = self.__get_member(id)

        # By default we end up Atom entry with the '.atom' extension. 
        # This is a bit weak but hey it's a tutorial :)
        if id.endswith('.atom'):
            content = member.xml(indent=True)
            cherrypy.response.headers['Content-Type'] = 'application/atom+xml;type=entry'
        else:
            content = member.content
            member.media_type = extract_media_type(member.atom)
            cherrypy.response.headers['Content-Type'] = member.media_type
            
        cherrypy.response.headers['ETag'] = compute_etag_from_entry(member.atom)

        return content

Next we define the POST handler.

    def POST(self):
        mimetype = self.__check_content_type()
        length = self.__check_length()
        slug = decode_slug(cherrypy.request.headers.get('slug', None))
        
        member = ResourceWrapper(self.collection, media_type=mimetype)

        # The next line generates the Atom entry associated with the posted
        # entity. In case the posted entity is an atom entry, the generated one
        # takes its metadata from it. 
        # Note that the returned content is gonna be the file object provided
        # as the source of the generate() call.
        content = member.generate(mimetype, source=cherrypy.request.body, slug=slug,
                                  length=length, preserve_dates=False)

        member.inherit_categories_from_collection()
        
        media_content = None
        # If the posted entity is not an Atom entry, then we do
        # read the request body
        if not member.is_entry_mimetype(mimetype):
            media_content = content.read(length)
            
        self.collection.attach(member, member_content=member.atom.xml(),
                               media_content=media_content)
        self.collection.store.commit(message='Adding %s' % member.member_id)

        # Regenerate the collection feed
        self.collection.feed_handler.set(self.collection.feed)

        cherrypy.response.status = '201 Created'
        member_uri = member.member_uri
        if member_uri:
            member_uri = member_uri.encode('utf-8')
            cherrypy.response.headers['Location'] = member_uri
            cherrypy.response.headers['Content-Location'] = member_uri
            
        cherrypy.response.headers['Content-Type'] = u'application/atom+xml;type=entry'
        cherrypy.response.headers['ETag'] = compute_etag_from_entry(member.atom)

        return member.xml(indent=True)

Note that this handler can take care of any media-type. However you might want to write more specific handlers for specific media-types. Usually though this should be enough.

Then we see how we can implement the PUT handler.

    def PUT(self, id):
        mimetype = self.__check_content_type()
        length = self.__check_length()
        member = self.__get_member(id)

        # Handle conditional PUT
        if cherrypy.request.headers.get('If-Match'):
            try:
                handle_if_match(compute_etag_from_entry(member.atom),
                                cherrypy.request.headers.elements('If-Match') or [])
                # We want to prevent the CherryPy Etag tool to process this header
                # again and break the response
                del cherrypy.request.headers['If-Match']
            except ResourceOperationException, roe:
                raise cherrypy.HTTPError(roe.code, roe.msg)

        # On a PUT we generate a new member resource and we simply merge the existing member
        # resource metadata into the new one.
        new_member = ResourceWrapper(self.collection, media_type=mimetype)
        content = new_member.generate(mimetype, source=cherrypy.request.body,
                                      existing_member=member, length=length)
        new_member.use_published_date_from(member) # let's keep the published date of the original member
        new_member.update_dates() # but update the other dates
        
        media_content = None
        if not new_member.is_entry_mimetype(mimetype):
            media_content = content.read(length)

        self.collection.attach(new_member, member_content=new_member.atom.xml(),
                               media_content=media_content)
        self.collection.store.commit(message='Updating %s' % new_member.member_id)

        # Regenerate the collection feed
        self.collection.feed_handler.set(self.collection.feed)
        
        cherrypy.response.headers['Content-Type'] = mimetype
        cherrypy.response.headers['ETag'] = compute_etag_from_entry(new_member.atom)
        return new_member.xml()

Finally we support the DELETE method.

    def DELETE(self, id):
        member = self.__get_member(id)
        self.collection.prune(member.member_id, member.media_id)
        self.collection.store.commit(message="Deleting %s and %s" % (member.member_id,
                                                                     member.media_id))
        
        # Regenerate the collection feed
        self.collection.feed_handler.set(self.collection.feed)

Step 7: Simple example of paging support

The following handler allows a simple support of paging of members. Basically it takes an offset within the collection and creates a feed of 10 members at most from that offset. On a GET this returns an Atom feed of those entries.

class CollectionPagingHandler(object):
    exposed = True
    
    def __init__(self, collection):
        self.collection = collection

    def GET(self, start=0):
        start = int(start)
        
        members = self.collection.reload_members(start=start, limit=10)
        feed = self.collection.to_feed(members=members)

        attrs = {u'rel': u'first', u'type': u'application/atom+xml;type=feed',
                 u'href': safe_url_join([self.collection.get_base_edit_uri(), u'paging'])}
        feed.feed.xml_append(feed.xml_create_element(qname(u"link", feed.feed.prefix),
                                                     ns=feed.feed.namespaceURI,
                                                     attributes=attrs))

        if start > 10:
            attrs = {u'rel': u'previous', u'type': u'application/atom+xml;type=feed',
                     u'href': safe_url_join([self.collection.get_base_edit_uri(),
                                             u'paging?start=%s' % unicode(start-10)])}
            feed.feed.xml_append(feed.xml_create_element(qname(u"link", feed.feed.prefix),
                                                         ns=feed.feed.namespaceURI,
                                                         attributes=attrs))
            
        attrs = {u'rel': u'next', u'type': u'application/atom+xml;type=feed',
                 u'href': safe_url_join([self.collection.get_base_edit_uri(),
                                         u'paging?start=%s' % unicode(start+10)])}
        feed.feed.xml_append(feed.xml_create_element(qname(u"link", feed.feed.prefix),
                                                     ns=feed.feed.namespaceURI,
                                                     attributes=attrs))
        
        cherrypy.response.headers['etag'] = compute_etag_from_feed(feed)
        cherrypy.response.headers['content-type'] = 'application/atom+xml;type=feed'
        return feed.xml(indent=True)

Step 8: Setting up the HTTP server

Finally we show how to setup the CherryPy? server for the applications defined above.

if __name__ == "__main__": 
    import cherrypy
    import os
    base_dir = os.getcwd()

    cherrypy.config.update({'engine.autoreload_on' : False,
                            'server.socket_port' : 8080, 
                            'server.socket_host': '127.0.0.1',
                            'server.socket_queue_size': 15,
                            'log.screen': True,
                            'log.access_file': os.path.join(base_dir, 'access.log'),
                            'log.error_file': os.path.join(base_dir, 'error.log'),
                            'checker.on': False,})

    index = setup_index()

    from cherrypy._cpdispatch import MethodDispatcher
    from application import Application, setup_index

    index = setup_index()
    mainapp = Application(index)
    cherrypy.tree.mount(mainapp, '/', {'/': { 'request.dispatch': MethodDispatcher(),
                                              'tools.etags.on': True,
                                              'tools.etags.autotags': False},
                                       '/static': {'tools.staticdir.on': True,
                                                   'tools.staticdir.dir': os.path.join(base_dir, 'static')}})
    
    from searchapplication import SearchApplication
    cherrypy.tree.mount(SearchApplication(index, mainapp.servdoc), '/search')
    
    cherrypy.signal_handler.subscribe()
    cherrypy.engine.start()
    cherrypy.engine.block()

Step 9: OpenSearch basic handling and usage of the indexer

As an extra bonus point we show how to support OpenSearch. This also demonstrates how to use indexers to browse for member resources.

import cherrypy

from amplee.ext.opensearch.description import OpenSearchDescription
from amplee.ext.opensearch.url import OpenSearchURL
#from amplee.ext.opensearch.image import OpenSearchImage
#from amplee.ext.opensearch.query import OpenSearchQuery
    
__all__ = ['SearchApplication']

class SearchApplication(object):
    def __init__(self, index, service):
        self.indexer = index
        self.service = service

        # Ensure to set all fields to unicode
        osd = OpenSearchDescription()
        osd.short_name = u'amplee search'
        osd.description = u'searching within amplee'
        osd.contact = u'sylvain <sh@defuze.org>'
        osd.tags = u'atom atompub soa'
        osd.developer = u'Sylvain Hellegouarch'
        osd.syndication_right = u'open'
        osd.urls.append(OpenSearchURL(u'http://localhost:8080/search/go?q={searchTerms}',
                                      mimetype=u'text/html'))
        #osd.languages.append(u'en-US')
        #osd.images.append(OpenSearchImage(u'http://myimage.png', mimetype=u'image/png'))
        #osd.queries.append(OpenSearchQuery(u'example'))

        self.osd = osd

    @cherrypy.expose
    def index(self):
        return """<html>
        <head>
        <link rel="search" type="application/opensearchdescription+xml" href="/search/description" title="Content search" />
        </head>
        <body>add the amplee search to your search bar if you're using Firefox 2 or IE7</body>
        </html>"""

    @cherrypy.expose
    def go(self, q):
        # We only lookup within the author and category indexes
        # result is a Python set
        result = self.indexer.indexes['ci'].lookup(term=q)
        result |= self.indexer.indexes['ai'].lookup(name=q, email=q)

        # We transform the result set into a dictionary
        items = self.indexer.to_dict(result)

        # We reload the matching members and generate an Atom feed from them
        cherrypy.response.headers['content-type'] = 'application/xml'
        return self.service.make_feed(items, title=u"Search Result",
                                      xslt_path='/static/opensearch_to_html.xsl').xml()

    @cherrypy.expose
    def description(self):
        cherrypy.response.headers['content-type'] = 'application/opensearchdescription+xml'
        return self.osd.to_document().xml(indent=True)

Step 10: Trying it

Retrieve the service document

>>> import httplib2
>>> h = httplib2.Http()
>>> r, c = h.request('http://localhost:8080/service')
>>> r
{'status': '200', 'content-length': '929', 'content-location': 'http://localhost:8080/service', 'server': 'CherryPy/3.1.0beta3', 'allow': 'GET, HEAD', 'date': 'Sun, 17 Feb 2008 14:17:36 GMT', 'content-type': 'application/atomsvc+xml'}
>>> print c
<?xml version="1.0" encoding="UTF-8"?>
<app:service xmlns:app="http://www.w3.org/2007/app" xml:lang="en" xml:base="http://localhost:8080/">
  <app:workspace>
    <atom:title xmlns:atom="http://www.w3.org/2005/Atom" type="text">Blog</atom:title>
    <app:collection href="service/blog/">
      <atom:title xmlns:atom="http://www.w3.org/2005/Atom" type="text">My blog and related thoughts</atom:title>
      <atom:link xmlns:atom="http://www.w3.org/2005/Atom" href="blog/" type="application/atom+xml;type=feed" rel="alternate"/>
      <app:accept>application/atom+xml;type=entry</app:accept>
      <app:accept>image/png</app:accept>
      <app:accept>application/x-www-form-urlencoded</app:accept>
      <app:categories fixed="no">
        <atom:category xmlns:atom="http://www.w3.org/2005/Atom" term="me" scheme="http://people.net" label="Me me me"/>
      </app:categories>
    </app:collection>
  </app:workspace>
</app:service>

Retrieving the collection feed

>>> r, c = h.request('http://localhost:8080/service/blog/')
>>> r
{'status': '200', 'content-length': '288', 'content-location': 'http://localhost:8080/service/blog/', 'server': 'CherryPy/3.1.0beta3', 'etag': '"637b9125e43be7063c66b9679cc64fe9"', 'allow': 'DELETE, GET, HEAD, POST, PUT', 'date': 'Sun, 17 Feb 2008 14:22:04 GMT', 'content-type': 'application/atom+xml;type=feed'}
>>> print c
<?xml version="1.0" encoding="UTF-8"?>
<atom:feed xmlns:atom="http://www.w3.org/2005/Atom"><atom:id>urn:uuid:c83911ab-1fdb-51c4-9bf1-3ec95c2d7bb1</atom:id><atom:updated>2008-02-17T14:17:07.554859Z</atom:updated><atom:title type="text">My blog and related thoughts</atom:title></atom:feed>

Posting to the collection feed

>>> body = 'message=hello'
>>> r, c = h.request('http://localhost:8080/service/blog/', method='POST', body=body, headers={'content-type': 'application/x-www-form-urlencoded', 'slug': 'hello world'})
>>> r
{'status': '201', 'content-length': '907', 'content-location': 'http://localhost:8080/service/blog/hello_world.atom', 'server': 'CherryPy/3.1.0beta3', 'etag': '"9568a33e568b30f32a39f319910025c5"', 'location': 'http://localhost:8080/service/blog/hello_world.atom', 'allow': 'DELETE, GET, HEAD, POST, PUT', 'date': 'Sun, 17 Feb 2008 14:23:55 GMT', 'content-type': 'application/atom+xml;type=entry'}
>>> print c
<?xml version="1.0" encoding="UTF-8"?>
<atom:entry xmlns:app="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
  <atom:id>urn:uuid:44818a24-807d-4c5d-88f3-b80557ff899d</atom:id>
  <atom:published>2008-02-17T14:23:55.343881Z</atom:published>
  <atom:updated>2008-02-17T14:23:55.343881Z</atom:updated>
  <app:edited>2008-02-17T14:23:55.343881Z</app:edited>
  <atom:link href="http://localhost:8080/service/blog/hello_world.atom" type="application/atom+xml;type=entry" rel="edit"/>
  <atom:link length="13" href="http://localhost:8080/service/blog/hello_world" type="application/x-www-form-urlencoded" rel="edit-media"/>
  <atom:content src="http://localhost:8080/blog/hello_world" type="application/x-www-form-urlencoded"/>
  <atom:title type="text"/>
  <atom:author>
    <atom:name/>
  </atom:author>
  <atom:category term="me" scheme="http://people.net" label="Me me me"/>
</atom:entry>

Retrieving the created resource

>>> r, c = h.request('http://localhost:8080/service/blog/hello_world')
>>> r
{'status': '200', 'content-length': '13', 'content-location': 'http://localhost:8080/service/blog/hello_world', 'server': 'CherryPy/3.1.0beta3', 'etag': '"9568a33e568b30f32a39f319910025c5"', 'allow': 'DELETE, GET, HEAD, POST, PUT', 'date': 'Sun, 17 Feb 2008 14:24:59 GMT', 'content-type': 'application/x-www-form-urlencoded'}
>>> print c
message=hello
>>> r, c = h.request('http://localhost:8080/service/blog/hello_world.atom')
>>> print c
<?xml version="1.0" encoding="UTF-8"?>
<atom:entry xmlns:app="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom">
  <atom:id>urn:uuid:44818a24-807d-4c5d-88f3-b80557ff899d</atom:id>
  <atom:published>2008-02-17T14:23:55.343881Z</atom:published>
  <atom:updated>2008-02-17T14:23:55.343881Z</atom:updated>
  <app:edited>2008-02-17T14:23:55.343881Z</app:edited>
  <atom:link href="http://localhost:8080/service/blog/hello_world.atom" type="application/atom+xml;type=entry" rel="edit"/>
  <atom:link length="13" href="http://localhost:8080/service/blog/hello_world" type="application/x-www-form-urlencoded" rel="edit-media"/>
  <atom:content src="http://localhost:8080/blog/hello_world" type="application/x-www-form-urlencoded"/>
  <atom:title type="text"/>
  <atom:author>
    <atom:name/>
  </atom:author>
  <atom:category term="me" scheme="http://people.net" label="Me me me"/>
</atom:entry>
>>> r, c = h.request('http://localhost:8080/service/blog/hello_world.atom', headers={'if-none-match': '"9568a33e568b30f32a39f319910025c5"'})
>>> r
{'date': 'Sun, 17 Feb 2008 14:27:43 GMT', 'status': '304', 'etag': '"9568a33e568b30f32a39f319910025c5"', 'server': 'CherryPy/3.1.0beta3'}

Update/Edit an existing resource

>>> r, c = h.request('http://localhost:8080/service/blog/hello_world', method='PUT', body='message=hey', headers={'content-type': 'application/x-www-form-urlencoded', 'if-match': '"9568a33e568b30f32a39f319910025c5"'})
>>> r
{'status': '200', 'content-length': '786', 'server': 'CherryPy/3.1.0beta3', 'etag': '"be7382a4adbdc28f5d27b33fd03617fb"', 'allow': 'DELETE, GET, HEAD, POST, PUT', 'date': 'Sun, 17 Feb 2008 14:30:06 GMT', 'content-type': 'application/x-www-form-urlencoded'}
>>> print c
<?xml version="1.0" encoding="UTF-8"?>
<atom:entry xmlns:app="http://www.w3.org/2007/app" xmlns:atom="http://www.w3.org/2005/Atom"><atom:id>urn:uuid:44818a24-807d-4c5d-88f3-b80557ff899d</atom:id><atom:published>2008-02-17T14:23:55.343881Z</atom:published><atom:updated>2008-02-17T14:30:06.831020Z</atom:updated><app:edited>2008-02-17T14:30:06.831020Z</app:edited><atom:link href="http://localhost:8080/service/blog/hello_world.atom" type="application/atom+xml;type=entry" rel="edit"/><atom:link href="http://localhost:8080/service/blog/hello_world" type="application/x-www-form-urlencoded" rel="edit-media"/><atom:content src="http://localhost:8080/blog/hello_world" type="application/x-www-form-urlencoded"/><atom:title type="text"/><atom:author><atom:name/></atom:author></atom:entry>

We can also retry the same request but since the ETag has changed it will fail.

>>> r, c = h.request('http://localhost:8080/service/blog/hello_world', method='PUT', body='message=hey', headers={'content-type': 'application/x-www-form-urlencoded', 'if-match': '"9568a33e568b30f32a39f319910025c5"'})
>>> r
{'status': '412', 'content-length': '1416', 'server': 'CherryPy/3.1.0beta3', 'allow': 'DELETE, GET, HEAD, POST, PUT', 'date': 'Sun, 17 Feb 2008 14:32:23 GMT', 'content-type': 'text/html'}

Deleting resources

>>> r, c = h.request('http://localhost:8080/service/blog/hello_world', method='DELETE')
>>> r
{'status': '200', 'content-length': '0', 'server': 'CherryPy/3.1.0beta3', 'allow': 'DELETE, GET, HEAD, POST, PUT', 'date': 'Sun, 17 Feb 2008 14:34:43 GMT', 'content-type': 'text/html'}

Searching for resources

>>> r, c = h.request('http://localhost:8080/service/blog/', method='POST', body=body, headers={'content-type': 'application/x-www-form-urlencoded', 'slug': 'hello world'})
>>> r, c = h.request('http://localhost:8080/search/go?q=me')
>>> r
{'status': '200', 'content-length': '1181', 'content-location': 'http://localhost:8080/search/go?q=me', 'server': 'CherryPy/3.1.0beta3', 'date': 'Sun, 17 Feb 2008 14:37:23 GMT', 'content-type': 'application/xml'}
>>> print c
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="/static/opensearch_to_html.xsl" type="text/xsl"?><atom:feed xmlns:atom="http://www.w3.org/2005/Atom" xml:lang="en" xml:base="http://localhost:8080/"><atom:id>urn:uuid:c18b3c00-2401-482d-8d23-a96a39ab2e46</atom:id><atom:updated>2008-02-17T14:37:23.485887Z</atom:updated><atom:title type="text">Search Result</atom:title><atom:entry><atom:id>urn:uuid:fa4cb536-9b72-4e42-8fc1-ee21d4396ff8</atom:id><atom:published>2008-02-17T14:36:36.248414Z</atom:published><atom:updated>2008-02-17T14:36:36.248414Z</atom:updated><app:edited xmlns:app="http://www.w3.org/2007/app">2008-02-17T14:36:36.248414Z</app:edited><atom:link href="http://localhost:8080/service/blog/hello_world.atom" type="application/atom+xml;type=entry" rel="edit"/><atom:link length="13" href="http://localhost:8080/service/blog/hello_world" type="application/x-www-form-urlencoded" rel="edit-media"/><atom:content src="http://localhost:8080/blog/hello_world" type="application/x-www-form-urlencoded"/><atom:title type="text"/><atom:author><atom:name/></atom:author><atom:category term="me" scheme="http://people.net" label="Me me me"/></atom:entry></atom:feed>