Skip to content
Closed
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/warm-sockets-keep-python.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@e2b/python-sdk': patch
---

Enable TCP keepalive with a 60-second initial delay where supported across Python SDK HTTP transports, including API, sandbox, volume, proxy, and template upload connections. Environment proxies (`HTTP_PROXY`/`HTTPS_PROXY`/`NO_PROXY`) are now honored by these transports, including template uploads.
203 changes: 199 additions & 4 deletions packages/python-sdk/e2b/api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,22 @@
import logging
import os
import re
import socket

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add a changeset for the Python SDK update

This commit changes packages/python-sdk, but the diff does not add or update any .changeset/*.md entry, so the release tooling has no patch note/version bump for the transport fix and consumers may not receive it in the next package release. Please add a changeset for the Python SDK change.

Useful? React with 👍 / 👎.

import sys
import threading
import weakref
from dataclasses import dataclass
from types import TracebackType
from typing import Callable, Optional, Protocol, Union
from typing import Any, Callable, Iterable, Optional, Protocol, TypeVar, Union, cast

import httpcore

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 This PR modifies packages/python-sdk (TCP keepalive on API/envd transports) but adds no .changeset entry, which CLAUDE.md explicitly requires for changes to packages/python-sdk. The changeset-bot comment on the PR already flags 'No Changeset found' — merging as-is won't trigger a version bump/release for @e2b/python-sdk despite the behavioral change.

Extended reasoning...

Bug

CLAUDE.md states explicitly: "Generate a changeset when updating packages/cli, packages/js-sdk, packages/python-sdk with pnpm changeset in the repository root." This PR touches three files under packages/python-sdk/e2b/api/ (__init__.py, client_async/__init__.py, client_sync/__init__.py) with a real behavioral change — enabling TCP keepalive socket options and rewiring the HTTP transports/proxy-mount logic — yet no changeset file accompanies the change.

Where this shows up

The repo already surfaces this: the changeset-bot[bot] comment on the PR timeline reads "No Changeset found... Merging this PR will not cause a version bump for any packages... This PR includes no changesets", and even links a suggested stub with "@e2b/python-sdk": patch. Checking .changeset/ in the repo confirms only two files exist (olive-pugs-refuse.md, tidy-moons-shout.md), and both were added by the prior, unrelated JS PR (#1607, commit b511953) targeting the e2b (JS) package — neither references @e2b/python-sdk or this PR's commit (3b1bd9b).

Why nothing else catches this

There is no CI gate in this repo that blocks merge on a missing changeset — the changeset-bot comment is advisory only, and nothing in the PR pipeline enforces the CLAUDE.md rule mechanically. It relies on the author (or reviewer) remembering to run pnpm changeset before merge.

Impact

@e2b/python-sdk participates in changeset-driven versioning (it is not in the changeset config.json ignore list) and is not on the shared ignore path with e2b (JS). Without a changeset, the next release run won't bump the python-sdk's version, so the keepalive fix (which addresses real long-lived-connection idle-drop behavior) will silently sit unreleased on main until someone notices and files it manually. Users depending on published e2b Python SDK versions won't receive the fix until that gap is caught.

Proof / how to reproduce

  1. Run git log --oneline -1 on this PR's tip — commit 3b1bd9b — and git show --stat 3b1bd9b: no file under .changeset/ appears in the diff.
  2. List .changeset/*.md in the repo: only olive-pugs-refuse.md and tidy-moons-shout.md exist, both predating this PR and both scoped to "e2b" (the JS package), not "@e2b/python-sdk".
  3. Read the changeset-bot[bot] PR comment: it independently confirms "No Changeset found" and even suggests the exact fix — a new file with frontmatter "@e2b/python-sdk": patch.
  4. Cross-reference CLAUDE.md's explicit instruction naming packages/python-sdk as one of the three packages requiring a changeset on update.

Fix

Run pnpm changeset in the repo root and select @e2b/python-sdk with a patch bump (this is a backward-compatible behavioral fix, not a breaking change), describing the keepalive change, before merging.

import httpx
from httpx import AsyncBaseTransport, BaseTransport, Limits, Timeout

# NOTE: httpx private API. Kept in sync with the `httpx>=0.27.0,<1.0.0` pin in
# pyproject.toml — re-verify this import when bumping httpx.
from httpx._utils import get_environment_proxies

from e2b.api.client.client import AuthenticatedClient
from e2b.api.client.types import Response
from e2b.api.metadata import default_headers
Expand Down Expand Up @@ -71,6 +78,183 @@ async def on_response(response: Response) -> None:
connection_retries = int(os.getenv("E2B_CONNECTION_RETRIES") or "3")


def _get_socket_options(
platform: str,
tcp_keepidle: Optional[int],
tcp_keepalive: Optional[int],
) -> tuple[tuple[int, int, int], ...]:
"""Build platform-specific TCP keepalive options for httpcore.

The 60-second initial-delay tuning uses ``TCP_KEEPIDLE`` where the
constant is available (Linux and other platforms exposing it) or macOS's
``TCP_KEEPALIVE``. Windows CPython also defines ``TCP_KEEPIDLE``, but
setting it raises ``OSError`` on Windows releases older than 10 1709, so
Windows only enables ``SO_KEEPALIVE`` and keeps the OS-default probe
timing. Platforms without either constant likewise fall back to
enabling keepalive only.
"""
options = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]
if platform == "win32":
return tuple(options)
if tcp_keepidle is not None:
options.append((socket.IPPROTO_TCP, tcp_keepidle, 60))
elif platform == "darwin" and tcp_keepalive is not None:
options.append((socket.IPPROTO_TCP, tcp_keepalive, 60))
return tuple(options)


_TCP_KEEPALIVE_SOCKET_OPTIONS = _get_socket_options(
sys.platform,
getattr(socket, "TCP_KEEPIDLE", None),
getattr(socket, "TCP_KEEPALIVE", None),
)


class _TCPKeepaliveNetworkBackend(httpcore.NetworkBackend):
"""Force TCP keepalive options through direct and proxy connections."""

def __init__(self, backend: httpcore.NetworkBackend):
self._backend = backend

def connect_tcp(
self,
host: str,
port: int,
timeout: Optional[float] = None,
local_address: Optional[str] = None,
socket_options: Optional[Iterable[tuple]] = None,
) -> httpcore.NetworkStream:
return self._backend.connect_tcp(
host,
port,
timeout=timeout,
local_address=local_address,
socket_options=_TCP_KEEPALIVE_SOCKET_OPTIONS,
)

def connect_unix_socket(
self,
path: str,
timeout: Optional[float] = None,
socket_options: Optional[Iterable[tuple]] = None,
) -> httpcore.NetworkStream:
return self._backend.connect_unix_socket(
path,
timeout=timeout,
socket_options=socket_options,
)


class _TCPKeepaliveAsyncNetworkBackend(httpcore.AsyncNetworkBackend):
"""Async counterpart of :class:`_TCPKeepaliveNetworkBackend`."""

def __init__(self, backend: httpcore.AsyncNetworkBackend):
self._backend = backend

async def connect_tcp(
self,
host: str,
port: int,
timeout: Optional[float] = None,
local_address: Optional[str] = None,
socket_options: Optional[Iterable[tuple]] = None,
) -> httpcore.AsyncNetworkStream:
return await self._backend.connect_tcp(
host,
port,
timeout=timeout,
local_address=local_address,
socket_options=_TCP_KEEPALIVE_SOCKET_OPTIONS,
)

async def connect_unix_socket(
self,
path: str,
timeout: Optional[float] = None,
socket_options: Optional[Iterable[tuple]] = None,
) -> httpcore.AsyncNetworkStream:
return await self._backend.connect_unix_socket(
path,
timeout=timeout,
socket_options=socket_options,
)

async def sleep(self, seconds: float) -> None:
await self._backend.sleep(seconds)


class _TCPKeepaliveHTTPTransport(httpx.HTTPTransport):
"""HTTPX transport that also covers HTTPcore proxy sockets."""

def __init__(self, *args, **kwargs):
kwargs["socket_options"] = _TCP_KEEPALIVE_SOCKET_OPTIONS
super().__init__(*args, **kwargs)
# NOTE: `_pool` / `_network_backend` are httpx/httpcore private API.
# HTTPcore 1.0.x drops `socket_options` when it builds proxy
# connections (HTTPProxy/SOCKSProxy), so wrap the backend to force
# the keepalive options at actual TCP connect time. Verified against
# the `httpcore>=1.0.5,<2.0.0` pin in pyproject.toml — re-check on bumps.
pool = cast(Any, self._pool)
pool._network_backend = _TCPKeepaliveNetworkBackend(pool._network_backend)


class _TCPKeepaliveAsyncHTTPTransport(httpx.AsyncHTTPTransport):
"""Async HTTPX transport that also covers HTTPcore proxy sockets."""

def __init__(self, *args, **kwargs):
kwargs["socket_options"] = _TCP_KEEPALIVE_SOCKET_OPTIONS
super().__init__(*args, **kwargs)
# NOTE: `_pool` / `_network_backend` are httpx/httpcore private API —
# see _TCPKeepaliveHTTPTransport for the rationale and version pins.
Comment on lines +192 to +208

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 This PR imports httpcore directly and subclasses httpcore.NetworkBackend/AsyncNetworkBackend, but httpcore isn't declared as a direct dependency in pyproject.toml (only httpx>=0.27.0,<1.0.0 is pinned there) — it's only pulled in transitively via httpx. The inline comment on _TCPKeepaliveHTTPTransport also claims this was 'verified against the httpcore>=1.0.5,<2 pin in pyproject.toml,' but no such pin exists, which will mislead future maintainers checking compatibility on httpx/httpcore bumps. Consider adding an explicit httpcore dependency pin and correcting the comment.

Extended reasoning...

What the bug is: packages/python-sdk/e2b/api/__init__.py adds import httpcore and defines _TCPKeepaliveNetworkBackend(httpcore.NetworkBackend) / _TCPKeepaliveAsyncNetworkBackend(httpcore.AsyncNetworkBackend), referencing httpcore.NetworkStream / httpcore.AsyncNetworkStream as return types. This is a genuine new direct usage of httpcore's API surface (specifically its network-backend abstraction, which is not part of httpx's public/stable API contract). Despite this, packages/python-sdk/pyproject.toml's dependencies list only contains httpx>=0.27.0,<1.0.0 — there is no httpcore entry at all.

Where it manifests: Lines ~195-196 and ~205 of the diff contain comments like:

# ... Verified against the httpcore>=1.0.5,<2 pin in pyproject.toml — re-check on bumps.

This comment asserts a specific version constraint (httpcore>=1.0.5,<2) exists in pyproject.toml as the safety net for this private-API usage. I checked pyproject.toml directly (dependencies block, lines 9-22) and confirmed no such pin is present — httpcore doesn't appear anywhere in the dependency list. The comment refers to a constraint that simply isn't there.

Why existing code doesn't prevent it: Today this doesn't break anything at runtime, because every supported version of httpx<1.0.0 transitively requires httpcore 1.x, so the import always resolves and the NetworkBackend/AsyncNetworkBackend classes exist with a stable-enough shape across httpcore 1.x releases. But that safety is accidental/inherited from httpx's own transitive pin, not an explicit guarantee this package controls. If a future httpx release changes its httpcore requirement (e.g., loosens it further or, in a hypothetical httpx 1.x, drops the hard httpcore dependency), there is nothing in this package's own dependency metadata to catch that at install/resolution time — uv/pip would only know about the httpx constraint, not about this code's specific reliance on httpcore.NetworkBackend's current shape.

Impact: Two distinct issues, both low severity but real:

  1. Dependency-hygiene fragility — code that directly imports and subclasses a package's semi-private API should declare that package as a direct dependency so version bumps are caught by the resolver/lockfile diff, rather than relying on it being transitively present.
  2. A factually incorrect comment — it tells future maintainers "check the httpcore pin in pyproject.toml when bumping httpx," but there's no such pin to check, which could cause someone to skip verifying compatibility because they believe a safety net exists that doesn't.

Step-by-step proof:

  1. Open packages/python-sdk/pyproject.toml, dependencies = [...] block (lines 9-22).
  2. Confirm entries: python-dateutil, wcmatch, protobuf-py, httpx>=0.27.0,<1.0.0, h2, attrs, packaging, typing-extensions, dockerfile-parse, rich, connectrpc, pyqwest. No httpcore entry exists.
  3. Open packages/python-sdk/e2b/api/__init__.py, note import httpcore (new in this PR) and the _TCPKeepaliveNetworkBackend(httpcore.NetworkBackend) / _TCPKeepaliveAsyncNetworkBackend(httpcore.AsyncNetworkBackend) classes — direct use of httpcore's API.
  4. Note the comment in _TCPKeepaliveHTTPTransport.__init__: "Verified against the httpcore>=1.0.5,<2 pin in pyproject.toml — re-check on bumps." Search pyproject.toml for httpcore — zero matches. The referenced pin does not exist.
  5. Conclusion: the direct httpcore usage is undeclared, and the comment pointing maintainers to a version pin as the trigger for re-verification is inaccurate, since no such pin is present to trigger off of.

Fix: Add an explicit httpcore>=1.0.5,<2 (or whatever range was actually verified) entry to pyproject.toml's dependencies, and update the comment to either reference the newly-added pin correctly, or state plainly that no pin currently exists and compatibility should be manually re-checked whenever httpx's own httpcore requirement changes.

Severity: This is a nit, not a blocker. It causes no runtime failure today — httpx's own dependency constraints guarantee a compatible httpcore 1.x is always present — so nothing breaks by merging as-is. It's a dependency-hygiene / documentation-accuracy cleanup, appropriate as a follow-up rather than something that should delay this PR.

pool = cast(Any, self._pool)
pool._network_backend = _TCPKeepaliveAsyncNetworkBackend(pool._network_backend)


class _TCPKeepaliveTransport(httpx.BaseTransport, httpx.AsyncBaseTransport):
"""Dual sync/async transport for directly constructed public clients."""

def __init__(self, **kwargs):
self._sync_transport = _TCPKeepaliveHTTPTransport(**kwargs)
self._async_transport = _TCPKeepaliveAsyncHTTPTransport(**kwargs)

def handle_request(self, request: httpx.Request) -> httpx.Response:
return self._sync_transport.handle_request(request)

async def handle_async_request(self, request: httpx.Request) -> httpx.Response:
return await self._async_transport.handle_async_request(request)

def close(self) -> None:
self._sync_transport.close()

async def aclose(self) -> None:
await self._async_transport.aclose()


# Any transport type produced by the factory passed to
# _build_env_proxy_mounts; keeps the returned mount dict precisely typed for
# both sync and async httpx clients.
_TransportT = TypeVar("_TransportT", bound=Union[BaseTransport, AsyncBaseTransport])
Comment on lines +210 to +236

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 This PR adds TCP keepalive to the API and envd transports in e2b/api/__init__.py, but the near-identical TransportWithLogger/AsyncTransportWithLogger classes in e2b/volume/client_sync/__init__.py and e2b/volume/client_async/__init__.py still build plain httpx.HTTPTransport/httpx.AsyncHTTPTransport with no socket_options. Since Volume Content API connections carry file upload/download traffic — exactly the long-lived-connection case keepalive is meant to protect — those sockets remain unprotected from idle NAT/load-balancer drops; consider having them subclass the new _TCPKeepaliveHTTPTransport/_TCPKeepaliveAsyncHTTPTransport classes as a follow-up.

Extended reasoning...

What the gap is: This PR introduces _TCPKeepaliveHTTPTransport and _TCPKeepaliveAsyncHTTPTransport in packages/python-sdk/e2b/api/__init__.py, and rewires the API/envd TransportWithLogger classes in e2b/api/client_sync/__init__.py and e2b/api/client_async/__init__.py to inherit from them instead of the plain httpx transports. That gives every API and envd connection SO_KEEPALIVE (plus a 60s TCP_KEEPIDLE/TCP_KEEPALIVE probe delay where supported), applied even through proxy connections via the custom _TCPKeepaliveNetworkBackend/_TCPKeepaliveAsyncNetworkBackend wrappers.

However, the Volume Content API has its own near-duplicate transport classes that were not touched: packages/python-sdk/e2b/volume/client_sync/__init__.py:58 defines class TransportWithLogger(httpx.HTTPTransport), and packages/python-sdk/e2b/volume/client_async/__init__.py:59 defines class AsyncTransportWithLogger(httpx.AsyncHTTPTransport). Both are constructed with only limits/proxy/retries (no socket_options), and both files already from e2b.api import connection_retries, make_logging_event_hooks (or the async equivalent) — proving they share code with the exact module this PR modifies, yet they were left on the plain httpx base classes.

Why it matters: Volume file upload/download is precisely the long-lived, potentially-idle connection scenario that TCP keepalive is meant to protect against, per this PR's own stated rationale (idle NAT/load-balancer drops). Both volume transports use the same pooling Limits with keepalive_expiry=300, so pooled connections can sit idle for up to 300 seconds — long enough for a NAT/LB to silently drop the socket — with no keepalive probes to detect or prevent that. After this PR merges, API and envd traffic is protected but Volume Content API traffic (uploads/downloads) is not, creating an inconsistency within the same codebase.

Why existing code doesn't prevent it: The volume transports are structurally independent classes — they don't inherit from or share a base with e2b.api.client_sync.TransportWithLogger/e2b.api.client_async.AsyncTransportWithLogger, so the PR's change to those classes has no effect on volume connections. The only coupling is the shared imports of connection_retries and the logging-hook helpers from e2b.api, neither of which carries the new keepalive behavior.

Concrete walk-through: (1) A user calls a Volume upload/download API, which reaches e2b/volume/client_sync/__init__.py:get_api_client. (2) That calls get_transport(config), which constructs TransportWithLogger(limits=limits, proxy=config.proxy, retries=connection_retries) — a plain httpx.HTTPTransport. (3) The underlying httpcore connection pool has keepalive_expiry=300, so an idle connection can be reused for up to 5 minutes without any TCP-level keepalive probes. (4) If a NAT device or load balancer between the client and the Volume Content API silently drops that idle connection before it's reused (a common occurrence well within a 300s window), the next request over that pooled connection will hang or fail with a connection-reset error — the exact failure mode this PR's keepalive change is meant to eliminate for API/envd traffic.

Suggested fix: Have packages/python-sdk/e2b/volume/client_sync/__init__.py's TransportWithLogger subclass e2b.api._TCPKeepaliveHTTPTransport instead of httpx.HTTPTransport, and similarly have packages/python-sdk/e2b/volume/client_async/__init__.py's AsyncTransportWithLogger subclass e2b.api._TCPKeepaliveAsyncHTTPTransport, mirroring exactly what was done for the API/envd transports in this PR.

On severity: This is not a regression — Volume connections had no keepalive before this PR either, so behavior for Volume traffic is unchanged. The PR's stated scope was explicitly "API and envd HTTP transports," and it does not touch the volume/ directory at all. I'm flagging this as a nit / worthwhile follow-up rather than a blocking issue, since merging as-is doesn't introduce new breakage — it just leaves an inconsistency where Volume traffic remains as exposed to idle-connection drops as it always was, right after a sibling code path fixed the identical problem.



def _build_env_proxy_mounts(
transport_factory: Callable[[str], _TransportT],
) -> dict[str, Optional[_TransportT]]:
"""Rebuild httpx's environment-proxy mounts for clients that pass an
explicit ``transport=``.

Passing an explicit transport to an httpx client disables its
``trust_env`` proxy handling entirely, so ``HTTP_PROXY``/``HTTPS_PROXY``/
``ALL_PROXY`` would silently be ignored. This reconstructs the same
mounts httpx would have built, creating a proxied transport per proxy URL
via ``transport_factory``. ``NO_PROXY`` entries map to ``None`` mounts,
which httpx routes to the default (direct) transport.
"""
return {
pattern: None if proxy_url is None else transport_factory(proxy_url)
for pattern, proxy_url in get_environment_proxies().items()
}


@dataclass
class SandboxCreateResponse:
sandbox_id: str
Expand Down Expand Up @@ -222,14 +406,25 @@ def __init__(
httpx_args = {
"event_hooks": self._logging_event_hooks(),
}
if transport is not None:
httpx_args["transport"] = transport
if (
transport is None
and transport_factory is None
and async_transport_factory is None
):
httpx_args["proxy"] = config.proxy
transport_options = {"verify": kwargs.get("verify_ssl", True)}
transport = _TCPKeepaliveTransport(
proxy=config.proxy,
**transport_options,
)
if config.proxy is None:
httpx_args["mounts"] = _build_env_proxy_mounts(
lambda proxy_url: _TCPKeepaliveTransport(
proxy=proxy_url,
**transport_options,
)
)
if transport is not None:
httpx_args["transport"] = transport

# config.request_timeout is None when the timeout is explicitly
# disabled (request_timeout=0), which httpx.Timeout(None) preserves.
Expand Down
28 changes: 18 additions & 10 deletions packages/python-sdk/e2b/api/client_async/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@
import weakref
from typing import Dict, Optional, Tuple

import httpx

from httpx._types import ProxyTypes

from e2b.api import AsyncApiClient, connection_retries, limits
from e2b.api import (
_TCPKeepaliveAsyncHTTPTransport,
AsyncApiClient,
connection_retries,
limits,
)
from e2b.connection_config import ConnectionConfig

TransportKey = Tuple[bool, Optional[ProxyTypes]]
Expand All @@ -20,7 +23,7 @@ def get_api_client(config: ConnectionConfig, **kwargs) -> AsyncApiClient:
)


class AsyncTransportWithLogger(httpx.AsyncHTTPTransport):
class AsyncTransportWithLogger(_TCPKeepaliveAsyncHTTPTransport):
# Keyed weakly by the event loop object itself, not id(loop) — CPython
# reuses object ids, so a new loop could otherwise inherit a transport
# bound to a previous, closed loop.
Expand All @@ -34,6 +37,16 @@ def pool(self):
return self._pool


def _create_transport(cls, config: ConnectionConfig, http2: bool):
"""Build a keepalive transport of the given class for this config."""
return cls(
limits=limits,
proxy=config.proxy,
http2=http2,
retries=connection_retries,
)


def _get_cached_transport(cls, config: ConnectionConfig, http2: bool):
loop = asyncio.get_running_loop()
loop_instances = cls._instances.get(loop)
Expand All @@ -44,12 +57,7 @@ def _get_cached_transport(cls, config: ConnectionConfig, http2: bool):
key: TransportKey = (http2, config.proxy)
transport = loop_instances.get(key)
if transport is None:
transport = cls(
limits=limits,
proxy=config.proxy,
http2=http2,
retries=connection_retries,
)
transport = _create_transport(cls, config, http2)
loop_instances[key] = transport

return transport
Expand Down
34 changes: 19 additions & 15 deletions packages/python-sdk/e2b/api/client_sync/__init__.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from typing import Dict, Optional, Tuple

import httpx
import threading

from httpx._types import ProxyTypes

from e2b.api import ApiClient, connection_retries, limits
from e2b.api import (
_TCPKeepaliveHTTPTransport,
ApiClient,
connection_retries,
limits,
)
from e2b.connection_config import ConnectionConfig

TransportKey = Tuple[bool, Optional[ProxyTypes]]
Expand All @@ -19,14 +23,24 @@ def get_api_client(config: ConnectionConfig, **kwargs) -> ApiClient:
)


class TransportWithLogger(httpx.HTTPTransport):
class TransportWithLogger(_TCPKeepaliveHTTPTransport):
_thread_local = threading.local()

@property
def pool(self):
return self._pool


def _create_transport(cls, config: ConnectionConfig, http2: bool):
"""Build a keepalive transport of the given class for this config."""
return cls(
limits=limits,
proxy=config.proxy,
http2=http2,
retries=connection_retries,
)


def get_transport(config: ConnectionConfig, http2: bool = True) -> TransportWithLogger:
instances: Dict[TransportKey, TransportWithLogger] = getattr(
TransportWithLogger._thread_local, "instances", {}
Expand All @@ -36,12 +50,7 @@ def get_transport(config: ConnectionConfig, http2: bool = True) -> TransportWith
if cached is not None:
return cached

transport = TransportWithLogger(
limits=limits,
proxy=config.proxy,
http2=http2,
retries=connection_retries,
)
transport = _create_transport(TransportWithLogger, config, http2)
instances[key] = transport
TransportWithLogger._thread_local.instances = instances
return transport
Expand All @@ -62,12 +71,7 @@ def get_envd_transport(
if cached is not None:
return cached

transport = EnvdTransportWithLogger(
limits=limits,
proxy=config.proxy,
http2=http2,
retries=connection_retries,
)
transport = _create_transport(EnvdTransportWithLogger, config, http2)
instances[key] = transport
EnvdTransportWithLogger._thread_local.instances = instances
return transport
1 change: 1 addition & 0 deletions packages/python-sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies = [
"wcmatch>=10.1,<11",
"protobuf-py>=0.1.1,<0.2",
"httpx>=0.27.0,<1.0.0",
"httpcore>=1.0.5,<2.0.0",
"h2>=4,<5",
"attrs>=23.2.0",
"packaging>=24.1",
Expand Down
Loading
Loading