Skip to content

feat(python-sdk): move the envd HTTP API client onto pyqwest - #1623

Draft
mishushakov wants to merge 2 commits into
migrate-template-uploads-to-pyqwestfrom
migrate-envd-api-to-pyqwest
Draft

feat(python-sdk): move the envd HTTP API client onto pyqwest#1623
mishushakov wants to merge 2 commits into
migrate-template-uploads-to-pyqwestfrom
migrate-envd-api-to-pyqwest

Conversation

@mishushakov

@mishushakov mishushakov commented Jul 29, 2026

Copy link
Copy Markdown
Member

What

Tracked in SDK-265 (part of the SDK-268 stack). Stacked on #1603, at the top of the pyqwest stack (#1601#1602#1603 → this). Migrate the envd HTTP API client — sandbox file transfers (files.read/write), health checks — from httpx-native transports to pyqwest via the httpx adapter, and dedupe the transport plumbing that #1558 (envd RPC) and #1601 (REST) each carried a copy of. With this, all Python SDK traffic runs on pyqwest: REST control plane (#1601), envd RPC (#1558, connectrpc), envd HTTP API (this PR); the volume content client (#1602) and template build uploads (#1603) sit below this one in the stack.

How

Shared plumbing (first commit): e2b.api becomes the canonical home for the proxy narrowing (proxy_to_config, with stack-neutral error messages), the pool tuning, and the flavor ConnectionRetryTransport + a new retrying_http_transport(proxy, read_timeout=None) factory; e2b.envd.client_sync/client_async import them instead of defining their own (envd RPC behavior unchanged, pools stay separate — unification is SDK-291).

envd HTTP API (second commit):

  • get_envd_transport(config, for_streaming=False) returns pyqwest-adapter transports cached per (proxy, streaming); get_envd_api(config, base_url, for_streaming=False) builds the httpx client with sandbox headers + logging hooks. The per-thread (sync) / per-loop (async) client caching in Filesystem/Commands/Pty/AsyncSandbox is gone — one shared client per module, same rationale as the ApiClient simplification in feat(python-sdk): move the REST API client onto pyqwest's httpx transport adapter #1601.

  • Streamed downloads: the streaming transport carries a 60s read_timeout — an idle bound that resets on every read, capping stalls without limiting total transfer time. It gets a dedicated pool because reqwest's read timer keeps ticking while a request body is sent and while waiting for the response head, so on the shared transport it would cut off uploads and slow unary responses. An explicit request_timeout becomes the whole-transfer deadline (adapter semantics) and is sent only when the caller set one; stream_idle_timeout stays honored on the async client via wait_for per read (so values above 60s work and 0 disables), and is documented as ignored on the sync client, which cannot interrupt a blocking read. Mirrors feat(python-sdk): move the volume content client onto pyqwest #1602's volume design.

  • Uploads: buffered uploads keep request_timeout as a whole-request deadline; streamed (file-like) uploads carry no client-side timeout and are bounded server-side (envd's idle read timeout) — both exactly the JS SDK's behavior (getSignal for buffered, no signal for streams).

  • Multipart: files= uploads go out as httpx's MultipartStream, which implements both SyncByteStream and AsyncByteStream. The pyqwest 0.7 adapter's sync content conversion matched AsyncByteStream first and raised TypeError("unreachable") from inside the body iterator, surfacing as a WriteError mid-request ("http2 error: stream error sent by user"). Fixed upstream in pyqwest#196, which matches the sync case first — so this PR needs pyqwest 0.8 (required by feat(python-sdk): move the REST API client onto pyqwest's httpx transport adapter #1601 already) and carries no workaround. The regression test stays, now covering the upstream fix.

  • The stream readers map the transport's idle timeout (builtin TimeoutError under pyqwest) to the documented httpx.ReadTimeout; handle_envd_api_transport_exception's health-probe path keeps working because the adapter maps HTTP/2 stream resets to httpx.RemoteProtocolError.

  • RPC logging: the LoggingInterceptor docstring no longer promises its own removal. pyqwest 0.8 does log requests (pyqwest#197), but on process-wide pyqwest/pyqwest.access loggers that can't carry the per-sandbox logger and don't see streamed messages or the Connect error code of a stream that fails inside a 200 OK — so the interceptor stays, with those loggers below it. pyqwest#192, the middleware it referenced, was closed in favor of Sync is da way #197.

Testing

  • Unit: envd transport keying (streaming vs regular vs REST pools), get_envd_api wiring (headers, transports), multipart regression through a local server, stream-reader timeout mapping + per-read idle bound (tests/test_file_stream_reader.py), rewritten client-lifecycle tests (shared across threads). 236 unit tests green; lint + typecheck green.
  • Integration against production sandboxes: full files suites sync+async (123 tests — these caught the multipart bug), commands + pty suites both flavors (57 tests). All green.

Usage example

No API changes:

sbx = Sandbox.create()
sbx.files.write("hello.txt", "hi")            # multipart/octet-stream over pyqwest
with sbx.files.read("hello.txt", format="stream") as stream:
    for chunk in stream:                       # stalls bounded by 60s idle read timeout
        ...

Only visible behavior shift: on the sync client, files.read(..., format="stream", stream_idle_timeout=...) is now a documented no-op (the transport-wide 60s idle bound applies); the async client honors it as before.

🤖 Generated with Claude Code

@cla-bot cla-bot Bot added the cla-signed label Jul 29, 2026
@changeset-bot

changeset-bot Bot commented Jul 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 77e563b

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@e2b/python-sdk Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@cursor

cursor Bot commented Jul 29, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Core sandbox I/O and timeout behavior change under the same public API; sync stream idle timeout is effectively a behavior change for callers who relied on stream_idle_timeout.

Overview
The envd HTTP client for file transfers and health checks now uses pyqwest through the httpx adapter instead of httpx-native transports. Shared retry and pool setup lives in e2b.api (retrying_http_transport, proxy_to_config); envd RPC reuses that factory. Per-thread sync and per-loop async envd httpx clients are removed in favor of process-global get_envd_api / get_envd_transport, with a separate streaming pool that applies a 60s idle read timeout on downloads.

Streamed files.read treats an explicit request_timeout as the full transfer deadline; default downloads are not capped in total time. Async stream_idle_timeout still works via per-read wait_for; sync documents that parameter as ignored. Buffered uploads keep request_timeout; file-like streamed uploads use no client timeout. Stream readers map pyqwest TimeoutError to httpx.ReadTimeout. httpx.Proxy custom headers are accepted via proxy_to_config for RPC as well as REST.

Reviewed by Cursor Bugbot for commit 77e563b. Bugbot is set up for automated code reviews on this repo. Configure here.

@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Package Artifacts

Built from d7b10db. Download artifacts from this workflow run.

JS SDK (e2b@2.36.2-migrate-envd-api-to-pyqwest.0):

npm install ./e2b-2.36.2-migrate-envd-api-to-pyqwest.0.tgz

CLI (@e2b/cli@2.16.1-migrate-envd-api-to-pyqwest.0):

npm install ./e2b-cli-2.16.1-migrate-envd-api-to-pyqwest.0.tgz

Python SDK (e2b==2.35.0+migrate.envd.api.to.pyqwest):

pip install ./e2b-2.35.0+migrate.envd.api.to.pyqwest-py3-none-any.whl

@linear-code

linear-code Bot commented Jul 30, 2026

Copy link
Copy Markdown

SDK-265

SDK-268

SDK-302

Comment thread packages/python-sdk/e2b/sandbox_sync/filesystem/filesystem.py
@mishushakov
mishushakov changed the base branch from migrate-rest-to-pyqwest to migrate-template-uploads-to-pyqwest July 30, 2026 16:35
@mishushakov
mishushakov force-pushed the migrate-envd-api-to-pyqwest branch from d34b5b1 to b86e23b Compare July 30, 2026 16:35
@mishushakov
mishushakov force-pushed the migrate-envd-api-to-pyqwest branch from b86e23b to f9cf7f2 Compare July 30, 2026 16:36
mishushakov and others added 2 commits July 31, 2026 16:55
…nvd RPC stack

With the envd RPC migration (#1558) on main, both stacks carried identical
copies of the transport pieces. Make e2b.api the canonical home: proxy_to_url
(with stack-neutral error messages) and the pool tuning move out of
e2b.envd.client_shared, and the flavor modules gain a shared
retrying_http_transport factory + ConnectionRetryTransport that
e2b.envd.client_sync/client_async now import instead of defining their own.
The stacks keep separate per-proxy pools (unification is a follow-up).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Swap the per-thread/per-loop envd httpx clients (file transfers, health
checks) for shared pyqwest-backed ones built from the same transport pieces
as the REST client and envd RPC. Streamed downloads default to a dedicated
transport whose 60s idle read timeout bounds stalls (reqwest's read timer
ticks during body send/TTFB, so it can't live on the shared transport);
explicit request timeouts become whole-transfer deadlines and streamed
uploads carry no client-side timeout, matching the JS SDK. Present httpx's
dual sync/async MultipartStream as sync-only — the stock adapter matches
AsyncByteStream first and kills multipart uploads mid-request.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mishushakov
mishushakov force-pushed the migrate-envd-api-to-pyqwest branch from f9cf7f2 to 77e563b Compare July 31, 2026 14:56

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 77e563b. Configure here.

# global idle cap, but API traffic goes to a single host, so the values map
# directly; reqwest has no counterpart to `E2B_MAX_CONNECTIONS` (it does not
# cap concurrent connections).
# Mirror the httpx pool tuning above with pyqwest's equivalents, shared by

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused httpx pool limits

Low Severity

limits is no longer referenced anywhere after the envd httpx transports were removed, so E2B_MAX_CONNECTIONS and the related httpx Limits tuning are dead. That makes the leftover env-var surface look effective when it is not.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 77e563b. Configure here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant