Skip to content

Commit 7c4c123

Browse files
authored
Merge pull request #235 from GetStream/feat/configurable-http-transport
[FEEDS-1373]feat: allow custom HTTP transport and client configuration
2 parents bc76d35 + 1ba6d9d commit 7c4c123

3 files changed

Lines changed: 394 additions & 64 deletions

File tree

getstream/base.py

Lines changed: 78 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,38 @@ def __init__(
158158
timeout=timeout,
159159
user_agent=user_agent,
160160
)
161-
self.client = httpx.Client(
162-
base_url=self.base_url or "",
163-
headers=self.headers,
164-
params=self.params,
165-
timeout=httpx.Timeout(self.timeout),
166-
)
161+
http_client = getattr(self, "_http_client", None)
162+
if http_client is not None:
163+
if not isinstance(http_client, httpx.Client):
164+
raise TypeError(
165+
f"http_client must be an httpx.Client instance, "
166+
f"got {type(http_client).__name__}"
167+
)
168+
http_client.headers.update(self.headers)
169+
http_client.params = http_client.params.merge(self.params)
170+
http_client.base_url = self.base_url or ""
171+
if self.timeout is not None:
172+
http_client.timeout = httpx.Timeout(self.timeout)
173+
self.client = http_client
174+
self._owns_http_client = False
175+
else:
176+
transport = getattr(self, "_transport", None)
177+
if transport is not None:
178+
self.client = httpx.Client(
179+
base_url=self.base_url or "",
180+
headers=self.headers,
181+
params=self.params,
182+
timeout=httpx.Timeout(self.timeout),
183+
transport=transport,
184+
)
185+
else:
186+
self.client = httpx.Client(
187+
base_url=self.base_url or "",
188+
headers=self.headers,
189+
params=self.params,
190+
timeout=httpx.Timeout(self.timeout),
191+
)
192+
self._owns_http_client = True
167193

168194
def __enter__(self):
169195
return self
@@ -348,8 +374,13 @@ def _upload_multipart(
348374
def close(self):
349375
"""
350376
Close HTTPX client.
377+
378+
If the client was provided externally via ``http_client``, this is a
379+
no-op — the caller that created the client is responsible for closing
380+
it.
351381
"""
352-
self.client.close()
382+
if getattr(self, "_owns_http_client", True):
383+
self.client.close()
353384

354385

355386
class AsyncBaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, ABC):
@@ -368,12 +399,38 @@ def __init__(
368399
timeout=timeout,
369400
user_agent=user_agent,
370401
)
371-
self.client = httpx.AsyncClient(
372-
base_url=self.base_url or "",
373-
headers=self.headers,
374-
params=self.params,
375-
timeout=httpx.Timeout(self.timeout),
376-
)
402+
http_client = getattr(self, "_http_client", None)
403+
if http_client is not None:
404+
if not isinstance(http_client, httpx.AsyncClient):
405+
raise TypeError(
406+
f"http_client must be an httpx.AsyncClient instance, "
407+
f"got {type(http_client).__name__}"
408+
)
409+
http_client.headers.update(self.headers)
410+
http_client.params = http_client.params.merge(self.params)
411+
http_client.base_url = self.base_url or ""
412+
if self.timeout is not None:
413+
http_client.timeout = httpx.Timeout(self.timeout)
414+
self.client = http_client
415+
self._owns_http_client = False
416+
else:
417+
transport = getattr(self, "_transport", None)
418+
if transport is not None:
419+
self.client = httpx.AsyncClient(
420+
base_url=self.base_url or "",
421+
headers=self.headers,
422+
params=self.params,
423+
timeout=httpx.Timeout(self.timeout),
424+
transport=transport,
425+
)
426+
else:
427+
self.client = httpx.AsyncClient(
428+
base_url=self.base_url or "",
429+
headers=self.headers,
430+
params=self.params,
431+
timeout=httpx.Timeout(self.timeout),
432+
)
433+
self._owns_http_client = True
377434

378435
async def __aenter__(self):
379436
return self
@@ -382,8 +439,14 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
382439
await self.aclose()
383440

384441
async def aclose(self):
385-
"""Close HTTPX async client (closes pools/keep-alives)."""
386-
await self.client.aclose()
442+
"""Close HTTPX async client (closes pools/keep-alives).
443+
444+
If the client was provided externally via ``http_client``, this is a
445+
no-op — the caller that created the client is responsible for closing
446+
it.
447+
"""
448+
if getattr(self, "_owns_http_client", True):
449+
await self.client.aclose()
387450

388451
async def _upload_multipart(
389452
self,

getstream/stream.py

Lines changed: 88 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from typing import List, Optional
77
from uuid import uuid4
88

9+
import httpx
910
import jwt
1011
from pydantic_settings import BaseSettings, SettingsConfigDict
1112

@@ -47,7 +48,12 @@ def __init__(
4748
timeout: Optional[float] = 6.0,
4849
base_url: Optional[str] = BASE_URL,
4950
user_agent: Optional[str] = None,
51+
transport=None,
52+
http_client=None,
5053
):
54+
if transport is not None and http_client is not None:
55+
raise ValueError("Cannot specify both 'transport' and 'http_client'")
56+
5157
if None in (api_key, api_secret, timeout, base_url):
5258
s = Settings() # loads from env and optional .env
5359
api_key = api_key or s.api_key
@@ -68,10 +74,29 @@ def __init__(
6874

6975
self.base_url = validate_and_clean_url(base_url)
7076
self.user_agent = user_agent
77+
self._transport = transport
78+
self._http_client = http_client
7179
self.token = self._create_token()
7280
super().__init__(
7381
self.api_key, self.base_url, self.token, self.timeout, self.user_agent
7482
)
83+
# After super().__init__(), self.client is fully built and configured.
84+
# When the user provided custom HTTP config, sub-clients share this
85+
# client instead of each building their own.
86+
if transport is not None or http_client is not None:
87+
self._shared_client = self.client
88+
else:
89+
self._shared_client = None
90+
91+
def _apply_shared_client(self, sub_client):
92+
"""Replace a sub-client's auto-created httpx client with the shared
93+
one built from user-provided transport/http_client config."""
94+
if self._shared_client is not None:
95+
if isinstance(sub_client.client, httpx.Client):
96+
sub_client.client.close()
97+
sub_client.client = self._shared_client
98+
sub_client._owns_http_client = False
99+
return sub_client
75100

76101
def create_token(
77102
self,
@@ -169,13 +194,15 @@ def video(self) -> AsyncVideoClient:
169194
Video stream client.
170195
171196
"""
172-
return AsyncVideoClient(
173-
api_key=self.api_key,
174-
base_url=self.base_url,
175-
token=self.token,
176-
timeout=self.timeout,
177-
stream=self,
178-
user_agent=self.user_agent,
197+
return self._apply_shared_client(
198+
AsyncVideoClient(
199+
api_key=self.api_key,
200+
base_url=self.base_url,
201+
token=self.token,
202+
timeout=self.timeout,
203+
stream=self,
204+
user_agent=self.user_agent,
205+
)
179206
)
180207

181208
@cached_property
@@ -184,13 +211,15 @@ def chat(self) -> AsyncChatClient:
184211
Chat stream client.
185212
186213
"""
187-
return AsyncChatClient(
188-
api_key=self.api_key,
189-
base_url=self.base_url,
190-
token=self.token,
191-
timeout=self.timeout,
192-
stream=self,
193-
user_agent=self.user_agent,
214+
return self._apply_shared_client(
215+
AsyncChatClient(
216+
api_key=self.api_key,
217+
base_url=self.base_url,
218+
token=self.token,
219+
timeout=self.timeout,
220+
stream=self,
221+
user_agent=self.user_agent,
222+
)
194223
)
195224

196225
@cached_property
@@ -199,13 +228,15 @@ def moderation(self) -> AsyncModerationClient:
199228
Moderation stream client.
200229
201230
"""
202-
return AsyncModerationClient(
203-
api_key=self.api_key,
204-
base_url=self.base_url,
205-
token=self.token,
206-
timeout=self.timeout,
207-
stream=self,
208-
user_agent=self.user_agent,
231+
return self._apply_shared_client(
232+
AsyncModerationClient(
233+
api_key=self.api_key,
234+
base_url=self.base_url,
235+
token=self.token,
236+
timeout=self.timeout,
237+
stream=self,
238+
user_agent=self.user_agent,
239+
)
209240
)
210241

211242
async def aclose(self):
@@ -291,13 +322,15 @@ def video(self) -> VideoClient:
291322
Video stream client.
292323
293324
"""
294-
return VideoClient(
295-
api_key=self.api_key,
296-
base_url=self.base_url,
297-
token=self.token,
298-
timeout=self.timeout,
299-
stream=self,
300-
user_agent=self.user_agent,
325+
return self._apply_shared_client(
326+
VideoClient(
327+
api_key=self.api_key,
328+
base_url=self.base_url,
329+
token=self.token,
330+
timeout=self.timeout,
331+
stream=self,
332+
user_agent=self.user_agent,
333+
)
301334
)
302335

303336
@cached_property
@@ -306,13 +339,15 @@ def chat(self) -> ChatClient:
306339
Chat stream client.
307340
308341
"""
309-
return ChatClient(
310-
api_key=self.api_key,
311-
base_url=self.base_url,
312-
token=self.token,
313-
timeout=self.timeout,
314-
stream=self,
315-
user_agent=self.user_agent,
342+
return self._apply_shared_client(
343+
ChatClient(
344+
api_key=self.api_key,
345+
base_url=self.base_url,
346+
token=self.token,
347+
timeout=self.timeout,
348+
stream=self,
349+
user_agent=self.user_agent,
350+
)
316351
)
317352

318353
@cached_property
@@ -321,13 +356,15 @@ def moderation(self) -> ModerationClient:
321356
Moderation stream client.
322357
323358
"""
324-
return ModerationClient(
325-
api_key=self.api_key,
326-
base_url=self.base_url,
327-
token=self.token,
328-
timeout=self.timeout,
329-
stream=self,
330-
user_agent=self.user_agent,
359+
return self._apply_shared_client(
360+
ModerationClient(
361+
api_key=self.api_key,
362+
base_url=self.base_url,
363+
token=self.token,
364+
timeout=self.timeout,
365+
stream=self,
366+
user_agent=self.user_agent,
367+
)
331368
)
332369

333370
@cached_property
@@ -336,13 +373,15 @@ def feeds(self) -> FeedsClient:
336373
Feeds stream client.
337374
338375
"""
339-
return FeedsClient(
340-
api_key=self.api_key,
341-
base_url=self.base_url,
342-
token=self.token,
343-
timeout=self.timeout,
344-
stream=self,
345-
user_agent=self.user_agent,
376+
return self._apply_shared_client(
377+
FeedsClient(
378+
api_key=self.api_key,
379+
base_url=self.base_url,
380+
token=self.token,
381+
timeout=self.timeout,
382+
stream=self,
383+
user_agent=self.user_agent,
384+
)
346385
)
347386

348387
@telemetry.operation_name("getstream.api.common.create_user")

0 commit comments

Comments
 (0)