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 rulesso, 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.
You can see, get and fork the code on Github.