Bottle and plugins

Introduction


Bottle is a mini-framework for Python that allows you to write web applications with high speed.

Just the word “mini” adds limitations, for example, there is no quick way to create an administrative panel. If you need to work with the database, then it must be connected separately. Thus, a bottle is a tool for writing linear web applications that do not require too much interaction between application elements.

If you need to write a handler that will take a link to a file, and then download it to s3 with some kind of processing, then bottle is perfect for checking the functionality.

To work with bottle, it is enough to describe the handlers themselves, for example:

from bottle import route, run, template
@route('/hello/')
def index(name):
    return template('Hello {{name}}!', name=name)
run(host='localhost', port=8080)

(An example is from the documentation .)

When writing more semantic functions (for example, a phone book with saving to the database), very quickly the need arises to work either with the database, then with the cache, or with the sessions. This gives rise to the need to shove the functionality of working with the database into the processor itself, then put it into separate modules so as not to duplicate the code. And after that we rewrite the CRUDL code for different objects in the form of something like meta-functions.

But you can go another way: start using a bottle plugin . The mechanism of plugins will be discussed in this publication.

About bottle plugins


Python has a powerful mechanism for expanding the capabilities of a function without rewriting - decorators .

The same mechanism is used as the main one for plugins.

In essence, a plugin is a decorator that is called for each handler when a request falls on it.

You can even write such code and it will be considered as a plugin:

from bottle import response, install
import time
def stopwatch(callback):
    def wrapper(*args, **kwargs):
        start = time.time()
        body = callback(*args, **kwargs)
        end = time.time()
        response.headers['X-Exec-Time'] = str(end - start)
        return body
    return wrapper
install(stopwatch)

(An example from the Documentation .)

However, it is better to write plugins according to the interface described in the documentation .

The plugin features include:

  • Retrieving Incoming Request Information
    • which URL is caused
    • The content of the HTTP request, i.e. all about the request

  • Formation of an output request

    • can change the HTTP header
    • add your variable
    • set your response content (even if empty)



In other words, plugins are an instrument of complete control over request processing.

How to use the plugin


I will not retype the bottle-sqlite plugin here, but the use itself is noteworthy:

sqlite = SQLitePlugin(dbfile='/tmp/test.db')
bottle.install(sqlite)
@route('/show/:page')
def show(page, db):
    row = db.execute('SELECT * from pages where name=?', page).fetchone()
    if row:
        return template('showpage', page=row)
    return HTTPError(404, "Page not found")
@route('/admin/set/:db#[a-zA-Z]+#', skip=[sqlite])
def change_dbfile(db):
    sqlite.dbfile = '/tmp/%s.db' % db
    return "Switched DB to %s.db" % db

(An example from the Documentation .)

The example shows how to install the plugin, as well as how to use it. This is what I wrote above. When using the plugin, it becomes possible to include an object (in this case, db - sqlite database) in the handler itself, which you can safely use.

Having considered examples from the documentation, I will proceed to the actual application.

Use cases for using plugins


The first use case is the forwarding of some object to the handler itself. This can be seen in the bottle-sqlite example (see above code).

The second option can be called this.

When writing a web application in the development team, some agreements can be formed on the received and returned data types.

In order not to go far, I will give a fictional code:

@route('/report/generate/:filename')
def example_handler(filename):
    try
        result = generate_report(filename)
    except Exception as e:
        result = {'ok': False, 'error': str(e)}
    response.content_type = 'application/json'
    return json.dumps(result)

That is, the team agreed that the return type would be json. You can duplicate the lines every time:

response.content_type = 'application/json'
return json.dumps(result)

This seems to be okay, but the same lines from function to function are callous. And if this “nothing wrong” lasts not two lines, but ten? In this case, plugins can save, we write an elementary function:

def wrapper(*args, **kwargs):
    response.content_type = 'application/json'
    return json.dumps(callback(*args, **kwargs))

(I will not give the rest of the plugin, because it is very similar to sqlite.)

And reduce the amount of code.

Let's go further. In the agreements, we agreed not only to give in json, but also to accept. It would be great not only to check the HTTP header for a type, but also to check for the existence of certain keys. This can be done, for example, like this:

def wrapper(*args, **kwargs):
    def gen_error(default_error, text_msg):
        res = default_error
        res.update(dict(error=text_msg))
        return res
    if request.get_header('CONTENT_TYPE', '') == 'application/json':
        if request.json:
            not_found_keys = []
            not_found_keys_flag = False
            for key in keys:
                if key not in request.json:
                    not_found_keys.append(key)
                    not_found_keys_flag = True
            if not_found_keys_flag:
                wr_res = gen_error(self.default_error,
                                   'Not found keys: | %s | in request' % ', '.join(not_found_keys))
            else:
                wr_res = callback(*args, **kwargs)
        else:
            wr_res = gen_error(
                self.default_error, 'Not found json in request')
    else:
        wr_res = gen_error(
            self.default_error, 'it is not json request')
    response.content_type = 'application/json'
    return json.dumps(wr_res)

And apply something like this:

@route('/observer/add',
           keys=['name', 'latitude', 'longitude', 'elevation'])
def observer_add():
    return set_observer(request.json)

The plugin itself will check for the existence of keys in json, and then it will also wrap the response in json.

Of course, there are more use cases, as with decorators. Depends on who and how invents them to apply.

Existing plugins


The list of plugins for bottle is not very extensive .

On github you can find plugins for session management, i18n, facebook, matplotlib, cql, logging, registration and authorization. However, their number is significantly inferior to flask and django.

conclusions


Bottle-plugins allow you to reduce the amount of code duplication, pull out general checks (such as “whether the user is authorized”) in a common place, expand the functionality and create modules that can be reused.

Also popular now: