-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Support async httpx client (#113)
- Loading branch information
Showing
12 changed files
with
236 additions
and
24 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
# Copyright 2025 The Meatie Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. | ||
from typing import Any, Optional | ||
|
||
import httpx | ||
from meatie import ( | ||
BaseAsyncClient, | ||
Cache, | ||
MeatieError, | ||
ProxyError, | ||
RequestError, | ||
ServerError, | ||
Timeout, | ||
TransportError, | ||
) | ||
from meatie.types import Request | ||
from typing_extensions import Self | ||
|
||
from .async_response import AsyncResponse | ||
from .client import build_kwargs | ||
|
||
|
||
class AsyncClient(BaseAsyncClient): | ||
def __init__( | ||
self, | ||
client: httpx.AsyncClient, | ||
client_params: Optional[dict[str, Any]] = None, | ||
local_cache: Optional[Cache] = None, | ||
limiter: Optional[Any] = None, | ||
prefix: Optional[str] = None, | ||
) -> None: | ||
super().__init__(local_cache, limiter) | ||
|
||
self.client = client | ||
self.client_params = client_params if client_params else {} | ||
self.prefix = prefix | ||
|
||
async def send(self, request: Request) -> AsyncResponse: | ||
kwargs = build_kwargs(request, self.client_params) | ||
|
||
path = request.path | ||
if self.prefix is not None: | ||
path = self.prefix + path | ||
|
||
try: | ||
response = await self.client.request(request.method, path, **kwargs) | ||
except (httpx.InvalidURL, httpx.UnsupportedProtocol) as exc: | ||
raise RequestError(exc) from exc | ||
except httpx.ProxyError as exc: | ||
raise ProxyError(exc) from exc | ||
except httpx.TimeoutException as exc: | ||
raise Timeout(exc) from exc | ||
except (httpx.NetworkError, httpx.RemoteProtocolError) as exc: | ||
raise ServerError(exc) from exc | ||
except (httpx.TooManyRedirects, httpx.ProtocolError) as exc: | ||
raise TransportError(exc) from exc | ||
except httpx.HTTPError as exc: | ||
raise MeatieError(exc) from exc | ||
return AsyncResponse(response) | ||
|
||
async def __aenter__(self) -> Self: | ||
return self | ||
|
||
async def __aexit__( | ||
self, | ||
exc_type: Optional[type[BaseException]], | ||
exc_val: Optional[BaseException], | ||
exc_tb: Any, | ||
) -> None: | ||
await self.close() | ||
|
||
async def close(self) -> None: | ||
await self.client.aclose() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
# Copyright 2025 The Meatie Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. | ||
from json.decoder import JSONDecodeError | ||
from typing import Any, Awaitable, Callable, Optional | ||
|
||
import httpx | ||
from meatie.error import ParseResponseError, ResponseError | ||
|
||
|
||
class AsyncResponse: | ||
def __init__( | ||
self, | ||
response: httpx.Response, | ||
get_json: Optional[Callable[[httpx.Response], Awaitable[Any]]] = None, | ||
get_text: Optional[Callable[[httpx.Response], Awaitable[str]]] = None, | ||
) -> None: | ||
self.response = response | ||
if get_json is not None: | ||
self.get_json = get_json # type: ignore[assignment] | ||
if get_text is not None: | ||
self.get_text = get_text # type: ignore[assignment] | ||
|
||
@property | ||
def status(self) -> int: | ||
return self.response.status_code | ||
|
||
async def read(self) -> bytes: | ||
try: | ||
return self.response.content | ||
except Exception as exc: | ||
raise ResponseError(self, exc) from exc | ||
|
||
async def text(self) -> str: | ||
try: | ||
return await self.get_text(self.response) | ||
except Exception as exc: | ||
raise ResponseError(self, exc) from exc | ||
|
||
async def json(self) -> dict[str, Any]: | ||
try: | ||
return await self.get_json(self.response) | ||
except JSONDecodeError as exc: | ||
text = await self.text() | ||
raise ParseResponseError(text, self, exc) from exc | ||
except Exception as exc: | ||
raise ResponseError(self, exc) from exc | ||
|
||
@classmethod | ||
async def get_json(cls, response: httpx.Response) -> Any: | ||
return response.json() | ||
|
||
@classmethod | ||
async def get_text(cls, response: httpx.Response) -> str: | ||
return response.text |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
# Copyright 2024 The Meatie Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. | ||
import asyncio | ||
from typing import Generator | ||
|
||
import httpx | ||
import pytest | ||
from http_test import ClientAdapter | ||
from meatie_httpx import AsyncClient | ||
from suite.client import DefaultSuite | ||
|
||
|
||
class TestAsyncHttpxDefaultSuite(DefaultSuite): | ||
@pytest.fixture(name="client") | ||
def client_fixture( | ||
self, | ||
event_loop: asyncio.AbstractEventLoop, | ||
) -> Generator[ClientAdapter, None, None]: | ||
with ClientAdapter(event_loop, AsyncClient(httpx.AsyncClient())) as client: | ||
yield client |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
# Copyright 2024 The Meatie Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. | ||
|
||
import asyncio | ||
from typing import Generator | ||
|
||
import httpx | ||
import pytest | ||
from http_test import ClientAdapter, HTTPTestServer | ||
from meatie import Request, ServerError | ||
from meatie_httpx import AsyncClient, Client | ||
|
||
|
||
class TestAsyncHttpxProxyErrorSuite: | ||
@pytest.fixture(name="client") | ||
def client_fixture( | ||
self, | ||
event_loop: asyncio.AbstractEventLoop, | ||
) -> Generator[ClientAdapter, None, None]: | ||
with ClientAdapter( | ||
event_loop, AsyncClient(httpx.AsyncClient(proxy="http://localhost:3128")) | ||
) as client: | ||
yield client | ||
|
||
@staticmethod | ||
def test_can_handle_proxy_error(client: Client, http_server: HTTPTestServer) -> None: | ||
# GIVEN | ||
request = Request("GET", http_server.base_url, params={}, headers={}) | ||
|
||
# WHEN | ||
with pytest.raises(ServerError) as exc_info: | ||
client.send(request) | ||
|
||
# THEN | ||
assert exc_info.value.__cause__ is not None |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
# Copyright 2024 The Meatie Authors. All rights reserved. | ||
# Use of this source code is governed by a BSD-style license that can be found in the LICENSE file. | ||
import asyncio | ||
from typing import Generator | ||
|
||
import httpx | ||
import pytest | ||
from http_test import ClientAdapter | ||
from meatie_httpx import AsyncClient | ||
from suite.client import TimeoutSuite | ||
|
||
|
||
class TestHttpxTimeoutSuite(TimeoutSuite): | ||
@pytest.fixture(name="client") | ||
def client_fixture( | ||
self, | ||
event_loop: asyncio.AbstractEventLoop, | ||
) -> Generator[ClientAdapter, None, None]: | ||
with ClientAdapter( | ||
event_loop, AsyncClient(httpx.AsyncClient(), client_params={"timeout": 0.005}) | ||
) as client: | ||
yield client |