- PVSM.RU - https://www.pvsm.ru -

Bottle и плагины

Введение

Bottle — это мини-фреймворк для Python, позволяющий писать веб-приложения с высокой скоростью.

Вот только слово «мини» добавляет ограничения, например, здесь нет быстрого способа создать административную панель. Если нужна работа с БД, то ее надо подключать отдельно. Таким образом, bottle — это инструмент для написания линейных web-приложений, которые не требуют слишком сильного взаимодействия между элементами приложения.

Если вам надо написать handler, который будет принимать ссылку на файл, а потом скачивать его в s3 с какой-то обработкой, то для проверки функционала bottle отлично подойдет.

Для работы с bottle достаточно описывать сами обработчики, например:

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

(Пример из документации [1].)

При написании более смысловых функций (например, телефонная книга с сохранением в БД), очень быстро возникает необходимость работы то с БД, то с кэшем, то с сессиями. Это порождает необходимость пихать функционал работы с БД в сам обработчик, затем выносить в отдельные модули, чтобы не дублировать код. А после этого код CRUDL для разных объектов переписываем в виде что-то типа мета-функций.

Но можно пойти и по другому пути: начать использовать bottle plugin [2]. О механизме плагинов и пойдет речь в этой публикации.

О плагинах bottle

В Python есть сильный механизм расширения возможностей функции без переписывания — декораторы [3].

Этот же механизм применяется в качестве основного для плагинов.

По сути, плагин — это декоратор, который вызывается для каждого обработчика, когда на него падает запрос.

Можно даже написать такой код и он будет считаться за плагин:

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)

(Пример из Документации [4].)

Однако, лучше писать плагины согласно интерфейсу, описанному в документации [5].

В возможности плагина входит:

  • Получение информации о входящем запросе
    • какой URL вызван
    • содержание HTTP-запроса, т.е. все о запросе

  • Формирование выходного запроса
    • можно изменить HTTP-заголовок
    • добавить свою переменную
    • установить свое содержание ответа (хоть пустое)

Иными словами, плагины являются инструментом полного контроля над обработкой запроса.

Как использовать плагин

Здесь не буду перепечатывать плагин bottle-sqlite [6], а вот само использование достойно внимания:

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

(Пример из Документации [4].)

В примере показано, как устанавливать плагин, а также как использовать. Это то, о чем писал выше. При использовании плагина, появляется возможность включить в сам обработчик объект (в данном случае db — sqlite БД), который спокойно можно использовать.

Рассмотрев примеры из документации, перейду к реальному применению.

Use cases к использованию плагинов

Первым вариантом использования можно назвать проброс какого-то объекта к самому обработчику. Это можно увидеть в примере использования bottle-sqlite (см. выше код).

Вторым вариантом можно назвать такой.

При написании web-приложения в команде разработчика могут сложиться некоторые соглашения по принимаемым и возвращаемым типам данных.

Дабы далеко не ходить, приведу вымышленный код:

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)

То есть в команде пришли к соглашению, что возвращаемым типом будет json. Можно каждый раз дублировать строки:

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

В этом вроде ничего страшного, да только мозолят одни и те же строки от функции к функции. А если это «ничего страшного» длится не две строки, а десять? В этом случае могут спасти плагины, пишем элементарную функцию:

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

(Остальной кусок плагина не буду приводить, ибо он очень похож на sqlite.)

И уменьшаем количество кода.

Пойдем дальше. В соглашениях мы условились не только отдавать в json, но и принимать. Было бы замечательно не только проверить HTTP-заголовок на тип, но и проверить существование определенных ключей. Это можно сделать, например, так:

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)

И применять примерно так:

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

Плагин сам проверит существование ключей в json, а затем еще и ответ обернет в json.

Вариантов использования, конечно, больше, как и с декораторами. Зависит от того, кто и как придумывает их применять.

Существующие плагины

Список плагинов для bottle не очень обширен [2].

На github можно найти плагины для управления сессией, i18n, facebook, matplotlib, cql, логгирования, регистрации и авторизации. Однако их количество в значительной мере уступает flask и django.

Выводы

Bottle-плагины позволяют уменьшить количество дублирования кода, вытащить общие проверки (такие, как «авторизован ли пользователь») в общее место, расширить функционал и создать модули, которые можно использовать повторно.

Автор: WarmongeR

Источник [7]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/python/83200

Ссылки в тексте:

[1] документации: http://bottlepy.org/docs/dev/index.html

[2] bottle plugin: http://bottlepy.org/docs/dev/plugins/index.html

[3] декораторы: http://habrahabr.ru/post/187482/

[4] Документации: http://bottlepy.org/docs/dev/plugindev.html

[5] документации: http://bottlepy.org/docs/dev/plugindev.html#plugin-api

[6] bottle-sqlite: http://bottlepy.org/docs/dev/plugindev.html#plugin-example-sqliteplugin

[7] Источник: http://habrahabr.ru/post/250831/