Skip to content

Commit ca635b4

Browse files
committed
fix: default python sdk transports to http1
1 parent f1516d5 commit ca635b4

9 files changed

Lines changed: 136 additions & 30 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@e2b/python-sdk": patch
3+
---
4+
5+
Default Python SDK API and envd transports to HTTP/1.1, with an `http2` connection option for explicit opt-in.

packages/python-sdk/e2b/api/client_async/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
def get_api_client(config: ConnectionConfig, **kwargs) -> AsyncApiClient:
1414
return AsyncApiClient(
1515
config,
16-
transport=get_transport(config),
16+
transport=get_transport(config, http2=config.http2),
1717
**kwargs,
1818
)
1919

@@ -37,7 +37,7 @@ def pool(self):
3737

3838

3939
def get_transport(
40-
config: ConnectionConfig, http2: bool = True
40+
config: ConnectionConfig, http2: bool = False
4141
) -> AsyncTransportWithLogger:
4242
loop_id = (id(asyncio.get_running_loop()), http2)
4343

@@ -59,7 +59,7 @@ class AsyncEnvdTransportWithLogger(AsyncTransportWithLogger):
5959

6060

6161
def get_envd_transport(
62-
config: ConnectionConfig, http2: bool = True
62+
config: ConnectionConfig, http2: bool = False
6363
) -> AsyncEnvdTransportWithLogger:
6464
loop_id = (id(asyncio.get_running_loop()), http2)
6565

packages/python-sdk/e2b/api/client_sync/__init__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
def get_api_client(config: ConnectionConfig, **kwargs) -> ApiClient:
1414
return ApiClient(
1515
config,
16-
transport=get_transport(config),
16+
transport=get_transport(config, http2=config.http2),
1717
**kwargs,
1818
)
1919

@@ -36,7 +36,7 @@ def pool(self):
3636
return self._pool
3737

3838

39-
def get_transport(config: ConnectionConfig, http2: bool = True) -> TransportWithLogger:
39+
def get_transport(config: ConnectionConfig, http2: bool = False) -> TransportWithLogger:
4040
instances: Dict[bool, TransportWithLogger] = getattr(
4141
TransportWithLogger._thread_local, "instances", {}
4242
)
@@ -59,7 +59,7 @@ class EnvdTransportWithLogger(TransportWithLogger):
5959

6060

6161
def get_envd_transport(
62-
config: ConnectionConfig, http2: bool = True
62+
config: ConnectionConfig, http2: bool = False
6363
) -> EnvdTransportWithLogger:
6464
instances: Dict[bool, EnvdTransportWithLogger] = getattr(
6565
EnvdTransportWithLogger._thread_local, "instances", {}

packages/python-sdk/e2b/connection_config.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ class ApiParams(TypedDict, total=False):
4848
sandbox_url: Optional[str]
4949
"""URL to connect to sandbox, defaults to `E2B_SANDBOX_URL` environment variable."""
5050

51+
http2: Optional[bool]
52+
"""Whether to use HTTP/2 for API and sandbox requests, defaults to `False`."""
53+
5154

5255
class ConnectionConfig:
5356
"""
@@ -93,6 +96,7 @@ def __init__(
9396
api_headers: Optional[Dict[str, str]] = None,
9497
extra_sandbox_headers: Optional[Dict[str, str]] = None,
9598
proxy: Optional[ProxyTypes] = None,
99+
http2: Optional[bool] = None,
96100
):
97101
self.domain = domain or ConnectionConfig._domain()
98102
self.debug = debug or ConnectionConfig._debug()
@@ -103,6 +107,7 @@ def __init__(
103107
self.__extra_sandbox_headers = extra_sandbox_headers or {}
104108

105109
self.proxy = proxy
110+
self.http2 = False if http2 is None else http2
106111

107112
self.request_timeout = ConnectionConfig._get_request_timeout(
108113
REQUEST_TIMEOUT,
@@ -195,6 +200,7 @@ def get_api_params(
195200
domain = opts.get("domain")
196201
debug = opts.get("debug")
197202
proxy = opts.get("proxy")
203+
http2 = opts.get("http2")
198204

199205
req_headers = self.headers.copy()
200206
if headers is not None:
@@ -211,6 +217,7 @@ def get_api_params(
211217
request_timeout=self.get_request_timeout(request_timeout),
212218
headers=req_headers,
213219
proxy=proxy if proxy is not None else self.proxy,
220+
http2=http2 if http2 is not None else self.http2,
214221
)
215222
)
216223

packages/python-sdk/e2b/sandbox_async/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ def __init__(
102102
"""
103103
super().__init__(**opts)
104104

105-
self._transport = get_transport(self.connection_config)
105+
self._transport = get_transport(
106+
self.connection_config, http2=self.connection_config.http2
107+
)
106108
self._envd_api = httpx.AsyncClient(
107109
base_url=self.connection_config.get_sandbox_url(
108110
self.sandbox_id, self.sandbox_domain

packages/python-sdk/e2b/sandbox_sync/main.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,9 @@ def __init__(self, **opts: Unpack[SandboxOpts]):
101101
"""
102102
super().__init__(**opts)
103103

104-
self._transport = get_transport(self.connection_config)
104+
self._transport = get_transport(
105+
self.connection_config, http2=self.connection_config.http2
106+
)
105107

106108
self._envd_api = httpx.Client(
107109
base_url=self.envd_api_url,

packages/python-sdk/tests/async/sandbox_async/test_config_propagation.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,31 @@ async def test_connect_sets_stable_host_routing_headers(monkeypatch, test_api_ke
114114
ConnectionConfig.envd_port
115115
)
116116
assert sandbox.connection_config.sandbox_headers["X-Access-Token"] == "tok"
117+
118+
119+
@pytest.mark.skip_debug()
120+
async def test_create_passes_http2_to_envd_transport(monkeypatch, test_api_key):
121+
dummy_transport = SimpleNamespace(pool=object())
122+
captured = {}
123+
124+
def get_transport(config, **kwargs):
125+
captured["config_http2"] = config.http2
126+
captured["http2"] = kwargs["http2"]
127+
return dummy_transport
128+
129+
monkeypatch.setattr(sandbox_async_main, "get_transport", get_transport)
130+
monkeypatch.setattr(
131+
sandbox_async_main.httpx, "AsyncClient", lambda *args, **kwargs: object()
132+
)
133+
monkeypatch.setattr(
134+
sandbox_async_main, "Filesystem", lambda *args, **kwargs: object()
135+
)
136+
monkeypatch.setattr(
137+
sandbox_async_main, "Commands", lambda *args, **kwargs: object()
138+
)
139+
monkeypatch.setattr(sandbox_async_main, "Pty", lambda *args, **kwargs: object())
140+
monkeypatch.setattr(sandbox_async_main, "Git", lambda *args, **kwargs: object())
141+
142+
await AsyncSandbox.create(debug=True, api_key=test_api_key, http2=True)
143+
144+
assert captured == {"config_http2": True, "http2": True}

packages/python-sdk/tests/sync/sandbox_sync/test_config_propagation.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,29 @@ def test_connect_sets_stable_host_routing_headers(monkeypatch, test_api_key):
108108
ConnectionConfig.envd_port
109109
)
110110
assert sandbox.connection_config.sandbox_headers["X-Access-Token"] == "tok"
111+
112+
113+
@pytest.mark.skip_debug()
114+
def test_create_passes_http2_to_envd_transport(monkeypatch, test_api_key):
115+
dummy_transport = SimpleNamespace(pool=object())
116+
captured = {}
117+
118+
def get_transport(config, **kwargs):
119+
captured["config_http2"] = config.http2
120+
captured["http2"] = kwargs["http2"]
121+
return dummy_transport
122+
123+
monkeypatch.setattr(sandbox_sync_main, "get_transport", get_transport)
124+
monkeypatch.setattr(
125+
sandbox_sync_main.httpx, "Client", lambda *args, **kwargs: object()
126+
)
127+
monkeypatch.setattr(
128+
sandbox_sync_main, "Filesystem", lambda *args, **kwargs: object()
129+
)
130+
monkeypatch.setattr(sandbox_sync_main, "Commands", lambda *args, **kwargs: object())
131+
monkeypatch.setattr(sandbox_sync_main, "Pty", lambda *args, **kwargs: object())
132+
monkeypatch.setattr(sandbox_sync_main, "Git", lambda *args, **kwargs: object())
133+
134+
Sandbox.create(debug=True, api_key=test_api_key, http2=True)
135+
136+
assert captured == {"config_http2": True, "http2": True}

packages/python-sdk/tests/test_api_client_transport.py

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import asyncio
22
from concurrent.futures import ThreadPoolExecutor
3+
from typing import Any
34

45
import pytest
56

@@ -27,6 +28,10 @@ def run_in_worker_thread(fn):
2728
return executor.submit(fn).result()
2829

2930

31+
def transport_uses_http2(transport: Any) -> bool:
32+
return bool(getattr(transport._pool, "_http2"))
33+
34+
3035
def test_sync_api_client_proxy_uses_explicit_transport(test_api_key):
3136
reset_sync_api_transports()
3237
config = ConnectionConfig(
@@ -39,32 +44,47 @@ def test_sync_api_client_proxy_uses_explicit_transport(test_api_key):
3944

4045
try:
4146
assert "proxy" not in api_client._httpx_args
42-
assert httpx_client._transport is get_sync_transport(config)
47+
assert httpx_client._transport is get_sync_transport(config, http2=False)
4348
assert httpx_client._mounts == {}
4449
finally:
4550
httpx_client.close()
4651
reset_sync_api_transports()
4752

4853

49-
def test_sync_get_transport_http2_opt_out_returns_distinct_instance(test_api_key):
54+
def test_sync_get_transport_http2_opt_in_returns_distinct_instance(test_api_key):
5055
reset_sync_api_transports()
5156
config = ConnectionConfig(api_key=test_api_key)
5257

5358
try:
54-
http2_transport = get_sync_transport(config)
55-
http1_transport = get_sync_transport(config, http2=False)
59+
http1_transport = get_sync_transport(config)
60+
http2_transport = get_sync_transport(config, http2=True)
5661

57-
assert http2_transport is not http1_transport
58-
assert http2_transport._pool._http2 is True
59-
assert http1_transport._pool._http2 is False
62+
assert transport_uses_http2(http1_transport) is False
63+
assert transport_uses_http2(http2_transport) is True
64+
assert http1_transport is not http2_transport
6065
# Subsequent calls with the same http2 flag return the cached
6166
# instance.
62-
assert get_sync_transport(config) is http2_transport
63-
assert get_sync_transport(config, http2=False) is http1_transport
67+
assert get_sync_transport(config) is http1_transport
68+
assert get_sync_transport(config, http2=True) is http2_transport
6469
finally:
6570
reset_sync_api_transports()
6671

6772

73+
def test_sync_api_client_respects_connection_config_http2(test_api_key):
74+
reset_sync_api_transports()
75+
config = ConnectionConfig(api_key=test_api_key, http2=True)
76+
77+
api_client = get_sync_api_client(config)
78+
httpx_client = api_client.get_httpx_client()
79+
80+
try:
81+
assert httpx_client._transport is get_sync_transport(config, http2=True)
82+
assert transport_uses_http2(httpx_client._transport) is True
83+
finally:
84+
httpx_client.close()
85+
reset_sync_api_transports()
86+
87+
6888
def test_sync_envd_transport_uses_separate_cache(test_api_key):
6989
reset_sync_api_transports()
7090
reset_sync_envd_transports()
@@ -77,7 +97,7 @@ def test_sync_envd_transport_uses_separate_cache(test_api_key):
7797
assert api_transport is not envd_transport
7898
assert get_sync_transport(config) is api_transport
7999
assert get_sync_envd_transport(config) is envd_transport
80-
assert envd_transport._pool._http2 is True
100+
assert transport_uses_http2(envd_transport) is False
81101
finally:
82102
reset_sync_api_transports()
83103
reset_sync_envd_transports()
@@ -112,8 +132,8 @@ def test_sync_envd_transport_cache_is_thread_local(test_api_key):
112132

113133
assert main_transport is get_sync_envd_transport(config)
114134
assert thread_transport is not main_transport
115-
assert main_transport._pool._http2 is True
116-
assert thread_transport._pool._http2 is True
135+
assert transport_uses_http2(main_transport) is False
136+
assert transport_uses_http2(thread_transport) is False
117137
finally:
118138
reset_sync_envd_transports()
119139

@@ -129,7 +149,7 @@ async def test_async_api_client_proxy_uses_explicit_transport(test_api_key):
129149
api_client = get_async_api_client(config)
130150
httpx_client = api_client.get_async_httpx_client()
131151
transport = AsyncTransportWithLogger._instances[
132-
(id(asyncio.get_running_loop()), True)
152+
(id(asyncio.get_running_loop()), False)
133153
]
134154

135155
try:
@@ -142,27 +162,43 @@ async def test_async_api_client_proxy_uses_explicit_transport(test_api_key):
142162

143163

144164
@pytest.mark.asyncio
145-
async def test_async_get_transport_http2_opt_out_returns_distinct_instance(
165+
async def test_async_get_transport_http2_opt_in_returns_distinct_instance(
146166
test_api_key,
147167
):
148168
AsyncTransportWithLogger._instances.clear()
149169
config = ConnectionConfig(api_key=test_api_key)
150170

151171
try:
152-
http2_transport = get_async_transport(config)
153-
http1_transport = get_async_transport(config, http2=False)
172+
http1_transport = get_async_transport(config)
173+
http2_transport = get_async_transport(config, http2=True)
154174

155-
assert http2_transport is not http1_transport
156-
assert http2_transport._pool._http2 is True
157-
assert http1_transport._pool._http2 is False
175+
assert transport_uses_http2(http1_transport) is False
176+
assert transport_uses_http2(http2_transport) is True
177+
assert http1_transport is not http2_transport
158178
# Subsequent calls with the same http2 flag return the cached
159179
# instance.
160-
assert get_async_transport(config) is http2_transport
161-
assert get_async_transport(config, http2=False) is http1_transport
180+
assert get_async_transport(config) is http1_transport
181+
assert get_async_transport(config, http2=True) is http2_transport
162182
finally:
163183
AsyncTransportWithLogger._instances.clear()
164184

165185

186+
@pytest.mark.asyncio
187+
async def test_async_api_client_respects_connection_config_http2(test_api_key):
188+
AsyncTransportWithLogger._instances.clear()
189+
config = ConnectionConfig(api_key=test_api_key, http2=True)
190+
191+
api_client = get_async_api_client(config)
192+
httpx_client = api_client.get_async_httpx_client()
193+
194+
try:
195+
assert httpx_client._transport is get_async_transport(config, http2=True)
196+
assert transport_uses_http2(httpx_client._transport) is True
197+
finally:
198+
await httpx_client.aclose()
199+
AsyncTransportWithLogger._instances.clear()
200+
201+
166202
@pytest.mark.asyncio
167203
async def test_async_envd_transport_uses_separate_cache(test_api_key):
168204
AsyncTransportWithLogger._instances.clear()
@@ -176,7 +212,7 @@ async def test_async_envd_transport_uses_separate_cache(test_api_key):
176212
assert api_transport is not envd_transport
177213
assert get_async_transport(config) is api_transport
178214
assert get_async_envd_transport(config) is envd_transport
179-
assert envd_transport._pool._http2 is True
215+
assert transport_uses_http2(envd_transport) is False
180216
finally:
181217
AsyncTransportWithLogger._instances.clear()
182218
AsyncEnvdTransportWithLogger._instances.clear()

0 commit comments

Comments
 (0)