A Minimalistic Rest Client
I needed a simple REST client to test a web service that I am developing with Django and TastyPie. Although TastyPie can easily handle a variety of formats (JSON, XML, HTML, …) I’m primarily interested in JSON.
So I needed a simple client that would implement the four basic HTTP request types (GET, PUT, POST, DELETE) and would be simple enough to use with JSON data payload.
Essentially, I needed a simple wrapper around httplib
that simplified the HTTP calls, and allowed me to do something along those lines:
result = client.put('/api/resource/', data_json=json.dumps(request_data))
result = client.delete('/api/resource/')
You can find the source code of this library (which I named miniREST) on GitHub. It is also available on PyPi and can be installed with PIP package manager.
What I wanted to show you here is how to handle calls to object methods, that aren.t actually defined as class functions.
Let me explain what I mean. Going back to the REST client example, I might have defined a class and four methods implementing four different HTTP calls: GET, PUT, POST and DELETE. Maybe something like this:
class RESTClient:
def get(self, uri):
result = httplib('GET', uri)
# do some data parsing
return result_data
def put(self, uri, data):
result = httplib('PUT', uri, body=data)
# do some data parsing
return result_data
def post(self, uri, data):
result = httplib('POST', uri, body=data)
# do some data parsing
return result_data
def delete(self, uri):
result = httplib('DELETE', uri)
# do some data parsing
return result_data
This would work, but it doesn’t feel quite right. You would be replicating effectively the same code four times. If you find a bug or want to make an improvement in the result data parsing you’ll have to make changes in four different functions. This can lead to an inconsistent code behaviour if you forget to change one function or make a mistake while changing it.
My approach to this problem was to create a generic method that is able to handle all four HTTP requests. Yet I wanted to be able to call GET, PUT, POST and DELETE as object methods . I think it’s more readable.
So I’m going to make use of the built-in method __getattr__
. This method is called every time if an attribute is not found using the usual methods, for example by looking up the object’s __dict__
attribute, which contains all attributes of an object as a dictionary.
The __getattr__
method should return either an attribute value or raise an AttributeError exception. This exception will be raised automatically if you try to reference a non-existant attribute and the __getattr__
is not defined:
>>> class C:
... def __init__(self):
... pass
...
>>> o = C()
>>> o.foo
Traceback (most recent call last):
File "", line 1, in
AttributeError: C instance has no attribute 'foo'
However if the __getattr__
is defined it will be called instead:
>>> class C:
... def __init__(self):
... self.bar = 1
... def __getattr__(self, attribute):
... print "You're trying to read %s, but it's not initialised yet" % attribute
... return 0
...
>>> o = C()
>>> r = o.bar
>>> print r
1
>>> r = o.foo
You're trying to read foo, but it's not initialised yet
>>> print r
0
OK, we’re getting there now. But there is a slight problem. The __getattr__
returns a computed value of an attribute. It’s easy for variables, but we want to reference a function, so __getattr__
needs to return a reference to a method and not the computed value.
This is easy, but our function would not know under what name it has been called:
>>> class C:
... def _my_func(self):
... print "in a function"
... def __getattr__(self, attr):
... return self._my_func
...
>>> o = C()
>>> o.test()
in a function
>>> o.foo()
in a function
Because we return a reference to a function (as opposed to actually calling it and returning a result) we can.t pass any arguments to it that would tell under what name it’s been called.
One of the easiest ways to solve this is to store the name as an object attribute and then look it up from the function:
>>> class C:
... def __init__(self):
... self.func_name = None
... def _my_func(self):
... print "Function called as '%s'" % self.func_name
... def __getattr__(self, attr):
... self.func_name = attr
... return self._my_func
...
>>> o = C()
>>> o.foo()
Function called as 'foo'
>>> o.bar()
Function called as 'bar'
And this is exactly the behaviour I was after. Now it’s an exercise to the reader . can you think of any problems that this approach may cause?