最近在看一些 API 规范化的资料,鉴于没有很合适的,就自己写了两个 Python 的包,顺便记录下感想。
先放上两个包的地址:
Why
说到规范,那基本就是 OpenAPI 了。目前最常用的工具就是 Swagger。第一次接触这个是看到果壳的一个有几百 star 的开源项目,扩展了官方的包,主要用来根据配置生成几个 Python Web Framework 的代码结构。之前重构 Dashboard 的时候还考虑过,看了下相关文档就放弃了,因为这个配置文件写起来真的很麻烦。这次重拾,也没打算真的让别人通过这种方式来规范化 API,感觉学习成本有点大,而且配置文件出错也不太好找原因(虽然有相关工具)。
看了一下目前有的几个 Python 的包,Flasgger 算是比较有代表性的一个,Date Science 各种教程里的后端基本也都是 Flask,直接选这个工具其实也是可以的。然后也看了下 FastAPI,这个项目真的挺有意思的,换了一种思路来做,通过 Python3 的注解取代配置文件里的类型,这里多亏了 pydantic 的封装,可以轻松定义数据类型,获取对应的 spec,还能做类型校验,非常好用。
这些库的 validation 大概有两种,一种是利用 marshmallow,另一种是 JSON,写起来其实都不如 pydantic 方便。
调查到这里,基本上就有点眉目了,首先否定了各种需要配置文件的库,这个太麻烦了,pydantic 已经做得很好了,实在没必要去写那些繁琐的配置。鉴于大多数人还是更熟悉 Flask,而且对异步没有要求,常用工具能满足需求就没必要换,所以 FastAPI 虽然不错但暂时不考虑。这时候就剩下两条路了,要么给 Flasgger 提 PR 增加新 feature,要么自己造轮子。
其实我本来倾向于 PR 的,这个东西已经小有名气了,代码质量也可以,接口也算是久经检验,更容易进行推广。不过后来发现了问题,pydantic 是 Python 3.6+ 的,而 Flasgger 还支持 Python2,这就剩下几个月的寿命了,没必要再折腾这个问题。而且为了兼容,Flasgger 的代码里面有很多略显冗余的代码,我要改的话可能也要考虑很多东西进去,这样就太麻烦了,估计 PR 也很难被 merge。思来想去,还是自己另外造个轮子吧,只需要满足自己的需求就可以了,不需要做那么多兼容的工作。
Implement
接下来就要开始动手了,这个工具的要求就是简单易用,能生成 API 文档,能做参数校验就可以了。
接口的设计,像 Flask 的 route 这样,用一个 decorator 就很方便,写起来很舒服,也被很多后端框架采用。剩下的部分参考 extensions/addons 规范来就可以了。
我 decorator 用的不多,只好临时再多学一下了,Real Python 有一篇不错的文章 Primer on Python Decorators,详细介绍了各种情况下 decorator 的用法。不过对于我的情况,还需要自己好好琢磨,比如我用了这个 decorator 之后,哪部分代码是初始化的时候被执行的,哪部分只会在调用该 function 的时候才会执行,修改哪个对应的 function 才是我从 route maps 里面拿到的那个 function。
另一方面就是对 Python 里面一切皆是 class 的理解,动态语言这方面还是有很高的可玩性,我可以在初始化的时候直接把对应的属性动态写入到 route function 上面去,然后生成文档的时候去 route maps 里面找就行了,这种做法比另外弄个 hashtable 之类的来折腾要舒服很多。
一开始其实是初始化的时候需要传入 Flask app 用来绑定的,这其实是 Flask extension 的一种常见做法了。不过后来发现这样会导致使用 blueprint 的时候容易遇到 cycling import 的问题,所以就改了一下,只需要最后 register 一下就可以了。这样其实就分成了两个独立的部分,validate 部分是用 decorator 绑定到 function 上面的,register 部分只负责添加 doc 的 route 并生成相应的文档,如果有特殊需要的话其实可以只用一部分。比如我其实只需要 CI 的时候生成文档然后发送的 document center 就行了,并不需要在 service 上保留 doc 的 route。
Usage
from flask import Flask, request
from pydantic import BaseModel, Schema
from random import random
from flaskerk import Flaskerk, HTTPException
app = Flask(__name__)
api = Flaskerk()
class Query(BaseModel):
text: str
class Response(BaseModel):
label: int
score: float = Schema(
...,
gt=0,
lt=1,
)
class Data(BaseModel):
uid: str
limit: int
vip: bool
e403 = HTTPException(code=403, msg='lucky for you')
@app.route('/api/predict//', methods=['POST'])
@api.validate(query=Query, data=Data, resp=Response, x=[e403])
def predict(source, target):
print(f'=> from{source}to{target}') # path
print(f'Data:{request.json_data}') # Data
print(f'Query:{request.query}') # Query
if random() < 0.5:
e403.abort()
return Response(label=int(10 * random()), score=random())
if __name__ == '__main__':
api.register(app)
app.run()
大概就是这样,目前支持对 Query,JSON data,Response 的检验。详情可以去 GitHub 看看。
Difference
这两个框架还是有些区别的。
首先,Flask 的 route 部分是放在 Werkzeug 里面的,最后 register 到 Flask app 里面,拿到的是一个简单的 hash table,key 是路径,value 是对应的 function。而 Falcon 是自己实现了这部分,可以看到有个状态机一样的东西。当然本质上都是一回事,而且拿过来之后都要根据对应的规则做一部分 parse,把路径里面的变量和参数提取出来。
然后是对 Exception 的处理,包括一些 HTTP status code,Flask 这部分也都是在 Werkzeug 里面,为了使用方便,我给它加了个自定义 status code 的功能。Falcon 里面分成了两部分,一个是 response 的 status code,一个是 HTTPStatus 的具体内容,我只需要知道可能的 code 用来生成文档就行了,所以这里简单把 status code 用正则处理了一下就完事儿。
最明显的使用差距,一个默认注册 route function,一个默认注册 route class,这一点反倒没有太多影响。
Why not Flask
其实我本来只实现了 Flaskerk,毕竟 Flask 是多数人的选择。但是后来在我去写 Service Template 的过程中,我发现很多东西要做得非常规范的话,就要用上 blueprint 之类的,否则新手写来写去很容易会遇到 circling import 的问题,虽然官方也给出了一个可行的做法,但是明显有点脏了,这里面涉及到 Python import 的细节,对新手极不友好。可以说,新手很容易就能用 Flask,但跟老手用起来完全不是一个概念,遇到点问题可能就很痛苦了。
其次就是 Flask 本身虽然是 microservice,但也包括了 template 部分,而 Data Science 需要的其实是一个纯粹的 API service 就可以了,Flask 默认返回的是 HTML 页面,不是 JSON 数据之类的,所以我封装 abort 的时候就顺手改了一下。Falcon 其实更适合,没有第三方依赖(当然还是需要 WSGI),很适合且本身就是为 API service 设计的。
最后 performance 的问题,当然是 Falcon 好了,不过这一点其实没那么重要,毕竟实际中 IO 不是瓶颈,CPU/GPU 部分的计算才是瓶颈。这也是为什么用不到异步。
在后面实现 Falibrary 的过程中,相当于重新思考了一遍,找出来原来实现上的一些小毛病,大概跟修改自己的作文一个效果吧,也不能算没有收获。
Rethink
我看到有人给 APIspec 加了 Falcon 的实现,想着大概也可以抽象出来一部分,不同的框架作为类似 middleware 的东西插进去,不过看起来好像也没有太多重复的部分,所以暂时不折腾了。我本来就是为了满足 Data Science 需求的,都没有去考虑 Cookie 之类的,剩下的以后再说吧。