标签归档:APIFlask

APIFlask 2.0 版本发布

一边上班一边赶工做开源项目真是太累了。更不用说还要一边焦虑写书的进度。不过这次 2.0 版本基本把想做的功能都完成了,剩下两个大的功能(为蓝本拆分单独的 OpenAPI Spec、解耦 marshmallow)实在是没精力加进来了,只好放到 2.1。

APIFlask 2.0 版本的 codename 是 Gongqing(年初我从五角场搬到了共青森林公园旁边。没错,1.0 版本是 Wujiaochang)。

回到正题,这次最主要的一个变动(breaking change)是把所有 input 装饰器的参数改成了关键字参数。

@app.post('/pets')
@app.input(PetQuery, location='query')
@app.input(PetIn)  # equals to app.input(PetIn, location='json')
def create_pet(query_data, json_data):
    pass

关键字参数的名字默认是 {location}_data,通过 arg_name 参数可以自定义。这个改动让我删掉了很多 hack Flask 的代码。推动这个功能在上游 webargs 实现,所以不用加代码在自己这里。再加上一些改动已经在 Flask 实现了,又删掉一些。非常轻松。理想情况是,文档越来越多,代码越来越少。

如果你感兴趣的话,下面是 2.0 版本的详细内容:

 

APIFlask 1.0 版本发布

2023/7 Update: APIFlask 2.0 版本有一些变动,详见这篇文章


APIFlask 是一个基于 Flask 和 marshmallow 实现的 API 框架(详细介绍见这篇文章)。经过一年的迭代,终于在这个月初为它发布了 1.0 版本(撒花~)。

1.0 版本主要带来了三个新功能:

  • 支持处理文件上传,新增 File 字段
  • 支持使用外部的认证库
  • 添加了几个新的请求解析位置:json_or_formform_and_files 以及 path

一个 API 变动(breaking change):

  • inputoutputdocauth_required 这四个装饰器移动到了程序实例和蓝本实例上(比如 app.input() / bp.input())。

还有一些代码上的修正和优化,具体可以在变更日志里看到。欢迎更新试用和提交反馈:

$ pip install -U apiflask

在代码之外,也有一些变动和改进:

如果你正在用它开发你的个人项目或是公司项目,欢迎在这里留言告诉我们(我)!

欢迎来 GOTC 大会听我的演讲

GOTC(The Global Opensource Technology Conference,全球开源技术峰会)是由开源中国(OSCHINA)和 Linux 软件基金会(The Linux Foundation)联合主办的开源技术大会。上海站(上海世博中心 SECC)在 7 月 9 日 ~ 7 月 10 日举办;深圳站(深圳会展中心)在 7 月 31 日 ~ 8 月 1 日举办。大会邀请到很多非常厉害的开发者,比如 Linus Torvalds。两个会场均免费开放,仅需报名即可。欢迎参加!

我的演讲主题是《基于 Python 的 OpenAPI 实践》,介绍如下:

OpenAPI(原 Swagger)是一个流行的 REST API 描述规范,围绕它有许多非常实用的 Web API 开发工具。而 Python 是一门流行的编程语言,使用它可以非常高效地开发 Web API。这个议题将会以 OpenAPI 的入门介绍开始,探索如何在流行的 Python Web 框架中实现 OpenAPI 支持,并在最后介绍一些现有的解决方案和工具。

这个演讲基本上是正在写的《Python Web API 设计与开发》第八章的内容概括 + 开发 APIFlask 的一些想法 + 周边项目(FastAPIConnexion 等)研究总结。暂定大纲为:

  • OpenAPI 介绍
  • Code-first 模式(从 Python Web 框架代码生成 OpenAPI spec)
  • Design/API-first 模式(从 OpenAPI spec 生成 Python Web 框架基础代码)

我的演讲在深圳站第一天分会场「编程语言艺术」专题论坛。如果你感兴趣的话,欢迎来听;如果对演讲内容有任何建议,欢迎提出。

请不要把 Flask 和 FastAPI 放到一起比较

The war has begun. Let’s end the war between Flask and FastAPI.

去年在知乎上看到一篇文章,后来想找却找不到了,最后通过关键词「5 分钟」和「四年」在掘金上找到了原文——《用它5分钟以后,我放弃用了四年的 Flask》。作者先是给出了一个手动用 if 验证请求数据的 Flask 视图函数:

@app.route('/insert', methods=['POST'])
def insert():
    info = request.json
    name = info.get('name', '')
    if not name:
        return {'success': False, 'msg': 'name 参数不可省略,不可为空!'}
    age = info.get('age', 0)
    if not isinstance(age, int):
        return {'success': False, 'msg': 'age参数不是数字!'}
    age_after_10_years = age + 10
    msg = f'此人名叫:{name},10年后,此人年龄:{age_after_10_years}'
    return {'success': True, 'msg': msg}

然后给出了一个使用 Pydantic 编写数据模型类 + FastAPI 路径操作函数的代码:

class People(BaseModel):
    name: str
    age: int
    address: str
    salary: float
    
@app.post('/insert')
def insert(people: People):
    age_after_10_years = people.age + 10
    msg = f'此人名字叫做:{people.name},十年后此人年龄:{age_after_10_years}'
    return {'success': True, 'msg': msg}

通过这两段代码进行对比,文章作者得出结论「我用了 Flask 四年,但在使用了5分钟 FastApi 以后,我决定以后不再使用 Flask 了」。你确定你用了四年的是 Flask 而不是 Flash?在此后的很长一段时间里,经常看到类似的对比和结论,所以我打算用这篇文章来澄清这个非常普遍的误解。如果看到有人把 FlaskFastAPI 放到一起比较,请把这篇文章的链接丢过去。

利益相关:笔者系 Flask 维护者和 APIFlask 作者。本文观点仅代表个人看法,与 Pallets 团队无关。

为什么 Flask 和 FastAPI 不能放到一起比较?

我在此前介绍 APIFlask 的文章里一开始就提到过这件事情:

经常看到有人把 FastAPI 和 Flask 放到一起比较,但是却没有意识到这完全是两种东西——前者是基于 Web 框架 Starlette 添加了 Web API 功能支持的(框架之上的)框架,而后者是和 Starlette 同类的通用 Web 框架。你怎么能让小明和骑电动车的小军赛跑然后还夸小军好快好强?

前两天看了 Reddit 上 Flask 2.0rc 帖子下的很多回复,预感到在 Flask 2.0 发布后在中文网络上也许会出现许多类似的问题和讨论,所以想着还是值得单独写一篇文章来谈这件事。

受那个 Reddit 主题里某个评论启发,想到一个更好的比喻: Flask 和 FastAPI 就像是苹果和橙汁。你不能把苹果和添加了更多蔗糖和添加剂的橙汁放在一起比较哪一个更甜。苹果(Flask)应该和橙子(Starlette)比较,橙汁(FastAPI)当然也应该和苹果汁(基于 Flask 的 Web API 框架)进行比较。也就是说,Flask 是一个通用型框架,和 FastAPI 依赖的 Starlette 一样,而 FastAPI 是添加 Web API 支持的二次框架。因此,Flask 应该和 FastAPI 所依赖的 Starlette 进行比较,而 FastAPI 应该和基于 Flask 的 Web API 框架进行比较。

同理,FastAPI 也不能和 Django、Tornado、web2py、Bottle、CherryPy、Pyramid、Sanic 等通用型 Web 框架比较。

更合理的比较对象是什么?

既然「FastAPI 应该和基于 Flask 的 Web API 框架比较」,那么合适的比较对象有哪些?Flask-RESTXFlask-Rebarflask-apispecflask-smorestFlask-RESTfulAPIFairy 这些虽然试图做成框架,但在具体实现上仍然是 Flask 扩展,所以真正公平合理的比较对象是:

这三个框架都是基于 Flask 实现的 Web API 框架。不过 Eve 没有内置 OpenAPI 支持,而 Connexion 是一个 Spec-First 框架,也就是 OpenAPI spec 优先(先编写 spec,然后生成项目基础代码进行开发)的框架。因此,尽管 APIFlask 只是一个刚发布没多久的实验项目,但它是最适合和 FastAPI 进行比较的项目。

FastAPI 和 APIFlask 有着同样的组成结构,都是作为一层胶水粘起来一个 Web 框架和一个数据序列化/验证库,然后加一点 Web API 功能支持:

在主要功能上也有很多交集:

  • 自动反序列化和验证请求数据,自动生成错误响应
  • 自动格式化和序列化响应数据
  • 自动生成 OpenAPI specification 文件
  • 自动生成交互式 API 文档

当然,相对于发布已经三年的 FastAPI,APIFlask 还只是一个三个月大的新项目,还有很多地方需要改进和完善。如果你对 APIFlask 感兴趣的话,可以在这篇文章了解详细介绍或是在它的 GitHub 仓库查看源码。

为什么会有这样的误解?

为什么会有那么多人把 FastAPI 和 Flask 放到一起比较?在我看来有三个原因:

首先是 FastAPI 采用了和 Flask 类似的装饰器路由,很容易让人联想到 Flask。

二是 FastAPI 没有给它的项目构成做足够的说明。如果 FastAPI 在其介绍的第一句就加上「based on Starlette and Pydantic」而不是放到 Requirements 部分才提及,这样也许会让 StarlettePydantic 获得更多应有的关注,也就不会有这么多人拿 Flask 和它比较。

再就是 FastAPI 的推介者对 FastAPI 了解的不够多。不清楚它的项目结构,自然就不了解它和其他 Web 框架的区别。而介绍时为了带来足够的吸引力,推介者常常选择拉上和用户量足够大的「竞品」进行对比这种方式,所以才会有大量类似本文开头提到的那种文章。这些文章甚至完全没有提及 Starlette 和 Pydantic,只是一味强调「这是一个超高性能的、碾压 Flask 的框架」。

FastAPI 不是银弹

FastAPI 只是说它能把「功能开发速度提升约 200% 至 300%」,距离十倍还差了一些,自然不能算是银弹。但是这两年看到了很多对 FastAPI 的盲目吹捧,仿佛 FastAPI 就是完美的解决方案。这也许都要归功于 FastAPI 在其 README 和文档里「大胆」的措辞和承诺以及不厌其烦的特性介绍。举例来说,如果它只是说「Very high performance, it is comparable to some frameworks in Go and NodeJS」,那么用户也许就会认为「和 Go/NodeJS 有一拼」,但是说成「Very high performance, on par with NodeJS and Go」,那么用户就会想「和 Go/NodeJS 不相上下」(以至于中文翻译是「可与 NodeJS 和 Go 比肩的极高性能」),这当然会带来一些争议。再比如,如果只是说「double or triple the development speed」,那么用户大概会想「效率还不错」,但是它用了「Increase the speed to develop features by about 200% to 300%」,数学不好的用户就会惊呼「哇,提高 30 倍开发效率」。同时这种营销带来的狂热用户,也很容易被煽动去给 Python Steering Council 施加压力——在 Python 3.10 快要发布的时候突然跳出来要求撤销一个 PEP。

这种营销至上的项目运营方式带来的直接后果是糟糕的维护状态。FastAPI 似乎在慢慢变成一个翻译项目,在代码里的文档字符串都还没写的情况下,其用户文档就已经开始扩展到十几种语言。而这十几种语言的翻译和项目源码都放在同一个仓库里,这导致所有开启的近 300 个 PR 里有过半是翻译。issue tracker 和 discussion 不做区分的使用,这导致所有开启的近 600 个 issue 里有九成是提问)。我在此前创建过一个 issue 来反馈这个问题,但并没有得到回应。另外凭个人喜好来说,在每个 commit 信息里都加上 emoji 并不可爱。而每一个 commit 都要触发 bot 更新 changelog,带来的是一份丑陋的 commit 历史(所有 commit 里有三分之一是在更新 changelog)。这些也许是发展社区或是让项目看起来很活跃的有效方法,但很显然这带来的是建立在混乱之上的虚假繁荣。

另外,许多推介文章都会在最后贴出来一张 benchmark 截图来证明 FastAPI 有多么 fast,但是完全不会提及的是:贴出来的 benchmark 对于开发生产应用来说有多大的意义?这里的 benchmark 背后有没有任何的 hacky?异步是否等同于高性能还是要看情况?框架本身的性能在一个请求处理流程中占多大的影响?asyncio 的生态怎么样?

从长远看这些大都是一些临时问题,而且 FastAPI 作者已经开始全职开发开源项目,这些问题在未来应该都会慢慢得到改善。指出这些是希望更多的人可以客观看待 FastAPI,吹捧并不能让一个东西变得更好,参与开发、介绍用法和回答社区提问是比盲目吹捧更有意义的事情。我当然期待 FastAPI 能够越来越好,也期待看到有更多优秀的 Python 框架出现,但我不喜欢过度炒作、盲目的吹捧和错误的对比。

P.S. Flask 2.0 版本即将发布

顺便说一句,Flask、Werkzeug、Jinja 等 Pallets 项目都将在 5 月 11 日(PyCon 开始)之前发布下一个主版本,主要包含下面这些新特性(完整变动见 changelog):

  • Flask、Werkzeug 和 Click 添加了 type hints
  • Flask 实现了有限的 async/await 支持
  • Flask 支持嵌套蓝本
  • Flask 添加了快捷版本的 route 装饰器(即 @app.get@app.post 等)
  • Flask 支持在视图类上使用 route 装饰器(我前两天刚提交了一个 PR 来实现这个功能,暂时不确定是否会被合并,在这先提前说一下 :p)
  • Werkzeug 的 multipart 解析性能大幅提升

你可以使用下面的命令安装 Flask 2.0.0rc 版本进行测试:

 $ pip install --pre flask

欢迎反馈 bug 和提改进建议。关于 2.0 版本的详细介绍将会在发布后另外撰文说明。

延伸阅读

APIFlask:一个基于 Flask 的 Web API 框架

经常看到有人把 FastAPI 和 Flask 放到一起比较,但是却没有意识到这完全是两种东西——前者是基于 Web 框架 Starlette 添加了 Web API 功能支持的(框架之上的)框架,而后者是和 Starlette 同类的通用 Web 框架。你怎么能让小明和骑电动车的小军赛跑然后还夸小军好快好强?为了让框架 PK 爱好者们有一个更公平的比较对象,从一份 APIFairy 0.6.2 版本的 fork 开始,我实现了一个基于 Flask 的 Web API 框架——APIFlask

APIFlask 在 Flask 的基础上添加了更多 Web API 相关的功能支持,核心特性包括:

  • 更多方便的装饰器,比如 @app.input()@app.output()@app.get()@app.post() 等等
  • 自动反序列化和验证请求格式,当请求数据不符合模式类要求时,会自动生成包含错误详细信息的错误响应(基于 Webargs
  • 自动格式化和序列化响应数据,在定义好响应模式后,你可以直接在视图函数返回一个模型类对象,或是返回字典(基于 Marshmallow
  • 自动生成 OpenAPI Specification 文件,你可以把这个文件导入到 API 调试工具或是用来生成客户端代码(基于 APISpec
  • 自动生成交互式 API 文档,并自动为蓝本和视图设置对应的标签分类(基于 Swagger UI and Redoc
  • 自动为 HTTP 错误生成 JSON 格式的错误响应

下面是一个最基础的示例程序:

 from apiflask import APIFlask, Schema, abort
 from apiflask.fields import Integer, String
 from apiflask.validators import Length, OneOf
 ​
 app = APIFlask(__name__)  # 可以使用 title 和 version 参数来自定义 API 的名称和版本
 ​
 pets = [
     {
         'id': 0,
         'name': 'Kitty',
         'category': 'cat'
     },
     {
         'id': 1,
         'name': 'Coco',
         'category': 'dog'
     }
 ]
 ​
 # 定义一个请求数据模式类
 class PetInSchema(Schema):
     name = String(required=True, validate=Length(0, 10))  # 可以使用 description 参数添加字段描述
     category = String(required=True, validate=OneOf(['dog', 'cat']))
 ​
 # 定义一个响应数据模式类
 class PetOutSchema(Schema):
     id = Integer()
     name = String()
     category = String()
 ​
 ​
 @app.get('/pets/<int:pet_id>')
 @app.output(PetOutSchema)  # 使用 @output 装饰器标记响应数据模式
 def get_pet(pet_id):
     if pet_id > len(pets) - 1:
         abort(404)
     # 在真实程序里,你可以直接返回 ORM 模型类的实例,比如
     # return Pet.query.get(1)
     return pets[pet_id]
 ​
 ​
@app.patch('/pets/<int:pet_id>')
@app.input(PetInSchema(partial=True))  # 使用 @input 装饰器标记请求数据模式
@app.output(PetOutSchema)
def update_pet(pet_id, json_data):  # 通过验证后的请求数据字典会注入到视图函数,默认参数名为 json_data
    if pet_id > len(pets) - 1:
        abort(404)
    for attr, value in json_data.items():
        pets[pet_id][attr] = value
    return pets[pet_id]

P.S. 你也可以使用类视图(class-based views),具体示例见这里

如果你想在你的电脑上运行这个示例,可以先用下面的命令安装 APIFlask(需要 Python 3.7 及以上版本,Flask 1.1 及以上版本):

Linux 和 macOS:

 $ pip3 install apiflask

Windows:

 > pip install apiflask

安装完成后把上面的代码保存到文件 app.py,然后执行下面的命令运行程序:

 $ flask run

现在你可以在浏览器访问 http://localhost:5000/docs 查看基于 Swagger UI 自动生成的交互式 API 文档:

Swagger UI

或者访问 http://localhost:5000/redoc 查看基于 Redoc 生成的 API 文档:

Redoc

访问 http://localhost:5000/openapi.json 可以获取自动生成的 OpenAPI spec 文件。

这个示例程序的完整版本可以在 https://github.com/greyli/apiflask/tree/master/examples 看到。如果你想了解更多用法,可以阅读文档中的基本用法一章(目前只有英文)。

APIFlask 只在 Flask 之上做了轻量级包装,所以你实际上仍然是在写一个 Flask 程序,所有 Flask 的特性都完全兼容。你只需要记住下面两处不同点:

  • 创建程序实例的时候使用 APIFlask 类(from apiflask import APIFlask)。
  • 创建蓝本实例的时候使用 APIBlueprint 类(from apiflask import APIBlueprint)。
  • 使用 apiflask.abort() 函数返回 JSON 格式的错误响应。

以下面的 Flask 程序为例:

 from flask import Flask, request, escape
 ​
 app = Flask(__name__)
 ​
 @app.route('/')
 def hello():
     name = request.args.get('name', 'Human')
     return f'Hello, {escape(name)}'

迁移到 APIFlask 只需要改动两行代码:

 from apiflask import APIFlask  # 第一行
 from flask import request, escape
 ​
 app = APIFlask(__name__)  # 第二行
 ​
 @app.route('/')
 def hello():
     name = request.args.get('name', 'Human')
     return f'Hello, {escape(name)}'

欢迎提出改进建议,报告 bug 或是分享其他任何相关的想法。你也可以在 GitHub 上创建 Issue,或是提交 PR 来改进它。谢谢!