|
4 | 4 | import re |
5 | 5 | from dataclasses import dataclass |
6 | 6 | from types import TracebackType |
7 | | -from typing import Optional, Protocol, Union |
| 7 | +from typing import NamedTuple, Optional, Protocol, Tuple, Union |
8 | 8 |
|
9 | 9 | import httpx |
10 | 10 | from httpx import AsyncBaseTransport, BaseTransport, Limits, Timeout |
11 | 11 | from httpx._types import ProxyTypes |
| 12 | +from pyqwest import Proxy |
12 | 13 |
|
13 | 14 | from e2b.api.client.client import AuthenticatedClient |
14 | 15 | from e2b.api.client.types import Response |
@@ -78,37 +79,50 @@ async def on_response(response: Response) -> None: |
78 | 79 | pool_max_idle_per_host = int(os.getenv("E2B_MAX_KEEPALIVE_CONNECTIONS") or "20") |
79 | 80 |
|
80 | 81 |
|
81 | | -def proxy_to_url(proxy: Optional[ProxyTypes]) -> Optional[str]: |
82 | | - """Narrow the ``proxy`` connection option to the proxy URL string pyqwest |
83 | | - transports take (scheme http, https, socks5, or socks5h, credentials in |
84 | | - the URL userinfo). ``httpx.URL`` and ``httpx.Proxy`` — which the httpx |
85 | | - transports accepted — are converted when they reduce to such a URL; |
86 | | - ``httpx.Proxy`` extras that don't (custom headers, an ssl_context) are |
87 | | - rejected rather than silently dropped.""" |
| 82 | +class ProxyConfig(NamedTuple): |
| 83 | + """The ``proxy`` connection option in the shape pyqwest transports take. |
| 84 | +
|
| 85 | + A tuple so it can key the transport caches directly: it is hashable and |
| 86 | + compares by value, where a ``pyqwest.Proxy`` compares by identity and |
| 87 | + would hand every call its own connection pool.""" |
| 88 | + |
| 89 | + url: str |
| 90 | + auth: Optional[Tuple[str, str]] = None |
| 91 | + headers: Tuple[Tuple[str, str], ...] = () |
| 92 | + |
| 93 | + def to_pyqwest(self) -> Proxy: |
| 94 | + """The ``pyqwest.Proxy`` to hand a transport.""" |
| 95 | + return Proxy(self.url, auth=self.auth, headers=self.headers or None) |
| 96 | + |
| 97 | + |
| 98 | +def proxy_to_config(proxy: Optional[ProxyTypes]) -> Optional[ProxyConfig]: |
| 99 | + """Convert the ``proxy`` connection option — a URL string, an |
| 100 | + ``httpx.URL``, or an ``httpx.Proxy`` — to the proxy configuration pyqwest |
| 101 | + transports take: a proxy URL (scheme http, https, socks5, or socks5h, |
| 102 | + credentials allowed in the userinfo), basic-auth credentials, and headers |
| 103 | + to send to the proxy. An ``httpx.Proxy`` ``ssl_context`` has no pyqwest |
| 104 | + counterpart and is rejected rather than silently dropped.""" |
88 | 105 | if proxy is None: |
89 | 106 | return None |
90 | 107 | if isinstance(proxy, str): |
91 | | - return proxy |
| 108 | + return ProxyConfig(proxy) |
92 | 109 | if isinstance(proxy, httpx.URL): |
93 | | - return str(proxy) |
| 110 | + return ProxyConfig(str(proxy)) |
94 | 111 | if isinstance(proxy, httpx.Proxy): |
95 | | - if proxy.headers: |
96 | | - raise InvalidArgumentException( |
97 | | - "E2B API calls don't support httpx.Proxy custom headers; " |
98 | | - "pass credentials in the proxy URL instead, " |
99 | | - 'e.g. proxy="http://user:pass@localhost:8030"' |
100 | | - ) |
101 | 112 | if proxy.ssl_context is not None: |
102 | 113 | raise InvalidArgumentException( |
103 | 114 | "E2B API calls don't support httpx.Proxy ssl_context" |
104 | 115 | ) |
105 | | - url = proxy.url |
106 | | - if proxy.auth is not None: |
107 | | - url = url.copy_with(username=proxy.auth[0], password=proxy.auth[1]) |
108 | | - return str(url) |
| 116 | + # httpx.Proxy splits userinfo out of the URL into `.auth`; pyqwest |
| 117 | + # takes the credentials the same way, so they pass straight through. |
| 118 | + return ProxyConfig( |
| 119 | + str(proxy.url), |
| 120 | + auth=proxy.auth, |
| 121 | + headers=tuple(proxy.headers.items()), |
| 122 | + ) |
109 | 123 | raise InvalidArgumentException( |
110 | | - "E2B API calls support only URL-string proxies, " |
111 | | - 'e.g. proxy="http://user:pass@localhost:8030"' |
| 124 | + "E2B API calls support only URL-string, httpx.URL, and httpx.Proxy " |
| 125 | + 'proxies, e.g. proxy="http://user:pass@localhost:8030"' |
112 | 126 | ) |
113 | 127 |
|
114 | 128 |
|
|
0 commit comments