Building a web-based API with Turbogears2
I am using TurboGears2 to export a python API over the web. Every API method is wrapper by a controller method that validates the parameters and returns the results encoded in JSON.
The basic idea is this:
@expose("json") def list_colours(self, filter=None, productID=None, maxResults=100, **kw): # Call API res = self.engine.list_colours(filter, productID, maxResults) # Return result return res
To validate the parameters we can use forms, it's their job after all:
class ListColoursForm(TableForm): fields = [ # One field per parameter twf.TextField("filter", help_text="Please enter the string to use as a filter"), twf.TextField("productID", help_text="Please enter the product ID"), twf.TextField("maxResults", validator=twfv.Int(min=0), default=200, size=5, help_text="Please enter the maximum number of results"), ] list_colours_form=ListColoursForm() #... @expose("json") @validate(list_colours_form, error_handler=list_colours_validation_error) def list_colours(self, filter=None, productID=None, maxResults=100, **kw): # Parameter validation is done by the form # Call API res = self.engine.list_colours(filter, productID, maxResults) # Return result return res
All straightforward so far. However, this means that we need two exposed methods for every API call: one for the API call and one error handler. For every API call, we have to type the name several times, which is error prone and risks to get things mixed up.
We can however have a single error handler for all methonds:
def get_method(): ''' The method name is the first url component after the controller name that does not start with 'test' ''' found_controller = False for name in pylons.c.url.split("/"): if not found_controller and name == "controllername": found_controller = True continue if name.startswith("test"): continue if found_controller: return name return None class ValidatorDispatcher: ''' Validate using the right form according to the value of the "method" field ''' def validate(self, args, state): method = args.get("method", None) # Extract the method from the URL if it is missing if method is None: method = get_method() args["method"] = method return forms[method].validate(args, state) validator_dispatcher = ValidatorDispatcher()
This validator will try to find the method name, either as a form field
or by parsing the URL. It will then use the method name to find the form to use
for validation, and pass control to the
validate method of that form.
We then need to add an extra "method" field to our forms, and arrange the forms inside a dictionary:
class ListColoursForm(TableForm): fields = [ # One hidden field to have a place for the method name twf.HiddenField("method") # One field per parameter twf.TextField("filter", help_text="Please enter the string to use as a filter"), #... forms["list_colours"] = ListColoursForm()
And now our methods become much nicer to write:
@expose("json") def api_validation_error(self, **kw): pylons.response.status = "400 Error" return dict(form_errors=pylons.c.form_errors) @expose("json") @validate(validator_dispatcher, error_handler=api_validation_error) def list_colours(self, filter=None, productID=None, maxResults=100, **kw): # Parameter validation is done by the form # Call API res = self.engine.list_colours(filter, productID, maxResults) # Return result return res
api_validation_error is interesting: it returns a proper HTTP error status,
and a JSON body with the details of the error, taken straight from the form
validators. It took me a while to find out that the form errors are in
pylons.c.form_errors (and for reference, the form values are in
pylons.response is a WebOb Response that we can play with.
So now our client side is able to call the API methods, and get a proper error if it calls them wrong.
But now that we have the forms ready, it doesn't take much to display them in web pages as well:
def _describe(self, method): "Return a dict describing an API method" ldesc = getattr(self.engine, method).__doc__.strip() sdesc = ldesc.split("\n") return dict(name=method, sdesc = sdesc, ldesc = ldesc) @expose("myappserver.templates.myappapi") def index(self): ''' Show an index of exported API methods ''' methods = dict() for m in forms.keys(): methods[m] = self._describe(m) return dict(methods=methods) @expose('myappserver.templates.testform') def testform(self, method, **kw): ''' Show a form with the parameters of an API method ''' kw["method"] = method return dict(method=method, action="/myapp/test/"+method, value=kw, info=self._describe(method), form=forms[method]) @expose(content_type="text/plain") @validate(validator_dispatcher, error_handler=testform) def test(self, method, **kw): ''' Run an API method and show its prettyprinted result ''' res = getattr(self, str(method))(**kw) return pprint.pformat(res)
In a few lines, we have all we need: an index of the API methods (including their documentation taken from the docstrings!), and for each method a form to invoke it and a page to see the results.
Make the forms children of AjaxForm, and you can even see the results together with the form.