Python Paste


The Object HTTP Mapper

About & License

OHM is by Ian Bicking, and written for The Open Planning Project.

It is licensed under an MIT-style license. Questions can go to the Paste mailing list, and bugs can go on the Paste Trac.

This project is very young. API changes are likely. You might find the To-Do list interesting. The server side of the API is more mature than the client side.

What Is OHM?

OHM is a library for serving Python objects as REST-style HTTP APIs, and for consuming REST-style APIs as Python objects.

The basic idea is that an object exists at some base URL, which is a container. (Getting the object there is outside of the scope of OHM, but WSGI-based dispatchers and frameworks can help you do this.) Each attribute is a resource under this container. E.g., if the object is at /article/1/, then you might have resources like /article/1/last_modified, /article/1/body.html, etc.

This generally exposes every attribute as an independent resource, independently gettable and settable. You can also serialize the entire object at once, and represent methods as POSTable objects (a borderline controller).

How to use OHM

There's two parts: client and server. These are generally symmetric, but use basic REST techniques and can thus connect easily to non-OHM sources of data, or non-OHM clients.

See the server section and the client section.

Server

You come up with your own object, typically one that is persistent. A very simplistic persistence system is available on ohm.persist (you might find test/test_persist.py helpful in understanding the persistence).

You then define a wrapper around this object which will be the WSGI application. The wrapper is a class, and instances of the wrapper are connected to a specific object (and serve as WSGI application wrapping that object).

The basic pattern is:

from ohm import server
from formencode import validators

class MyWrapper(server.ApplicationWrapper):
    simple_attr = server.Setter()
    unicode_attr = server.Setter(unicode=True)
    typed_attr = server.Setter(content_type='text/plain')
    special_path_attr = server.Setter(uri_path='special-place.txt')
    json_attr = server.JSONSetter()
    int_attr = server.Setter(validator=validators.AsInt())

Each attribute in the class is an attribute of the underlying object that is exposed. In this case the object is assumed to have the attributes simple_attr, unicode_attr, etc. By default the values are exposed at paths like /simple_attr, etc. -- the exception in this example is special_path_attr which is exposed at /special-place.txt.

If you don't give any other information, each is exposed as an 8-bit str object. With unicode=True they are exposed as encoded objects (with charset=utf8). The library is picky about unicode -- you can't put unicode objects in str attributes (even ASCII-safe unicode objects) and vice versa.

You can provide a content_type -- otherwise every attribute will be served as application/octet-stream.

Any attribute can have a FormEncode validator. These validators are basically two-way conversion routines. In the example validators.AsInt() turns strings into ints, and ints into strings. JSONSetter() is a subclass of Setter() that uses the ohm.validators.JSONConverter validator to encode and decode JSON (using simplejson). Validators form the generic serialization support. Notably JSONSetter also serves its content as application/json.

To use your wrapper do:

wsgi_app = MyWrapper(an_object)

Now you can serve requests like wsgi_app(environ, start_response) that will wrap the specific instance an_object.

A more practical example might be helpful for "why would I want to use this?" Imagine you are exposing a database record (here expressed with SQLObject):

from ohm import server
from sqlobject import *
from formencode import validators
from paste.urlmap import URLMap
from paste import httpserver

class Article(SQLObject):
    created = DateTimeCol(default=datetime.now)
    title = StringCol()
    body = UnicodeCol()
    # This creates a calculated .render attribute:
    def _get_render(self):
        return u'''<html><title>%(title)s</title>
        <body><h1>%(title)s</h1>
        <div><i>Created: %(created)s</i></div>
        <div>%(body)s</div>
        </body></html>''' % dict(
            created=self.created, title=self.title,
            body=self.body)

class ArticleApp(server.ApplicationWrapper):
    created = server.Setter(
        validator=validators.DateTimeConverter())
    title = server.Setter()
    body = server.Setter(unicode=True)
    render = server.Setter(uri_path='article.html',
                           content_type='text/html')

a = Article(title='About Me', body='All about me...')
wsgi_app = ArticleApp(a)
mapper = URLMap()
mapper['/article/%s' % a.id] = a
httpserver.serve(mapper)

This will serve the mapper app on http://localhost:8080, and say the article is the first article, id 1. So the article has been mounted at http://localhost:8080/article/1. And lets say it's January 20, 2007.

Now you can access several resources:

  • http://localhost:8080/article/1/created returns 1/20/2007. you can also PUT a value there, like 1/21/2007 to update that value.
  • http://localhost:8080/article/1/title returns About Me. Again, you can PUT a new value there to update.
  • http://localhost:8080/article/1/body returns All about me...
  • http://localhost:8080/article/1/article.html returns that HTML page (as rendered by _get_render). You can't PUT to this, because the property itself is read-only.

Client

The client looks somewhat similar:

from ohm import client
from formencode import validators

class MyRemote(object):

    def __init__(self, base_uri):
        self.base_uri = base_uri

    simple_attr = client.remote('simple_attr')
    unicode_attr = client.remote('simple_attr', unicode=True)
    typed_attr = client.remote('typed_attr', content_type='text/plain')
    special_path_attr = client.remote('special-place.txt')
    json_attr = client.json_remote('json_attr')
    int_attr = client.remote('int_attr', validator=validators.AsInt())

Unlike server.ApplicationWrapper client.remote does not pick up the attribute names automatically. (This may be changed in the future, or it may not.) client.remote requires the object it is attached to to have an attribute obj.base_uri (this is the container that holds all the sub-resources). The attributes are simple descriptors and thus can be attached to any new-style class (be sure you subclass from object!)

You can then use the object like:

my_remote = MyRemote('http://localhost:8080/myobj')
print my_remote.int_attr # etc...

To extend the SQLObject example from above, here's how you might implement the client side of that:

from ohm import client
from formencode import validators

class Article(object):
    container_uri = 'http://localhost:8080/article/%(id)s'
    def __init__(self, base_uri):
        self.base_uri = base_uri
    @classmethod
    def get(cls, id):
        base_uri = cls.container_uri % dict(id=id)
        return cls(base_uri)
    created = client.remote('created',
                            validator=validators.DateConverter())
    title = client.remote('title')
    body = client.remote('body', unicode=True)
    render = client.remote('article.html',
                           content_type='text/html')

a = Article.get(1)
print a.title

This mimics the SQLObject API, but all the methods turn into HTTP calls, fetching the respective resources from the server.