python

Мега-Учебник Flask, Часть 11: Поддержка e-mail

  • четверг, 28 августа 2014 г. в 03:10:43
http://habrahabr.ru/post/234737/

Это одиннадцатая статья в серии, где я описываю свой опыт написания веб-приложения на Python с использованием микрофреймворка Flask.

Цель данного руководства — разработать довольно функциональное приложение-микроблог, которое я за полным отсутствием оригинальности решил назвать microblog.
Оглавление
Часть 1: Привет, Мир!
Часть 2: Шаблоны
Часть 3: Формы
Часть 4: База данных
Часть 5: Вход пользователей
Часть 6: Страница профиля и аватары
Часть 7: Unit-тестирование
Часть 8: Подписчики, контакты и друзья
Часть 9: Пагинация
Часть 10: Полнотекстовый поиск
Часть 11: Поддержка e-mail(данная статья)
Часть 12: Реконструкция
Часть 13: Дата и время
Часть 14: I18n and L10n
Часть 15: Ajax
Часть 16: Отладка, тестирование и профилирование
Часть 17: Развертывание на Linux (даже на Raspberry Pi!)
Часть 18: Развертывание на Heroku Cloud


Краткое повторение


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

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

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

Введение в Flask-Mail


К счастью для нас, Flask уже имеет расширение обрабатывающее электронную почту, и хоть оно не выполняет 100% задач, оно очень близко к этому.

Установка Flask-Mail в наше виртуальное окружение довольно простая. Пользователи на отличных от Windows системах должны сделать:

flask/bin/pip install flask-mail

Для пользователей Windows всё немножко сложней, потому что одна из зависимостей Flask-Mail не работает в этой OS. На Windows вам нужно сделать следующее:

flask\Scripts\pip install --no-deps lamson chardet flask-mail


Конфигурация


Ранее, когда мы добавляли Unit-тестирование мы добавили конфигурацию для Flask в которой указали email на который должны отсылаться уведомления об ошибках в production-версии нашего приложения. Та же информация используется для отправки почты приложением.

Нужно запомнить что нам нужно следующая информация:

  • сервер через который отправляются email
  • электронный адрес администратора


Это то, что мы сделали в предыдущей статье (файл config.py):

# email server
MAIL_SERVER = 'your.mailserver.com'
MAIL_PORT = 25
MAIL_USE_TLS = False
MAIL_USE_SSL = False
MAIL_USERNAME = 'you'
MAIL_PASSWORD = 'your-password'

# administrator list
ADMINS = ['you@example.com']


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

# email server
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 465
MAIL_USE_TLS = False
MAIL_USE_SSL = True
MAIL_USERNAME = 'your-gmail-username'
MAIL_PASSWORD = 'your-gmail-password'

# administrator list
ADMINS = ['your-gmail-username@gmail.com']


Мы также должны инициализировать объект Mail, т.к. это будет объект, который будет соединяться с SMTP сервером и отправлять электронные письма для нас(файл app/__init__.py):

from flask.ext.mail import Mail
mail = Mail(app)


Давайте отправим email.

Чтобы понять как работает Flask-Mail работает, нам нужно отправить email из командной строки. Давайте запустим Python из нашего виртуального окружения и наберем следующее:

>>> from flask.ext.mail import Message
>>> from app import app, mail
>>> from config import ADMINS
>>> msg = Message('test subject', sender = ADMINS[0], recipients = ADMINS)
>>> msg.body = 'text body'
>>> msg.html = '<b>HTML</b> body'
>>> with app.app_context():
...     mail.send(msg)
....


Фрагмент кода выше отправит письмо списку администраторов, указанных в config.py. Отправителем будет указан первый администратор из списка. Писмо будет иметь текстовую и HTML версии, что вы увидите зависит от настроек вашего почтового клиента. Обратите внимание, нам нужно создать app_context, чтобы отправить email. Последние релизы Flask-Mail это требуют. Контекст создается автоматически, когда запрос обрабатывается Flask.

Поскольку мы не находимся внутри запроса, мы можем создать контекст руками.

Теперь пришло время интегрировать этот код в наше приложение.

Простой email фреймворк


Сейчас мы напишем впомогательную функцию, которая отправляет email. Это просто более общая версия вышеуказанного теста. Мы поместим эту функцию в новый файл, который выделим для наших функций связанных с email (файл app/emails.py):

from flask.ext.mail import Message
from app import mail

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender = sender, recipients = recipients)
    msg.body = text_body
    msg.html = html_body
    mail.send(msg)


Обратите внимание что Flask-mail поддерживает больше чем мы используем. Например списки скрытых копий и вложения доступны, но мы не будем использовать их в нашем приложении.

Уведомления о подписках


Теперь, когда у нас есть базовый фреймворк для отправки электронной почты, мы можем написать функцию уведомляющую о подписчиках (файл app/emails.py):

from flask import render_template
from config import ADMINS

def follower_notification(followed, follower):
    send_email("[microblog] %s is now following you!" % follower.nickname,
        ADMINS[0],
        [followed.email],
        render_template("follower_email.txt", 
            user = followed, follower = follower),
        render_template("follower_email.html", 
            user = followed, follower = follower))


Увидели что-то неожиданное?
Наш старый друг — функция render_template создает вид письма. Если вы помните, мы использовали эту функцию чтобы рендерить все HTML шаблоны из нашего представления. Так же как наши HTML, тело письма идеальный кандидат на использование шаблонов. Мы хотим, насколько это возможно, отделить логику от представления, поэтому письма будут идти в папке с шаблонами вместе с другими view.

Итак, сейчас мы напишем шаблоны для текстовой и HTML версий для нашего уведомления. Это текстовая версия (файл app/templates/follower_email.txt):
Dear {{user.nickname}},

{{follower.nickname}} is now a follower. Click on the following link to visit {{follower.nickname}}'s profile page:

{{url_for('user', nickname = follower.nickname, _external = True)}}

Regards,

The microblog admin


Для HTML версии мы можем сделать всё немножко красивей и показывать аватар подписчика и информацию из профиля (файл app/templates/follower_email.html):

<p>Dear {{user.nickname}},</p>
<p><a href="{{url_for('user', nickname = follower.nickname, _external = True)}}">{{follower.nickname}}</a> is now a follower.</p>
<table>
    <tr valign="top">
        <td><img src=""></td>
        <td>
            <a href="{{url_for('user', nickname = follower.nickname, _external = True)}}">{{follower.nickname}}</a><br />
            {{follower.about_me}}
        </td>
    </tr>
</table>
<p>Regards,</p>
<p>The <code>microblog</code> admin</p>


Обратите внимание на _external = True в поле url_for нашего шаблона. По умолчанию, функция url_for генерирует URL'ы относительно текущей страницы. Для примера, code{url_for(«index»)} будет /index, в то время, когда мы ожидаем http://localhost:5000/index. В электронной почте нет доменного контекста, поэтому мы должны указывать полные адреса URL, которые включают домен, в этом нам и поможет _external.

Финальным шагом станет подключение отправки электронного письма с функцией представления, которая обрабатывает "Follow" (файл app/views.py):

from emails import follower_notification

@app.route('/follow/<nickname>')
@login_required
def follow(nickname):
    user = User.query.filter_by(nickname = nickname).first()
    # ...
    follower_notification(user, g.user)
    return redirect(url_for('user', nickname = nickname))


Сейчас вы должны создать двух пользователей (если вы еще не сделали этого) и сделать одного подписчиком другого, чтобы увидеть как работает уведомление по электронной почте. Это то что нужно? Мы закончили?

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

Но если вы поиграли с нашим приложением, вы заметили, что теперь, когда мы реализовали уведомления по электронной почте, после нажатия на «Follow» проходит несколько секунд, прежде чем браузер обновит страницу. А раньше это происходило мгновенно.

Итак, что произошло?

Проблема в том, что Flask-Mail отправляет электронные письма синхронно. Сервер блокируется, пока писмо отправляется и отправляет ответ браузеру, только когда сообщение будет доставлено. Вы можете представить что случится если мы попробуем отправить электронное письмо медленному серверу, или, что еще хуже, выключенному? Это плохо.

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

Асинхронные вызовы в Python


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

Оказывается Python уже поддерживает запуск асинхронных задач, даже более чем одним способом. Модули threading и multiprocessing могут нам помочь.

Запуск нового потока, каждый раз, когда нам нужно отправить письмо, гораздо менее ресурсоемкая операция чем запуск нового процесса, поэтому давайте переместим вызов mail.send(msg) в поток(файл app/emails.py):

from threading import Thread

def send_async_email(msg):
    mail.send(msg)

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender = sender, recipients = recipients)
    msg.body = text_body
    msg.html = html_body
    thr = Thread(target = send_async_email, args = [msg])
    thr.start()


Если вы тестируете функцию «Follow» вы обратите внимание что браузер показывает обновленную страницу прежде чем писмо будет отправлено.

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

Мы можем реализовать наше решение в виде декоратора. С декоратором код выше изменится на этот:

from decorators import async

@async
def send_async_email(msg):
    mail.send(msg)

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender = sender, recipients = recipients)
    msg.body = text_body
    msg.html = html_body
    send_async_email(msg)


Гораздо лучше, не правда ли?

Код который делает эту магию, на самом деле очень простой. Мы запишем его в новый файл (файл app/decorators.py):

from threading import Thread

def async(f):
    def wrapper(*args, **kwargs):
        thr = Thread(target = f, args = args, kwargs = kwargs)
        thr.start()
    return wrapper


Сейчас когда мы случайно создали хорошую основу для асинхронных задач мы можем сказать что все сделано!

Ради упражнения давайте рассмотрим как изменилось бы наше решение, если бы мы использовали процессы вместо потоков.
Мы не хотим чтобы новый процесс стартовал каждый раз, когда мы отправляем письмо, вместо этого мы можем использовать класс Pool из модуля multiprocessing. Этот класс создает необходимое количество процессов (которые являются форками основного процесса) и все процессы ждут задачи, которые передаются через метод apply_async. Это может быть полезным и интересным для загруженых сайтов, но сейчас мы остановимся на потоках.

Заключительные слова


Исходный код обновленного приложения доступен ниже:

Скачать microblog-0.11.zip.

Я получил несколько просьб разместить это приложение на GitHub или похожем сайте, я думаю это очень хорошая идея. Я буду работать над этим в ближайшем будущем. Оставайтесь на связи.

Спасибо за то что следите за серией моих туториалов. Надеюсь увидеть вас в следующих частях.

Miguel