Python-Brightkite for... you get the idea

11/16/2008

Last week Yashh pinged me about starting to write a Python wrapper for Brightkite. Based on the Brightkite restful api we've managed to make some fairly respectable progress towards a working library.

You can contribute or check it out in it's GitHub repository, and can see remaining tasks in the wiki.

Installing Python-Brightkite

Installing Python-Brightkite is a fairly straightforward process.

easy_install httplib2
git clone git://github.com/lethain/python_brightkite.git

It also depends upon xml2dict, but that dependency is packaged along with it (we're packaging it because it doesn't have an easy_install package, and hasn't changed in a couple of years).

For the time being you'll probably do best to copy the .py files from the repository into your project, but we'll solidify a more humane deployment solution as the project reaches the toddler stage of development.

cp python_brightkite/*.py ~/path/to/your/project/

Promise it'll get smoother Real Soon Now.

Using Python-Brightkite

Python-Brightkite is pretty straightforward to use, although the ridiculous quantity of APIs exposed by Brightkite means that you may have to wade around looking for the right API.

>>> from bk import Brightkite
x >>> x = Brightkite("lethain","some-password")
>>> x.friends()
{'friends': {'person': {'last_checked_in_as_words': {'value': '9 days'}, 'last_active_at': {'type': {'value': 'datetime'}, 'value': '2008-11-07T03:44:03Z'}, 'tiny_avatar_url': {'value': 'http://s3.amazonaws.com/bk_store/images/user/avatar/878f74269a5211dd9951003048c10834/avatar-tiny.png'}, 'value': '\n    ', 'last_checked_in': {'type': {'value': 'datetime'}, 'value': '2008-11-07T03:44:03Z'}, 'small_avatar_url': {'value': 'http://s3.amazonaws.com/bk_store/images/user/avatar/878f74269a5211dd9951003048c10834/avatar-small.png'}, 'place': {'name': {'value': 'Middlefield Station (1)'}, 'display_location': {}, 'longitude': {'type': {'value': 'float'}, 'value': '-122.051959'}, 'value': '\n      ', 'latitude': {'type': {'value': 'float'}, 'value': '37.39607'}, 'scope': {'value': 'country'}, 'id': {'value': '522478caac7e11dd979a003048c0801e'}}, 'smaller_avatar_url': {'value': 'http://s3.amazonaws.com/bk_store/images/user/avatar/878f74269a5211dd9951003048c10834/avatar-smaller.png'}, 'fullname': {'value': 'Yashh'}, 'login': {'value': 'yashh'}}, 'type': {'value': 'array'}, 'value': '\n  '}}
>>> results = x.places_search("new york")
>>> results
{'place': {'name': {'value': 'New York City'}, 'display_location': {}, 'longitude': {'type': {'value': 'float'}, 'value': '-74.005973'}, 'value': '\n  ', 'latitude': {'type': {'value': 'float'}, 'value': '40.714269'}, 'scope': {'value': 'city'}, 'id': {'value': 'ede07eeea22411dda0ef53e233ec57ca'}}}
>>> results['place']
{'name': {'value': 'New York City'}, 'display_location': {}, 'longitude': {'type': {'value': 'float'}, 'value': '-74.005973'}, 'value': '\n  ', 'latitude': {'type': {'value': 'float'}, 'value': '40.714269'}, 'scope': {'value': 'city'}, 'id': {'value': 'ede07eeea22411dda0ef53e233ec57ca'}}
>>> results['place']['id']
{'value': 'ede07eeea22411dda0ef53e233ec57ca'}
>>> results['place']['id']['value']
'ede07eeea22411dda0ef53e233ec57ca'

So, those are two of the twenty four APIs implemented thus far in Python-Brightkite. For all of them we are directly translating the XML files into Python datastructures, so you'll need to look at the returned datastructure or raw XML to figure out the structure of the returned data for each API.

Limitations of Python-Brightkite

We've actually managed to implement a majority of the functionality exposed by the Brightkite restful API, but there are a few minor blips, and one major blop.

Specifically, all the data retrieval commands have been tested, but the creation/deletion/updating aspects haven't really been trialed much (but the code is pretty much drop-dead simple, so fixing any problems shouldn't take more than a few minutes after they've been identified).

The one big missing piece of functionality is using OAuth for authentication. Since the only other option is HTTP Basic Auth, that's currently all that Python-Brightkite supports. Before Python-Brightkite can be used seriously, this limitation will need to be resolved. If you're interested in looking into that, there are resources like this snippet and this library. I might marshall the necessary mental resources next weekend if it hasn't been resolved by then.

A Few Notes on Implementation

This was a pretty fun and quick project to put together (keeping in mind that Yashh contributed to the project as well). After trying out XML::Simple, I am pretty convinced that converting XML into native datastructures is the right way to deal with XML unless performance is a big deal for your use case.

Using xml2dict (which I first saw in Vik Singh's library for using Yahoo! BOSS), which translates XML documents into Python datastructures, along with httplib2 made the foundation of the app pretty short and sweet. (Be kind and ignore the awfulness of the _unescape_uri method. It was the ten second fix to realizing I had escaped too aggressively, but wanted to see if the overall library would work.)

def _unescape_uri(self, uri):
    return uri.replace("%3A",":").replace("%3F","?").replace("%26","&").replace("%3D","=")

def _get(self, uri):
    "Fetch content via the GET method. Returns body of returned content."
    uri = self._unescape_uri(uri)
    header, content = self.http.request(uri, "GET")
    return content

def _post(self, uri, content={}):
    uri = self._unescape_uri(uri)
    header, content = self.http.request(uri, "POST", body=content)
    return content

def _delete(self, uri):
    uri = self._unescape_uri(uri)
    header, content = self.http.request(uri, "DELETE")
    return content

def _convert_xml(self, xml):
    "Stub method."
    try:
        return self.xml.fromstring(xml)
    except ExpatError:
        msg = "Couldn't parse response from Brightkite API."
        raise BrightkiteException(msg, xml)

Built on those five methods, the API calls are all very simple. Here are a pair of representative ones:

def friends(self, username=None):
    "Fetch friends for specified user, or self if no user specified."
    username = username or self.user
    uri = "http://brightkite.com/people/%s/friends.xml" % username
    return self._convert_xml(self._get(quote(uri)))

def checkin(self, place_hash):
    "Checkin at given specified position."
    uri = "http://brightkite.com/places/%s/checkins" % place_hash
    self._post(uri)

Really, that's all there is to the implementation other than the constructor and the implementation of the http and xml properties.

I can remember thinking, some time back, about how sweet it was that people put together wrapper libraries around APIs like Amazon's or Facebook's. Then I started to realize that implementing a wrapper API is mostly a matter of time, not talent. Now that I've played around with a couple, I've started realizing that even further it's less a matter of time, and more a matter of just doing it. (Although, maintaining wrapper libraries will try to kill you.)

If we could just find a universal format for describing APIs, then we'd be able to just bypass writing wrapper libraries altogther. That way, even if the developers didn't write the API spec, it would just take a couple of developers a couple of hours to write it up, and then we'd have pretty wrapper libraries for even the most bleeding edge of APIs. So much possibility, so little time.

All Rights Reserved, Will Larson 2007 - 2014.