Spec-Zone.ru › Bottle 0.12

Учебник

В этом учебнике вы познакомитесь с концепциями и функциями веб-фреймворка Bottle, включая базовые и продвинутые темы. Вы можете прочитать его от начала до конца или использовать в качестве справочного материала позже. Также для вас может быть интересна автоматически сгенерированная Ссылка на API. Она содержит больше деталей, но объясняет меньше, чем этот учебник. Решения наиболее распространенных вопросов можно найти в нашей коллекции Рецептов или на странице Часто задаваемых вопросов. Если вам нужна помощь, присоединяйтесь к нашей почтовой рассылке или посетите наш IRC-канал.

Установка

Bottle не зависит от каких-либо внешних библиотек. Вы можете просто загрузить bottle.py в директорию вашего проекта и начать кодирование:

$ wget http://bottlepy.org/bottle.py

Это позволит вам получить последнюю версию снэпшота разработки, которая включает все новые функции. Если вы предпочитаете более стабильную среду, вы должны использовать стабильные релизы. Они доступны на PyPI и могут быть установлены с помощью pip (рекомендуется), easy_install или вашего менеджера пакетов:

$ sudo pip install bottle              # recommended
$ sudo easy_install bottle             # alternative without pip
$ sudo apt-get install python-bottle   # works for debian, ubuntu, ...

В любом случае, вам понадобится Python 2.5 или более поздняя версия (включая 3.x) для запуска приложений Bottle. Если у вас нет прав на установку пакетов в системе или вы просто не хотите этого делать, создайте virtualenv сначала:

$ virtualenv develop              # Create virtual environment
$ source develop/bin/activate     # Change default python to virtual one
(develop)$ pip install -U bottle  # Install bottle to virtual environment

Или, если virtualenv не установлен на вашей системе:

$ wget https://raw.github.com/pypa/virtualenv/master/virtualenv.py
$ python virtualenv.py develop    # Create virtual environment
$ source develop/bin/activate     # Change default python to virtual one
(develop)$ pip install -U bottle  # Install bottle to virtual environment

Быстрый старт: “Hello World”

Этот учебник предполагает, что у вас Bottle либо установлен, либо скопирован в директорию вашего проекта. Начнём с очень простого примера “Hello World”:

from bottle import route, run

@route('/hello')
def hello():
    return "Hello World!"

run(host='localhost', port=8080, debug=True)

Вот и всё. Запустите этот скрипт, посетите http://localhost:8080/hello, и вы увидите “Hello World!” в вашем браузере. Вот как это работает:

Декоратор route() связывает фрагмент кода с путём URL. В данном случае мы связываем путь /hello с функцией hello(). Это называется route (отсюда и название декоратора) и является наиболее важной концепцией этого фреймворка. Вы можете определять столько маршрутов, сколько хотите. Всякий раз, когда браузер запрашивает URL, вызывается связанная функция, а возвращаемое значение отправляется обратно в браузер. Всё так просто.

Вызов run() в последней строке запускает встроенный сервер разработки. Он работает на порту localhost 8080 и обслуживает запросы до тех пор, пока вы не нажмёте Control-c. Вы можете поменять сервер позже, но пока нам нужен только сервер разработки. Он не требует настройки и является невероятно простым способом запуска приложения для локальных тестов.

Режим отладки Debug Mode очень полезен на ранней стадии разработки, но его следует отключить для публичных приложений. Имейте это в виду.

Конечно, это очень простой пример, но он демонстрирует основную концепцию того, как строятся приложения с помощью Bottle. Продолжайте чтение, и вы увидите, что ещё возможно.

Приложение по умолчанию

Для простоты большинство примеров в этом учебнике используют декоратор route() на уровне модуля для определения маршрутов. Это добавляет маршруты в глобальное «приложение по умолчанию», экземпляр Bottle, который автоматически создаётся при первом вызове route(). Несколько других декораторов и функций на уровне модуля связаны с этим приложением по умолчанию, но если вы предпочитаете объектно-ориентированный подход и не возражаете против дополнительного набора текста, вы можете создать отдельный объект приложения и использовать его вместо глобального:

from bottle import Bottle, run

app = Bottle()

@app.route('/hello')
def hello():
    return "Hello World!"

run(app, host='localhost', port=8080)

Объектно-ориентированный подход описан далее в разделе Приложение по умолчанию. Просто имейте в виду, что у вас есть выбор.

Маршрутизация запросов

В последней главе мы создали очень простое веб-приложение с единственным маршрутом. Вот часть маршрутизации примера “Hello World” ещё раз:

@route('/hello')
def hello():
    return "Hello World!"

Декоратор route() связывает путь URL с функцией обратного вызова и добавляет новый маршрут в приложение по умолчанию. Приложение с единственным маршрутом довольно скучно, однако. Добавьте ещё несколько:

@route('/')
@route('/hello/<name>')
def greet(name='Stranger'):
    return template('Hello {{name}}, how are you?', name=name)

Этот пример демонстрирует два момента: вы можете связать более одного маршрута с одним обратным вызовом, и вы можете добавлять подстановочные знаки в URL и получать доступ к ним через ключевые аргументы.

Динамические маршруты

Маршруты, содержащие подстановочные знаки, называются dynamic routes (в отличие от static routes) и соответствуют более чем одному URL одновременно. Простой подстановочный знак состоит из имени в угловых скобках (например, <name>) и принимает один или несколько символов до следующего слэша (/). Например, маршрут /hello/<name> принимает запросы для /hello/alice и /hello/bob, но не для /hello, /hello/ или /hello/mr/smith.

Каждый подстановочный знак передаёт соответствующую часть URL в качестве ключевого аргумента функции обратного вызова. Вы можете использовать их сразу и легко реализовать RESTful, понятные и осмысленные URL. Вот некоторые другие примеры вместе с URL, которым они соответствуют:

@route('/wiki/<pagename>')            # matches /wiki/Learning_Python
def show_wiki_page(pagename):
    ...

@route('/<action>/<user>')            # matches /follow/defnull
def user_api(action, user):
    ...

Новое в версии 0.10.

Фильтры используются для определения более конкретных подстановочных знаков и/или преобразования соответствующей части URL перед передачей в функцию обратного вызова. Отфильтрованный подстановочный знак объявляется как <name:filter> или <name:filter:config>. Синтаксис необязательной части конфигурации зависит от используемого фильтра.

По умолчанию реализованы следующие фильтры, и могут быть добавлены ещё:

  • :int соответствует только цифрам (со знаком) и преобразует значение в целое число.
  • :float аналогично :int, но для десятичных чисел.
  • :path соответствует всем символам, включая символ слэша, не жадно, и может использоваться для соответствия более чем одному сегменту пути.
  • :re позволяет указать пользовательское регулярное выражение в поле конфигурации. Соответствующее значение не изменяется.

Давайте рассмотрим несколько практических примеров:

@route('/object/<id:int>')
def callback(id):
    assert isinstance(id, int)

@route('/show/<name:re:[a-z]+>')
def callback(name):
    assert name.isalpha()

@route('/static/<path:path>')
def callback(path):
    return static_file(path, ...)

Вы также можете добавить свои собственные фильтры. Смотрите Routing для получения подробной информации.

Изменено в версии 0.10.

Новый синтаксис правил был введён в Bottle 0.10 для упрощения некоторых общих случаев использования, но старый синтаксис всё ещё работает, и вы можете найти много примеров кода, использующих его. Различия лучше всего описать на примерах:

Старый синтаксис Новый синтаксис
:name <name>
:name#regexp# <name:re:regexp>
:#regexp# <:re:regexp>
:## <:re>

Старайтесь избегать старого синтаксиса в будущих проектах, если можете. Он в настоящее время не устарел, но в конечном итоге им будет.

Методы HTTP-запросов

Протокол HTTP определяет несколько методов запросов (иногда называемых «глаголами») для различных задач. GET является значением по умолчанию для всех маршрутов без указания другого метода. Эти маршруты будут соответствовать только запросам GET. Для обработки других методов, таких как POST, PUT или DELETE, добавьте ключевой аргумент method к декоратору route() или используйте один из четырёх альтернативных декораторов: get(), post(), put() или delete().

Метод POST обычно используется для отправки HTML-форм. Этот пример показывает, как обработать форму входа с помощью POST:

from bottle import get, post, request # or route

@get('/login') # or @route('/login')
def login():
    return '''
        <form action="/login" method="post">
            Username: <input name="username" type="text" />
            Password: <input name="password" type="password" />
            <input value="Login" type="submit" />
        </form>
    '''

@post('/login') # or @route('/login', method='POST')
def do_login():
    username = request.forms.get('username')
    password = request.forms.get('password')
    if check_login(username, password):
        return "<p>Your login information was correct.</p>"
    else:
        return "<p>Login failed.</p>"

В этом примере URL /login связан с двумя разными функциями обратного вызова, одной для запросов GET и другой для запросов POST. Первая отображает пользователю HTML-форму. Вторая функция обратного вызова вызывается при отправке формы и проверяет учётные данные пользователя, введённые в форму. Использование Request.forms описано далее в разделе Данные запроса.

Специальные методы: HEAD и ANY

Метод HEAD используется для запроса ответа, идентичного тому, который соответствовал бы запросу GET, но без тела ответа. Это полезно для получения метаданных о ресурсе без загрузки всего документа. Bottle автоматически обрабатывает эти запросы, переходя к соответствующему маршруту GET и отрезая тело запроса, если оно есть. Вам не нужно указывать какие-либо маршруты HEAD.

Кроме того, нестандартный метод ANY работает как резервный вариант низкого приоритета: Маршруты, которые слушают ANY, будут соответствовать запросам независимо от их метода HTTP, но только если не определён другой более конкретный маршрут. Это полезно для маршрутов-прокси, которые перенаправляют запросы в более конкретные подприложения.

Подводя итог: запросы HEAD переходят к маршрутам GET, а все запросы переходят к маршрутам ANY, но только если нет совпадающего маршрута для исходного метода запроса. Это так просто.

Маршрутизация статических файлов

Статические файлы, такие как изображения или файлы CSS, не обрабатываются автоматически. Вам нужно добавить маршрут и функцию обратного вызова, чтобы контролировать, какие файлы будут обрабатываться и где их найти:

from bottle import static_file
@route('/static/<filename>')
def server_static(filename):
    return static_file(filename, root='/path/to/your/static/files')

Функция static_file() служит помощником для безопасного и удобного предоставления файлов (см. Статические файлы). Этот пример ограничен файлами непосредственно в каталоге /path/to/your/static/files, так как подстановка <filename> не будет соответствовать пути с косой чертой. Для предоставления файлов в подкаталогах измените подстановку на использование фильтра path.

@route('/static/<filepath:path>')
def server_static(filepath):
    return static_file(filepath, root='/path/to/your/static/files')

Будьте осторожны при указании относительного корневого пути, например, root='./static/files'. Рабочий каталог (./) и каталог проекта не всегда совпадают.

Страницы ошибок

При возникновении проблем Bottle отображает информативную, но довольно простую страницу ошибки. Вы можете переопределить стандартную страницу для определённого HTTP-кода статуса с помощью декоратора error():

from bottle import error
@error(404)
def error404(error):
    return 'Nothing here, sorry'

Отныне ошибки 404 File not Found будут отображать пользовательскую страницу ошибки. Единственный параметр, передаваемый обработчику ошибок, — это экземпляр HTTPError. Помимо этого, обработчик ошибок очень похож на обычную обратную функцию запроса. Вы можете читать из request, записывать в response и возвращать любой поддерживаемый тип данных, кроме экземпляров HTTPError.

Обработчики ошибок используются только в том случае, если ваше приложение возвращает или вызывает исключение HTTPError (abort() делает именно это). Изменение Request.status или возвращение HTTPResponse не вызовет обработчик ошибок.

Генерация содержимого

В чистом WSGI диапазон типов, которые вы можете вернуть из вашего приложения, очень ограничен. Приложения должны возвращать итерируемый объект, который генерирует байтовые строки. Вы можете вернуть строку (поскольку строки итерируемы), но это заставит большинство серверов передавать ваше содержимое символ за символом. Unicode-строки вообще недопустимы. Это не очень практично.

Bottle гораздо более гибкий и поддерживает широкий спектр типов. Он даже добавляет заголовок Content-Length при возможности и автоматически кодирует unicode, так что вам не нужно. Ниже приведён список типов данных, которые вы можете вернуть из обратных функций вашего приложения, и краткое описание того, как фреймворк обрабатывает эти данные:

Словари
Как упоминалось выше, словари Python (или их подклассы) автоматически преобразуются в JSON-строки и возвращаются браузеру с установленным заголовком Content-Type в значение application/json. Это упрощает реализацию API на основе JSON. Также поддерживаются форматы данных, отличные от JSON. Подробнее см. tutorial-output-filter.
Empty Strings, False, None or other non-true values:
Эти значения производят пустой вывод с заголовком Content-Length, установленным в 0.
Unicode-строки
Unicode-строки (или итерируемые объекты, возвращающие Unicode-строки) автоматически кодируются с помощью кодека, указанного в заголовке Content-Type (по умолчанию utf8), а затем обрабатываются как обычные байтовые строки (см. ниже).
Байтовые строки
Bottle возвращает строки целиком (вместо итерирования по каждому символу) и добавляет заголовок Content-Length на основе длины строки. Список байтовых строк объединяется сначала. Другие итерируемые объекты, возвращающие байтовые строки, не объединяются, так как они могут стать слишком большими для хранения в памяти. В этом случае заголовок Content-Length не устанавливается.
Instances of HTTPError or HTTPResponse
Возвращение этих значений имеет тот же эффект, что и при их генерировании как исключения. В случае HTTPError применяется обработчик ошибок. Подробнее см. Страницы ошибок.
Объекты файлов
Всё, что имеет метод .read(), обрабатывается как объект файла или похожий на файл, и передаётся вызываемому объекту wsgi.file_wrapper, определённому фреймворком WSGI-сервера. Некоторые реализации WSGI-серверов могут использовать оптимизированные системные вызовы (sendfile) для более эффективной передачи файлов. В других случаях это просто итерирует по частям, которые помещаются в память. Дополнительные заголовки, такие как Content-Length или Content-Type, не устанавливаются автоматически. Используйте send_file() если это возможно. Подробнее см. Статические файлы.
Итерируемые объекты и генераторы
Вы можете использовать yield внутри своих обратных функций или вернуть итерируемый объект, при условии, что итерируемый объект генерирует байтовые строки, unicode-строки, HTTPError или HTTPResponse экземпляры. Вложенные итерируемые объекты не поддерживаются, извините. Обратите внимание, что код HTTP-статуса и заголовки отправляются браузеру, как только итерируемый объект генерирует своё первое ненулевое значение. Изменение их позже не оказывает никакого влияния.

Порядок в этом списке важен. Например, вы можете вернуть подкласс str с методом read(). Он всё равно будет обрабатываться как строка, а не как файл, потому что строки обрабатываются первыми.

Изменение кодировки по умолчанию

Bottle использует параметр charset заголовка Content-Type для определения способа кодирования Unicode-строк. Этот заголовок по умолчанию равен text/html; charset=UTF8 и может быть изменён с помощью атрибута Response.content_type или путём непосредственного задания атрибута Response.charset. (Объект Response описан в разделе Объект ответа.)

from bottle import response
@route('/iso')
def get_iso():
    response.charset = 'ISO-8859-15'
    return u'This will be sent with ISO-8859-15 encoding.'

@route('/latin9')
def get_latin():
    response.content_type = 'text/html; charset=latin9'
    return u'ISO-8859-15 is also known as latin9.'

В некоторых редких случаях имена кодировок Python отличаются от имен, поддерживаемых спецификацией HTTP. В этом случае вам нужно сделать оба шага: сначала установить заголовок Response.content_type (который отправляется клиенту без изменений), а затем установить атрибут Response.charset (который используется для кодирования unicode).

Статические файлы

Вы можете напрямую вернуть объекты файлов, но static_file() — это рекомендуемый способ предоставления статических файлов. Он автоматически определяет MIME-тип, добавляет заголовок Last-Modified, ограничивает пути каталогом root по соображениям безопасности и генерирует соответствующие ответы об ошибках (403 при ошибках доступа, 404 при отсутствии файлов). Он даже поддерживает заголовок If-Modified-Since и в конечном итоге генерирует ответ 304 Not Modified. Вы можете передать пользовательский MIME-тип, чтобы отключить определение.

from bottle import static_file
@route('/images/<filename:re:.*\.png>')
def send_image(filename):
    return static_file(filename, root='/path/to/image/files', mimetype='image/png')

@route('/static/<filename:path>')
def send_static(filename):
    return static_file(filename, root='/path/to/static/files')

Вы можете сгенерировать исключение из возвращаемого значения static_file() , если это действительно необходимо.

Вынужденная загрузка

Большинство браузеров пытаются открыть загружаемые файлы, если известен MIME-тип и ему назначено приложение (например, PDF-файлы). Если это не то, что вы хотите, вы можете принудительно вызвать диалог загрузки и даже предложить пользователю имя файла:

@route('/download/<filename:path>')
def download(filename):
    return static_file(filename, root='/path/to/static/files', download=filename)

Если параметр download просто True, используется исходное имя файла.

HTTP-ошибки и перенаправления

Функция abort() — это сокращение для генерации страниц HTTP-ошибок.

from bottle import route, abort
@route('/restricted')
def restricted():
    abort(401, "Sorry, access denied.")

Чтобы перенаправить клиента на другой URL, можно отправить ответ 303 See Other с заголовком Location , установленным на новый URL. redirect() делает это за вас:

from bottle import redirect
@route('/wrong/url')
def wrong():
    redirect("/right/url")

В качестве второго параметра можно указать другой HTTP-код статуса.

Примечание

Обе функции прерывают код вашей обратной функции, вызывая исключение HTTPError.

Другие исключения

Все исключения, кроме HTTPResponse или HTTPError, приведут к ответу 500 Internal Server Error, поэтому они не повредят ваш WSGI-сервер. Вы можете отключить это поведение, чтобы обрабатывать исключения в вашем средстве, установив bottle.app().catchall в значение False.

Объект Response

Метаданные ответа, такие как код HTTP-статуса, заголовки ответа и куки, хранятся в объекте, называемом response до момента, когда они передаются браузеру. Вы можете напрямую изменять эти метаданные или использовать предварительно определённые вспомогательные методы для этого. Полный API и список функций описан в разделе API (см. Response), но здесь также охватываются наиболее распространённые случаи использования и функции.

Код статуса

Код статуса HTTP управляет поведением браузера и по умолчанию равен 200 OK. В большинстве случаев вам не нужно устанавливать атрибут Response.status вручную, а используйте вспомогательную функцию abort() или верните экземпляр HTTPResponse с соответствующим кодом статуса. Допускаются любые целые числа, но коды, отличные от определённых в спецификации HTTP, будут только вводить браузер в заблуждение и нарушать стандарты.

Заголовок ответа

Заголовки ответа, такие как Cache-Control или Location, определяются с помощью метода Response.set_header(). Этот метод принимает два параметра: имя заголовка и значение. Часть с именем нечувствительна к регистру:

@route('/wiki/<page>')
def wiki(page):
    response.set_header('Content-Language', 'en')
    ...

Большинство заголовков уникальны, то есть один заголовок на имя отправляется клиенту. Однако некоторые специальные заголовки могут появляться несколько раз в ответе. Чтобы добавить дополнительный заголовок, используйте Response.add_header() вместо Response.set_header().

response.set_header('Set-Cookie', 'name=value')
response.add_header('Set-Cookie', 'name2=value2')

Обратите внимание, что это всего лишь пример. Если вы хотите работать с куки, прочитайте дальше.

Куки

Куки — это именованный фрагмент текста, хранящийся в профиле браузера пользователя. Вы можете получить доступ к ранее определенным куки через Request.get_cookie() и установить новые куки с помощью Response.set_cookie():

@route('/hello')
def hello_again():
    if request.get_cookie("visited"):
        return "Welcome back! Nice to see you again"
    else:
        response.set_cookie("visited", "yes")
        return "Hello there! Nice to meet you"

Метод Response.set_cookie() принимает ряд дополнительных ключевых аргументов, которые управляют сроком действия и поведением куки. Некоторые из наиболее распространенных настроек описаны здесь:

  • max_age: Максимальный срок действия в секундах. (по умолчанию: None)
  • expires: Объект datetime или временная метка Unix. (по умолчанию: None)
  • domain: Домен, которому разрешено читать куки. (по умолчанию: текущий домен)
  • path: Ограничить куки заданным путем (по умолчанию: /)
  • secure: Ограничить куки соединениями HTTPS (по умолчанию: выключено).
  • httponly: Предотвратить чтение этого куки со стороны Javascript (по умолчанию: выключено, требуется Python 2.6 или более поздняя версия).

Если ни expires ни max_age не установлены, куки истекают в конце сессии браузера или как только окно браузера закрывается. Есть и другие нюансы, которые следует учитывать при использовании куки:

  • В большинстве браузеров куки ограничены 4 КБ текста.
  • Некоторые пользователи настраивают свои браузеры так, чтобы они вообще не принимали куки. Большинство поисковых систем также игнорируют куки. Убедитесь, что ваше приложение по-прежнему работает без куки.
  • Куки хранятся на стороне клиента и никак не шифруются. Пользователь может прочитать всё, что хранится в куки. Хуже того, злоумышленник может украсть куки пользователя через уязвимости XSS на вашей стороне. Известно, что некоторые вирусы также считывают куки браузера. Поэтому никогда не храните конфиденциальную информацию в куки.
  • Куки легко подделываются злонамеренными клиентами. Не доверяйте куки.

Подписанные куки

Как упоминалось выше, куки легко подделываются злонамеренными клиентами. Bottle может криптографически подписать ваши куки, чтобы предотвратить такую манипуляцию. Всё, что вам нужно сделать, это предоставить ключ подписи через ключевой аргумент secret при чтении или установке куки и сохранить этот ключ в секрете. В результате, Request.get_cookie() вернёт None если куки не подписано или ключи подписи не совпадают:

@route('/login')
def do_login():
    username = request.forms.get('username')
    password = request.forms.get('password')
    if check_login(username, password):
        response.set_cookie("account", username, secret='some-secret-key')
        return template("<p>Welcome {{name}}! You are now logged in.</p>", name=username)
    else:
        return "<p>Login failed.</p>"

@route('/restricted')
def restricted_area():
    username = request.get_cookie("account", secret='some-secret-key')
    if username:
        return template("Hello {{name}}. Welcome back.", name=username)
    else:
        return "You are not logged in. Access denied."

Кроме того, Bottle автоматически упаковывает и распаковывает любые данные, хранящиеся в подписанных куках. Это позволяет хранить любые упаковываемые объекты (не только строки) в куках, если упакованные данные не превышают лимит в 4 КБ.

Предупреждение

Подписанные куки не зашифрованы (клиент по-прежнему может видеть содержимое) и не защищены от копирования (клиент может восстановить старый куки). Основная цель — сделать безопасным упаковывание и распаковывание и предотвратить манипуляции, а не хранить секретную информацию на стороне клиента.

Данные запроса

Куки, HTTP-заголовки, поля HTML <form> и другие данные запроса доступны через глобальный объект request. Этот специальный объект всегда ссылается на текущий запрос, даже в многопоточных средах, где одновременно обрабатываются несколько подключений клиентов:

from bottle import request, route, template

@route('/hello')
def hello():
    name = request.cookies.username or 'Guest'
    return template('Hello {{name}}', name=name)

Объект request является подклассом BaseRequest и имеет очень богатый API для доступа к данным. Здесь мы рассматриваем только наиболее часто используемые функции, но этого должно быть достаточно для начала.

Введение в FormsDict

Bottle использует специальный тип словаря для хранения данных форм и куки. FormsDict ведёт себя как обычный словарь, но имеет некоторые дополнительные функции, чтобы облегчить вашу работу.

Доступ к атрибутам: Все значения в словаре также доступны как атрибуты. Эти виртуальные атрибуты возвращают строковые значения unicode, даже если значение отсутствует или декодирование unicode не удается. В этом случае строка пустая, но всё же присутствует:

name = request.cookies.name

# is a shortcut for:

name = request.cookies.getunicode('name') # encoding='utf-8' (default)

# which basically does this:

try:
    name = request.cookies.get('name', '').decode('utf-8')
except UnicodeError:
    name = u''

Несколько значений на ключ: FormsDict является подклассом MultiDict и может хранить более одного значения на ключ. Стандартные методы доступа к словарю возвращают только одно значение, но метод getall() возвращает список всех значений для определённого ключа (возможно, пустой):

for choice in request.forms.getall('multiple_choice'):
    do_something(choice)

Поддержка WTForms: Некоторые библиотеки (например, WTForms) хотят получать на вход словари с значениями unicode. FormsDict.decode() делает это за вас. Он декодирует все значения и возвращает копию себя, сохраняя при этом несколько значений на ключ и все другие функции.

Примечание

В Python 2 все ключи и значения — строковые значения байтов. Если вам нужны unicode-значения, вы можете вызвать FormsDict.getunicode() или получить значения через доступ к атрибутам. Оба метода пытаются декодировать строку (по умолчанию: utf8) и возвращают пустую строку, если это не удаётся. Не нужно перехватывать UnicodeError:

>>> request.query['city']
'G\xc3\xb6ttingen'  # A utf8 byte string
>>> request.query.city
u'Göttingen'        # The same string as unicode

В Python 3 все строки являются unicode, но HTTP — это протокол обмена байтами. Сервер должен каким-то образом декодировать строковые значения байтов перед передачей их приложению. Для большей надёжности WSGI рекомендует ISO-8859-1 (также известный как latin1), обратимый кодек с одним байтом, который можно перекодировать с помощью другого кодирования позже. Bottle делает это для FormsDict.getunicode() и доступа к атрибутам, но не для методов доступа к словарю. Они возвращают неизменённые значения, предоставленные реализацией сервера, что, вероятно, не то, что вам нужно.

>>> request.query['city']
'Göttingen' # An utf8 string provisionally decoded as ISO-8859-1 by the server
>>> request.query.city
'Göttingen'  # The same string correctly re-encoded as utf8 by bottle

Если вам нужен весь словарь с правильно декодированными значениями (например, для WTForms), вы можете вызвать FormsDict.decode() для получения закодированной копии.

Куки

Куки — это небольшие фрагменты текста, хранящиеся в браузере клиента и отправляемые обратно на сервер с каждым запросом. Они полезны для сохранения состояния на несколько запросов (сам HTTP бессостоятелен), но не должны использоваться для задач безопасности. Их легко подделать клиенту.

Все куки, отправленные клиентом, доступны через BaseRequest.cookies (a FormsDict). Этот пример демонстрирует простой счётчик просмотров на основе куки:

from bottle import route, request, response
@route('/counter')
def counter():
    count = int( request.cookies.get('counter', '0') )
    count += 1
    response.set_cookie('counter', str(count))
    return 'You visited this page %d times' % count

Метод BaseRequest.get_cookie() — это другой способ доступа к куки. Он поддерживает декодирование подписанных куки, как описано в отдельном разделе.

HTTP-заголовки

Все HTTP-заголовки, отправленные клиентом (например, Referer, Agent или Accept-Language) хранятся в WSGIHeaderDict и доступны через атрибут BaseRequest.headers. WSGIHeaderDict по сути является словарем с ключами, нечувствительными к регистру:

from bottle import route, request
@route('/is_ajax')
def is_ajax():
    if request.headers.get('X-Requested-With') == 'XMLHttpRequest':
        return 'This is an AJAX request'
    else:
        return 'This is a normal request'

Переменные запроса

Строка запроса (как в /forum?id=1&page=5) обычно используется для передачи небольшого количества пар «ключ-значение» на сервер. Вы можете использовать атрибут BaseRequest.query (a FormsDict) для доступа к этим значениям, а атрибут BaseRequest.query_string — для получения всей строки.

from bottle import route, request, response, template
@route('/forum')
def display_forum():
    forum_id = request.query.id
    page = request.query.page or '1'
    return template('Forum ID: {{id}} (page {{page}})', id=forum_id, page=page)

Обработка HTML <form>

Давайте начнем с начала. В HTML типичная <form> выглядит примерно так:

<form action="/login" method="post">
    Username: <input name="username" type="text" />
    Password: <input name="password" type="password" />
    <input value="Login" type="submit" />
</form>

Атрибут action указывает URL, который получит данные формы. method определяет используемый HTTP-метод (GET или POST). С method="get" значения формы добавляются к URL и доступны через BaseRequest.query, как описано выше. Это считается небезопасным и имеет другие ограничения, поэтому мы используем method="post" здесь. В случае сомнений используйте POST формы.

Поля форм, переданные через POST, хранятся в BaseRequest.forms как FormsDict. Код на стороне сервера может выглядеть так:

from bottle import route, request

@route('/login')
def login():
    return '''
        <form action="/login" method="post">
            Username: <input name="username" type="text" />
            Password: <input name="password" type="password" />
            <input value="Login" type="submit" />
        </form>
    '''

@route('/login', method='POST')
def do_login():
    username = request.forms.get('username')
    password = request.forms.get('password')
    if check_login(username, password):
        return "<p>Your login information was correct.</p>"
    else:
        return "<p>Login failed.</p>"

Существует несколько других атрибутов, используемых для доступа к данным форм. Некоторые из них объединяют значения из разных источников для более удобного доступа. Следующая таблица должна дать вам общее представление.

Атрибут Поля формы GET Поля формы POST Загрузка файлов
BaseRequest.query да нет нет
BaseRequest.forms нет да нет
BaseRequest.files нет нет да
BaseRequest.params да да нет
BaseRequest.GET да нет нет
BaseRequest.POST нет да да

Загрузка файлов

Для поддержки загрузки файлов, нам необходимо немного изменить тег <form>. Во-первых, мы сообщим браузеру закодировать данные формы другим способом, добавив атрибут enctype="multipart/form-data" к тегу <form>. Затем, мы добавим теги <input type="file" /> для возможности выбора файла пользователем. Вот пример:

<form action="/upload" method="post" enctype="multipart/form-data">
  Category:      <input type="text" name="category" />
  Select a file: <input type="file" name="upload" />
  <input type="submit" value="Start upload" />
</form>

Bottle хранит загруженные файлы в BaseRequest.files в качестве экземпляров FileUpload, вместе с некоторыми метаданными о загрузке. Предположим, что вы хотите сохранить файл на диск:

@route('/upload', method='POST')
def do_upload():
    category   = request.forms.get('category')
    upload     = request.files.get('upload')
    name, ext = os.path.splitext(upload.filename)
    if ext not in ('.png','.jpg','.jpeg'):
        return 'File extension not allowed.'

    save_path = get_save_path_for_category(category)
    upload.save(save_path) # appends upload.filename automatically
    return 'OK'

FileUpload.filename содержит имя файла на файловой системе клиента, но очищено и нормализовано для предотвращения ошибок, вызванных неподдерживаемыми символами или частями пути в имени файла. Если вам нужно имя в исходном виде, отправленное клиентом, обратите внимание на FileUpload.raw_filename.

Метод FileUpload.save настоятельно рекомендуется, если вы хотите сохранить файл на диск. Он предотвращает некоторые распространённые ошибки (например, не перезаписывает существующие файлы, если вы не укажете обратное) и хранит файл с высокой эффективностью использования памяти. Вы можете напрямую получить доступ к объекту файла через FileUpload.file. Будьте осторожны.

JSON-данные

Некоторые JavaScript или REST-клиенты отправляют application/json данные на сервер. Атрибут BaseRequest.json содержит распарсенную структуру данных, если она доступна.

Необработанное тело запроса

Вы можете получить доступ к необработанным данным тела как к объекту, подобному файлу, через BaseRequest.body. Это BytesIO буфер или временный файл в зависимости от длины содержимого и настройки BaseRequest.MEMFILE_MAX. В обоих случаях тело полностью буферизуется перед доступом к атрибуту. Если вы ожидаете большие объёмы данных и хотите получить прямой небуферизованный доступ к потоку, ознакомьтесь с request['wsgi.input'].

WSGI-окружение

Каждый экземпляр BaseRequest оборачивает словарь WSGI-окружения. Исходный словарь хранится в BaseRequest.environ, но сам объект запроса также ведет себя как словарь. Большая часть интересной информации представлена через специальные методы или атрибуты, но если вы хотите получить прямой доступ к переменным WSGI-окружения, вы можете сделать это:

@route('/my_ip')
def show_ip():
    ip = request.environ.get('REMOTE_ADDR')
    # or ip = request.get('REMOTE_ADDR')
    # or ip = request['REMOTE_ADDR']
    return template("Your IP is: {{ip}}", ip=ip)

Шаблоны

Bottle поставляется с быстрым и мощным встроенным движком шаблонов под названием SimpleTemplate Engine. Для рендеринга шаблона можно использовать функцию template() или декоратор view(). Всё, что вам нужно сделать, это указать имя шаблона и переменные, которые вы хотите передать в шаблон в качестве ключевых аргументов. Вот простой пример рендеринга шаблона:

@route('/hello')
@route('/hello/<name>')
def hello(name='World'):
    return template('hello_template', name=name)

Это загрузит файл шаблона hello_template.tpl и рендерит его с установленной переменной name. Bottle будет искать шаблоны в папке ./views/ или в любой другой папке, указанной в списке bottle.TEMPLATE_PATH.

Декоратор view() позволяет возвращать словарь с переменными шаблона вместо вызова template():

@route('/hello')
@route('/hello/<name>')
@view('hello_template')
def hello(name='World'):
    return dict(name=name)

Синтаксис

Синтаксис шаблона – это очень тонкий слой над языком Python. Его основная цель – обеспечение правильного отступа блоков, так что вы можете форматировать свой шаблон, не беспокоясь об отступах. Следуйте ссылке для полного описания синтаксиса: SimpleTemplate Engine

Вот пример шаблона:

%if name == 'World':
    <h1>Hello {{name}}!</h1>
    <p>This is a test.</p>
%else:
    <h1>Hello {{name.title()}}!</h1>
    <p>How are you?</p>
%end

Кэширование

Шаблоны кэшируются в памяти после компиляции. Изменения, внесённые в файлы шаблонов, не повлияют, пока вы не очистите кэш шаблонов. Для этого вызовите bottle.TEMPLATES.clear(). Кэширование отключено в отладочном режиме.

Плагины

Введено в версии 0.9.

Основные возможности Bottle охватывают большинство распространённых случаев использования, но как микрофреймворк он имеет свои ограничения. Здесь на сцену выходят «Плагины». Плагины добавляют недостающую функциональность фреймворку, интегрируют сторонние библиотеки или просто автоматизируют повторяющуюся работу.

У нас есть растущий Список доступных плагинов, и большинство плагинов разработаны для переносимости и повторного использования в различных приложениях. Вероятность того, что ваша проблема уже решена и существует готовый плагин, достаточно высока. Если нет, руководство по разработке плагинов Plugin Development Guide может помочь.

Эффекты и API плагинов многообразны и зависят от конкретного плагина. Например, плагин SQLitePlugin обнаруживает обратные вызовы, требующие ключевого аргумента db, и создаёт свежий объект соединения с базой данных каждый раз, когда вызывается обратный вызов. Это делает использование базы данных очень удобным:

from bottle import route, install, template
from bottle_sqlite import SQLitePlugin

install(SQLitePlugin(dbfile='/tmp/test.db'))

@route('/show/<post_id:int>')
def show(db, post_id):
    c = db.execute('SELECT title, content FROM posts WHERE id = ?', (post_id,))
    row = c.fetchone()
    return template('show_post', title=row['title'], text=row['content'])

@route('/contact')
def contact_page():
    ''' This callback does not need a db connection. Because the 'db'
        keyword argument is missing, the sqlite plugin ignores this callback
        completely. '''
    return template('contact')

Другие плагины могут заполнять потокобезопасный объект local, изменять детали объекта request, фильтровать данные, возвращаемые обратным вызовом, или полностью пропускать обратный вызов. Например, плагин «аутентификации» мог бы проверять наличие действительной сессии и возвращать страницу входа вместо вызова исходного обратного вызова. То, что происходит на самом деле, зависит от плагина.

Установка плагинов на уровне приложения

Плагины могут быть установлены на уровне приложения или только для определённых маршрутов, которым требуется дополнительная функциональность. Большинство плагинов могут безопасно устанавливаться для всех маршрутов и достаточно разумны, чтобы не добавлять избыточную нагрузку к обратным вызовам, которым их функциональность не требуется.

Давайте рассмотрим плагин SQLitePlugin в качестве примера. Он влияет только на обратные вызовы маршрутов, которым требуется соединение с базой данных. Другие маршруты остаются без изменений. Благодаря этому мы можем установить плагин на уровне приложения без дополнительной нагрузки.

Для установки плагина просто вызовите install() с плагином в качестве первого аргумента:

from bottle_sqlite import SQLitePlugin
install(SQLitePlugin(dbfile='/tmp/test.db'))

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

Удаление плагинов

Вы можете использовать имя, класс или экземпляр для uninstall() ранее установленного плагина:

sqlite_plugin = SQLitePlugin(dbfile='/tmp/test.db')
install(sqlite_plugin)

uninstall(sqlite_plugin) # uninstall a specific plugin
uninstall(SQLitePlugin)  # uninstall all plugins of that type
uninstall('sqlite')      # uninstall all plugins with that name
uninstall(True)          # uninstall all plugins at once

Плагины могут устанавливаться и удаляться в любое время, даже во время выполнения запросов. Это позволяет реализовать несколько полезных приёмов (установка медленных плагинов отладки или профилирования только при необходимости), но не следует злоупотреблять этим. Каждый раз, когда список плагинов изменяется, кэш маршрутов очищается, и все плагины применяются заново.

Примечание

Функции install() и uninstall() на уровне модуля влияют на Основное приложение. Для управления плагинами для определённого приложения используйте соответствующие методы объекта приложения Bottle.

Установка плагинов для конкретного маршрута

Параметр apply декоратора route() полезен, если вы хотите установить плагины только для небольшого числа маршрутов:

sqlite_plugin = SQLitePlugin(dbfile='/tmp/test.db')

@route('/create', apply=[sqlite_plugin])
def create(db):
    db.execute('INSERT INTO ...')

Игнорирование плагинов

Вам может потребоваться явно отключить плагин для ряда маршрутов. Декоратор route() имеет параметр skip для этой цели:

sqlite_plugin = SQLitePlugin(dbfile='/tmp/test1.db')
install(sqlite_plugin)

dbfile1 = '/tmp/test1.db'
dbfile2 = '/tmp/test2.db'

@route('/open/<db>', skip=[sqlite_plugin])
def open_db(db):
    # The 'db' keyword argument is not touched by the plugin this time.

    # The plugin handle can be used for runtime configuration, too.
    if db == 'test1':
        sqlite_plugin.dbfile = dbfile1
    elif db == 'test2':
        sqlite_plugin.dbfile = dbfile2
    else:
        abort(404, "No such database.")

    return "Database File switched to: " + sqlite_plugin.dbfile

Параметр skip принимает одно значение или список значений. Вы можете использовать имя, класс или экземпляр для идентификации плагина, который необходимо пропустить. Установите skip=True для пропуска всех плагинов сразу.

Плагины и подприложения

Большинство плагинов специфичны для приложения, к которому они были установлены. Следовательно, они не должны влиять на подприложения, подключенные с помощью Bottle.mount(). Вот пример:

root = Bottle()
root.mount('/blog', apps.blog)

@root.route('/contact', template='contact')
def contact():
    return {'email': 'contact@example.com'}

root.install(plugins.WTForms())

Всякий раз, когда вы устанавливаете приложение, Bottle создает прокси-маршрут в основном приложении, который перенаправляет все запросы в подприложение. Плагины по умолчанию отключены для этого вида прокси-маршрута. В результате, наш (вымышленный) WTForms плагин влияет на /contact маршрут, но не влияет на маршруты /blog подприложения.

Это поведение по умолчанию, но может быть переопределено. Следующий пример повторно активирует все плагины для определенного прокси-маршрута:

root.mount('/blog', apps.blog, skip=None)

Но есть проблема: плагин видит всё подприложение как единственный маршрут, а именно прокси-маршрут, упомянутый выше. Чтобы повлиять на каждый отдельный маршрут подприложения, вам нужно явно установить плагин в установленное приложение.

Разработка

Итак, вы освоили основы и хотите написать собственное приложение? Вот несколько советов, которые помогут вам повысить производительность.

Приложение по умолчанию

Bottle поддерживает глобальный стек экземпляров Bottle и использует вершину стека в качестве значения по умолчанию для некоторых функций и декораторов модуля. Например, декоратор route() — это сокращение для вызова Bottle.route() в приложении по умолчанию:

@route('/')
def hello():
    return 'Hello World'

Это очень удобно для небольших приложений и экономит написание кода, но также означает, что как только ваш модуль импортируется, маршруты устанавливаются в глобальном приложении. Чтобы избежать таких побочных эффектов импорта, Bottle предлагает второй, более явный способ создания приложений:

app = Bottle()

@app.route('/')
def hello():
    return 'Hello World'

Отделение объекта приложения также значительно улучшает его повторное использование. Другие разработчики могут безопасно импортировать объект app из вашего модуля и использовать Bottle.mount() для объединения приложений.

В качестве альтернативы, вы можете использовать стек приложений для изоляции своих маршрутов, сохраняя при этом удобные сокращения:

default_app.push()

@route('/')
def hello():
    return 'Hello World'

app = default_app.pop()

Как app(), так и default_app() являются экземплярами AppStack и реализуют интерфейс стека. Вы можете добавлять и удалять приложения из стека по мере необходимости. Это также полезно, если вы хотите импортировать сторонний модуль, который не предоставляет отдельный объект приложения:

default_app.push()

import some.module

app = default_app.pop()

Режим отладки

Во время ранней разработки режим отладки может быть очень полезен.

bottle.debug(True)

В этом режиме Bottle более подробен и предоставляет полезную информацию об отладке при возникновении ошибки. Он также отключает некоторые оптимизации, которые могут помешать вам, и добавляет проверки, предупреждающие о возможной неправильной настройке.

Вот неполный список изменений в режиме отладки:

  • В стандартной странице ошибки отображается трассировка.
  • Шаблоны не кэшируются.
  • Плагины применяются немедленно.

Только убедитесь, что вы не используете режим отладки на сервере производства.

Автоматическое переподключение

Во время разработки вам часто приходится перезапускать сервер, чтобы протестировать недавние изменения. Автоматическое переподключение может сделать это за вас. Каждый раз, когда вы редактируете файл модуля, переподключение перезапускает процесс сервера и загружает последнюю версию вашего кода.

from bottle import run
run(reloader=True)

Как это работает: основной процесс не запускает сервер, а запускает новый дочерний процесс с теми же аргументами командной строки, которые использовались для запуска основного процесса. Весь код уровня модуля выполняется как минимум дважды! Будьте внимательны.

Дочерний процесс будет иметь os.environ['BOTTLE_CHILD'] значение True и запустится как обычный сервер приложения без переподключения. Как только любой из загруженных модулей изменится, дочерний процесс завершается и перезапускается основным процессом. Изменения в файлах шаблонов не вызовут переподключение. Пожалуйста, используйте режим отладки для отключения кэширования шаблонов.

Переподключение зависит от возможности остановки дочернего процесса. Если вы работаете в Windows или другой операционной системе, не поддерживающей signal.SIGINT (что вызывает KeyboardInterrupt в Python), используется signal.SIGTERM для завершения дочернего процесса. Обратите внимание, что обработчики выхода и блоки finally и т. д. не выполняются после SIGTERM.

Интерфейс командной строки

Начиная с версии 0.10 вы можете использовать bottle как инструмент командной строки:

$ python -m bottle

Usage: bottle.py [options] package.module:app

Options:
  -h, --help            show this help message and exit
  --version             show version number.
  -b ADDRESS, --bind=ADDRESS
                        bind socket to ADDRESS.
  -s SERVER, --server=SERVER
                        use SERVER as backend.
  -p PLUGIN, --plugin=PLUGIN
                        install additional plugin/s.
  --debug               start server in debug mode.
  --reload              auto-reload on file changes.

Поле ADDRESS принимает IP-адрес или пару IP:PORT и по умолчанию равно localhost:8080. Другие параметры должны быть понятны.

Плагины и приложения указываются через выражения импорта. Они состоят из пути импорта (например, package.module) и выражения, которое должно быть вычислено в пространстве имен этого модуля, разделенных двоеточием. Подробнее см. load(). Вот несколько примеров:

# Grab the 'app' object from the 'myapp.controller' module and
# start a paste server on port 80 on all interfaces.
python -m bottle -server paste -bind 0.0.0.0:80 myapp.controller:app

# Start a self-reloading development server and serve the global
# default application. The routes are defined in 'test.py'
python -m bottle --debug --reload test

# Install a custom debug plugin with some parameters
python -m bottle --debug --reload --plugin 'utils:DebugPlugin(exc=True)'' test

# Serve an application that is created with 'myapp.controller.make_app()'
# on demand.
python -m bottle 'myapp.controller:make_app()''

Развертывание

Bottle по умолчанию работает на встроенном сервере wsgiref WSGIServer. Этот не-потоковый HTTP-сервер отлично подходит для разработки и раннего производства, но может стать узким местом в производительности при увеличении нагрузки сервера.

Самый простой способ повышения производительности — установить библиотеку многопоточного сервера, такую как paste или cherrypy, и указать Bottle использовать её вместо однопоточного сервера:

bottle.run(server='paste')

Этот и многие другие варианты развертывания описаны в отдельной статье: Развертывание

Словарь терминов

callback
Код программиста, который должен вызываться при возникновении некоторого внешнего действия. В контексте веб-фреймворков сопоставление между путями URL и кодом приложения часто достигается путем указания функции обратного вызова для каждого URL.
decorator
Функция, возвращающая другую функцию, обычно применяемая как преобразование функции с помощью синтаксиса @decorator. См. документацию python для определения функции для получения дополнительной информации о декораторах.
environ
Структура, где сохраняется информация обо всех документах в корне, и используется для перекрестной ссылки. Окружение заархивировано после этапа парсинга, так что последующие запуски потребуют только чтения и анализа новых и измененных документов.
handler function
Функция для обработки определенного события или ситуации. В веб-фреймворке приложение разрабатывается путем привязки функции обработки как функции обратного вызова к каждому конкретному URL, составляющему приложение.
source directory
Директория, которая, включая поддиректории, содержит все исходные файлы для одного проекта Sphinx.

© 2009–2017 Marcel Hellkamp
Licensed under the MIT License.
https://bottlepy.org/docs/0.12/tutorial.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API