首页 | 新闻 | 新品 | 文库 | 方案 | 视频 | 下载 | 商城 | 开发板 | 数据中心 | 座谈新版 | 培训 | 工具 | 博客 | 论坛 | 百科 | GEC | 活动 | 主题月 | 电子展
返回列表 回复 发帖

Python API 类型系统的设计与演变(3)OpenAPI 与可解释性

Python API 类型系统的设计与演变(3)OpenAPI 与可解释性

OpenAPI                与可解释性对于在线服务的描述和定义,本文比较倾向于参考 OpenAPI 规范,原因是它对机器更加友好,有着严谨的 Spec                定义,有利于生成和分析,同时背后有谷歌、微软等商业公司和强大的社区支持。
相对于 、  等规范所强调的人类可读性(Human Readable),                     更加注重定义的规范化和通用性,鼓励社区共同推进规范的演进,在本文写作时,  已经发布,一个欣欣向荣的社区也是影响本文选型的关键因素。
类型系统在这里的作用是,对在线服务的接口定义进行描述,并生成一个符合 OpenAPI 规范定义的 JSON 文档,以支持文档生成工具(比如  )、前端 Mock 工具(比如国内的  )、接口测试工具(比如下文提到的基于                  的实现)和前端验证库的需要。
在 OpenAPI 规范中,与类型系统相关的部分主要集中在 paths、schema、data types 三个章节,本文主要实现 data                types 章节中所描述的类型与   类型之间的映射,这里举几个特殊的例子。
表                    1 OAS Data Type 与 Marshmallow Type 的映射OAS TypeOAS FormatMarshmallow描述 string  email  Email  电子邮件  string  uuid  UUID  UUID  integer  enum  Enum(Int)  上文中定义的枚举类型  string
List(Str)  字符串列表
在                  的定义里,每一个类型(type)都有一个可选的格式(format)可以定义,通常是根据业务所需来定制,这里取 fields 类的类名(小写)作为                format 值。
这里有一个特例,对于容器类型,比如 Enum 和                List,它们的类型取决于它所包装的类型,对于在线服务,常常需要类型系统具有确定性,是不允许 Union 类型存在的,这样设计主要是为了减少序列化                / 反序列化的成本,同时简化代码的分支逻辑。
这里举例说明容器类型的类型定义是如何翻译成                  的类型定义的:
清单                5. List(Int) 翻译为 OpenAPI/OAS 示例
1
2
3
4
5
6
{
  "type": "array",
  "items": {
    "type": "integer"
  }
}




清单                6. Enum(Int) 翻译为 OpenAPI/OAS 示例
1
2
3
4
5
6
7
8
9
{
  "schema": {
    "type": "integer",
    "enum": [
      400
      404
    ]
  }
}




接口测试与文档生成在完成了上述基础的工作之后,就要与测试框架进行集成了。
类型系统与测试框架集成的意义是什么呢?可以分两个类别来看待:
  • 第一个类别是需要严格限定接口响应字段的类型,这个时候开发人员会在代码中对接口的响应做类型声明,那么在测试用例中,类型系统的作用自然就是对响应字段类型的校验了,本文称之为严格模式。
  • 第二个类别是接口响应无类型声明,那么接口的响应定义就不再具备可解释性,而可解释性对自动化的文档生成是最重要的因素。本文所描述的在线业务处于这样一个阶段,所以在类型系统实现中主要解决的就是这个问题。
如果没有响应参数的类型定义,就需要推导响应的类型,类型推导的方式有两种,静态的和动态的(运行时)。
静态分析在 Python 2                中的实现难度比较高,因为大量的第三方库都没有明确的类型信息,同时许多要经过网络的上下游服务也都没有提供严格的定义,难以在这样复杂的环境中通过静态分析拿到接口响应类型信息。
由于团队有写接口测试的习惯,最终选择了在运行接口测试的时候,和 Python 的测试框架 py.test                集成,通过收集接口测试的返回值来做运行时的类型推导。
下面尽可能简单地描述一下一个真实的实现,本文使用                  来做用例的定义,比如:
清单 7. 使用                Yaml 描述的测试用例示例
1
2
3
4
5
6
7
8
- uri: /echo
  method: GET
  desc: 测试 ECHO 服务
  status: 200
  params:
    ping: "pong"
  responses:
    ping: "pong"




Pytest 提供了参数化的功能可以用来生成用例,apis 是用例定义的列表:
清单 8.                描述文件与 pytest 集成的示例
1
2
3
4
5
@pytest.mark.parametrize("case", apis)
def test_api(case, case_manager, mocker):
    case_obj = case_manager.add(case)
    case_obj.run(mocker)
    print(case_obj.real_response)




用例执行后,用例的响应被保存下来,再尝试对每一个响应字段的值做一个简单的类型推导。
清单 9.                一种响应值类型推导的实现示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
pattern_inferer_map = {
    date_pattern: {'type': 'string', 'format': 'date'},
    datetime_pattern: {'type': 'string', 'format': 'date-time'},
    ip_pattern: {'type': 'string', 'format': 'ip'},
    uuid_pattern: {'type': 'string', 'format': 'uuid'},
    base64_pattern: {'type': 'string', 'format': 'byte'},
    // ...
}

def infer_value(value):
    if isinstance(value, string_types):
        for pattern, type_info in pattern_inferer_map.items():
            if pattern.match(value):
                return type_info
        return {'type': 'string'}
    elif isinstance(value, int):
        return {'type': 'number', 'format': 'int64'}
    elif isinstance(value, float):
        return {'type': 'number', 'format': 'double'}
    elif isinstance(value, bool):
        return {'type': 'boolean'}


def inferer_response(response):
    return {k: infer_value(v) for k, v in response.items()}




暴力地对 Python 类型和 OAS 的类型做一个映射,这样就用最简单的办法完成了一个接口响应的类型推断。
很容易看出,这样的类型推断会存在许多问题,比如 int 和 float 类型的精度无法表达,字符串类型的 format                可能会有误判,尤其依赖完备的测试用例等等。
但本文为什么仍然愿意推荐这种方法,因为它可以使用最小的成本,最大限度地满足研发人员的基本诉求——拿到接口相应的基本类型信息,提升可解释性,这是类型系统中非常重要的一部分。
小结就这样,本文通过重载服务框架的路由装饰器来收集 API 的参数类型信息,通过接口测试来收集 API                的响应类型信息,通过注册自定义的枚举类型和业务类型,再配合框架本身的属性,就可以生成定制化的、符合 OpenAPI 规范的文档了。
拥有类型系统的在线服务,在接口校验、异常处理、测试和文档生成等方面都有全方位的提升,满足了工程师们对一个服务在安全性和可解释性上的基本诉求,这是非常值得投入的一件事。
返回列表