Skip to content

Commit baa3f36

Browse files
authored
Merge branch 'development' into feat/91-streaming-downloads
2 parents 41f5089 + da14cb9 commit baa3f36

4 files changed

Lines changed: 198 additions & 1 deletion

File tree

README.md

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
- [Authentication](#authentication)
1818
- [API Keys](#api-keys)
1919
- [Bearer Token](#bearer-token)
20+
- [Lifecycle Management](#lifecycle-management)
2021
- [Deployments](#deployments)
2122
- [List Deployments](#list-deployments)
2223
- [Get Deployment by Id](#get-deployment-by-id)
@@ -121,6 +122,45 @@ async_client = AsyncDial(
121122
)
122123
```
123124

125+
### Lifecycle Management
126+
127+
For deterministic shutdown of underlying HTTP clients, both client types and
128+
client pools expose lifecycle APIs.
129+
130+
```python
131+
from aidial_client import AsyncDial, AsyncDialClientPool, Dial, DialClientPool
132+
133+
# Sync client
134+
with Dial(api_key="your_api_key", base_url="https://your-dial-instance.com") as client:
135+
...
136+
137+
client = Dial(api_key="your_api_key", base_url="https://your-dial-instance.com")
138+
client.close()
139+
140+
# Async client
141+
async with AsyncDial(
142+
api_key="your_api_key", base_url="https://your-dial-instance.com"
143+
) as async_client:
144+
...
145+
146+
async_client = AsyncDial(
147+
api_key="your_api_key", base_url="https://your-dial-instance.com"
148+
)
149+
await async_client.aclose()
150+
151+
# Sync pool
152+
with DialClientPool() as pool:
153+
pooled_client = pool.create_client(
154+
base_url="https://your-dial-instance.com", api_key="your-api-key"
155+
)
156+
157+
# Async pool
158+
async with AsyncDialClientPool() as async_pool:
159+
pooled_async_client = async_pool.create_client(
160+
base_url="https://your-dial-instance.com", api_key="your-api-key"
161+
)
162+
```
163+
124164
You can also pass `bearer_token` as a function without parameters, that returns a `string`:
125165

126166
```python
@@ -945,7 +985,7 @@ second_client = client_pool.create_client(
945985
#### Asynchronous Client Pool
946986

947987
```python
948-
from dial_client import (
988+
from aidial_client import (
949989
AsyncDialClientPool,
950990
)
951991

aidial_client/_client.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from abc import ABC, abstractmethod
22
from pathlib import PurePosixPath
3+
from types import TracebackType
34
from typing import Generic, TypeVar
45
from urllib.parse import urljoin
56

@@ -164,6 +165,20 @@ def my_appdata_home(self) -> PurePosixPath | None:
164165
def auth_headers(self) -> dict[str, str]:
165166
return self._http_client.auth_headers()
166167

168+
def close(self) -> None:
169+
self._http_client.internal_http_client.close()
170+
171+
def __enter__(self) -> "Dial":
172+
return self
173+
174+
def __exit__(
175+
self,
176+
exc_type: type[BaseException] | None,
177+
exc_value: BaseException | None,
178+
traceback: TracebackType | None,
179+
) -> None:
180+
self.close()
181+
167182

168183
class AsyncDial(BaseDialClient[AsyncHTTPClient, AsyncAuthValue]):
169184
def _init_resources(self) -> None:
@@ -253,3 +268,17 @@ async def my_appdata_home(self) -> PurePosixPath | None:
253268

254269
async def auth_headers(self) -> dict[str, str]:
255270
return await self._http_client.auth_headers()
271+
272+
async def aclose(self) -> None:
273+
await self._http_client.internal_http_client.aclose()
274+
275+
async def __aenter__(self) -> "AsyncDial":
276+
return self
277+
278+
async def __aexit__(
279+
self,
280+
exc_type: type[BaseException] | None,
281+
exc_value: BaseException | None,
282+
traceback: TracebackType | None,
283+
) -> None:
284+
await self.aclose()

aidial_client/_client_pool.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
from types import TracebackType
2+
13
import httpx
24

35
from aidial_client._auth import AsyncAuthValue, SyncAuthValue
@@ -44,6 +46,20 @@ def create_client(
4446
),
4547
)
4648

49+
def close(self) -> None:
50+
self._internal_http_client.close()
51+
52+
def __enter__(self) -> "DialClientPool":
53+
return self
54+
55+
def __exit__(
56+
self,
57+
exc_type: type[BaseException] | None,
58+
exc_value: BaseException | None,
59+
traceback: TracebackType | None,
60+
) -> None:
61+
self.close()
62+
4763

4864
class AsyncDialClientPool:
4965
def __init__(
@@ -78,3 +94,17 @@ def create_client(
7894
internal_http_client=self._internal_http_client,
7995
),
8096
)
97+
98+
async def aclose(self) -> None:
99+
await self._internal_http_client.aclose()
100+
101+
async def __aenter__(self) -> "AsyncDialClientPool":
102+
return self
103+
104+
async def __aexit__(
105+
self,
106+
exc_type: type[BaseException] | None,
107+
exc_value: BaseException | None,
108+
traceback: TracebackType | None,
109+
) -> None:
110+
await self.aclose()

tests/test_lifecycle.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
from unittest.mock import patch
2+
3+
import pytest
4+
5+
from aidial_client import AsyncDialClientPool, Dial, DialClientPool
6+
from aidial_client._client import AsyncDial
7+
8+
9+
def test_dial_close():
10+
client = Dial(api_key="dummy", base_url="http://dial.core")
11+
12+
with patch.object(
13+
client._http_client.internal_http_client, "close"
14+
) as close_mock:
15+
client.close()
16+
17+
close_mock.assert_called_once()
18+
19+
20+
def test_dial_context_manager():
21+
client = Dial(api_key="dummy", base_url="http://dial.core")
22+
23+
with (
24+
patch.object(
25+
client._http_client.internal_http_client, "close"
26+
) as close_mock,
27+
client as managed_client,
28+
):
29+
assert managed_client is client
30+
31+
close_mock.assert_called_once()
32+
33+
34+
@pytest.mark.asyncio
35+
async def test_async_dial_aclose():
36+
client = AsyncDial(api_key="dummy", base_url="http://dial.core")
37+
38+
with patch.object(
39+
client._http_client.internal_http_client, "aclose"
40+
) as aclose_mock:
41+
await client.aclose()
42+
43+
aclose_mock.assert_awaited_once()
44+
45+
46+
@pytest.mark.asyncio
47+
async def test_async_dial_context_manager():
48+
client = AsyncDial(api_key="dummy", base_url="http://dial.core")
49+
50+
with patch.object(
51+
client._http_client.internal_http_client, "aclose"
52+
) as aclose_mock:
53+
async with client as managed_client:
54+
assert managed_client is client
55+
56+
aclose_mock.assert_awaited_once()
57+
58+
59+
def test_dial_client_pool_close():
60+
pool = DialClientPool()
61+
62+
with patch.object(pool._internal_http_client, "close") as close_mock:
63+
pool.close()
64+
65+
close_mock.assert_called_once()
66+
67+
68+
def test_dial_client_pool_context_manager():
69+
pool = DialClientPool()
70+
71+
with (
72+
patch.object(pool._internal_http_client, "close") as close_mock,
73+
pool as managed_pool,
74+
):
75+
assert managed_pool is pool
76+
77+
close_mock.assert_called_once()
78+
79+
80+
@pytest.mark.asyncio
81+
async def test_async_dial_client_pool_aclose():
82+
pool = AsyncDialClientPool()
83+
84+
with patch.object(pool._internal_http_client, "aclose") as aclose_mock:
85+
await pool.aclose()
86+
87+
aclose_mock.assert_awaited_once()
88+
89+
90+
@pytest.mark.asyncio
91+
async def test_async_dial_client_pool_context_manager():
92+
pool = AsyncDialClientPool()
93+
94+
with patch.object(pool._internal_http_client, "aclose") as aclose_mock:
95+
async with pool as managed_pool:
96+
assert managed_pool is pool
97+
98+
aclose_mock.assert_awaited_once()

0 commit comments

Comments
 (0)