linode-api-python-part-1

05 Jun 2008

Ataraxia Consulting


The great Linode is preparing an api for customers to manipulate their account and twiddle their dns zone settings etc. Apparently I was supposed to write the Python bindings, the reality is somewhere between volunteer and victim I suspect. So I spent a little bit time working up an example how they might work.

A little background on the API, it is generously simple, you can submit via GET or POST, send your api_key in HTTP auth or in the GET/POST, and you can get results in JSON/WDDX/Human readable formats. It’s still a bit in flux as far as what methods are available etc. The bindings are deliciously simple (if I do say so myself), however they rely on a certain amount of evil to the casual python fan. When designing the python bindings I wanted a way to quickly add and remove api methods (re: flux), but also minimize the copy and paste effort required. The flow for a request boils down to create an http request, determine the action (the api method), append necessary state vars, submit, read response, return json decoded result. The system I’ve devised uses the powerful python decorators to achieve this.

Here’s an example of how a method is defined in the api:

@__api_request
def domainList(self, request):
  """ Retrieve the list of DNS Domains Associated with this API Key """
That's it! You can now call this method and successfully submit an api request...
api = Api(key)
print api.domainList()

Couple of notes about the above method definition:

The privateness of the decorator relates to another hack utilized elsewhere to determine the list of available api methods quickly (the reason for this will be in the follow up post) Because there’s no other real instructions to this method the doc string declaration is important so as not to annoy the python block definitions (stupid whitespace idiocy [I’m more of a ruby lover at heart]) You may ask, how is it exactly that you achieve such simple definitions of methods? Python decorators of course! Decorators are a way of manipulating a function/method before the actual function/method is invoked. Let’s look at the definition of __api_request

def __api_request(func):
  def decorator(self, *__args, **__kw):
    request = {'action' : func.__name__}
    for k in __kw: request[k] = __kw[k]
    if len(__args) == 1:
      for k in __args[0]: request[k] = __args[0][k]
    result = func(self, request)
    if result is not None:
      request = result
    return self.__send_request(request)
  return decorator

You’ll notice that the input to the method __api_request is the method we’re decorating, and what’s returned is a separate callable method that we’ve defined dynamically. Oh the wonder of it all! There’s more than just your normal bit of trickery going on here, if you wanted to determine the number of arguments that were passed to domainList() you’d change the parameters for “def decorator”, and the number and types of parameters don’t even have to match what we defined as domainList() anyway. In fact, we defined domainList initially to take self and request, however we called it with no parameters. That’s why my decorator function uses *__args and **__kw, it’s a heavy handed var_args for python.

The rest of the decorator function relates to how the internals of generating an api request work, it translates func.__name__ into the action to be performed, takes any named parameters [method(ParamName=foo)] adds them the dictionary (it also looks for 1 dictionary argument that may or may not be present and adds any of those parameters to the request, the reason for this will be discussed in a later blog). The decorator then calls the original method definition, checks to see if anything was returned, and then calls the base __send_message and returns that result to the callee. Phew!

The reason the decorator calls the original method is to allow for some input sanitization and other sanity checks before allowing the request to go out. You could add default values or check for more complex relationships between required parameters. Speaking of required parameters, you can also use another decorator to achieve that:

def __api_required(*args, **kw):
  def decorator(func):
    def wrapper(*__args,**__kw):
      for k in args:
        if not __kw.has_key(k) and (len(__args) == 2 and not __args[1].has_key(k)):
          raise MissingRequiredArgument(k)
      return func(*__args,**__kw)
    return wrapper
  return decorator

Checks the named parameter list (and the odd regular argument) for the required parameters and raises an exception if they’re not found, pretty snazzy. To use it:

@__api_required('DomainID')
@__api_request
def domainGet(self, request):
  """ Get the set DNS information for a specific DomainID """
You call it with:

api.domainGet(DomainID=10)

If you’re interested in seeing the rest of the code you can visit http://github.com/tjfontaine/linode-python or clone it yourself with git clone git://github.com/tjfontaine/linode-python.git

I learned about using python decorators by reading this Dr. Dobbs article, and by looking at more examples.