Skip to content

Commit

Permalink
Version 0.11.0 (#190)
Browse files Browse the repository at this point in the history
* Fix max_keepalive_connections config

* Use .arequest for the async version of the API

* Linting

* Update tests

* Switch to transport API with 'ext' interface

* Run unasync

* Use plain strings in 'ext'. Bump version to 0.11.0

* Version 0.11

* Update CHANGELOG

* Update CHANGELOG
  • Loading branch information
tomchristie authored Sep 22, 2020
1 parent 06d0e77 commit 60f3475
Show file tree
Hide file tree
Showing 16 changed files with 288 additions and 333 deletions.
53 changes: 53 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,59 @@ 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.11.0 (September 22nd, 2020)

The Transport API with 0.11.0 has a couple of significant changes.

Firstly we've moved changed the request interface in order to allow extensions, which will later enable us to support features
such as trailing headers, HTTP/2 server push, and CONNECT/Upgrade connections.

The interface changes from:

```python
def request(method, url, headers, stream, timeout):
return (http_version, status_code, reason, headers, stream)
```

To instead including an optional dictionary of extensions on the request and response:

```python
def request(method, url, headers, stream, ext):
return (status_code, headers, stream, ext)
```

Having an open-ended extensions point will allow us to add later support for various optional features, that wouldn't otherwise be supported without these API changes.

In particular:

* Trailing headers support.
* HTTP/2 Server Push
* sendfile.
* Exposing raw connection on CONNECT, Upgrade, HTTP/2 bi-di streaming.
* Exposing debug information out of the API, including template name, template context.

Currently extensions are limited to:

* request: `timeout` - Optional. Timeout dictionary.
* response: `http_version` - Optional. Include the HTTP version used on the response.
* response: `reason` - Optional. Include the reason phrase used on the response. Only valid with HTTP/1.*.

See https://github.com/encode/httpx/issues/1274#issuecomment-694884553 for the history behind this.

Secondly, the async version of `request` is now namespaced as `arequest`.

This allows concrete transports to support both sync and async implementations on the same class.

### Added

- Add curio support. (Pull #168)
- Add anyio support, with `backend="anyio"`. (Pull #169)

### Changed

- Update the Transport API to use 'ext' for optional extensions. (Pull #190)
- Update the Transport API to use `.request` and `.arequest` so implementations can support both sync and async. (Pull #189)

## 0.10.2 (August 20th, 2020)

### Added
Expand Down
2 changes: 1 addition & 1 deletion httpcore/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
"WriteError",
"WriteTimeout",
]
__version__ = "0.10.2"
__version__ = "0.11.0"

__locals = locals()

Expand Down
20 changes: 7 additions & 13 deletions httpcore/_async/base.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import enum
from types import TracebackType
from typing import AsyncIterator, List, Tuple, Type
from typing import AsyncIterator, Tuple, Type

from .._types import URL, Headers, T, TimeoutDict
from .._types import URL, Headers, T


class NewConnectionRequired(Exception):
Expand Down Expand Up @@ -67,8 +67,8 @@ async def arequest(
url: URL,
headers: Headers = None,
stream: AsyncByteStream = None,
timeout: TimeoutDict = None,
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
ext: dict = None,
) -> Tuple[int, Headers, AsyncByteStream, dict]:
"""
The interface for sending a single HTTP request, and returning a response.
Expand All @@ -80,23 +80,17 @@ async def arequest(
* **headers** - `Optional[List[Tuple[bytes, bytes]]]` - Any HTTP headers
to send with the request.
* **stream** - `Optional[AsyncByteStream]` - The body of the HTTP request.
* **timeout** - `Optional[Dict[str, Optional[float]]]` - A dictionary of
timeout values for I/O operations. Supported keys are "pool" for acquiring a
connection from the connection pool, "read" for reading from the connection,
"write" for writing to the connection and "connect" for opening the connection.
Values are floating point seconds.
* **ext** - `Optional[dict]` - A dictionary of optional extensions.
** Returns:**
A five-tuple of:
A four-tuple of:
* **http_version** - `bytes` - The HTTP version used by the server,
such as `b'HTTP/1.1'`.
* **status_code** - `int` - The HTTP status code, such as `200`.
* **reason_phrase** - `bytes` - Any HTTP reason phrase, such as `b'OK'`.
* **headers** - `List[Tuple[bytes, bytes]]` - Any HTTP headers included
on the response.
* **stream** - `AsyncByteStream` - The body of the HTTP response.
* **ext** - `dict` - A dictionary of optional extensions.
"""
raise NotImplementedError() # pragma: nocover

Expand Down
11 changes: 7 additions & 4 deletions httpcore/_async/connection.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ssl import SSLContext
from typing import List, Optional, Tuple
from typing import Optional, Tuple, cast

from .._backends.auto import AsyncBackend, AsyncLock, AsyncSocketStream, AutoBackend
from .._types import URL, Headers, Origin, TimeoutDict
Expand Down Expand Up @@ -72,9 +72,12 @@ async def arequest(
url: URL,
headers: Headers = None,
stream: AsyncByteStream = None,
timeout: TimeoutDict = None,
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
ext: dict = None,
) -> Tuple[int, Headers, AsyncByteStream, dict]:
assert url_to_origin(url) == self.origin
ext = {} if ext is None else ext
timeout = cast(TimeoutDict, ext.get("timeout", {}))

async with self.request_lock:
if self.state == ConnectionState.PENDING:
if not self.socket:
Expand All @@ -94,7 +97,7 @@ async def arequest(
logger.trace(
"connection.arequest method=%r url=%r headers=%r", method, url, headers
)
return await self.connection.arequest(method, url, headers, stream, timeout)
return await self.connection.arequest(method, url, headers, stream, ext)

async def _open_socket(self, timeout: TimeoutDict = None) -> AsyncSocketStream:
scheme, hostname, port = self.origin
Expand Down
19 changes: 10 additions & 9 deletions httpcore/_async/connection_pool.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import warnings
from ssl import SSLContext
from typing import AsyncIterator, Callable, Dict, List, Optional, Set, Tuple
from typing import AsyncIterator, Callable, Dict, List, Optional, Set, Tuple, cast

from .._backends.auto import AsyncLock, AsyncSemaphore
from .._backends.base import lookup_async_backend
Expand Down Expand Up @@ -153,15 +153,17 @@ async def arequest(
url: URL,
headers: Headers = None,
stream: AsyncByteStream = None,
timeout: TimeoutDict = None,
) -> Tuple[bytes, int, bytes, Headers, AsyncByteStream]:
ext: dict = None,
) -> Tuple[int, Headers, AsyncByteStream, dict]:
if url[0] not in (b"http", b"https"):
scheme = url[0].decode("latin-1")
raise UnsupportedProtocol(f"Unsupported URL protocol {scheme!r}")
if not url[1]:
raise LocalProtocolError("Missing hostname in URL.")

origin = url_to_origin(url)
ext = {} if ext is None else ext
timeout = cast(TimeoutDict, ext.get("timeout", {}))

await self._keepalive_sweep()

Expand Down Expand Up @@ -190,7 +192,7 @@ async def arequest(

try:
response = await connection.arequest(
method, url, headers=headers, stream=stream, timeout=timeout
method, url, headers=headers, stream=stream, ext=ext
)
except NewConnectionRequired:
connection = None
Expand All @@ -199,10 +201,11 @@ async def arequest(
await self._remove_from_pool(connection)
raise

status_code, headers, stream, ext = response
wrapped_stream = ResponseByteStream(
response[4], connection=connection, callback=self._response_closed
stream, connection=connection, callback=self._response_closed
)
return response[0], response[1], response[2], response[3], wrapped_stream
return status_code, headers, wrapped_stream, ext

async def _get_connection_from_pool(
self, origin: Origin
Expand Down Expand Up @@ -305,10 +308,8 @@ async def _keepalive_sweep(self) -> None:
await connection.aclose()

async def _add_to_pool(
self, connection: AsyncHTTPConnection, timeout: TimeoutDict = None
self, connection: AsyncHTTPConnection, timeout: TimeoutDict
) -> None:
timeout = {} if timeout is None else timeout

logger.trace("adding connection to pool=%r", connection)
await self._connection_semaphore.acquire(timeout=timeout.get("pool", None))
async with self._thread_lock:
Expand Down
15 changes: 10 additions & 5 deletions httpcore/_async/http11.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from ssl import SSLContext
from typing import AsyncIterator, List, Tuple, Union
from typing import AsyncIterator, List, Tuple, Union, cast

import h11

Expand Down Expand Up @@ -53,11 +53,12 @@ async def arequest(
url: URL,
headers: Headers = None,
stream: AsyncByteStream = None,
timeout: TimeoutDict = None,
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
ext: dict = None,
) -> Tuple[int, Headers, AsyncByteStream, dict]:
headers = [] if headers is None else headers
stream = PlainByteStream(b"") if stream is None else stream
timeout = {} if timeout is None else timeout
ext = {} if ext is None else ext
timeout = cast(TimeoutDict, ext.get("timeout", {}))

self.state = ConnectionState.ACTIVE

Expand All @@ -73,7 +74,11 @@ async def arequest(
aiterator=self._receive_response_data(timeout),
aclose_func=self._response_closed,
)
return (http_version, status_code, reason_phrase, headers, response_stream)
ext = {
"http_version": http_version.decode("ascii", errors="ignore"),
"reason": reason_phrase.decode("ascii", errors="ignore"),
}
return (status_code, headers, response_stream, ext)

async def start_tls(
self, hostname: bytes, timeout: TimeoutDict = None
Expand Down
32 changes: 14 additions & 18 deletions httpcore/_async/http2.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from http import HTTPStatus
from ssl import SSLContext
from typing import AsyncIterator, Dict, List, Tuple
from typing import AsyncIterator, Dict, List, Tuple, cast

import h2.connection
import h2.events
Expand All @@ -19,13 +18,6 @@
logger = get_logger(__name__)


def get_reason_phrase(status_code: int) -> bytes:
try:
return HTTPStatus(status_code).phrase.encode("ascii")
except ValueError:
return b""


class AsyncHTTP2Connection(AsyncBaseHTTPConnection):
READ_NUM_BYTES = 64 * 1024
CONFIG = H2Configuration(validate_inbound_headers=False)
Expand Down Expand Up @@ -99,9 +91,10 @@ async def arequest(
url: URL,
headers: Headers = None,
stream: AsyncByteStream = None,
timeout: TimeoutDict = None,
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
timeout = {} if timeout is None else timeout
ext: dict = None,
) -> Tuple[int, Headers, AsyncByteStream, dict]:
ext = {} if ext is None else ext
timeout = cast(TimeoutDict, ext.get("timeout", {}))

async with self.init_lock:
if not self.sent_connection_init:
Expand All @@ -123,7 +116,7 @@ async def arequest(
h2_stream = AsyncHTTP2Stream(stream_id=stream_id, connection=self)
self.streams[stream_id] = h2_stream
self.events[stream_id] = []
return await h2_stream.arequest(method, url, headers, stream, timeout)
return await h2_stream.arequest(method, url, headers, stream, ext)
except Exception: # noqa: PIE786
self.max_streams_semaphore.release()
raise
Expand Down Expand Up @@ -283,11 +276,12 @@ async def arequest(
url: URL,
headers: Headers = None,
stream: AsyncByteStream = None,
timeout: TimeoutDict = None,
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
ext: dict = None,
) -> Tuple[int, Headers, AsyncByteStream, dict]:
headers = [] if headers is None else [(k.lower(), v) for (k, v) in headers]
stream = PlainByteStream(b"") if stream is None else stream
timeout = {} if timeout is None else timeout
ext = {} if ext is None else ext
timeout = cast(TimeoutDict, ext.get("timeout", {}))

# Send the request.
seen_headers = set(key for key, value in headers)
Expand All @@ -301,12 +295,14 @@ async def arequest(

# Receive the response.
status_code, headers = await self.receive_response(timeout)
reason_phrase = get_reason_phrase(status_code)
response_stream = AsyncIteratorByteStream(
aiterator=self.body_iter(timeout), aclose_func=self._response_closed
)

return (b"HTTP/2", status_code, reason_phrase, headers, response_stream)
ext = {
"http_version": "HTTP/2",
}
return (status_code, headers, response_stream, ext)

async def send_headers(
self,
Expand Down
Loading

0 comments on commit 60f3475

Please sign in to comment.