Bottle is my favorite python web framework, it is lightweight and fast. Those are good qualities, especially when you need to write web services.
However it misses some nice features of other “bigger” framework. One of them is a nice way to handle and validate query parameters.
To make an example, lets suppose that your route function expect the GET parameters a, b and c. In bottle you will have to write something like this:
from bottle import route, request @route('/test/') def test(): a = int(request.GET.get("a")) b = float(request.GET.get("b")) c = request.GET.get("c").upper() # logic go here return "ok"
As you can see we have tree line of boilerplate code that will reduce the focus from the real logic of the function. The situation will be further complicated if we want to have some of this parameter required and others optional with a fallback default value and if we want to handle the errors in a proper way.
While one of my web service requires to get some parameters from the query string, I decide to write down this decorator:
""" Copyright (c) 2011, Giuseppe Tribulato. License: MIT (see http://www.opensource.org/licenses/mit-license.php for details) """ from bottle import route, request import functools import inspect def checkParams(**types): def decorate(f): farg, _, _, def_params = inspect.getargspec(f) if def_params is None: def_params = [] farg = farg[:len(farg) - len(def_params)] param_info = [(par, ptype, par in farg) for par, ptype in types.iteritems()] @functools.wraps(f) def wrapper(*args, **kargs): getparam = request.GET.get for par, ptype, required in param_info: value = getparam(par) if not value: # None or empty str if required: error = "%s() requires the parameter %s" % (wrapper.__name__, par) raise TypeError(error) continue try: kargs[par] = ptype(value) except: error = "Cannot convert parameter %s to %s" % (par, ptype.__name__) raise ValueError(error) return f(*args, **kargs) return wrapper return decorate
To see how it works, lets rewrite the previous example using checkParams:
import string @route('/test/') @checkParams(a = int, b = bool, c = string.upper) def test(a, b, c): return dict(a = a, b = b, c = c)
Inside checkParams we are declaring that we want to handle the GET parameters a, b and c with the provided conversion functions and we expect such parameters to be passed to our decorated function test.
We can test it, pointing the browser to our server (in my case on localhost:8080) with the following url:
http://localhost:8080/test/?a=10&b=1&c=test
this will output
{"a": 10, "c": "TEST", "b": true}
The parameters were parsed, converted and passed to our test function as expected. I define all the three parameters of the function test as required. Lets take a look to what happens removing one of the parameters from the query string:
http://localhost:8080/test/?a=10&b=1
if you set bottle.debug(True) this will output:
Internal Server Error
TypeError('test() requires the parameter c',)
If I want to define c as optional, I can give to it a default value:
@route('/test/') @checkParams(a = int, b = bool, c = string.upper) def test(a, b, c = "default"): return dict(a = a, b = b, c = c)
This time we get a valid response:
{"a": 10, "c": "default", "b": true}
Finally lets try what happens if we pass an invalid type to a:
http://localhost:8080/test/?a=no&b=1
this will output:
Internal Server Error
ValueError('Cannot convert parameter a to int',)
That’s it! I hope that this snippet can be useful to someone.
