Wednesday, March 17, 2010

Use data to do the work for you

When trying to use complex objects in with a remote API, I've noticed that there is a slow way to use the objects and a fast way. If you wanted to create one of these objects, you might not know the exact construction of this object. It might help to have an example of the object. Let's say the API gives you the ability to Read, Create, and Update these objects. You yourself only have client access to the api through python.

What you could do is try inspecting the object:
>>> obj = myApiAccess.getFoo(id=123)
>>> print dir(obj)
['__doc__', '__module__', 'id', 'name', 'description']

Let's try constructing the object and use the create method of the API since you want to create your own object without the UI:
>>> nextObj = Foo()
>>> nextObj.id = 123
>>> nextObj.name = "super cool object"
>>> nextObj.description = "created the slow way"
>>> myApiAccess.createFoo(nextObj)

This is fine for simple objects. But when you have complex nested objects, you will not be able to manually construct objects like this without getting frustrated and use slightly duplicated work. Let's say Foo objects now have a list of Bar objects. You would need to go construct each Bar object, then insert the bar object into Foo update Foo's fields, then run the create method.

>>> nextObj = Foo()
>>> nextObj.id = 123
>>> nextObj.name = "super cool object"
>>> nextObj.description = "created the slow way"
>>> barData = ['a','b','c']
>>> barList = []
>>> for b in barData:
barObj = Bar()
barObj.val = b
barList.append(barObj)
>>> nextObj.bars = barList
>>> myApiAccess.createFoo(nextObj)

A better way of doing this is by creating a mechanism to allow you to dump an object from the api to a json-like format and load it back.

You can then use the UI for your product to create example objects of how you want to construct your object. Then get the object and dump it to a file. Edit the field you want to change, then update or create an object based on this template.

When you dump the object it could look something like this:
{"api_class": "Foo",
"id": 123,
"name" : "super cool object",
"description" : "created the fast way",
"bars" : [
{"api_class": "Bar",
"val":"a"
},
{"api_class": "Bar",
"val":"b"
},
{"api_class": "Bar",
"val":"c"
}
]
}

Great! We have abstracted the blueprints of how to construct API objects. Dumping objects like this will involve 2 steps. First you turn the object into a python dictionary. Then you use simplejson to dump the object. Loading the object requires using simplejson to load the object, then you have a reverse parser that recursively creates objects. With this abstract construction module we can do the real work that we care about: interact with the API and create/update objects.

# this is an example creating an object by template

# a module you created to abstract object construction
import objectConstructionModule
# your client module
import clientApi

#initialize api access around here
# ...

fp = open('./FooTemplate')
jsonStr = fp.read()
fp.close()
pythonObj = simplejson.loads(jsonStr)
apiObj = objectConstructionModule.parseObj(pythonObj)
myApiAccess.createFoo(apiObj)

This method alone is useful for testing. You could have a file for each way you want the object to look before running some other test that uses the object.

We can even extend this to large scale testing. What if you wanted to create 1000 Foo objects? You could read in the template and try creating the object over and over. But this could run into constraints like unique names for the object.

To accomplish this, you could alter the json behavior to use a function that randomizes a part of the object that needs to be unique when you parse the object. What you need to do is make your parser understand that a list object could be a hidden function with parameters:

{
"api_class": "Foo",
"id": 123,
"name" : ["< function randName >", 10, "Rand"],
"description" : "created the fast way",
"bars" : []
}

Then in the module the loads the file could have a function randName:


import random
import simplejson
#letters and numbers
alphabet = map(chr,range(48,58))+map(chr,range(65,91))+map(chr,range(97,123))
def randName(length=10, prefix = ""):
global alphabet
retstr = ""
lenAlphabet = len(alphabet)
for x in range(0,length):
retstr = retstr + alphabet[int(random.random()*lenAlphabet)]
return prefix + retstr

fp = open('./FooTemplate')
jsonStr = fp.read()
pythonObj = simplejson.loads(jsonStr)
fp.close()
for x in range(0,1000):
# parseObj calls the randName function each parse
apiObj = objectConstructionModule.parseObj(pythonObj)
myApiAccess.createFoo(apiObj)

Of course this all depends upon your implementation of the json dumper/parser module as well as your API's behavior. But I just thought it was interesting how you can use data and functions to do more work for you. Also its always nice to have a way to inspect objects at a high level. That way the API client user doesn't have to know too much about how the objects are constructed to get started fast.