用Python编写的HTTP API的类型:Instagram体验

今天,我们将发布该系列的第二篇材料,专门介绍如何在Instagram中使用Python。 一次它正在检查Instagram服务器代码的类型。 服务器是用Python编写的整体。 它由几百万行代码组成,并具有数千个Django端点。



本文是关于Instagram如何在使用HTTP API时使用类型来记录HTTP API和执行合同的。

情况概述


当您打开Instagram移动客户端时,它通过HTTP访问我们的Python(Django)服务器的JSON-API。

这是有关我们系统的一些信息,使您可以了解用于组织移动客户端工作的API的复杂性。 所以这就是我们所拥有的:

  • 服务器上超过2000个端点。
  • 客户端数据对象中的200多个顶级字段,它们代表应用程序中的图像,视频或故事。
  • 数以百计的编写服务器代码的程序员(甚至还有更多处理客户端的程序员)。
  • 每天对服务器代码进行数百次提交并修改API。 这是为新系统功能提供支持所必需的。

我们使用类型来记录我们复杂且不断发展的HTTP API,并在使用它们时强制执行合同。

种类


让我们从头开始。 PEP 484中提供了Python代码中类型注释的语法描述。 为什么要在代码中添加类型注释?

考虑一下下载有关《星球大战》英雄信息的功能:

def get_character(id, calendar):     if id == 1000:         return Character(             id=1000,             name="Luke Skywalker",             birth_year="19BBY" if calendar == Calendar.BBY else ...         )     ... 

为了理解此功能,您需要阅读其代码。 完成此操作后,您可以找到以下内容:

  • 它使用字符的整数标识符( id )。
  • 它从相应的枚举( calendar )中获取值。 例如, Calendar.BBY代表“亚文战役之前”,即“亚文战役之前”。
  • 它以实体的形式返回有关该字符的信息,该实体包含代表该字符的标识符,其名称和出生年份的字段。

该函数具有隐式协定,程序员在每次读取函数代码时都必须恢复该隐式协定。 但是功能代码只编写一次,而且您必须阅读多次,因此使用这种代码的方法并不是特别好。

此外,很难验证调用函数的机制是否遵守上述隐式协定。 同样,很难验证该合同是否在功能主体中得到遵守。 在大型代码库中,此类情况可能导致错误。

现在考虑声明类型注释的同一函数:

 def get_character(id: int, calendar: Calendar) -> Character:    ... 

类型批注允许您显式表达此函数的约定。 为了了解需要向函数输入什么以及该函数返回什么,只需阅读其签名即可。 类型检查系统可以静态分析功能并验证代码中是否符合合同规定。 这样您就可以摆脱一整类错误!

各种HTTP API的类型


我们将开发一个HTTP-API,使您可以接收有关《星球大战》英雄的信息。 为了描述使用此API时使用的显式协定,我们将使用类型注释。

我们的API应该接受字符标识符( id )作为URL参数,并接受calendar枚举的值作为request参数。 API应该返回带有字符信息的JSON响应。

以下是API请求的外观及其返回的响应:

 curl -X GET https://api.starwars.com/characters/1000?calendar=BBY {    "id": 1000,    "name": "Luke Skywalker",    "birth_year": "19BBY" } 

要在Django中实现此API,您首先需要注册URL路径和负责接收沿该路径发出的HTTP请求并返回响应的view函数。

 urlpatterns = [    url("characters/<id>/", get_character) ] 

该函数作为输入接受请求和URL参数(在我们的示例中为id )。 它将calendar请求参数(来自相应枚举的值)解析并转换为所需的类型。 它从商店加载字符数据,并返回以JSON序列化并包装在HTTP响应中的字典。

 def get_character(request: IGWSGIRequest, id: str) -> JsonResponse:    calendar = Calendar(request.GET.get("calendar", "BBY"))    character = Store.get_character(id, calendar)    return JsonResponse(asdict(character)) 

尽管该函数提供了类型注释,但并未明确描述HTTP API的硬性约定。 从该函数的签名中,我们无法找到请求参数的名称或类型,或响应字段及其类型。

是否可以使功能表示的签名与以前考虑过的带有类型注释的功能的签名完全相同?

 def get_character(id: int, calendar: Calendar) -> Character:    ... 

函数参数可以是查询参数(URL,查询或查询主体参数)。 函数返回的值的类型可以表示响应的内容。 通过这种方法,我们将可以使用HTTP API的明确而易懂的合同,可以通过类型检查系统确保对它的遵守。

实作


如何实现这个想法?

我们使用装饰器将强类型的表示形式函数转换为Django表示形式函数。 此步骤不需要更改使用Django框架的情况。 我们可以使用相同的中间件,相同的路由和其他组件。

 @api_view def get_character(id: int, calendar: Calendar) -> Character:    ... 

考虑一下api_view装饰器api_view的细节:

 def api_view(view):    @functools.wraps(view)    def django_view(request, *args, **kwargs):        params = {            param_name: param.annotation(extract(request, param))            for param_name, param in inspect.signature(view).parameters.items()        }        data = view(**params)        return JsonResponse(asdict(data))       return django_view 

这是一段很难理解的代码。 让我们分析一下它的功能。
我们将一个强类型表示函数作为输入值,并将其包装在常规Django表示函数中,然后返回:

 def api_view(view):    @functools.wraps(view)    def django_view(request, *args, **kwargs):        ...    return django_view 

现在看一下Django视图功能的实现。 首先,我们需要为强类型表示函数构造参数。 我们使用自省和检查模块来获取此函数的签名并对其参数进行迭代:

 for param_name, param in inspect.signature(view).parameters.items() 

对于每个参数,我们调用extract函数,该函数从请求中提取参数值。

然后,将参数强制转换为签名中指定的期望类型(例如,将字符串Calendar强制转换为Calendar枚举的元素的值)。

 param.annotation(extract(request, param)) 

我们使用构造的参数调用强类型视图函数:

 data = view(**params) 

该函数返回Character类的强类型值。 我们使用此值,将其转换为字典,并将其包装为JSON格式的HTTP响应:

 return JsonResponse(asdict(data)) 

太好了! 现在,我们有了一个Django视图函数,该函数包装了一个强类型的视图函数。 最后,看一下extract函数:

 def extract(request: HttpRequest, param: Parameter) -> Any:    if request.resolver_match.route.contains(f"<{param}>"):        return request.resolver_match.kwargs.get(param.name)    else:        return request.GET.get(param.name) 

每个参数可以是URL参数或请求参数。 请求URL路径(我们一开始就注册的路径)在Django URL定位器系统的route对象中可用。 我们在路径中检查参数名称。 如果有名称,那么我们有一个URL参数。 这意味着我们可以以某种方式从请求中提取它。 否则,这是一个查询参数,我们也可以提取它,但是可以采用其他方式。

仅此而已。 这是一个简化的实现,但是它说明了键入API的基本思想。

资料类型


用于表示HTTP响应内容的类型(即Character )可以由数据类或类型化的字典表示。

数据是表示数据的紧凑类描述格式。

 from dataclasses import dataclass @dataclass(frozen=True) class Character:    id: int    name: str    birth_year: str luke = Character(    id=1000,    name="Luke Skywalker",    birth_year="19BBY" ) 

Instagram通常使用数据类来建模HTTP响应对象。 它们的主要功能如下:

  • 他们自动生成模板结构和各种辅助方法。
  • 它们对于类型检查系统是可以理解的,这意味着可以对值进行类型检查。
  • 由于frozen=True结构,它们保持了免疫力。
  • 它们在Python标准库3.7中可用,或者在Python包索引中作为反向端口提供。

不幸的是,Instagram具有过时的代码库,该库使用在功能和模块之间传递的大型无类型字典。 将所有这些代码从字典转换为数据类并不容易。 结果,我们将数据类用于新代码,而在过时的代码中,我们使用类型化字典

使用类型字典可以使我们将类型注释添加到客户端字典对象,并且在不更改工作系统行为的情况下,可以使用类型检查功能。

 from mypy_extensions import TypedDict class Character(TypedDict):    id: int    name: str    birth_year: str luke: Character = {"id": 1000} luke["name"] = "Luke Skywalker" luke["birth_year"] = 19 # type error, birth_year expects a str luke["invalid_key"] # type error, invalid_key does not exist 

错误处理


期望视图函数以Character实体的形式返回字符信息。 如果我们需要将错误返回给客户该怎么办?

您可以引发框架捕获的异常,并将其转换为带有错误信息的HTTP响应。

 @api_view("GET") def get_character(id: str, calendar: Calendar) -> Character:    try:        return Store.get_character(id)    except CharacterNotFound:        raise Http404Exception() 

此示例还演示了装饰器中的HTTP方法,该方法设置了此API允许的HTTP方法。

工具


使用HTTP方法,请求类型和响应类型对HTTP API进行强类型化。 我们可以内省该API,并确定它应该接受GET请求,该请求的URL路径中的id字符串,以及与查询字符串中的相应枚举相关的calendar值。 我们还可以了解到,响应这样的请求,应该给JSON响应提供有关Character的性质的信息。

所有这些信息可以做什么?

OpenAPI是一种API描述格式,在此格式下可以创建丰富的辅助工具集。 这是一个完整的生态系统。 如果我们编写一些代码来执行端点自省并根据接收到的数据生成OpenAPI规范,则这意味着我们将拥有这些工具的功能。

 paths:  /characters/{id}:    get:      parameters:        - in: path          name: id          schema:            type: integer          required: true        - in: query          name: calendar          schema:            type: string            enum: ["BBY"]      responses:        '200':          content:            application/json:              schema:                type: object                ... 

我们可以为get_character API生成HTTP API文档,其中包括名称,类型,请求和响应信息。 对于需要满足对适当端点的请求的客户端开发人员而言,这是适当的抽象级别。 他们不需要读取此端点的Python实现代码。


API文档

在此基础上,您可以创建其他工具。 例如,一种执行来自浏览器的请求的方法。 这使开发人员无需编写代码即可访问他们感兴趣的HTTP API。 我们甚至可以生成类型安全的客户端代码,以确保类型在客户端和服务器上都能正常工作。 因此,我们可以在服务器上使用严格类型的API,使用严格类型的客户端代码执行对其的调用。

另外,我们可以创建一个向后兼容性检查系统。 如果我们发布服务器代码的新版本以访问相关API,我们需要使用idnamebirth_year ,然后我们知道我们不知道所有字符的生日,会发生什么? 在这种情况下, birth_year参数birth_year需要设置为可选,但是期望类似参数的旧版本客户端可能只是停止工作。 尽管我们的API在显式键入方面有所不同,但相应的类型可以更改(例如,如果使用字符的出生年份是强制性的,然后变为可选的,则API将会更改)。 我们可以跟踪API更改,并通过在适当的时候向他们发出提示的方式来警告API开发人员,这些提示通过进行某些更改可能会破坏客户端的性能。

总结


计算机可以使用多种应用程序协议来相互通信。

频谱框架的一方面由Thrift和gRPC等RPC框架表示。 它们的不同之处在于,它们通常为请求和响应设置严格的类型,并生成用于组织请求操作的客户端和服务器代码。 他们可以不用HTTP,甚至不用JSON。

另一方面,存在一些用Python编写的非结构化Web框架,这些框架没有明确的请求和响应协定。 我们的方法为结构更清晰的框架提供了典型的机会,但同时又允许您继续使用HTTP + JSON捆绑包,并有助于您必须对应用程序代码进行最少的更改。

重要的是要注意,这个想法并不新鲜。 有许多以强类型语言编写的框架为开发人员提供了我们描述的功能。 如果我们谈论Python,那么这就是APIStar框架。

我们已成功委托HTTP API使用类型。 由于该方法非常适用于现有的表示功能,因此我们能够将所描述的方法用于在整个代码库中键入API。 我们所做的工作的价值对我们所有的程序员都是显而易见的。 即,我们正在谈论这样一个事实,即自动生成的文档已成为与服务器开发人员和编写Instagram客户端的人员进行通信的有效手段。

亲爱的读者们! 您如何在Python项目中进行HTTP API的设计?


Source: https://habr.com/ru/post/zh-CN474308/


All Articles