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

Transport API #963

Merged
merged 16 commits into from
May 21, 2020
25 changes: 25 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,3 +619,28 @@ If you do need to make HTTPS connections to a local server, for example to test
>>> r
Response <200 OK>
```

## Custom Transports

HTTPX's `Client` also accepts a `transport` argument. This argument allows you
to inject a custom Transport objects that will be used to perform the actual
yeraydiazdiaz marked this conversation as resolved.
Show resolved Hide resolved
sending of the requests.

These Transport objects must implement some methods from
yeraydiazdiaz marked this conversation as resolved.
Show resolved Hide resolved
[`httpcore`'s API](https://www.encode.io/httpcore/api/), either
`httpcore.AsyncHTTPTransport` if you're using the asynchronous `Client`, or
yeraydiazdiaz marked this conversation as resolved.
Show resolved Hide resolved
`httpcore.SyncHTTPTransport` if you're using a synchronous one.

Specifically you *MUST* implement `request`, and `close` or `aclose` depending
on the type of client you're using.

For example, HTTPX ships with a transport that uses the excellent
[`urllib3` library](https://urllib3.readthedocs.io/en/latest/):

```python
>>> import httpx
>>> from httpx._dispatch.urllib3 import URLLib3Dispatcher
yeraydiazdiaz marked this conversation as resolved.
Show resolved Hide resolved
>>> client = httpx.Client(transport=URLLib3Dispatcher())
>>> client.get("https://example.org")
<Response [200 OK]>
```
6 changes: 4 additions & 2 deletions httpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from ._auth import Auth, BasicAuth, DigestAuth
from ._client import AsyncClient, Client
from ._config import PoolLimits, Proxy, Timeout
from ._dispatch.asgi import ASGIDispatch
from ._dispatch.wsgi import WSGIDispatch
from ._transports.asgi import ASGIDispatch, ASGITransport
from ._transports.wsgi import WSGIDispatch, WSGITransport
yeraydiazdiaz marked this conversation as resolved.
Show resolved Hide resolved
from ._exceptions import (
ConnectTimeout,
CookieConflict,
Expand Down Expand Up @@ -44,6 +44,7 @@
"stream",
"codes",
"ASGIDispatch",
"ASGITransport",
"AsyncClient",
"Auth",
"BasicAuth",
Expand Down Expand Up @@ -79,4 +80,5 @@
"Response",
"DigestAuth",
"WSGIDispatch",
"WSGITransport",
]
91 changes: 58 additions & 33 deletions httpx/_client.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import functools
import typing
import warnings
from types import TracebackType

import hstspreload
Expand Down Expand Up @@ -91,7 +92,7 @@ def get_proxy_map(
return {"all": proxy}
elif isinstance(proxies, httpcore.AsyncHTTPTransport): # pragma: nocover
raise RuntimeError(
"Passing a dispatcher instance to 'proxies=' is no longer "
"Passing a transport instance to 'proxies=' is no longer "
"supported. Use `httpx.Proxy() instead.`"
)
else:
Expand All @@ -102,7 +103,7 @@ def get_proxy_map(
new_proxies[str(key)] = proxy
elif isinstance(value, httpcore.AsyncHTTPTransport): # pragma: nocover
raise RuntimeError(
"Passing a dispatcher instance to 'proxies=' is "
"Passing a transport instance to 'proxies=' is "
"no longer supported. Use `httpx.Proxy() instead.`"
)
return new_proxies
Expand Down Expand Up @@ -417,7 +418,10 @@ class Client(BaseClient):
that should be followed.
* **base_url** - *(optional)* A URL to use as the base when building
request URLs.
* **dispatch** - *(optional)* A dispatch class to use for sending requests
* **transport** - *(optional)* A transport class to use for sending requests
over the network.
* **dispatch** - *(optional)* A deprecated alias for transport.
* **transport** - *(optional)* A transport class to use for sending requests
yeraydiazdiaz marked this conversation as resolved.
Show resolved Hide resolved
over the network.
* **app** - *(optional)* An WSGI application to send requests to,
rather than sending actual network requests.
Expand All @@ -439,6 +443,7 @@ def __init__(
pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
base_url: URLTypes = None,
transport: httpcore.SyncHTTPTransport = None,
dispatch: httpcore.SyncHTTPTransport = None,
app: typing.Callable = None,
trust_env: bool = True,
Expand All @@ -456,16 +461,24 @@ def __init__(

proxy_map = self.get_proxy_map(proxies, trust_env)

self.dispatch = self.init_dispatch(
if dispatch is not None:
warnings.warn(
yeraydiazdiaz marked this conversation as resolved.
Show resolved Hide resolved
yeraydiazdiaz marked this conversation as resolved.
Show resolved Hide resolved
"The dispatch argument is deprecated since v0.13 and will be "
"removed in a future release, please use 'transport'"
)
if transport is None:
transport = dispatch

self.transport = self.init_transport(
verify=verify,
cert=cert,
pool_limits=pool_limits,
dispatch=dispatch,
transport=transport,
app=app,
trust_env=trust_env,
)
self.proxies: typing.Dict[str, httpcore.SyncHTTPTransport] = {
key: self.init_proxy_dispatch(
key: self.init_proxy_transport(
proxy,
verify=verify,
cert=cert,
Expand All @@ -475,17 +488,17 @@ def __init__(
for key, proxy in proxy_map.items()
}

def init_dispatch(
def init_transport(
self,
verify: VerifyTypes = True,
cert: CertTypes = None,
pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
dispatch: httpcore.SyncHTTPTransport = None,
transport: httpcore.SyncHTTPTransport = None,
app: typing.Callable = None,
trust_env: bool = True,
) -> httpcore.SyncHTTPTransport:
if dispatch is not None:
return dispatch
if transport is not None:
return transport

if app is not None:
return WSGIDispatch(app=app)
Expand All @@ -502,7 +515,7 @@ def init_dispatch(
max_connections=max_connections,
)

def init_proxy_dispatch(
def init_proxy_transport(
self,
proxy: Proxy,
verify: VerifyTypes = True,
Expand All @@ -525,7 +538,7 @@ def init_proxy_dispatch(
max_connections=max_connections,
)

def dispatcher_for_url(self, url: URL) -> httpcore.SyncHTTPTransport:
def transport_for_url(self, url: URL) -> httpcore.SyncHTTPTransport:
"""
Returns the transport instance that should be used for a given URL.
This will either be the standard connection pool, or a proxy.
Expand All @@ -545,10 +558,10 @@ def dispatcher_for_url(self, url: URL) -> httpcore.SyncHTTPTransport:
)
for proxy_key in proxy_keys:
if proxy_key and proxy_key in self.proxies:
dispatcher = self.proxies[proxy_key]
return dispatcher
transport = self.proxies[proxy_key]
return transport

return self.dispatch
return self.transport

def request(
self,
Expand Down Expand Up @@ -680,7 +693,7 @@ def send_single_request(self, request: Request, timeout: Timeout) -> Response:
Sends a single request, without handling any redirections.
"""

dispatcher = self.dispatcher_for_url(request.url)
transport = self.transport_for_url(request.url)

try:
(
Expand All @@ -689,7 +702,7 @@ def send_single_request(self, request: Request, timeout: Timeout) -> Response:
reason_phrase,
headers,
stream,
) = dispatcher.request(
) = transport.request(
request.method.encode(),
request.url.raw,
headers=request.headers.raw,
Expand Down Expand Up @@ -892,7 +905,7 @@ def delete(
)

def close(self) -> None:
self.dispatch.close()
self.transport.close()
for proxy in self.proxies.values():
proxy.close()

Expand Down Expand Up @@ -951,6 +964,9 @@ class AsyncClient(BaseClient):
request URLs.
* **dispatch** - *(optional)* A dispatch class to use for sending requests
over the network.
* **transport** - *(optional)* A transport class to use for sending requests
over the network.
* **dispatch** - *(optional)* A deprecated alias for transport.
* **app** - *(optional)* An ASGI application to send requests to,
rather than sending actual network requests.
* **trust_env** - *(optional)* Enables or disables usage of environment
Expand All @@ -973,6 +989,7 @@ def __init__(
max_redirects: int = DEFAULT_MAX_REDIRECTS,
base_url: URLTypes = None,
dispatch: httpcore.AsyncHTTPTransport = None,
transport: httpcore.AsyncHTTPTransport = None,
app: typing.Callable = None,
trust_env: bool = True,
):
Expand All @@ -987,19 +1004,27 @@ def __init__(
trust_env=trust_env,
)

if dispatch is not None:
warnings.warn(
"The dispatch argument is deprecated since v0.13 and will be "
"removed in a future release, please use 'transport'"
)
if transport is None:
transport = dispatch

proxy_map = self.get_proxy_map(proxies, trust_env)

self.dispatch = self.init_dispatch(
self.transport = self.init_transport(
verify=verify,
cert=cert,
http2=http2,
pool_limits=pool_limits,
dispatch=dispatch,
transport=transport,
app=app,
trust_env=trust_env,
)
self.proxies: typing.Dict[str, httpcore.AsyncHTTPTransport] = {
key: self.init_proxy_dispatch(
key: self.init_proxy_transport(
proxy,
verify=verify,
cert=cert,
Expand All @@ -1010,18 +1035,18 @@ def __init__(
for key, proxy in proxy_map.items()
}

def init_dispatch(
def init_transport(
self,
verify: VerifyTypes = True,
cert: CertTypes = None,
http2: bool = False,
pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
dispatch: httpcore.AsyncHTTPTransport = None,
transport: httpcore.AsyncHTTPTransport = None,
app: typing.Callable = None,
trust_env: bool = True,
) -> httpcore.AsyncHTTPTransport:
if dispatch is not None:
return dispatch
if transport is not None:
return transport

if app is not None:
return ASGIDispatch(app=app)
Expand All @@ -1039,7 +1064,7 @@ def init_dispatch(
http2=http2,
)

def init_proxy_dispatch(
def init_proxy_transport(
self,
proxy: Proxy,
verify: VerifyTypes = True,
Expand All @@ -1064,7 +1089,7 @@ def init_proxy_dispatch(
http2=http2,
)

def dispatcher_for_url(self, url: URL) -> httpcore.AsyncHTTPTransport:
def transport_for_url(self, url: URL) -> httpcore.AsyncHTTPTransport:
"""
Returns the transport instance that should be used for a given URL.
This will either be the standard connection pool, or a proxy.
Expand All @@ -1084,10 +1109,10 @@ def dispatcher_for_url(self, url: URL) -> httpcore.AsyncHTTPTransport:
)
for proxy_key in proxy_keys:
if proxy_key and proxy_key in self.proxies:
dispatcher = self.proxies[proxy_key]
return dispatcher
transport = self.proxies[proxy_key]
return transport

return self.dispatch
return self.transport

async def request(
self,
Expand Down Expand Up @@ -1222,7 +1247,7 @@ async def send_single_request(
Sends a single request, without handling any redirections.
"""

dispatcher = self.dispatcher_for_url(request.url)
transport = self.transport_for_url(request.url)

try:
(
Expand All @@ -1231,7 +1256,7 @@ async def send_single_request(
reason_phrase,
headers,
stream,
) = await dispatcher.request(
) = await transport.request(
request.method.encode(),
request.url.raw,
headers=request.headers.raw,
Expand Down Expand Up @@ -1434,7 +1459,7 @@ async def delete(
)

async def aclose(self) -> None:
await self.dispatch.aclose()
await self.transport.aclose()
for proxy in self.proxies.values():
await proxy.aclose()

Expand Down
File renamed without changes.
26 changes: 22 additions & 4 deletions httpx/_dispatch/asgi.py → httpx/_transports/asgi.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import typing
from typing import Callable, Dict, List, Optional, Tuple
import warnings

import httpcore
import sniffio
Expand All @@ -24,9 +25,9 @@ def create_event() -> "Event":
return asyncio.Event()


class ASGIDispatch(httpcore.AsyncHTTPTransport):
class ASGITransport(httpcore.AsyncHTTPTransport):
"""
A custom AsyncDispatcher that handles sending requests directly to an ASGI app.
A custom AsyncTransport that handles sending requests directly to an ASGI app.
The simplest way to use this functionality is to use the `app` argument.

```
Expand All @@ -35,10 +36,10 @@ class ASGIDispatch(httpcore.AsyncHTTPTransport):

Alternatively, you can setup the dispatch instance explicitly.
This allows you to include any additional configuration arguments specific
to the ASGIDispatch class:
to the ASGITransport class:

```
dispatch = httpx.ASGIDispatch(
dispatch = httpx.ASGITransport(
app=app,
root_path="/submount",
client=("1.2.3.4", 123)
Expand Down Expand Up @@ -153,3 +154,20 @@ async def send(message: dict) -> None:
stream = ByteStream(b"".join(body_parts))

return (b"HTTP/1.1", status_code, b"", response_headers, stream)


class ASGIDispatch(ASGITransport):
def __init__(
self,
app: Callable,
raise_app_exceptions: bool = True,
root_path: str = "",
client: Tuple[str, int] = ("127.0.0.1", 123),
) -> None:
warnings.warn("ASGIDispatch is deprecated, please use ASGITransport")
super().__init__(
app=app,
raise_app_exceptions=raise_app_exceptions,
root_path=root_path,
client=client,
)
Loading