Akara Tutorial

Writing your first adapter

For the basic set-up of an Akara module, you can start with echo.py and then customize accordingly.

The following are key lines from a simplified version of that module, with some notes.

import amara
from akara.services import simple_service

amara contains a lot of useful core facilities, including for XML processing. It's not really used in this example, but it's worth knowing about early on.

If you basically want to work with a Python function that takes a few parameters and returns a result, wrapping this whole thing as a Web service, you probably just need the @simple_service decorator.

ECHO_SERVICE_ID = 'http://purl.org/xml3k/akara/services/demo/echo'

All Akara services have an ID, a URI, which represents the essence of that service, i.e. its inputs, outputs and behavior. You and I might take the same Akara code, and you host it on your server and I host it on mine. The service ID will be the same in both cases, but the access endpoint, i.e. what URL users invoke to use the services, will be different.

@simple_service('POST', ECHO_SERVICE_ID, 'akara.echo')

Use the decorator to indicate that a function is a service, and specify what HTTP methods it handles, the ID for the service, and the default mount point, which is the trailing bit of the access endpoint URL. If you mount this service on an Akara instance running at http://localhost:8880, then its access endpoint will be http://localhost:8880. The user can HTTP POST some data to this URL, and the decorated function will be invoked.

def akara_echo_body(body, ctype):

This is the decorated function. Simple service implementation functions wrapped as HTTP POST methods receive the HTTP POST body and the HTTP Content Type header as parameters. The latter is a convenience. You can also get any of the other headers using WSGI (more on this later).

    return body

The echo function is super simple. It just returns the HTTP request body as the response, hence the service name "echo".

The Hello World

What would a tutorial be without a "Hello world"? The following Akara module implements a simple Hello world service.

from akara.services import simple_service

HELLO_SERVICE_ID = 'http://example.org/my-services/hello'

@simple_service('GET', HELLO_SERVICE_ID, 'hello')
def helloworld(friend=None):
    return u'Hello, ' + friend

Save this as hello.py and make it available in `PYTHONPATH`, and update the akara.conf of an Akara instance to load the module. If the instance is at localhost:8880, you can invoke the new module as follows:

$ curl http://localhost:8880/hello?friend=Uche
Hello, Uche

Or, if you prefer, put http://localhost:8880/hello?friend=Uche into your browser to get the nice greeting. Go ahead and play around with URL basics, e.g.:

$ curl http://localhost:8880/hello?friend=Uche+Ogbuji
Hello, Uche Ogbuji

Which in this case behaves just like http://localhost:8880/hello?friend=Uche%20Ogbuji

The Web is an international place

Feeling confident about all this progress, why not go out on a limb and say hello to your friend José:

$ curl http://localhost:8880/hello?friend=Jos%C3%A9%20Sanchez
<?xml version="1.0" encoding="ISO-8859-1"?><!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
<head>
  <title>Internal Server Error</title>
</head>
<body>
  <h1>Internal Server Error</h1>
  <p>
  Server got itself in trouble
  </p>
  <h2>Error 500</h2>
</body>
</html>

Oops. You have to go to the logs to see the actual error, but the problem is the usual string/unicode divide in Python. You have to be very careful about such things when deploying real-world Web applications. First of all the fix. Update the module as follows:

from akara.services import simple_service

HELLO_SERVICE_ID = 'http://example.org/my-services/hello'

@simple_service('GET', HELLO_SERVICE_ID, 'hello')
def helloworld(friend=None):
    return u'Hello, ' + friend.decode('utf-8')   #THIS IS THE CHANGED LINE

Notice the new decode method. Now re-deploy, restart Akara and start again:

$ curl http://localhost:8880/hello?friend=Jos%C3%A9%20Sanchez
Hello, José Sanchez

All better, so let's examine the fix more closely. Akara sends the GET parameter as a UTF-8 encoded string. In order to safely concatenate this to the unicode constant u'Hello, ', you want to make convert it to Unicode, which is what the decode method accomplishes.

Introducing WSGI, and working with URL path hierarchy

The above approach works fine if you are creating very simple, dynamic query services, but it gets very tempting to do too much of that, and to squander much of the benefit of REST.

In many Web applications, rather than calculating a greeting on the fly, we're instead gathering information and even modifying some well-known, referenceable resource. In such cases, the common convention is to use hierarchical URLs to represent the different resources. As an example, say we're developing a database of poets and their works. Each poet would be a distinct resource, e.g. at http://localhost:8880/poetdb/poet/ep.

To get this somewhat more sophisticated behavior, we take advantage of the common WSGI convention of Python. The following complete Akara module implements the poet database.

from wsgiref.util import shift_path_info

from akara.services import simple_service
from akara import request

POETDB_SERVICE_ID = 'http://example.org/my-services/poetdb'

#Cheap DBMS
POETDB = {
  u'poet':
  {
    u'ep': (u'Ezra Pound', u'45 Usura Place, Hailey, ID'),
    u'co': (u'Christopher Okigbo', u'7 Heaven\'s Gate, Idoto, Anambra, Nigeria')
  },
  u'work':
  {
    u'cantos': (u'The Cantos', u'../poet/ep'),
    u'mauberley': (u'Hugh Selwyn Mauberley', u'../poet/ep'),
    u'thunderpaths': (u'Paths of Thunder', u'../poet/co')
  },
}

@simple_service('GET', POETDB_SERVICE_ID, 'poetdb', 'text/html')
def poetdb():
    entitytype = shift_path_info(request.environ)
    eid = shift_path_info(request.environ)
    info = POETDB[entitytype][eid]
    if entitytype == u'poet':
        #name, address = info
        return u'<p>Poet: %s, of %s</p>'%info
    elif entitytype == u'work':
        #name, poet = info
        return u'<p>Work: %s, <a href="%s">click for poet info</a></p>'%info

Focusing in on some key lines:

from akara import request
...
    entitytype = shift_path_info(request.environ)

The request object, which becomes available to your module through the import, is the main way to access information from the HTTP request, using WSGI conventions, such as the environ mapping. The Python stdlib function wsgiref.shift_path_info allows you to extract one hierarchical path component from the URL used to access the service.

So going back to the sample URL for a poet, http://localhost:8880/poetdb/poet/ep, Akara itself is mounted at http://localhost:8880/ and the service defined above is mounted at http://localhost:8880/poetdb/. The first wsgiref.shift_path_info extracts the poet component. There is a second one that extracts the ep component.

@simple_service('GET', POETDB_SERVICE_ID, 'poetdb', 'text/html')

Notice the additional argument, which declares the return content type. The output of this service is HTML.

        return u'<p>Poet: %s, of %s</p>'%info

Again the return value is a Unicode object. You can return from an Akara service handler string or Unicode, or even parsed Amara XML objects.

Deploy this module and restart Akara and now if you go to e.g. http://localhost:8880/poetdb/work/cantos in a browser you will get a page saying "Work: The Cantos, click for poet info," and if you click the link it will take you to a page with the representation of the poet resource http://localhost:8880/poetdb/poet/ep, based on the relative link set up in the POETRYDB data structure.

Now you're really getting into the Web application space, and rubbing up a bit against REST in that resources such as poet and work are clearly identified by URL, and clearly referenced within the content via hypermedia (i.e. good old Web links).

Error handling, and making things more robust

Try out the following URL on the above service:

http://localhost:8880/poetdb/poet/noep

You get the dreaded 500 error. The Web is a wild place, and you never know what input or conditions you're going to be dealing with, so anticipating and gracefully handling errors is important. Let's set it up so that the server returns a 404 "Not Found" error in case the URL path doesn't match anything in the database. Let's also set up some basic link index pages to help the user. In general the following is a much more complete and functional example.

from wsgiref.util import shift_path_info, request_uri

from amara.lib.iri import join

from akara.services import simple_service
from akara import request, response

POETDB_SERVICE_ID = 'http://example.org/my-services/poetdb'

#Cheap DBMS
POETDB = {
  u'poet':
  {
    u'ep': (u'Ezra Pound', u'45 Usura Place, Hailey, ID'),
    u'co': (u'Christopher Okigbo', u'7 Heaven\'s Gate, Idoto, Anambra, Nigeria')
  },
  u'work':
  {
    u'cantos': (u'The Cantos', u'../poet/ep'),
    u'mauberley': (u'Hugh Selwyn Mauberley', u'../poet/ep'),
    u'thunder': (u'Paths of Thunder', u'../poet/co')
  },
}

def not_found(baseuri):
    ruri = request_uri(request.environ)
    response.code = "404 Not Found"
    return u'<p>Unable to find: %s, try the <a href="%s">index of works</a></p>'%(ruri, baseuri)

@simple_service('GET', POETDB_SERVICE_ID, 'poetdb', 'text/html')
def poetdb():
    baseuri = request.environ['SCRIPT_NAME'] + '/'
    def get_work(wid):
        uri = join(baseuri, 'work', wid)
        name, poet = POETDB[u'work'][wid]
        puri = join(baseuri, 'poet', poet)
        return '<p>Poetic work: <a href="%s">%s</a>, by <a href="%s">linked poet</a></p>'%(uri, name, puri)
    def get_poet(pid):
        uri = join(baseuri, 'poet', pid)
        name, address = POETDB[u'poet'][pid]
        return '<p>Poet: <a href="%s">%s</a></p>'%(uri, name)
    getters = { u'work': get_work, u'poet': get_poet }
    entitytype = shift_path_info(request.environ)
    if not entitytype:
        entitytype = u'work'
    if entitytype not in POETDB:
        return not_found(baseuri)
    eid = shift_path_info(request.environ)
    if not eid:
        #Return an index of works or poets
        works = []
        for work_id, (name, poet) in POETDB[u'work'].iteritems():
            works.append(getters[entitytype](work_id))
        return '\n'.join(works)
    try:
        return getters[entitytype](eid)
    except KeyError:
        return not_found(baseuri)

Again, focusing on the key new bits:

from amara.lib.iri import join

Amara comes with a lot of URI, and more generally IRI (internationalized URI) functions which are more RFC-compliant than the urllib equivalents, including the join function which constructs URI references from hierarchical path components.

from akara import request, response

The response object allows you to manage HTTP request status, headers, and such..

def not_found(baseuri):
    ruri = request_uri(request.environ)
    response.code = "404 Not Found"
    return u'<p>Unable to find: %s, try the <a href="%s">index of works</a></p>'%(ruri, baseuri)

Just a little utility function to provide a 404 response, with some information useful to the end user. request_uri is a Python stdlib function to reconstruct the request URI from a WSGI environment.

    baseuri = request.environ['SCRIPT_NAME'] + '/'

Here you construct the URL to access this service.

    def get_work(wid):
        uri = join(baseuri, 'work', wid)
        name, poet = POETDB[u'work'][wid]
        puri = join(baseuri, 'poet', poet)
        return '<p>Poetic work: <a href="%s">%s</a>, by <a href="%s">linked poet</a></p>'%(uri, name, puri)

A routine to generate HTML of the information for a single work. Notice how amara.lib.iri.join is used to construct links.

    getters = { u'work': get_work, u'poet': get_poet }

Just a way to package up the reusable routines for generating poet and work info.

        #Return an index of works or poets
        works = []
        for work_id, (name, poet) in POETDB[u'work'].iteritems():
            works.append(getters[entitytype](work_id))
        return '\n'.join(works)

Go through the index of works and return an aggregate HTML from the fragments.

Handling HTTP POST

The above example handles HTTP GET, and of course POST is a big part of the Web. It's best known for Web forms, though Akara is not specialized for such usage in the way more mainstream Web frameworks are (CherryPy, Django, etc.) You can use Akara to handle Web forms, but more often Akara users will be dealing with data services, often using requests directly POSTed to the endpoint. This is a common pattern for open Web APIs such as those of social networks.

Since POST on the Web is generally used in cases where state of Web resources are changing, this is usually the area where you need to deal with some sort of persistence in your application. You'll see an example of that in this section, moving from the in-memory data structure of the previous section to something more serious. You'll also see an example of how to read configuration information.

The following listing is an Akara module for accepting reservations of business resources such as conference rooms and the like.

<!> This example is designed to illustrate the mechanics of POST handling, but is not a good example of REST style. The Web style is to be improved on in future sections.

import shelve

from amara import bindery
from amara.lib import U

import akara
from akara.services import simple_service

DBFNAME = akara.module_config()['dbfile']

NEWPOET_SERVICE_ID = 'http://example.org/my-services/new-poet'


@simple_service('POST', NEWPOET_SERVICE_ID, 'newpoet', 'plain/text')
def newpoet(body, ctype):
    '''
    Add a poet to the database.
    
    Sample POST body: 
    <newpoet id="co">
      <name>Christopher Okigbo</name><address>Christopher Okigbo</address>
    </newpoet>
    '''
    dbfile = shelve.open(DBFNAME)
    #Warning: no validation of incoming XML
    doc = bindery.parse(body)
    dbfile[U(doc.newpoet.id)] = (U(doc.newpoet.name), U(doc.newpoet.address))
    dbfile.close()
    return 'Poet added OK'

This module requires a configuration variable, dbfile, which you can provide by adding the following (or similar) to akara.conf:

[tutorial_post]
dbfile = /tmp/poet

Once the service is running, you can use something like the following command line to add a poet:

curl --request POST --data-binary "@-" "http://localhost:8880/newpoet" << END
<newpoet id="co">
  <name>Christopher Okigbo</name><address>Christopher Okigbo</address>
</newpoet>
END

You can observe the result easily enough.

>>> import shelve
>>> d=shelve.open('/tmp/poet')
>>> print d.keys()
['co']
>>> print d['co']
(u'Christopher Okigbo', u'Christopher Okigbo')

<!> This tutorial uses shelve for simplicity, but for real world applications, you almost certainly want to use another persistence back end, such as sqlite. Also, shelve and this these examples are not safe for concurrent access from multiple module instances, which is just about guaranteed for a real-world application. sqlite may be, but you should test carefully in planned deployment configurations, or use a more ambitious DBMS such a PostgreSQL.

Service discovery

One of the key benefits of Akara is natural support for service discovery.

from akara.registry import get_a_service_by_id


See also

Other notes:

Akara/Tutorial (last edited 2011-11-14 02:39:47 by UcheOgbuji)