flask如何嵌入swagger文档

开始之前,先了解下什么是swagger。

Swagger 是最流行的 API 开发工具,它遵循 OpenAPI Specification(OpenAPI 规范,也简称 OAS)。
Swagger 可以贯穿于整个 API 生态,如 API 的设计、编写 API 文档、测试和部署。
Swagger 是一种通用的,和编程语言无关的 API 描述规范。

swagger_editornew.png

过多的也不是说了,可以看官网详细介绍 https://swagger.io/irc/

今天主要说的就是flask里面如何嵌入swagger文档。

首先需要的准备工作,配置环境之类的就不在这里多言。主要的依赖除了flask外,还有一个flask-restplus。我们平常用的RESTful风格的开发依赖包是flask-restful,在这里要替换一下,两者之间并没有什么太大的区别,flask-restful有的东西和使用方法在flask-restplus里面都有。可能为什么叫plus就是这个原因吧。。。

言归正传,安装flask-restplus可以直接使用pip来安装

pip install flask-restplus。

在正常的开发过程中,我们一般都是需要先构建蓝图

blue_print_obj = Blueprint(app_string, __name__, url_prefix=url_string)
user_blue_api = Api(blue_print_obj)

这里我们就可以修改一下

self.blueprint = Blueprint(app_string, __name__)
self.api = Api(self.blueprint, title="Biz Service Api", version="v1.0.1",
               description="biz service", doc="/swagger/doc")

Blueprint的参数可以和原来的不变,Api的参数就需要变了,第一个参数依旧为一个app或Blueprint对象,title、version、desription顾名思义就是原意,doc的参数就是项目启动后访问的路径,如:http://localhost:8080/swagger/doc

除了这些区别外,其所处的级别位置也有所变更,在flask-restful中,Buleprint配合一个Api对象定义的和操作的是一个模块,如:订单模块、用户模块。每个模块里面的各个接口再通过add_resource方法进行注册添加,而在flask-restplus中,代表的是一个版本(个人理解,有不同意见的可以在评论留言,勿喷)。
flask-restplus比flask-restful多的就是一个命名空间namespace

ns = self.api.namespace(name=name, description=description, path="url_prefix")

而add_resource方法就在namespace里面,namespace方法就是创建一个命名空间,其对应的还有一个add_namespace方法,但是我们在创建的时候不用调用,因为在调用namespace方法的同时,他的返回值就是add_namespace方法的调用,return一个Namespace对象回来。而我们后续关于swagger的所有操作都是建立在这个Namespace上的。之前在网上各种搜索,关于如何在flask项目里面嵌套进swagger文档,都只是很浅显的讲了需要namespace,至于文档的书写位置都采用的是原始的装饰器写法,如:

@ns.route('/<string:todo_id>')
@api.doc(responses={404: 'Todo not found'}, params={'todo_id': 'The Todo ID'})
class Todo(Resource):

ns就是被赋值Namespace的变量,而api就是一个Apiece对象,不过api.doc同样可以换成ns.doc。(个人喜欢这样,而且后面讲的也是基于ns)。在这里我们从api.doc已经可以看出一些端倪了。没错,就是在接口上通过装饰器的方式定义该接口的请求、响应参数的格式等等。。。但是,我想说的是这样的写法太难受了。我们可以想象,一个成熟的flask项目,接口肯定不是一个两个,也可能是好几个版本的。那么在views里面要多写多少的装饰器来定义路径、请求、响应参数。用我们的玩笑话说,太low了。因此,我们要进行一些改善,在构建蓝图,注册路由的时候就把swagger需要的接口信息给添加上去。

我们首先要认识到,装饰器并不是特别的东西,你把它理解为一个特别的函数也并非不可以。所以,我们就可以在注册路由的时候主动调用这个装饰器函数来加载接口信息。现在我们在构建蓝图的时候,一般都是通过配置文件来动态加载,配置路径。所以在这里,我们处理需要常规的接口路径配置文件外,同样需要一份儿接口文档的配置文件


Capture.PNG

这儿我采用的就是json文件,一份可以在Swagger Editor上面完整显示出来的json文件。文件的写法完全符合Swagger Editor的语法要求,用yaml文件也可以,个人喜欢json文件,看着顺眼,这个全凭个人喜好。无论哪种,第一,都是按照Swagger Editor的语法写的,第二,对于python来说,读取出来转换完毕都是字典对象(JSON)。

说回正题。我们首先导入swagger的json文件。

with open("utils/swagger.json", "r", encoding="utf-8") as f:
    swagger_dict = json.loads(f.read())

前面都说了,flask-restplus内动态嵌入文档的关键即使Namespace对象。所以在使用ns调用add_resource方法的时候,我们就开始在这上面做文章了。

path_info = swagger_dict["paths"].get(url)
ns.add_resource(doc(ns, views_obj, path_info, url), url, endpoint=views_obj.__name__)

url:很明显的就是接口的路径,如上图所示,paths里面的键:"/token",至于url怎么来的不用多少了吧,通过配置文件动态读取出来的,后期上源码以后可以仔细研究。
path_info:就是写好的swagger文档,关于该接口的所有信息(如上图)。
doc:自定义的方法(给接口嵌套文档),需要四个参数,很明显的需要namespace(ns)、接口对象(views_obj),接口文档信息(path_info),接口路径(url)。

doc

def doc(ns, obj, path_info=None, url=None):
    if path_info is None:
        return obj
    for method, method_info in path_info.items():
        method_info["responses"] = {code: format_responses(ns, url, method, code, response_info) for code, response_info
                                    in method_info["responses"].items()}
        method_info["params"] = {parameter_info_dict.pop("name"): parameter_info_dict for parameter_info_dict in
                                 method_info.pop('parameters')}
    return ns.doc(**path_info)(obj)

doc方法其实主要做的就是对swagger文档进行一些格式转换,把swagger语法转换成flask-restplus能够识别的格式(觉得可以直接导入的童鞋们就真的有点儿图样图森破了)。
method:接口的方法get,post,put,delete...
method_info:该方法的属性/参数。
params:该方法的请求参数。params对应的就是swagger文档内的parameters参数(parameters为列表,而params是字典结构)。数据结构不是单纯的{"params": {......}}这种。而是要以参数/字段名为键,值为该参数/字段的属性/参数。具体怎么实现的就看代码吧,不过多讲解。注:源码flask-reatplus.swagger.Swagger.parameters_for()可查,444行。
responses:该方法的响应参数。响应参数相对来说比请求参数复杂一些,其包含五种数据类型:string, integer,bool,dict,list。而dict和list又可以互相嵌套,需要着重处理来应对各种嵌套情况。因此,我们创建新的方法format_responses来进行处理。

常量FIELD_TYPE

FIELD_TYPE = {
    "integer": fields.Integer,
    "boolean": fields.Boolean,
    "string": fields.String,
    "array": fields.List,
    "object": fields.Nested
}

format_responses

def format_responses(ns, url, method, code, response_info):
    description = response_info.get('description') or ""
    schema = response_info.get("schema")
    model_name = ns.name + url.replace(r"/", r"-") + r"-" + method.upper() + r"-" + code
    if schema:
        schema = set_schema(ns, model_name, schema)
    else:
        schema = "NoneDict"
    return description, schema

description:该method的描述
schema:参数详情
model_name:model名字,这里我们使用了模块名、url、大写method以"-"进行了拼接。具体什么用,我们后面再讲。

format_responses方法只是对响应参数做了一个初步的判断,如果你没有写description和schema,最有都将返回默认值,如果schema有值,再通过set_schema进行下一步的判断

set_schema

def set_schema(ns, model_name, schema):
    if 'properties' in schema:
        schema = set_object(ns, model_name, schema.get('properties', {}))
    elif "$ref" in schema:
        model_name = schema.get("$ref").rsplit(r"/", 1)[-1]
        if model_name in ns.models:
            schema = model_name
        else:
            temp_swagger = swagger_dict['definitions'][model_name]['properties']
            schema = set_object(ns, model_name, deepcopy(temp_swagger))
    elif schema.get("type") == "array":
        schema = [set_schema(ns, model_name, schema.get("items", {}))]
    else:
        schema = FIELD_TYPE[schema.get("type", "string")](**schema)
    return schema

正常来说schema里面会出现这几种情况:
properties:如果有这个字段,说明code的响应结构为字典。取出放入
set_object方法进行处理。
ref:如果有ref字段,说明需要导入model(swagger文档最后有已写好的model)。在文档的"definitions"字段里面。model名字则为"ref"对应值以"/"分割的后面一段。查看ns.models内是否有该model,如果有直接返回model_name,如果没有则取出该model放入set_object方法进行处理。
array:代表数据结构是列表。这里是通过get("type")获得的,如果是array那肯定是列表。因为这里是第一层,所以如果是列表的话,需要在外面嵌套一个[],并把代表列表元素属性/参数的items取出来,递归set_schema方法。注:flask-reatplus.swagger.Swagger.serialize_schema()可查。548行。
else:能运行到这里,说明第一层不是字典和列表,那就只能是string,integer,bool。直接get("type"),默认为"string"。从常量FIELD_TYPE中取出fields对象。
可能有同学质疑为什么这么判断,这个问题不好多少,研究下swagger语法自然就明白了。

敲黑板 重点来了

转换响应参数的数据结构,关键点就是梳理清楚,常量FIELD_TYPE 定义的五种数据类型(field对象)之间的关系。

首先来说,string,integer,bool之间没有什么联系,能独用或嵌套进字典或列表里面,不用多少。
object:swagger语法定义type为object的话,那就肯定是字典了,其参数只有一个就是"properties": {......},"properties"是跟"type"同级位置的。如果没有"properties"的话,代表是个空字典。
array:swagger语法定义type为array的话,那就肯定是列表了。与type同级必有一个items的key,里面放着列表内的元素属性/参数。这里要说一下,按照swagger的语法,items这个key是肯定有的,可以为空,但是必须有。
回到flask-restplus上,object类型对应的field对象是fields.Nested, 而array类型对应的是fields.List。他们之间没有必然的关系。
fields.Nested:对应的type就是object,第一个参数需要的是一个Model对象,还记不记得前面一直传的一个参数,model_name。这个时候就派上用场了。其实可以理解为一个字典结构就是一个model,如果没有参数就是一个空字典。如果可以确定这个model在之前已经创建过的话,这里不需要重新创建,直接把model的名字以字符串的形式放在这里就可以,不用再创建model。
fields.List:对应的type就是array,第一个参数需要的是一个fields对象。五种fields对象都可以。
fields.Integer、fields.Boolean、fields.String 这三种类型就比较简单了。对应的type类型就是integer,boolean,string,直接把响应的参数以关键字的形式代入就可以了。
因此,在编写构建逻辑的时候,我就以功能不同给分成了几个方法,依照type类型的不同,循环嵌套调用进行构建。

def set_field(ns, name, items):
    property_type = items.pop("type", "string")
    if "$ref" in items:
        model_name = items['$ref'].rsplit(r"/", 1)[-1]
        if model_name in ns.models:
            field_obj = fields.Nested(model_name)
        else:
            temp_swagger = swagger_dict['definitions'][model_name]['properties']
            field_obj = fields.Nested(set_object(ns, model_name, deepcopy(temp_swagger)))
    elif property_type == "array":
        properties = items.get("items", {})
        field_obj = set_array(ns, name, properties)
    elif property_type == "object" or "properties" in items:
        properties = items.get("properties", {})
        field_obj = fields.Nested(set_object(ns, name, properties))
    else:
        field_obj = FIELD_TYPE[property_type](**items)
    return field_obj


def set_array(ns, name, items):
    """
    每一个list的具体参数(详情)字典
    :param ns: 命名空间
    :param name: model名字
    :param items: {...}
    :return: fields.List对象
    """
    field_obj = set_field(ns, name, items)
    return fields.List(field_obj)


def set_object(ns, model_name, items):
    """
    每一个model的具体key: value
    :param ns: 命名空间
    :param model_name: model名字
    :param items: {
                "code": {
                    "type": "string",
                    "example": "123fsf"
                },
                "error": {...},
                "errMsg": {...},
                "data": {...}
                    }
    :return: model对象
    """
    result = {}
    for property_name, property_params in iteritems(items):
        field_obj = set_field(ns, model_name + r"-" + property_name, property_params)
        result.update({property_name: field_obj})
    return ns.model(model_name if result else "NoneDict", result)

讲的比较分,所以代码给的也比较分。不过结合整篇代码融合一下就明白了。如果有不同见解要沟通,或者想要源码的评论留言或私信我,看到后一定及时回复。空闲之余所写,不是很及时,大家就将就看吧。。。

转载、引用请注明出处

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