程序员人生 网站导航

Flask Web 开发学习稿(三)

栏目:综合技术时间:2016-06-20 07:47:58

第6章 电子邮件

当我们需要在特定事件产生时提示用户,包装了 smtplibFlask-Mail 扩大能更好的和 Flask 集成
安装 pip install flask-mail
Flask-Mail 连接到 SMTP 服务器,如果不进行配置,Flask-Mail 会连接 localhost 上的端口 25

配置 默许值 说明
MAIL_SERVER localhost Email服务器的ip地址或主机名
MAIL_PORT 25 Email服务器端口
MAIL_USE_TLS False 启用传输层安全协议
MAIL_USE_SSL False 启用安全套接曾协议
MAIL_USERNAME None Email用户名
MAIL_PASSWORD None Email密码

使用外部 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 子文件夹中,以便和普通模版辨别开来。电子邮件的模版中要有1个模版参数是用户,因此调用 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章 大型程序的结构

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

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() 函数就是程序的工厂函数,接受1个参数,是程序使用的配置名
配置类在 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() 以后才存在,这时候定义路由就太晚了
在蓝本中注册的路由都处于休眠状态,直到蓝本注册到程序上后,路由才真正成为程序的1部份。使用位于全局作用域的蓝本时,定义路由的方法几近和单脚本程序1样
为了取得最大的灵活性,程序包中创建了1个子包用于保存蓝本

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

通过实例化1个蓝本类对象可以创建蓝本,构造函数有两个参数:蓝本的名字和蓝本所在的模块或包,大多数情况下,Python的 __name__变量就是第2个参数所需要的值
利用程序的路由放在app/main/views.py模块中, 毛病处理放在app/main/errors.py中。导入这些模块以后,路由和毛病处理就和蓝本关联起来了。
有1点要注意,路由和毛病处理模块要在 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())

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

  1. 路由修饰器由蓝本提供
  2. url_for() 的用法不同

Flask 会为蓝本中的全部端点都加上1个命名空间,这样就能够在不同的蓝本中使用相同的端点名定义视图函数,而不会产生冲突。命名空间就是蓝本的名字(蓝本构造函数的第1个参数),所以视图函数 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, 然后新建了1个全新的数据库数据库和利用程序实例最后都会在tearDown() 方法被烧毁。

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

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

@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,同时还提供了1个默许的 SQLite 数据库做备用
可使用以下命令创建数据表或升级到最新修订版本
python mange.py db upgrade

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐