diff --git a/.changeset/dark-adults-join.md b/.changeset/dark-adults-join.md new file mode 100644 index 0000000000..aeb6d92130 --- /dev/null +++ b/.changeset/dark-adults-join.md @@ -0,0 +1,5 @@ +--- +'@e2b/python-sdk': minor +--- + +http2 support for python sdk diff --git a/packages/python-sdk/e2b/api/__init__.py b/packages/python-sdk/e2b/api/__init__.py index d3dac5bb01..c30b037e96 100644 --- a/packages/python-sdk/e2b/api/__init__.py +++ b/packages/python-sdk/e2b/api/__init__.py @@ -132,16 +132,19 @@ def __init__( kwargs.pop("auth_header_name", None) kwargs.pop("prefix", None) + httpx_args = { + "event_hooks": { + "request": [self._log_request], + "response": [self._log_response], + }, + "transport": transport, + } + if transport is None: + httpx_args["proxy"] = config.proxy + super().__init__( base_url=config.api_url, - httpx_args={ - "event_hooks": { - "request": [self._log_request], - "response": [self._log_response], - }, - "proxy": config.proxy, - "transport": transport, - }, + httpx_args=httpx_args, headers=headers, token=token or "", auth_header_name=auth_header_name, diff --git a/packages/python-sdk/e2b/api/client_async/__init__.py b/packages/python-sdk/e2b/api/client_async/__init__.py index 1e97a688b8..0f0c595495 100644 --- a/packages/python-sdk/e2b/api/client_async/__init__.py +++ b/packages/python-sdk/e2b/api/client_async/__init__.py @@ -46,6 +46,7 @@ def get_transport(config: ConnectionConfig) -> AsyncTransportWithLogger: transport = AsyncTransportWithLogger( limits=limits, proxy=config.proxy, + http2=True, ) AsyncTransportWithLogger._instances[loop_id] = transport return transport diff --git a/packages/python-sdk/e2b/api/client_sync/__init__.py b/packages/python-sdk/e2b/api/client_sync/__init__.py index f3d62ca729..a2d77278be 100644 --- a/packages/python-sdk/e2b/api/client_sync/__init__.py +++ b/packages/python-sdk/e2b/api/client_sync/__init__.py @@ -45,6 +45,7 @@ def get_transport(config: ConnectionConfig) -> TransportWithLogger: transport = TransportWithLogger( limits=limits, proxy=config.proxy, + http2=True, ) TransportWithLogger.singleton = transport return transport diff --git a/packages/python-sdk/poetry.lock b/packages/python-sdk/poetry.lock index ea9aeec13a..8a5186b132 100644 --- a/packages/python-sdk/poetry.lock +++ b/packages/python-sdk/poetry.lock @@ -518,6 +518,34 @@ files = [ {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, ] +[[package]] +name = "h2" +version = "4.3.0" +description = "Pure-Python HTTP/2 protocol implementation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"}, + {file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"}, +] + +[package.dependencies] +hpack = ">=4.1,<5" +hyperframe = ">=6.1,<7" + +[[package]] +name = "hpack" +version = "4.1.0" +description = "Pure-Python HPACK header encoding" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, + {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -565,6 +593,18 @@ http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "hyperframe" +version = "6.1.0" +description = "Pure-Python HTTP/2 framing" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, + {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, +] + [[package]] name = "idna" version = "3.11" @@ -1819,4 +1859,4 @@ tomli = {version = ">=2.0.1", markers = "python_version < \"3.11\""} [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "3bbcdd84cf818e3fa11493c20ada01141c2450ad4ebdfcc5d12c4ba8dbfa86a5" +content-hash = "10b34c02d9b97fb1fd229107629913601c669a7d81fc558d11dce61a92812743" diff --git a/packages/python-sdk/pyproject.toml b/packages/python-sdk/pyproject.toml index 478959a44e..f7e6a76580 100644 --- a/packages/python-sdk/pyproject.toml +++ b/packages/python-sdk/pyproject.toml @@ -16,6 +16,7 @@ wcmatch = "^10.1" protobuf = ">=4.21.0" httpcore = "^1.0.5" httpx = ">=0.27.0, <1.0.0" +h2 = ">=4,<5" attrs = ">=23.2.0" packaging = ">=24.1" typing-extensions = ">=4.1.0" diff --git a/packages/python-sdk/tests/test_api_client_transport.py b/packages/python-sdk/tests/test_api_client_transport.py new file mode 100644 index 0000000000..ce157e1c26 --- /dev/null +++ b/packages/python-sdk/tests/test_api_client_transport.py @@ -0,0 +1,49 @@ +import asyncio + +import pytest + +from e2b.api.client_async import AsyncTransportWithLogger +from e2b.api.client_async import get_api_client as get_async_api_client +from e2b.api.client_sync import TransportWithLogger +from e2b.api.client_sync import get_api_client as get_sync_api_client +from e2b.connection_config import ConnectionConfig + + +def test_sync_api_client_proxy_uses_explicit_transport(): + TransportWithLogger.singleton = None + config = ConnectionConfig( + api_key="test", + proxy="http://127.0.0.1:9999", + ) + + api_client = get_sync_api_client(config) + httpx_client = api_client.get_httpx_client() + + try: + assert "proxy" not in api_client._httpx_args + assert httpx_client._transport is TransportWithLogger.singleton + assert httpx_client._mounts == {} + finally: + httpx_client.close() + TransportWithLogger.singleton = None + + +@pytest.mark.asyncio +async def test_async_api_client_proxy_uses_explicit_transport(): + AsyncTransportWithLogger._instances.clear() + config = ConnectionConfig( + api_key="test", + proxy="http://127.0.0.1:9999", + ) + + api_client = get_async_api_client(config) + httpx_client = api_client.get_async_httpx_client() + transport = AsyncTransportWithLogger._instances[id(asyncio.get_running_loop())] + + try: + assert "proxy" not in api_client._httpx_args + assert httpx_client._transport is transport + assert httpx_client._mounts == {} + finally: + await httpx_client.aclose() + AsyncTransportWithLogger._instances.clear()