在 Python 中调用 API 的现代方法
HTTP 库
响应数据验证
创建 API 客户端
调用 API 的好方法
尽管我最初对此持怀疑态度,但现在我坚信 Python 中的异步将塑造这门语言的未来。Python 的生态系统越来越发达,许多优秀的人才正在加入异步的行列,帮助 Python 开发者掌握这种新的编码方式。当然,编写异步代码比同步代码更复杂,但在过去几年中,很多事情发生了变化,入门门槛比以往任何时候都低。
在 Strio,我们主要使用 FastAPI 作为前端 API。它是一个异步 Web 框架,让我们能够轻松实现良好的性能。在前端、其他客户端或直接来自客户的请求期间,我们通常需要调用许多其他方:外部 API、AMQP 代理、SQL 或 NoSQL 数据库……我们的 API 充当着各种服务之间的粘合剂,由于所有这些调用都相当长,我们可以利用异步代码来为客户保持卓越的性能。
在我们调用的服务中,有很多 HTTP 请求需要发送。在当今的微服务时代,这通常符合您对前端 API 的期望:当需要执行一些相当复杂的操作时,只需调用负责该操作的服务并让它执行即可。因此,让我们尝试构建一种与这些 HTTP API 进行异步通信的现代方式。
HTTP 库
我们需要的第一个模块是 HTTP 库。为什么要使用 HTTP 库?HTTP 是一个标准,你无法真正避免它,但是这个协议有时很难实现,而且参考资料中充满了技巧。幸运的是,有些人为 Python 编写了一些很棒的 HTTP 库,我们可以使用它。
我真正喜欢的是httpx,它是由encode 团队创建的,你可以在其中找到一些最著名的 Python 开发者。httpx本质上是请求,但增加了异步支持、类型等等。它的简单语法灵感来源于请求,这使得它非常易于使用和理解,尤其对于那些刚接触异步代码并需要阅读它的人来说。
让我们看一下如何GET
异步地发出简单请求:
>>> import httpx
>>> async with httpx.AsyncClient() as client:
... r = await client.get('https://ifconfig.co/json')
...
>>> r
<Response [200 OK]>
这是 Asyncio 解释器,启动时python -m asyncio
我介绍的这个砖块的目标是进行 HTTP 调用并以类型化和验证的模型返回响应。90% 的时间我们需要访问需要验证的响应的 JSON 主体。
响应数据验证
下一个关键点是 HTTPX 对响应主体的数据进行验证。为什么需要验证?这不仅有助于开发人员构建响应的类型,这对于自动补全和提高开发人员的工作效率大有裨益,而且还能在进一步操作之前验证数据。如果 API 返回了意外结果,我们希望立即返回一个友好的异常,而不是AttributeError
在请求的后期才返回。
为了完成这项工作,我选择了pydantic,这是一个非常棒的库,可以简化数据的检查和验证。让我们看看如何将它集成到我们的请求管道中:
>>> from pydantic import BaseModel
>>> from ipaddress import IPv4Address
>>> class IfConfig(BaseModel):
... ip: IPv4Address
... ip_decimal: int
...
>>> ifconfig = IfConfig.parse_obj(r.json())
>>> ifconfig
IfConfig(ip=IPv4Address('78.153.21.75'), ip_decimal=1807729179)
现在我们已经为响应数据建立了静态类型,我们知道ifconfig.ip
存在并且是IPv4Address
。然而,数据也经过了验证,因此,如果ip
响应中缺少该字段,或者我们尝试解析 IPv6 等类型的数据,我们就会抛出异常ValidationError
,然后我们就可以针对这种异常事件采取措施。
创建 API 客户端
为了将这两块砖粘合在一起,我们可以将它们集成到一个名为 API 客户端的类中。
这个类的作用是抽象出代码库不同部分的 API 引用逻辑,这样开发者就可以忽略底层,只关注他们想要获取或修改的资源。这个类还允许我们将外部 API 的定义分离到一个地方,以便于测试和重构。
让我们创建我们的IfConfigClient
代码,以便我们代码中的所有内容,从我们自己的 API 到我们的后台作业都可以轻松查询此服务:
from ipaddress import IPv4Address
from pydantic import BaseModel
from httpx import AsyncClient
IFCONFIG_URL = "https://ifconfig.co/"
class IfConfig(BaseModel):
ip: IPv4Address
ip_decimal: int
class IfConfigClient(AsyncClient):
def __init__(self):
super().__init__(base_url=IFCONFIG_URL)
async def get_ifconfig(self):
request = await self.get('json')
try:
ifconfig = IfConfig.parse_obj(request.json())
except ValidationError:
print("Something went wrong!")
return ifconfig
这里的技巧是让我们的客户端继承自AsyncClient
类httpx
。这使得开发和使用都变得非常简单。我们将这段代码保存在一个ifconfig.py
文件中,以便我们可以在python -m asyncio
解释器中导入它:
>>> from ifconfig import IfConfigClient
>>> async with IfConfigClient() as client:
... ifconfig = await client.get_ifconfig()
...
>>> ifconfig
IfConfig(ip=IPv4Address('78.153.21.75'), ip_decimal=1807729179)
调用 API 的好方法
我们构建了一个相当现代化的 API 客户端:它是异步的,支持输入并验证数据。此外,出于多种原因,我希望未来人们也能以这种方式编写 API 包装器。
关注点分离
通过这种配置,API 对用户完全不透明,可以单独进行正确测试。如果 API 本身发生变化,例如某个端点新增了参数,则可以全局更改,而不会破坏当前使用此端点的所有代码。我们在 Python 生态系统中已经实践了相当长一段时间,但现在创建客户端变得更加简单。
模拟测试
我非常喜欢对复杂函数进行单元测试,但是拦截代码中所有发出的数据,比如 HTTP 调用,通常会很麻烦。多亏了我们的代理,我们有一种标准的方法来修补测试,甚至可以通过输入以下内容来创建 Fixture:
from unittest.mock import patch
import ifconfig
mock_ip = IPv4Address('78.153.21.75')
mock_ip_decimal = 1807729179
mock_ifconfig = ifconfig.IfConfig(ip=mock_ip, ip_decimal=mock_ip_decimal)
@patch.object(ifconfig.IfConfigClient, "get_ifconfig", return_value=mock_ifconfig)
def test_ifconfig_processing(get_ifconfig):
...
assert get_ifconfig.assert_awaited_once()
...
随处打字
客户端现已集成类型功能:您无需再费力翻阅 JSON 和字典来获取数据。所有可从请求响应中访问的字段,均可在您常用的 IDE 中自动补全,并包含其类型。与mypy或pyright结合使用,它还允许您在测试和提交之前执行静态类型检查。
数据验证
现在,API 客户端可以自行处理 API 响应中出现的异常情况(例如某个字段消失),而不是在其他人编写的代码中访问该字段时出现问题。这对于测试至关重要,而且在 API 发生变更时,也有利于代码的可维护性。
我很高兴看到像httpx和pydantic这样的新工具在 Python 领域涌现,我认为它们让构建简洁的代码变得更容易,并为复杂的代码库强制执行良好的标准。这种编写外部请求的新方式将会推广。就连 Elasticsearch 的 Python 维护人员在讨论其 Python 客户端的未来时也表示赞同。
鏂囩珷鏉ユ簮锛�https://dev.to/fuegoio/the-modern-way-to-call-apis-in-python-2emh