воскресенье, 28 июля 2013 г.

Декораторы представлений во Flask

Перевод статьи: View Decorators

В Python имеется любопытная функциональность, которая называется декораторами функций. Декораторы позволяют сделать веб-приложения изящнее. Поскольку каждое представление во Flask является функцией, можно использовать декораторы для добавления дополнительной функциональности к одной или более функций. Декоратор route() - один из тех, который вы возможно уже используете. Но можно реализовать собственные декораторы. Например, представьте, что есть представление, которое должно быть доступно только аутентифицированным пользователям. Если пользователь вошёл на сайт, но не аутентифицировался, его нужно перенаправить на страницу аутентификации. Это хороший случай, в котором декоратор может прийтись как нельзя кстати.

Декоратор "необходима аутентификация"

Ну что ж, давайте реализуем этот декоратор. Декоратор - это функция, возвращающая функцию. На самом деле очень просто. Единственное, о чём нужно помнить при реализации чего-то подобного, это об обновлении __name__, __module__ и других атрибутов функции. Часто об этом забывают, нам не потребуется делать это вручную, потому что есть функция, которая может сделать это за нас и используется в качестве декоратора (functools.wraps()).

Пример подразумевает, что страница для ввода учётных данных называется 'login', а текущий пользователь хранится в g.user. Если пользователь не аутентифицирован, то в g.user хранится None:
from functools import wraps
from flask import g, request, redirect, url_for

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if g.user is None:
            return redirect(url_for('login', next=request.url))
        return f(*args, **kwargs)
    return decorated_function
Итак, как же теперь воспользоваться этим декоратором? Применим его в качестве наиболее глубоко вложенного декоратора. При дальнейшем применении декораторов помните, что декоратор route() должен быть самым внешним:
@app.route('/secret_page')
@login_required
def secret_page():
    pass
Кэширующий декоратор

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

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

Задекорированная функция будет работать следующим образом:
  1. приготовит уникальный ключ кэша для текущего запроса, основываясь на текущем пути.
  2. получит значение для этого ключа из кэша. Если кэш вернул что-нибудь, мы вернём это значение.
  3. в противном случае будет вызвана оригинальная функция, возвращённое значение будет помещено в кэш с указанным временем кэширования (по умолчанию - 5 минут).
Вот код:
from functools import wraps
from flask import request

def cached(timeout=5 * 60, key='view/%s'):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            cache_key = key % request.path
            rv = cache.get(cache_key)
            if rv is not None:
                return rv
            rv = f(*args, **kwargs)
            cache.set(cache_key, rv, timeout=timeout)
            return rv
        return decorated_function
    return decorator
В этом примере подразумевается, что объект cache уже инициализирован, как описано в разделе Кэширование.

Шаблонизирующий декоратор

В TurboGears некоторое время назад было распространено использование шаблонизирующих декораторов. Смысл этого декоратора заключается в том, что функция представления возвращает словарь со значениями для шаблона, а шаблон отрисовывается автоматически. В этом случае следующие три примера делают одно и то же:
@app.route('/')
def index():
    return render_template('index.html', value=42)

@app.route('/')
@templated('index.html')
def index():
    return dict(value=42)

@app.route('/')
@templated()
def index():
    return dict(value=42)
Как можно заметить, если имя шаблона не указано, используется конечная точка карты URL с точками, преобразованными в косые черты и с добавленным справа текстом '.html'. В противном случае, используется шаблон с указанным именем. Когда задекорированная функция завершается, возвращённый ею словарь передаётся в функцию отрисовки шаблона. Если ничего не возвращено, подразумевается пустой словарь, а если возвращён не словарь, мы возвращаем это значение неизменным. Таким образом по-прежнему можно пользоваться функцией redirect или возвращать обычные строки.

Вот код этого декоратора:
from functools import wraps
from flask import request

def templated(template=None):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            template_name = template
            if template_name is None:
                template_name = request.endpoint.replace('.', '/') + '.html'
            ctx = f(*args, **kwargs)
            if ctx is None:
                ctx = {}
            elif not isinstance(ctx, dict):
                return ctx
            return render_template(template_name, **ctx)
        return decorated_function
    return decorator
Декоратор конечной точки

Если хочется воспользоваться системой маршрутизации werkzeug для достижения большей гибкости, нужно отобразить конечную точку в соответствии с правилом для для функции представления. Это можно сделать с помощью декоратора. Например:
from flask import Flask
from werkzeug.routing import Rule

app = Flask(__name__)
app.url_map.add(Rule('/', endpoint='index'))

@app.endpoint('index')
def my_index():
    return "Hello world"
Примечания переводчика

Этот и другие переводы можно найти на сайте проекта перевода документации по Flask. Автор проекта - Виталий Кузьмин aka ferm32.

Кэширование во Flask

Перевод статьи: Caching

Когда приложение работает медленно, можно попробовать воспользоваться кэшированием. По крайней мере это самый простой способ ускорения программы. Как работает кэширование? Допустим, имеется функция, которая работает довольно долго, но результаты её работы будут пригодны для использования даже через 5 минут. Смысл в том, что на самом деле мы на некоторое время помещаем результаты расчётов в кэш.

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

Настройка кэша

Объект кэша создаётся единожды и продолжает работать примерно так же, как это происходит с объектами Flask. Если речь идёт о сервере разработки, то можно создать объект SimpleCache, который представляет собой простейший кэш, хранящий элементы в памяти интерпретатора Python:
from werkzeug.contrib.cache import SimpleCache
cache = SimpleCache()
Для использования memcached нужно установить один из поддерживаемых модулей memcache (его можно взять на PyPI) и запустить где-нибудь сервер memcached. Теперь можно подключиться к серверу memcached:
from werkzeug.contrib.cache import MemcachedCache
cache = MemcachedCache(['127.0.0.1:11211'])
Если вы пользуетесь App Engine, можно легко подключиться к серверу memcache из App Engine:
from werkzeug.contrib.cache import GAEMemcachedCache
cache = GAEMemcachedCache()
Использование кэша

Как теперь воспользоваться этим кэшем? Имеется две очень важные операции: get() и set(). Вот как их использовать:

Чтобы получить элемент из кэша, вызовите get(), указав ей ключ - имя элемента. Если что-нибудь есть в кэше, оно будет возвращено. В противном случае функция вернёт None:
rv = cache.get('my-item')
Чтобы добавить элемент в кэш, воспользуйтесь методом set(). Первый аргумент - это ключ, а второй - его значение. Также можно указать время кэширования, по истечении которого элемент будет автоматически удалён.

Вот полный пример того, как это обычно выглядит:
def get_my_item():
    rv = cache.get('my-item')
    if rv is None:
        rv = calculate_value()
        cache.set('my-item', rv, timeout=5 * 60)
    return rv
Примечания переводчика

Под App Engine подразумевается облачный сервис веб-приложений от Google: https://appengine.google.com/start. Чуть подробнее о нём можно узнать из статьи на Википедии: Google App Engine.

Этот и другие переводы можно найти на сайте проекта перевода документации по Flask. Автор проекта - Виталий Кузьмин aka ferm32.

воскресенье, 21 июля 2013 г.

Журналирование ошибок приложения на Flask

Перевод статьи: Logging Application Errors

Новинка версии 0.3.

В приложениях и серверах иногда происходят ошибки. Рано или поздно вы увидите исключение на сервере в эксплуатации. Даже если ваш код на 100% правильный, вы всё равно будете время от времени видеть исключения. Почему? Потому что может сломаться что-то другое. Вот некоторые ситуации, в которых совершенный код может приводить к ошибкам на сервере:
  • клиент завершил запрос раньше, а приложение по-прежнему ожидает поступления данных.
  • сервер базы данных был перегружен и не смог обработать запрос.
  • в файловой системе закончилось место.
  • сломался жёсткий диск.
  • перегружен сервер-бэкэнд.
  • ошибка в используемой библиотеке.
  • ошибка сетевого соединения сервера с другой системой.
И это только небольшой список причин, который можно продолжить. Как же справляться с проблемами такого рода? По умолчанию, если приложение запущено в рабочем режиме, Flask покажет очень простую страницу и занесёт исключение в журнал.

Но для обработки ошибок можно сделать и больше, если задать соответствующие настройки.

Письма об ошибках

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

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

Flask использует встроенную систему журналирования Python, и действительно может отправлять письма об ошибках, чем вы можете воспользоваться. Вот как можно настроить систему журналирования Flask для отправки писем об исключениях:
ADMINS = ['yourname@example.com']

if not app.debug:
    import logging
    from logging.handlers import SMTPHandler
    mail_handler = SMTPHandler('127.0.0.1',
                               'server-error@example.com',
                               ADMINS, 'Сбой в приложении')
    mail_handler.setLevel(logging.ERROR)
    app.logger.addHandler(mail_handler)
Что это даст? Мы создали новый обработчик SMTPHandler, который отправит письма через почтовый сервер с IP-адресом 127.0.0.1 на все адреса из ADMINS с адреса server-error@example.com и с темой "Сбой в приложении". Если почтовый сервер требует авторизации, можно указать необходимые для неё данные. За информацией о том, как это сделать, обратитесь к документации на SMTPHandler.

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

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

Журналирование в файл

Даже если вы получили письмо, вам скорее всего захочется посмотреть и на предупреждения в журнале. Хорошо бы сохранить как можно больше информации, полезной для отладки проблемы. Отметим, что ядро Flask само по себе не выдаёт предупреждений, поэтому вам самим нужно позаботиться о том, чтобы генерировать предупреждения в вашем коде, в случае если что-то пошло не так.

Эта пара обработчиков поставляется в комплекте с системой журналирования, но не каждый из них подходит для начального журналирования ошибок. Наиболее интересными могут показаться следующие:
  • FileHandler - ведёт журнал ошибок в файле.
  • RotatingFileHandler - ведёт журнал ошибок в фале и создаёт новый файл после определённого количества сообщений.
  • NTEventLogHandler - использует системный журнал событий Windows. Может пригодиться при развёртывании приложения на Windows-компьютере.
  • SysLogHandler - отправляет сообщения в syslog, системный журнал UNIX.
Как только вы подберёте подходящий обработчик журнала, можете настроить обработчик SMTP из примера выше. Просто убедитесь в том, что снизили порог критичности сообщений (я рекомендую WARNING):
if not app.debug:
    import logging
    from themodule import TheHandlerYouWant
    file_handler = TheHandlerYouWant(...)
    file_handler.setLevel(logging.WARNING)
    app.logger.addHandler(file_handler)
Управление форматом журнала

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

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

Вот примеры настройки:

Журналирование на почту

from logging import Formatter
mail_handler.setFormatter(Formatter('''
Message type: %(levelname)s
Location: %(pathname)s:%(lineno)d
Module: %(module)s
Function: %(funcName)s
Time: %(asctime)s
Message:
%(message)s
'''))
Журналирование в файл
from logging import Formatter
file_handler.setFormatter(Formatter(
'%(asctime)s %(levelname)s: %(message)s '
'[in %(pathname)s:%(lineno)d]'
))
Сложное форматирование журналов

Вот список полезных переменных форматирования для подстановки в строку формата. Отметим, что этот список не полный, полный список можно найти в официальной документации пакета журналирования.
Формат Описание
%(levelname)s Уровень серьёзности сообщения ('DEBUG' - отладочное, 'INFO' - информационное, 'WARNING' - предупреждение, 'ERROR' - ошибка, 'CRITICAL' - критичное).
%(pathname)s Полный путь к файлу с исходным текстом, из которого была вызвана функция журналирования (если доступен).
%(filename)s Имя файла с исходным текстом.
%(module)s Модуль (часть имени файла).
%(funcName)s Имя функции, из который была вызвана функция журналирования.
%(lineno)d Номер строки в файле исходного текста, в которой произошёл вызов функции журналирования (если доступна).
%(asctime)s Время создания записи в журнале в человеко-читаемом виде. По умолчанию используется формат "2003-07-08 16:49:45,896" (числа после запятой - это миллисекунды). Можно изменить путём создания класса-наследника от formatter и заменой метода formatTime().
%(message)s Журнальное сообщение, полученное из выражения msg % args
Если вы хотите выполнить тонкую настройку форматирования, нужно создать класс-наследник от formatter. formatter имеет три полезных метода:

format():
  • Занимается собственно форматированием. Принимает объект LogRecord и возвращает отформатированную строку.
formatTime():
  • Вызывается для форматирования asctime. Если нужно задать другой формат времени, можно заменить этот метод.
formatException():
  • Вызывается для форматирования исключений. Принимает кортеж exc_info и возвращает строку. В большинстве случаев подойдёт метод по умолчанию, поэтому скорее всего вам не потребуется его заменять.

За более подробной информацией обратитесь к официальной документации.

Другие библиотеки

Таким образом мы настроили журналирование событий, порождаемых самим приложением. Другие библиотеки могут вести собственный журнал. Например, SQLAlchemy широко использует журналирование в собственном ядре. Хотя этот способ пригоден для настройки сразу всех средств журналирования в пакете logging, пользоваться им не рекомендуется. Может возникнуть ситуация, когда нужно различать приложения, работающие в пределах одного интерпретатора Python, но будет невозможно сделать для них отдельные настройки журналирования.

Вместо этого рекомендуется выяснить, какие средства журналирования нужны, получить их с помощью функции getLogger() и перебрать все присоединённые к ним обработчики:
from logging import getLogger
loggers = [app.logger, getLogger('sqlalchemy'),
           getLogger('otherlibrary')]

for logger in loggers:
    logger.addHandler(mail_handler)
    logger.addHandler(file_handler)
Отладка ошибок приложения

Для приложений в эксплуатации настройте журналирование и уведомления так, как описано в разделе Журналирование ошибок приложения. Этот раздел предоставит указания для отладки конфигурации развёртывания и более глубокого исследования с использованием полнофункционального отладчика Python.

В случае сомнений запускайте вручную

Возникли проблемы с настройкой приложения в эксплуатации? Если имеется доступ к командной строке на сервере, проверьте, можете ли вы запустить приложение вручную из командной строки в режиме разработки. Убедитесь, что запустили его под той же учётной записью, под которой оно установлено, чтобы отследить ошибки, связанные с неправильной настройкой прав доступа. Можете воспользоваться встроенным во Flask сервером разработки, передав ему аргумент debug=True на сервере эксплуатации, что может помочь в отлове проблем с настройками, но убедитесь, что делаете это временно в управляемом окружении. Не запускайте приложение в эксплуатацию с аргументом debug=True.

Работа с отладчиками

Для более глубокого исследования можно выполнить трассировку кода. Flask содержит отладчик в стандартной поставке (смотрите Режим отладки). Если вам нравится пользоваться другим отладчиком Python, учтите, что они могут мешать друг другу. Можно указать несколько опций, чтобы использовать ваш любимый отладчик:
  • debug - указывает, нужно ли включить режим отладки и захват исключений.
  • use_debugger - указывает, нужно ли использовать отладчик, встроенный во Flask.
  • use_reloader - указывает, нужно ли перезагружать и перезапускать процесс, если произошло исключение.
Опция debug должна иметь значение True (то есть, исключения должны захватываться), для того чтобы учитывались две следующие опции.

Если вы используете Aptana/Eclipse для отладки, вам нужно установить обе опции use_debugger и use_reloader в False.

Возможно, лучшие всего настроить эти опции в файле config.yaml (измените блок, так как вам нужно):
FLASK:
    DEBUG: True
    DEBUG_WITH_APTANA: True
В точке входа в ваше приложение (main.py), нужно написать что-то вроде этого:
if __name__ == "__main__":
    # Чтобы разрешить aptana получать ошибки, задайте use_debugger=False
    app = create_app(config="config.yaml")
    if app.debug: use_debugger = True
    try:
        # Отключаем отладчик Flask, если запрошено использование внешнего отладчика
        use_debugger = not(app.config.get('DEBUG_WITH_APTANA'))
    except:
        pass
    app.run(use_debugger=use_debugger, debug=app.debug,
            use_reloader=use_debugger, host='0.0.0.0')
Примечания переводчика

Этот и другие переводы можно найти на сайте проекта перевода документации по Flask. Автор проекта - Виталий Кузьмин aka ferm32.

воскресенье, 14 июля 2013 г.

Thunar: доступ по SFTP и миниатюры

Ещё один тривиальный пост о том, как подключаться из Thunar по SFTP и о том, как заставить Thunar показывать миниатюры.

До недавних пор у меня не возникало желания подключаться по SFTP чем-либо, кроме консольного клиента sftp, но буквально только что такое желание возникло. Всё оказалось очень просто - нужно лишь поставить пакет gvfs-backends:
# apt-get install gvfs-backends
Теперь вводим в адресной строке Thunar URI нужного нам ресурса (sftp://ufadeb.homelinux.org):

В открывшемся окне вводим учётные данные пользователя удалённого компьютера:

И получаем доступ:

В адресной строке можно также указать имя пользователя (sftp://stupin@ufadeb.homelinux.org), в таком случае система предложит лишь ввести его пароль.

Заодно настроил построение миниатюр. Ранее этим заведовал пакет thunar-tumbnailers, однако в мире Linux постоянно всё течёт и меняется и теперь этим занимается пакет с названием tumbler. Нужно лишь поставить пакет:
# apt-get install tumbler
Затем, выставить в настройках Thunar галочку, предписывающую показывать миниатюры, если это возможно:

И Thunar начнёт показывать миниатюры картинок:

воскресенье, 7 июля 2013 г.

Всплывающие сообщения во Flask

Перевод статьи: Message Flashing

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

Пример всплывающих сообщений

Вот полный пример:
from flask import Flask, flash, redirect, render_template, \
     request, url_for

app = Flask(__name__)
app.secret_key = 'some_secret'

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
    error = None
    if request.method == 'POST':
        if request.form['username'] != 'admin' or \
           request.form['password'] != 'secret':
            error = u'Неправильные учётные данные'
        else:
            flash(u'Вы успешно вошли')
            return redirect(url_for('index'))
    return render_template('login.html', error=error)

if __name__ == "__main__":
    app.run()
А вот шаблон макета layout.html, отображающий сообщение:
<!doctype html>
<title>Моё приложение</title>
{% with messages = get_flashed_messages() %}
  {% if messages %}
    <ul class=flashes>
    {% for message in messages %}
      <li>{{ message }}</li>
    {% endfor %}
    </ul>
  {% endif %}
{% endwith %}
{% block body %}{% endblock %}
А это - шаблон index.html:
{% extends "layout.html" %}
{% block body %}
  <h1>Обзор</h1>
  <p>Не желаете ли <a href="{{ url_for('login') }}">войти?</a>
{% endblock %}
И, конечно, шаблон страницы входа:
{% extends "layout.html" %}
{% block body %}
  <h1>Вход</h1>
  {% if error %}
    <p class=error><strong>Ошибка:</strong> {{ error }}
  {% endif %}
  <form action="" method=post>
    <dl>
      <dt>Имя пользователя:
      <dd><input type=text name=username value="{{
        request.form.username }}">
      <dt>Пароль:
      <dd><input type=password name=password>
    </dl>
    <p><input type=submit value="Войти">
  </form>
{% endblock %}
Всплывающие сообщения с категориями

Новинка версии 0.3.

Всплывающему сообщению можно назначить категорию. По умолчанию, если категория не указана, используется категория 'message' - сообщение. Для более качественной обратной связи с пользователем можно указывать другие категории. Например, сообщения об ошибках должны отображаться на красном фоне.

Для вывода всплывающего сообщения с другой категорией, просто передайте её вторым аргументом функции flash():
flash(u'Указан неправильный пароль', 'error')
Внутри шаблона можно сообщить функции get_flashed_messages(), что нужно вернуть ещё и категорию. В этом случае цикл выглядит несколько иначе:
{% with messages = get_flashed_messages(with_categories=true) %}
  {% if messages %}
    <ul class=flashes>
    {% for category, message in messages %}
      <li class="{{ category }}">{{ message }}</li>
    {% endfor %}
    </ul>
  {% endif %}
{% endwith %}
Это просто пример того, как можно отображать всплывающие сообщения. Можно использовать категорию для добавления к сообщению префикса, например, <strong>Ошибка:</strong>.

Фильтрация всплывающих сообщений

Новинка версии 0.9.

При желании можно передать список категорий, который будет использован функцией get_flashed_messages() для фильтрации сообщений. Это полезно в тех случаях, если нужно выводить сообщения каждой категории в отдельном блоке.
{% with errors = get_flashed_messages(category_filter=["error"]) %}
  {% if errors %}
    <div class="alert-message block-message error">
      <a class="close" href="#">×</a>
      <ul>
      {%- for msg in errors %}
        <li>{{ msg }}</li>
      {% endfor -%}
      </ul>
    </div>
  {% endif %}
{% endwith %}
Примечания переводчика

В оригинале были "мигающие" или "вспыхивающие" сообщения, но я счёл дословный перевод не вполне соответствующим истине - эти сообщения не мигают и не вспыхивают. Хотя они также и не всплывают, но это слово показалось мне более удачным.

Этот и другие переводы можно найти на сайте проекта перевода документации по Flask. Автор проекта - Виталий Кузьмин aka ferm32.