Это одиннадцатая статья в серии, где я описываю свой опыт написания веб-приложения на Python с использованием микрофреймворка Flask.
Цель данного руководства — разработать довольно функциональное приложение-микроблог, которое я за полным отсутствием оригинальности решил назватьmicroblog
.
Краткое повторение
В последних уроках мы занимались, в основном, улучшениями связанными с нашей базой данных.
Сегодня мы позволим нашей базе немного отдохнуть, и вместо этого посмотрим на одну очень важную функцию, которая есть у большинства веб-приложений: возможность отправки 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 = ['[email protected]']
Разумеется вам придется ввести фактические данные в этот конфиг, для того, чтобы приложение действительно смогло отправлять вам электронные письма. Например, если вы хотите использовать приложение для отправки писем через 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 = ['[email protected]']
Мы также должны инициализировать объект 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
. Это может быть полезным и интересным для загруженых сайтов, но сейчас мы остановимся на потоках.
Заключительные слова
Я получил несколько просьб разместить это приложение на GitHub или похожем сайте, я думаю это очень хорошая идея. Я буду работать над этим в ближайшем будущем. Оставайтесь на связи.
Спасибо за то что следите за серией моих туториалов. Надеюсь увидеть вас в следующих частях.