04 发送邮件蓝图和单元测试
邮件扩展
在开发过程中,很多应用程序都需要通过邮件提醒用户,Flask的扩展包Flask-Mail通过包装了Python内置的smtplib包,可以用在Flask程序中发送邮件。
Flask-Mail连接到简单邮件协议(Simple Mail Transfer Protocol,SMTP)服务器,并把邮件交给服务器发送。
设置邮箱授权码

发送邮件借助Flask中的两个模块
from flask_mail import Mail, Message
在调用Mail模块之前要先配置,才能使用,可以点进去Mail模块,看响应的配置参数
app.config['MAIL_SERVER'] = "smtp.126.com" app.config['MAIL_PORT'] = 465 app.config['MAIL_USE_SSL'] = True app.config['MAIL_USERNAME'] = "yinqiaoyin@126.com" app.config['MAIL_PASSWORD'] = "111qqqaaazzz" app.config['MAIL_DEFAULT_SENDER'] = u'你好a<yinqiaoyin@126.com>'
然后实例化,Mail对象
mail = Mail(app)
实例化Message对象初始化发送邮件的信息,通过mail.send方法,发送邮件
msg = Message('发送测试邮件', ['1273844671@qq.com'], html='<h1>哈哈</h1>')
mail.send(msg)
如下示例,通过开启QQ邮箱SMTP服务设置,发送邮件
创建一个test1.py的项目
# coding:utf8 import sys reload(sys) sys.setdefaultencoding('utf-8') from flask import Flask from flask_mail import Mail, Message app = Flask(__name__) # 配置邮件:服务器/端口/安全套接字层/邮箱名/授权码 app.config['MAIL_SERVER'] = "smtp.126.com" app.config['MAIL_PORT'] = 465 app.config['MAIL_USE_SSL'] = True app.config['MAIL_USERNAME'] = "yinqiaoyin@126.com" app.config['MAIL_PASSWORD'] = "111qqqaaazzz" app.config['MAIL_DEFAULT_SENDER'] = u'你好a<yinqiaoyin@126.com>' mail = Mail(app) @app.route('/') def index(): return '<a href="/send_mail">发送邮件</a>' @app.route('/send_mail') def send_mail(): msg = Message('发送测试邮件', ['1273844671@qq.com'], html='<h1>哈哈</h1>') mail.send(msg) return '发送完成' if __name__ == '__main__': app.run(debug=True)
使用多线程发送邮件
需要使用with的语法去将Flask当前的当下文环境搬到多线程的函数中,不然会报working outside of application context的错误信息
创建一个test2.py的项目
# coding:utf8 import sys reload(sys) sys.setdefaultencoding('utf8') from flask import Flask from flask_mail import Mail, Message from threading import Thread app = Flask(__name__) # 配置邮件:服务器/端口/安全套接字层/邮箱名/授权码 app.config['MAIL_SERVER'] = "smtp.126.com" app.config['MAIL_PORT'] = 465 app.config['MAIL_USE_SSL'] = True app.config['MAIL_USERNAME'] = "yinqiaoyin@126.com" app.config['MAIL_PASSWORD'] = "111qqqaaazzz" app.config['MAIL_DEFAULT_SENDER'] = u'你好sd<yinqiaoyin@126.com>' # 然后实例化,Mail对象 mail = Mail(app) @app.route('/') def index(): return '<a href="/send_mail">发送邮件</a>' def send_mail_thread(): # 使用with的语法去将当前的当下文环境搬到些处 with app.app_context(): msg = Message('发送测试邮件', ['1273844671@qq.com'], html='<h1>哈哈</h1>') mail.send(msg) @app.route('/send_mail') def send_mail(): t = Thread(target=send_mail_thread) t.start() return '发送完成' if __name__ == '__main__': app.run(debug=True)
蓝图(Blueprint)
模块化
随着flask程序越来越复杂,我们需要对程序进行模块化的处理,之前学习过python的模块化管理,于是针对一个简单的flask程序进行模块化处理
蓝图的引入思考
路由实质上就是一个装饰器
我们可以自定义一个.py的文件里面只写函数,在主函数中导入这个文件通过装饰器 app.route('/orders')(orders)给这个函数添加路由信息
创建一个oder.py文件,代码如下
def orders():
return 'orders'
创建一个main.py的模块代码如下:
# coding:utf8 from flask import Flask from order import orders app=Flask(__name__) # 给导入过来的函数增加路由信息 app.route('/orders')(orders) @app.route('/') def index(): return '<a href="/send_mail">发送邮件</a>' if __name__ == '__main__': print app.url_map app.run(debug=True)

在浏览器中访问导入来的函数
http://127.0.0.1:5000/orders

但是这种通过导入函数后通过装饰器,给其加上路由的方式有一个致命的弊端,那就是如果一个文件中包含成百上千个函数,就要装饰这成百上千个装饰器
解决的方法就是使用Flask内部的蓝图
Blueprint概念
简单来说,Blueprint 是一个存储操作方法的容器,这些操作在这个Blueprint 被注册到一个应用之后就可以被调用,Flask 可以通过Blueprint来组织URL以及处理请求。
Flask使用Blueprint让应用实现模块化,在Flask中,Blueprint具有如下属性:
- 一个应用可以具有多个Blueprint
- 可以将一个Blueprint注册到任何一个未使用的URL下比如 “/”、“/sample”或者子域名
- 在一个应用中,一个模块可以注册多次
- Blueprint可以单独具有自己的模板、静态文件或者其它的通用操作方法,它并不是必须要实现应用的视图和函数的
- 在一个应用初始化时,就应该要注册需要使用的Blueprint
但是一个Blueprint并不是一个完整的应用,它不能独立于应用运行,而必须要注册到某一个应用中。
初识蓝图
蓝图/Blueprint对象用起来和一个应用/Flask对象差不多,最大的区别在于一个 蓝图对象没有办法独立运行,必须将它注册到一个应用对象上才能生效
使用蓝图可以分为以下步骤
导入蓝图
from flask import Blueprint
- 创建一个蓝图对象
# 创建蓝图对象,指定两参数:蓝图的名字,导入的模块名 user_app = Blueprint('user', __name__)
- 在这个蓝图对象上进行操作,注册路由,指定静态文件夹,注册模版过滤器
@user_app.route('/user_info') def user_info(): return "user_info"
- 在应用对象上(另一个使用蓝图的模块)注册这个蓝图对象
app.register_blueprint(user_app)
当这个应用启动后,可以访问到蓝图中定义的视图函数
创建一个蓝图为user.py代码如下:
# -*- coding:utf-8 -*- from flask import Blueprint # 创建蓝图对象,指定两参数:蓝图的名字,导入的模块名 user_app = Blueprint('user', __name__) @user_app.route('/user_info') def user_info(): return "user_info"
在main.py中使用蓝图
# coding:utf8 from flask import Flask from order import orders from user import user_app app=Flask(__name__) # 注册蓝图:将 user_app这个蓝图中所有的路由添加到app的url_map中 app.register_blueprint(user_app, url_prefix='/user') app.route('/orders')(orders) @app.route('/') def index(): return '<a href="/send_mail">发送邮件</a>' if __name__ == '__main__': print app.url_map app.run(debug=True)
运行项目

在浏览器中访问导入来的蓝图的url路径
http://127.0.0.1:5000/user_info

运行机制
- 蓝图是保存了一组将来可以在应用对象上执行的操作,注册路由就是一种操作
- 当在应用对象上调用 route 装饰器注册路由时,这个操作将修改对象的url_map路由表
- 然而,蓝图对象根本没有路由表,当我们在蓝图对象上调用route装饰器注册路由时,它只是在内部的一个延迟操作记录列表defered_functions中添加了一个项
- 当执行应用对象的 register_blueprint() 方法时,应用对象将从蓝图对象的 defered_functions 列表中取出每一项,并以自身作为参数执行该匿名函数,即调用应用对象的 add_url_rule() 方法,这将真正的修改应用对象的路由表
蓝图的url前缀
- 当我们在应用对象上注册一个蓝图时,可以指定一个url_prefix关键字参数(这个参数默认是/)
-
在应用最终的路由表 url_map中,在蓝图上注册的路由URL自动被加上了这个前缀,这个可以保证在多个蓝图中使用相同的URL规则而不会最终引起冲突,只要在注册蓝图时将不同的蓝图挂接到不同的自路径即可
在main.py中在注册的时候增加url前缀
app.register_blueprint(user_app, url_prefix='/user')
运行main.py的项目

在浏览器中访问新的带前缀的url路径
http://127.0.0.1:5000/user/user_info

使用目录的形式使用蓝图
创建一个cart的包,在其内部创建一个views.py的模块

__init__中的代码如下:
# -*- coding:utf-8 -*- from flask import Blueprint # 创建蓝图对象,指定两参数:蓝图的名字,导入的模块名 cart_app = Blueprint('cart', __name__) #为了执行views里的函数 from views import cart_list
views.py中的代码如下:
# -*- coding:utf-8 -*- # 放置的是Cart模块的所的视图函数 from flask import render_template from . import cart_app @cart_app.route('/cart_list') def cart_list(): return 'cart list' @cart_app.route('/cart_page') def cart_page(): return render_template('cart.html')
创建一个main1.py的项目引入cart包中的蓝图,代码如下:
# coding:utf8 from flask import Flask from cart import cart_app app = Flask(__name__) # 注册蓝图:将 user_app这个蓝图中所有的路由添加到app的url_map中 app.register_blueprint(cart_app, url_prefix='/cart') @app.route('/') def index(): return '<a href="/send_mail">发送邮件</a>' if __name__ == '__main__': print app.url_map app.run(debug=True)
运行项目

在浏览器中访问包中的蓝图路径
http://127.0.0.1:5000/cart/cart_list

设置模版目录
蓝图对象默认的模板目录为系统的模版目录,可以在创建蓝图对象时使用 template_folder 关键字参数设置模板目录
cart_app = Blueprint('cart', import_name=__name__, template_folder='templates')
在cart包中创建一个templates目录
在其内部创建一个cart.html代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>cart page</h1> </body> </html>
在__init__.py中为蓝图指定模板的目录
cart_app = Blueprint('cart', import_name=__name__, template_folder='templates')
运行项目main1.py

在浏览器中输入
http://127.0.0.1:5000/cart/cart_page

把cart目录中的templates目录复制一份到根目录下,把修改cart.html中的内容

在一次在浏览器中输入
http://127.0.0.1:5000/cart/cart_page

如果主目录的templates中有当前蓝图模块里面相同名字的模板文件 ,那么会先加载主目录中
注册静态路由
和应用对象不同,蓝图对象创建时不会默认注册静态目录的路由。需要我们在 创建时指定 static_folder 参数
在cart包中创建静态目录,static,放一张图片进去

在__init__.py在实例蓝图的时候将蓝图所在目录下的static目录设置为静态目录
cart_app = Blueprint('cart', import_name=__name__, template_folder='templates', static_folder='static', static_url_path='/lib')
运行main1.py项目

在浏览器中输入
http://127.0.0.1:5000/cart/static/bbb.png

还可以通过static_url_path参数修改静态的路径
cart_app = Blueprint('cart', import_name=__name__, template_folder='templates', static_folder='static', static_url_path='/lib')
运行项目

在浏览器中输入
http://127.0.0.1:5000/cart/lib/bbb.png

单元测试
为什么要测试?
Web程序开发过程一般包括以下几个阶段:[需求分析,设计阶段,实现阶段,测试阶段]。其中测试阶段通过人工或自动来运行测试某个系统的功能。目的是检验其是否满足需求,并得出特定的结果,以达到弄清楚预期结果和实际结果之间的差别的最终目的。
测试的分类:
测试从软件开发过程可以分为:
- 单元测试
- 对单独的代码块(例如函数)分别进行测试,以保证它们的正确性
- 集成测试
- 对大量的程序单元的协同工作情况做测试
- 系统测试
- 同时对整个系统的正确性进行检查,而不是针对独立的片段
在众多的测试中,与程序开发人员最密切的就是单元测试,因为单元测试是由开发人员进行的,而其他测试都由专业的测试人员来完成。所以我们主要学习单元测试。
什么是单元测试?
程序开发过程中,写代码是为了实现需求。当我们的代码通过了编译,只是说明它的语法正确,功能能否实现则不能保证。 因此,当我们的某些功能代码完成后,为了检验其是否满足程序的需求。可以通过编写测试代码,模拟程序运行的过程,检验功能代码是否符合预期。
单元测试就是开发者编写一小段代码,检验目标代码的功能是否符合预期。通常情况下,单元测试主要面向一些功能单一的模块进行。
举个例子:一部手机有许多零部件组成,在正式组装一部手机前,手机内部的各个零部件,CPU、内存、电池、摄像头等,都要进行测试,这就是单元测试。
在Web开发过程中,单元测试实际上就是一些“断言”(assert)代码。
断言就是判断一个函数或对象的一个方法所产生的结果是否符合你期望的那个结果。 python中assert断言是声明布尔值为真的判定,如果表达式为假会发生异常。单元测试中,一般使用assert来断言结果。
断言方法的使用:
# 在实际开发中,我们可以 使用 assert 去判断别人传入的参数是否附合规范
# 而单元测试其实最主要的就是使用 assert 去判断某个函数的执行结果是否附合预期
创建一个test2.py
# -*- coding:utf-8 -*- def fun(num1, num2): # 使用断言去判断 num2 不能为0,如果num2一旦为0,那么就会抛出异常(AssertionError) assert num2 != 0, '除数不能为0' return num1 / num2 if __name__ == '__main__': print fun(100, 0)

常用的断言方法:
assertEqual 如果两个值相等,则pass
assertNotEqual 如果两个值不相等,则pass
assertTrue 判断bool值为True,则pass
assertFalse 判断bool值为False,则pass
assertIsNone 不存在,则pass
assertIsNotNone 存在,则pass
写一个要被测试的函数test3_login.py
# -*- coding:utf-8 -*- from flask import Flask, request, jsonify app = Flask(__name__) # 模拟POST登录的逻辑,该路由函数返回登录结果(JSON) @app.route('/login', methods=['POST']) def login(): # 取到用户传入的参数 username = request.form.get('username') password = request.form.get('password') a = 10 / 0 # 判断参数是否为空 if not all([username, password]): return jsonify(errcode=-2, errmsg='params error') # 判断登录名和密码是否正确 if username == 'admin' and password == '123456': return jsonify(errcode=0, errmsg='login success') else: return jsonify(errcode=-1, errmsg='username or password wrong') if __name__ == '__main__': app.run(debug=True)
写一个单元测试的模块test3_login_test.py代码如下:
# 要执行测试的方法都要以test开头,不然不会被单元测试执行
# -*- coding:utf-8 -*- from flask import Flask, request, jsonify app = Flask(__name__) # 模拟POST登录的逻辑,该路由函数返回登录结果(JSON) @app.route('/login', methods=['POST']) def login(): # 取到用户传入的参数 username = request.form.get('username') password = request.form.get('password') # 判断参数是否为空 if not all([username, password]): return jsonify(errcode=-2, errmsg='params error') # 判断登录名和密码是否正确 if username == 'admin' and password == '123456': return jsonify(errcode=0, errmsg='login success') else: return jsonify(errcode=-1, errmsg='username or password wrong') if __name__ == '__main__': app.run(debug=True)
运行单元测试

把test3_login.py中的errocde=-2改为-3
if not all([username, password]): return jsonify(errcode=-3, errmsg='params error')
再次运行单元测试的代码

然后在把test3_logint.py中的errocde的值改为-2
在test3_login_test.py中加入其他为空的测是
# -*- coding:utf-8 -*- import unittest import json from test3_login import app class LoginTestCase(unittest.TestCase): def test_login_params_empty(self): """测试参数为空的情况""" # 取到flask提供的测试客户端 client = app.test_client() # 使用测试客户端发起post请求,什么数据都没传入 ret = client.post('/login', data={}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) # 判断errcode是否在响应中 self.assertIn('errcode', resp, '返回数据格式不正确') # 判断错误的状态麻是否为2 self.assertEqual(resp['errcode'], -2, '返回结果有误') # 使用测试客户端发起post请求,只传入用户名 ret = client.post('/login', data={'username': 'admin'}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) # 判断errcode是否在响应中 self.assertIn('errcode', resp, '返回数据格式不正确') # 判断错误的状态麻是否为2 self.assertEqual(resp['errcode'], -2, '返回结果有误') # 使用测试客户端发起post请求,只传入密码 ret = client.post('/login', data={'password': '123456'}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) # 判断errcode是否在响应中 self.assertIn('errcode', resp, '返回数据格式不正确') # 判断错误的状态麻是否为2 self.assertEqual(resp['errcode'], -2, '返回结果有误')
运行test3_login_test.py的测试

使其可以在命令行中运行单元测试的模块
只需在最后添加以下两行代码
if __name__ == '__main__': unittest.main()
通过命令行运行代码
python test3_login_test.py

在test3_login_test.py中添加有参数的判断,增加一个方法如下:
def test_login_params(self): # 取到flask提供的测试客户端 client = app.test_client() # 使用测试客户端发起post请求,指定请求的路由并且传入参数 ret = client.post('/login', data={'username': 'admin', 'password': '123456'}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) self.assertIn('errcode', resp, '返回数据格式不正确') self.assertEqual(resp['errcode'], 0, '返回结果有误')
完整的代码:
# -*- coding:utf-8 -*- import unittest import json from test3_login import app class LoginTestCase(unittest.TestCase): def test_login_params(self): # 取到flask提供的测试客户端 client = app.test_client() # 使用测试客户端发起post请求,指定请求的路由并且传入参数 ret = client.post('/login', data={'username': 'admin', 'password': '123456'}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) self.assertIn('errcode', resp, '返回数据格式不正确') self.assertEqual(resp['errcode'], 0, '返回结果有误') def test_login_params_empty(self): """测试参数为空的情况""" # 取到flask提供的测试客户端 client = app.test_client() # 使用测试客户端发起post请求,什么数据都没传入 ret = client.post('/login', data={}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) # 判断errcode是否在响应中 self.assertIn('errcode', resp, '返回数据格式不正确') # 判断错误的状态麻是否为2 self.assertEqual(resp['errcode'], -2, '返回结果有误') # 使用测试客户端发起post请求,只传入用户名 ret = client.post('/login', data={'username': 'admin'}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) # 判断errcode是否在响应中 self.assertIn('errcode', resp, '返回数据格式不正确') # 判断错误的状态麻是否为2 self.assertEqual(resp['errcode'], -2, '返回结果有误') # 使用测试客户端发起post请求,只传入密码 ret = client.post('/login', data={'password': '123456'}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) # 判断errcode是否在响应中 self.assertIn('errcode', resp, '返回数据格式不正确') # 判断错误的状态麻是否为2 self.assertEqual(resp['errcode'], -2, '返回结果有误') if __name__ == '__main__': unittest.main()
运行单元测试
2个测试的方法通过
python test3_login_test.py

单元测试中的setUp方法,相当于类中的一些初始化,
#该方法会首先执行,相当于做测试前的准备工作 def setUp(self): pass
使用的场景 client = app.test_client()在每个方法中都多次使用,我们可以给他放在初始化方法zhong
def setUp(self):
self.client = app.test_client()
还可以把一些配置放到setUp方法中 app.config['TESTING'] = True,如果出错,会提示测试的文件出错在具体的哪一行
def setUp(self): # 开启测试模式,如果测试的函数抛出异常,那么会打印异常信息 app.config['TESTING'] = True # 这一句代码类似于上一句代码 # app.testing = True self.client = app.test_client()
完整代码
创建一个test4.py的文件
# -*- coding:utf-8 -*- import unittest import json from test3_login import app class LoginTestCase(unittest.TestCase): # 在测试之前做相关初始化操作,会在所有的测试方法被调用之前调用 def setUp(self): # 开启测试模式,如果测试的函数抛出异常,那么会打印异常信息 app.config['TESTING'] = True # 这一句代码类似于上一句代码 # app.testing = True self.client = app.test_client() def test_login_params(self): # 取到flask提供的测试客户端 # client = app.test_client() # 使用测试客户端发起post请求,指定请求的路由并且传入参数 ret = self.client.post('/login', data={'username': 'admin', 'password':'123456'}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) self.assertIn('errcode', resp, '返回数据格式不正确') self.assertEqual(resp['errcode'], 0, '返回结果有误') # 要执行测试的方法都要以test开头,不然不会被单元测试执行 def test_login_params_empty(self): """测试参数为空的情况""" # 取到flask提供的测试客户端 # client = app.test_client() # 使用测试客户端发起post请求,指定请求的路由并且传入参数 ret = self.client.post('/login', data={}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) self.assertIn('errcode', resp, '返回数据格式不正确') self.assertEqual(resp['errcode'], -2, '返回结果有误') # 使用测试客户端发起post请求,指定请求的路由并且传入参数 ret = self.client.post('/login', data={'username': 'admin'}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) self.assertIn('errcode', resp, '返回数据格式不正确') self.assertEqual(resp['errcode'], -2, '返回结果有误') # 使用测试客户端发起post请求,指定请求的路由并且传入参数 ret = self.client.post('/login', data={'password': '123456'}) # 取到响应体 resp = ret.data # 转JSON数据 resp = json.loads(resp) self.assertIn('errcode', resp, '返回数据格式不正确') self.assertEqual(resp['errcode'], -2, '返回结果有误') if __name__ == '__main__': unittest.main()
运行test4.py
python test4.py
在test3_login.py中添加以下的错误代码,主要是看 在setUp方法中 app.config['TESTING'] = True的作用
a = 10 / 0

运行单元测试的文件
python test4.py

指出了具体的哪一行出错,如果不写,则不会提示出具体的信息,把它注销,再次运行项目
python test4.py

数据库测试:
tearDown该方法会在测试代码执行完后执行,相当于做测试后的扫尾工作和setUp方法相反
def tearDown(self):
pass
创建一个authorbook.py用来被进行单元测试,主要是测试能不能向数据库中添加数据,代码如下:
代码不需要看,了解就行
# -*- coding:utf-8 -*- from flask import Flask, render_template, request, flash, redirect, url_for from flask_sqlalchemy import SQLAlchemy from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired from flask_script import Manager from flask_migrate import Migrate, MigrateCommand import sys reload(sys) # 设置全局默认编码 sys.setdefaultencoding('utf-8') app = Flask(__name__) # 配置数据库链接参数 app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://root:mysql@127.0.0.1:3306/test2" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True app.config['SECRET_KEY'] = "AAAAAA" db = SQLAlchemy(app) # 初始化迁移的对象,并且关联当前的应用 migrate = Migrate(app, db) manager = Manager(app) # 添加command,指定名字为db manager.add_command('db', MigrateCommand) # 自定义的表单对象 class AddBookForm(FlaskForm): author_name = StringField(label='作者:', validators=[DataRequired('请输入作者')]) book_name = StringField(label='书名:', validators=[DataRequired('请输入书名')]) submit = SubmitField(label='添加') # 一 作者 class Author(db.Model): __tablename__ = "authors" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) # 添加关系并指定返向引用 books = db.relationship('Book', backref='author', lazy='dynamic') # 多 书籍 class Book(db.Model): __tablename__ = "books" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64)) # 外键 author_id = db.Column(db.Integer, db.ForeignKey('authors.id')) @app.route('/delete_author/<int:author_id>') def delete_author(author_id): author = Author.query.get(author_id) if author: try: # 删除author下面的所有书籍 author.books.delete() # 删除作者 db.session.delete(author) db.session.commit() except Exception as e: print e flash('操作数据库失败') db.session.rollback(); else: flash('未找到该作者') return redirect(url_for('index')) @app.route('/delete_book/<int:book_id>') def delete_book(book_id): book = Book.query.get(book_id) if book: try: db.session.delete(book) db.session.commit() except Exception as e: print e db.session.rollback() else: flash('书籍不存在') return redirect(url_for('index')) @app.route('/', methods=['GET', 'POST']) def index(): add_book_form = AddBookForm() if add_book_form.validate_on_submit(): book_name = add_book_form.book_name.data author_name = add_book_form.author_name.data # 判断数据,并且添加数据 # 判断是否有对应名字的作者 author = Author.query.filter(Author.name==author_name).first() # 如果作者不存在 if not author: try: # 如果没有查询到作者信息,需要添加 author = Author(name=author_name) db.session.add(author) db.session.commit() # 初始化一本书,并指定其作者的id book = Book(name=book_name, author_id=author.id) db.session.add(book) db.session.commit() except Exception as e: db.session.rollback() print e else: book = Book.query.filter(Book.name==book_name).first() # 如果没有查询到同名的书 if not book: try: book = Book(name=book_name, author_id=author.id) db.session.add(book) db.session.commit() except Exception as e: print e db.session.rollback() else: flash('该书名已存在') else: if request.method == 'POST': flash('输入内容有误') authors = Author.query.all() return render_template('test2.html', authors=authors, add_book_form=add_book_form) if __name__ == '__main__': # db.drop_all() # db.create_all() # # # 生成数据 # au1 = Author(name='老王') # au2 = Author(name='帅尹') # au3 = Author(name='老刘') # # 把数据提交给用户会话 # db.session.add_all([au1, au2, au3]) # # 提交会话 # db.session.commit() # bk1 = Book(name='老王回忆录', author_id=au1.id) # bk2 = Book(name='我读书少,你别骗我', author_id=au1.id) # bk3 = Book(name='如何才能让自己更骚', author_id=au2.id) # bk4 = Book(name='怎样征服美丽少女', author_id=au3.id) # bk5 = Book(name='如何征服英俊少男', author_id=au3.id) # # 把数据提交给用户会话 # db.session.add_all([bk1, bk2, bk3, bk4, bk5]) # # 提交会话 # db.session.commit() # # app.run(debug=True) manager.run()
创建一个单元测试的文件authorbook_test.py
通过项数据库中添加一条数据,然后在查询这条数据,来验证数据库的操作
运行项目
python authorbook_test.py

注意的事项:
测试数据库,需要单独为测试添加数据库
重点了解两个方法
初始化的方法
一些初始化的设置,创建所有的表,和一些配置
def setUp(self): # app.config['TESTING'] = True app.testing = True # 测试数据库,需要单独为测试添加数据库 app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql://root:mysql@127.0.0.1:3306/flask_test' db.create_all()
完成测试后的收尾方法
关闭数据库的连接,删除所有的表
# 会在测试完成之后调用的方法,一般在这个方法里面去做数据的清除和一些收尾工作 def tearDown(self): # 关闭数据库连接 db.session.remove() db.drop_all()
利用uWSGI和nginx进行服务器部署
区分几个概念:
WSGI:
- 全称是Web Server Gateway Interface(web服务器网关接口)
- 它是一种规范,它是web服务器和web应用程序之间的接口
- 它的作用就像是桥梁,连接在web服务器和web应用框架之间
- 没有官方的实现,更像一个协议。只要遵照这些协议,WSGI应用(Application)都可以在任何服务器(Server)上运行
uwsgi:是一种传输协议,用于定义传输信息的类型。常用于在uWSGI服务器与其他网络服务器的数据通信
uWSGI:是实现了uwsgi协议WSGI的web服务器。
阿里云服务器
- 选择云服务器:阿里云服务器 https://www.aliyun.com
- 购买服务器:在首页最底下有一个免费使用的优惠购买:最便宜的套餐为9.9元,送一个入门级别的云服务器ECS和其他的一些服务器
- 购买后,再次进入首页最底下,点击免费获取 https://free.aliyun.com/
创建服务器选择ubuntu16.04 64位的操作系统
- 利用命令行进行远程服务器登录
ssh root@47.95.8.70
登陆后的相关软件安装
先更新apt软件源
sudo apt-get update
python和pip
这两个环境是ubuntu16.04自带的
uwsgi安装
- uwsgi是一个能够运行flask项目的高性能web服务器,需要先安装两个依赖
apt-get install build-essential python-dev
- 然后进行uwsgi的安装
pip install uwsgi
nginx安装
apt-get install nginx
mysql的安装:
apt-get install mysql-server
apt-get install libmysqlclient-dev
虚拟环境的安装
- virtualenv和virtualenvwrapper的安装:
pip install virtualenv
pip install virtualenvwrapper
- 使得安装的virtualenvwrapper生效,编辑~/.bashrc文件,内容如下:
export WORKON_HOME=$HOME/.virtualenvs export PROJECT_HOME=$HOME/workspace source /usr/local/bin/virtualenvwrapper.sh
- 使编辑后的文件生效
source ~/.bashrc
hello world程序的部署
要让自己的服务器支持5000端口这个要自己去阿里云上设置

创建一个app.py的项目
from flask import Flask app = Flask(__name__) @app.route('/') def index(): return 'hello' if __name__ == '__main__': app.run(debug=True)
创建一个usgi.ini
#需要声明uwsgi使得uwsgi能够识别当前文件 [uwsgi] master = true # 使用 nginx 配合连接时使用 # socket = :5000 # 直接做web服务器使用 http = :5000 # 设定进程数 processes = 4 # 设定线程数 threads = 2 # 指定运行的文件 wsgi-file = app.py #指定运行的项目的目录[自已项目在哪个目录就用哪个目录] chdir = /root # 指定运行的实例 callable = app # 指定uwsgi服务器的缓冲大小 buffer-size = 32768 # 在虚拟环境中运行需要指定python目录 pythonpath = /root/.virtualenvs/py_flask/lib/python2.7/site-packages # 设置进程id文件 pidfile = uwsgi.pid # 以守护的形式运行,设置log输出位置 # daemonize = uwsgi.log
把这两个文件从本地推送到服务器
scp ./app.py root@47.95.8.70:~/
scp ./uwsgi.ini root@47.95.8.70:~/

远程登陆自己的阿里云
ssh root@47.95.8.70
查看上传过来的文件
ls

运行app.py程序
uwsgi --ini uwsgi.ini
在浏览器中输入
http://47.95.8.70:5000/

在服务器端以守护的形式运行,设置log输出位置
vi uwsgi.ini
把最后一条的注释删掉

运行uwsgi
uwsgi --ini uwsgi.ini

这时候光标不会卡主,会把log信息写入到文件中,uwsgi在后台运行

停止运行uwsgi
uwsgi --stop uwsgi.pid
配置nginx服务器
编辑文件:/etc/nginx/sites-available/default
修改为如下内容:
server { listen 80 default_server; server_name 47.95.8.70; location / { include uwsgi_params; uwsgi_pass 47.95.8.70:5000; uwsgi_read_timeout 100; }
将server中原有的,上述配置中不能存在的内容注释或删除掉
- 启动和停止nginx服务器
/etc/init.d/nginx start #启动 /etc/init.d/nginx stop #停止 /etc/init.d/nginx restart #重启
重启nginx服务器
/etc/init.d/nginx restart
把uwsgi.ini中的socket = :5000的注释打开
把 http = :5000注释掉
启动uwsgi
uwsgi --ini uwsgi.ini
这是可以在浏览器中不用输入端口号,也能访问
http://47.95.8.70

停止运行uwsgi
uwsgi --stop uwsgi.pid
把一个小案例部署到阿里云
创建一个目录Flask_day3
在其内部创建test2.py代码如下:
# -*- coding:utf-8 -*- from flask import Flask, render_template, request, flash, redirect, url_for from flask_sqlalchemy import SQLAlchemy from flask_wtf import FlaskForm from wtforms import StringField, SubmitField from wtforms.validators import DataRequired from flask_script import Manager from flask_migrate import Migrate, MigrateCommand import sys reload(sys) # 设置全局默认编码 sys.setdefaultencoding('utf-8') app = Flask(__name__) # 配置数据库链接参数 app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://root:mysql@127.0.0.1:3306/test2" app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = True app.config['SECRET_KEY'] = "AAAAAA" db = SQLAlchemy(app) # 初始化迁移的对象,并且关联当前的应用 migrate = Migrate(app, db) manager = Manager(app) # 添加command,指定名字为db manager.add_command('db', MigrateCommand) # 自定义的表单对象 class AddBookForm(FlaskForm): author_name = StringField(label='作者:', validators=[DataRequired('请输入作者')]) book_name = StringField(label='书名:', validators=[DataRequired('请输入书名')]) submit = SubmitField(label='添加') # 一 作者 class Author(db.Model): __tablename__ = "authors" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64), unique=True) # 添加关系并指定返向引用 books = db.relationship('Book', backref='author', lazy='dynamic') # 多 书籍 class Book(db.Model): __tablename__ = "books" id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(64)) # 外键 author_id = db.Column(db.Integer, db.ForeignKey('authors.id')) @app.route('/delete_author/<int:author_id>') def delete_author(author_id): author = Author.query.get(author_id) if author: try: # 删除author下面的所有书籍 author.books.delete() # 删除作者 db.session.delete(author) db.session.commit() except Exception as e: print e flash('操作数据库失败') db.session.rollback(); else: flash('未找到该作者') return redirect(url_for('index')) @app.route('/delete_book/<int:book_id>') def delete_book(book_id): book = Book.query.get(book_id) if book: try: db.session.delete(book) db.session.commit() except Exception as e: print e db.session.rollback() else: flash('书籍不存在') return redirect(url_for('index')) @app.route('/', methods=['GET', 'POST']) def index(): add_book_form = AddBookForm() if add_book_form.validate_on_submit(): book_name = add_book_form.book_name.data author_name = add_book_form.author_name.data # 判断数据,并且添加数据 # 判断是否有对应名字的作者 author = Author.query.filter(Author.name==author_name).first() # 如果作者不存在 if not author: try: # 如果没有查询到作者信息,需要添加 author = Author(name=author_name) db.session.add(author) db.session.commit() # 初始化一本书,并指定其作者的id book = Book(name=book_name, author_id=author.id) db.session.add(book) db.session.commit() except Exception as e: db.session.rollback() print e else: book = Book.query.filter(Book.name==book_name).first() # 如果没有查询到同名的书 if not book: try: book = Book(name=book_name, author_id=author.id) db.session.add(book) db.session.commit() except Exception as e: print e db.session.rollback() else: flash('该书名已存在') else: if request.method == 'POST': flash('输入内容有误') authors = Author.query.all() return render_template('test2.html', authors=authors, add_book_form=add_book_form) if __name__ == '__main__': manager.run()
在其内部创建一个uwsgi.ini代码如下:
#需要声明uwsgi使得uwsgi能够识别当前文件 [uwsgi] master = true # 使用 nginx 配合连接时使用 socket = :5000 # 直接做web服务器使用 #http = :5000 # 设定进程数 processes = 4 # 设定线程数 threads = 2 # 指定运行的文件 wsgi-file = test2.py #指定运行的项目的目录[自已项目在哪个目录就用哪个目录] chdir = /root/Flask_day3 # 指定运行的实例 callable = app # 指定uwsgi服务器的缓冲大小 buffer-size = 32768 # 在虚拟环境中运行需要指定python目录 pythonpath = /root/.virtualenvs/py_flask/lib/python2.7/site-packages # 设置进程id文件 pidfile = uwsgi.pid # 以守护的形式运行,设置log输出位置 #daemonize = uwsgi.log
在Flask_day3目录下创建一个模板目录templates
在templates模板目录下,创建一个test3.html代码如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form method="post"> {{ add_book_form.csrf_token() }} {{ add_book_form.author_name.label }}{{ add_book_form.author_name }}<br/> {{ add_book_form.book_name.label }}{{ add_book_form.book_name }}<br/> {{ add_book_form.submit }}<br/> {% for message in get_flashed_messages() %} {{ message }} {% endfor %} </form> <hr> <h1>书籍列表</h1> <ul> {% for author in authors %} <li>{{author.name}}<a href="/delete_author/{{ author.id }}">删除</a></li> <ul> {% for book in author.books %} <li>{{ book.name }}<a href="/delete_book/{{ book.id }}">删除</a></li> {% endfor %} </ul> {% endfor %} </ul> </body> </html>
把Flask_day3推送到阿里云服务器:
scp -r ./Flask_day3 root@47.95.8.70:~/
在阿里云服务器进入到虚拟环境
workon py_flask
cd Flask_day3/

创建迁移仓库
python test2.py db init
创建迁移脚本
python test2.py db migrate -m'intitial'
更新数据库
python test2.py db upgrade
运行uwsgi
uwsgi --ini uwsgi.ini

在浏览器中输入
http://47.95.8.70

进入uwsgi.ini
vi uwsgi.ini
把守护线程打开,把log信息写入到文件中

运行uwsgi
uwsgi --ini uwsgi.ini

在浏览器中输入
http://47.95.8.70

停止uwsgi
uwsgi --stop uwsgi.pid
RESTful
REST是设计风格而不是标准。是指客户端和服务器的交互形式。我们需要关注的重点是如何设计REST风格的网络接口。
- REST的特点:
-
具象的。一般指表现层,要表现的对象就是资源。比如,客户端访问服务器,获取的数据就是资源。比如文字、图片、音视频等。
-
表现:资源的表现形式。txt格式、html格式、json格式、jpg格式等。浏览器通过URL确定资源的位置,但是需要在HTTP请求头中,用Accept和Content-Type字段指定,这两个字段是对资源表现的描述。
-
状态转换:客户端和服务器交互的过程。在这个过程中,一定会有数据和状态的转化,这种转化叫做状态转换。其中,GET表示获取资源,POST表示新建资源,PUT表示更新资源,DELETE表示删除资源。HTTP协议中最常用的就是这四种操作方式。
- RESTful架构:
- 每个URL代表一种资源;
- 客户端和服务器之间,传递这种资源的某种表现层;
- 客户端通过四个http动词,对服务器资源进行操作,实现表现层状态转换。
如何设计符合RESTful风格的API:
一、域名:
将api部署在专用域名下:
http://api.example.com
或者将api放在主域名下:
http://www.example.com/api/
二、版本:
将API的版本号放在url中。
http://www.example.com/app/1.0/info
http://www.example.com/app/1.2/info
三、路径:
路径表示API的具体网址。每个网址代表一种资源。 资源作为网址,网址中不能有动词只能有名词,一般名词要与数据库的表名对应。而且名词要使用复数。
错误示例:
不要出现动词
http://www.example.com/getGoods
http://www.example.com/listOrders
正确示例:
#获取单个商品 http://www.example.com/app/goods/1 #获取所有商品 http://www.example.com/app/goods
四、使用标准的HTTP方法:
对于资源的具体操作类型,由HTTP动词表示。 常用的HTTP动词有四个。
GET SELECT :从服务器获取资源。
POST CREATE :在服务器新建资源。
PUT UPDATE :在服务器更新资源。
DELETE DELETE :从服务器删除资源。
示例:
#获取指定商品的信息 GET http://www.example.com/goods/ID #新建商品的信息 POST http://www.example.com/goods #更新指定商品的信息 PUT http://www.example.com/goods/ID #删除指定商品的信息 DELETE http://www.example.com/goods/ID
五、过滤信息:
如果资源数据较多,服务器不能将所有数据一次全部返回给客户端。API应该提供参数,过滤返回结果。 实例:
#指定返回数据的数量 http://www.example.com/goods?limit=10 #指定返回数据的开始位置 http://www.example.com/goods?offset=10 #指定第几页,以及每页数据的数量 http://www.example.com/goods?page=2&per_page=20
六、状态码:
服务器向用户返回的状态码和提示信息,常用的有:
200 OK :服务器成功返回用户请求的数据 201 CREATED :用户新建或修改数据成功。 202 Accepted:表示请求已进入后台排队。 400 INVALID REQUEST :用户发出的请求有错误。 401 Unauthorized :用户没有权限。 403 Forbidden :访问被禁止。 404 NOT FOUND :请求针对的是不存在的记录。 406 Not Acceptable :用户请求的的格式不正确。 500 INTERNAL SERVER ERROR :服务器发生错误。
七、错误信息:
一般来说,服务器返回的错误信息,以键值对的形式返回。
{ error: 'Invalid API KEY' }
八、响应结果:
针对不同结果,服务器向客户端返回的结果应符合以下规范。
#返回商品列表 GET http://www.example.com/goods #返回单个商品 GET http://www.example.com/goods/cup #返回新生成的商品 POST http://www.example.com/goods #返回一个空文档 DELETE http://www.example.com/goods
九、使用链接关联相关的资源:
在返回响应结果时提供链接其他API的方法,使客户端很方便的获取相关联的信息。
十、其他:
服务器返回的数据格式,应该尽量使用JSON,避免使用XML。

浙公网安备 33010602011771号