第七章 构建RESTful API
API,即应用编程接口,可以概括为应用对开发者的接口。就像用户有一个可以和应用沟通的可视化界面一样,开发者同样需要一个接口和应用交互。REST,即表现层状态转移,它不是一个协议或者标准。它只是一种软件架构风格,或者是为编写应用程序定义的一组约束,旨在简化应用程序内外接口。当web服务API遵循了REST风格进行编写时,它们就可以称为RESTful API。RESTful使得API和应用内部细节分离。这使得扩展很容易,并且使得事情变得简单。统一接口确保每个请求都得文档化。
提示
关于REST和SOAP哪个好存在一个争论。它实际上是一个主观问题,因为它取决于需要做什么。每个都有它自己的好处,应该根据应用程序的需要来进行选择。
这一章,我们将包含下面小节:
- 创建一个基于类的REST接口
- 创建一个基于扩展的REST接口
- 创建一个SQLAlchemy-independent REST API
- 一个完整的REST API例子
介绍
从名字可以看出,表现层状态转移(REST)意味着可以分离API到逻辑资源,这些资源可以通过使用HTTP请求获得和操作,一个HTTP请求由GET,POST,PUT,PATCH,DELETE中的一个(还有其他HTTP方法,但这些是最常使用的)。这些方法中的每一个都有一个特定的意义。REST的关键隐含原则之一是资源的逻辑分组应该是简单容易理解的,提供简单性和可移植性。
这本书到这里,我们一直在使用一个资源叫做Product。让我们来看看怎么讲API调用映射到资源分离上:
- GET /products/1:获取ID为1的商品
- GET /products:获取商品列表
- POST /products:创建一个新商品
- PUT /products/1:更新ID为1的商品
- PATCH /products/1:部分更新ID为1的商品
- DELETE /products/1:删除ID为1的商品
创建一个基于类的REST接口
在第四章里我们看到了在Flask里如何使用基于类的视图。我们将使用相同的概念去创建视图,为我们应用提供REST接口。
准备
让我们写一个简单的视图来处理Product模型的REST接口。
怎么做
需要简单的修改商品视图,来继承MethodView类:
from flask.views import MethodView
class ProductView(MethodView):
def get(self, id=None, page=1):
if not id:
products = Product.query.paginate(page, 10).items
res = {}
for product in products:
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name
}
# 译者注 加上这一句,否则会报错
res = json.dumps(res)
else:
product = Product.query.filter_by(id=id).first()
if not product:
abort(404)
res = json.dumps({
'name': product.name,
'price': product.price,
'category': product.category.name
})
return res
get()方法搜索product,然后返回JSON结果。
可以用同样的方式完成post(),put(),delete()方法:
def post(self):
# Create a new product.
# Return the ID/object of newly created product.
return
def put(self, id):
# Update the product corresponding provided id.
# Return the JSON corresponding updated product.
return
def delete(self, id):
# Delete the product corresponding provided id.
# Return success or error message.
return
很多人会想为什么我们没在这里写路由。为了包含路由,我们得像下面这样做:
product_view = ProductView.as_view('product_view')
app.add_url_rule('/products/', view_func=product_view, methods=['GET', 'POST'])
app.add_url_rule('/products/<int:id>', view_func=product_view, methods=['GET', 'PUT', 'DELETE'])
第一句首先转换类为实际的视图函数,这样才可以用在路由系统中。后面两句是URL规则和其对应的请求方法。
译者注
测试时如果遇到/products/路由已经注册,原因可能是第四章已经定义了一个/products/视图函数,注释掉即可,或者修改这里的路由名称。
原理
MethodView类定义了请求中的HTTP方法,并将名字转为小写。请求到来时,HTTP方法匹配上类中定义的方法,就会调用相应的方法。所以,如果对ProductView进行一个GET调用,它将自动的匹配上get()方法。
更多
我们还可以使用一个叫做Flask-Classy的扩展(https://pythonhosted.or/Flask-Classy
)。这将在很大程度上自动处理类和路由,并使生活更加美好。我们不会在这里讨论这些,但它是一个值得研究的扩展。
创建基于扩展的REST接口
前面一节中,我们看到如何使用热插拔的视图创建一个REST接口。这里我们将使用一个Flask扩展叫做Flask-Restless。Flask-Restless是完全为了构建REST接口而开发的。它提供了一个简单的为使用SQLAlchemy创建的数据模型构建RESTful APIs的方法。这些生成的api以JSON格式发送和接收消息。
准备
首先,需安装Flask-Restless扩展:
$ pip install Flask-Restless
我们借用第四章的程序构建我们的应用,以此来包含RESTful API接口。
提示
如果views和handlers的概念不是很清楚,建议在继续阅读之前,先去阅读第四章。
怎么做
通过使用Flask-Restless是非常容易向一个SQLAlchemy模型新增RESTful API接口的。首先,需向应用新增扩展提供的REST API管理器,然后通过使用app对象创建一个实例:
from flask_restless import APIManager
manager = APIManager(app, flask_sqlalchemy_db=db)
之后,我们需要通过使用manager实例使能模型里的API创建。为此,需向views.py新增下面代码:
from my_app import manager
manager.create_api(Product, methods=['GET', 'POST', 'DELETE'])
manager.create_api(Category, methods=['GET', 'POST', 'DELETE'])
这将在Product和Category模型里创建GET,POST,DELETE这些RESTful API。通常,如果methods参数缺失的话,只支持GET方法。
原理
为了测试和理解这些是如何工作的,我们通过使用Python requests库发送一些请求:
>>> import requests
>>> import json
>>> res = requests.get("http://127.0.0.1:5000/api/category")
>>> res.json()
{u'total_pages': 0, u'objects': [], u'num_results': 0, u'page': 1}
译者注
res.json()可能会从出错,可使用res.text
我们发送了一个GET请求去获取类别列表,但是现在没有记录。来看一下商品:
>>> res = requests.get('http://127.0.0.1:5000/api/product')
>>> res.json()
{u'total_pages': 0, u'objects': [], u'num_results': 0, u'page': 1}
我们发送了一个GET请求去获取商品列表,但是没有记录。现在让我们创建一个商品:
>>> d = {'name': u'iPhone', 'price': 549.00, 'category':{'name':'Phones'}}
>>> res = requests.post('http://127.0.0.1:5000/api/product', data=json.dumps(d), headers={'Content-Type': 'application/json'})
>>> res.json()
{u'category': {u'id': 1, u'name': u'Phones'}, u'name': u'iPhone',
u'company': u'', u'price': 549.0, u'category_id': 1, u'id': 2, u'image_path': u''}
我们发送了一个POST请求去创建一个商品。注意看请求里的headers参数。每个发给Flask-Restless的POST请求都应该包含这个头。现在,我们再一次搜索商品列表:
>>> res = requests.get('http://127.0.0.1:5000/api/product')
>>> res.json()
{u'total_pages': 1, u'objects': [{u'category': {u'id': 1, u'name': u'Phones'}, u'name': u'iPhone', u'company': u'', u'price': 549.0, u'category_id': 1, u'id': 1, u'image_path': u''}], u'num_results': 1, u'page': 1}
我们可以看到新创建的商品已经在数据库中了。
同样需要注意的是,查询结果默认已经分好页了,这是优秀的API的标识之一。
更多
自动创建RESTful API接口非常的酷,但是每个应用都需要一些自定义,验证,处理业务的逻辑。
这使得使用preprocessors和postprocessors成为可能。从名字可以看出,preprocessors会在请求被处理前运行,postprocessors会在请求处理完,发送给应用前运行。它们被定义在create_api()中,做为请求类型(GET,POST等)映射,并且作为前处理程序或后处理程序的方法列表,用于处理指定的请求:
manager.create_api(
Product,
methods=['GET', 'POST', 'DELETE'],
preprocessors={
'GET_SINGLE': ['a_preprocessor_for_single_get'],
'GET_MANY': ['another_preprocessor_for_many_get'],
'POST': ['a_preprocessor_for_post']
},
postprocessors={
'DELETE': ['a_postprocessor_for_delete']
}
)
单个或多个记录都可以调用GET,PUT,PATCH方法;但是它们各有两个变体(variants)。举个例子,前面的代码里,对于GET请求有GET_SINGLE和GET_MANY。preprocessors和postprocessors对于各自请求接收不同的参数,然后执行它们,并且没有返回值。参见https://flask-restless.readthedocs.org/en/latest/
了解更多细节。
译者注
对preprocessor和postprocessors的理解,参见http://flask-restless.readthedocs.io/en/stable/customizing.html#request-preprocessors-and-postprocessors
创建一个SQLAlchemy-independent REST API
在前一小节中,我们看到了如何使用依赖于SQLAlchemy的扩展创建一个REST API接口。现在我们将使用一个名为Flask-Restful的扩展,它是在Flask可插拔视图上编写的,并且独立于ORM。
准备
首先,安装扩展:
$ pip install Flask-Restful
我们将修改前面的商品目录应用,通过使用这个扩展增加一个REST接口。
怎么做
通常,首先要修改应用的配置,看起来像这样:
from flask_restful import Api
api = Api(app)
这里,app是我们应用的对象/实例。
接下来,在views.py里创建API。在这里,我们将尝试理解API的框架,更详细的实现在下一小节里:
from flask_restful import Resource
from my_app import api
class ProductApi(Resource):
def get(self, id=None):
# Return product data
return 'This is a GET response'
def post(self):
# Create a new product
return 'This is a POST response'
def put(self, id):
# Update the product with given id
return 'This is a PUT response'
def delete(self, id):
# Delete the product with given id
return 'This is a DELETE response'
前面的API结构是很容易理解的。看下面代码:
api.add_resource(ProductApi, '/api/product', '/api/product/<int:id>')
这里,我们为ProductApi创建路由,我们可以根据需要指定多条路由。
原理
我们将使用Python requests库在看这些是如何工作的,就像前一小节那样:
>>> import requests
>>> res = requests.get('http://127.0.0.1:5000/api/product')
>>> res.json()
u'This is a GET response'
>>> res = requests.post('http://127.0.0.1:5000/api/product')
>u'This is a POST response'
>>> res = requests.put('http://127.0.0.1:5000/api/product/1')
u'This is a PUT response'
>>> res = requests.delete('http://127.0.0.1:5000/api/product/1')
u'This is a DELETE response'
在前面一小段代码中,我们看到了我们的请求被相应的方法处理了;从回复中可以确认这一点。
其他
- 确保在继续向下阅读之前先阅读完这一小节
一个完整的REST API例子
这一小节,我们将上一小节的API框架改写为一个完整的RESTful API接口。
准备
我们将使用上一小节的API框架作为基础,来创建一个完整的SQLAlchemy-independent RESTful API。尽管我们使用SQLAlchemy作为ORM来进行演示,这一小节可以使用任何ORM或者底层数据库进行编写。
怎么做
下面的代码是Product模型完整的RESTful API接口。views.py看起来像这样:
from flask_restful import reqparse
parser = reqparse.RequestParser()
parser.add_argument('name', type=str)
parser.add_argument('price', type=float)
parser.add_argument('category', type=dict)
前面的一小段代码,我们为希望在POST,PUT请求中解析出来的参数创建了parser。请求期待每个参数不是空值。如果任何参数的值是缺失的,则将使用None做为值。看下面代码:
class ProductApi(Resource):
def get(self, id=None, page=1):
if not id:
products = Product.query.paginate(page, 10).items
else:
products = [Product.query.get(id)]
if not products:
abort(404)
res = {}
for product in products:
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name
}
return json.dumps(res)
前面的get方法对应于GET请求,如果没有传递id,将返回商品分好页的商品列表;否则,返回匹配的商品。看下面POST请求代码:
def post(self):
args = parser.parse_args()
name = args['name']
price = args['price']
categ_name = args['category']['name']
category = Category.query.filter_by(name=categ_name).first()
if not category:
category = Category(categ_name)
product = Product(name, price, category)
db.session.add(product)
db.session.commit()
res = {}
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name,
}
return json.dumps(res)
前面post()方法将在POST请求时创建一个新的商品。看下面代码:
def put(self, id):
args = parser.parse_args()
name = args['name']
price = args['price']
categ_name = args['category']['name']
category = Category.query.filter_by(name=categ_name).first()
Product.query.filter_by(id=id).update({
'name': name,
'price': price,
'category_id': category.id,
})
db.session.commit()
product = Product.query.get_or_404(id)
res = {}
res[product.id] = {
'name': product.name,
'price': product.price,
'category': product.category.name,
}
return json.dumps(res)
前面代码,通过PUT请求更新了一个已经存在的商品。这里,我们应该提供所有的参数,即使我们仅仅想更新一部分。这是因为PUT被定义的工作方式就是这样。如果我们想要一个请求只传递那些我们想要更新的参数,这应该使用PATCH请求。看下面代码:
def delete(self, id):
product = Product.query.filter_by(id=id)
product.delete()
db.session.commit()
return json.dumps({'response': 'Success'})
最后同样重要的是,DELETE请求将删除匹配上id的商品。看下面代码:
api.add_resource(
ProductApi,
'/api/product',
'/api/product/<int:id>',
'/api/product/<int:id>/<int:page>'
)
上一句代码是我们的API可以容纳的所有URL的定义。
提示
REST API的一个重要方面是基于令牌的身份验证,它只允许有限和经过身份验证的用户能够使用和调用API。这将留给你自己探索。我们在第6章Flask认证中介绍的用户身份验证的基础知识,将作为此概念的基础。
代码下载链接:
https://pan.baidu.com/s/1o7GyZUi 密码:x9rw
http://download.csdn.net/download/liusple/10186764
完。