您的位置:首页 > 编程语言 > Python开发

Flask Web 开发学习稿(三)

2016-05-31 12:42 591 查看

第六章 电子邮件

当我们需要在特定事件发生时提醒用户,包装了
smtplib
Flask-Mail
扩展能更好的和 Flask 集成

安装
pip install flask-mail


Flask-Mail 连接到 SMTP 服务器,如果不进行配置,Flask-Mail 会连接 localhost 上的端口 25

配置默认值说明
MAIL_SERVERlocalhostEmail服务器的ip地址或者主机名
MAIL_PORT25Email服务器端口
MAIL_USE_TLSFalse启用传输层安全协议
MAIL_USE_SSLFalse启用安全套接曾协议
MAIL_USERNAMENoneEmail用户名
MAIL_PASSWORDNoneEmail密码
使用外部 SMTP 服务器更加方便

from flask.ext.mail import Mail

app.config['MAIL_SERVER'] = 'mail.xxx.com'
app.config['MAIL_PORT'] = '587'
app.config['MAIL_USE_TLS'] = 'True'
app.config['MAIL_USERNAME'] = 'username'
app.config['MAIL_PASSWORD'] = 'pwd'
mail = Mail(app)


将账户和密码写在程序里太不安全了,为了保护敏感信息,需要让脚本从环境变量中导入这些信息

app.config['MAIL_USERNAME'] = os.environ.get('MALI_USERNAME')
app.config['MAIL_PASSWORD'] = os.environ.get('MALI_PASSWORD')


如何设置环境变量呢?

# Linux 或者 Mac OS X
export MALI_USERNAME=<YOU_USERNAME>
export MALI_PASSWORD=<YOU_PASSWORD>
# Windows
set MALI_USERNAME=<YOU_USERNAME>
set MALI_PASSWORD=<YOU_PASSWORD>


在程序中集成发送电子邮件功能

from flask.ext.mail import Message

app.config['FLASKY_MAIL_SUBJECT_PREFIX'] = '[Flasky]'
app.config['FLASKY_MAIL_SENDER'] = 'Flasky Admin <flasky@example.com>'

def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASKY_MAIL_SENDER'], recipients=[to])
msg.body = render_template(template + '.txt', **kwargs)
msg.html = render_template(template + '.html', **kwargs)
mail.send(msg)


这两个程序特定配置项,分别定义了邮件主题的前缀和发件人的地址

send_email()
函数的参数分别为收件人地址,主题,渲染邮件正文的模版和关键字参数列表

指定模版时不能包含扩展名,这样才能使用两个模版分别渲染纯文本正文和富文本正文

调用者将关键字参数传给
render_template()
函数以便在模版中使用,进而生成电子邮件正文,下面修改视图函数

app.config['FLASKY_ADMIN'] = os.environ.get('FLASKY_ADMIN')
#...
@app.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
user = User.query.filter_by(username=form.name.data).first()
if user is None:
user = User(username=form.name.data)
db.session.add(user)
session['known'] = False
if app.config['FLASKY_ADMIN']:
send_email(app.config['FLASKY_ADMIN'], 'New User',
'mail/new_user', user=user)
else:
session['known'] = True

session['name'] = form.name.data
form.name.data = ''

return redirect(url_for('index'))
return render_template('index.html', form=form, name=session.get('name'), known=session.get('known', False))


我们要创建两个模版文件,分别用于渲染纯文本和 HTML 版的邮件正文,这两个模版文件都保存在 tmplates 文件夹下的 mail 子文件夹中,以便和普通模版区分开来。电子邮件的模版中要有一个模版参数是用户,因此调用 send_mail() 函数时要以关键字参数的形式传入用户

这样的程序会在发送邮件的时候造成短暂阻塞,异步发送电子邮件来消除这种不必要的延迟

from threading import Thread

def send_async_email(app, msg):
with app.app_context():
mail.send(msg)

def send_email(to, subject, template, **kwargs):
msg = Message(app.config['FLASKY_MAIL_SUBJECT_PREFIX'] + subject,
sender=app.config['FLASKY_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


很多 Flask 扩展都假设已经存在激活的程序上下文和请求上下文,Flask-Mail 中的
send()
函数使用
current_app
,因此必须激活程序上下文,不过不同线程中执行
mail.send()
函数时,程序上下文要使用
app.app_context()
人工创建

当你需要大量发送电子邮件时,使用 Celery 任务队列更合适

第七章 大型程序的结构

现在我们已经完成了很多功能的学习,但是随着程序越来越大,我们将学会如何组织大型程序的结构

7.1 项目结构

├─Flsky
│  │─app  # Flask 程序
│  |  ├─static
│  |  |─templates
│  |  |─main
│  |  |  │-__init__.py
│  │  |  |-errors.py
│  │  |  |-forms.py
│  │  |  |-views.py
│  │  |-__init__.py
│  │  |-email.py
│  │  |-models.py
|  |-migrations  # 数据库迁移脚本
|  |-tests  # 单元测试
|  |  |-__init__.py
|  |  |-test*.py
│  |-config.py  # 储存配置
│  |-manage.py  # 用于启动程序以及其他的程序任务
│  |-requirements.txt  # 列出全部依赖包
|  └─ venv # python虚拟环境


7.2 配置选项

从现在开始我们我们的配置不会再像之前那样用简单的字典结构,而是使用配置类

#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'
SQLALCHEMY_COMMIT_ON_TEARDOWN = True
FLASKY_MAIL_SUBJECT_PREFIX = '[Flasky]'
FLASKY_MAIL_SENDER = 'Flasky Admin <flasky@example.com>'
FLASKY_ADMIN = os.environ.get('FLASKY_ADMIN')

@staticmethod
def init_app(app):
pass

class DevelopmentConfig(Config): DEBUG = True
MAIL_SERVER = 'smtp.googlemail.com'
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = os.environ.get('MAIL_USERNAME')
MAIL_PASSWORD = os.environ.get('MAIL_PASSWORD')
SQLALCHEMY_DATABASE_URI = os.environ.get('DEV_DATABASE_URL') or \
'sqlite:///' + os.path.join(basedir, 'data-dev.sqlite')

class TestingConfig(Config):
TESTING = True
SQLALCHEMY_DATABASE_URI = os.environ.get('TEST_DATABASE_URL') or \ 'sqlite:///' + os.path.join(basedir, 'data-test.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
}


配置类可以定义
init_app()
方法,其参数是程序实例,在这个方法中,可以执行对当前环境的配置初始化,现在基类 Config 中的
init_app()
方法为空

7.3 程序包

程序包用来保存程序的所有代码、模版和静态文件。

7.3.1 使用程序工厂函数

把创建过程移到可显式调用的工厂函数中,程序的工厂函数在 app 包的构造文件中定义

构造文件导入了太多正在使用的 Flask 扩展,由于尚未初始化所需的程序实例,所以没有初始化扩展,创建扩展类时没有向构造函数传入参数

create_app()
函数就是程序的工厂函数,接受一个参数,是程序使用的配置名

配置类在 config.py 文件中定义,其中保存的配置可以使用 Flask app.config 配置对象提供的
from_object()
方法直接导入程序。至于扩展对象,则可以通过名字从 config 字典中选择。

程序创建配置好后,就能初始化扩展了,在之前创建的扩展对象上调用
init_app()
可以完成初始化过程

# app/__init__.py
from flask import Flask, render_template
from flask.ext.bootstrap import Bootstrap
from flask.ext.mail import Mail
from flask.ext.moment import Moment
from flask.ext.sqlalchemy import SQLAlchemy
from config import config

bootstrap = Bootstrap()
mail = Mail()
moment = Moment()
db = SQLAlchemy()

def create_app(config_name):
app = Flask(__name__)
app.config.from_object(config[config_name])
config[config_name].init_app(app)
bootstrap.init_app(app)
mail.init_app(app)
moment.init_app(app)
db.init_app(app)
# 附加路由和自定义错误页面

return app


7.3.2 在蓝本中实现程序的功能

在单脚本程序中,程序实例存在于全局作用域中,路由可以直接使用 app.route 修饰器定义。但是现在应用程序实例是运行时创建的,
app.route
只在在
create_app()
以后才存在,这时定义路由就太晚了

在蓝本中注册的路由都处于休眠状态,直到蓝本注册到程序上后,路由才真正成为程序的一部分。使用位于全局作用域的蓝本时,定义路由的方法几乎和单脚本程序一样

为了获得最大的灵活性,程序包中创建了一个子包用于保存蓝本

app/main/__init__.py
from flask import Blueprint
main = Blueprint('main', __name__)
from . import views, errors


通过实例化一个蓝本类对象可以创建蓝本,构造函数有两个参数:蓝本的名字和蓝本所在的模块或者包,大多数情况下,Python的
__name__
变量就是第二个参数所需要的值

应用程序的路由放在
app/main/views.py
模块中, 错误处理放在
app/main/errors.py
中。导入这些模块以后,路由和错误处理就和蓝本关联起来了。

有一点要注意,路由和错误处理模块要在
app/__init__.py
的底部被导入,因为views.py 和 errors.py 要导入 main blueprint,所以为了避免循环依赖我们要等到 main 被创建出来才能够导入路由和错误处理。

蓝本在工厂函数 create_app() 中注册到程序上

# app/__init__.py 注册蓝本
def create_app(config_name):
# ...
from main import main as main_blueprint
app.register_blueprint(main_blueprint)
return app


错误处理程序如下

#app/main/error.py
from flask import render_template
from . import main

@main.app_errorhandler(404)
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


如果使用
errorhandler
修饰器,只有蓝本中的错误才能触发处理程序,要想注册程序全局的错误处理程序,必须使用
app_errorhandler


在蓝本中定义路由如下

# app/main/views.py
from datetime import datetime
from flask import render_template, session, redirect, url_for
from . import main
from .forms import NameForm
from .. import db
from ..models import User

@main.route('/', methods=['GET', 'POST'])
def index():
form = NameForm()
if form.validate_on_submit():
# ...
return redirect(url_for('.index'))
return render_template('index.html',
form=form, name=session.get('name'),
known=session.get('known', False),
current_time=datetime.utcnow())


在蓝本中的视图函数的区别

路由修饰器由蓝本提供

url_for()
的用法不同

Flask 会为蓝本中的全部端点都加上一个命名空间,这样就可以在不同的蓝本中使用相同的端点名定义视图函数,而不会产生冲突。命名空间就是蓝本的名字(蓝本构造函数的第一个参数),所以视图函数
index()
注册的端点名是
main.inedx
其 URL 使用
url_for('main.index')
获取

为了完全修改程序的页面,表单对象也要移到蓝本中,保存于
app/main/forms.py
模块

7.4 启动脚本

manage.py
文件用于启动程序

#!/usr/bin/env python
import os
from app import create_app, db
from app.models import User, Role
from flask.ext.script import Manager, Shell
from flask.ext.migrate import Migrate, MigrateCommand

app = create_app(os.getenv('FLASK_CONFIG') or 'default')
manager = Manager(app)
migrate = Migrate(app, db)

def make_shell_context():
return dict(app=app, db=db, User=User, Role=Role)

manager.add_command("shell", Shell(make_context=make_shell_context))
manager.add_command('db', MigrateCommand)

if __name__ == '__main__':
manager.run()


该脚本首先创建应用程序实例,然后从系统环境中读取
FLASK_CONFIG
变量,如果该变量没有定义则使用默认值。然后初始化
Flask-Script
,
Flask-Migrate
和为 Python Shell 定义的上下文

7.5 需求文件

pip 可以使用如下命令自动生成这个文件

pip freeze >requirements.txt


当你在另一个环境中准备安装这些依赖时,执行如下命令

pip install -r requirements.txt


7.6 单元测试

import unittest
from flask import current_app
from app import create_app, db

class BasicsTestCase(unittest.TestCase):

def setUp(self):
self.app = create_app('testing')
self.app_context = self.app.app_context()
self.app_context.push()
db.create_all()

def tearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()

def test_app_exists(self):
self.assertFalse(current_app is None)

def test_app_is_testing(self):
self.assertTrue(current_app.config['TESTING'])


测试是按照典型的单元测试的写法来构建的,类似于运行中的程序,首先使用测试配置创建程序,然后激活上下文。
setUp()
tearDown()
方法在每个测试方法执行前后都会运行,任何以
test_
开头的方法都会被当做测试方法来执行。
setUp()
方法创建了测试所需的环境, 他首先创建了应用程序实例用作测试的山下文环境,这样就能确保测试拿到
current_app
, 然后新建了一个全新的数据库。数据库和应用程序实例最后都会在
tearDown()
方法被销毁。

第一个测试确保了应用程序实例是存在的,第二个测试应用程序实例在测试配置下运行。为了确保测试文件夹有正确的包结构,我们需要添加一个
tests/__init__.py
文件,这样单元测试包就能扫描所有在测试文件夹中的模块了。

为了运行单元测试,我们可以在manage.py中添加一个自定义命令,

@manager.command
def test():
"""Run the unit tests."""
import unittest
tests = unittest.TestLoader().discover('tests')
unittest.TextTestRunner(verbosity=2).run(tests)


运行方法如下

(venv) $ python manage.py test
test_app_exists (test_basics.BasicsTestCase) ... ok
test_app_is_testing (test_basics.BasicsTestCase) ... ok
.----------------------------------------------------------------------
Ran 2 tests in 0.001s
OK


7.7 创建数据库

首选从环境变量中读取数据库的 URL,同时还提供了一个默认的 SQLite 数据库做备用

可以使用如下命令创建数据表或者升级到最新修订版本

python mange.py db upgrade
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息