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/<string:name>')

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

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中):

# 路由(../<int:id>)有参数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)

未完待续

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,951评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,606评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,601评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,478评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,565评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,587评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,590评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,337评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,785评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,096评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,273评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,935评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,578评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,199评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,440评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,163评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,133评论 2 352

推荐阅读更多精彩内容