Resty applications in QP, Part III
Traversing the URI Path in QP Applications
In order to complete the Songs application we should understand how QP's default traversal mechanism makes it easy to provide a navigable UI for our objects we want to expose to the web.
In the last article in this series we saw through a hello, world example that a QP application has two key classes, a SitePublisher and a SiteDirectory, and that SiteDirectory exposes the root UI of a QP application. Let's flesh out a HTML interface to go along with the REST API offered by the Songs application, to include:
URI segment Name Returns to web browser (export) ------------------------------------------------------------- class SiteDirectory / index HTML listing of songs /new new HTML form / creation of new song /delete clear HTML form allowing deletion of all songs
The functionality at these URI segments will be provided by SiteDirectory:
class SiteDirectory(Directory):
def get_exports(self):
yield ('', 'index', 'Song List', 'This is the root / page of this site.')
yield ('api', 'api', 'API', 'API documentation and entry point')
yield ('new', 'new', 'New', 'Add a song to collection')
yield ('delete', 'clear', 'Delete', 'Delete all songs from collection')
def index:xml(self):
header('Song Database')
'<h1>Songs</h1>'
'<ul>'
for song in get_song_db().itervalues():
'<li>%s: <a href="%s/">%s</a></li>' % (
song.key, song.key, song.title)
'</ul>'
footer()
# ... for the forms examples provided by new/delete
# read the SongDirectory class at the end of this article
What of the API? Thanks to content-negotiation capability provided by the Resource subclass of Directory, we could intermingle our API functionality at the same URI as our HTML UI. Emphasis is on the word could because from this point forward, purely for the sake of clarity in this and subsequent articles on this theme, I'm going to risk angering the RESTafarian gods and instead host the entire REST API under the /api/ url segment. To the SiteDirectory class above we'll merely add an attribute and an entry in get_exports():
class SiteDirectory(Directory):
def get_exports(self):
yield ('', 'index', 'Song List', 'This is the root / page of this site.')
yield ('new', 'new', 'New', 'Add a song to collection')
yield ('delete', 'clear', 'Delete', 'Delete all songs from collection')
yield ('api', 'api', 'API', 'API documentation and entry point')
api = SongDatabaseResource()
# ...
Therefore our API will be accessible via the SongDatabaseResource subclass (first discussed here) of Resource and will expose the following:
URI method accept Return
-------------------------------------------------------------
class SongDatabaseResource
/api/ GET application/json json dict of all songs
/api/ DELETE application/json json {'result': 'true'} on success
/api/ POST application/json json dict of new song data
Architectural style purity aside, there are reasons in the practical world why one might wish to bury an API, even a REST API, behind a mount point as we've done. The busy developer with security (or marketing) concerns might want to only expose the API to authenticated users. Let's do that:
class SongDatabaseResource(Resource):
def get_resources(self):
get_publisher().ensure_signed_in()
yield export('', '__api__') # docs for web browsers
yield export('', 'dump', method='GET', accept='application/json')
yield export('', 'new', method='POST', accept='application/json')
yield export('', 'clear', method='DELETE', accept='application/json')
# ...
OK, we sketched out the first / of the HTML UI. How do we traverse to individual songs?
class SiteDirectory(Directory):
def get_exports(self):
yield ('', 'index', 'Song List', 'This is the root / page of this site.')
yield ('api', 'api', 'API', 'API documentation and entry point')
yield ('new', 'new', 'New', 'Add a song to collection')
yield ('delete', 'clear', 'Delete', 'Delete all songs from collection')
# ...
def _q_lookup(self, component):
try:
component = int(component)
song = get_song_db().get(component)
if song:
return SongDirectory(song)
except ValueError:
# None / an empty '' response being returned from _q_lookup
# will result in 404 not_found being delivered as the response.
return None
api = SongDatabaseResource()
The important mechanism to note here is _q_lookup. The default QP traversal mechanism (which may easily be changed) traverses Directory subclasses looking for callables or objects. One can create arbitrarily complex URI schemes with levels nested many deep, if that turns your crank.
The object traversal approach is a flexible mechanism, quite different than mapping callables or objects to URIs via regular expressions. But one could do that in QP too, if you found a need to. If you need even more flexibility you can override the Directory class _q_traverse method or dive into the Publisher itself.
One benefit of this approach is others who may use these classes generally do not need to concern themselves at all about where in their URI namespace they deploy these UI components.
The attached file: song.qpy, is a QPY template file containing a quick draft of the HTML UI.
This RESTy Songs database example has morphed into something of a QP howto. For those who just want to see the end result, fear not, I see that coming around the bend soon. It is time to tie it all together... tomorrow.