Skip to content

Commit 0ec032d

Browse files
author
Patrick J. McNerthney
committed
Implement support for specifying the httpcore Network Backend when creating default transports
Signed-off-by: Patrick J. McNerthney <patrick.mcnerthney@fortra.com>
1 parent ae1b9f6 commit 0ec032d

File tree

3 files changed

+127
-0
lines changed

3 files changed

+127
-0
lines changed

docs/advanced/transports.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,18 @@ connecting via a Unix Domain Socket that is only available via this low-level AP
3535
{"ID": "...", "Containers": 4, "Images": 74, ...}
3636
```
3737

38+
Another advanced configuration is supplying a custom httpcore [Network Backend](https://www.encode.io/httpcore/network-backends/).
39+
40+
```pycon
41+
>>> import httpx
42+
>>> import myk8s
43+
>>> # This custom network backend enables remote access to ports inside a Kubernetes Cluster using pod port forwarding.
44+
>>> backend = myk8s.NetworkBackend('cluster.local')
45+
>>> transport = httpx.HTTPTransport(network_backend=backend)
46+
>>> client = httpx.Client(transport=transport)
47+
>>> response = client.get("http://argocd-server.argocd.svc.cluster.local")
48+
```
49+
3850
## WSGI Transport
3951

4052
You can configure an `httpx` client to call directly into a Python web application using the WSGI protocol.

httpx/_transports/default.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
* uds: str
77
* local_address: str
88
* retries: int
9+
* network_backend: httpcore.NetworkBackend
910
1011
Example usages...
1112
@@ -22,6 +23,13 @@
2223
# Using advanced httpcore configuration, with unix domain sockets.
2324
transport = httpx.HTTPTransport(uds="socket.uds")
2425
client = httpx.Client(transport=transport)
26+
27+
# Using advanced httpcore configuration, with custom network backend.
28+
import myk8s
29+
backend = myk8s.NetworkBackend('cluster.local')
30+
transport = httpx.HTTPTransport(network_backend=backend)
31+
client = httpx.Client(transport=transport)
32+
response = client.get("http://argocd-server.argocd.svc.cluster.local")
2533
"""
2634

2735
from __future__ import annotations
@@ -33,6 +41,8 @@
3341
if typing.TYPE_CHECKING:
3442
import ssl # pragma: no cover
3543

44+
import httpcore # pragma: no cover
45+
3646
import httpx # pragma: no cover
3747

3848
from .._config import DEFAULT_LIMITS, Limits, Proxy, create_ssl_context
@@ -146,6 +156,7 @@ def __init__(
146156
local_address: str | None = None,
147157
retries: int = 0,
148158
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
159+
network_backend: httpcore.NetworkBackend | None = None,
149160
) -> None:
150161
import httpcore
151162

@@ -164,6 +175,7 @@ def __init__(
164175
local_address=local_address,
165176
retries=retries,
166177
socket_options=socket_options,
178+
network_backend=network_backend,
167179
)
168180
elif proxy.url.scheme in ("http", "https"):
169181
self._pool = httpcore.HTTPProxy(
@@ -183,6 +195,7 @@ def __init__(
183195
http1=http1,
184196
http2=http2,
185197
socket_options=socket_options,
198+
network_backend=network_backend,
186199
)
187200
elif proxy.url.scheme in ("socks5", "socks5h"):
188201
try:
@@ -207,6 +220,7 @@ def __init__(
207220
keepalive_expiry=limits.keepalive_expiry,
208221
http1=http1,
209222
http2=http2,
223+
network_backend=network_backend,
210224
)
211225
else: # pragma: no cover
212226
raise ValueError(
@@ -290,6 +304,7 @@ def __init__(
290304
local_address: str | None = None,
291305
retries: int = 0,
292306
socket_options: typing.Iterable[SOCKET_OPTION] | None = None,
307+
network_backend: httpcore.AsyncNetworkBackend | None = None,
293308
) -> None:
294309
import httpcore
295310

@@ -308,6 +323,7 @@ def __init__(
308323
local_address=local_address,
309324
retries=retries,
310325
socket_options=socket_options,
326+
network_backend=network_backend,
311327
)
312328
elif proxy.url.scheme in ("http", "https"):
313329
self._pool = httpcore.AsyncHTTPProxy(
@@ -327,6 +343,7 @@ def __init__(
327343
http1=http1,
328344
http2=http2,
329345
socket_options=socket_options,
346+
network_backend=network_backend,
330347
)
331348
elif proxy.url.scheme in ("socks5", "socks5h"):
332349
try:
@@ -351,6 +368,7 @@ def __init__(
351368
keepalive_expiry=limits.keepalive_expiry,
352369
http1=http1,
353370
http2=http2,
371+
network_backend=network_backend,
354372
)
355373
else: # pragma: no cover
356374
raise ValueError(
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
import typing
2+
3+
import httpcore
4+
import pytest
5+
6+
import httpx
7+
8+
9+
def test_network_backend():
10+
class Backend(httpcore.NetworkBackend):
11+
def connect_tcp(
12+
self,
13+
host: str,
14+
port: int,
15+
timeout: typing.Optional[float] = None,
16+
local_address: typing.Optional[str] = None,
17+
socket_options: typing.Optional[
18+
typing.Iterable[httpcore.SOCKET_OPTION]
19+
] = None,
20+
) -> httpcore.NetworkStream:
21+
return Stream()
22+
23+
class Stream(httpcore.NetworkStream):
24+
body = b"\r\n".join(
25+
[
26+
b"HTTP/1.1 200 OK",
27+
b"",
28+
b"From Backend!",
29+
]
30+
)
31+
32+
def read(self, max_bytes: int, timeout: typing.Optional[float] = None) -> bytes:
33+
body = self.body
34+
if body:
35+
self.body = b""
36+
return body
37+
38+
def write(self, buffer: bytes, timeout: typing.Optional[float] = None) -> None:
39+
pass
40+
41+
def close(self) -> None:
42+
pass
43+
44+
backend = Backend()
45+
transport = httpx.HTTPTransport(network_backend=backend)
46+
with httpx.Client(transport=transport) as client:
47+
response = client.get("http://www.example.org")
48+
assert response.status_code == 200
49+
assert response.text == "From Backend!"
50+
51+
52+
@pytest.mark.anyio
53+
async def test_async_network_backend():
54+
class AsyncBackend(httpcore.AsyncNetworkBackend):
55+
async def connect_tcp(
56+
self,
57+
host: str,
58+
port: int,
59+
timeout: typing.Optional[float] = None,
60+
local_address: typing.Optional[str] = None,
61+
socket_options: typing.Optional[
62+
typing.Iterable[httpcore.SOCKET_OPTION]
63+
] = None,
64+
) -> httpcore.AsyncNetworkStream:
65+
return AsyncStream()
66+
67+
class AsyncStream(httpcore.AsyncNetworkStream):
68+
body = b"\r\n".join(
69+
[
70+
b"HTTP/1.1 200 OK",
71+
b"",
72+
b"From Async Backend!",
73+
]
74+
)
75+
76+
async def read(
77+
self, max_bytes: int, timeout: typing.Optional[float] = None
78+
) -> bytes:
79+
body = self.body
80+
if body:
81+
self.body = b""
82+
return body
83+
84+
async def write(
85+
self, buffer: bytes, timeout: typing.Optional[float] = None
86+
) -> None:
87+
pass
88+
89+
async def aclose(self) -> None:
90+
pass
91+
92+
backend = AsyncBackend()
93+
transport = httpx.AsyncHTTPTransport(network_backend=backend)
94+
async with httpx.AsyncClient(transport=transport) as client:
95+
response = await client.get("http://www.example.org")
96+
assert response.status_code == 200
97+
assert response.text == "From Async Backend!"

0 commit comments

Comments
 (0)