Skip to content

Commit fba17e9

Browse files
feat(client): add support for aiohttp
1 parent b68d394 commit fba17e9

File tree

10 files changed

+162
-10
lines changed

10 files changed

+162
-10
lines changed

README.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,42 @@ asyncio.run(main())
7272

7373
Functionality between the synchronous and asynchronous clients is otherwise identical.
7474

75+
### With aiohttp
76+
77+
By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
78+
79+
You can enable this by installing `aiohttp`:
80+
81+
```sh
82+
# install from PyPI
83+
pip install isaacus[aiohttp]
84+
```
85+
86+
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
87+
88+
```python
89+
import os
90+
import asyncio
91+
from isaacus import DefaultAioHttpClient
92+
from isaacus import AsyncIsaacus
93+
94+
95+
async def main() -> None:
96+
async with AsyncIsaacus(
97+
api_key=os.environ.get("ISAACUS_API_KEY"), # This is the default and can be omitted
98+
http_client=DefaultAioHttpClient(),
99+
) as client:
100+
universal_classification = await client.classifications.universal.create(
101+
model="kanon-universal-classifier",
102+
query="This is a confidentiality clause.",
103+
texts=["I agree not to tell anyone about the document."],
104+
)
105+
print(universal_classification.classifications)
106+
107+
108+
asyncio.run(main())
109+
```
110+
75111
## Using types
76112

77113
Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typing.html#typing.TypedDict). Responses are [Pydantic models](https://docs.pydantic.dev) which also provide helper methods for things like:

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ classifiers = [
3737
Homepage = "https://github.com/isaacus-dev/isaacus-python"
3838
Repository = "https://github.com/isaacus-dev/isaacus-python"
3939

40+
[project.optional-dependencies]
41+
aiohttp = ["aiohttp", "httpx_aiohttp>=0.1.6"]
4042

4143
[tool.rye]
4244
managed = true

requirements-dev.lock

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,24 @@
1010
# universal: false
1111

1212
-e file:.
13+
aiohappyeyeballs==2.6.1
14+
# via aiohttp
15+
aiohttp==3.12.8
16+
# via httpx-aiohttp
17+
# via isaacus
18+
aiosignal==1.3.2
19+
# via aiohttp
1320
annotated-types==0.6.0
1421
# via pydantic
1522
anyio==4.4.0
1623
# via httpx
1724
# via isaacus
1825
argcomplete==3.1.2
1926
# via nox
27+
async-timeout==5.0.1
28+
# via aiohttp
29+
attrs==25.3.0
30+
# via aiohttp
2031
certifi==2023.7.22
2132
# via httpcore
2233
# via httpx
@@ -34,23 +45,33 @@ execnet==2.1.1
3445
# via pytest-xdist
3546
filelock==3.12.4
3647
# via virtualenv
48+
frozenlist==1.6.2
49+
# via aiohttp
50+
# via aiosignal
3751
h11==0.14.0
3852
# via httpcore
3953
httpcore==1.0.2
4054
# via httpx
4155
httpx==0.28.1
56+
# via httpx-aiohttp
4257
# via isaacus
4358
# via respx
59+
httpx-aiohttp==0.1.6
60+
# via isaacus
4461
idna==3.4
4562
# via anyio
4663
# via httpx
64+
# via yarl
4765
importlib-metadata==7.0.0
4866
iniconfig==2.0.0
4967
# via pytest
5068
markdown-it-py==3.0.0
5169
# via rich
5270
mdurl==0.1.2
5371
# via markdown-it-py
72+
multidict==6.4.4
73+
# via aiohttp
74+
# via yarl
5475
mypy==1.14.1
5576
mypy-extensions==1.0.0
5677
# via mypy
@@ -65,6 +86,9 @@ platformdirs==3.11.0
6586
# via virtualenv
6687
pluggy==1.5.0
6788
# via pytest
89+
propcache==0.3.1
90+
# via aiohttp
91+
# via yarl
6892
pydantic==2.10.3
6993
# via isaacus
7094
pydantic-core==2.27.1
@@ -98,11 +122,14 @@ tomli==2.0.2
98122
typing-extensions==4.12.2
99123
# via anyio
100124
# via isaacus
125+
# via multidict
101126
# via mypy
102127
# via pydantic
103128
# via pydantic-core
104129
# via pyright
105130
virtualenv==20.24.5
106131
# via nox
132+
yarl==1.20.0
133+
# via aiohttp
107134
zipp==3.17.0
108135
# via importlib-metadata

requirements.lock

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,51 @@
1010
# universal: false
1111

1212
-e file:.
13+
aiohappyeyeballs==2.6.1
14+
# via aiohttp
15+
aiohttp==3.12.8
16+
# via httpx-aiohttp
17+
# via isaacus
18+
aiosignal==1.3.2
19+
# via aiohttp
1320
annotated-types==0.6.0
1421
# via pydantic
1522
anyio==4.4.0
1623
# via httpx
1724
# via isaacus
25+
async-timeout==5.0.1
26+
# via aiohttp
27+
attrs==25.3.0
28+
# via aiohttp
1829
certifi==2023.7.22
1930
# via httpcore
2031
# via httpx
2132
distro==1.8.0
2233
# via isaacus
2334
exceptiongroup==1.2.2
2435
# via anyio
36+
frozenlist==1.6.2
37+
# via aiohttp
38+
# via aiosignal
2539
h11==0.14.0
2640
# via httpcore
2741
httpcore==1.0.2
2842
# via httpx
2943
httpx==0.28.1
44+
# via httpx-aiohttp
45+
# via isaacus
46+
httpx-aiohttp==0.1.6
3047
# via isaacus
3148
idna==3.4
3249
# via anyio
3350
# via httpx
51+
# via yarl
52+
multidict==6.4.4
53+
# via aiohttp
54+
# via yarl
55+
propcache==0.3.1
56+
# via aiohttp
57+
# via yarl
3458
pydantic==2.10.3
3559
# via isaacus
3660
pydantic-core==2.27.1
@@ -41,5 +65,8 @@ sniffio==1.3.0
4165
typing-extensions==4.12.2
4266
# via anyio
4367
# via isaacus
68+
# via multidict
4469
# via pydantic
4570
# via pydantic-core
71+
yarl==1.20.0
72+
# via aiohttp

src/isaacus/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
UnprocessableEntityError,
2727
APIResponseValidationError,
2828
)
29-
from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient
29+
from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
3030
from ._utils._logs import setup_logging as _setup_logging
3131

3232
__all__ = [
@@ -68,6 +68,7 @@
6868
"DEFAULT_CONNECTION_LIMITS",
6969
"DefaultHttpxClient",
7070
"DefaultAsyncHttpxClient",
71+
"DefaultAioHttpClient",
7172
]
7273

7374
if not _t.TYPE_CHECKING:

src/isaacus/_base_client.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,24 @@ def __init__(self, **kwargs: Any) -> None:
12641264
super().__init__(**kwargs)
12651265

12661266

1267+
try:
1268+
import httpx_aiohttp
1269+
except ImportError:
1270+
1271+
class _DefaultAioHttpClient(httpx.AsyncClient):
1272+
def __init__(self, **_kwargs: Any) -> None:
1273+
raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra")
1274+
else:
1275+
1276+
class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore
1277+
def __init__(self, **kwargs: Any) -> None:
1278+
kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
1279+
kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
1280+
kwargs.setdefault("follow_redirects", True)
1281+
1282+
super().__init__(**kwargs)
1283+
1284+
12671285
if TYPE_CHECKING:
12681286
DefaultAsyncHttpxClient = httpx.AsyncClient
12691287
"""An alias to `httpx.AsyncClient` that provides the same defaults that this SDK
@@ -1272,8 +1290,12 @@ def __init__(self, **kwargs: Any) -> None:
12721290
This is useful because overriding the `http_client` with your own instance of
12731291
`httpx.AsyncClient` will result in httpx's defaults being used, not ours.
12741292
"""
1293+
1294+
DefaultAioHttpClient = httpx.AsyncClient
1295+
"""An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`."""
12751296
else:
12761297
DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient
1298+
DefaultAioHttpClient = _DefaultAioHttpClient
12771299

12781300

12791301
class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):

tests/api_resources/classifications/test_universal.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,9 @@ def test_streaming_response_create(self, client: Isaacus) -> None:
7676

7777

7878
class TestAsyncUniversal:
79-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
79+
parametrize = pytest.mark.parametrize(
80+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
81+
)
8082

8183
@pytest.mark.skip()
8284
@parametrize

tests/api_resources/extractions/test_qa.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ def test_streaming_response_create(self, client: Isaacus) -> None:
8484

8585

8686
class TestAsyncQa:
87-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
87+
parametrize = pytest.mark.parametrize(
88+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
89+
)
8890

8991
@pytest.mark.skip()
9092
@parametrize

tests/api_resources/test_rerankings.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ def test_streaming_response_create(self, client: Isaacus) -> None:
101101

102102

103103
class TestAsyncRerankings:
104-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
104+
parametrize = pytest.mark.parametrize(
105+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
106+
)
105107

106108
@pytest.mark.skip()
107109
@parametrize

tests/conftest.py

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
import logging
77
from typing import TYPE_CHECKING, Iterator, AsyncIterator
88

9+
import httpx
910
import pytest
1011
from pytest_asyncio import is_async_test
1112

12-
from isaacus import Isaacus, AsyncIsaacus
13+
from isaacus import Isaacus, AsyncIsaacus, DefaultAioHttpClient
14+
from isaacus._utils import is_dict
1315

1416
if TYPE_CHECKING:
1517
from _pytest.fixtures import FixtureRequest # pyright: ignore[reportPrivateImportUsage]
@@ -27,6 +29,19 @@ def pytest_collection_modifyitems(items: list[pytest.Function]) -> None:
2729
for async_test in pytest_asyncio_tests:
2830
async_test.add_marker(session_scope_marker, append=False)
2931

32+
# We skip tests that use both the aiohttp client and respx_mock as respx_mock
33+
# doesn't support custom transports.
34+
for item in items:
35+
if "async_client" not in item.fixturenames or "respx_mock" not in item.fixturenames:
36+
continue
37+
38+
if not hasattr(item, "callspec"):
39+
continue
40+
41+
async_client_param = item.callspec.params.get("async_client")
42+
if is_dict(async_client_param) and async_client_param.get("http_client") == "aiohttp":
43+
item.add_marker(pytest.mark.skip(reason="aiohttp client is not compatible with respx_mock"))
44+
3045

3146
base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
3247

@@ -45,9 +60,25 @@ def client(request: FixtureRequest) -> Iterator[Isaacus]:
4560

4661
@pytest.fixture(scope="session")
4762
async def async_client(request: FixtureRequest) -> AsyncIterator[AsyncIsaacus]:
48-
strict = getattr(request, "param", True)
49-
if not isinstance(strict, bool):
50-
raise TypeError(f"Unexpected fixture parameter type {type(strict)}, expected {bool}")
51-
52-
async with AsyncIsaacus(base_url=base_url, api_key=api_key, _strict_response_validation=strict) as client:
63+
param = getattr(request, "param", True)
64+
65+
# defaults
66+
strict = True
67+
http_client: None | httpx.AsyncClient = None
68+
69+
if isinstance(param, bool):
70+
strict = param
71+
elif is_dict(param):
72+
strict = param.get("strict", True)
73+
assert isinstance(strict, bool)
74+
75+
http_client_type = param.get("http_client", "httpx")
76+
if http_client_type == "aiohttp":
77+
http_client = DefaultAioHttpClient()
78+
else:
79+
raise TypeError(f"Unexpected fixture parameter type {type(param)}, expected bool or dict")
80+
81+
async with AsyncIsaacus(
82+
base_url=base_url, api_key=api_key, _strict_response_validation=strict, http_client=http_client
83+
) as client:
5384
yield client

0 commit comments

Comments
 (0)