开始之前,先了解下什么是swagger。
Swagger 是最流行的 API 开发工具,它遵循 OpenAPI Specification(OpenAPI 规范,也简称 OAS)。
Swagger 可以贯穿于整个 API 生态,如 API 的设计、编写 API 文档、测试和部署。
Swagger 是一种通用的,和编程语言无关的 API 描述规范。
过多的也不是说了,可以看官网详细介绍 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需要的接口信息给添加上去。
我们首先要认识到,装饰器并不是特别的东西,你把它理解为一个特别的函数也并非不可以。所以,我们就可以在注册路由的时候主动调用这个装饰器函数来加载接口信息。现在我们在构建蓝图的时候,一般都是通过配置文件来动态加载,配置路径。所以在这里,我们处理需要常规的接口路径配置文件外,同样需要一份儿接口文档的配置文件
这儿我采用的就是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)
讲的比较分,所以代码给的也比较分。不过结合整篇代码融合一下就明白了。如果有不同见解要沟通,或者想要源码的评论留言或私信我,看到后一定及时回复。空闲之余所写,不是很及时,大家就将就看吧。。。