跳转到主要内容

REST思维且通用的HTTP Python客户端,具有异步和同步接口

项目描述

https://img.shields.io/pypi/v/http-noah.svg https://img.shields.io/travis/haizaar/http-noah.svg https://img.shields.io/pypi/dm/http-noah.svg

通用的同步(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)

让我们更仔细地看看这里发生了什么

  • 我们只提供了hostportapi_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模型进一步解析。

  • 否则,您可以请求返回strbytes

这导致了一个限制,即使用这个库,你不能将JSON响应作为字符串检索。但是,由于这是一个高级REST客户端,我在实践中还没有遇到过这个限制。

总结一下,这里是您为 response_type 参数的可选方案

  • bytes 当请求返回二进制数据时,例如图片

  • str 当请求返回文本时(从技术上讲,“当内容类型不是 application/json” 时)

  • dictlistintboolfloatstr(即任何 JSON -> Python 原生类型),当您的请求返回JSON数据,并且您不想进一步将其解析为 Pydantic 对象时。

错误处理

在尝试同步和异步代码之间保持一致时,我在 http_noah.sync_clientasync_client 中将常见的错误基类别名设置为常见的名称 ConnectionErrorHTTPErrorTimeoutError。但这只是个开始——在这些名称背后,如果你要深入研究,这些仍然是 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

如您所见,PetClientPetSanctuaryClient 都没有定义任何超时逻辑,但我们完全可以应用超时。

表单

表单现在不太使用了。然而,当需要登录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 (31.4 kB 查看散列)

上传时间

由以下机构支持

AWS AWS 云计算和安全赞助商 Datadog Datadog 监控 Fastly Fastly CDN Google Google 下载分析 Microsoft Microsoft PSF 赞助商 Pingdom Pingdom 监控 Sentry Sentry 错误日志 StatusPage StatusPage 状态页面