APItoPy – a Pythonic way to access HTTP APIs

How apitopy came to be

I recently had the need to create a Python client for an HTTP API (to be more specific, for Sprint.ly).

There was an existing implementation which did not work for us (it was failing due to authentication problems) based on urllib2. I knew better and was considering rewriting the client using requests.

At the same time, for a personal project I was working on a tool that will eventually need to talk to multiple APIs. So I thought that taking advantage of some nice dynamic Python features I could write a quick universal HTTP API client that still looked like accessing Python objects to the casual user.

This is roughly how I wanted the Python API to look (these examples are made-up and not based on any service I know of):

api = Api('http://api.example.com', ...)
data = api.people[24].projects(status='cancelled')
print(data[0].name)

This should generate a GET request to http://api.example.com/people/24/projects?status=cancelled and print the name of the first cancelled project.

An hour of hacking later and thanks to Python’s great __getattr__ and __getitem__ I had a working client that looked like a bespoke implementation to the casual eye. And it worked better than the bespoke client I was trying to use in the first place. The whole library needs a few more than 100 lines of code (not counting tests).

This is an example usage accessing the Sprint.ly API:

from apitopy import Api

sprintly = Api('https://sprint.ly/api/', (USER, TOKEN),
                verify_ssl_cert=False, suffix='.json')
# initialise the API. Sprint.ly does not honor content negotiation,
# you must add the ".json" suffix to API requests

product = sprintly.products[9122]
# generates an endpoint https://sprint.ly/api/products/9122
# but doesn't perform any HTTP request yet

items = product.items(assigned_to=2122, status='in-progress')
# HTTP GET https://sprint.ly/api/products/9122/items.json?assigned_to=2122&status=in-progress
# Returns a list of parsed JSON objects

for item in items:
    print(u"#{number:<4} {type:8} {status:12} {title:40}".format(
        **item
    ))

Some Internals

The key to implementing access to arbitrary attributes in a Python class is to override __getattr__. __getattr__ gets called when the interpreter does not find an attribute in the usual places, meaning it is the fallback method before throwing an AttributeError.

In order to implement a custom access to items (var[index]) you must implement __getitem__.

Our implementation of EndPoint makes use of __getattr__ and __getitem__, by making them behave the same. This allows for using the [] notation when you want to access a part of the URL that will vary in your code (and you want to populate from a variable) or that contains characters that are not valid as a Python identifier (e.g. all numbers). I find that numbers, which typically represent object identifiers, fit the [] notation better anyway in this case.

class EndPoint(object):
    """
    A potential end point of an API, where we can get JSON data from.

    An instance of `EndPoint` is a callable that upon invocation performs
    a GET request. Any kwargs passed in to the call will be used to build
    a query string.
    """

    def __init__(self, api, path, suffix=''):
        self.api = api
        self.path = path
        self.suffix = suffix

    def __getitem__(self, item):
        return EndPoint(self.api,
                        '/'.join([self.path, str(item)]),
                        self.suffix)

    def __getattr__(self, attr):
        return self[attr]

    def __call__(self, **kwargs):
        extra = ''
        if kwargs:
            url_args = ['{0}={1}'.format(k, v) for k, v in kwargs.items()]
            extra = '?' + '&'.join(url_args)
        return self.GET("{0}{1}{2}".format(self.path, self.suffix, extra))

    def GET(self, url):
        response = self.api.GET(url)
        return dotify(response.json())

Get it, use it, and send your feedback

You can find the whole library in Github

It’s in PyPI, so installation is easy:

pip install apitopy

Further Work

I’m specially interested in extending apitopy to support other HTTP verbs (at least POST, PUT, DELETE) and still thinking of the best syntax for that. How would you like that code to look like, from the point of view of the client? Suggestions are welcome.

Another open point is how to make autocompletion work on a shell like ipython. That would be a killer feature, it would need to rely on properly hyperlinked discoverable APIs. I’m all up for hearing your opinions on this as well.

Leave a comment