Tutorial
- Tutorial
- What is amplee and what it isn't
- Requirements
-
AtomPub store tutorial
- Step 1: Initialize the AtomPub application module
- Step 2: Create the indexers
- Step 3: AtomPub store application and loading the service document
- Step 4: The AtomPub service HTTP handler
- Step 5: The member resource interface
- Step 6: The collection web service
- Step 7: Simple example of paging support
- Step 8: Setting up the HTTP server
- Step 9: OpenSearch basic handling and usage of the indexer
- Step 10: Trying it
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>
