Skip to content

Commit 87378dd

Browse files
committed
feat: allow custom HTTP transport and client configuration
Add `transport` and `http_client` parameters to `Stream` and `AsyncStream` so users can configure connection pool limits, retries, SSL, HTTP/2, and other httpx transport-level settings — matching the Java SDK pattern where a pre-built OkHttpClient can be passed in. - `transport` (primary): user provides an `httpx.HTTPTransport` / `httpx.AsyncHTTPTransport`; the SDK builds its own client with it and manages the lifecycle. - `http_client` (escape hatch): user provides a fully built `httpx.Client` / `httpx.AsyncClient`; the SDK layers auth headers and params on top. Caller manages lifecycle. When either is provided, all sub-clients (video, chat, moderation, feeds) share a single underlying httpx client via the `stream` back-reference instead of each creating their own. Made-with: Cursor
1 parent bc76d35 commit 87378dd

10 files changed

Lines changed: 336 additions & 22 deletions

File tree

getstream/base.py

Lines changed: 87 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -158,12 +158,44 @@ 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 = self._resolve_http_client()
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+
client_kwargs = dict(
177+
base_url=self.base_url or "",
178+
headers=self.headers,
179+
params=self.params,
180+
timeout=httpx.Timeout(self.timeout),
181+
)
182+
transport = getattr(self, "_transport", None)
183+
if transport is not None:
184+
client_kwargs["transport"] = transport
185+
self.client = httpx.Client(**client_kwargs)
186+
self._owns_http_client = True
187+
188+
def _resolve_http_client(self):
189+
"""Return a pre-built httpx client if one was provided, checking both
190+
the instance attribute (set by BaseStream) and the parent stream
191+
back-reference (set by sub-clients like VideoClient)."""
192+
client = getattr(self, "_http_client", None)
193+
if client is not None:
194+
return client
195+
stream = getattr(self, "stream", None)
196+
if stream is not None:
197+
return getattr(stream, "_shared_client", None)
198+
return None
167199

168200
def __enter__(self):
169201
return self
@@ -348,8 +380,13 @@ def _upload_multipart(
348380
def close(self):
349381
"""
350382
Close HTTPX client.
383+
384+
If the client was provided externally via ``http_client``, this is a
385+
no-op — the caller that created the client is responsible for closing
386+
it.
351387
"""
352-
self.client.close()
388+
if getattr(self, "_owns_http_client", True):
389+
self.client.close()
353390

354391

355392
class AsyncBaseClient(TelemetryEndpointMixin, BaseConfig, ResponseParserMixin, ABC):
@@ -368,12 +405,41 @@ def __init__(
368405
timeout=timeout,
369406
user_agent=user_agent,
370407
)
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-
)
408+
http_client = self._resolve_http_client()
409+
if http_client is not None:
410+
if not isinstance(http_client, httpx.AsyncClient):
411+
raise TypeError(
412+
f"http_client must be an httpx.AsyncClient instance, "
413+
f"got {type(http_client).__name__}"
414+
)
415+
http_client.headers.update(self.headers)
416+
http_client.params = http_client.params.merge(self.params)
417+
http_client.base_url = self.base_url or ""
418+
if self.timeout is not None:
419+
http_client.timeout = httpx.Timeout(self.timeout)
420+
self.client = http_client
421+
self._owns_http_client = False
422+
else:
423+
client_kwargs = dict(
424+
base_url=self.base_url or "",
425+
headers=self.headers,
426+
params=self.params,
427+
timeout=httpx.Timeout(self.timeout),
428+
)
429+
transport = getattr(self, "_transport", None)
430+
if transport is not None:
431+
client_kwargs["transport"] = transport
432+
self.client = httpx.AsyncClient(**client_kwargs)
433+
self._owns_http_client = True
434+
435+
def _resolve_http_client(self):
436+
client = getattr(self, "_http_client", None)
437+
if client is not None:
438+
return client
439+
stream = getattr(self, "stream", None)
440+
if stream is not None:
441+
return getattr(stream, "_shared_client", None)
442+
return None
377443

378444
async def __aenter__(self):
379445
return self
@@ -382,8 +448,14 @@ async def __aexit__(self, exc_type, exc_val, exc_tb):
382448
await self.aclose()
383449

384450
async def aclose(self):
385-
"""Close HTTPX async client (closes pools/keep-alives)."""
386-
await self.client.aclose()
451+
"""Close HTTPX async client (closes pools/keep-alives).
452+
453+
If the client was provided externally via ``http_client``, this is a
454+
no-op — the caller that created the client is responsible for closing
455+
it.
456+
"""
457+
if getattr(self, "_owns_http_client", True):
458+
await self.client.aclose()
387459

388460
async def _upload_multipart(
389461
self,

getstream/chat/async_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515

1616
class ChatClient(ChatRestClient):
1717
def __init__(self, api_key: str, base_url, token, timeout, stream, user_agent=None):
18+
self.stream = stream
1819
super().__init__(
1920
api_key=api_key,
2021
base_url=base_url,
2122
token=token,
2223
timeout=timeout,
2324
user_agent=user_agent,
2425
)
25-
self.stream = stream
2626

2727
def channel(self, call_type: str, id: str) -> Channel:
2828
return Channel(self, call_type, id)

getstream/chat/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,14 @@
1515

1616
class ChatClient(ChatRestClient):
1717
def __init__(self, api_key: str, base_url, token, timeout, stream, user_agent=None):
18+
self.stream = stream
1819
super().__init__(
1920
api_key=api_key,
2021
base_url=base_url,
2122
token=token,
2223
timeout=timeout,
2324
user_agent=user_agent,
2425
)
25-
self.stream = stream
2626

2727
def channel(self, call_type: str, id: str) -> Channel:
2828
return Channel(self, call_type, id)

getstream/feeds/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@
55

66
class FeedsClient(FeedsRestClient):
77
def __init__(self, api_key: str, base_url, token, timeout, stream, user_agent=None):
8+
self.stream = stream
89
super().__init__(
910
api_key=api_key,
1011
base_url=base_url,
1112
token=token,
1213
timeout=timeout,
1314
user_agent=user_agent,
1415
)
15-
self.stream = stream
1616

1717
def feed(
1818
self, feed_type: str, feed_id: str, custom_data: Optional[Dict] = None

getstream/moderation/async_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
class ModerationClient(ModerationRestClient):
55
def __init__(self, api_key: str, base_url, token, timeout, stream, user_agent=None):
6+
self.stream = stream
67
super().__init__(
78
api_key=api_key,
89
base_url=base_url,
910
token=token,
1011
timeout=timeout,
1112
user_agent=user_agent,
1213
)
13-
self.stream = stream

getstream/moderation/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33

44
class ModerationClient(ModerationRestClient):
55
def __init__(self, api_key: str, base_url, token, timeout, stream, user_agent=None):
6+
self.stream = stream
67
super().__init__(
78
api_key=api_key,
89
base_url=base_url,
910
token=token,
1011
timeout=timeout,
1112
user_agent=user_agent,
1213
)
13-
self.stream = stream

getstream/stream.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,12 @@ def __init__(
4747
timeout: Optional[float] = 6.0,
4848
base_url: Optional[str] = BASE_URL,
4949
user_agent: Optional[str] = None,
50+
transport=None,
51+
http_client=None,
5052
):
53+
if transport is not None and http_client is not None:
54+
raise ValueError("Cannot specify both 'transport' and 'http_client'")
55+
5156
if None in (api_key, api_secret, timeout, base_url):
5257
s = Settings() # loads from env and optional .env
5358
api_key = api_key or s.api_key
@@ -68,10 +73,19 @@ def __init__(
6873

6974
self.base_url = validate_and_clean_url(base_url)
7075
self.user_agent = user_agent
76+
self._transport = transport
77+
self._http_client = http_client
7178
self.token = self._create_token()
7279
super().__init__(
7380
self.api_key, self.base_url, self.token, self.timeout, self.user_agent
7481
)
82+
# After super().__init__(), self.client is fully built and configured.
83+
# When the user provided custom HTTP config, sub-clients share this
84+
# client instead of each building their own.
85+
if transport is not None or http_client is not None:
86+
self._shared_client = self.client
87+
else:
88+
self._shared_client = None
7589

7690
def create_token(
7791
self,

getstream/video/async_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ def __init__(self, api_key: str, base_url, token, timeout, stream, user_agent=No
1212
:param timeout: A number representing the time limit for a request
1313
:param user_agent: Optional custom user agent string
1414
"""
15+
self.stream = stream
1516
super().__init__(
1617
api_key=api_key,
1718
base_url=base_url,
1819
token=token,
1920
timeout=timeout,
2021
user_agent=user_agent,
2122
)
22-
self.stream = stream
2323

2424
def call(self, call_type: str, id: str) -> Call:
2525
return Call(self, call_type, id)

getstream/video/client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,14 @@ def __init__(self, api_key: str, base_url, token, timeout, stream, user_agent=No
1212
:param timeout: A number representing the time limit for a request
1313
:param user_agent: Optional custom user agent string
1414
"""
15+
self.stream = stream
1516
super().__init__(
1617
api_key=api_key,
1718
base_url=base_url,
1819
token=token,
1920
timeout=timeout,
2021
user_agent=user_agent,
2122
)
22-
self.stream = stream
2323

2424
def call(self, call_type: str, id: str) -> Call:
2525
return Call(self, call_type, id)

0 commit comments

Comments
 (0)