Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add HTTPTransport and AsyncHTTPTransport #1399

Merged
merged 15 commits into from
Jan 8, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
46 changes: 18 additions & 28 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -968,46 +968,36 @@ sending of the requests.
### Usage

For some advanced configuration you might need to instantiate a transport
class directly, and pass it to the client instance. The `httpcore` package
provides a `local_address` configuration that is only available via this
low-level API.
class directly, and pass it to the client instance. One example is the
`local_address` configuration which is only available via this low-level API.

```pycon
>>> import httpx, httpcore
>>> ssl_context = httpx.create_ssl_context()
>>> transport = httpcore.SyncConnectionPool(
... ssl_context=ssl_context,
... max_connections=100,
... max_keepalive_connections=20,
... keepalive_expiry=5.0,
... local_address="0.0.0.0"
... ) # Use the standard HTTPX defaults, but with an IPv4 only 'local_address'.
>>> import httpx
>>> transport = httpx.HTTPTransport(local_address="0.0.0.0")
>>> client = httpx.Client(transport=transport)
```

Similarly, `httpcore` provides a `uds` option for connecting via a Unix Domain Socket that is only available via this low-level API:
Connection retries are also available via this interface.

```python
>>> import httpx, httpcore
>>> ssl_context = httpx.create_ssl_context()
>>> transport = httpcore.SyncConnectionPool(
... ssl_context=ssl_context,
... max_connections=100,
... max_keepalive_connections=20,
... keepalive_expiry=5.0,
... uds="/var/run/docker.sock",
... ) # Connect to the Docker API via a Unix Socket.
```pycon
>>> import httpx
>>> transport = httpx.HTTPTransport(retries=1)
>>> client = httpx.Client(transport=transport)
```

Similarly, instantiating a transport directly provides a `uds` option for
connecting via a Unix Domain Socket that is only available via this low-level API:

```pycon
>>> import httpx
>>> # Connect to the Docker API via a Unix Socket.
>>> transport = httpx.HTTPTransport(uds="/var/run/docker.sock")
>>> client = httpx.Client(transport=transport)
>>> response = client.get("http://docker/info")
>>> response.json()
{"ID": "...", "Containers": 4, "Images": 74, ...}
```

Unlike the `httpx.Client()`, the lower-level `httpcore` transport instances
do not include any default values for configuring aspects such as the
connection pooling details, so you'll need to provide more explicit
configuration when using this API.

### urllib3 transport

This [public gist](https://gist.github.com/florimondmanca/d56764d78d748eb9f73165da388e546e) provides a transport that uses the excellent [`urllib3` library](https://urllib3.readthedocs.io/en/latest/), and can be used with the sync `Client`...
Expand Down
13 changes: 13 additions & 0 deletions docs/async.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,19 @@ async def upload_bytes():
await client.post(url, data=upload_bytes())
```

### Explicit transport instances

When instantiating a transport instance directly, you need to use `httpx.AsyncHTTPProxy`.
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved

For instance:

```pycon
>>> import httpx
>>> transport = httpx.AsyncHTTPTransport(retries=1)
>>> async with httpx.AsyncClient(transport=transport) as client:
>>> ...
```

## Supported async environments

HTTPX supports either `asyncio` or `trio` as an async environment.
Expand Down
3 changes: 3 additions & 0 deletions httpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
from ._models import URL, Cookies, Headers, QueryParams, Request, Response
from ._status_codes import StatusCode, codes
from ._transports.asgi import ASGITransport
from ._transports.default import AsyncHTTPTransport, HTTPTransport
from ._transports.wsgi import WSGITransport

__all__ = [
Expand All @@ -44,6 +45,7 @@
"__version__",
"ASGITransport",
"AsyncClient",
"AsyncHTTPTransport",
"Auth",
"BasicAuth",
"Client",
Expand All @@ -62,6 +64,7 @@
"Headers",
"HTTPError",
"HTTPStatusError",
"HTTPTransport",
"InvalidURL",
"Limits",
"LocalProtocolError",
Expand Down
26 changes: 7 additions & 19 deletions httpx/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from ._models import URL, Cookies, Headers, QueryParams, Request, Response
from ._status_codes import codes
from ._transports.asgi import ASGITransport
from ._transports.default import AsyncHTTPTransport, HTTPTransport
from ._transports.wsgi import WSGITransport
from ._types import (
AuthTypes,
Expand Down Expand Up @@ -64,7 +65,6 @@

logger = get_logger(__name__)

KEEPALIVE_EXPIRY = 5.0
USER_AGENT = f"python-httpx/{__version__}"
ACCEPT_ENCODING = ", ".join(
[key for key in SUPPORTED_DECODERS.keys() if key != "identity"]
Expand Down Expand Up @@ -650,14 +650,8 @@ def _init_transport(
if app is not None:
return WSGITransport(app=app)

ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)

return httpcore.SyncConnectionPool(
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=KEEPALIVE_EXPIRY,
http2=http2,
return HTTPTransport(
verify=verify, cert=cert, http2=http2, limits=limits, trust_env=trust_env
)

def _init_proxy_transport(
Expand All @@ -678,7 +672,7 @@ def _init_proxy_transport(
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=KEEPALIVE_EXPIRY,
keepalive_expiry=limits.keepalive_expiry,
http2=http2,
)

Expand Down Expand Up @@ -1293,14 +1287,8 @@ def _init_transport(
if app is not None:
return ASGITransport(app=app)

ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)

return httpcore.AsyncConnectionPool(
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=KEEPALIVE_EXPIRY,
http2=http2,
return AsyncHTTPTransport(
verify=verify, cert=cert, http2=http2, limits=limits, trust_env=trust_env
)

def _init_proxy_transport(
Expand All @@ -1321,7 +1309,7 @@ def _init_proxy_transport(
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=KEEPALIVE_EXPIRY,
keepalive_expiry=limits.keepalive_expiry,
http2=http2,
)

Expand Down
6 changes: 5 additions & 1 deletion httpx/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -294,22 +294,26 @@ def __init__(
*,
max_connections: int = None,
max_keepalive_connections: int = None,
keepalive_expiry: typing.Optional[float] = 5.0,
):
self.max_connections = max_connections
self.max_keepalive_connections = max_keepalive_connections
self.keepalive_expiry = keepalive_expiry

def __eq__(self, other: typing.Any) -> bool:
return (
isinstance(other, self.__class__)
and self.max_connections == other.max_connections
and self.max_keepalive_connections == other.max_keepalive_connections
and self.keepalive_expiry == other.keepalive_expiry
)

def __repr__(self) -> str:
class_name = self.__class__.__name__
return (
f"{class_name}(max_connections={self.max_connections}, "
f"max_keepalive_connections={self.max_keepalive_connections})"
f"max_keepalive_connections={self.max_keepalive_connections}, "
f"keepalive_expiry={self.keepalive_expiry})"
)


Expand Down
131 changes: 131 additions & 0 deletions httpx/_transports/default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
"""
Custom transports, with nicely configured defaults.

The following additional keyword arguments are currently supported by httpcore...

* uds: str
* local_address: str
* retries: int
* backend: str ("auto", "asyncio", "trio", "curio", "anyio", "sync")

Example usages...

# Disable HTTP/2 on a single specfic domain.
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved
mounts = {
"all://": httpx.HTTPTransport(http2=True),
"all://*example.org": httpx.HTTPTransport()
}

# Using advanced httpcore configuration, with connection retries.
transport = httpx.HTTPTransport(retries=1)
client = httpx.Client(transport=transport)

# Using advanced httpcore configuration, with unix domain sockets.
transport = httpx.HTTPTransport(uds="socket.uds")
client = httpx.Client(transport=transport)
"""
import typing
from types import TracebackType

import httpcore

from .._config import DEFAULT_LIMITS, Limits, create_ssl_context
from .._types import CertTypes, VerifyTypes

T = typing.TypeVar("T")
Headers = typing.List[typing.Tuple[bytes, bytes]]
URL = typing.Tuple[bytes, bytes, typing.Optional[int], bytes]


class HTTPTransport(httpcore.SyncHTTPTransport):
def __init__(
self,
verify: VerifyTypes = True,
cert: CertTypes = None,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
trust_env: bool = True,
**kwargs: typing.Any,
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved
):
florimondmanca marked this conversation as resolved.
Show resolved Hide resolved
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)

self._pool = httpcore.SyncConnectionPool(
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http2=http2,
**kwargs,
)

def __enter__(self: T) -> T:
return self._pool.__enter__() # type: ignore

def __exit__(
self,
exc_type: typing.Type[BaseException] = None,
exc_value: BaseException = None,
traceback: TracebackType = None,
) -> None:
self._pool.__exit__()

def request(
self,
method: bytes,
url: URL,
headers: Headers = None,
stream: httpcore.SyncByteStream = None,
ext: dict = None,
) -> typing.Tuple[int, Headers, httpcore.SyncByteStream, dict]:
return self._pool.request(method, url, headers=headers, stream=stream, ext=ext)

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


class AsyncHTTPTransport(httpcore.AsyncHTTPTransport):
def __init__(
self,
verify: VerifyTypes = True,
cert: CertTypes = None,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
trust_env: bool = True,
**kwargs: typing.Any,
):
ssl_context = create_ssl_context(verify=verify, cert=cert, trust_env=trust_env)

self._pool = httpcore.AsyncConnectionPool(
ssl_context=ssl_context,
max_connections=limits.max_connections,
max_keepalive_connections=limits.max_keepalive_connections,
keepalive_expiry=limits.keepalive_expiry,
http2=http2,
**kwargs,
)

async def __aenter__(self: T) -> T:
return await self._pool.__aenter__() # type: ignore

async def __aexit__(
self,
exc_type: typing.Type[BaseException] = None,
exc_value: BaseException = None,
traceback: TracebackType = None,
) -> None:
await self._pool.__aexit__()

async def arequest(
self,
method: bytes,
url: URL,
headers: Headers = None,
stream: httpcore.AsyncByteStream = None,
ext: dict = None,
) -> typing.Tuple[int, Headers, httpcore.AsyncByteStream, dict]:
return await self._pool.arequest(
method, url, headers=headers, stream=stream, ext=ext
)

async def aclose(self) -> None:
await self._pool.aclose()
3 changes: 2 additions & 1 deletion tests/test_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,8 @@ def test_create_ssl_context_with_get_request(server, cert_pem_file):

def test_limits_repr():
limits = httpx.Limits(max_connections=100)
assert repr(limits) == "Limits(max_connections=100, max_keepalive_connections=None)"
expected = "Limits(max_connections=100, max_keepalive_connections=None, keepalive_expiry=5.0)"
assert repr(limits) == expected


def test_limits_eq():
Expand Down