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
24 changes: 24 additions & 0 deletions docs/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,3 +619,27 @@ 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 `AsyncClient`, or
`httpcore.SyncHTTPTransport` if you're using `Client`.

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
>>> client = httpx.Client(transport=httpx.URLLib3Transport())
>>> client.get("https://example.org")
<Response [200 OK]>
```
8 changes: 6 additions & 2 deletions httpx/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
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 ._exceptions import (
ConnectTimeout,
CookieConflict,
Expand All @@ -27,6 +25,9 @@
)
from ._models import URL, Cookies, Headers, QueryParams, Request, Response
from ._status_codes import StatusCode, codes
from ._transports.asgi import ASGIDispatch, ASGITransport
from ._transports.urllib3 import URLLib3Transport
from ._transports.wsgi import WSGIDispatch, WSGITransport

__all__ = [
"__description__",
Expand All @@ -44,6 +45,7 @@
"stream",
"codes",
"ASGIDispatch",
"ASGITransport",
"AsyncClient",
"Auth",
"BasicAuth",
Expand Down Expand Up @@ -71,6 +73,7 @@
"TooManyRedirects",
"WriteTimeout",
"URL",
"URLLib3Transport",
"StatusCode",
"Cookies",
"Headers",
Expand All @@ -79,4 +82,5 @@
"Response",
"DigestAuth",
"WSGIDispatch",
"WSGITransport",
]
99 changes: 61 additions & 38 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 All @@ -18,11 +19,11 @@
UnsetType,
)
from ._content_streams import ContentStream
from ._dispatch.asgi import ASGIDispatch
from ._dispatch.wsgi import WSGIDispatch
from ._exceptions import HTTPError, InvalidURL, RequestBodyUnavailable, TooManyRedirects
from ._models import URL, Cookies, Headers, Origin, QueryParams, Request, Response
from ._status_codes import codes
from ._transports.asgi import ASGITransport
from ._transports.wsgi import WSGITransport
from ._types import (
AuthTypes,
CertTypes,
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,8 +418,9 @@ 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.
* **app** - *(optional)* An WSGI application to send requests to,
rather than sending actual network requests.
* **trust_env** - *(optional)* Enables or disables usage of environment
Expand All @@ -439,6 +441,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 +459,25 @@ 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'",
DeprecationWarning,
)
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,20 +487,20 @@ 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)
return WSGITransport(app=app)

ssl_context = SSLConfig(
verify=verify, cert=cert, trust_env=trust_env
Expand All @@ -502,7 +514,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 +537,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 +557,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 +692,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 +701,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 +904,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 @@ -949,8 +961,9 @@ class AsyncClient(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.
* **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 @@ -972,6 +985,7 @@ def __init__(
pool_limits: PoolLimits = DEFAULT_POOL_LIMITS,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
base_url: URLTypes = None,
transport: httpcore.AsyncHTTPTransport = None,
dispatch: httpcore.AsyncHTTPTransport = None,
app: typing.Callable = None,
trust_env: bool = True,
Expand All @@ -987,19 +1001,28 @@ 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'",
DeprecationWarning,
)
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,21 +1033,21 @@ 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)
return ASGITransport(app=app)

ssl_context = SSLConfig(
verify=verify, cert=cert, trust_env=trust_env
Expand All @@ -1039,7 +1062,7 @@ def init_dispatch(
http2=http2,
)

def init_proxy_dispatch(
def init_proxy_transport(
self,
proxy: Proxy,
verify: VerifyTypes = True,
Expand All @@ -1064,7 +1087,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 +1107,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 +1245,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 +1254,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 +1457,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.
Loading