11import asyncio
2+ import threading
23import weakref
3- from typing import Dict , Optional , Tuple
4+ from typing import Dict , Optional , Tuple , Union
45
56import httpx
67
78from httpx ._types import ProxyTypes
8-
9- from e2b .api import AsyncApiClient , connection_retries , limits
9+ from pyqwest import HTTPTransport , Request , Response
10+ from pyqwest .httpx import AsyncPyqwestTransport
11+ from pyqwest .middleware .retry import RetryTransport
12+
13+ from e2b .api import (
14+ AsyncApiClient ,
15+ connection_retries ,
16+ limits ,
17+ pool_idle_timeout ,
18+ pool_max_idle_per_host ,
19+ proxy_to_url ,
20+ )
1021from e2b .connection_config import ConnectionConfig
1122
1223TransportKey = Tuple [bool , Optional [ProxyTypes ]]
@@ -20,31 +31,92 @@ def get_api_client(config: ConnectionConfig, **kwargs) -> AsyncApiClient:
2031 )
2132
2233
23- class AsyncTransportWithLogger (httpx .AsyncHTTPTransport ):
34+ class AsyncApiPyqwestTransport (AsyncPyqwestTransport ):
35+ """Strip the ``Host`` header httpx adds to every request: hyper derives
36+ HTTP/1 ``Host`` and HTTP/2 ``:authority`` from the URL itself, and
37+ forwarding an explicit ``host`` header on an HTTP/2 connection makes the
38+ E2B API edge reset the stream with PROTOCOL_ERROR. (Custom ``Host``
39+ overrides are therefore not honored, matching hyper's URL-derived
40+ behavior.)"""
41+
42+ async def handle_async_request (self , request : httpx .Request ) -> httpx .Response :
43+ if "host" in request .headers :
44+ del request .headers ["host" ]
45+ return await super ().handle_async_request (request )
46+
47+
48+ class ConnectionRetryTransport (RetryTransport ):
49+ """Retry only failures establishing the connection, matching the
50+ connect-only ``retries`` of the httpx transport this replaced: pyqwest
51+ raises the builtin ``ConnectionError`` only before the request was
52+ written, so these retries can never replay a request the API may have
53+ received. The retry middleware's default policy would otherwise also
54+ retry I/O errors and 429/5xx responses for idempotent methods."""
55+
56+ def should_retry_response (
57+ self , request : Request , response : Union [Response , Exception ]
58+ ) -> bool :
59+ return isinstance (response , ConnectionError )
60+
61+
62+ _transport_lock = threading .Lock ()
63+ # One transport (= one connection pool) per proxy; None is the direct pool.
64+ # pyqwest's I/O runs on its own Rust runtime, so unlike the httpx envd
65+ # transports below, the transport is not bound to an event loop and the
66+ # cache is process-global rather than per-loop.
67+ _transports : Dict [Optional [str ], "AsyncApiPyqwestTransport" ] = {}
68+
69+
70+ def get_transport (config : ConnectionConfig ) -> "AsyncApiPyqwestTransport" :
71+ """The shared pyqwest-backed httpx transport for REST API calls. For TLS
72+ connections ALPN negotiates the HTTP version (HTTP/2 against the E2B
73+ API), like the http2-enabled httpx transport this replaced."""
74+ proxy_url = proxy_to_url (config .proxy )
75+ with _transport_lock :
76+ transport = _transports .get (proxy_url )
77+ if transport is None :
78+ transport = AsyncApiPyqwestTransport (
79+ ConnectionRetryTransport (
80+ HTTPTransport (
81+ tls_include_system_certs = True ,
82+ proxy = proxy_url ,
83+ pool_idle_timeout = pool_idle_timeout ,
84+ pool_max_idle_per_host = pool_max_idle_per_host ,
85+ ),
86+ max_retries = connection_retries ,
87+ )
88+ )
89+ _transports [proxy_url ] = transport
90+ return transport
91+
92+
93+ class AsyncEnvdTransportWithLogger (httpx .AsyncHTTPTransport ):
2494 # Keyed weakly by the event loop object itself, not id(loop) — CPython
2595 # reuses object ids, so a new loop could otherwise inherit a transport
2696 # bound to a previous, closed loop.
2797 _instances : weakref .WeakKeyDictionary [
2898 asyncio .AbstractEventLoop ,
29- Dict [TransportKey , "AsyncTransportWithLogger " ],
99+ Dict [TransportKey , "AsyncEnvdTransportWithLogger " ],
30100 ] = weakref .WeakKeyDictionary ()
31101
32102 @property
33103 def pool (self ):
34104 return self ._pool
35105
36106
37- def _get_cached_transport (cls , config : ConnectionConfig , http2 : bool ):
107+ def get_envd_transport (
108+ config : ConnectionConfig , http2 : bool = True
109+ ) -> AsyncEnvdTransportWithLogger :
38110 loop = asyncio .get_running_loop ()
39- loop_instances = cls ._instances .get (loop )
111+ loop_instances = AsyncEnvdTransportWithLogger ._instances .get (loop )
40112 if loop_instances is None :
41113 loop_instances = {}
42- cls ._instances [loop ] = loop_instances
114+ AsyncEnvdTransportWithLogger ._instances [loop ] = loop_instances
43115
44116 key : TransportKey = (http2 , config .proxy )
45117 transport = loop_instances .get (key )
46118 if transport is None :
47- transport = cls (
119+ transport = AsyncEnvdTransportWithLogger (
48120 limits = limits ,
49121 proxy = config .proxy ,
50122 http2 = http2 ,
@@ -53,22 +125,3 @@ def _get_cached_transport(cls, config: ConnectionConfig, http2: bool):
53125 loop_instances [key ] = transport
54126
55127 return transport
56-
57-
58- def get_transport (
59- config : ConnectionConfig , http2 : bool = True
60- ) -> AsyncTransportWithLogger :
61- return _get_cached_transport (AsyncTransportWithLogger , config , http2 )
62-
63-
64- class AsyncEnvdTransportWithLogger (AsyncTransportWithLogger ):
65- _instances : weakref .WeakKeyDictionary [
66- asyncio .AbstractEventLoop ,
67- Dict [TransportKey , "AsyncEnvdTransportWithLogger" ],
68- ] = weakref .WeakKeyDictionary ()
69-
70-
71- def get_envd_transport (
72- config : ConnectionConfig , http2 : bool = True
73- ) -> AsyncEnvdTransportWithLogger :
74- return _get_cached_transport (AsyncEnvdTransportWithLogger , config , http2 )
0 commit comments