Flask RESTful接口编写总结(一)

Flask RESTful接口编写总结(一)

1、创建一个虚拟环境:

> mkdir myflask
> cd myflask
> virtualenv -p /usr/bin/python venv
> source venv/bin/activate
(venv)> python # 查看一下python版本,我使用mac自带的2.7.10
(venv)>>> exit()

2、创建应用目录:

> mkdir app

按照下面结构创建目录结构:
———— app
    |—— __init__.py             创建工厂函数
    |—— ext.py                  插件实例化对象脚本
    |—— config.py               配置脚本
    |—— core/                   核心目录
    |—— common/                 通用目录
    |—— shared/                 共享目录
    |—— resource/               所有接口
    |—— utils/                  工具库
    ...

3、实现项目基本运行

开始编写项目的入口创建flask对象实例的工厂函数(__init__.py):

首先安装flask:

(venv)> pip install flask

在__init__.py中:

from flask import Flask

def create_app(app_config='default'):
    app = Flask(__name__)

    return app

if __name__ == "__main__":
    app = create_app()
    app.run(debug=True)

这样最简单的一个flask应用就可以通过命令运行:

(venv)> python __init__.py

* Serving Flask app "__init__" (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: on
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)
* Restarting with stat
* Debugger is active!
* Debugger PIN: 183-285-309

但是这样启动完全没有体现我们工厂函数的价值,接下来就要在此基础上使用flask_script插件拓展功能:

首先安装flask_script:

(venv)> pip install flask_script

进到项目根目录(app的上层),创建一个manage.py文件,以后这个文件将成为我们管理flask应用的脚本:

(venv)> cd ..
(venv)> touch manage.py

注意:从这以后创建目录,文件以及安装插件的命令都不在给出!

在manage.py中:

# -*- coding: utf-8 -*-
import os
import sys
from flask_script import Manager
from app import create_app

# 设置默认编码为utf-8
# 关于python2的编码,我也踩过不少坑,下面这篇文章讲得不错
# https://blog.csdn.net/vickyrocker1/article/details/50952095
reload(sys)
sys.setdefaultencoding('utf-8')

app = create_app()
manager = Manager(app)

# 在shell模式下默认引入的上下文对象
@manager.shell
def make_shell_context():
    return dict()

if __name__ == '__main__':
    manager.run() # 启动

在此尝试执行命令:

python manage.py runserver -h 0.0.0.0 -p 5000

* Serving Flask app "app" (lazy loading)
* Environment: production
WARNING: Do not use the development server in a production environment.
Use a production WSGI server instead.
* Debug mode: off
* Running on http://0.0.0.0:5000/ (Press CTRL+C to quit)

OK,又成功了,但是怎么会有一个WARNING呢?原来是我没有给flask应用提供相关配置,接下来开始配置应用:

进入app/config.py:

# -*- coding: utf-8 -*-
import os

# 取到根目录(../../myflask)
base_path = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))

# 由于我们可能有多个配置,传入app_config
def load_config(app, app_config):
    app.config.from_json(os.path.join(base_path, 'config/{}.json'.format(app_config)))

通过from_json这个方法导入配置,顾名思义需要一个json文件,方便管理,我们统一放在config文件夹下:

在config/default.json中:

{
    "ENV": "development",
    "TESTING": true,
    "DEBUG": true,
    "CSRF_ENABLED": true,
    "SECRET_KEY": "you-will-never-guess"
}

具体配置参数请查看官方文档,这里不一一介绍。

然后在工厂函数create_app中添加:

load_config(app, app_config)

这样再次启动警告就会消失。

4、实现RESTful接口

进入app/resource目录,创建目录结构:

———— demo                               以蓝本实例划分的业务模块
    |———— __init__.py                   创建蓝本实例
    |———— greet                         以Api实例划分的任务模块
         |—— view.py                    视图逻辑
         |—— url.py                     路由注册
         |—— __init__.py                创建Api实例
    ...
...

在demo/__init__.py中:

# -*- coding: utf-8 -*-
from flask import Blueprint

# 默认使用当前模块的文件夹名作为蓝本名称
blueprint = Blueprint(__name__.split('.')[-1], __name__)

# 注册路由到蓝本
from greet import url as greet_url

在demo/greet/__init__.py中:

# -*- coding: utf-8 -*-
from flask_restful import Api
from .. import blueprint

# 默认使用当前模块的文件夹名作为蓝本名称
api = Api(blueprint, prefix='/' + __name__.split('.')[-1])

在demo/greet/view.py中创建HelloResource接口:

# -*- coding: utf-8 -*-
from flask_restful import Resource

class HelloResource(Resource):
    def get(self, name):
        return 'hello {}!'.format(name)

在demo/greet/url.py中:

from . import api
from view import *

api.add_resource(HelloResource, '/hello/')

之后我们需要在工厂函数中添加:

from resource.demo import blueprint as demo_blueprint
app.register_blueprint(demo_blueprint, url_prefix='/' + demo_blueprint.name)

这样就把api->blueprint->app一层层向上绑定,再次启动项目,访问浏览器 http://0.0.0.0:5000/demo/greet/hello/world 收到响应:

"hello world!"

至此我们完成了从基本架构搭建到响应简单http请求等任务,接下来围绕处理请求,返回响应为核心,我们需要提供更多功能,包括以下方面:

1、请求参数的序列化解析/校验
2、响应(对象->字典)的序列化
3、错误请求的处理
4、异常的处理和记录
5、数据库的增删改查(核心、核心、核心,重要的事情说三遍)

5、实现输入输出的序列化

我使用到了webargs插件和flask_restful marsh_with函数分别定义输出输出字段

新建一个接口:

class WhoAreYouResource(Resource):
    @req_in({
        'name': _in.String(required=True),
        'gender': _in.String(required=True),
        'age': _in.Integer(required=True)
    })
    @req_out({
        'name': _out.String(),
        'gender': _out.String(),
        'age': _out.Integer()
    })
    def post(self, params):
        return params

在头部导入相关包:

from flask_restful import Resource, fields as _out, marshal_with as req_out
from webargs import fields as _in
from webargs.flaskparser import use_args as req_in

这里需要注意的是使用webargs的use_args装饰器,需要给应用绑定一个异常处理函数:

from webargs.flaskparser import parser

@parser.error_handler
def handle_request_parsing_error(error, request, schema):
    abort(status.UNPROCESSABLE_ENTITY, message=error.messages)

而且查看它的源码:

if as_kwargs:
    kwargs.update(parsed_args)
    return func(*args, **kwargs)
else:
    # Add parsed_args after other positional arguments
    new_args = args + (parsed_args,)
    return func(*new_args, **kwargs)

会把序列化的结果作为一个字典传递给函数定位参数的最后一个(路由参数在kwargs中):

# 路由(../)有参数id的情况下
@req_in(...)
def get(self, params, id):
    pass

并且,输入输出的序列化都支持嵌套:

class WhoAreYouResource(Resource):
    @req_in({
        'name': _in.String(required=True),
        'gender': _in.String(required=True),
        'age': _in.Integer(required=True),
        'deskmate': _in.Nested({
            'name': _in.String(required=True),
            'gender': _in.String(required=True),
            'age': _in.Integer(required=True)
        })
    }, locations=('json',))  # 指定取参的位置
    @req_out({
        'name': _out.String(),
        'gender': _out.String(),
        'age': _out.Integer(),
        'deskmate': _out.Nested({
            'name': _out.String(),
            'gender': _out.String(),
            'age': _out.Integer()
        })
    })
    def post(self, params):
        return params

就算嵌套字段的缺失,报错提示信息也非常直观:

{
    "message": {
        "deskmate": {
            "age": ["Missing data for required field."]
        }
    }
}

接下来进行参数的校验,参数校验只会发生在输入,输出往往由内部逻辑保证准确性和合法性,一般不需要再次校验。

对于需要校验的字段,可以指定它的validate属性:

'name': _in.String(required=True, validate=lambda val: len(val.decode('utf-8)) >= 8) # 用户名长度大于等于8

还可以指定为函数,只要返回结果为False或者触发ValidationError都意味着校验失败,错误信息默认为"Invalid value."。

既然遇到了错误请求,那么我们来处理这一问题:

首先修改flask_restful abort函数的功能,血的教训告诉我,只打印错误信息,之后来排错必定会无所适从,
我们需要打印错误的堆栈信息,准确定位错误代码:

在app/core/error.py中:

# -*- coding: utf-8 -*-
import sys
import traceback
from flask import current_app
from flask_restful import abort as rest_abort

def abort(http_status_code, **kwargs):
    """
    封装flask_restful的abort函数
    仅当开发环境时,接口返回详细错误信息
    """
    exc = traceback.format_exc()
    if not exc.startswith('None'):
        current_app.logger.exception(sys.exc_info())
        if current_app.config.get('ENV') == 'development':
            kwargs.setdefault('detail', exc)
    rest_abort(http_status_code, **kwargs)

这样开发环境下,错误返回会携带detail字段,说明错误的堆栈信息。

目前我们只在控制台打印出来了日志,实际生产环境中,我们还需要日志文件来定位问题:

在app/logger.py中创建函数add_file_logger:

def add_file_logger(app):
    handler = TimedRotatingFileHandler(app.config['LOG_FILE_PATH'], 'D', 1, 7, None, False, False)
    handler.setLevel(logging.DEBUG)
    handler.setFormatter(
        logging.Formatter('%(asctime)s - %(levelname)s - %(filename)s - %(funcName)s - %(lineno)s - %(message)s'))
    app.logger.addHandler(handler)

在app/config.py中添加:

log_dir_path = os.path.join(base_path, 'logs')
make_dirs(log_dir_path)
log_file_path = os.path.join(log_dir_path, time.strftime('%Y-%m-%d', time.localtime(time.time())) + '.log')
app.config['LOG_FILE_PATH'] = log_file_path
add_file_logger(app)

这样根目录下会根据日期生成日志:

———— logs
    |—— 2018-12-25.log
    ...

项目地址:https://github.com/dollphis/myyyyflask (commit: 1e0c28fcc233f975fc2e427fed7ce08491e61ab9)

未完待续

你可能感兴趣的:(Flask RESTful接口编写总结(一))