Jun 22, 2012

Web API and Metaprogramming

In the last week I hacked a little with the preview of Diablo 3 web API . My aim was to produce a simple python client. I started the old way, parsing the JSON data the service returned and building Classes for each element. Then I realized I wasn't doing it not pythonic nor DRY. So I started to build generic objects that could hold the data and take the shape of each element. The basic idea is to read the maps and convert them in to python object structure. The fulcrum is this:

 class ApiObject(object):  
   def __init__(self, arg, host, battle_id):  
       self.__dict__.update(self.fetch_map(arg))  

In this way I can update the dict internal representation of the newly created object. the fetch_map method
recursively create other ApiObject for nested dictionaries, lists of ApiObject for... lists or plain attibutes for key-value elements.
An issue I early discovered is that some data were ref to other elements or resources exposed through a different service. I didn't want to fetch tons of may be useless data so I extended the ApiObject to implement a lazy behavior for some attributes:
 class LazyObject(ApiObject):  
   hydrate = False  
   http_client = requests  
   def http_client_callback(self):  
     pass  
   def __getattribute__(self, name):  
     if not object.__getattribute__(self, 'hydrate') and name in object.__getattribute__(self, 'lazy_load_attrs')():  
       data = self.http_client_callback()  
       self.__dict__.update(self.fetch_map(data))  
       object. __setattr__(self, 'hydrate', True)  
     return object.__getattribute__(self, name)  

So that if the client code tries to access a lazily loaded attribute and the object is not yet 'hydrated'  the remote call is performed and lazy data loaded. It keeps the concrete implementation of the object simple and tight to his own needs

 class Hero(LazyObject):  
    def lazy_load_attrs(self):  
     return ['skills', 'items', 'followers', 'progress']  
   def http_client_callback(self):  
     return load_hero(self.host, self.battle_id, self.name, http_client=self.http_client)  

Final mention to the "special cases". Now...
Special cases are never special enough to break the rules 
so, lets create rules for special cases!

The rule is:
If the ApiObject implementation has a method named "manage_[key in the dict]" is  as special cases and that method should be used to decode/deserialize that entry. 

I'm happy of the code right now. It has decent integration test against specs from the Diablo 3 web API, is flexible enough is those specs change in near future and is totally PEP-8 compliant.

And checked by Travis-CI too!

You can see, get and fork the code on Github.

No comments: