REST思维且通用的HTTP Python客户端,具有异步和同步接口
项目描述
通用的同步(requests)和异步(aiohttp)操作的HTTP客户端。
“Noah”在希伯来语中意为“方便”。
目前我只支持Python 3.8+。如果您需要支持早期版本,请提交问题。
动机
如果您曾经使用Python接口过REST API,它可能开始如下
class PetSanctuaryClient:
def __init__(self):
self.session = requests.Session()
def get(self, url):
res = self.session.get(url)
res.raise_for_status()
return res.json()
从这个点开始,它显然会迅速变得复杂… .jsoin()返回字典或列表,但通常您至少想以某种方式验证它,或者更好的是使用像Pydantic这样的专用工具。继续上面的假设示例
from pydantic import BaseModel, ValidationError
from typing import List
class Pet(BaseModel):
name: str
class Pets(BaseModel):
__root__ = List[Pet]
class PetSanctuaryClient:
...
def list_pets(self) -> Pets:
pets_info = self.get(...)
try:
return Pets.parse_obj(pets_info)
except ValidationError:
logger.info("Failed to parse pets_info", pets_info=pets_info) # hooray structlog
当然,上述内容必须被适当地分解,您最终会得到以下类签名
class PetSanctuaryClient:
def list_pets(...)
def get_pet(...)
def delete_pet(...)
def assign_pet_to_carer(...)
def list_carers(...)
def get_carer(...)
...
如果您的目标API不仅仅是平凡的,您会很快陷入一个方法纠缠的混乱。当然,命名约定有助于解决这个问题,但它很快就会变成一个怪物般的类。如果我们能把这个单体结构分解成单独的子API,然后在它们各自的类中实现,然后将它们层次化地插入主API,那会怎么样?我认为以下内容更容易理解
psc = PetSanctuaryClient(...)
psc.pets.get(..)
psc.pets.list(...)
psc.cares.list(...)
...
我希望这能给您一些关于为什么这个项目产生的想法。把asyncio和许多角落案例,比如构建URL、在调用.raise_for_status()时释放aiohttp连接等,都考虑在内。
当我开始使用FastAPI作为后端服务,并且已经有了可以重用客户端的Pydantic模型时,这一切才开始变得有意义。
安装
安装有同步和异步两种版本,以确保只拉取相关的依赖项(例如,你可能不想在同步应用程序中使用aiohttp)。
同步版本
pip install --upgrade http-noah[sync]
异步版本
pip install --upgrade http-noah[async]
要安装同步和异步版本,请使用all扩展规范而不是sync / async。
使用方法
基本示例
让我们从一个基本示例开始。假设我们的宠物庇护所API正在运行在http://localhost:8080/api/v1
from pydantic import BaseModel
from http_noah.sync_client import SyncHTTPClient
class Pet(BaseModel):
name: str
def main():
with SyncHTTPClient("localhost", 8080) as client:
pet: Pet = client.get("/pets/1", response_type=Pet)
让我们更仔细地看看这里发生了什么
我们只提供了host和port,api_base默认为/api/v1,这样我们就不必在每个URL调用前添加它
我们要求http_noah将API响应转换为期望类型的实例(否则抛出异常)
我们使用上下文管理器来确保一切都会及时清理。在更复杂的代码中,你可能考虑一种生命周期管理器,例如我在我的演示Hanuka项目中使用的那种(源代码)
异步示例与此类似
from http_noah.async_client import AsyncHTTPClient
async def main():
async with AsyncHTTPClient("localhost", 8080) as client:
pet: Pet = await client.get("/pets/1", response_type=Pet)
由于这个库的目的是为同步和异步代码提供类似接口,所以我将专注于异步示例,并在存在差异的情况下留下注释,我会努力将这些差异减少到很少。
客户端支持以下方法,这些方法映射对应的HTTP动词
.get(...)
.post(...)
.put(...)
.delete(...)
发送数据同样简单 - 不论是字典还是Pydantic模型。
对于Pydantic模型,您可以直接将它们传递给例如.post()的body参数
async def create_pet():
async with AsyncHTTPClient("localhost", 8080) as client:
pet = Pet(name="Crispy")
await client.post("/pets", body=pet, response_type=Pet)
如果您只想发送JSON数据,则需要明确指出
from http_noah.common import JSONData
async def create_pet():
async with AsyncHTTPClient("localhost", 8080) as client:
pet = {"name": "Crispy"}
await client.post("/pets", body=JSONData(data=pet), response_type=Pet)
这对于http_noah了解您的意图是要发送JSON还是表单(两者都可以是Python字典)是必要的。有关表单和文件上传的更多信息,请参阅下面的专用部分。
再次,我更喜欢用Pydantic模型来表示我发送和接收的所有内容 - 这使得生活变得如此简单,以至于您会很快上瘾。
嵌套客户端
现在我们已经了解了基本用法,让我们看看如何构建我一开始承诺的美丽嵌套客户端。
让我们从根类开始构建我们的假设宠物庇护所API客户端
from __future__ import annotations
from http_noah.async_client import AsyncAPIClientBase, AsyncHTTPClient
class PetSanctuaryClient(AsyncAPIClientBase):
@classmethod
def new(cls, host: str, port: int, scheme: str = "https") -> PetSanctuaryClient:
client = AsyncHTTPClient(host=host, port=port, scheme=scheme)
return cls(client=client)
目前,这只是一个样板类,除了有一个构建函数外,没有任何惊人的功能。注意,我使用的是AsyncAPIClientBase而不是AsyncHTTPClient。
现在让我们实现宠物子API
from __future__ import annotations
from dataclasses import dataclass
from http_noah.async_client import AsyncAPIClientBase, AsyncHTTPClient
# Skipped model definitions here - as in the basic example
@dataclass
class PetClient:
client: AsyncHTTPClient
class paths:
prefix: str = "/pets"
list: str = prefix
get: str = prefix + "/{id}"
create: str = prefix
async def list(self) -> Pets:
return await self.client.get(self.paths.list, response_type=Pets)
async def get(self, id: int) -> Pet:
return await self.client.get(self.paths.get.format(id=id), response_type=Pet)
async def create(self, pet: Pet) -> Pet:
return await self.client.post(self.paths.create, body=Pet, response_type=Pet)
@dataclass
class PetSanctuaryClient(AsyncAPIClientBase):
pets: PetClient
@classmethod
def new(cls, host: str, port: int, scheme: str = "https") -> PetSanctuaryClient:
client = AsyncHTTPClient(host=host, port=port, scheme=scheme)
pet_client = PetClient(client)
return cls(client=client, pets=pet_client)
现在我们开始吧!让我们享受这个过程
psc = PetSanctuaryClient("localhost", 8080, scheme="http")
async with psc:
pets = await psc.pets.list()
pet = await psc.pets.get(1)
同样,我们可以实现其他子API客户端,并轻松地将它们嵌套。
认真对待
响应类型
指定响应类型是强制性的,除非您预计您的请求会以HTTP 204“无内容”响应,这通常适用于DELETE操作。
如果响应内容类型标题设置为application/json,则将为您解码JSON数据,并可以使用您选择的Pydantic模型进一步解析。
否则,您可以请求返回str或bytes。
这导致了一个限制,即使用这个库,你不能将JSON响应作为字符串检索。但是,由于这是一个高级REST客户端,我在实践中还没有遇到过这个限制。
总结一下,这里是您为 response_type 参数的可选方案
bytes 当请求返回二进制数据时,例如图片
str 当请求返回文本时(从技术上讲,“当内容类型不是 application/json” 时)
dict,list,int,bool,float,str(即任何 JSON -> Python 原生类型),当您的请求返回JSON数据,并且您不想进一步将其解析为 Pydantic 对象时。
错误处理
在尝试同步和异步代码之间保持一致时,我在 http_noah.sync_client 和 async_client 中将常见的错误基类别名设置为常见的名称 ConnectionError,HTTPError 和 TimeoutError。但这只是个开始——在这些名称背后,如果你要深入研究,这些仍然是 requests / aiohttp 错误类。
http_noah为您做的另一件有用的事情是确保在发生错误时记录HTTP体。这通常是帮助您了解情况的小而至关重要的信息。遗憾的是,挖掘这些信息需要相当多的调整。举例来说,调用aiohttp响应对象的 raise_for_status() 方法实际上会将底层的HTTP连接返回到池中,让您无法读取错误体。
再次强调,当http_noah遇到HTTP错误时,它会记录HTTP(错误)体。
超时
可以通过将 http_noah.common.Timeout 类的实例传递给 .get(),put() 等方法或通过 ClientOptions 为每个客户端实例设置来配置超时。
from http_noah.common import ClientOptions, Timeout
from http_noah.async_client import AsyncHTTPClient
options = ClientOptions(Timeout(total=10)
async with AsyncHTTPClient(host="localhost", port=80, options=options) as client:
await client.get(...) # Limited to 10 seconds
await client.post(..., timeout=Timeout(total=20)) # per call override
然而,如果您回顾一下之前建议的嵌套客户端方法,您会很快注意到在所有高级方法中重新定义 timeout 参数是非常繁琐的。幸运的是,http_noah名副其实,借助同步和异步客户端都实现的 timeout 上下文管理器提供了一个简单的解决方案。
继续我们的 PetSanctuaryClient 示例
from http_noah.common import Timeout
async with PetSanctuaryClient("localhost", 8080, scheme="http") as psc:
pets = await psc.pets.list()
with psc.client.timeout(Timeout(total=1):
pet = await psc.pets.get(1) # Limited to 1 second
如您所见,PetClient 和 PetSanctuaryClient 都没有定义任何超时逻辑,但我们完全可以应用超时。
表单
表单现在不太使用了。然而,当需要登录API以获取Bearer令牌时,我仍然会遇到它们。
要使用http_noah中的表单,只需将其填充为 dict,就像使用 aiohttp / requests 一样,并通过包装在 FormData 中的 body 参数传递即可。
from typing import Literal
from pydantic import BaseModel
from http_noah.common import FormData
class TokenResponse(BaseModel):
access_token: str
token_type: Literal["bearer"]
async def get_access_token():
login_form = FormData(data={
"grant_type": "password",
"username": "foo",
"password": "secret",
})
async with AsyncHTTPClient("localhost", 8080) as client:
tr = await client.post("/access_token", body=login_form, response_type=TokenResponse)
文件
http-noah提供了一种简单的方法来上传文件作为多部分编码的表单。以下是一个示例
from pathlib import Path
from http_noah.common import UploadFile
async with AsyncHTTPClient("localhost", 8080) as client:
await client.post(
"/pets/1/photo",
body=UploadFile(name="thumbnail", path=Path("myphoto.jpg"),
)
SSL
SSL/TLS 支持如 requests 和 aiohttp 中的实现。然而,有时可能需要禁用 SSL 验证,例如在开发环境中。这可以通过 ClientOptions 实现。
from http_noah.common import ClientOptions
from http_noah.async_client import AsyncHTTPClient
options = ClientOptions(ssl_verify_cert=False)
async with AsyncHTTPClient(host="localhost", port=80, options=options) as client:
...
身份验证
http-noah 支持基本和 Bearer 令牌客户端身份验证。这些可以在现有客户端上随时设置。
async with AsyncHTTPClient("localhost", 8080) as client:
# Bearer token
client.set_auth_token("my-secret-token")
# Or Basic Auth
client.set_auth_basic("my-username", "my-password")
这是一个故意的工程设计决定,从构造函数中省略身份验证参数,因为在例如 Bearer 令牌的情况下,身份验证信息可能事先未知,因为可能需要首先提交登录表单。因此,需要在后期设置身份验证信息。
开发
要开发 http_noah,您需要安装 Python 3.8+、pipenv 和 direnv。
然后,在克隆存储库后,只需运行 make bootstrap,等待片刻,您就完成了 - 下次进入克隆的目录时,环境将为您设置。
从代码的角度来看,您不可能有既支持同步又支持异步的相同代码。至少在可读性方面不行。由于可读性很重要,而简单胜过复杂,我宁愿有两个非常简单的代码版本,每个版本分别实现同步和异步,而不是一个回调污染、基于迭代器或充满黑魔法的代码库。
对每个库功能都进行了功能测试。
祝您享受并期待您的 PR!
项目详情
http-noah-0.2.1.tar.gz 的散列
算法 | 散列摘要 | |
---|---|---|
SHA256 | 02c8b0c99b670ad1b426a4cba02ada9eea22678531d52fdbeedcd476ee543e12 |
|
MD5 | 6e06e112268f7b5c734bb4a41467ede4 |
|
BLAKE2b-256 | e49c7add64a8f5bb8540fd2ec515a0f16941da8f7bafdab221e64c0beed449a4 |