Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
13 changes: 11 additions & 2 deletions CHANGELOG.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,14 +4,23 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

## 0.17.1
## Master

The 0.18.x release series formalises our low-level Transport API, introducing the
base classes `httpx.BaseTransport` and `httpx.AsyncBaseTransport`.

* Transport instances now inherit from `httpx.BaseTransport` or `httpx.AsyncBaseTransport`,
and should implement either the `handle_request` method or `handle_async_request` method.
* The `response.ext` property and `Response(ext=...)` argument are now named `extensions`.

## 0.17.1 (March 15th, 2021)

### Fixed

* Type annotation on `CertTypes` allows `keyfile` and `password` to be optional. (Pull #1503)
* Fix httpcore pinned version. (Pull #1495)

## 0.17.0
## 0.17.0 (Februray 28th, 2021)

### Added

Expand Down
39 changes: 23 additions & 16 deletions docs/advanced.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -1015,31 +1015,39 @@ This [public gist](https://gist.github.com/florimondmanca/d56764d78d748eb9f73165

### Writing custom transports

A transport instance must implement the Transport API defined by
[`httpcore`](https://www.encode.io/httpcore/api/). You
should either subclass `httpcore.AsyncHTTPTransport` to implement a transport to
use with `AsyncClient`, or subclass `httpcore.SyncHTTPTransport` to implement a
transport to use with `Client`.
A transport instance must implement the low-level Transport API, which deals
with sending a single request, and returning a response. You should either
subclass `httpx.BaseTransport` to implement a transport to use with `Client`,
or subclass `httpx.AsyncBaseTransport` to implement a transport to
use with `AsyncClient`.

At the layer of the transport API we're simply using plain primitives.
No `Request` or `Response` models, no fancy `URL` or `Header` handling.
This strict point of cut-off provides a clear design separation between the
HTTPX API, and the low-level network handling.

See the `handle_request` and `handle_async_request` docstrings for more details
on the specifics of the Transport API.

A complete example of a custom transport implementation would be:

```python
import json
import httpcore
import httpx


class HelloWorldTransport(httpcore.SyncHTTPTransport):
class HelloWorldTransport(httpx.BaseTransport):
"""
A mock transport that always returns a JSON "Hello, world!" response.
"""

def request(self, method, url, headers=None, stream=None, ext=None):
def handle_request(self, method, url, headers=None, stream=None, extensions=None):
message = {"text": "Hello, world!"}
content = json.dumps(message).encode("utf-8")
stream = httpcore.PlainByteStream(content)
stream = [content]
headers = [(b"content-type", b"application/json")]
ext = {"http_version": b"HTTP/1.1"}
return 200, headers, stream, ext
extensions = {}
return 200, headers, stream, extensions
```

Which we can use in the same way:
Expand DownExpand Up@@ -1084,23 +1092,22 @@ which transport an outgoing request should be routed via, with [the same style
used for specifying proxy routing](#routing).

```python
import httpcore
import httpx

class HTTPSRedirectTransport(httpcore.SyncHTTPTransport):
class HTTPSRedirectTransport(httpx.BaseTransport):
"""
A transport that always redirects to HTTPS.
"""

def request(self, method, url, headers=None, stream=None, ext=None):
def handle_request(self, method, url, headers=None, stream=None, extensions=None):
scheme, host, port, path = url
if port is None:
location = b"https://%s%s" % (host, path)
else:
location = b"https://%s:%d%s" % (host, port, path)
stream = httpcore.PlainByteStream(b"")
stream = [b""]
headers = [(b"location", location)]
ext = {"http_version": b"HTTP/1.1"}
extensions = {}
return 303, headers, stream, ext


Expand Down
3 changes: 3 additions & 0 deletions httpx/__init__.py
Original file line numberDiff line numberDiff 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.base import AsyncBaseTransport, BaseTransport
from ._transports.default import AsyncHTTPTransport, HTTPTransport
from ._transports.mock import MockTransport
from ._transports.wsgi import WSGITransport
Expand All@@ -45,9 +46,11 @@
"__title__",
"__version__",
"ASGITransport",
"AsyncBaseTransport",
"AsyncClient",
"AsyncHTTPTransport",
"Auth",
"BaseTransport",
"BasicAuth",
"Client",
"CloseError",
Expand Down
56 changes: 28 additions & 28 deletions httpx/_client.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,8 +4,6 @@
import warnings
from types import TracebackType

import httpcore

from .__version__ import __version__
from ._auth import Auth, BasicAuth, FunctionAuth
from ._config import (
Expand All@@ -29,6 +27,7 @@
from ._models import URL, Cookies, Headers, QueryParams, Request, Response
from ._status_codes import codes
from ._transports.asgi import ASGITransport
from ._transports.base import AsyncBaseTransport, BaseTransport
from ._transports.default import AsyncHTTPTransport, HTTPTransport
from ._transports.wsgi import WSGITransport
from ._types import (
Expand DownExpand Up@@ -560,14 +559,14 @@ def __init__(
cert: CertTypes = None,
http2: bool = False,
proxies: ProxiesTypes = None,
mounts: typing.Mapping[str, httpcore.SyncHTTPTransport] = None,
mounts: typing.Mapping[str, BaseTransport] = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
limits: Limits = DEFAULT_LIMITS,
pool_limits: Limits = None,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
event_hooks: typing.Mapping[str, typing.List[typing.Callable]] = None,
base_url: URLTypes = "",
transport: httpcore.SyncHTTPTransport = None,
transport: BaseTransport = None,
app: typing.Callable = None,
trust_env: bool = True,
):
Expand DownExpand Up@@ -611,9 +610,7 @@ def __init__(
app=app,
trust_env=trust_env,
)
self._mounts: typing.Dict[
URLPattern, typing.Optional[httpcore.SyncHTTPTransport]
] = {
self._mounts: typing.Dict[URLPattern, typing.Optional[BaseTransport]] = {
URLPattern(key): None
if proxy is None
else self._init_proxy_transport(
Expand All@@ -639,10 +636,10 @@ def _init_transport(
cert: CertTypes = None,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
transport: httpcore.SyncHTTPTransport = None,
transport: BaseTransport = None,
app: typing.Callable = None,
trust_env: bool = True,
) -> httpcore.SyncHTTPTransport:
) -> BaseTransport:
if transport is not None:
return transport

Expand All@@ -661,7 +658,7 @@ def _init_proxy_transport(
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
trust_env: bool = True,
) -> httpcore.SyncHTTPTransport:
) -> BaseTransport:
return HTTPTransport(
verify=verify,
cert=cert,
Expand All@@ -671,7 +668,7 @@ def _init_proxy_transport(
proxy=proxy,
)

def _transport_for_url(self, url: URL) -> httpcore.SyncHTTPTransport:
def _transport_for_url(self, url: URL) -> BaseTransport:
"""
Returns the transport instance that should be used for a given URL.
This will either be the standard connection pool, or a proxy.
Expand DownExpand Up@@ -853,24 +850,24 @@ def _send_single_request(self, request: Request, timeout: Timeout) -> Response:
timer.sync_start()

with map_exceptions(HTTPCORE_EXC_MAP, request=request):
(status_code, headers, stream, ext) = transport.request(
(status_code, headers, stream, extensions) = transport.handle_request(
request.method.encode(),
request.url.raw,
headers=request.headers.raw,
stream=request.stream, # type: ignore
ext={"timeout": timeout.as_dict()},
extensions={"timeout": timeout.as_dict()},
)

def on_close(response: Response) -> None:
response.elapsed = datetime.timedelta(seconds=timer.sync_elapsed())
if hasattr(stream, "close"):
stream.close()
stream.close() # type: ignore

response = Response(
status_code,
headers=headers,
stream=stream, # type: ignore
ext=ext,
extensions=extensions,
request=request,
on_close=on_close,
)
Expand DownExpand Up@@ -1193,14 +1190,14 @@ def __init__(
cert: CertTypes = None,
http2: bool = False,
proxies: ProxiesTypes = None,
mounts: typing.Mapping[str, httpcore.AsyncHTTPTransport] = None,
mounts: typing.Mapping[str, AsyncBaseTransport] = None,
timeout: TimeoutTypes = DEFAULT_TIMEOUT_CONFIG,
limits: Limits = DEFAULT_LIMITS,
pool_limits: Limits = None,
max_redirects: int = DEFAULT_MAX_REDIRECTS,
event_hooks: typing.Mapping[str, typing.List[typing.Callable]] = None,
base_url: URLTypes = "",
transport: httpcore.AsyncHTTPTransport = None,
transport: AsyncBaseTransport = None,
app: typing.Callable = None,
trust_env: bool = True,
):
Expand DownExpand Up@@ -1245,9 +1242,7 @@ def __init__(
trust_env=trust_env,
)

self._mounts: typing.Dict[
URLPattern, typing.Optional[httpcore.AsyncHTTPTransport]
] = {
self._mounts: typing.Dict[URLPattern, typing.Optional[AsyncBaseTransport]] = {
URLPattern(key): None
if proxy is None
else self._init_proxy_transport(
Expand All@@ -1272,10 +1267,10 @@ def _init_transport(
cert: CertTypes = None,
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
transport: httpcore.AsyncHTTPTransport = None,
transport: AsyncBaseTransport = None,
app: typing.Callable = None,
trust_env: bool = True,
) -> httpcore.AsyncHTTPTransport:
) -> AsyncBaseTransport:
if transport is not None:
return transport

Expand All@@ -1294,7 +1289,7 @@ def _init_proxy_transport(
http2: bool = False,
limits: Limits = DEFAULT_LIMITS,
trust_env: bool = True,
) -> httpcore.AsyncHTTPTransport:
) -> AsyncBaseTransport:
return AsyncHTTPTransport(
verify=verify,
cert=cert,
Expand All@@ -1304,7 +1299,7 @@ def _init_proxy_transport(
proxy=proxy,
)

def _transport_for_url(self, url: URL) -> httpcore.AsyncHTTPTransport:
def _transport_for_url(self, url: URL) -> AsyncBaseTransport:
"""
Returns the transport instance that should be used for a given URL.
This will either be the standard connection pool, or a proxy.
Expand DownExpand Up@@ -1489,25 +1484,30 @@ async def _send_single_request(
await timer.async_start()

with map_exceptions(HTTPCORE_EXC_MAP, request=request):
(status_code, headers, stream, ext) = await transport.arequest(
(
status_code,
headers,
stream,
extensions,
) = await transport.handle_async_request(
request.method.encode(),
request.url.raw,
headers=request.headers.raw,
stream=request.stream, # type: ignore
ext={"timeout": timeout.as_dict()},
extensions={"timeout": timeout.as_dict()},
)

async def on_close(response: Response) -> None:
response.elapsed = datetime.timedelta(seconds=await timer.async_elapsed())
if hasattr(stream, "aclose"):
with map_exceptions(HTTPCORE_EXC_MAP, request=request):
await stream.aclose()
await stream.aclose() # type: ignore

response = Response(
status_code,
headers=headers,
stream=stream, # type: ignore
ext=ext,
extensions=extensions,
request=request,
on_close=on_close,
)
Expand Down
8 changes: 4 additions & 4 deletions httpx/_models.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -902,7 +902,7 @@ def __init__(
json: typing.Any = None,
stream: ByteStream = None,
request: Request = None,
ext: dict = None,
extensions: dict = None,
history: typing.List["Response"] = None,
on_close: typing.Callable = None,
):
Expand All@@ -917,7 +917,7 @@ def __init__(

self.call_next: typing.Optional[typing.Callable] = None

self.ext = {} if ext is None else ext
self.extensions = {} if extensions is None else extensions
self.history = [] if history is None else list(history)
self._on_close = on_close

Expand DownExpand Up@@ -988,11 +988,11 @@ def request(self, value: Request) -> None:

@property
def http_version(self) -> str:
return self.ext.get("http_version", "HTTP/1.1")
return self.extensions.get("http_version", "HTTP/1.1")

@property
def reason_phrase(self) -> str:
return self.ext.get("reason", codes.get_reason_phrase(self.status_code))
return self.extensions.get("reason", codes.get_reason_phrase(self.status_code))

@property
def url(self) -> typing.Optional[URL]:
Expand Down
Loading