Skip to content

Commit d54f53c

Browse files
feat(client): add support for aiohttp
1 parent 3517a3d commit d54f53c

23 files changed

+199
-23
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,40 @@ asyncio.run(main())
6666

6767
Functionality between the synchronous and asynchronous clients is otherwise identical.
6868

69+
### With aiohttp
70+
71+
By default, the async client uses `httpx` for HTTP requests. However, for improved concurrency performance you may also use `aiohttp` as the HTTP backend.
72+
73+
You can enable this by installing `aiohttp`:
74+
75+
```sh
76+
# install from PyPI
77+
pip install contextual-client[aiohttp]
78+
```
79+
80+
Then you can enable it by instantiating the client with `http_client=DefaultAioHttpClient()`:
81+
82+
```python
83+
import os
84+
import asyncio
85+
from contextual import DefaultAioHttpClient
86+
from contextual import AsyncContextualAI
87+
88+
89+
async def main() -> None:
90+
async with AsyncContextualAI(
91+
api_key=os.environ.get("CONTEXTUAL_API_KEY"), # This is the default and can be omitted
92+
http_client=DefaultAioHttpClient(),
93+
) as client:
94+
create_agent_output = await client.agents.create(
95+
name="Example",
96+
)
97+
print(create_agent_output.id)
98+
99+
100+
asyncio.run(main())
101+
```
102+
69103
## Using types
70104

71105
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/ContextualAI/contextual-client-python"
3838
Repository = "https://github.com/ContextualAI/contextual-client-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 contextual-client
17+
# via httpx-aiohttp
18+
aiosignal==1.3.2
19+
# via aiohttp
1320
annotated-types==0.6.0
1421
# via pydantic
1522
anyio==4.4.0
1623
# via contextual-client
1724
# via httpx
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
4256
# via contextual-client
57+
# via httpx-aiohttp
4358
# via respx
59+
httpx-aiohttp==0.1.6
60+
# via contextual-client
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 contextual-client
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 contextual-client
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 contextual-client
17+
# via httpx-aiohttp
18+
aiosignal==1.3.2
19+
# via aiohttp
1320
annotated-types==0.6.0
1421
# via pydantic
1522
anyio==4.4.0
1623
# via contextual-client
1724
# via httpx
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 contextual-client
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
3044
# via contextual-client
45+
# via httpx-aiohttp
46+
httpx-aiohttp==0.1.6
47+
# via contextual-client
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 contextual-client
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 contextual-client
68+
# via multidict
4469
# via pydantic
4570
# via pydantic-core
71+
yarl==1.20.0
72+
# via aiohttp

src/contextual/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
UnprocessableEntityError,
3737
APIResponseValidationError,
3838
)
39-
from ._base_client import DefaultHttpxClient, DefaultAsyncHttpxClient
39+
from ._base_client import DefaultHttpxClient, DefaultAioHttpClient, DefaultAsyncHttpxClient
4040
from ._utils._logs import setup_logging as _setup_logging
4141

4242
__all__ = [
@@ -78,6 +78,7 @@
7878
"DEFAULT_CONNECTION_LIMITS",
7979
"DefaultHttpxClient",
8080
"DefaultAsyncHttpxClient",
81+
"DefaultAioHttpClient",
8182
]
8283

8384
if not _t.TYPE_CHECKING:

src/contextual/_base_client.py

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

12911291

1292+
try:
1293+
import httpx_aiohttp
1294+
except ImportError:
1295+
1296+
class _DefaultAioHttpClient(httpx.AsyncClient):
1297+
def __init__(self, **_kwargs: Any) -> None:
1298+
raise RuntimeError("To use the aiohttp client you must have installed the package with the `aiohttp` extra")
1299+
else:
1300+
1301+
class _DefaultAioHttpClient(httpx_aiohttp.HttpxAiohttpClient): # type: ignore
1302+
def __init__(self, **kwargs: Any) -> None:
1303+
kwargs.setdefault("timeout", DEFAULT_TIMEOUT)
1304+
kwargs.setdefault("limits", DEFAULT_CONNECTION_LIMITS)
1305+
kwargs.setdefault("follow_redirects", True)
1306+
1307+
super().__init__(**kwargs)
1308+
1309+
12921310
if TYPE_CHECKING:
12931311
DefaultAsyncHttpxClient = httpx.AsyncClient
12941312
"""An alias to `httpx.AsyncClient` that provides the same defaults that this SDK
@@ -1297,8 +1315,12 @@ def __init__(self, **kwargs: Any) -> None:
12971315
This is useful because overriding the `http_client` with your own instance of
12981316
`httpx.AsyncClient` will result in httpx's defaults being used, not ours.
12991317
"""
1318+
1319+
DefaultAioHttpClient = httpx.AsyncClient
1320+
"""An alias to `httpx.AsyncClient` that changes the default HTTP transport to `aiohttp`."""
13001321
else:
13011322
DefaultAsyncHttpxClient = _DefaultAsyncHttpxClient
1323+
DefaultAioHttpClient = _DefaultAioHttpClient
13021324

13031325

13041326
class AsyncHttpxClientWrapper(DefaultAsyncHttpxClient):

tests/api_resources/agents/datasets/test_evaluate.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,9 @@ def test_path_params_metadata(self, client: ContextualAI) -> None:
369369

370370

371371
class TestAsyncEvaluate:
372-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
372+
parametrize = pytest.mark.parametrize(
373+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
374+
)
373375

374376
@parametrize
375377
async def test_method_create(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/agents/datasets/test_tune.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,7 +369,9 @@ def test_path_params_metadata(self, client: ContextualAI) -> None:
369369

370370

371371
class TestAsyncTune:
372-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
372+
parametrize = pytest.mark.parametrize(
373+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
374+
)
373375

374376
@parametrize
375377
async def test_method_create(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/agents/evaluate/test_jobs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ def test_path_params_metadata(self, client: ContextualAI) -> None:
153153

154154

155155
class TestAsyncJobs:
156-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
156+
parametrize = pytest.mark.parametrize(
157+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
158+
)
157159

158160
@parametrize
159161
async def test_method_list(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/agents/test_evaluate.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,9 @@ def test_path_params_create(self, client: ContextualAI) -> None:
7474

7575

7676
class TestAsyncEvaluate:
77-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
77+
parametrize = pytest.mark.parametrize(
78+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
79+
)
7880

7981
@parametrize
8082
async def test_method_create(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/agents/test_query.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -279,7 +279,9 @@ def test_path_params_retrieval_info(self, client: ContextualAI) -> None:
279279

280280

281281
class TestAsyncQuery:
282-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
282+
parametrize = pytest.mark.parametrize(
283+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
284+
)
283285

284286
@parametrize
285287
async def test_method_create(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/agents/test_tune.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,9 @@ def test_path_params_create(self, client: ContextualAI) -> None:
7777

7878

7979
class TestAsyncTune:
80-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
80+
parametrize = pytest.mark.parametrize(
81+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
82+
)
8183

8284
@parametrize
8385
async def test_method_create(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/agents/tune/test_jobs.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ def test_path_params_metadata(self, client: ContextualAI) -> None:
153153

154154

155155
class TestAsyncJobs:
156-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
156+
parametrize = pytest.mark.parametrize(
157+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
158+
)
157159

158160
@parametrize
159161
async def test_method_list(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/agents/tune/test_models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ def test_path_params_list(self, client: ContextualAI) -> None:
5757

5858

5959
class TestAsyncModels:
60-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
60+
parametrize = pytest.mark.parametrize(
61+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
62+
)
6163

6264
@parametrize
6365
async def test_method_list(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/datastores/test_documents.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,9 @@ def test_path_params_set_metadata(self, client: ContextualAI) -> None:
278278

279279

280280
class TestAsyncDocuments:
281-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
281+
parametrize = pytest.mark.parametrize(
282+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
283+
)
282284

283285
@parametrize
284286
async def test_method_list(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/test_agents.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -320,7 +320,9 @@ def test_path_params_reset(self, client: ContextualAI) -> None:
320320

321321

322322
class TestAsyncAgents:
323-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
323+
parametrize = pytest.mark.parametrize(
324+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
325+
)
324326

325327
@parametrize
326328
async def test_method_create(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/test_datastores.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,9 @@ def test_path_params_reset(self, client: ContextualAI) -> None:
203203

204204

205205
class TestAsyncDatastores:
206-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
206+
parametrize = pytest.mark.parametrize(
207+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
208+
)
207209

208210
@parametrize
209211
async def test_method_create(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/test_generate.py

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

9191

9292
class TestAsyncGenerate:
93-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
93+
parametrize = pytest.mark.parametrize(
94+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
95+
)
9496

9597
@parametrize
9698
async def test_method_create(self, async_client: AsyncContextualAI) -> None:

tests/api_resources/test_lmunit.py

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

5757

5858
class TestAsyncLMUnit:
59-
parametrize = pytest.mark.parametrize("async_client", [False, True], indirect=True, ids=["loose", "strict"])
59+
parametrize = pytest.mark.parametrize(
60+
"async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
61+
)
6062

6163
@parametrize
6264
async def test_method_create(self, async_client: AsyncContextualAI) -> None:

0 commit comments

Comments
 (0)