Skip to content

Commit 60f3475

Browse files
Version 0.11.0 (#190)
* 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
1 parent 06d0e77 commit 60f3475

File tree

16 files changed

+288
-333
lines changed

16 files changed

+288
-333
lines changed

CHANGELOG.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,59 @@ All notable changes to this project will be documented in this file.
44

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

7+
## 0.11.0 (September 22nd, 2020)
8+
9+
The Transport API with 0.11.0 has a couple of significant changes.
10+
11+
Firstly we've moved changed the request interface in order to allow extensions, which will later enable us to support features
12+
such as trailing headers, HTTP/2 server push, and CONNECT/Upgrade connections.
13+
14+
The interface changes from:
15+
16+
```python
17+
def request(method, url, headers, stream, timeout):
18+
return (http_version, status_code, reason, headers, stream)
19+
```
20+
21+
To instead including an optional dictionary of extensions on the request and response:
22+
23+
```python
24+
def request(method, url, headers, stream, ext):
25+
return (status_code, headers, stream, ext)
26+
```
27+
28+
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.
29+
30+
In particular:
31+
32+
* Trailing headers support.
33+
* HTTP/2 Server Push
34+
* sendfile.
35+
* Exposing raw connection on CONNECT, Upgrade, HTTP/2 bi-di streaming.
36+
* Exposing debug information out of the API, including template name, template context.
37+
38+
Currently extensions are limited to:
39+
40+
* request: `timeout` - Optional. Timeout dictionary.
41+
* response: `http_version` - Optional. Include the HTTP version used on the response.
42+
* response: `reason` - Optional. Include the reason phrase used on the response. Only valid with HTTP/1.*.
43+
44+
See https://github.com/encode/httpx/issues/1274#issuecomment-694884553 for the history behind this.
45+
46+
Secondly, the async version of `request` is now namespaced as `arequest`.
47+
48+
This allows concrete transports to support both sync and async implementations on the same class.
49+
50+
### Added
51+
52+
- Add curio support. (Pull #168)
53+
- Add anyio support, with `backend="anyio"`. (Pull #169)
54+
55+
### Changed
56+
57+
- Update the Transport API to use 'ext' for optional extensions. (Pull #190)
58+
- Update the Transport API to use `.request` and `.arequest` so implementations can support both sync and async. (Pull #189)
59+
760
## 0.10.2 (August 20th, 2020)
861

962
### Added

httpcore/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
"WriteError",
5252
"WriteTimeout",
5353
]
54-
__version__ = "0.10.2"
54+
__version__ = "0.11.0"
5555

5656
__locals = locals()
5757

httpcore/_async/base.py

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import enum
22
from types import TracebackType
3-
from typing import AsyncIterator, List, Tuple, Type
3+
from typing import AsyncIterator, Tuple, Type
44

5-
from .._types import URL, Headers, T, TimeoutDict
5+
from .._types import URL, Headers, T
66

77

88
class NewConnectionRequired(Exception):
@@ -67,8 +67,8 @@ async def arequest(
6767
url: URL,
6868
headers: Headers = None,
6969
stream: AsyncByteStream = None,
70-
timeout: TimeoutDict = None,
71-
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
70+
ext: dict = None,
71+
) -> Tuple[int, Headers, AsyncByteStream, dict]:
7272
"""
7373
The interface for sending a single HTTP request, and returning a response.
7474
@@ -80,23 +80,17 @@ async def arequest(
8080
* **headers** - `Optional[List[Tuple[bytes, bytes]]]` - Any HTTP headers
8181
to send with the request.
8282
* **stream** - `Optional[AsyncByteStream]` - The body of the HTTP request.
83-
* **timeout** - `Optional[Dict[str, Optional[float]]]` - A dictionary of
84-
timeout values for I/O operations. Supported keys are "pool" for acquiring a
85-
connection from the connection pool, "read" for reading from the connection,
86-
"write" for writing to the connection and "connect" for opening the connection.
87-
Values are floating point seconds.
83+
* **ext** - `Optional[dict]` - A dictionary of optional extensions.
8884
8985
** Returns:**
9086
91-
A five-tuple of:
87+
A four-tuple of:
9288
93-
* **http_version** - `bytes` - The HTTP version used by the server,
94-
such as `b'HTTP/1.1'`.
9589
* **status_code** - `int` - The HTTP status code, such as `200`.
96-
* **reason_phrase** - `bytes` - Any HTTP reason phrase, such as `b'OK'`.
9790
* **headers** - `List[Tuple[bytes, bytes]]` - Any HTTP headers included
9891
on the response.
9992
* **stream** - `AsyncByteStream` - The body of the HTTP response.
93+
* **ext** - `dict` - A dictionary of optional extensions.
10094
"""
10195
raise NotImplementedError() # pragma: nocover
10296

httpcore/_async/connection.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from ssl import SSLContext
2-
from typing import List, Optional, Tuple
2+
from typing import Optional, Tuple, cast
33

44
from .._backends.auto import AsyncBackend, AsyncLock, AsyncSocketStream, AutoBackend
55
from .._types import URL, Headers, Origin, TimeoutDict
@@ -72,9 +72,12 @@ async def arequest(
7272
url: URL,
7373
headers: Headers = None,
7474
stream: AsyncByteStream = None,
75-
timeout: TimeoutDict = None,
76-
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
75+
ext: dict = None,
76+
) -> Tuple[int, Headers, AsyncByteStream, dict]:
7777
assert url_to_origin(url) == self.origin
78+
ext = {} if ext is None else ext
79+
timeout = cast(TimeoutDict, ext.get("timeout", {}))
80+
7881
async with self.request_lock:
7982
if self.state == ConnectionState.PENDING:
8083
if not self.socket:
@@ -94,7 +97,7 @@ async def arequest(
9497
logger.trace(
9598
"connection.arequest method=%r url=%r headers=%r", method, url, headers
9699
)
97-
return await self.connection.arequest(method, url, headers, stream, timeout)
100+
return await self.connection.arequest(method, url, headers, stream, ext)
98101

99102
async def _open_socket(self, timeout: TimeoutDict = None) -> AsyncSocketStream:
100103
scheme, hostname, port = self.origin

httpcore/_async/connection_pool.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import warnings
22
from ssl import SSLContext
3-
from typing import AsyncIterator, Callable, Dict, List, Optional, Set, Tuple
3+
from typing import AsyncIterator, Callable, Dict, List, Optional, Set, Tuple, cast
44

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

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

166168
await self._keepalive_sweep()
167169

@@ -190,7 +192,7 @@ async def arequest(
190192

191193
try:
192194
response = await connection.arequest(
193-
method, url, headers=headers, stream=stream, timeout=timeout
195+
method, url, headers=headers, stream=stream, ext=ext
194196
)
195197
except NewConnectionRequired:
196198
connection = None
@@ -199,10 +201,11 @@ async def arequest(
199201
await self._remove_from_pool(connection)
200202
raise
201203

204+
status_code, headers, stream, ext = response
202205
wrapped_stream = ResponseByteStream(
203-
response[4], connection=connection, callback=self._response_closed
206+
stream, connection=connection, callback=self._response_closed
204207
)
205-
return response[0], response[1], response[2], response[3], wrapped_stream
208+
return status_code, headers, wrapped_stream, ext
206209

207210
async def _get_connection_from_pool(
208211
self, origin: Origin
@@ -305,10 +308,8 @@ async def _keepalive_sweep(self) -> None:
305308
await connection.aclose()
306309

307310
async def _add_to_pool(
308-
self, connection: AsyncHTTPConnection, timeout: TimeoutDict = None
311+
self, connection: AsyncHTTPConnection, timeout: TimeoutDict
309312
) -> None:
310-
timeout = {} if timeout is None else timeout
311-
312313
logger.trace("adding connection to pool=%r", connection)
313314
await self._connection_semaphore.acquire(timeout=timeout.get("pool", None))
314315
async with self._thread_lock:

httpcore/_async/http11.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from ssl import SSLContext
2-
from typing import AsyncIterator, List, Tuple, Union
2+
from typing import AsyncIterator, List, Tuple, Union, cast
33

44
import h11
55

@@ -53,11 +53,12 @@ async def arequest(
5353
url: URL,
5454
headers: Headers = None,
5555
stream: AsyncByteStream = None,
56-
timeout: TimeoutDict = None,
57-
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
56+
ext: dict = None,
57+
) -> Tuple[int, Headers, AsyncByteStream, dict]:
5858
headers = [] if headers is None else headers
5959
stream = PlainByteStream(b"") if stream is None else stream
60-
timeout = {} if timeout is None else timeout
60+
ext = {} if ext is None else ext
61+
timeout = cast(TimeoutDict, ext.get("timeout", {}))
6162

6263
self.state = ConnectionState.ACTIVE
6364

@@ -73,7 +74,11 @@ async def arequest(
7374
aiterator=self._receive_response_data(timeout),
7475
aclose_func=self._response_closed,
7576
)
76-
return (http_version, status_code, reason_phrase, headers, response_stream)
77+
ext = {
78+
"http_version": http_version.decode("ascii", errors="ignore"),
79+
"reason": reason_phrase.decode("ascii", errors="ignore"),
80+
}
81+
return (status_code, headers, response_stream, ext)
7782

7883
async def start_tls(
7984
self, hostname: bytes, timeout: TimeoutDict = None

httpcore/_async/http2.py

Lines changed: 14 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
from http import HTTPStatus
21
from ssl import SSLContext
3-
from typing import AsyncIterator, Dict, List, Tuple
2+
from typing import AsyncIterator, Dict, List, Tuple, cast
43

54
import h2.connection
65
import h2.events
@@ -19,13 +18,6 @@
1918
logger = get_logger(__name__)
2019

2120

22-
def get_reason_phrase(status_code: int) -> bytes:
23-
try:
24-
return HTTPStatus(status_code).phrase.encode("ascii")
25-
except ValueError:
26-
return b""
27-
28-
2921
class AsyncHTTP2Connection(AsyncBaseHTTPConnection):
3022
READ_NUM_BYTES = 64 * 1024
3123
CONFIG = H2Configuration(validate_inbound_headers=False)
@@ -99,9 +91,10 @@ async def arequest(
9991
url: URL,
10092
headers: Headers = None,
10193
stream: AsyncByteStream = None,
102-
timeout: TimeoutDict = None,
103-
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
104-
timeout = {} if timeout is None else timeout
94+
ext: dict = None,
95+
) -> Tuple[int, Headers, AsyncByteStream, dict]:
96+
ext = {} if ext is None else ext
97+
timeout = cast(TimeoutDict, ext.get("timeout", {}))
10598

10699
async with self.init_lock:
107100
if not self.sent_connection_init:
@@ -123,7 +116,7 @@ async def arequest(
123116
h2_stream = AsyncHTTP2Stream(stream_id=stream_id, connection=self)
124117
self.streams[stream_id] = h2_stream
125118
self.events[stream_id] = []
126-
return await h2_stream.arequest(method, url, headers, stream, timeout)
119+
return await h2_stream.arequest(method, url, headers, stream, ext)
127120
except Exception: # noqa: PIE786
128121
self.max_streams_semaphore.release()
129122
raise
@@ -283,11 +276,12 @@ async def arequest(
283276
url: URL,
284277
headers: Headers = None,
285278
stream: AsyncByteStream = None,
286-
timeout: TimeoutDict = None,
287-
) -> Tuple[bytes, int, bytes, List[Tuple[bytes, bytes]], AsyncByteStream]:
279+
ext: dict = None,
280+
) -> Tuple[int, Headers, AsyncByteStream, dict]:
288281
headers = [] if headers is None else [(k.lower(), v) for (k, v) in headers]
289282
stream = PlainByteStream(b"") if stream is None else stream
290-
timeout = {} if timeout is None else timeout
283+
ext = {} if ext is None else ext
284+
timeout = cast(TimeoutDict, ext.get("timeout", {}))
291285

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

302296
# Receive the response.
303297
status_code, headers = await self.receive_response(timeout)
304-
reason_phrase = get_reason_phrase(status_code)
305298
response_stream = AsyncIteratorByteStream(
306299
aiterator=self.body_iter(timeout), aclose_func=self._response_closed
307300
)
308301

309-
return (b"HTTP/2", status_code, reason_phrase, headers, response_stream)
302+
ext = {
303+
"http_version": "HTTP/2",
304+
}
305+
return (status_code, headers, response_stream, ext)
310306

311307
async def send_headers(
312308
self,

0 commit comments

Comments
 (0)