Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/python-sdk-restore-http2-parameter.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@e2b/python-sdk': patch
---

Restore the `http2` parameter on `get_transport` and `get_envd_transport`, which the pyqwest migration dropped in 2.38.0. `http2=False` again returns a transport pinned to HTTP/1.1, on its own connection pool.
61 changes: 44 additions & 17 deletions packages/python-sdk/e2b/api/client_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

import httpx

from pyqwest import HTTPTransport, Request, Response
from pyqwest import HTTPTransport, HTTPVersion, Request, Response
from pyqwest.httpx import AsyncPyqwestTransport
from pyqwest.middleware.retry import RetryTransport

Expand Down Expand Up @@ -40,7 +40,9 @@ def should_retry_response(


def retrying_http_transport(
proxy: Optional[ProxyConfig], read_timeout: Optional[float] = None
proxy: Optional[ProxyConfig],
read_timeout: Optional[float] = None,
http2: bool = True,
) -> ConnectionRetryTransport:
"""A fresh pyqwest transport (= its own connection pool) with the SDK's
shared tuning — system CA certs (without which TLS through an
Expand All @@ -51,6 +53,9 @@ def retrying_http_transport(
``read_timeout`` bounds every read on the transport's connections; see
:func:`get_envd_transport` for when that is (and isn't) appropriate.

``http2=False`` pins the transport to HTTP/1.1; see :func:`get_transport`
for when that matters.

Requests are logged by pyqwest itself on the ``pyqwest.access`` and
``pyqwest`` loggers at ``DEBUG`` (off unless enabled) — the transport-level
diagnostics httpcore used to provide. The SDK's own ``logger`` option is
Expand All @@ -62,6 +67,10 @@ def retrying_http_transport(
pool_idle_timeout=pool_idle_timeout,
pool_max_idle_per_host=pool_max_idle_per_host,
read_timeout=read_timeout,
# `None` leaves the version to ALPN on TLS connections (HTTP/2
# against the E2B API) and uses HTTP/1 for plaintext, like the
# http2-enabled httpx transport this replaced.
http_version=None if http2 else HTTPVersion.HTTP1,
# Redirects belong to the httpx client above (which the generated
# clients leave off), not to reqwest.
follow_redirects=False,
Expand All @@ -71,33 +80,47 @@ def retrying_http_transport(


_transport_lock = threading.Lock()
# One transport (= one connection pool) per proxy; None is the direct pool.
# pyqwest's I/O runs on its own Rust runtime, so unlike the httpx transports
# they replaced, the transports are not bound to an event loop and the
# caches are process-global rather than per-loop.
_transports: Dict[Optional[ProxyConfig], AsyncPyqwestTransport] = {}
# One transport (= one connection pool) per (proxy, http2) pair; a None proxy
# is the direct pool. pyqwest's I/O runs on its own Rust runtime, so unlike
# the httpx transports they replaced, the transports are not bound to an event
# loop and the caches are process-global rather than per-loop.
_transports: Dict[Tuple[Optional[ProxyConfig], bool], AsyncPyqwestTransport] = {}


def get_transport(config: ConnectionConfig) -> AsyncPyqwestTransport:
def get_transport(
config: ConnectionConfig, http2: bool = True
) -> AsyncPyqwestTransport:
"""The shared pyqwest-backed httpx transport for REST API calls. For TLS
connections ALPN negotiates the HTTP version (HTTP/2 against the E2B
API), like the http2-enabled httpx transport this replaced."""
API), like the http2-enabled httpx transport this replaced.

``http2=False`` returns a separate transport (its own pool) pinned to
HTTP/1.1. That matters for a server that reacts to a client going away:
HTTP/2 multiplexes requests over one connection, so abandoning a request
only resets its stream and the server may never notice, while HTTP/1.1's
one-connection-per-request closes the connection and the server observes
the disconnect."""
proxy = proxy_to_config(config.proxy)
key = (proxy, http2)
with _transport_lock:
transport = _transports.get(proxy)
transport = _transports.get(key)
if transport is None:
transport = AsyncPyqwestTransport(retrying_http_transport(proxy))
_transports[proxy] = transport
transport = AsyncPyqwestTransport(
retrying_http_transport(proxy, http2=http2)
)
_transports[key] = transport
return transport


# One transport per (proxy, streaming) pair, separate from the REST API
# pools — envd traffic goes to per-sandbox hosts.
_envd_transports: Dict[Tuple[Optional[ProxyConfig], bool], AsyncPyqwestTransport] = {}
# One transport per (proxy, http2, streaming) triple, separate from the REST
# API pools — envd traffic goes to per-sandbox hosts.
_envd_transports: Dict[
Tuple[Optional[ProxyConfig], bool, bool], AsyncPyqwestTransport
] = {}


def get_envd_transport(
config: ConnectionConfig, *, for_streaming: bool = False
config: ConnectionConfig, http2: bool = True, *, for_streaming: bool = False
) -> AsyncPyqwestTransport:
"""The shared pyqwest-backed httpx transports for the envd HTTP API
(file transfers, health checks).
Expand All @@ -111,16 +134,20 @@ def get_envd_transport(
the response head, so on the regular transport it would cut off uploads
and slow unary responses longer than the idle bound (those stay bounded
by their whole-request deadlines instead).

``http2=False`` pins the transport to HTTP/1.1 — see
:func:`get_transport`.
"""
proxy = proxy_to_config(config.proxy)
key = (proxy, for_streaming)
key = (proxy, http2, for_streaming)
with _transport_lock:
transport = _envd_transports.get(key)
if transport is None:
transport = AsyncPyqwestTransport(
retrying_http_transport(
proxy,
read_timeout=READ_TIMEOUT if for_streaming else None,
http2=http2,
)
)
_envd_transports[key] = transport
Expand Down
54 changes: 38 additions & 16 deletions packages/python-sdk/e2b/api/client_sync/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import httpx
import threading

from pyqwest import SyncHTTPTransport, SyncRequest, SyncResponse
from pyqwest import HTTPVersion, SyncHTTPTransport, SyncRequest, SyncResponse
from pyqwest.httpx import PyqwestTransport
from pyqwest.middleware.retry import SyncRetryTransport

Expand Down Expand Up @@ -40,7 +40,9 @@ def should_retry_response(


def retrying_http_transport(
proxy: Optional[ProxyConfig], read_timeout: Optional[float] = None
proxy: Optional[ProxyConfig],
read_timeout: Optional[float] = None,
http2: bool = True,
) -> ConnectionRetryTransport:
"""A fresh pyqwest transport (= its own connection pool) with the SDK's
shared tuning — system CA certs (without which TLS through an
Expand All @@ -51,6 +53,9 @@ def retrying_http_transport(
``read_timeout`` bounds every read on the transport's connections; see
:func:`get_envd_transport` for when that is (and isn't) appropriate.

``http2=False`` pins the transport to HTTP/1.1; see :func:`get_transport`
for when that matters.

Requests are logged by pyqwest itself on the ``pyqwest.access`` and
``pyqwest`` loggers at ``DEBUG`` (off unless enabled) — the transport-level
diagnostics httpcore used to provide. The SDK's own ``logger`` option is
Expand All @@ -62,6 +67,10 @@ def retrying_http_transport(
pool_idle_timeout=pool_idle_timeout,
pool_max_idle_per_host=pool_max_idle_per_host,
read_timeout=read_timeout,
# `None` leaves the version to ALPN on TLS connections (HTTP/2
# against the E2B API) and uses HTTP/1 for plaintext, like the
# http2-enabled httpx transport this replaced.
http_version=None if http2 else HTTPVersion.HTTP1,
# Redirects belong to the httpx client above (which the generated
# clients leave off), not to reqwest.
follow_redirects=False,
Expand All @@ -71,32 +80,41 @@ def retrying_http_transport(


_transport_lock = threading.Lock()
# One transport (= one connection pool) per proxy; None is the direct pool.
# pyqwest transports are thread-safe, so unlike the httpx transports they
# replaced, the caches are process-global rather than per-thread.
_transports: Dict[Optional[ProxyConfig], PyqwestTransport] = {}
# One transport (= one connection pool) per (proxy, http2) pair; a None proxy
# is the direct pool. pyqwest transports are thread-safe, so unlike the httpx
# transports they replaced, the caches are process-global rather than
# per-thread.
_transports: Dict[Tuple[Optional[ProxyConfig], bool], PyqwestTransport] = {}


def get_transport(config: ConnectionConfig) -> PyqwestTransport:
def get_transport(config: ConnectionConfig, http2: bool = True) -> PyqwestTransport:
"""The shared pyqwest-backed httpx transport for REST API calls. For TLS
connections ALPN negotiates the HTTP version (HTTP/2 against the E2B
API), like the http2-enabled httpx transport this replaced."""
API), like the http2-enabled httpx transport this replaced.

``http2=False`` returns a separate transport (its own pool) pinned to
HTTP/1.1. That matters for a server that reacts to a client going away:
HTTP/2 multiplexes requests over one connection, so abandoning a request
only resets its stream and the server may never notice, while HTTP/1.1's
one-connection-per-request closes the connection and the server observes
the disconnect."""
proxy = proxy_to_config(config.proxy)
key = (proxy, http2)
with _transport_lock:
transport = _transports.get(proxy)
transport = _transports.get(key)
if transport is None:
transport = PyqwestTransport(retrying_http_transport(proxy))
_transports[proxy] = transport
transport = PyqwestTransport(retrying_http_transport(proxy, http2=http2))
_transports[key] = transport
return transport


# One transport per (proxy, streaming) pair, separate from the REST API
# pools — envd traffic goes to per-sandbox hosts.
_envd_transports: Dict[Tuple[Optional[ProxyConfig], bool], PyqwestTransport] = {}
# One transport per (proxy, http2, streaming) triple, separate from the REST
# API pools — envd traffic goes to per-sandbox hosts.
_envd_transports: Dict[Tuple[Optional[ProxyConfig], bool, bool], PyqwestTransport] = {}


def get_envd_transport(
config: ConnectionConfig, *, for_streaming: bool = False
config: ConnectionConfig, http2: bool = True, *, for_streaming: bool = False
) -> PyqwestTransport:
"""The shared pyqwest-backed httpx transports for the envd HTTP API
(file transfers, health checks).
Expand All @@ -111,16 +129,20 @@ def get_envd_transport(
head, so on the regular transport it would cut off uploads and slow
unary responses longer than the idle bound (those stay bounded by their
whole-request deadlines instead).

``http2=False`` pins the transport to HTTP/1.1 — see
:func:`get_transport`.
"""
proxy = proxy_to_config(config.proxy)
key = (proxy, for_streaming)
key = (proxy, http2, for_streaming)
with _transport_lock:
transport = _envd_transports.get(key)
if transport is None:
transport = PyqwestTransport(
retrying_http_transport(
proxy,
read_timeout=READ_TIMEOUT if for_streaming else None,
http2=http2,
)
)
_envd_transports[key] = transport
Expand Down
Loading
Loading