Flask - Email
Date: Apr. 28th, 2019
Lecturer: Chia
OUTLINE
-
使用Flask-Mail擴充套件
- 透過Python Shell寄送
- 整合email與app
- 寄送非同步email
學習目標:使用Flask app傳送email。
使用Flask-Mail擴充套件
1. 使用pip安裝Flask-Mail擴充套件
(venv) $ pip install flask-mail
- 此擴充套件會連接SMTP伺服器。
- SMTP是一種傳輸郵件的規範。
E-mail 要件
os.environ.get('MAIL_USERNAME')
os.environ.get('MAIL_PASSWORD')
sender
recipients
2. 連接外部的SMTP
- 目標:使用Gmail帳號傳送email
- 為了保護帳號資訊,故不把帳號資訊直接寫在腳本裡,而是從環境變數匯入。
### db_demo.py:設置Flask-Mail來使用Gmail ###
import os
# ...
app.config['MAIL_SERVER'] = 'smtp.googlemail.com'
app.config['MAIL_PORT'] = 587
app.config['MAIL_USE_TLS'] = True
app.config['MAIL_USERNAME'] = os.environ.get('MAIL_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MAIL_PASSWORD')
3. 設定Gmail以接受SMTP身份驗證
Google帳號設定網頁 -> 安全性 -> "低安全性應用程式存取權" 開啟!
4. 初始化Flask-Mail
### db_demo.py ###
from flask_mail import Mail
mail = Mail(app)
5. Windows cmd 設定環境變數
- 匯入email伺服器使用者名稱與密碼的環境變數
- 利用Gmail帳密,來使用Google的SMTP server
(venv) $ set MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ set MAIL_PASSWORD=**************
透過Python Shell寄送
- 目標:透過Python Shell測試設置的結果。
- 做法:必須每次手動建立email訊息 (HTML/純文字)。
(venv) $ set FLASK_APP=db_demo.py
(venv) $ flask shell
>>> from flask_mail import Message
>>> from db_demo import mail
>>> msg = Message('test_email', sender='pcsh110576@gmail.com',
recipients=['404040523@mail.fju.edu.tw'])
>>> msg.html = 'This is the <b>flask test</b> mail.'
>>> with app.app_context():
... mail.send(msg)
>>>
>>> msg.body = 'This is the plain text body'
LAB - 1
-
透過Python Shell寄送純文字訊息
整合email與app
- 目標:寫成一個函式,以避免每次手動建立email訊息。
- 優點:彈性高,且可將Jinja2模板轉譯成email內文。
from flask_mail import Message
#主旨的開頭文字
app.config['MAIL_SUBJECT_PREFIX'] = '[Flasky]'
#寄件者的地址,與SMTP Server相同帳號
app.config['MAIL_SENDER'] = 'Flasky Admin <pcsh110576@gmail.com>'
#send_email(收件人地址, 主旨, email內文模板, 關鍵字引數)
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject,
sender = app.config['MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
mail.send(msg)
msg.html = render_template(template + '.html', **kwargs)
E-mail 要件
os.environ.get('MAIL_USERNAME')
os.environ.get('MAIL_PASSWORD')
app.config['MAIL_SENDER']
app.config['MAIL_ADMIN']
- 純文字模板
- HTML模板
建立位置:templates/mail/new_user.txt
User {{ user.username }} has joined. (txt)
建立位置:templates/mail/new_user.html
User <b>{{ user.username }}</b> has joined. (html)
建立email內文模板
- 目標:當收到表單送來的新名字時,寄email給管理者。
### db_demo.py ###
# ...
#收件者
app.config['MAIL_ADMIN'] = os.environ.get('FLASKY_ADMIN')
@app.route('/name_form', methods=['GET', 'POST'])
def name_form():
# ...
session['known'] = False
### 新增
if app.config['MAIL_ADMIN']:
send_email(app.config['MAIL_ADMIN'], 'New User', 'mail/new_user', user=user)
else:
session['known'] = True
# ...
擴充view函式 (db_demo.py)
Windows cmd 設定環境變數
(venv) $ set MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ set MAIL_PASSWORD=**************
(venv) $ set FLASKY_ADMIN=404040523@mail.fju.edu.tw
LAB - 2
使用HTML模板來寄送Email
先建立HTML模板,再設定環境變數(MAIL_USERNAME、MAIL_PASSWORD、FLASKY_ADMIN),最終寄出Email。
寄送非同步email
- 目標:將email寄送函式移往背景執行緒,以避免處理請求時產生延遲。
#當你要同時做很多事情時,就可以用到threading達成多執行緒。
from threading import Thread
def send_email(to, subject, template, **kwargs):
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject,
sender = app.config['MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
#msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
Flask -
Large App Structure
Date: May. 12th, 2019
Lecturer: Chia
OUTLINE
- App基本結構
- 組態設定選項config.py
- App套件
- App主腳本 db_demo.py
- requirements檔
- 單元測試
- 執行App
App基本結構
C:\Users\pcsh1\flask_proj
│ config.py --- 組態設定
│ db_demo.py --- 定義Flask App實例 & 協助管理App的工作
│ requirements.txt --- 套件依賴項目,以便重建一致的虛擬環境
│
├─app --- (app套件) Flask App
├─migrations --- 資料庫遷移腳本
├─tests --- (tests套件) 單元測試
└─venv --- Python虛擬環境
- 學習目標:架構較大型的App,以增加擴充性。
- 原因:db_demo.py腳本變大,不便使用。
組態設定選項 (config.py)
import os
basedir = os.path.abspath(os.path.dirname(__file__))
class Config:
SECRET_KEY = os.environ.get('SECRET_KEY') or 'hard to guess string'
MAIL_SERVER = os.environ.get('MAIL_SERVER', 'smtp.googlemail.com')
MAIL_PORT = int(os.environ.get('MAIL_PORT', '587'))
MAIL_USE_TLS = os.environ.get('MAIL_USE_TLS', 'true').lower() in ['true', 'on', '1']
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
MAIL_SUBJECT_PREFIX = '[Flasky]'
MAIL_SENDER = 'Flasky Admin <pcsh110576@gmail.com>'
MAIL_ADMIN = os.environ.get('FLASKY_ADMIN')
SQLALCHEMY_TRACK_MODIFICATIONS = False
@staticmethod #實作空的init_app()方法
def init_app(app):
pass
- Config基礎類別:所有組態共同的設定
class DevelopmentConfig(Config):
DEBUG = True
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'mysql://flaskdb_chia:gibe258deny700@localhost:3306/flaskdb'
class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \
'sqlite://'
#預設為【記憶體內部資料庫】,測試完成後將不保留資料。
class ProductionConfig(Config):
SQLALCHEMY_DATABASE_URI = os.environ.get('DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data.sqlite')
config = {
'development': DevelopmentConfig,
'testing': TestingConfig,
'production': ProductionConfig,
'default': DevelopmentConfig #將Development組態,註冊為預設值
}
-
子類別:分別定義特定組態專屬的設定
- 分離Development、Testing與Production期間所使用的不同資料庫,以免相互干擾。
App套件
C:\Users\pcsh1\flask_proj
├─app
│ email.py --- email支援函式
│ models.py --- 資料庫模型
│ __init__.py
│
├─main
│ │ errors.py
│ │ forms.py
│ │ views.py
│ │ __init__.py
│
├─static --- 靜態檔案
├─templates --- 模板
│ 404.html
│ 500.html
│ base.html
│ base_index.html
│ webform.html
│
└─mail
│ new_user.html
│ new_user.txt
App套件
- 使用App工廠 (app/__init__.py)
-
在藍圖中實作App功能
- 建立主藍圖
- 註冊主藍圖
- 主藍圖裡的錯誤處理函式
- 主藍圖裡的app路由
- 表單物件
- app/email.py
- app/models.py
使用App工廠
- 原因:
- 用單一檔案來建立App,等同在全域範圍內建立App,固然方便,但無法動態套用組態的改變。
- 亦即【已經幫你買好固定規格的家具】
- 用單一檔案來建立App,等同在全域範圍內建立App,固然方便,但無法動態套用組態的改變。
- 目的:
- Allows different Flask applications to be created with different configurations.
- 可建立多個App實例 (家具)
-
Useful during testing, where the configuration can often be different from development or production.
-
Application Factory
The steps of initialize a Flask project :
- Creating the Flask application as an instance of the Flask class. (空房子)
- Configuring the Flask application (設定家具規格)
- Initializing the extensions to be used (買家具)
- Registering the Blueprints in the project (劃分房間)
These steps can be put together in a single function called an Application Factory function.
### app/__init__.py:App套件建構式 ###
##匯入多數目前正在使用的Flask擴充套件
from flask import Flask
from flask_bootstrap import Bootstrap
from flask_mail import Mail
from flask_moment import Moment
from flask_sqlalchemy import SQLAlchemy
#匯入config.py
from config import config
#尚未初始化
bootstrap = Bootstrap()
db = SQLAlchemy()
mail = Mail()
moment = Moment()
To start, import all of the modules (including the Flask extensions) that are needed for the Flask project:
Next, create the instances(實例) of each of the Flask extensions without initializing them:
#create_app()為"App工廠函式",回傳建立好的App實例。
def create_app(config_name): #config_name接收組態名稱,以讓App使用
#建立App實例
app = Flask(__name__)
#將config.py內定義的組態類別,其所儲存的組態設定直接匯入App
app.config.from_object(config[config_name])
#App初始化,使用init_app()
config[config_name].init_app(app)
bootstrap.init_app(app)
db.init_app(app)
mail.init_app(app)
moment.init_app(app)
### 註冊主藍圖 ###
# 在這裡指派"路由"與"錯誤頁面處理函式" #直到藍圖被app註冊時,才成為app的一部分。
return app
Next, the actual Application Factory function (create_app) is defined:
- Initialize each Flask extension.
- Blueprints are registered with the Flask application.
你以為這樣就結束了嗎...
-
路由 & 自定義錯誤頁面處理程式?
-
下一節的主題 --- 藍圖
在藍圖中實作App功能
- 原因:
- 單一腳本app時,app實例位於全域範圍內,故能用app.route裝飾器來定義路由。
- 現在app在非全域範圍內,故使用藍圖(blueprint)的方式來定義路由。
-
藍圖(blueprint)
- 可以定義路由和錯誤處理函式。
- 當在藍圖裡定義時,處於休眠狀態。直到藍圖被app註冊時,才成為app的一部分。
App工廠的轉化工作,引出了路由的複雜化...
Blueprints
-
Allow you to cleanly organize the source code of your Flask application.
- Each Blueprint encapsulates a significant piece of functionality in your application.
- One project can have multiple Blueprints. (房間)
1. 建立主藍圖
- 目標:在app套件內,建立子套件來乘載app的第一個藍圖。
- 【我要建立一個房間】
- 特別留意:main(藍圖名稱)必須先被定義,views、errors才能匯入main藍圖物件,否則將會匯入失敗。
### app/main/__init__.py ###
from flask import Blueprint
#實例化Blueprint類別。
#此類別的建構式必須接收兩個引數:藍圖名稱與藍圖所在的模組或套件。
main = Blueprint('main', __name__)
#匯入app套件的模組,以建立與藍圖的關係 (包含app路由&錯誤頁面處理函式)
# . 代表目前的套件; .. 代表目前套件的父代
from . import views, errors #相對匯入
2. 註冊主藍圖
- 藍圖是使用create_app()工廠函式內的app來註冊。
- 【我真的建立一個房間】
### app/__init__.py ###
# ...
### 註冊主藍圖 ###
#連接到app/main/__init.py
from .main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app
3. 主藍圖裡的錯誤處理函式
- 若要設定遍及app範圍,則須改用app_errorhandler裝飾器。
### app/main/errors.py ###
from flask import render_template
from . import main
# 全域(app_errorhandler裝飾器)的錯誤處理函式 #
@main.app_errorhandler(404) #路由裝飾器來自藍圖(main.route)
def page_not_found(e):
return render_template('404.html'), 404
@main.app_errorhandler(500)
def internal_server_error(e):
return render_template('500.html'), 500
4. 主藍圖裡的app路由
- 在藍圖裡編寫view函式,差異有二:
- 路由裝飾器來自藍圖,故使用main.route。
- url_for()函式,改為套用藍圖名稱(main)的main.index。
- 各個不同的藍圖可使用相同的端點名稱來定義view函式,不會造成衝突!
- main.index
- core.index
from flask import render_template, session, redirect, url_for, flash, current_app
from datetime import datetime
from . import main
from .forms import NameForm #表單物件
from .. import db
from ..models import User
from ..email import send_email
### app/main/views.py ###
@main.route('/name_form', methods=['GET', 'POST']) #路由裝飾器來自藍圖(main.route)
def name_form():
form = NameForm()
if form.validate_on_submit():
# 用表單收到的name在資料庫中查詢
user = User.query.filter_by(username=form.name.data).first()
# 查無此姓名的話,將該姓名寫入資料庫
if user is None:
user = User(username=form.name.data)
db.session.add(user)
db.session.commit() #把表單所輸入的,寫進資料庫
session['known'] = False
if current_app.config['MAIL_ADMIN']:
send_email(current_app.config['MAIL_ADMIN'], 'New User', 'mail/new_user', user=user) ###
else:
session['known'] = True
session['name'] = form.name.data
form.name.data = ''
old_name = session.get('name')
if old_name is not None and old_name != form.name.data:
flash('Looks like you have changed your name!')
# 將命名空間套用到在藍圖中所定義的所有端點
return redirect(url_for('main.name_form')) #等同於 '.name_form'
return render_template('webform.html', form=form, name=session.get('name'),
current_time=datetime.utcnow(), known=session.get('known', False))
5. 表單物件
from flask_wtf import FlaskForm
from wtforms import StringField, SubmitField
from wtforms.validators import DataRequired
class NameForm(FlaskForm):
name = StringField('What is your name?',
validators=[DataRequired()])
submit = SubmitField('Submit')
### app/main/forms.py ###
app/email.py
from threading import Thread
from flask import render_template, current_app ###
from flask_mail import Message
from . import mail
def send_email(to, subject, template, **kwargs):
app = current_app._get_current_object() ###
msg = Message(app.config['MAIL_SUBJECT_PREFIX'] + subject,
sender = app.config['MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
#msg.html = render_template(template + '.html', **kwargs)
thr = Thread(target=send_async_email, args=[app, msg])
thr.start()
return thr
def send_async_email(app, msg):
with app.app_context():
mail.send(msg)
app/models.py
from . import db
class Role(db.Model):
__tablename__ = 'roles'
id = db.Column(db.Integer, primary_key = True)
name = db.Column(db.String(64), unique = True)
users = db.relationship('User', backref='role', lazy='dynamic')
def __repr__(self):
return '<Role %r>' % self.name
class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key = True)
username = db.Column(db.String(64), unique = True, index = True)
role_id = db.Column(db.Integer, db.ForeignKey('roles.id'))
def __repr__(self):
return '<User %r>' % self.username
App主腳本 (db_demo.py)
import os
from app import create_app, db
from app.models import User, Role
from flask_migrate import Migrate
#建立一個App,從環境變數或預設來取得組態設置。
app = create_app(os.getenv('FLASK_CONFIG') or 'default')
##初始化Flask-Migrate
migrate = Migrate(app, db)
##Python殼層的自訂context
@app.shell_context_processor
def make_shell_context():
return dict(db=db, User=User, Role=Role)
### 單元測試 ###
requirements檔
(venv) $ pip freeze > requirements.txt
- 紀錄所有的套件依賴項目,以及確切的版本號。
- 透過Windows cmd,自動產生。
LAB - 3
(venv) $ deactivate
$ cd ..
$ mkdir test
$ cd test
$ virtualenv venv_test
$ venv_test\Scripts\activate
(venv_test) $ pip install C:\Users\pcsh1\OneDrive\桌面\mysqlclient-1.4.2-cp37-cp37m-win32.whl
(venv_test) $ pip install -r requirements.txt
(venv_test) $ pip freeze
複製完全一樣的虛擬環境
單元測試
C:\Users\pcsh1\flask_beginner_1.2.3.4
├─tests
│ test_basics.py
│ __init__.py --- 讓測試目錄成為有效的套件 (空檔案)
- __init__.py 可以為空檔案,因為unittest套件會掃描所有的模組來尋找測試程式。
單元測試 (tests/test_basics.py)
import unittest
from flask import current_app
from app import create_app, db
- 使用Python標準程式庫的unittest套件
- 目標:定義兩個簡單的測試 (test_app_exists、test_app_is_testing)
class BasicsTestCase(unittest.TestCase):
#在每次測試之前執行,幫測試程式建立一個類似運行中App的環境
def setUp(self):
#建立一個testing組態的App,並啟動context
self.app = create_app('testing') #動態套用testing組態設定
self.app_context = self.app.app_context() #維持目前(套用testingDB)的狀態
self.app_context.push() #push() is used to add items to the stack.
db.create_all() #為測試程式建立全新的資料庫
#在每次測試之後執行
def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
#確認App實例的存在
def test_app_exists(self):
self.assertFalse(current_app is None)
#確認App在測試組態下運行
def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])
單元測試啟動命令 (db_demo.py)
# ...
#自訂命令
@app.cli.command()
def test1():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests') #會自動去找【tests資料夾】
unittest.TextTestRunner(verbosity=2).run(tests)
#verbosity=1 輸出結果之詳細程度(簡略) vs. verbosity=2 (詳細)
-
透過Windows cmd,執行test1測試
(venv) $ flask test1
用Flask-Migrate進行資料庫遷移
C:\Users\pcsh1\flask_beginner_1.2.3.4
├─migrations
│ versions
│ ...
- 有時候我們會改動Model,例如新增或刪除某個欄位,這時候就要用Migrate來進行更新
(venv) $ pip install Flask-Migrate
-
安裝套件
-
初始化(db_demo.py)
from flask_migrate import Migrate
##初始化Flask-Migrate
migrate = Migrate(app, db)
-
執行指令
(venv) $ flask db init
testcol = db.Column(db.Integer) #新增至models.py
-
建立遷移腳本
-
執行遷移腳本
(venv) $ flask db migrate
(venv) $ flask db upgrade
先修改完model.py之後,使用以下指令。
-
檢查是否更新到資料庫
show databases;
use flaskdb;
show tables;
SELECT * FROM users;
執行App
(venv) $ set FLASK_APP=db_demo.py
(venv) $ set FLASK_DEBUG=1
(venv) $ set MAIL_USERNAME=pcsh110576@gmail.com
(venv) $ set MAIL_PASSWORD=**************
(venv) $ set FLASKY_ADMIN=bessy110576@gmail.com
(venv) $ flask run
-
透過Windows cmd,執行flask run
Thanks for your listening
Flask - Email
By BessyHuang
Flask - Email
- 363