fix(python-sdk): restore the http2 parameter on the transport factories (#1671)
The pyqwest migration in 2.38.0 dropped the `http2` parameter from `get_transport` and `get_envd_transport` (added deliberately in #1347, 2.32.0) and collapsed the transport cache key to the proxy alone, so `e2b-code-interpreter`'s Jupyter requests — `get_transport(self.connection_config, http2=False)` — now raise `TypeError: get_transport() got an unexpected keyword argument 'http2'`; that is already live, since `e2b = "^2.26.0"` resolves to 2.38.x, and it blocks the Python half of code-interpreter [#328](https://github.com/e2b-dev/code-interpreter/pull/328). pyqwest supports the capability, it just was not threaded through: this restores the pre-2.38.0 signature (so no consumer code changes, only an `e2b` floor bump) by passing `http_version=None if http2 else HTTPVersion.HTTP1` into the pyqwest transports, and puts the HTTP version back into both cache keys — without that, whichever caller asks second is handed a transport of the wrong version. The default is unchanged: `None` leaves TLS connections to ALPN (HTTP/2 against the E2B API) and uses HTTP/1 for plaintext, exactly as today. HTTP/1.1 is not cosmetic for the consumer — with HTTP/2 multiplexing, abandoning a request only resets its stream, so the code-interpreter server never sees the `http.disconnect` it needs to interrupt the kernel, while HTTP/1.1's one connection per request closes the connection and the server observes it. ## Usage Both factories are internal (nothing is exported from `e2b/__init__.py`), so there is no public API change; consumers reaching into them get the 2.32.0 call back: ```python from e2b.api.client_sync import get_transport, get_envd_transport # Unchanged: ALPN negotiates the version (HTTP/2 against the E2B API). transport = get_transport(config) # Its own pool, pinned to HTTP/1.1, so a cancelled request closes the # connection and the server observes the disconnect. http1 = get_transport(config, http2=False) envd_http1 = get_envd_transport(config, http2=False) ``` The async mirror (`e2b.api.client_async`) is identical. ## Tests Six new cases in `packages/python-sdk/tests/test_api_client_transport.py`, sync and async: cache separation and identity across `http2` / proxy / `for_streaming`, the `http_version` value actually reaching the pyqwest transport (`[None, HTTP1, HTTP1]`), and a round trip proving the pinned transport works. The negotiated version can't be observed locally — the test echo server is plaintext, where both settings speak HTTP/1 — so it is asserted at the constructor, with the reason in a comment; it was verified by hand against `https://api.e2b.app/health` via the `pyqwest.access` logger, which shows `"HTTP/2 200 OK"` on the default and `"HTTP/1.1 200 OK"` with `http2=False` on both factories (and confirms `httpx.Response.http_version` is unreliable through the adapter — it reports HTTP/1.1 either way). 256 unit tests pass, plus `make lint`, `make typecheck` and `make format`. No JS change: its transport is an undici-dispatcher `fetch` with no HTTP-version knob, and the JS half of code-interpreter #328 is a clean bump. Closes [SDK-335](https://linear.app/e2b/issue/SDK-335/python-sdk-get-transport-lost-its-http2-parameter-in-2380-breaking-e2b) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
@@ -3,7 +3,7 @@ from typing import Dict, Optional, Tuple, Union
|
||||
|
||||
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
|
||||
|
||||
@@ -40,7 +40,9 @@ class ConnectionRetryTransport(RetryTransport):
|
||||
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -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).
|
||||
@@ -111,9 +134,12 @@ 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:
|
||||
@@ -121,6 +147,7 @@ def get_envd_transport(
|
||||
retrying_http_transport(
|
||||
proxy,
|
||||
read_timeout=READ_TIMEOUT if for_streaming else None,
|
||||
http2=http2,
|
||||
)
|
||||
)
|
||||
_envd_transports[key] = transport
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Dict, Optional, Tuple, Union
|
||||
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
|
||||
|
||||
@@ -40,7 +40,9 @@ class ConnectionRetryTransport(SyncRetryTransport):
|
||||
|
||||
|
||||
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
|
||||
@@ -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
|
||||
@@ -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,
|
||||
@@ -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).
|
||||
@@ -111,9 +129,12 @@ 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:
|
||||
@@ -121,6 +142,7 @@ def get_envd_transport(
|
||||
retrying_http_transport(
|
||||
proxy,
|
||||
read_timeout=READ_TIMEOUT if for_streaming else None,
|
||||
http2=http2,
|
||||
)
|
||||
)
|
||||
_envd_transports[key] = transport
|
||||
|
||||
@@ -9,6 +9,7 @@ from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from pyqwest import HTTPVersion
|
||||
from pyqwest.httpx import AsyncPyqwestTransport, PyqwestTransport
|
||||
|
||||
import e2b.api.client_async as client_async
|
||||
@@ -86,6 +87,68 @@ def test_sync_get_transport_keyed_by_proxy(test_api_key):
|
||||
reset_sync_api_transports()
|
||||
|
||||
|
||||
def test_sync_transports_keyed_by_http_version(test_api_key):
|
||||
# The HTTP version is part of the cache key: without it, whichever caller
|
||||
# asked second would get a transport pinned to the other version.
|
||||
reset_sync_api_transports()
|
||||
config = ConnectionConfig(api_key=test_api_key)
|
||||
proxied_config = ConnectionConfig(
|
||||
api_key=test_api_key,
|
||||
proxy="http://127.0.0.1:9999",
|
||||
)
|
||||
|
||||
try:
|
||||
negotiated = get_sync_transport(config)
|
||||
http1 = get_sync_transport(config, http2=False)
|
||||
envd_negotiated = get_sync_envd_transport(config)
|
||||
envd_http1 = get_sync_envd_transport(config, http2=False)
|
||||
|
||||
assert http1 is not negotiated
|
||||
assert envd_http1 is not envd_negotiated
|
||||
assert envd_http1 is not http1
|
||||
# Each version still has one pool per proxy, and repeat calls with the
|
||||
# same arguments reuse it.
|
||||
assert get_sync_transport(proxied_config, http2=False) not in (
|
||||
http1,
|
||||
negotiated,
|
||||
)
|
||||
assert get_sync_transport(config, http2=False) is http1
|
||||
assert get_sync_transport(config) is negotiated
|
||||
assert get_sync_envd_transport(config, http2=False) is envd_http1
|
||||
assert (
|
||||
get_sync_envd_transport(config, http2=False, for_streaming=True)
|
||||
is not envd_http1
|
||||
)
|
||||
finally:
|
||||
reset_sync_api_transports()
|
||||
|
||||
|
||||
def test_sync_transports_pass_http_version_to_pyqwest(test_api_key, monkeypatch):
|
||||
# `http_version=None` leaves the version to ALPN (HTTP/2 against the E2B
|
||||
# API), `HTTP1` pins HTTP/1.1. Which version was negotiated is only
|
||||
# observable over TLS — the local echo server is plaintext, where both
|
||||
# settings speak HTTP/1 — so assert what reaches the pyqwest transport.
|
||||
reset_sync_api_transports()
|
||||
config = ConnectionConfig(api_key=test_api_key)
|
||||
captured = []
|
||||
build_transport = client_sync.SyncHTTPTransport
|
||||
|
||||
def record(**kwargs):
|
||||
captured.append(kwargs["http_version"])
|
||||
return build_transport(**kwargs)
|
||||
|
||||
monkeypatch.setattr(client_sync, "SyncHTTPTransport", record)
|
||||
|
||||
try:
|
||||
get_sync_transport(config)
|
||||
get_sync_transport(config, http2=False)
|
||||
get_sync_envd_transport(config, http2=False)
|
||||
|
||||
assert captured == [None, HTTPVersion.HTTP1, HTTPVersion.HTTP1]
|
||||
finally:
|
||||
reset_sync_api_transports()
|
||||
|
||||
|
||||
def test_sync_api_client_applies_request_timeout(test_api_key):
|
||||
reset_sync_api_transports()
|
||||
config = ConnectionConfig(api_key=test_api_key, request_timeout=1.5)
|
||||
@@ -219,6 +282,54 @@ async def test_async_get_transport_keyed_by_proxy(test_api_key):
|
||||
reset_async_api_transports()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_transports_keyed_by_http_version(test_api_key):
|
||||
reset_async_api_transports()
|
||||
config = ConnectionConfig(api_key=test_api_key)
|
||||
|
||||
try:
|
||||
negotiated = get_async_transport(config)
|
||||
http1 = get_async_transport(config, http2=False)
|
||||
envd_negotiated = get_async_envd_transport(config)
|
||||
envd_http1 = get_async_envd_transport(config, http2=False)
|
||||
|
||||
assert http1 is not negotiated
|
||||
assert envd_http1 is not envd_negotiated
|
||||
assert envd_http1 is not http1
|
||||
assert get_async_transport(config, http2=False) is http1
|
||||
assert get_async_transport(config) is negotiated
|
||||
assert get_async_envd_transport(config, http2=False) is envd_http1
|
||||
assert (
|
||||
get_async_envd_transport(config, http2=False, for_streaming=True)
|
||||
is not envd_http1
|
||||
)
|
||||
finally:
|
||||
reset_async_api_transports()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_transports_pass_http_version_to_pyqwest(test_api_key, monkeypatch):
|
||||
reset_async_api_transports()
|
||||
config = ConnectionConfig(api_key=test_api_key)
|
||||
captured = []
|
||||
build_transport = client_async.HTTPTransport
|
||||
|
||||
def record(**kwargs):
|
||||
captured.append(kwargs["http_version"])
|
||||
return build_transport(**kwargs)
|
||||
|
||||
monkeypatch.setattr(client_async, "HTTPTransport", record)
|
||||
|
||||
try:
|
||||
get_async_transport(config)
|
||||
get_async_transport(config, http2=False)
|
||||
get_async_envd_transport(config, http2=False)
|
||||
|
||||
assert captured == [None, HTTPVersion.HTTP1, HTTPVersion.HTTP1]
|
||||
finally:
|
||||
reset_async_api_transports()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_api_client_is_shared_across_loops(test_api_key):
|
||||
# pyqwest's I/O runs on its own Rust runtime, so neither the transport
|
||||
@@ -591,6 +702,48 @@ async def test_async_api_client_body_timeout_raises_httpx_read_timeout(
|
||||
reset_async_api_transports()
|
||||
|
||||
|
||||
def test_sync_http1_transport_round_trips(test_api_key, echo_server, caplog):
|
||||
# The HTTP/1.1-pinned transport is functional, not just configured: pinning
|
||||
# a version reqwest can't use for a request would fail at connect time.
|
||||
reset_sync_api_transports()
|
||||
config = ConnectionConfig(api_key=test_api_key)
|
||||
client = httpx.Client(
|
||||
base_url=echo_server, transport=get_sync_transport(config, http2=False)
|
||||
)
|
||||
|
||||
try:
|
||||
with caplog.at_level(logging.DEBUG, logger="pyqwest.access"):
|
||||
response = client.get("/sandboxes")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["path"] == "/sandboxes"
|
||||
# pyqwest logs the response's version (the stdlib test server answers
|
||||
# HTTP/1.0); `httpx.Response.http_version` is not meaningful through the
|
||||
# adapter, which reports HTTP/1.1 either way.
|
||||
assert f'GET {echo_server}/sandboxes "HTTP/1.0 200 OK"' in caplog.text
|
||||
finally:
|
||||
client.close()
|
||||
reset_sync_api_transports()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_http1_transport_round_trips(test_api_key, echo_server):
|
||||
reset_async_api_transports()
|
||||
config = ConnectionConfig(api_key=test_api_key)
|
||||
client = httpx.AsyncClient(
|
||||
base_url=echo_server, transport=get_async_transport(config, http2=False)
|
||||
)
|
||||
|
||||
try:
|
||||
response = await client.get("/sandboxes")
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["path"] == "/sandboxes"
|
||||
finally:
|
||||
await client.aclose()
|
||||
reset_async_api_transports()
|
||||
|
||||
|
||||
def test_sync_transport_sends_multipart_bodies(test_api_key, echo_server):
|
||||
# `files=` uploads (envd `files.write`) go out as httpx's MultipartStream,
|
||||
# which implements both SyncByteStream and AsyncByteStream. The adapter's
|
||||
|
||||
Reference in New Issue
Block a user